mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
Compare commits
53
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 |
@@ -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
+46
-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,6 +198,7 @@ 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
|
||||||
@@ -220,6 +232,7 @@ 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/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
|
||||||
@@ -243,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
|
||||||
@@ -281,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
|
||||||
@@ -315,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
|
||||||
@@ -324,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}")
|
||||||
@@ -341,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
|
||||||
@@ -538,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");
|
||||||
|
|||||||
@@ -181,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
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -209,7 +210,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
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) {
|
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||||
reasons.push_back("no three-channel multisample storage format on OpenGL ES");
|
reasons.push_back("no colour-renderable three-channel format on OpenGL ES");
|
||||||
}
|
}
|
||||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||||
reasons.push_back("EXT_render_snorm not supported");
|
reasons.push_back("EXT_render_snorm not supported");
|
||||||
@@ -552,26 +553,60 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
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);
|
||||||
// A multisample texture can only ever be rendered into, so its storage format
|
// Colour-attachable targets need a colour-renderable fallback; the ordinary
|
||||||
// has to stay colour-renderable; the ordinary fallback for a three-channel
|
// fallback for a three-channel format is another three-channel one, which ES
|
||||||
// format is a three-channel one, which ES accepts as a texture but rejects as
|
// accepts as a texture but never as an attachment. Recompute the fallback per
|
||||||
// multisample storage. Recompute the fallback per target so those formats get
|
// target so those formats get widened where the target demands it.
|
||||||
// widened here and nowhere else.
|
const Flags<PixelFormatNormalizeOptionBit> renderTargetOptions =
|
||||||
Flags<PixelFormatNormalizeOptionBit> targetOptions;
|
TextureImpl::GetRenderTargetNormalizeOptions(capabilities, targetIndex);
|
||||||
if (IsGLESProbeMultisampleTarget(target)) {
|
// Multisample storage has no three-channel form on ES at all, so its widening
|
||||||
targetOptions |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
// is unconditional and skips the native probe (which cannot succeed). Every
|
||||||
if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) {
|
// other target keeps the widening on the DRIVER branch, behind the native
|
||||||
targetOptions |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
// 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;
|
GLESProbeFormatInfo fallbackInfo = outerFallbackInfo;
|
||||||
Bool hasForcedFallback = outerHasForcedFallback;
|
Bool hasForcedFallback = outerHasForcedFallback;
|
||||||
if (targetOptions) {
|
if (renderTargetOptions) {
|
||||||
hasForcedFallback = BuildFallbackProbeFormatInfo(
|
// Folded into the forced options only when a forced fallback already
|
||||||
requestedInternalFormat, forcedOptions | targetOptions, true, fallbackInfo);
|
// applies, so the render-target bits never *create* one: ANGLE's forced
|
||||||
if (!hasForcedFallback) {
|
// GL_RGB8_SNORM -> GL_RGB16F is still three-channel and still needs
|
||||||
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions | targetOptions, false,
|
// 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);
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,8 +652,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex();
|
const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex();
|
||||||
Bool shouldProbeFallbackRenderbuffer = outerHasForcedFallback;
|
// A renderbuffer exists only to be attached, so it needs the same three-channel
|
||||||
if (!outerHasForcedFallback) {
|
// 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) {
|
||||||
@@ -632,16 +685,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
shouldProbeFallbackRenderbuffer = true;
|
shouldProbeFallbackRenderbuffer = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (shouldProbeFallbackRenderbuffer && outerFallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
|
if (shouldProbeFallbackRenderbuffer && renderbufferFallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
|
||||||
ProbeRenderbuffer(gl, outerFallbackInfo.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, outerFallbackInfo);
|
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, renderbufferFallbackInfo);
|
||||||
}
|
}
|
||||||
const Int maxSamples =
|
const Int maxSamples =
|
||||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, outerFallbackInfo.ImageFormat);
|
GetGLESFormatMaxSamples(capabilities, logicalFormat, renderbufferFallbackInfo.ImageFormat);
|
||||||
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
|
cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts(
|
||||||
ProbeRenderbufferSampleCounts(gl, outerFallbackInfo.InternalFormat, logicalFormat, maxSamples);
|
gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, maxSamples);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -891,6 +944,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// extension explicitly permits. It is also the only thing that
|
// extension explicitly permits. It is also the only thing that
|
||||||
// exposes glProgramParameteri before GL 4.1.
|
// exposes glProgramParameteri before GL 4.1.
|
||||||
E_GL_ARB_get_program_binary};
|
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.
|
||||||
@@ -951,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;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -92,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();
|
||||||
@@ -182,6 +174,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
namespace XfbImpl {
|
namespace XfbImpl {
|
||||||
Bool AreTransformFeedbacksSupported();
|
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 BeginTransformFeedback(GLenum primitiveMode);
|
||||||
void EndTransformFeedback();
|
void EndTransformFeedback();
|
||||||
void PauseTransformFeedback();
|
void PauseTransformFeedback();
|
||||||
@@ -191,6 +188,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void OnBackendContextDestroyed();
|
void OnBackendContextDestroyed();
|
||||||
} // namespace XfbImpl
|
} // 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,11 +375,70 @@ 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;
|
||||||
};
|
};
|
||||||
@@ -336,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;
|
||||||
@@ -373,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();
|
||||||
|
|
||||||
@@ -388,6 +618,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// clean probe compares this before rebuilding shape info and scanning
|
// clean probe compares this before rebuilding shape info and scanning
|
||||||
// per-level dirty flags; 0 never matches a real version (they start at 1).
|
// per-level dirty flags; 0 never matches a real version (they start at 1).
|
||||||
Uint64 m_syncedContentVersion = 0;
|
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};
|
||||||
@@ -404,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;
|
||||||
@@ -484,11 +733,81 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// has to apply the clamp itself.
|
// has to apply the clamp itself.
|
||||||
Bool IsFixedPointFallbackReadAttachment();
|
Bool IsFixedPointFallbackReadAttachment();
|
||||||
|
|
||||||
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboBindVersions;
|
// 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;
|
||||||
|
|
||||||
@@ -584,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.
|
||||||
@@ -630,6 +953,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Float lastAssignedLodBias = 0.0f;
|
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();
|
||||||
~BackendProgramObjectImpl();
|
~BackendProgramObjectImpl();
|
||||||
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||||
@@ -637,6 +997,9 @@ 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
|
// False when the last SyncToBackend could not produce a usable program (a
|
||||||
@@ -658,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; }
|
||||||
@@ -686,6 +1050,7 @@ 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;
|
||||||
@@ -700,6 +1065,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
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
|
||||||
@@ -61,29 +61,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
requestedInternalFormat, GetDriverPixelFormatNormalizeOptions() | extraOptions);
|
requestedInternalFormat, GetDriverPixelFormatNormalizeOptions() | extraOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multisample textures can only ever be rendered into, never uploaded to, so a fallback
|
|
||||||
// format for them has to stay colour-renderable - a three-channel float fallback is a legal
|
|
||||||
// ES texture format but not a legal multisample storage format. Widening to four channels
|
|
||||||
// is safe here precisely because there is no transfer path that would have to expand
|
|
||||||
// three-channel client data, and the alpha the draw writes for a three-channel source is
|
|
||||||
// already the 1.0 the frontend format implies.
|
|
||||||
Bool TargetRequiresRenderableFormat(SizeT targetIndex) {
|
|
||||||
return targetIndex == static_cast<SizeT>(TextureTarget::Texture2DMultisample) ||
|
|
||||||
targetIndex == static_cast<SizeT>(TextureTarget::Texture2DMultisampleArray);
|
|
||||||
}
|
|
||||||
|
|
||||||
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(SizeT targetIndex) {
|
|
||||||
Flags<PixelFormatNormalizeOptionBit> options;
|
|
||||||
if (!TargetRequiresRenderableFormat(targetIndex)) {
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
options |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
|
||||||
if (!g_GLESCapabilities.SupportsRenderSnorm || !g_GLESCapabilities.SupportsNorm16Texture) {
|
|
||||||
options |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool HasCachedFormatCapability(TextureInternalFormat internalFormat,
|
Bool HasCachedFormatCapability(TextureInternalFormat internalFormat,
|
||||||
SizeT targetIndex,
|
SizeT targetIndex,
|
||||||
Bool caveat,
|
Bool caveat,
|
||||||
@@ -141,14 +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(
|
||||||
GetRenderTargetNormalizeOptions(targetIndex));
|
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
|
||||||
@@ -178,20 +202,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
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) {
|
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target) {
|
||||||
const SizeT targetIndex =
|
const SizeT targetIndex =
|
||||||
target == TextureTarget::Unknown ? kFormatCapabilityTargetCount : GetFormatCapabilityTargetIndex(target);
|
target == TextureTarget::Unknown ? kFormatCapabilityTargetCount : GetFormatCapabilityTargetIndex(target);
|
||||||
if (!TargetRequiresRenderableFormat(targetIndex)) {
|
return BackendFormatAddsAlpha(internalFormat, targetIndex);
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
if (pActiveBackendObject && !ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat) {
|
||||||
return false;
|
return BackendFormatAddsAlpha(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
||||||
}
|
|
||||||
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
|
||||||
const Flags<PixelFormatNormalizeOptionBit> options =
|
|
||||||
GetRuntimeFallbackNormalizeOptions(requestedInternalFormat,
|
|
||||||
GetRenderTargetNormalizeOptions(targetIndex));
|
|
||||||
return static_cast<Bool>(options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget);
|
|
||||||
}
|
}
|
||||||
} // namespace TextureImpl
|
} // namespace TextureImpl
|
||||||
namespace PrgramImpl {
|
namespace PrgramImpl {
|
||||||
@@ -1107,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,6 +36,16 @@ 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);
|
||||||
@@ -41,10 +53,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
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 texture is actually created with has an alpha channel the
|
// True when the format the image is actually created with has an alpha channel the
|
||||||
// frontend format does not (the three-channel multisample widening). GL reads such a
|
// frontend format does not (the three-channel colour-renderable widening). GL reads such
|
||||||
// channel back as 1.0, so any swizzle source of ALPHA has to be answered with ONE.
|
// 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 BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
|
||||||
|
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat);
|
||||||
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
|
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
|
||||||
} // namespace TextureImpl
|
} // namespace TextureImpl
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -523,6 +524,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
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.
|
||||||
@@ -601,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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -873,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;
|
||||||
@@ -1233,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");
|
||||||
@@ -1392,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) {
|
||||||
@@ -1444,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,
|
||||||
|
|||||||
@@ -97,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,
|
||||||
|
|||||||
@@ -2379,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;
|
||||||
@@ -2580,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);
|
||||||
|
|||||||
@@ -105,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;
|
||||||
|
|
||||||
@@ -262,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
|
||||||
@@ -312,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;
|
||||||
@@ -207,11 +212,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
for (auto& entry : m_descriptorReuseMemo) {
|
for (auto& entry : m_descriptorReuseMemo) {
|
||||||
entry.valid = false;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,6 +259,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
for (auto& entry : m_descriptorReuseMemo) {
|
for (auto& entry : m_descriptorReuseMemo) {
|
||||||
entry.valid = false;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,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
|
||||||
@@ -365,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) {
|
||||||
@@ -420,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,
|
||||||
@@ -430,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(
|
||||||
@@ -808,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()));
|
||||||
@@ -820,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);
|
||||||
@@ -841,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,
|
||||||
@@ -1155,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");
|
||||||
@@ -1170,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.
|
||||||
@@ -1201,6 +1426,14 @@ 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);
|
||||||
|
|
||||||
|
// Eligibility probe for FastRebindMemo, filled by this walk: exactly one
|
||||||
|
// dynamic-UBO descriptor (no arrayed elements) and otherwise only
|
||||||
|
// combined-image samplers, so the whole set's content is pinned by the
|
||||||
|
// sampler hint plus one (buffer, range) compare.
|
||||||
|
Uint32 dynamicUboDescriptorCount = 0;
|
||||||
|
Uint32 fastRebindUboBinding = 0;
|
||||||
|
Bool fastRebindKindsEligible = true;
|
||||||
|
|
||||||
// Iterate only the bindings this program declares. The old walk covered all 256 slots of
|
// 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.
|
// bindingKinds on every draw to find the 1-8 a real program uses.
|
||||||
for (const Uint32 binding : programObj.activeBindings) {
|
for (const Uint32 binding : programObj.activeBindings) {
|
||||||
@@ -1221,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,
|
||||||
@@ -1304,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);
|
||||||
@@ -1317,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);
|
||||||
@@ -1329,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);
|
||||||
@@ -1341,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(
|
||||||
@@ -1372,7 +1560,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// 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) {
|
||||||
@@ -1435,34 +1622,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip the driver call when this exact binding is already live on the
|
// (Re)record the dynamic-offset-only rebind memo. Recording on every
|
||||||
// command buffer (see the bind-dedup shadow in the header).
|
// cacheable walk (allocated or reused set alike - both hold exactly the
|
||||||
const Uint32 offsetCount = static_cast<Uint32>(dynamicOffsets.size());
|
// content just computed) keeps the single slot tracking the most recent
|
||||||
Bool identicalBind = m_lastBindValid && m_lastBindSet == descriptorSet &&
|
// program; a non-cacheable override walk drops it alongside the reuse
|
||||||
m_lastBindLayout == programObj.pipelineLayout && m_lastBindPoint == bindPoint &&
|
// memo above.
|
||||||
m_lastBindOffsetCount == offsetCount && offsetCount <= kMaxShadowedDynamicOffsets;
|
if (cacheable && fastRebindKindsEligible && dynamicUboDescriptorCount == 1) {
|
||||||
if (identicalBind) {
|
m_fastRebindMemo = FastRebindMemo{
|
||||||
for (Uint32 i = 0; i < offsetCount; ++i) {
|
/*valid=*/true, frameIndex, program.GetLifetimeId(), programObj.hash,
|
||||||
if (m_lastBindOffsets[i] != dynamicOffsets[i]) {
|
fastRebindUboBinding, bufferInfos[0].buffer,
|
||||||
identicalBind = false;
|
bufferInfos[0].range, descriptorSet};
|
||||||
break;
|
} else {
|
||||||
}
|
m_fastRebindMemo.valid = false;
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!identicalBind) {
|
|
||||||
vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1,
|
|
||||||
&descriptorSet, offsetCount, dynamicOffsets.data());
|
|
||||||
if (offsetCount <= kMaxShadowedDynamicOffsets) {
|
|
||||||
m_lastBindValid = true;
|
|
||||||
m_lastBindSet = descriptorSet;
|
|
||||||
m_lastBindLayout = programObj.pipelineLayout;
|
|
||||||
m_lastBindPoint = bindPoint;
|
|
||||||
m_lastBindOffsetCount = offsetCount;
|
|
||||||
std::copy_n(dynamicOffsets.data(), offsetCount, m_lastBindOffsets);
|
|
||||||
} else {
|
|
||||||
m_lastBindValid = 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(
|
||||||
@@ -196,6 +248,37 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
DescriptorReuseEntry m_descriptorReuseMemo[kDescriptorReuseMemoSize];
|
DescriptorReuseEntry m_descriptorReuseMemo[kDescriptorReuseMemoSize];
|
||||||
Uint32 m_descriptorReuseMemoNext = 0;
|
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
|
||||||
// driver call can be skipped outright. Command-buffer-scope state; reset
|
// driver call can be skipped outright. Command-buffer-scope state; reset
|
||||||
@@ -253,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
|
||||||
|
|||||||
@@ -33,14 +33,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
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)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
||||||
@@ -71,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.
|
||||||
@@ -79,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
|
||||||
|
|||||||
@@ -258,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;
|
||||||
@@ -272,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({
|
||||||
@@ -358,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()) {
|
||||||
@@ -390,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;
|
||||||
@@ -422,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;
|
||||||
@@ -481,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();
|
||||||
@@ -608,8 +627,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
} else if (resource->transientChangeSerial == changeSerial && resource->transientSize == size &&
|
} else if (resource->transientChangeSerial == changeSerial && resource->transientSize == size &&
|
||||||
resource->transientFrameSerial != 0) {
|
resource->transientFrameSerial != 0) {
|
||||||
if (++resource->unchangedStreak >= kStreamedPromotionStreak) {
|
if (++resource->unchangedStreak >= kStreamedPromotionStreak) {
|
||||||
|
// Promotion moves the buffer off the arena and onto resident storage.
|
||||||
resource->promotedResident = true;
|
resource->promotedResident = true;
|
||||||
resource->promotedChangeSerial = changeSerial;
|
resource->promotedChangeSerial = changeSerial;
|
||||||
|
BumpSliceEpoch(*resource);
|
||||||
if (AcquireResidentSlice(kind, bufferObject, outSlice)) {
|
if (AcquireResidentSlice(kind, bufferObject, outSlice)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -619,6 +640,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
resource->unchangedStreak = 0;
|
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,6 +57,17 @@ 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;
|
||||||
@@ -132,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.
|
||||||
@@ -158,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;
|
||||||
@@ -170,5 +188,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
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; }
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -620,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
|
||||||
@@ -1859,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,
|
||||||
@@ -1869,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() &&
|
||||||
@@ -1919,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();
|
||||||
@@ -2070,6 +2305,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
IntVec3 regionLo = {0, 0, 0};
|
IntVec3 regionLo = {0, 0, 0};
|
||||||
IntVec3 regionSize = {0, 0, 0};
|
IntVec3 regionSize = {0, 0, 0};
|
||||||
SizeT texelBytes = 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;
|
||||||
@@ -2133,6 +2374,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
static_cast<SizeT>(uploadItem.regionSize.y()) *
|
static_cast<SizeT>(uploadItem.regionSize.y()) *
|
||||||
static_cast<SizeT>(uploadItem.regionSize.z()) *
|
static_cast<SizeT>(uploadItem.regionSize.z()) *
|
||||||
uploadItem.texelBytes;
|
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) {
|
||||||
@@ -2271,61 +2529,66 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rare mid-frame hazard, kept at parity with the old per-upload
|
||||||
|
// submits: this image already has an upload recorded in the OPEN batch
|
||||||
|
// and has since been referenced by the frame's open recording (drawn).
|
||||||
|
// Appending here would merge both uploads into the same pre-frame
|
||||||
|
// submission the old code split into two; flush first so the second
|
||||||
|
// upload lands in its own later submission, exactly like before.
|
||||||
|
if (m_uploadBatchOpen && WasTouchedThisRecording(outResource) &&
|
||||||
|
std::find(m_uploadBatchImages.begin(), m_uploadBatchImages.end(), outResource.image) !=
|
||||||
|
m_uploadBatchImages.end()) {
|
||||||
|
FlushPendingUploads();
|
||||||
|
}
|
||||||
|
// Bound the staging bytes a single batch can pin before its fence can
|
||||||
|
// reclaim them.
|
||||||
|
constexpr VkDeviceSize kMaxBatchStagingBytes = 64u * 1024u * 1024u;
|
||||||
|
if (m_uploadBatchOpen && m_uploadBatchStagingBytes + stagingSize > kMaxBatchStagingBytes) {
|
||||||
|
FlushPendingUploads();
|
||||||
|
}
|
||||||
|
|
||||||
|
VkCommandBuffer commandBuffer = EnsureUploadBatchOpen();
|
||||||
VkBuffer stagingBuffer = VK_NULL_HANDLE;
|
VkBuffer stagingBuffer = VK_NULL_HANDLE;
|
||||||
VmaAllocation stagingAllocation = nullptr;
|
VkDeviceSize stagingBase = 0;
|
||||||
|
Uint8* mapped = AcquireUploadStagingSpace(stagingSize, stagingBuffer, stagingBase);
|
||||||
VkBufferCreateInfo bufferInfo{};
|
|
||||||
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
|
||||||
bufferInfo.size = stagingSize;
|
|
||||||
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;
|
|
||||||
stagingAllocationInfo.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
|
|
||||||
VK_VERIFY(vmaCreateBuffer(m_allocator, &bufferInfo, &stagingAllocationInfo, &stagingBuffer, &stagingAllocation, nullptr),
|
|
||||||
"vmaCreateBuffer(staging texture)");
|
|
||||||
|
|
||||||
void* mapped = nullptr;
|
|
||||||
VK_VERIFY(vmaMapMemory(m_allocator, stagingAllocation, &mapped), "vmaMapMemory(staging texture)");
|
|
||||||
for (const auto& item : uploadItems) {
|
for (const auto& item : uploadItems) {
|
||||||
Uint8* dst = static_cast<Uint8*>(mapped) + item.offset;
|
Uint8* dst = mapped + item.offset;
|
||||||
if (!item.subRegion) {
|
if (!item.subRegion) {
|
||||||
std::memcpy(dst, item.source, item.uploadByteSize);
|
std::memcpy(dst, item.source, item.uploadByteSize);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Tight-pack the dirty box: the shadow keeps whole-level rows, the
|
// Tight-pack the dirty box(es): the shadow keeps whole-level rows, the
|
||||||
// staging slice holds only the region (bufferRowLength stays 0).
|
// staging slice holds only the region (bufferRowLength stays 0). Multi-
|
||||||
|
// 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 levelRowBytes = static_cast<SizeT>(item.texelSize.x()) * item.texelBytes;
|
||||||
const SizeT levelSliceBytes = static_cast<SizeT>(item.texelSize.y()) * levelRowBytes;
|
const SizeT levelSliceBytes = static_cast<SizeT>(item.texelSize.y()) * levelRowBytes;
|
||||||
const SizeT regionRowBytes = static_cast<SizeT>(item.regionSize.x()) * item.texelBytes;
|
|
||||||
const Uint8* src = static_cast<const Uint8*>(item.source);
|
const Uint8* src = static_cast<const Uint8*>(item.source);
|
||||||
for (Int z = 0; z < item.regionSize.z(); ++z) {
|
const auto packBox = [&](Uint8* out, const IntVec3& lo, const IntVec3& boxSize) {
|
||||||
for (Int y = 0; y < item.regionSize.y(); ++y) {
|
const SizeT boxRowBytes = static_cast<SizeT>(boxSize.x()) * item.texelBytes;
|
||||||
const Uint8* srcRow = src +
|
for (Int z = 0; z < boxSize.z(); ++z) {
|
||||||
static_cast<SizeT>(item.regionLo.z() + z) * levelSliceBytes +
|
for (Int y = 0; y < boxSize.y(); ++y) {
|
||||||
static_cast<SizeT>(item.regionLo.y() + y) * levelRowBytes +
|
const Uint8* srcRow = src + static_cast<SizeT>(lo.z() + z) * levelSliceBytes +
|
||||||
static_cast<SizeT>(item.regionLo.x()) * item.texelBytes;
|
static_cast<SizeT>(lo.y() + y) * levelRowBytes +
|
||||||
std::memcpy(dst + (static_cast<SizeT>(z) * item.regionSize.y() + y) * regionRowBytes,
|
static_cast<SizeT>(lo.x()) * item.texelBytes;
|
||||||
srcRow, regionRowBytes);
|
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);
|
||||||
}
|
}
|
||||||
vmaUnmapMemory(m_allocator, stagingAllocation);
|
|
||||||
|
|
||||||
VkCommandBufferAllocateInfo allocInfo{};
|
|
||||||
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
|
||||||
allocInfo.commandPool = m_commandPool;
|
|
||||||
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
|
||||||
allocInfo.commandBufferCount = 1;
|
|
||||||
|
|
||||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
|
||||||
VK_VERIFY(vkAllocateCommandBuffers(m_device, &allocInfo, &commandBuffer), "vkAllocateCommandBuffers(texture)");
|
|
||||||
|
|
||||||
VkCommandBufferBeginInfo beginInfo{};
|
|
||||||
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
|
||||||
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
|
||||||
VK_VERIFY(vkBeginCommandBuffer(commandBuffer, &beginInfo), "vkBeginCommandBuffer(texture)");
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -2348,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;
|
||||||
@@ -2383,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);
|
||||||
@@ -2406,36 +2709,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
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__);
|
||||||
@@ -2445,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,11 @@ public:
|
|||||||
VkPipelineStageFlags sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
|
VkPipelineStageFlags sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
|
||||||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
||||||
VK_PIPELINE_STAGE_COMPUTE_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 {
|
||||||
@@ -308,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.
|
||||||
@@ -453,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;
|
||||||
@@ -506,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
@@ -327,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};
|
||||||
@@ -477,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;
|
||||||
@@ -508,6 +536,10 @@ 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;
|
||||||
@@ -531,7 +563,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Per-pipeline provoking-vertex mode. capturesXfbFromGeometryStage must be a LINK-TIME
|
// Per-pipeline provoking-vertex mode. capturesXfbFromGeometryStage must be a LINK-TIME
|
||||||
// property of the program, never the dynamic "is transform feedback active" flag: the
|
// 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
|
// 8-entry m_pipelineMemo and the SetupDrawSnapshot fast path key on programObj.hash and
|
||||||
// GetRenderStateParametersVersion(), neither of which moves when glBeginTransformFeedback is
|
// the pipeline-state value hash, neither of which moves when glBeginTransformFeedback is
|
||||||
// called, so a dynamic input here would hand back a stale VkPipeline.
|
// called, so a dynamic input here would hand back a stale VkPipeline.
|
||||||
VkProvokingVertexModeEXT SelectProvokingVertexMode(VkPrimitiveTopology topology,
|
VkProvokingVertexModeEXT SelectProvokingVertexMode(VkPrimitiveTopology topology,
|
||||||
Bool capturesXfbFromGeometryStage) const;
|
Bool capturesXfbFromGeometryStage) const;
|
||||||
@@ -623,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;
|
||||||
};
|
};
|
||||||
@@ -631,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;
|
||||||
@@ -692,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;
|
||||||
@@ -707,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
|
||||||
@@ -779,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();
|
||||||
@@ -809,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);
|
||||||
|
|||||||
@@ -12,8 +12,10 @@
|
|||||||
* libEGL.so.1 for the native driver, or a libMobileGL.so path for either
|
* 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
|
* 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
|
* context on a small pbuffer, renders into its own FBO and paces frames with
|
||||||
* glFinish. No window system is required beyond what the provider itself
|
* glFinish. No window system is required: the default display is tried first
|
||||||
* needs - see run_driver_bench.sh.
|
* 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:
|
* Every case models one hot pattern from captured Minecraft traces:
|
||||||
* draw_tiny back-to-back glDrawElements, shared state (chunk batch)
|
* draw_tiny back-to-back glDrawElements, shared state (chunk batch)
|
||||||
@@ -67,6 +69,7 @@ typedef unsigned int EGLenum;
|
|||||||
#define EGL_CONTEXT_MINOR_VERSION 0x30FB
|
#define EGL_CONTEXT_MINOR_VERSION 0x30FB
|
||||||
#define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD
|
#define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD
|
||||||
#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001
|
#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001
|
||||||
|
#define EGL_PLATFORM_SURFACELESS_MESA 0x31DD
|
||||||
|
|
||||||
/* ---- GL constants ---- */
|
/* ---- GL constants ---- */
|
||||||
#define GL_COLOR_BUFFER_BIT 0x00004000
|
#define GL_COLOR_BUFFER_BIT 0x00004000
|
||||||
@@ -89,6 +92,11 @@ typedef unsigned int EGLenum;
|
|||||||
#define GL_NEAREST 0x2600
|
#define GL_NEAREST 0x2600
|
||||||
#define GL_NEAREST_MIPMAP_LINEAR 0x2702
|
#define GL_NEAREST_MIPMAP_LINEAR 0x2702
|
||||||
#define GL_DEPTH_TEST 0x0B71
|
#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_VERTEX_SHADER 0x8B31
|
||||||
#define GL_FRAGMENT_SHADER 0x8B30
|
#define GL_FRAGMENT_SHADER 0x8B30
|
||||||
#define GL_COMPILE_STATUS 0x8B81
|
#define GL_COMPILE_STATUS 0x8B81
|
||||||
@@ -133,6 +141,9 @@ static void* g_provider;
|
|||||||
GLF(void, glClear, (unsigned))
|
GLF(void, glClear, (unsigned))
|
||||||
GLF(void, glClearColor, (float, float, float, float))
|
GLF(void, glClearColor, (float, float, float, float))
|
||||||
GLF(void, glEnable, (GLenum))
|
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(void, glViewport, (GLint, GLint, GLsizei, GLsizei))
|
||||||
GLF(const unsigned char*, glGetString, (GLenum))
|
GLF(const unsigned char*, glGetString, (GLenum))
|
||||||
GLF(GLenum, glGetError, (void))
|
GLF(GLenum, glGetError, (void))
|
||||||
@@ -280,7 +291,21 @@ static void run_case(const char* name, case_fn body, long a, long b, long opsPer
|
|||||||
if (glGetError() != GL_NO_ERROR) fprintf(stderr, "WARN: GL error after %s\n", name);
|
if (glGetError() != GL_NO_ERROR) fprintf(stderr, "WARN: GL error after %s\n", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* a = draws per frame */
|
/* 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 ---- */
|
/* ---- EGL bootstrap: one provider library, pbuffer, desktop-GL context ---- */
|
||||||
static int boot_egl(void) {
|
static int boot_egl(void) {
|
||||||
const char* libpath = getenv("DRIVERBENCH_EGL_LIB");
|
const char* libpath = getenv("DRIVERBENCH_EGL_LIB");
|
||||||
@@ -304,14 +329,30 @@ static int boot_egl(void) {
|
|||||||
ESYM(eglGetError)
|
ESYM(eglGetError)
|
||||||
g_eglGetProcAddress = (void* (*)(const char*))p_eglGetProcAddress;
|
g_eglGetProcAddress = (void* (*)(const char*))p_eglGetProcAddress;
|
||||||
|
|
||||||
EGLDisplay dpy = ((EGLDisplay(*)(void*))p_eglGetDisplay)(EGL_DEFAULT_DISPLAY);
|
EGLint (*getError)(void) = (EGLint(*)(void))p_eglGetError;
|
||||||
if (!dpy) { fprintf(stderr, "FAIL: eglGetDisplay\n"); return 1; }
|
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;
|
EGLint maj = 0, min = 0;
|
||||||
if (!((EGLBoolean(*)(EGLDisplay, EGLint*, EGLint*))p_eglInitialize)(dpy, &maj, &min)) {
|
const char* how = "default display";
|
||||||
fprintf(stderr, "FAIL: eglInitialize (0x%x)\n", ((EGLint(*)(void))p_eglGetError)());
|
EGLDisplay dpy = ((EGLDisplay(*)(void*))p_eglGetDisplay)(EGL_DEFAULT_DISPLAY);
|
||||||
return 1;
|
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\n", maj, min, libpath);
|
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
|
// 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
|
// written against), GLES 3 second so the same binary can measure a device's
|
||||||
@@ -348,7 +389,10 @@ static int boot_egl(void) {
|
|||||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_NONE};
|
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_NONE};
|
||||||
ncfg = 0;
|
ncfg = 0;
|
||||||
if (!chooseConfig(dpy, esCfgAttribs, &cfg, 1, &ncfg) || ncfg < 1) {
|
if (!chooseConfig(dpy, esCfgAttribs, &cfg, 1, &ncfg) || ncfg < 1) {
|
||||||
const EGLint relaxed[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RED_SIZE, 8, EGL_NONE};
|
// 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) {
|
if (!chooseConfig(dpy, relaxed, &cfg, 1, &ncfg) || ncfg < 1) {
|
||||||
fprintf(stderr, "FAIL: eglChooseConfig\n");
|
fprintf(stderr, "FAIL: eglChooseConfig\n");
|
||||||
return 1;
|
return 1;
|
||||||
@@ -358,20 +402,22 @@ static int boot_egl(void) {
|
|||||||
ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, esCtxAttribs);
|
ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, esCtxAttribs);
|
||||||
}
|
}
|
||||||
if (ctx == EGL_NO_CONTEXT) {
|
if (ctx == EGL_NO_CONTEXT) {
|
||||||
fprintf(stderr, "FAIL: eglCreateContext (0x%x)\n", ((EGLint(*)(void))p_eglGetError)());
|
fprintf(stderr, "FAIL: eglCreateContext (0x%x)\n", getError());
|
||||||
return 1;
|
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};
|
const EGLint pbAttribs[] = {EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE};
|
||||||
EGLSurface surf = ((EGLSurface(*)(EGLDisplay, EGLConfig, const EGLint*))p_eglCreatePbufferSurface)(
|
EGLSurface surf = ((EGLSurface(*)(EGLDisplay, EGLConfig, const EGLint*))p_eglCreatePbufferSurface)(
|
||||||
dpy, cfg, pbAttribs);
|
dpy, cfg, pbAttribs);
|
||||||
if (surf == EGL_NO_SURFACE) {
|
if (surf == EGL_NO_SURFACE)
|
||||||
fprintf(stderr, "FAIL: eglCreatePbufferSurface (0x%x)\n", ((EGLint(*)(void))p_eglGetError)());
|
fprintf(stderr, "no pbuffer (0x%x), using a surfaceless context\n", getError());
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (!((EGLBoolean(*)(EGLDisplay, EGLSurface, EGLSurface, EGLContext))p_eglMakeCurrent)(dpy, surf,
|
if (!((EGLBoolean(*)(EGLDisplay, EGLSurface, EGLSurface, EGLContext))p_eglMakeCurrent)(dpy, surf,
|
||||||
surf, ctx)) {
|
surf, ctx)) {
|
||||||
fprintf(stderr, "FAIL: eglMakeCurrent (0x%x)\n", ((EGLint(*)(void))p_eglGetError)());
|
fprintf(stderr, "FAIL: eglMakeCurrent (0x%x)\n", getError());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,6 +431,7 @@ static int boot_egl(void) {
|
|||||||
if (!name) { fprintf(stderr, "FAIL: resolve %s\n", #name); return 1; } \
|
if (!name) { fprintf(stderr, "FAIL: resolve %s\n", #name); return 1; } \
|
||||||
} while (0)
|
} while (0)
|
||||||
RESOLVE(glClear); RESOLVE(glClearColor); RESOLVE(glEnable); RESOLVE(glViewport);
|
RESOLVE(glClear); RESOLVE(glClearColor); RESOLVE(glEnable); RESOLVE(glViewport);
|
||||||
|
RESOLVE(glDisable); RESOLVE(glBlendFuncSeparate); RESOLVE(glDrawBuffers);
|
||||||
RESOLVE(glGetString); RESOLVE(glGetError); RESOLVE(glFinish); RESOLVE(glFlush);
|
RESOLVE(glGetString); RESOLVE(glGetError); RESOLVE(glFinish); RESOLVE(glFlush);
|
||||||
RESOLVE(glGenBuffers); RESOLVE(glBindBuffer); RESOLVE(glBufferData); RESOLVE(glBufferSubData);
|
RESOLVE(glGenBuffers); RESOLVE(glBindBuffer); RESOLVE(glBufferData); RESOLVE(glBufferSubData);
|
||||||
RESOLVE(glGenVertexArrays); RESOLVE(glBindVertexArray); RESOLVE(glEnableVertexAttribArray);
|
RESOLVE(glGenVertexArrays); RESOLVE(glBindVertexArray); RESOLVE(glEnableVertexAttribArray);
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ static GLuint g_uboRing;
|
|||||||
static GLint g_uboAlign = 256;
|
static GLint g_uboAlign = 256;
|
||||||
static size_t g_uboSlot = 256;
|
static size_t g_uboSlot = 256;
|
||||||
static GLuint g_sampler;
|
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};
|
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 */
|
/* Minecraft chunk vertex: pos 3f, color 4ub, uv 2f, packed light 2s -> 32 B */
|
||||||
@@ -162,10 +165,13 @@ static void setup_vao(GLuint vao, GLuint vbo, GLuint ibo) {
|
|||||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static GLuint g_mainFbo;
|
||||||
|
|
||||||
static void build_resources(void) {
|
static void build_resources(void) {
|
||||||
/* offscreen render target: 1280x720 RBO FBO, like CTS fbo surface mode */
|
/* offscreen render target: 1280x720 RBO FBO, like CTS fbo surface mode */
|
||||||
GLuint fbo, rboColor, rboDepth;
|
GLuint fbo, rboColor, rboDepth;
|
||||||
glGenFramebuffers(1, &fbo);
|
glGenFramebuffers(1, &fbo);
|
||||||
|
g_mainFbo = fbo;
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
glGenRenderbuffers(1, &rboColor);
|
glGenRenderbuffers(1, &rboColor);
|
||||||
glBindRenderbuffer(GL_RENDERBUFFER, rboColor);
|
glBindRenderbuffer(GL_RENDERBUFFER, rboColor);
|
||||||
@@ -257,6 +263,21 @@ static void build_resources(void) {
|
|||||||
glBufferData(GL_UNIFORM_BUFFER, 4 * 1024 * 1024, g_scratch, GL_DYNAMIC_DRAW);
|
glBufferData(GL_UNIFORM_BUFFER, 4 * 1024 * 1024, g_scratch, GL_DYNAMIC_DRAW);
|
||||||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
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);
|
glGenSamplers(1, &g_sampler);
|
||||||
glSamplerParameteri(g_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
glSamplerParameteri(g_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
glSamplerParameteri(g_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
glSamplerParameteri(g_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
@@ -516,6 +537,72 @@ static void case_mc_sampler_churn(int frame, long a, long b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 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 --------------------------------
|
/* ---- the case table both harnesses iterate --------------------------------
|
||||||
* a/b are the case's own knobs; opsPerFrame is what one bench frame is
|
* 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
|
* normalised by, so ns_per_op compares across renderers. The mc_* rates are
|
||||||
@@ -536,6 +623,10 @@ static const BenchCaseDesc kBenchCases[] = {
|
|||||||
{"mc_tex_stream", case_mc_tex_stream, 95, 0, 95},
|
{"mc_tex_stream", case_mc_tex_stream, 95, 0, 95},
|
||||||
{"mc_uniform_lookup", case_mc_uniform_lookup, 41, 0, 41},
|
{"mc_uniform_lookup", case_mc_uniform_lookup, 41, 0, 41},
|
||||||
{"mc_sampler_churn", case_mc_sampler_churn, 306, 0, 306},
|
{"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_tiny", case_draw_tiny, 2048, 0, 2048},
|
||||||
{"draw_uniform", case_draw_uniform, 2048, 0, 2048},
|
{"draw_uniform", case_draw_uniform, 2048, 0, 2048},
|
||||||
{"draw_multi_vao", case_draw_multi_vao, 2048, 0, 2048},
|
{"draw_multi_vao", case_draw_multi_vao, 2048, 0, 2048},
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "GL_Buffer.h"
|
#include "GL_Buffer.h"
|
||||||
#include "Validators.h"
|
#include "Validators.h"
|
||||||
#include "../Texture/GL_Texture.h"
|
#include "../Texture/GL_Texture.h"
|
||||||
|
#include "../Getter/GL_Getter.h"
|
||||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
@@ -861,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(
|
||||||
@@ -871,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1013,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,
|
||||||
@@ -1021,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,
|
||||||
@@ -1057,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,
|
||||||
@@ -1065,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,
|
||||||
@@ -1486,12 +1484,71 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GetBufferBindingSlot(bufferTarget).Bind(bufferObject);
|
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) {
|
||||||
MGLOG_D("%s: target = %s, index = %u, buffer = %u, offset = %d, size = %d", __func__,
|
MGLOG_D("%s: target = %s, index = %u, buffer = %u, offset = %d, size = %d", __func__,
|
||||||
MG_Util::ConvertGLEnumToString(target).c_str(), index, buffer, offset, size);
|
MG_Util::ConvertGLEnumToString(target).c_str(), index, buffer, offset, size);
|
||||||
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,
|
||||||
@@ -1665,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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -474,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,6 +502,24 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -977,8 +977,8 @@ 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)
|
||||||
@@ -1273,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)
|
||||||
@@ -1381,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)
|
||||||
|
|||||||
@@ -106,9 +106,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// `capabilityTargetIndex` is the row of the cache the attachment actually lives in;
|
// `capabilityTargetIndex` is the row of the cache the attachment actually lives in;
|
||||||
// kFormatCapabilityTargetCount asks about the format in general. Asking per target matters
|
// kFormatCapabilityTargetCount asks about the format in general. Asking per target matters
|
||||||
// because a capability recorded for one of them says nothing about the others: DirectGLES
|
// because a capability recorded for one of them says nothing about the others: DirectGLES
|
||||||
// widens three-channel formats to four channels to keep them renderable as *multisample*
|
// decides each target's substitution against that target's own probe, and a buffer texture
|
||||||
// storage, and a format that survives only through that substitution is still texture-only
|
// never gets one at all. This is also where the three-channel widening becomes visible to
|
||||||
// on every ordinary target.
|
// the application - a GL_RGB8_SNORM colour attachment on a driver with no renderable
|
||||||
|
// three-channel format answers COMPLETE because the backend stores it as GL_RGBA16F and
|
||||||
|
// recorded FramebufferRenderable in CaveatCaps.
|
||||||
Bool IsColorInternalFormatRenderable(TextureInternalFormat format, SizeT capabilityTargetIndex) {
|
Bool IsColorInternalFormatRenderable(TextureInternalFormat format, SizeT capabilityTargetIndex) {
|
||||||
const SizeT formatIndex = static_cast<SizeT>(format);
|
const SizeT formatIndex = static_cast<SizeT>(format);
|
||||||
if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) {
|
if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) {
|
||||||
|
|||||||
@@ -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,37 @@ 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
|
// 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).
|
// binding point, not by attribute (GL 4.6 core 10.3.1).
|
||||||
case GL_VERTEX_BINDING_BUFFER:
|
case GL_VERTEX_BINDING_BUFFER:
|
||||||
@@ -782,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(
|
||||||
@@ -958,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;
|
||||||
@@ -1069,12 +1209,32 @@ 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_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;
|
||||||
@@ -1516,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;
|
||||||
@@ -1618,87 +1774,6 @@ 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;
|
||||||
@@ -1971,6 +2046,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#include "GL_Program.h"
|
#include "GL_Program.h"
|
||||||
|
#include "ProgramInterface.h"
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
#include <MG_Util/Converters/GLToMG/ProgramEnumConverter.h>
|
#include <MG_Util/Converters/GLToMG/ProgramEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h>
|
||||||
#include <MG_Util/Converters/SPIRVCrossToGL/SpvcTypeConverter.h>
|
#include <MG_Util/Converters/SPIRVCrossToGL/SpvcTypeConverter.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 {
|
||||||
@@ -108,32 +110,37 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return programObject;
|
return programObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool IsProgramInterfaceEnum(GLenum programInterface) {
|
// The four non-location interface queries validate the NAME only: GL 4.6 imposes the
|
||||||
switch (programInterface) {
|
// successful-link requirement on GetProgramResourceLocation/LocationIndex alone, and
|
||||||
case GL_UNIFORM:
|
// requires the others to report a program that has never linked as one with zero active
|
||||||
case GL_UNIFORM_BLOCK:
|
// resources. Being stricter leaves a stray GL_INVALID_OPERATION behind that aborts the
|
||||||
case GL_PROGRAM_INPUT:
|
// caller's next subcase.
|
||||||
case GL_PROGRAM_OUTPUT:
|
static const SharedPtr<MG_State::GLState::ProgramObject>& TryToGetProgramForInterfaceQuery(GLuint program,
|
||||||
case GL_BUFFER_VARIABLE:
|
const char* caller) {
|
||||||
case GL_SHADER_STORAGE_BLOCK:
|
static const SharedPtr<MG_State::GLState::ProgramObject> nullProgramObject = nullptr;
|
||||||
case GL_ATOMIC_COUNTER_BUFFER:
|
if (!MG_State::pGLContext->ValidateProgramName(program)) {
|
||||||
case GL_TRANSFORM_FEEDBACK_VARYING:
|
const ErrorCode error = MG_State::pGLContext->ValidateShaderName(program)
|
||||||
case GL_VERTEX_SUBROUTINE:
|
? ErrorCode::InvalidOperation
|
||||||
case GL_TESS_CONTROL_SUBROUTINE:
|
: ErrorCode::InvalidValue;
|
||||||
case GL_TESS_EVALUATION_SUBROUTINE:
|
MG_State::pGLContext->RecordError(
|
||||||
case GL_GEOMETRY_SUBROUTINE:
|
error,
|
||||||
case GL_FRAGMENT_SUBROUTINE:
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||||
case GL_COMPUTE_SUBROUTINE:
|
std::to_string(program) + " is not a program object."));
|
||||||
case GL_VERTEX_SUBROUTINE_UNIFORM:
|
return nullProgramObject;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
auto& programObject = MG_State::pGLContext->GetProgramObject(program);
|
||||||
|
if (!programObject) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||||
|
std::to_string(program) + " is not a program object."));
|
||||||
|
return nullProgramObject;
|
||||||
|
}
|
||||||
|
return programObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsProgramInterfaceEnum(GLenum programInterface) {
|
||||||
|
return ProgramInterface::IsInterfaceEnum(programInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool IsSubroutineUniformInterface(GLenum programInterface) {
|
static bool IsSubroutineUniformInterface(GLenum programInterface) {
|
||||||
@@ -156,11 +163,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_ACTIVE_RESOURCES:
|
case GL_ACTIVE_RESOURCES:
|
||||||
break;
|
break;
|
||||||
case GL_MAX_NAME_LENGTH:
|
case GL_MAX_NAME_LENGTH:
|
||||||
valid = valid && programInterface != GL_ATOMIC_COUNTER_BUFFER;
|
// Neither buffer interface has resource names. GL_TRANSFORM_FEEDBACK_BUFFER only
|
||||||
|
// became reachable here when IsInterfaceEnum grew the GL 4.4 interfaces, so it
|
||||||
|
// needs the same exclusion GL_ATOMIC_COUNTER_BUFFER already had.
|
||||||
|
valid = valid && programInterface != GL_ATOMIC_COUNTER_BUFFER &&
|
||||||
|
programInterface != GL_TRANSFORM_FEEDBACK_BUFFER;
|
||||||
break;
|
break;
|
||||||
case GL_MAX_NUM_ACTIVE_VARIABLES:
|
case GL_MAX_NUM_ACTIVE_VARIABLES:
|
||||||
valid = programInterface == GL_UNIFORM_BLOCK || programInterface == GL_ATOMIC_COUNTER_BUFFER ||
|
valid = programInterface == GL_UNIFORM_BLOCK || programInterface == GL_ATOMIC_COUNTER_BUFFER ||
|
||||||
programInterface == GL_SHADER_STORAGE_BLOCK;
|
programInterface == GL_SHADER_STORAGE_BLOCK ||
|
||||||
|
programInterface == GL_TRANSFORM_FEEDBACK_BUFFER;
|
||||||
break;
|
break;
|
||||||
case GL_MAX_NUM_COMPATIBLE_SUBROUTINES:
|
case GL_MAX_NUM_COMPATIBLE_SUBROUTINES:
|
||||||
valid = IsSubroutineUniformInterface(programInterface);
|
valid = IsSubroutineUniformInterface(programInterface);
|
||||||
@@ -179,7 +191,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool ValidateNamedProgramResourceInterface(GLenum programInterface, const char* caller) {
|
static bool ValidateNamedProgramResourceInterface(GLenum programInterface, const char* caller) {
|
||||||
if (!IsProgramInterfaceEnum(programInterface) || programInterface == GL_ATOMIC_COUNTER_BUFFER) {
|
if (!ProgramInterface::IsNamedInterface(programInterface)) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||||
@@ -189,66 +201,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Int GetKnownProgramResourceCount(const SharedPtr<MG_State::GLState::ProgramObject>& programObject,
|
|
||||||
GLenum programInterface) {
|
|
||||||
switch (programInterface) {
|
|
||||||
case GL_UNIFORM:
|
|
||||||
return programObject->GetUniformCount();
|
|
||||||
case GL_UNIFORM_BLOCK:
|
|
||||||
return programObject->GetActiveUniformBlocksCount();
|
|
||||||
case GL_PROGRAM_INPUT:
|
|
||||||
return programObject->GetActiveAttributesCount();
|
|
||||||
case GL_PROGRAM_OUTPUT:
|
|
||||||
return programObject->GetActiveFragmentOutputCount();
|
|
||||||
default:
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The GL_UNIFORM interface and glGetActiveUniform(s)iv are the same query in two
|
|
||||||
// spellings, so they answer from the same place - the frontend reflection. The backend
|
|
||||||
// program is not that place: it does not exist at all for a program whose types its
|
|
||||||
// shading language cannot express (a double-precision uniform has no ESSL form), and
|
|
||||||
// the interface queries would then describe a program with no uniforms.
|
|
||||||
//
|
|
||||||
// Writes the GL_UNIFORM value of `prop` for active uniform `index`; false for a prop
|
|
||||||
// the reflection does not model, which the caller forwards to the backend instead.
|
|
||||||
Bool GetUniformResourceProp(const SharedPtr<MG_State::GLState::ProgramObject>& programObject, Uint index,
|
|
||||||
GLenum prop, GLint* out) {
|
|
||||||
switch (prop) {
|
|
||||||
case GL_TYPE:
|
|
||||||
*out = static_cast<GLint>(programObject->GetActiveUniformType(index));
|
|
||||||
return true;
|
|
||||||
case GL_ARRAY_SIZE:
|
|
||||||
*out = programObject->GetActiveUniformArraySize(index);
|
|
||||||
return true;
|
|
||||||
case GL_NAME_LENGTH:
|
|
||||||
*out = static_cast<GLint>(programObject->GetActiveUniformName(index).length() + 1);
|
|
||||||
return true;
|
|
||||||
case GL_BLOCK_INDEX:
|
|
||||||
*out = programObject->GetActiveUniformBlockIndex(index);
|
|
||||||
return true;
|
|
||||||
case GL_OFFSET:
|
|
||||||
*out = programObject->GetActiveUniformOffset(index);
|
|
||||||
return true;
|
|
||||||
case GL_ARRAY_STRIDE:
|
|
||||||
*out = programObject->GetActiveUniformArrayStride(index);
|
|
||||||
return true;
|
|
||||||
case GL_MATRIX_STRIDE:
|
|
||||||
*out = programObject->GetActiveUniformMatrixStride(index);
|
|
||||||
return true;
|
|
||||||
case GL_IS_ROW_MAJOR:
|
|
||||||
*out = programObject->GetActiveUniformIsRowMajor(index);
|
|
||||||
return true;
|
|
||||||
case GL_LOCATION:
|
|
||||||
// A block member has no location; GetUniformLocation already reports -1 for one.
|
|
||||||
*out = programObject->GetUniformLocation(programObject->GetActiveUniformName(index));
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CopyStr(GLsizei bufSize, GLsizei* length, GLchar* dst, const char* src, GLsizei srcLength) {
|
void CopyStr(GLsizei bufSize, GLsizei* length, GLchar* dst, const char* src, GLsizei srcLength) {
|
||||||
if (bufSize <= 0) {
|
if (bufSize <= 0) {
|
||||||
if (length) *length = 0;
|
if (length) *length = 0;
|
||||||
@@ -355,19 +307,72 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
shaderObject->Compile();
|
shaderObject->Compile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// glMaxShaderCompilerThreadsKHR / glMaxShaderCompilerThreadsARB - one implementation,
|
||||||
|
// because GL_KHR_parallel_shader_compile and GL_ARB_parallel_shader_compile define the
|
||||||
|
// same entry point with the same semantics and GetProcAddress.cpp maps both spellings.
|
||||||
|
//
|
||||||
|
// The three cases the extension defines, and what each means here:
|
||||||
|
//
|
||||||
|
// count == 0 "no compiler threads": compilation must happen on the
|
||||||
|
// application's thread. Everything already in flight is joined
|
||||||
|
// first, so that after this call returns NOTHING is outstanding
|
||||||
|
// and every GL_COMPLETION_STATUS_KHR reads GL_TRUE - which is
|
||||||
|
// the observable the extension actually specifies. The pool
|
||||||
|
// keeps its worker threads (this is not teardown); what changes
|
||||||
|
// is that AsyncShaderCompileActive() now says no, so
|
||||||
|
// glCompileShader/glLinkProgram run their bodies inline.
|
||||||
|
// count == 0xFFFFFFFF "implementation maximum": the pool's full thread count.
|
||||||
|
// otherwise a concurrency budget, clamped to the thread count - asking for
|
||||||
|
// more threads than exist cannot conjure any.
|
||||||
|
//
|
||||||
|
// A nonzero count is also what LIFTS a previous zero: the suspension lasts exactly until
|
||||||
|
// the application asks for threads again, and nothing else re-arms it (no implicit
|
||||||
|
// restore at eglInitialize, at a context switch or at a join). An application that turned
|
||||||
|
// compiler threads off keeps them off until it says otherwise.
|
||||||
|
//
|
||||||
|
// Legal - and a no-op beyond bookkeeping - while MOBILEGL_ASYNC_SHADER_COMPILE is off:
|
||||||
|
// compilation is already inline, and the call must not fail just because MobileGL had
|
||||||
|
// nothing to suspend.
|
||||||
|
void MaxShaderCompilerThreadsKHR_State(GLuint count) {
|
||||||
|
namespace Async = MG_Util::Async;
|
||||||
|
if (count == 0) {
|
||||||
|
MGLOG_D("%s: count = 0; joining all pending shader work and compiling inline", __func__);
|
||||||
|
Async::SetAsyncShaderCompileSuspended(true);
|
||||||
|
// Suspend BEFORE joining, not after. The post-condition this call owes the
|
||||||
|
// application is "nothing is in flight when I return", and only this order
|
||||||
|
// guarantees it: with the latch already set, anything the join itself causes to
|
||||||
|
// be compiled runs inline and is therefore already settled when the join ends.
|
||||||
|
// Joining first would leave a window in which a fresh enqueue is still legal.
|
||||||
|
if (MG_State::pGLContext) MG_State::pGLContext->JoinAllPendingShaderWork();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Async::ShaderCompilePool& pool = Async::ShaderCompilePool::Get();
|
||||||
|
const Uint threadCount = pool.GetThreadCount();
|
||||||
|
const Uint requested = count == 0xFFFFFFFFu ? threadCount : std::min<Uint>(count, threadCount);
|
||||||
|
pool.SetMaxConcurrency(requested);
|
||||||
|
Async::SetAsyncShaderCompileSuspended(false);
|
||||||
|
MGLOG_D("%s: count = %u; concurrency = %u of %u threads", __func__, count, requested, threadCount);
|
||||||
|
}
|
||||||
|
|
||||||
GLuint CreateProgram_State() {
|
GLuint CreateProgram_State() {
|
||||||
return MG_State::pGLContext->CreateProgram();
|
return MG_State::pGLContext->CreateProgram();
|
||||||
}
|
}
|
||||||
|
|
||||||
GLuint CreateShader_State(GLenum type) {
|
GLuint CreateShader_State(GLenum type) {
|
||||||
auto shaderId = MG_State::pGLContext->CreateShader(MG_Util::ConvertGLEnumToShaderStage(type));
|
// GL 4.6 core 7.1: shaderType is an enum, so an unrecognised one is INVALID_ENUM (it
|
||||||
if (shaderId == 0) {
|
// used to be documented as INVALID_VALUE). The check has to happen HERE: the state
|
||||||
|
// layer hands out a name for ShaderStage::Unknown just as happily as for a real
|
||||||
|
// stage, so the old "shaderId == 0 means bad type" test could never fire and an
|
||||||
|
// unknown shaderType silently produced a usable shader name and no error at all.
|
||||||
|
const ShaderStage stage = MG_Util::ConvertGLEnumToShaderStage(type);
|
||||||
|
if (stage == ShaderStage::Unknown) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidEnum,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "`shaderType` is not an accepted value."));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "`shaderType` is not an accepted value."));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return shaderId;
|
return MG_State::pGLContext->CreateShader(stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeleteProgram_State(GLuint program) {
|
void DeleteProgram_State(GLuint program) {
|
||||||
@@ -693,6 +698,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL_KHR_parallel_shader_compile. THIS CASE MUST NOT JOIN - it is the one program
|
||||||
|
// query whose entire purpose is to answer without waiting, and routing it through
|
||||||
|
// any of ProgramObject's Artifacts() accessors (the join gate, invariant I5) would
|
||||||
|
// block the caller and make the extension a lie: an application polling it would
|
||||||
|
// serialize itself on the very link it is trying to overlap. IsLinkComplete() is the
|
||||||
|
// node-direct reader that exists for exactly this.
|
||||||
|
//
|
||||||
|
// No link at all reads GL_TRUE, which is what the extension requires: the query
|
||||||
|
// means "is anything still outstanding", not "has this program ever been linked".
|
||||||
|
case GL_COMPLETION_STATUS_KHR:
|
||||||
|
*params = programObject->IsLinkComplete() ? GL_TRUE : GL_FALSE;
|
||||||
|
break;
|
||||||
|
|
||||||
case GL_PROGRAM_BINARY_LENGTH:
|
case GL_PROGRAM_BINARY_LENGTH:
|
||||||
// No program binary format is exposed, so a program never has a retrievable
|
// No program binary format is exposed, so a program never has a retrievable
|
||||||
// binary and its length is zero (ARB_get_program_binary).
|
// binary and its length is zero (ARB_get_program_binary).
|
||||||
@@ -746,6 +764,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_SHADER_SOURCE_LENGTH:
|
case GL_SHADER_SOURCE_LENGTH:
|
||||||
*params = shaderObject->GetShaderSource().empty() ? 0 : (GLint)shaderObject->GetShaderSource().length() + 1;
|
*params = shaderObject->GetShaderSource().empty() ? 0 : (GLint)shaderObject->GetShaderSource().length() + 1;
|
||||||
break;
|
break;
|
||||||
|
// GL_KHR_parallel_shader_compile. THIS CASE MUST NOT JOIN - see the identical case in
|
||||||
|
// GetProgramiv_State. GL_COMPILE_STATUS two cases up deliberately DOES join (it has
|
||||||
|
// to: it reports the outcome); this one reports whether there is an outcome yet, and
|
||||||
|
// reading it through Compiled() would defeat the whole extension.
|
||||||
|
case GL_COMPLETION_STATUS_KHR:
|
||||||
|
*params = shaderObject->IsCompileComplete() ? GL_TRUE : GL_FALSE;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
@@ -786,6 +811,31 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return loc;
|
return loc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A float matrix lives in the global UBO under std140 rules - one 16-byte-aligned column
|
||||||
|
// vector per column - while the value glGetUniform* must return is tightly packed
|
||||||
|
// columns * rows floats. Only mat4 is the same either way; every other shape needs the
|
||||||
|
// padding undone, and the readback has to undo exactly what UniformMatrixfv_Object put
|
||||||
|
// there. Returns false when `ttype` is not a float matrix (nothing to unpack).
|
||||||
|
Bool TryGatherFloatMatrixColumns(const glslang::TType* ttype, const char* pBase, void* params) {
|
||||||
|
if (ttype == nullptr || !ttype->isMatrix() || ttype->getBasicType() == glslang::EbtDouble) return false;
|
||||||
|
const Int columns = ttype->getMatrixCols();
|
||||||
|
const Int rows = ttype->getMatrixRows();
|
||||||
|
for (Int column = 0; column < columns; ++column) {
|
||||||
|
Memcpy(static_cast<char*>(params) + static_cast<SizeT>(column) * rows * sizeof(GLfloat),
|
||||||
|
pBase + static_cast<SizeT>(column) * 4 * sizeof(GLfloat), rows * sizeof(GLfloat));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes a uniform actually occupies in the global UBO. It is the tight GL type size for
|
||||||
|
// everything except a float matrix, whose padded columns make it wider.
|
||||||
|
SizeT UniformStorageSpanInBytes(const glslang::TType* ttype, SizeT tightSize) {
|
||||||
|
if (ttype != nullptr && ttype->isMatrix() && ttype->getBasicType() != glslang::EbtDouble) {
|
||||||
|
return static_cast<SizeT>(ttype->getMatrixCols()) * 4 * sizeof(GLfloat);
|
||||||
|
}
|
||||||
|
return tightSize;
|
||||||
|
}
|
||||||
|
|
||||||
void GetUniform_State(GLuint program, GLint location, void* params) {
|
void GetUniform_State(GLuint program, GLint location, void* params) {
|
||||||
auto& programObject = TryToGetProgramObject(program);
|
auto& programObject = TryToGetProgramObject(program);
|
||||||
if (!programObject) return;
|
if (!programObject) return;
|
||||||
@@ -816,23 +866,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
auto size = programObject->GetUniformSizesInBytes(location);
|
auto size = programObject->GetUniformSizesInBytes(location);
|
||||||
char* pUBO = (char*)programObject->MapUBO();
|
char* pUBO = (char*)programObject->MapUBO();
|
||||||
auto* ttype = programObject->GetUniformTType(location);
|
auto* ttype = programObject->GetUniformTType(location);
|
||||||
|
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
||||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||||
offset + size > programObject->GetUBOSize()) {
|
offset + span > programObject->GetUBOSize()) {
|
||||||
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||||
program, location);
|
program, location);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ttype->isMatrix() || ttype->getMatrixCols() != 3)
|
if (!TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) {
|
||||||
Memcpy(params, pUBO + offset, size);
|
Memcpy(params, pUBO + offset, size);
|
||||||
else {
|
|
||||||
// TODO: we only deal with mat3 yet, deal with other types later
|
|
||||||
// assuming float here, which may not be the case
|
|
||||||
auto* pBase = pUBO + offset;
|
|
||||||
for (int i = 0; i < ttype->getMatrixRows(); i++) {
|
|
||||||
Memcpy((char*)params + ttype->getMatrixCols() * sizeof(float) * i, pBase + 4 * sizeof(float) * i,
|
|
||||||
ttype->getMatrixCols() * sizeof(float));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: handle 1i variant as texture unit
|
// TODO: handle 1i variant as texture unit
|
||||||
@@ -871,23 +914,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
auto size = programObject->GetUniformSizesInBytes(location);
|
auto size = programObject->GetUniformSizesInBytes(location);
|
||||||
char* pUBO = static_cast<char*>(programObject->MapUBO());
|
char* pUBO = static_cast<char*>(programObject->MapUBO());
|
||||||
auto* ttype = programObject->GetUniformTType(location);
|
auto* ttype = programObject->GetUniformTType(location);
|
||||||
|
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
||||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||||
offset + size > programObject->GetUBOSize()) {
|
offset + span > programObject->GetUBOSize()) {
|
||||||
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||||
program, location);
|
program, location);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if constexpr (std::is_same_v<T, GLfloat>) {
|
if constexpr (std::is_same_v<T, GLfloat>) {
|
||||||
if (ttype->getBasicType() != glslang::EbtDouble && ttype->isMatrix() &&
|
if (TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) return;
|
||||||
ttype->getMatrixCols() == 3) {
|
|
||||||
auto* pBase = pUBO + offset;
|
|
||||||
for (int i = 0; i < ttype->getMatrixRows(); i++) {
|
|
||||||
Memcpy(reinterpret_cast<char*>(params) + ttype->getMatrixCols() * sizeof(GLfloat) * i,
|
|
||||||
pBase + 4 * sizeof(GLfloat) * i, ttype->getMatrixCols() * sizeof(GLfloat));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A double-precision uniform is the one case where the stored component type can
|
// A double-precision uniform is the one case where the stored component type can
|
||||||
@@ -1188,6 +1224,52 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// glUniformMatrix*fv / glProgramUniformMatrix*fv, every shape (square and non-square).
|
||||||
|
// A float matrix sits in the global UBO under std140 rules: each of its `columns`
|
||||||
|
// column vectors starts on its own 16-byte boundary no matter how many rows it has, so
|
||||||
|
// the only shape that may be written as one contiguous block is mat4. Writing a matNxM
|
||||||
|
// as N*M packed floats puts every column after the first at the wrong byte offset.
|
||||||
|
template <typename Program>
|
||||||
|
void UniformMatrixfv_Object(Program& programObject, const char* caller, GLint location, GLsizei count,
|
||||||
|
GLboolean transpose, const GLfloat* value, Int columns, Int rows,
|
||||||
|
const String& ownerDescription) {
|
||||||
|
// std140: a column vector of a float matrix is padded out to a vec4.
|
||||||
|
constexpr SizeT kColumnStride = 4 * sizeof(GLfloat);
|
||||||
|
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
|
||||||
|
GLfloat column[4] = {};
|
||||||
|
for (GLint matrix = 0; matrix < count; ++matrix) {
|
||||||
|
if (matrix > 0 && !programObject.UniformLocationsAliasSameUniform(location, location + matrix)) {
|
||||||
|
// GL 3.3 2.11.4: values for elements beyond the end of the uniform array
|
||||||
|
// are ignored. Never step onto a neighboring uniform's location.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!programObject.IsValidUniformLocation(location + matrix)) {
|
||||||
|
RecordInvalidUniformLocationError(caller, location + matrix, ownerDescription);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (programObject.IsUniformOpaqueAtLocation(location + matrix)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||||
|
"Opaque uniforms cannot be set with matrix Uniform calls."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value == nullptr) return;
|
||||||
|
const GLfloat* source = value + static_cast<SizeT>(matrix) * componentCount;
|
||||||
|
for (Int c = 0; c < columns; ++c) {
|
||||||
|
for (Int r = 0; r < rows; ++r) {
|
||||||
|
column[r] = transpose == GL_TRUE ? source[r * columns + c] : source[c * rows + r];
|
||||||
|
}
|
||||||
|
const SizeT byteOffset = static_cast<SizeT>(c) * kColumnStride;
|
||||||
|
switch (rows) {
|
||||||
|
case 2: Uniform_State<2>(programObject, location + matrix, column, byteOffset); break;
|
||||||
|
case 3: Uniform_State<3>(programObject, location + matrix, column, byteOffset); break;
|
||||||
|
default: Uniform_State<4>(programObject, location + matrix, column, byteOffset); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to transpose a 2x2 matrix
|
// Helper function to transpose a 2x2 matrix
|
||||||
void TransposeMatrix2x2(const GLfloat* input, GLfloat* output) {
|
void TransposeMatrix2x2(const GLfloat* input, GLfloat* output) {
|
||||||
// Input matrix is in column-major order (OpenGL default)
|
// Input matrix is in column-major order (OpenGL default)
|
||||||
@@ -1293,8 +1375,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UniformMatrix2fv_State(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
void UniformMatrix2fv_State(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||||
// For 2x2 matrices, we have 4 elements per matrix
|
// A mat2 is NOT four contiguous floats in the global UBO: std140 pads each column
|
||||||
// If transpose is GL_TRUE, we need to transpose the matrix data
|
// vector out to 16 bytes, so column 1 starts at byte 16, not byte 8.
|
||||||
if (location == -1) return;
|
if (location == -1) return;
|
||||||
|
|
||||||
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||||
@@ -1305,26 +1387,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For matrix uniforms, we handle each matrix individually
|
UniformMatrixfv_Object(*programObject, __func__, location, count, transpose, value, 2, 2,
|
||||||
for (GLint i = 0; i < count; i++) {
|
"the current program object");
|
||||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
|
||||||
// Values for elements beyond the end of the uniform array are ignored.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
|
||||||
RecordInvalidUniformLocationError(__func__, location + i, "the current program object");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (transpose == GL_TRUE) {
|
|
||||||
// Transpose the matrix before uploading
|
|
||||||
GLfloat transposedMatrix[4];
|
|
||||||
TransposeMatrix2x2(value + i * 4, transposedMatrix);
|
|
||||||
Uniform_State<4>(*programObject, location + i, transposedMatrix);
|
|
||||||
} else {
|
|
||||||
// No transpose needed, directly copy the matrix data
|
|
||||||
Uniform_State<4>(*programObject, location + i, value + i * 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UniformMatrix3fv_State(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
void UniformMatrix3fv_State(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||||
@@ -1402,7 +1466,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UniformMatrixNonSquarefv_State(const char* caller, GLint location, GLsizei count) {
|
void UniformMatrixNonSquarefv_State(const char* caller, GLint location, GLsizei count, GLboolean transpose,
|
||||||
|
const GLfloat* value, Int columns, Int rows) {
|
||||||
if (location == -1) return;
|
if (location == -1) return;
|
||||||
|
|
||||||
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||||
@@ -1413,21 +1478,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (GLint i = 0; i < count; i++) {
|
UniformMatrixfv_Object(*programObject, caller, location, count, transpose, value, columns, rows,
|
||||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
"the current program object");
|
||||||
RecordInvalidUniformLocationError(caller, location + i, "the current program object");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (programObject->IsUniformOpaqueAtLocation(location + i)) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
|
||||||
"Opaque uniforms cannot be set with matrix Uniform calls."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Implement non-square matrix uniform uploads for non-opaque uniforms.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramUniformMatrix2fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
void ProgramUniformMatrix2fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||||
@@ -1445,23 +1497,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (GLint i = 0; i < count; i++) {
|
UniformMatrixfv_Object(*programObject, __func__, location, count, transpose, value, 2, 2,
|
||||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
"program " + std::to_string(program));
|
||||||
// Values for elements beyond the end of the uniform array are ignored.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
|
||||||
RecordInvalidUniformLocationError(__func__, location + i, "program " + std::to_string(program));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (transpose == GL_TRUE) {
|
|
||||||
GLfloat transposedMatrix[4];
|
|
||||||
TransposeMatrix2x2(value + i * 4, transposedMatrix);
|
|
||||||
Uniform_State<4>(*programObject, location + i, transposedMatrix);
|
|
||||||
} else {
|
|
||||||
Uniform_State<4>(*programObject, location + i, value + i * 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramUniformMatrix3fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
void ProgramUniformMatrix3fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||||
@@ -1536,7 +1573,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramUniformMatrixNonSquarefv_State(const char* caller, GLuint program, GLint location, GLsizei count) {
|
void ProgramUniformMatrixNonSquarefv_State(const char* caller, GLuint program, GLint location, GLsizei count,
|
||||||
|
GLboolean transpose, const GLfloat* value, Int columns, Int rows) {
|
||||||
if (location == -1) return;
|
if (location == -1) return;
|
||||||
|
|
||||||
auto& programObject = TryToGetProgramObject(program);
|
auto& programObject = TryToGetProgramObject(program);
|
||||||
@@ -1550,21 +1588,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (GLint i = 0; i < count; i++) {
|
UniformMatrixfv_Object(*programObject, caller, location, count, transpose, value, columns, rows,
|
||||||
if (!programObject->IsValidUniformLocation(location + i)) {
|
"program " + std::to_string(program));
|
||||||
RecordInvalidUniformLocationError(caller, location + i, "program " + std::to_string(program));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (programObject->IsUniformOpaqueAtLocation(location + i)) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
|
||||||
"Opaque uniforms cannot be set with matrix Uniform calls."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Implement non-square matrix uniform uploads for non-opaque uniforms.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GLuint GetUniformBlockIndex_State(GLuint program, const GLchar* uniformBlockName) {
|
GLuint GetUniformBlockIndex_State(GLuint program, const GLchar* uniformBlockName) {
|
||||||
@@ -1832,6 +1857,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void CompileShader(GLuint shader) {
|
void CompileShader(GLuint shader) {
|
||||||
CompileShader_State(shader);
|
CompileShader_State(shader);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MaxShaderCompilerThreadsKHR(GLuint count) {
|
||||||
|
MaxShaderCompilerThreadsKHR_State(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL_ARB_parallel_shader_compile's spelling of the same entry point.
|
||||||
|
void MaxShaderCompilerThreadsARB(GLuint count) {
|
||||||
|
MaxShaderCompilerThreadsKHR_State(count);
|
||||||
|
}
|
||||||
|
|
||||||
GLuint CreateProgram(void) {
|
GLuint CreateProgram(void) {
|
||||||
return CreateProgram_State();
|
return CreateProgram_State();
|
||||||
}
|
}
|
||||||
@@ -2363,27 +2398,27 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UniformMatrix2x3fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
void UniformMatrix2x3fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||||
UniformMatrixNonSquarefv_State(__func__, location, count);
|
UniformMatrixNonSquarefv_State(__func__, location, count, transpose, value, 2, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UniformMatrix3x2fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
void UniformMatrix3x2fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||||
UniformMatrixNonSquarefv_State(__func__, location, count);
|
UniformMatrixNonSquarefv_State(__func__, location, count, transpose, value, 3, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UniformMatrix2x4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
void UniformMatrix2x4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||||
UniformMatrixNonSquarefv_State(__func__, location, count);
|
UniformMatrixNonSquarefv_State(__func__, location, count, transpose, value, 2, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UniformMatrix4x2fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
void UniformMatrix4x2fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||||
UniformMatrixNonSquarefv_State(__func__, location, count);
|
UniformMatrixNonSquarefv_State(__func__, location, count, transpose, value, 4, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UniformMatrix3x4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
void UniformMatrix3x4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||||
UniformMatrixNonSquarefv_State(__func__, location, count);
|
UniformMatrixNonSquarefv_State(__func__, location, count, transpose, value, 3, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UniformMatrix4x3fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
void UniformMatrix4x3fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
|
||||||
UniformMatrixNonSquarefv_State(__func__, location, count);
|
UniformMatrixNonSquarefv_State(__func__, location, count, transpose, value, 4, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramUniform1f(GLuint program, GLint location, GLfloat v0) {
|
void ProgramUniform1f(GLuint program, GLint location, GLfloat v0) {
|
||||||
@@ -2508,32 +2543,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void ProgramUniformMatrix2x3fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
void ProgramUniformMatrix2x3fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||||
const GLfloat* value) {
|
const GLfloat* value) {
|
||||||
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count);
|
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count, transpose, value, 2, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramUniformMatrix3x2fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
void ProgramUniformMatrix3x2fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||||
const GLfloat* value) {
|
const GLfloat* value) {
|
||||||
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count);
|
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count, transpose, value, 3, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramUniformMatrix2x4fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
void ProgramUniformMatrix2x4fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||||
const GLfloat* value) {
|
const GLfloat* value) {
|
||||||
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count);
|
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count, transpose, value, 2, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramUniformMatrix4x2fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
void ProgramUniformMatrix4x2fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||||
const GLfloat* value) {
|
const GLfloat* value) {
|
||||||
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count);
|
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count, transpose, value, 4, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramUniformMatrix3x4fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
void ProgramUniformMatrix3x4fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||||
const GLfloat* value) {
|
const GLfloat* value) {
|
||||||
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count);
|
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count, transpose, value, 3, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramUniformMatrix4x3fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
void ProgramUniformMatrix4x3fv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||||
const GLfloat* value) {
|
const GLfloat* value) {
|
||||||
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count);
|
ProgramUniformMatrixNonSquarefv_State(__func__, program, location, count, transpose, value, 4, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
GLuint GetUniformBlockIndex(GLuint program, const GLchar* uniformBlockName) {
|
GLuint GetUniformBlockIndex(GLuint program, const GLchar* uniformBlockName) {
|
||||||
@@ -2570,171 +2605,160 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params) {
|
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params) {
|
||||||
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
|
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
|
||||||
if (!programObject) return;
|
if (!programObject) return;
|
||||||
if (!ValidateProgramInterfaceivQuery(programInterface, pname)) return;
|
if (!ValidateProgramInterfaceivQuery(programInterface, pname)) return;
|
||||||
auto getProgramInterfaceiv = MG_Backend::gBackendFunctionsTable.GL.GetProgramInterfaceiv;
|
if (!params) return;
|
||||||
if (!getProgramInterfaceiv) {
|
switch (pname) {
|
||||||
MG_State::pGLContext->RecordError(
|
case GL_ACTIVE_RESOURCES:
|
||||||
ErrorCode::InvalidOperation,
|
*params = ProgramInterface::GetActiveResourceCount(*programObject, programInterface);
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
return;
|
||||||
"Backend does not support program interface queries."));
|
case GL_MAX_NAME_LENGTH:
|
||||||
|
*params = ProgramInterface::GetMaxNameLength(*programObject, programInterface);
|
||||||
|
return;
|
||||||
|
case GL_MAX_NUM_ACTIVE_VARIABLES:
|
||||||
|
*params = ProgramInterface::GetMaxNumActiveVariables(*programObject, programInterface);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
// GL_MAX_NUM_COMPATIBLE_SUBROUTINES: the subroutine interfaces are always empty
|
||||||
|
// here (glslang refuses `subroutine` when generating SPIR-V), so zero it is.
|
||||||
|
*params = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (programInterface == GL_UNIFORM) {
|
|
||||||
if (pname == GL_ACTIVE_RESOURCES) {
|
|
||||||
*params = static_cast<GLint>(programObject->GetUniformCount());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pname == GL_MAX_NAME_LENGTH) {
|
|
||||||
// Stored as the bare length; GL_MAX_NAME_LENGTH counts the terminator.
|
|
||||||
*params = programObject->GetUniformMaxLength() + 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getProgramInterfaceiv(program, programInterface, pname, params);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name) {
|
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name) {
|
||||||
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
|
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
|
||||||
if (!programObject) return GL_INVALID_INDEX;
|
if (!programObject) return GL_INVALID_INDEX;
|
||||||
if (!ValidateNamedProgramResourceInterface(programInterface, __func__)) return GL_INVALID_INDEX;
|
if (!ValidateNamedProgramResourceInterface(programInterface, __func__)) return GL_INVALID_INDEX;
|
||||||
if (!name) return GL_INVALID_INDEX;
|
if (!name) return GL_INVALID_INDEX;
|
||||||
if (programInterface == GL_UNIFORM) {
|
return ProgramInterface::GetResourceIndex(*programObject, programInterface, name);
|
||||||
const Int uniformIndex = programObject->GetActiveUniformIndex(name);
|
|
||||||
return uniformIndex < 0 ? GL_INVALID_INDEX : static_cast<GLuint>(uniformIndex);
|
|
||||||
}
|
|
||||||
auto getProgramResourceIndex = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceIndex;
|
|
||||||
if (!getProgramResourceIndex) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
|
||||||
"Backend does not support program interface queries."));
|
|
||||||
return GL_INVALID_INDEX;
|
|
||||||
}
|
|
||||||
GLuint index = getProgramResourceIndex(program, programInterface, name);
|
|
||||||
const String resourceName = name;
|
|
||||||
if (index == GL_INVALID_INDEX && resourceName.length() > 3 &&
|
|
||||||
resourceName.compare(resourceName.length() - 3, 3, "[0]") == 0) {
|
|
||||||
index = getProgramResourceIndex(program, programInterface,
|
|
||||||
resourceName.substr(0, resourceName.length() - 3).c_str());
|
|
||||||
}
|
|
||||||
return index;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei* length,
|
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei* length,
|
||||||
GLchar* name) {
|
GLchar* name) {
|
||||||
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
|
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
|
||||||
if (!programObject) return;
|
if (!programObject) return;
|
||||||
if (!ValidateNamedProgramResourceInterface(programInterface, __func__)) return;
|
if (!ValidateNamedProgramResourceInterface(programInterface, __func__)) return;
|
||||||
const Int resourceCount = GetKnownProgramResourceCount(programObject, programInterface);
|
|
||||||
if (resourceCount >= 0 && index >= static_cast<GLuint>(resourceCount)) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidValue,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "index is out of range."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (bufSize < 0) {
|
if (bufSize < 0) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufSize must be non-negative."));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufSize must be non-negative."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (programInterface == GL_UNIFORM) {
|
String resourceName;
|
||||||
// Same index space GetProgramResourceIndex answers in, and the range check above
|
if (!ProgramInterface::GetResourceName(*programObject, programInterface, index, resourceName)) {
|
||||||
// already used it.
|
|
||||||
const String& uniformName = programObject->GetActiveUniformName(index);
|
|
||||||
CopyStr(bufSize, length, name, uniformName.c_str(), static_cast<GLsizei>(uniformName.length()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto getProgramResourceName = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceName;
|
|
||||||
if (!getProgramResourceName) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "index is out of range."));
|
||||||
"Backend does not support program interface queries."));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getProgramResourceName(program, programInterface, index, bufSize, length, name);
|
CopyStr(bufSize, length, name, resourceName.c_str(), static_cast<GLsizei>(resourceName.length()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
||||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params) {
|
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params) {
|
||||||
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
|
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
|
||||||
if (!programObject) return;
|
if (!programObject) return;
|
||||||
if (propCount < 0 || bufSize < 0) {
|
if (!ProgramInterface::IsInterfaceEnum(programInterface)) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
ErrorCode::InvalidEnum,
|
||||||
"propCount and bufSize must be non-negative."));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Unsupported program interface."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (programInterface == GL_UNIFORM) {
|
if (propCount <= 0 || bufSize < 0) {
|
||||||
if (index >= programObject->GetUniformCount()) {
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"propCount must be positive and bufSize "
|
||||||
|
"non-negative."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (props == nullptr) return;
|
||||||
|
// Both prop checks run BEFORE any value is produced: a property this command does
|
||||||
|
// not know at all is INVALID_ENUM, one it knows but the interface does not carry is
|
||||||
|
// INVALID_OPERATION (GL 4.6 Table 7.2). The two are deliberately different errors.
|
||||||
|
for (GLsizei i = 0; i < propCount; ++i) {
|
||||||
|
if (!ProgramInterface::IsResourceProp(props[i])) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "prop is not a valid property name."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ProgramInterface::InterfaceSupportsProp(programInterface, props[i])) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"prop is not supported for this program interface."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector<GLint> values;
|
||||||
|
for (GLsizei i = 0; i < propCount; ++i) {
|
||||||
|
if (!ProgramInterface::GetResourceProp(*programObject, programInterface, index, props[i], values)) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "index is out of range."));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "index is out of range."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (props == nullptr || params == nullptr) return;
|
|
||||||
GLsizei written = 0;
|
|
||||||
for (GLsizei i = 0; i < propCount && written < bufSize; ++i) {
|
|
||||||
GLint value = 0;
|
|
||||||
if (!GetUniformResourceProp(programObject, index, props[i], &value)) {
|
|
||||||
// GL_ATOMIC_COUNTER_BUFFER_INDEX and the GL_REFERENCED_BY_* stage props are
|
|
||||||
// not modelled here; ask the backend, which indexes resources by name.
|
|
||||||
auto backendGetIndex = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceIndex;
|
|
||||||
auto backendGetiv = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceiv;
|
|
||||||
if (backendGetIndex && backendGetiv) {
|
|
||||||
const GLuint backendIndex = backendGetIndex(program, GL_UNIFORM,
|
|
||||||
programObject->GetActiveUniformName(index).c_str());
|
|
||||||
if (backendIndex != GL_INVALID_INDEX) {
|
|
||||||
GLsizei one = 0;
|
|
||||||
backendGetiv(program, GL_UNIFORM, backendIndex, 1, &props[i], 1, &one, &value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
params[written++] = value;
|
|
||||||
}
|
|
||||||
if (length) *length = written;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
auto getProgramResourceiv = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceiv;
|
if (params == nullptr) return;
|
||||||
if (!getProgramResourceiv) {
|
const GLsizei written = static_cast<GLsizei>(std::min<SizeT>(values.size(), static_cast<SizeT>(bufSize)));
|
||||||
MG_State::pGLContext->RecordError(
|
for (GLsizei i = 0; i < written; ++i) params[i] = values[i];
|
||||||
ErrorCode::InvalidOperation,
|
if (length) *length = written;
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
|
||||||
"Backend does not support program interface queries."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
getProgramResourceiv(program, programInterface, index, propCount, props, bufSize, length, params);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name) {
|
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name) {
|
||||||
|
// Unlike the four queries above, this one and GetProgramResourceLocationIndex really
|
||||||
|
// do require a successful link (GL 4.6 §7.3.1.3).
|
||||||
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
|
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
|
||||||
if (!programObject) return -1;
|
if (!programObject) return -1;
|
||||||
auto getProgramResourceLocation = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceLocation;
|
if (!ProgramInterface::InterfaceHasLocations(programInterface)) {
|
||||||
if (!getProgramResourceLocation) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidEnum,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
"Backend does not support program interface queries."));
|
"Program interface has no locations."));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return getProgramResourceLocation(program, programInterface, name);
|
return ProgramInterface::GetResourceLocation(*programObject, programInterface, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name) {
|
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name) {
|
||||||
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
|
auto& programObject = TryToGetLinkedProgramForInterfaceQuery(program, __func__);
|
||||||
if (!programObject) return -1;
|
if (!programObject) return -1;
|
||||||
auto getProgramResourceLocationIndex = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceLocationIndex;
|
if (programInterface != GL_PROGRAM_OUTPUT) {
|
||||||
if (!getProgramResourceLocationIndex) return -1;
|
MG_State::pGLContext->RecordError(
|
||||||
return getProgramResourceLocationIndex(program, programInterface, name);
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"GetProgramResourceLocationIndex only accepts GL_PROGRAM_OUTPUT."));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return ProgramInterface::GetResourceLocationIndex(*programObject, programInterface, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL 4.6 §7.6.2: <storageBlockIndex> is an active shader storage block index of <program>
|
||||||
|
// - that is, exactly what glGetProgramResourceIndex(GL_SHADER_STORAGE_BLOCK) returned.
|
||||||
|
// Since wave 2 that index is the interface-query layer's, so this is where the one index
|
||||||
|
// space the application sees gets turned into whatever the backend's is; the backends are
|
||||||
|
// handed the block NAME and do their own lookup. Getting this wrong is silent: the call
|
||||||
|
// succeeds and rebinds a DIFFERENT buffer.
|
||||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) {
|
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) {
|
||||||
auto& programObject = TryToGetProgramObject(program);
|
auto& programObject = TryToGetProgramObject(program);
|
||||||
if (!programObject || !programObject->GetLinkStatus()) return;
|
if (!programObject || !programObject->GetLinkStatus()) return;
|
||||||
if (!ValidateShaderStorageBlockBinding(storageBlockBinding)) return;
|
if (!ValidateShaderStorageBlockBinding(storageBlockBinding)) return;
|
||||||
|
String blockName;
|
||||||
|
if (!ProgramInterface::GetResourceName(*programObject, GL_SHADER_STORAGE_BLOCK, storageBlockIndex,
|
||||||
|
blockName)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"storageBlockIndex is not an active shader storage block index."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Recorded before the backend call, and independently of whether a backend is even
|
||||||
|
// present: this is the state GL_BUFFER_BINDING reports, and it is also what reseeds a
|
||||||
|
// backend's own reflection cache after any rebuild.
|
||||||
|
programObject->SetShaderStorageBlockBinding(blockName, storageBlockBinding);
|
||||||
auto shaderStorageBlockBinding = MG_Backend::gBackendFunctionsTable.GL.ShaderStorageBlockBinding;
|
auto shaderStorageBlockBinding = MG_Backend::gBackendFunctionsTable.GL.ShaderStorageBlockBinding;
|
||||||
if (!shaderStorageBlockBinding) {
|
if (!shaderStorageBlockBinding) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -2743,7 +2767,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
"Backend does not support shader storage block binding."));
|
"Backend does not support shader storage block binding."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
shaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding);
|
shaderStorageBlockBinding(program, blockName.c_str(), storageBlockBinding);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ValidateProgram(GLuint program) {
|
void ValidateProgram(GLuint program) {
|
||||||
@@ -2780,6 +2804,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// is written as that sequence rather than as a private shortcut - every error it can
|
// is written as that sequence rather than as a private shortcut - every error it can
|
||||||
// raise is one of theirs, raised at the point they would raise it.
|
// raise is one of theirs, raised at the point they would raise it.
|
||||||
GLuint CreateShaderProgramv(GLenum type, GLsizei count, const GLchar* const* strings) {
|
GLuint CreateShaderProgramv(GLenum type, GLsizei count, const GLchar* const* strings) {
|
||||||
|
// GL 4.6 core 7.3: a negative count is INVALID_VALUE and is checked before anything
|
||||||
|
// is created, so a bad count never leaks a shader name. An unrecognised type is
|
||||||
|
// INVALID_ENUM, which CreateShader_State raises below.
|
||||||
|
if (count < 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "count must be non-negative."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
const GLuint shader = CreateShader_State(type);
|
const GLuint shader = CreateShader_State(type);
|
||||||
if (shader == 0) return 0;
|
if (shader == 0) return 0;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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>
|
||||||
@@ -268,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"));
|
||||||
@@ -309,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"));
|
||||||
|
|||||||
@@ -86,17 +86,65 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DSA emulation: the by-name entry points are implemented by putting the named texture
|
||||||
|
// on the active unit's slot for their target, running the classic bound-texture code,
|
||||||
|
// then putting the previous binding back.
|
||||||
|
//
|
||||||
|
// Both of those binds are REAL changes to "which texture is bound at this unit" for as
|
||||||
|
// long as `fn` runs, so both have to move the texture bind generation. Backends memoise
|
||||||
|
// per-unit work keyed on that generation and BORROW the binding slot (they hold a
|
||||||
|
// pointer to the slot's shared_ptr, not a copy); a slot swap the generation never saw
|
||||||
|
// let such a memo replay texture A's backend twin against texture B now sitting in the
|
||||||
|
// slot - which re-specified A's backend storage with B's shape and silently destroyed
|
||||||
|
// A's GPU-rendered contents (Minecraft's lightmap, blanked by a by-name upload to an
|
||||||
|
// Iris shadow map, which then discarded every glyph).
|
||||||
|
//
|
||||||
|
// The generation is bumped directly rather than through NoteTextureUnitTouched because
|
||||||
|
// the touched-unit HIGH-WATER MARK must NOT move: glActiveTexture does not advance it,
|
||||||
|
// so a DSA-only app would otherwise have every later draw walk up to the highest unit it
|
||||||
|
// ever aimed a by-name call at. Not advancing it is also sufficient - a unit above the
|
||||||
|
// mark is outside every memo's coverage and outside the epoch walk, so nothing can
|
||||||
|
// observe the transient swap there; at or below it, the bump is exactly what makes the
|
||||||
|
// epoch re-derive. Bumping only on a real change keeps the very common redundant case (a
|
||||||
|
// by-name call on the texture already bound to the active unit) free.
|
||||||
|
//
|
||||||
|
// The restore is a scope guard because `fn` can throw (the unsupported-state paths use
|
||||||
|
// THROW_EXCEPTION): leaking the temporary binding would leave the wrong texture bound to
|
||||||
|
// a live unit for the rest of the context's life.
|
||||||
template <typename Fn>
|
template <typename Fn>
|
||||||
void WithTemporarilyBoundNamedTexture(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
void WithTemporarilyBoundNamedTexture(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||||
Fn&& fn) {
|
Fn&& fn) {
|
||||||
if (!textureObject) return;
|
if (!textureObject) return;
|
||||||
|
|
||||||
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
const Int activeUnitIndex = MG_State::pGLContext->GetActiveTextureUnit();
|
||||||
|
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(activeUnitIndex);
|
||||||
auto& bindingSlot = activeUnit.GetBindingSlot(textureObject->GetTarget());
|
auto& bindingSlot = activeUnit.GetBindingSlot(textureObject->GetTarget());
|
||||||
const auto previousBinding = bindingSlot.GetBoundObject();
|
const auto previousBinding = bindingSlot.GetBoundObject();
|
||||||
bindingSlot.Bind(textureObject);
|
|
||||||
|
using SlotType = std::remove_reference_t<decltype(bindingSlot)>;
|
||||||
|
class ScopedSlotRestore {
|
||||||
|
public:
|
||||||
|
ScopedSlotRestore(SlotType& slot, SharedPtr<MG_State::GLState::ITextureObject> previous)
|
||||||
|
: m_slot(slot), m_previous(Move(previous)) {}
|
||||||
|
~ScopedSlotRestore() {
|
||||||
|
if (m_slot.Bind(m_previous)) {
|
||||||
|
MG_State::pGLContext->BumpTextureBindGeneration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ScopedSlotRestore(const ScopedSlotRestore&) = delete;
|
||||||
|
ScopedSlotRestore& operator=(const ScopedSlotRestore&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
SlotType& m_slot;
|
||||||
|
SharedPtr<MG_State::GLState::ITextureObject> m_previous;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bindingSlot.Bind(textureObject)) {
|
||||||
|
MG_State::pGLContext->BumpTextureBindGeneration();
|
||||||
|
}
|
||||||
|
ScopedSlotRestore restore(bindingSlot, previousBinding);
|
||||||
|
|
||||||
fn(MG_Util::ConvertTextureTargetToGLEnum(textureObject->GetTarget()));
|
fn(MG_Util::ConvertTextureTargetToGLEnum(textureObject->GetTarget()));
|
||||||
bindingSlot.Bind(previousBinding);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SizeT ComputeTextureStorageByteSize(TextureInternalFormat textureInternalFormat, GLsizei width, GLsizei height,
|
SizeT ComputeTextureStorageByteSize(TextureInternalFormat textureInternalFormat, GLsizei width, GLsizei height,
|
||||||
|
|||||||
@@ -106,6 +106,21 @@ 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,
|
// 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
|
// 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.
|
// 10F_11F_11F attribute is one 32-bit word regardless of its component count.
|
||||||
@@ -179,6 +194,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
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(
|
||||||
@@ -944,6 +964,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
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,
|
||||||
@@ -1007,6 +1034,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
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,
|
||||||
@@ -1066,6 +1100,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
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,
|
||||||
@@ -1204,6 +1242,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET:
|
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET:
|
||||||
*param = static_cast<GLint>(vao->GetAttributeRelativeOffset(index));
|
*param = static_cast<GLint>(vao->GetAttributeRelativeOffset(index));
|
||||||
return;
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_BINDING:
|
||||||
|
*param = static_cast<GLint>(vao->GetAttributeBindingIndex(index));
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
|
|||||||
@@ -147,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.
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -198,8 +206,11 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
@@ -185,6 +185,13 @@ 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;
|
||||||
@@ -207,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
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
#include "Core.h"
|
#include "Core.h"
|
||||||
#include "MG_State/GLState/RenderbufferState/RenderbufferObject.h"
|
#include "MG_State/GLState/RenderbufferState/RenderbufferObject.h"
|
||||||
#include "MG_State/EGLState/Core.h"
|
#include "MG_State/EGLState/Core.h"
|
||||||
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_State {
|
namespace MobileGL::MG_State {
|
||||||
@@ -24,8 +27,27 @@ namespace MobileGL::MG_State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace GLState {
|
namespace GLState {
|
||||||
|
const SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv>& GLContext::GetCompileEnv() {
|
||||||
|
const void* backend = static_cast<const void*>(MG_Backend::pActiveBackendObject.get());
|
||||||
|
if (!m_compileEnv || m_compileEnvBackend != backend) {
|
||||||
|
// First use, or the backend was swapped underneath us. Re-capturing rolls the
|
||||||
|
// fingerprint, so every P0b preprocess memo computed against the old backend's
|
||||||
|
// limits becomes structurally unreachable instead of silently reusable.
|
||||||
|
m_compileEnv = MG_Util::ShaderTranspiler::CaptureCompileEnv();
|
||||||
|
m_compileEnvBackend = backend;
|
||||||
|
}
|
||||||
|
return m_compileEnv;
|
||||||
|
}
|
||||||
|
|
||||||
// Error
|
// Error
|
||||||
void GLContext::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
|
void GLContext::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
|
||||||
|
// Invariant I1, mechanically enforced: the GL error state is GL-thread-owned.
|
||||||
|
// A compile or link body that needs to raise an error must append to its node's
|
||||||
|
// JobDiagnostics and let the join replay it here (see the P1 design section 6);
|
||||||
|
// reaching this from a worker would corrupt the sticky-flag set that
|
||||||
|
// glGetError's ordering depends on.
|
||||||
|
MOBILEGL_ASSERT(!MG_Util::Async::ShaderCompilePool::IsPoolThread(),
|
||||||
|
"GLContext::RecordError() called from a shader-compile pool thread");
|
||||||
m_errorState.RecordError(code, Move(info));
|
m_errorState.RecordError(code, Move(info));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,6 +357,10 @@ namespace MobileGL::MG_State {
|
|||||||
return m_programState.GetShaderObject(index);
|
return m_programState.GetShaderObject(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GLContext::JoinAllPendingShaderWork() {
|
||||||
|
m_programState.JoinAllPendingWork();
|
||||||
|
}
|
||||||
|
|
||||||
void GLContext::UseProgram(Uint program) {
|
void GLContext::UseProgram(Uint program) {
|
||||||
return m_programState.UseProgram(program);
|
return m_programState.UseProgram(program);
|
||||||
}
|
}
|
||||||
@@ -346,11 +372,35 @@ namespace MobileGL::MG_State {
|
|||||||
const SharedPtr<ProgramObject>& GLContext::GetProgramForDraw() {
|
const SharedPtr<ProgramObject>& GLContext::GetProgramForDraw() {
|
||||||
static const SharedPtr<ProgramObject> nullProgram = nullptr;
|
static const SharedPtr<ProgramObject> nullProgram = nullptr;
|
||||||
const auto& currentProgram = m_programState.GetCurrentProgram();
|
const auto& currentProgram = m_programState.GetCurrentProgram();
|
||||||
if (currentProgram) return currentProgram;
|
if (currentProgram) {
|
||||||
|
// P1 join site J1, plain glUseProgram half. The backends read a program's
|
||||||
|
// lifetimeId / backendStateVersion / UBO content version to decide whether
|
||||||
|
// their per-program caches are still valid, and none of those pass through
|
||||||
|
// ProgramObject's join gate - so a draw could sample a version, join later
|
||||||
|
// inside the same draw when it finally touched an artifact, and cache under a
|
||||||
|
// version the publish had already superseded. Settling here means every
|
||||||
|
// version a backend reads during a draw describes the program it is drawing.
|
||||||
|
// One null check in steady state.
|
||||||
|
currentProgram->JoinLink();
|
||||||
|
return currentProgram;
|
||||||
|
}
|
||||||
if (m_boundProgramPipeline == 0) return nullProgram;
|
if (m_boundProgramPipeline == 0) return nullProgram;
|
||||||
const auto& pipeline = GetBoundProgramPipeline();
|
const auto& pipeline = GetBoundProgramPipeline();
|
||||||
if (!pipeline) return nullProgram;
|
if (!pipeline) return nullProgram;
|
||||||
|
|
||||||
|
// P1 join site J1. ComputeDrawProgramSignature() keys the composite cache on each
|
||||||
|
// stage program's lifetimeId and backendStateVersion - NON-artifact fields, so
|
||||||
|
// they do not pass through ProgramObject's join gate and a pending link would
|
||||||
|
// stay pending right through the signature. Since the version is bumped both at
|
||||||
|
// enqueue and at publish, the signature computed inside a pending window is one
|
||||||
|
// that will never be produced again: every draw would miss the cache and rebuild
|
||||||
|
// (and relink) the composite. Join first, so the signature describes settled
|
||||||
|
// programs. In steady state this is a null check per stage.
|
||||||
|
for (SizeT stage = 0; stage < static_cast<SizeT>(ShaderStage::ShaderStageCount); ++stage) {
|
||||||
|
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
|
||||||
|
if (stageProgram) stageProgram->JoinLink();
|
||||||
|
}
|
||||||
|
|
||||||
const auto signature = pipeline->ComputeDrawProgramSignature();
|
const auto signature = pipeline->ComputeDrawProgramSignature();
|
||||||
if (const auto& cached = pipeline->GetCachedDrawProgram(signature)) return cached;
|
if (const auto& cached = pipeline->GetCachedDrawProgram(signature)) return cached;
|
||||||
|
|
||||||
@@ -378,6 +428,10 @@ namespace MobileGL::MG_State {
|
|||||||
// A pipeline with no fragment stage still rasterises, so the default fragment
|
// A pipeline with no fragment stage still rasterises, so the default fragment
|
||||||
// shader is wanted here even though the separable stage programs never get one.
|
// shader is wanted here even though the separable stage programs never get one.
|
||||||
composite->Link(true);
|
composite->Link(true);
|
||||||
|
// P1 join site J2. The draw that asked for this program is the very next thing to
|
||||||
|
// happen, so enqueueing the composite's link buys nothing and only moves the wait
|
||||||
|
// to whichever backend accessor happens to touch its artifacts first.
|
||||||
|
composite->JoinLink();
|
||||||
pipeline->SetCachedDrawProgram(signature, Move(composite));
|
pipeline->SetCachedDrawProgram(signature, Move(composite));
|
||||||
return pipeline->GetCachedDrawProgram(signature);
|
return pipeline->GetCachedDrawProgram(signature);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
#include "VertexArrayState/VertexArrayState.h"
|
#include "VertexArrayState/VertexArrayState.h"
|
||||||
#include "RenderbufferState/RenderbufferState.h"
|
#include "RenderbufferState/RenderbufferState.h"
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||||
|
struct CompileEnv;
|
||||||
|
}
|
||||||
|
|
||||||
namespace MobileGL {
|
namespace MobileGL {
|
||||||
namespace MG_State {
|
namespace MG_State {
|
||||||
void Init();
|
void Init();
|
||||||
@@ -120,6 +124,18 @@ namespace MobileGL {
|
|||||||
// per-draw sampled-texture set.
|
// per-draw sampled-texture set.
|
||||||
Uint64 GetTextureBindGeneration() const { return m_textureState.GetTextureBindGeneration(); }
|
Uint64 GetTextureBindGeneration() const { return m_textureState.GetTextureBindGeneration(); }
|
||||||
void BumpTextureBindGeneration() { m_textureState.BumpTextureBindGeneration(); }
|
void BumpTextureBindGeneration() { m_textureState.BumpTextureBindGeneration(); }
|
||||||
|
// Monotonic counter bumped whenever a texture's shape or a sampler object's
|
||||||
|
// parameters change, i.e. whenever a bound texture's mipmap-completeness (and so
|
||||||
|
// whether a backend binds it at all) can have flipped without any bind moving;
|
||||||
|
// see TextureState::GetSamplingResolutionGeneration.
|
||||||
|
Uint64 GetSamplingResolutionGeneration() const {
|
||||||
|
return m_textureState.GetSamplingResolutionGeneration();
|
||||||
|
}
|
||||||
|
void BumpSamplingResolutionGeneration() { m_textureState.BumpSamplingResolutionGeneration(); }
|
||||||
|
// Never-reused id of this context, for backend memos keyed on the two counters
|
||||||
|
// above: both restart at 0 in a new context, and a recreated context can land on
|
||||||
|
// the old heap address. See TextureState::GetContextId.
|
||||||
|
Uint64 GetTextureContextId() const { return m_textureState.GetContextId(); }
|
||||||
Bool ValidateTextureName(Uint index) const;
|
Bool ValidateTextureName(Uint index) const;
|
||||||
Bool ValidateTextureObject(Uint index) const;
|
Bool ValidateTextureObject(Uint index) const;
|
||||||
Int GetActiveTextureUnit() const;
|
Int GetActiveTextureUnit() const;
|
||||||
@@ -137,6 +153,14 @@ namespace MobileGL {
|
|||||||
Bool ValidateShaderName(Uint index) const;
|
Bool ValidateShaderName(Uint index) const;
|
||||||
const SharedPtr<ProgramObject>& GetProgramObject(Uint index);
|
const SharedPtr<ProgramObject>& GetProgramObject(Uint index);
|
||||||
const SharedPtr<ShaderObject>& GetShaderObject(Uint index);
|
const SharedPtr<ShaderObject>& GetShaderObject(Uint index);
|
||||||
|
// Settles every compile and link this context still owns; see
|
||||||
|
// ProgramState::JoinAllPendingWork. Called by glMaxShaderCompilerThreadsKHR(0).
|
||||||
|
void JoinAllPendingShaderWork();
|
||||||
|
// P1 stage 6: the per-context index of adoptable compile nodes, for its
|
||||||
|
// adoption counter. Diagnostics and tests only - no GL entry point reads it.
|
||||||
|
ShaderCompileAdoptionMap& GetShaderCompileAdoptionMap() {
|
||||||
|
return m_programState.GetShaderCompileAdoptionMap();
|
||||||
|
}
|
||||||
void UseProgram(Uint program);
|
void UseProgram(Uint program);
|
||||||
const SharedPtr<ProgramObject>& GetCurrentProgram();
|
const SharedPtr<ProgramObject>& GetCurrentProgram();
|
||||||
// What a draw or dispatch actually executes: the program in use, or - when
|
// What a draw or dispatch actually executes: the program in use, or - when
|
||||||
@@ -368,6 +392,15 @@ namespace MobileGL {
|
|||||||
Bool ValidateRenderbufferName(Uint index) const;
|
Bool ValidateRenderbufferName(Uint index) const;
|
||||||
Bool ValidateRenderbufferObject(Uint index) const;
|
Bool ValidateRenderbufferObject(Uint index) const;
|
||||||
|
|
||||||
|
// P1: the shader compile/link pipeline's snapshot of everything it reads from
|
||||||
|
// outside its own (stage, source) inputs. Captured lazily here because it
|
||||||
|
// cannot be captured in MG_State::Init() - that runs BEFORE MG_Backend::Init(),
|
||||||
|
// so there is no backend to query yet. Re-captured whenever the active backend
|
||||||
|
// object changes, which also rolls the fingerprint and therefore invalidates
|
||||||
|
// every P0b preprocess memo keyed against the old one.
|
||||||
|
// GL thread only.
|
||||||
|
const SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv>& GetCompileEnv();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// State Components
|
// State Components
|
||||||
ErrorState m_errorState;
|
ErrorState m_errorState;
|
||||||
@@ -425,6 +458,11 @@ namespace MobileGL {
|
|||||||
FramebufferState m_framebufferState;
|
FramebufferState m_framebufferState;
|
||||||
SamplerState m_samplerState;
|
SamplerState m_samplerState;
|
||||||
RenderbufferState m_renderbufferState;
|
RenderbufferState m_renderbufferState;
|
||||||
|
|
||||||
|
mutable SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> m_compileEnv;
|
||||||
|
// Identity of the backend object m_compileEnv was captured against; a plain
|
||||||
|
// pointer compare, never dereferenced.
|
||||||
|
const void* m_compileEnvBackend = nullptr;
|
||||||
};
|
};
|
||||||
} // namespace GLState
|
} // namespace GLState
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,111 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.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 <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||||
|
#include <MG_State/GLState/ProgramState/ShaderCompileTask.h>
|
||||||
|
#include <MG_Util/Async/JobNode.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
// One attached shader, as the link sees it: never the ShaderObject, always a snapshot.
|
||||||
|
//
|
||||||
|
// The ShaderObject is GL-thread-owned and may be re-sourced, detached or destroyed while
|
||||||
|
// this link is still queued; everything below is either immutable or independently owned,
|
||||||
|
// so none of that can reach the worker.
|
||||||
|
struct LinkShaderInput {
|
||||||
|
ShaderStage stage = ShaderStage::Unknown;
|
||||||
|
// For the compile-error diagnostic and the compute local_size check, both of which
|
||||||
|
// quote the ORIGINAL source rather than the preprocessed one.
|
||||||
|
SharedPtr<const String> source;
|
||||||
|
// The authoritative compiled state. Null, or non-Complete, both read as "this shader
|
||||||
|
// did not compile" - the same verdict ShaderObject's join gate produces.
|
||||||
|
SharedPtr<const ShaderCompileTask> compiled;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The unit of asynchronous linking: one glLinkProgram's worth of pure CPU work - glslang
|
||||||
|
// link + mapIO, SPIR-V generation and optimization, the GL-facing reflection surface, the
|
||||||
|
// global-UBO routing tables, fragment-output validation and transform-feedback
|
||||||
|
// resolution - with every input it needs snapshotted at enqueue.
|
||||||
|
//
|
||||||
|
// Same ownership rule as ShaderCompileTask: the body reads nothing but `in` (all of it
|
||||||
|
// owned or immutable) and writes nothing but `artifacts`. No GL call, no
|
||||||
|
// pActiveBackendObject read, no pGLContext->RecordError(); the device limits arrive
|
||||||
|
// through the CompileEnv snapshot and diagnostics are deferred to the join.
|
||||||
|
//
|
||||||
|
// ONE LINK IS ONE HANDLER. RunBody() runs start to finish inside a single pool handler
|
||||||
|
// and is the only place `artifacts` is written. Do not split it across handlers to
|
||||||
|
// "pipeline" the reflection half: the intermediates that GlslangToSpv and buildReflection
|
||||||
|
// share are mutated in a strict order (see the GenerateSpirv-before-DoReflection comment
|
||||||
|
// in Run()), and a second handler would let a cancel land between them and publish a
|
||||||
|
// program whose SPIR-V and reflection describe different things.
|
||||||
|
class ProgramLinkTask final : public MG_Util::Async::JobNode {
|
||||||
|
public:
|
||||||
|
// ---- inputs, snapshotted on the GL thread in ProgramObject::Link()'s prologue ----
|
||||||
|
struct Inputs {
|
||||||
|
Uint externalIndex = 0; // logs only
|
||||||
|
Vector<LinkShaderInput> shaders; // already stage-sorted
|
||||||
|
SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> env;
|
||||||
|
// The four "takes effect at the next link" request maps. Snapshotted rather than
|
||||||
|
// referenced, which is precisely what makes glBindAttribLocation and friends
|
||||||
|
// legal to call over a pending link without cancelling it: the pending link keeps
|
||||||
|
// linking the inputs it was given.
|
||||||
|
UnorderedMap<String, Uint> explicitAttribLocations; // glBindAttribLocation
|
||||||
|
UnorderedMap<String, Uint> explicitFragDataLocation; // glBindFragDataLocation
|
||||||
|
UnorderedMap<String, Uint> explicitFragDataIndex; // glBindFragDataLocationIndexed
|
||||||
|
Vector<String> requestedXfbVaryings; // glTransformFeedbackVaryings
|
||||||
|
GLenum requestedXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||||
|
Int maxFragmentOutputColorNumber = 8; // GL_MAX_DRAW_BUFFERS, stamped in by the entry point
|
||||||
|
} in;
|
||||||
|
|
||||||
|
// ---- output: valid iff IsComplete(), immutable afterwards ----
|
||||||
|
// Moved (never copied) into the ProgramObject by EnsureLinkJoined().
|
||||||
|
ProgramObject::LinkArtifacts artifacts;
|
||||||
|
|
||||||
|
// Posts this job once every compile in `deps` is terminal - and not one moment
|
||||||
|
// earlier, so the body never waits on anything (invariant I4: no job body may block
|
||||||
|
// on another job, or the pool could deadlock with all its workers waiting on each
|
||||||
|
// other). `deps` is the subset of the snapshot's compile nodes that were still
|
||||||
|
// in flight; an already-terminal one needs no edge.
|
||||||
|
//
|
||||||
|
// GL thread only, and only after the caller has stored a SharedPtr to this node:
|
||||||
|
// OnDepSettled takes shared_from_this().
|
||||||
|
void SubmitAfter(const Vector<SharedPtr<ShaderCompileTask>>& deps);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void RunBody() override;
|
||||||
|
|
||||||
|
// Runs when one dependency goes terminal - on whichever thread drove it there, which
|
||||||
|
// is a pool worker for a compile that finished on one. Non-throwing by construction;
|
||||||
|
// see the definition.
|
||||||
|
void OnDepSettled();
|
||||||
|
|
||||||
|
// ---- the link body, split exactly as ProgramObject::Link() had it ----
|
||||||
|
// Each returns false to abort the link with `artifacts.infoLog` already set, which is
|
||||||
|
// GL's definition of a failed link: LINK_STATUS false plus a log, never a GL error.
|
||||||
|
Bool ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders);
|
||||||
|
Bool DoReflection(const MG_Util::ShaderTranspiler::CompileEnv& env);
|
||||||
|
Bool ValidateFragmentOutputLocations();
|
||||||
|
Bool ResolveTransformFeedbackVaryings();
|
||||||
|
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
|
||||||
|
void GenerateSpirv();
|
||||||
|
void BuildGlobalUboRouting();
|
||||||
|
|
||||||
|
// Worker-side MGLOG replacement: appended to diagnostics.logLines and replayed by the
|
||||||
|
// join, on the GL thread, where a serial implementation would have printed it.
|
||||||
|
// Logging straight from a worker interleaves mid-line with the GL thread's output and
|
||||||
|
// lands out of order relative to the glLinkProgram that caused it.
|
||||||
|
void DeferLog(String line);
|
||||||
|
|
||||||
|
// Counts down to zero exactly once. Starts at deps + 1: the extra guard is released
|
||||||
|
// by SubmitAfter itself, so a dependency that settles while the edges are still being
|
||||||
|
// registered cannot post the job from under a half-built dependency list.
|
||||||
|
std::atomic<Int> m_remainingDeps{0};
|
||||||
|
};
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -14,9 +14,22 @@
|
|||||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
// The link job. Only ever held by SharedPtr here, so a forward declaration is enough -
|
||||||
|
// ProgramLinkTask.h includes THIS header (it outputs a LinkArtifacts), so including it
|
||||||
|
// back would be circular. The destructor is therefore out of line.
|
||||||
|
class ProgramLinkTask;
|
||||||
|
|
||||||
class ProgramObject {
|
class ProgramObject {
|
||||||
public:
|
public:
|
||||||
ProgramObject(Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {}
|
ProgramObject(Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {}
|
||||||
|
// Cancel-not-join, exactly like ~ShaderObject: the link job owns its inputs, so an
|
||||||
|
// in-flight link whose program just went away is safe to abandon where it stands.
|
||||||
|
// Nothing can observe its result any more - this object was the only route to it.
|
||||||
|
// Out of line because ProgramLinkTask is incomplete here.
|
||||||
|
~ProgramObject();
|
||||||
|
ProgramObject(const ProgramObject&) = delete;
|
||||||
|
ProgramObject& operator=(const ProgramObject&) = delete;
|
||||||
|
|
||||||
bool ShaderIsAttached(const SharedPtr<ShaderObject>& shader);
|
bool ShaderIsAttached(const SharedPtr<ShaderObject>& shader);
|
||||||
// GL-visible attachment: in the attach list and not pending detach (glDetachShader
|
// GL-visible attachment: in the attach list and not pending detach (glDetachShader
|
||||||
// defers the actual removal to the next link).
|
// defers the actual removal to the next link).
|
||||||
@@ -44,31 +57,39 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
Vector<SharedPtr<ShaderObject>>& GetAttachedShaders();
|
Vector<SharedPtr<ShaderObject>>& GetAttachedShaders();
|
||||||
const Vector<SharedPtr<ShaderObject>>& GetAttachedShaders() const;
|
const Vector<SharedPtr<ShaderObject>>& GetAttachedShaders() const;
|
||||||
const String& GetInfoLog() const { return m_infoLog; }
|
const String& GetInfoLog() const { return Artifacts().infoLog; }
|
||||||
// glCreateShaderProgramv folds the shader's compile log into the program's log, which
|
// glCreateShaderProgramv folds the shader's compile log into the program's log, which
|
||||||
// is the only place a caller can read it from once the shader name is gone.
|
// is the only place a caller can read it from once the shader name is gone.
|
||||||
void AppendInfoLog(const String& text) {
|
void AppendInfoLog(const String& text) {
|
||||||
if (text.empty()) return;
|
if (text.empty()) return;
|
||||||
if (!m_infoLog.empty() && m_infoLog.back() != '\n') m_infoLog += '\n';
|
if (!Artifacts().infoLog.empty() && Artifacts().infoLog.back() != '\n') Artifacts().infoLog += '\n';
|
||||||
m_infoLog += text;
|
Artifacts().infoLog += text;
|
||||||
}
|
}
|
||||||
Int GetUniformMaxLength() const { return m_uniformNameMaxLength; }
|
Int GetUniformMaxLength() const { return Artifacts().uniformNameMaxLength; }
|
||||||
Uint GetUniformCount() const { return m_activeUniformCount; }
|
Uint GetUniformCount() const { return Artifacts().activeUniformCount; }
|
||||||
Uint GetMaxUniformLocation() const { return m_maxUniformLocation; }
|
Uint GetMaxUniformLocation() const { return Artifacts().maxUniformLocation; }
|
||||||
Int GetUniformLocation(const String& name) const {
|
Int GetUniformLocation(const String& name) const {
|
||||||
const auto it = m_uniformLocations.find(name);
|
const auto it = Artifacts().uniformLocations.find(name);
|
||||||
if (it != m_uniformLocations.end()) return (Int)it->second;
|
if (it != Artifacts().uniformLocations.end()) return (Int)it->second;
|
||||||
|
|
||||||
// Reflection stores GL-style names: an array uniform is keyed "arr[0]" (its base
|
// Reflection stores GL-style names: an array uniform is keyed "arr[0]" (its base
|
||||||
// location). A bare "arr" query resolves to that entry; an "arr[k]" query resolves
|
// location). A bare "arr" query resolves to that entry; an "arr[k]" query resolves
|
||||||
// to base + k because DoReflection reserves one location per array element.
|
// to base + k because DoReflection reserves one location per array element.
|
||||||
if (name.empty()) return -1;
|
if (name.empty()) return -1;
|
||||||
if (name.back() != ']') {
|
if (name.back() != ']') {
|
||||||
const auto suffixedIt = m_uniformLocations.find(name + "[0]");
|
const auto suffixedIt = Artifacts().uniformLocations.find(name + "[0]");
|
||||||
if (suffixedIt != m_uniformLocations.end()) return (Int)suffixedIt->second;
|
if (suffixedIt != Artifacts().uniformLocations.end()) return (Int)suffixedIt->second;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (name.length() < 4) return -1;
|
if (name.length() < 4) return -1;
|
||||||
|
// An array of arrays is keyed by its full "[0]"-terminated spelling
|
||||||
|
// ("a[2][1][0]"), so a query that already ends in a subscript may still be the
|
||||||
|
// NAME of an array rather than an element of one. Try that first; only then
|
||||||
|
// treat the trailing subscript as an element index.
|
||||||
|
{
|
||||||
|
const auto arrayOfArraysIt = Artifacts().uniformLocations.find(name + "[0]");
|
||||||
|
if (arrayOfArraysIt != Artifacts().uniformLocations.end()) return (Int)arrayOfArraysIt->second;
|
||||||
|
}
|
||||||
const SizeT bracket = name.rfind('[');
|
const SizeT bracket = name.rfind('[');
|
||||||
// Require at least one digit between the brackets.
|
// Require at least one digit between the brackets.
|
||||||
if (bracket == String::npos || bracket + 1 >= name.length() - 1) return -1;
|
if (bracket == String::npos || bracket + 1 >= name.length() - 1) return -1;
|
||||||
@@ -78,21 +99,21 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
element = element * 10 + static_cast<Uint>(name[i] - '0');
|
element = element * 10 + static_cast<Uint>(name[i] - '0');
|
||||||
if (element > 0x0FFFFFFFu) return -1;
|
if (element > 0x0FFFFFFFu) return -1;
|
||||||
}
|
}
|
||||||
auto baseIt = m_uniformLocations.find(name.substr(0, bracket) + "[0]");
|
auto baseIt = Artifacts().uniformLocations.find(name.substr(0, bracket) + "[0]");
|
||||||
if (baseIt == m_uniformLocations.end()) {
|
if (baseIt == Artifacts().uniformLocations.end()) {
|
||||||
// Legacy key without the "[0]" suffix (defensive; reflection normally
|
// Legacy key without the "[0]" suffix (defensive; reflection normally
|
||||||
// stores the suffixed form for arrays).
|
// stores the suffixed form for arrays).
|
||||||
baseIt = m_uniformLocations.find(name.substr(0, bracket));
|
baseIt = Artifacts().uniformLocations.find(name.substr(0, bracket));
|
||||||
if (baseIt == m_uniformLocations.end()) return -1;
|
if (baseIt == Artifacts().uniformLocations.end()) return -1;
|
||||||
}
|
}
|
||||||
const Int base = (Int)baseIt->second;
|
const Int base = (Int)baseIt->second;
|
||||||
if (!IsValidUniformLocation(base)) return -1;
|
if (!IsValidUniformLocation(base)) return -1;
|
||||||
const Int index = m_uniformIndexInTProgram[base];
|
const Int index = Artifacts().uniformIndexInTProgram[base];
|
||||||
// "[k]" only addresses arrays ("scalar[0]" is not a uniform name), and only
|
// "[k]" only addresses arrays ("scalar[0]" is not a uniform name), and only
|
||||||
// in-range elements.
|
// in-range elements.
|
||||||
const glslang::TType* type = m_program->getUniform(index).getType();
|
const glslang::TType* type = Artifacts().program->getUniform(index).getType();
|
||||||
if (type == nullptr || !type->isArray()) return -1;
|
if (type == nullptr || !type->isArray()) return -1;
|
||||||
if (static_cast<GLint>(element) >= GetActiveUniformArraySize(index)) return -1;
|
if (static_cast<GLint>(element) >= GetUniformArraySizeByTIndex(index)) return -1;
|
||||||
const Int location = base + (Int)element;
|
const Int location = base + (Int)element;
|
||||||
if (!UniformLocationsAliasSameUniform(base, location)) return -1;
|
if (!UniformLocationsAliasSameUniform(base, location)) return -1;
|
||||||
return location;
|
return location;
|
||||||
@@ -101,14 +122,41 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// True when both locations are element slots of the same uniform variable.
|
// True when both locations are element slots of the same uniform variable.
|
||||||
Bool UniformLocationsAliasSameUniform(Int a, Int b) const {
|
Bool UniformLocationsAliasSameUniform(Int a, Int b) const {
|
||||||
if (!IsValidUniformLocation(a) || !IsValidUniformLocation(b)) return false;
|
if (!IsValidUniformLocation(a) || !IsValidUniformLocation(b)) return false;
|
||||||
return m_uniformIndexInTProgram[a] == m_uniformIndexInTProgram[b];
|
return Artifacts().uniformIndexInTProgram[a] == Artifacts().uniformIndexInTProgram[b];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- GL index <-> glslang TProgram index translation ----
|
||||||
|
// The single relaxed parse enumerates artifacts GL must not see: every declared
|
||||||
|
// default-block uniform (even dead ones) as a member of the synthesized
|
||||||
|
// MGL_GLOBAL_UBO, and that block itself. DoReflection builds filtered GL-facing
|
||||||
|
// index spaces; every public "index"-taking getter translates through them, so
|
||||||
|
// GL and backend consumers keep seeing exactly the pre-P0a surface.
|
||||||
|
Int TProgramUniformIndex(Uint glIndex) const {
|
||||||
|
return Artifacts().glUniformIndexToTProgram[glIndex];
|
||||||
|
}
|
||||||
|
Int GlUniformIndexFromTProgram(Int tIndex) const {
|
||||||
|
if (tIndex < 0 || tIndex >= static_cast<Int>(Artifacts().tProgramUniformIndexToGl.size())) return -1;
|
||||||
|
return Artifacts().tProgramUniformIndexToGl[tIndex];
|
||||||
|
}
|
||||||
|
// GL uniform-block index -> glslang TProgram block index (the inverse of
|
||||||
|
// GlBlockIndexFromTProgram). The interface-query layer needs it to reach block
|
||||||
|
// properties glslang exposes but no typed getter here does.
|
||||||
|
Int TProgramBlockIndex(Uint glBlockIndex) const {
|
||||||
|
return glBlockIndex < Artifacts().glBlockIndexToTProgram.size()
|
||||||
|
? Artifacts().glBlockIndexToTProgram[glBlockIndex]
|
||||||
|
: -1;
|
||||||
|
}
|
||||||
|
Int GlBlockIndexFromTProgram(Int tBlockIndex) const {
|
||||||
|
if (tBlockIndex < 0 || tBlockIndex >= static_cast<Int>(Artifacts().tProgramBlockIndexToGl.size())) return -1;
|
||||||
|
return Artifacts().tProgramBlockIndexToGl[tBlockIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
Int GetActiveUniformIndex(const String& name) const {
|
Int GetActiveUniformIndex(const String& name) const {
|
||||||
const Int uniformIndex = m_program->getUniformIndex(name.c_str());
|
const Int tProgramCount = static_cast<Int>(Artifacts().tProgramUniformIndexToGl.size());
|
||||||
if (uniformIndex >= 0 && uniformIndex < m_activeUniformCount &&
|
const Int uniformIndex = Artifacts().program->getUniformIndex(name.c_str());
|
||||||
m_program->getUniform(uniformIndex).name == name) {
|
if (uniformIndex >= 0 && uniformIndex < tProgramCount &&
|
||||||
return uniformIndex;
|
Artifacts().program->getUniform(uniformIndex).name == name) {
|
||||||
|
return GlUniformIndexFromTProgram(uniformIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reflection stores an array uniform under "arr[0]"; accept the bare "arr"
|
// Reflection stores an array uniform under "arr[0]"; accept the bare "arr"
|
||||||
@@ -116,61 +164,60 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// robustness against non-suffixed reflection entries.
|
// robustness against non-suffixed reflection entries.
|
||||||
if (!name.empty() && name.back() != ']') {
|
if (!name.empty() && name.back() != ']') {
|
||||||
const String suffixedName = name + "[0]";
|
const String suffixedName = name + "[0]";
|
||||||
const Int suffixedIndex = m_program->getUniformIndex(suffixedName.c_str());
|
const Int suffixedIndex = Artifacts().program->getUniformIndex(suffixedName.c_str());
|
||||||
if (suffixedIndex >= 0 && suffixedIndex < m_activeUniformCount &&
|
if (suffixedIndex >= 0 && suffixedIndex < tProgramCount &&
|
||||||
m_program->getUniform(suffixedIndex).name == suffixedName) {
|
Artifacts().program->getUniform(suffixedIndex).name == suffixedName) {
|
||||||
return suffixedIndex;
|
return GlUniformIndexFromTProgram(suffixedIndex);
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name.length() <= 3 || name.compare(name.length() - 3, 3, "[0]") != 0) return -1;
|
if (name.length() <= 3 || name.compare(name.length() - 3, 3, "[0]") != 0) return -1;
|
||||||
const String baseName = name.substr(0, name.length() - 3);
|
const String baseName = name.substr(0, name.length() - 3);
|
||||||
const Int baseIndex = m_program->getUniformIndex(baseName.c_str());
|
const Int baseIndex = Artifacts().program->getUniformIndex(baseName.c_str());
|
||||||
if (baseIndex < 0 || baseIndex >= m_activeUniformCount) return -1;
|
if (baseIndex < 0 || baseIndex >= tProgramCount) return -1;
|
||||||
return m_program->getUniform(baseIndex).name == baseName ? baseIndex : -1;
|
return Artifacts().program->getUniform(baseIndex).name == baseName ? GlUniformIndexFromTProgram(baseIndex)
|
||||||
|
: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool IsValidUniformLocation(Int location) const {
|
Bool IsValidUniformLocation(Int location) const { return IsValidUniformLocation(Artifacts(), location); }
|
||||||
if (location < 0 || location > static_cast<Int>(m_maxUniformLocation)) return false;
|
|
||||||
if (static_cast<SizeT>(location) >= m_uniformIndexInTProgram.size()) return false;
|
|
||||||
const Int uniformIndexInProgram = m_uniformIndexInTProgram[location];
|
|
||||||
return uniformIndexInProgram != glslang::TQualifier::layoutLocationEnd &&
|
|
||||||
uniformIndexInProgram >= 0 && uniformIndexInProgram < m_activeUniformCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLenum GetUniformType(Uint location) const {
|
GLenum GetUniformType(Uint location) const {
|
||||||
auto& uniform = m_program->getUniform(m_uniformIndexInTProgram[location]);
|
auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]);
|
||||||
return uniform.glDefineType;
|
return uniform.glDefineType;
|
||||||
}
|
}
|
||||||
|
|
||||||
GLenum GetActiveUniformType(Uint index) const {
|
GLenum GetActiveUniformType(Uint index) const {
|
||||||
auto& uniform = m_program->getUniform(static_cast<Int>(index));
|
auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
|
||||||
return uniform.glDefineType;
|
return uniform.glDefineType;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Number of active array elements (GL_UNIFORM_SIZE / GL_ARRAY_SIZE); 1 for a non-array.
|
// Number of active array elements (GL_UNIFORM_SIZE / GL_ARRAY_SIZE); 1 for a non-array.
|
||||||
// glslang's TObjectReflection.size only carries the element count for a NON-block array; for
|
// glslang's TObjectReflection.size only carries the element count for a NON-block array; for
|
||||||
// a block array member it reports 1, so take the count from the TType, which is authoritative
|
// a block array member it reports 1, so take the count from the TType, which is authoritative
|
||||||
// for both. GL 3.3 core uniforms are always sized.
|
// for both. GL 3.3 core uniforms are always sized. Takes a TProgram uniform index (the space
|
||||||
|
// the artifacts' uniformIndexInTProgram stores).
|
||||||
|
GLint GetUniformArraySizeByTIndex(Int tIndex) const {
|
||||||
|
return GetUniformArraySizeByTIndex(Artifacts(), tIndex);
|
||||||
|
}
|
||||||
|
|
||||||
GLint GetActiveUniformArraySize(Uint index) const {
|
GLint GetActiveUniformArraySize(Uint index) const {
|
||||||
const auto& uniform = m_program->getUniform(static_cast<Int>(index));
|
return GetUniformArraySizeByTIndex(TProgramUniformIndex(index));
|
||||||
const glslang::TType* type = uniform.getType();
|
|
||||||
if (type != nullptr && type->isSizedArray()) {
|
|
||||||
return type->getOuterArraySize();
|
|
||||||
}
|
|
||||||
return uniform.size < 1 ? 1 : uniform.size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Int GetActiveUniformBlockIndex(Uint index) const {
|
Int GetActiveUniformBlockIndex(Uint index) const {
|
||||||
auto& uniform = m_program->getUniform(static_cast<Int>(index));
|
auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
|
||||||
return uniform.index;
|
// Members of the synthesized global UBO are default-block uniforms to GL: -1.
|
||||||
|
return GlBlockIndexFromTProgram(uniform.index);
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL_UNIFORM_OFFSET: byte offset within the owning named block. glslang already reports -1
|
// GL_UNIFORM_OFFSET: byte offset within the owning named block; -1 for a default-block
|
||||||
// for a default-block uniform, which is exactly the spec value there.
|
// uniform. The relaxed parse gives global-UBO members real byte offsets, but GL must keep
|
||||||
|
// seeing them as default-block uniforms, so gate on the GL-visible block index.
|
||||||
GLint GetActiveUniformOffset(Uint index) const {
|
GLint GetActiveUniformOffset(Uint index) const {
|
||||||
return m_program->getUniform(static_cast<Int>(index)).offset;
|
const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
|
||||||
|
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
|
||||||
|
return uniform.offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL_UNIFORM_ARRAY_STRIDE: byte stride of an array member in a named block; 0 for a non-array
|
// GL_UNIFORM_ARRAY_STRIDE: byte stride of an array member in a named block; 0 for a non-array
|
||||||
@@ -182,8 +229,8 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// generated SPIR-V lay the array out with std140 16-byte-rounded strides. MobileGL's UBO
|
// generated SPIR-V lay the array out with std140 16-byte-rounded strides. MobileGL's UBO
|
||||||
// layout is always std140, where every array element stride rounds up to a vec4.
|
// layout is always std140, where every array element stride rounds up to a vec4.
|
||||||
GLint GetActiveUniformArrayStride(Uint index) const {
|
GLint GetActiveUniformArrayStride(Uint index) const {
|
||||||
const auto& uniform = m_program->getUniform(static_cast<Int>(index));
|
const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
|
||||||
if (uniform.index < 0) return -1;
|
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
|
||||||
const glslang::TType* type = uniform.getType();
|
const glslang::TType* type = uniform.getType();
|
||||||
if (type == nullptr || !type->isArray()) return 0;
|
if (type == nullptr || !type->isArray()) return 0;
|
||||||
if (type->isMatrix()) {
|
if (type->isMatrix()) {
|
||||||
@@ -202,13 +249,13 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// check suffices; the getUniformBlock() fallback is defensive for a config that instead leaves
|
// check suffices; the getUniformBlock() fallback is defensive for a config that instead leaves
|
||||||
// an inheriting member's layoutMatrix == ElmNone.
|
// an inheriting member's layoutMatrix == ElmNone.
|
||||||
GLint GetActiveUniformIsRowMajor(Uint index) const {
|
GLint GetActiveUniformIsRowMajor(Uint index) const {
|
||||||
const auto& uniform = m_program->getUniform(static_cast<Int>(index));
|
const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
|
||||||
if (uniform.index < 0) return 0;
|
if (GlBlockIndexFromTProgram(uniform.index) < 0) return 0;
|
||||||
const glslang::TType* type = uniform.getType();
|
const glslang::TType* type = uniform.getType();
|
||||||
if (type == nullptr || !type->isMatrix()) return 0;
|
if (type == nullptr || !type->isMatrix()) return 0;
|
||||||
glslang::TLayoutMatrix layoutMatrix = type->getQualifier().layoutMatrix;
|
glslang::TLayoutMatrix layoutMatrix = type->getQualifier().layoutMatrix;
|
||||||
if (layoutMatrix == glslang::ElmNone) {
|
if (layoutMatrix == glslang::ElmNone) {
|
||||||
layoutMatrix = m_program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix;
|
layoutMatrix = Artifacts().program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix;
|
||||||
}
|
}
|
||||||
return (layoutMatrix == glslang::ElmRowMajor) ? 1 : 0;
|
return (layoutMatrix == glslang::ElmRowMajor) ? 1 : 0;
|
||||||
}
|
}
|
||||||
@@ -220,13 +267,13 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// out as std140 (packed/shared are coerced), so this matches the offsets glslang reports. For
|
// out as std140 (packed/shared are coerced), so this matches the offsets glslang reports. For
|
||||||
// every GL 3.3 float matrix this evaluates to 16, independent of majorness.
|
// every GL 3.3 float matrix this evaluates to 16, independent of majorness.
|
||||||
GLint GetActiveUniformMatrixStride(Uint index) const {
|
GLint GetActiveUniformMatrixStride(Uint index) const {
|
||||||
const auto& uniform = m_program->getUniform(static_cast<Int>(index));
|
const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
|
||||||
if (uniform.index < 0) return -1;
|
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
|
||||||
const glslang::TType* type = uniform.getType();
|
const glslang::TType* type = uniform.getType();
|
||||||
if (type == nullptr || !type->isMatrix()) return 0;
|
if (type == nullptr || !type->isMatrix()) return 0;
|
||||||
glslang::TLayoutMatrix layoutMatrix = type->getQualifier().layoutMatrix;
|
glslang::TLayoutMatrix layoutMatrix = type->getQualifier().layoutMatrix;
|
||||||
if (layoutMatrix == glslang::ElmNone) {
|
if (layoutMatrix == glslang::ElmNone) {
|
||||||
layoutMatrix = m_program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix;
|
layoutMatrix = Artifacts().program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix;
|
||||||
}
|
}
|
||||||
const bool rowMajor = (layoutMatrix == glslang::ElmRowMajor);
|
const bool rowMajor = (layoutMatrix == glslang::ElmRowMajor);
|
||||||
const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows();
|
const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows();
|
||||||
@@ -238,50 +285,50 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const glslang::TType* GetUniformTType(Uint location) const {
|
const glslang::TType* GetUniformTType(Uint location) const {
|
||||||
auto& uniform = m_program->getUniform(m_uniformIndexInTProgram[location]);
|
auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]);
|
||||||
return uniform.getType();
|
return uniform.getType();
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool IsUniformOpaqueAtLocation(Uint location) const { return GetUniformTType(location)->isOpaque(); }
|
Bool IsUniformOpaqueAtLocation(Uint location) const { return GetUniformTType(location)->isOpaque(); }
|
||||||
|
|
||||||
const String& GetUniformName(Uint location) const {
|
const String& GetUniformName(Uint location) const {
|
||||||
auto& uniform = m_program->getUniform(m_uniformIndexInTProgram[location]);
|
auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]);
|
||||||
return uniform.name;
|
return uniform.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
const String& GetActiveUniformName(Uint index) const {
|
const String& GetActiveUniformName(Uint index) const {
|
||||||
auto& uniform = m_program->getUniform(static_cast<Int>(index));
|
auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
|
||||||
return uniform.name;
|
return uniform.name;
|
||||||
}
|
}
|
||||||
// Sentinel for a uniform location without global-UBO backing storage (should not
|
// Sentinel for a uniform location without global-UBO backing storage (should not
|
||||||
// survive linking: GenerateBinary falls back to tail-allocated scratch storage).
|
// survive linking: GenerateBinary falls back to tail-allocated scratch storage).
|
||||||
static constexpr Uint kInvalidUniformOffset = ~0u;
|
static constexpr Uint kInvalidUniformOffset = ~0u;
|
||||||
Uint GetUniformOffset(Uint location) const { return m_uniformOffsets[location]; }
|
Uint GetUniformOffset(Uint location) const { return Artifacts().uniformOffsets[location]; }
|
||||||
Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); }
|
Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); }
|
||||||
|
|
||||||
Int GetAttributeLocation(const String& name) {
|
Int GetAttributeLocation(const String& name) {
|
||||||
const auto it = std::find(m_attribs.begin(), m_attribs.end(), name);
|
const auto it = std::find(Artifacts().attribs.begin(), Artifacts().attribs.end(), name);
|
||||||
return (it == m_attribs.end()) ? -1 : (Int)std::distance(m_attribs.begin(), it);
|
return (it == Artifacts().attribs.end()) ? -1 : (Int)std::distance(Artifacts().attribs.begin(), it);
|
||||||
}
|
}
|
||||||
Uint32 GetActiveAttributeLocationMask() const {
|
Uint32 GetActiveAttributeLocationMask() const {
|
||||||
Uint32 mask = 0;
|
Uint32 mask = 0;
|
||||||
const SizeT count = std::min<SizeT>(m_attribs.size(), 32);
|
const SizeT count = std::min<SizeT>(Artifacts().attribs.size(), 32);
|
||||||
for (SizeT index = 0; index < count; ++index) {
|
for (SizeT index = 0; index < count; ++index) {
|
||||||
if (!m_attribs[index].empty()) {
|
if (!Artifacts().attribs[index].empty()) {
|
||||||
mask |= (1u << index);
|
mask |= (1u << index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return mask;
|
return mask;
|
||||||
}
|
}
|
||||||
Uint32 GetActiveFragmentOutputLocationMask() const {
|
Uint32 GetActiveFragmentOutputLocationMask() const {
|
||||||
if (!m_program) {
|
if (!Artifacts().program) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint32 mask = 0;
|
Uint32 mask = 0;
|
||||||
const Int outputCount = m_program->getNumPipeOutputs();
|
const Int outputCount = Artifacts().program->getNumPipeOutputs();
|
||||||
for (Int index = 0; index < outputCount; ++index) {
|
for (Int index = 0; index < outputCount; ++index) {
|
||||||
const Int location = static_cast<Int>(m_program->getPipeOutput(index).layoutLocation());
|
const Int location = static_cast<Int>(Artifacts().program->getPipeOutput(index).layoutLocation());
|
||||||
if (location >= 0 && location < 32) {
|
if (location >= 0 && location < 32) {
|
||||||
mask |= (1u << location);
|
mask |= (1u << location);
|
||||||
}
|
}
|
||||||
@@ -289,47 +336,59 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return mask;
|
return mask;
|
||||||
}
|
}
|
||||||
Int GetActiveFragmentOutputCount() const {
|
Int GetActiveFragmentOutputCount() const {
|
||||||
return m_program ? m_program->getNumPipeOutputs() : 0;
|
return Artifacts().program ? Artifacts().program->getNumPipeOutputs() : 0;
|
||||||
}
|
}
|
||||||
const String& GetActiveFragmentOutputName(Uint index) const {
|
const String& GetActiveFragmentOutputName(Uint index) const {
|
||||||
MOBILEGL_ASSERT(m_program != nullptr, "ProgramObject::GetActiveFragmentOutputName: program is null");
|
MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetActiveFragmentOutputName: program is null");
|
||||||
MOBILEGL_ASSERT(index < static_cast<Uint>(m_program->getNumPipeOutputs()),
|
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
|
||||||
"ProgramObject::GetActiveFragmentOutputName: index=%u out of range", index);
|
"ProgramObject::GetActiveFragmentOutputName: index=%u out of range", index);
|
||||||
return m_program->getPipeOutput(static_cast<Int>(index)).name;
|
return Artifacts().program->getPipeOutput(static_cast<Int>(index)).name;
|
||||||
}
|
}
|
||||||
Int GetFragmentOutputLocation(Uint index) const {
|
Int GetFragmentOutputLocation(Uint index) const {
|
||||||
MOBILEGL_ASSERT(m_program != nullptr, "ProgramObject::GetFragmentOutputLocation: program is null");
|
MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetFragmentOutputLocation: program is null");
|
||||||
MOBILEGL_ASSERT(index < static_cast<Uint>(m_program->getNumPipeOutputs()),
|
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
|
||||||
"ProgramObject::GetFragmentOutputLocation: index=%u out of range",
|
"ProgramObject::GetFragmentOutputLocation: index=%u out of range",
|
||||||
index);
|
index);
|
||||||
return static_cast<Int>(m_program->getPipeOutput(static_cast<Int>(index)).layoutLocation());
|
return static_cast<Int>(Artifacts().program->getPipeOutput(static_cast<Int>(index)).layoutLocation());
|
||||||
}
|
}
|
||||||
GLint GetActiveFragmentOutputArraySize(Uint index) const {
|
GLint GetActiveFragmentOutputArraySize(Uint index) const {
|
||||||
MOBILEGL_ASSERT(m_program != nullptr, "ProgramObject::GetActiveFragmentOutputArraySize: program is null");
|
MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetActiveFragmentOutputArraySize: program is null");
|
||||||
MOBILEGL_ASSERT(index < static_cast<Uint>(m_program->getNumPipeOutputs()),
|
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
|
||||||
"ProgramObject::GetActiveFragmentOutputArraySize: index=%u out of range", index);
|
"ProgramObject::GetActiveFragmentOutputArraySize: index=%u out of range", index);
|
||||||
return m_program->getPipeOutput(static_cast<Int>(index)).size;
|
return Artifacts().program->getPipeOutput(static_cast<Int>(index)).size;
|
||||||
}
|
}
|
||||||
GLenum GetFragmentOutputType(Uint index) const {
|
GLenum GetFragmentOutputType(Uint index) const {
|
||||||
MOBILEGL_ASSERT(m_program != nullptr, "ProgramObject::GetFragmentOutputType: program is null");
|
MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetFragmentOutputType: program is null");
|
||||||
MOBILEGL_ASSERT(index < static_cast<Uint>(m_program->getNumPipeOutputs()),
|
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
|
||||||
"ProgramObject::GetFragmentOutputType: index=%u out of range",
|
"ProgramObject::GetFragmentOutputType: index=%u out of range",
|
||||||
index);
|
index);
|
||||||
return m_program->getPipeOutput(static_cast<Int>(index)).glDefineType;
|
return Artifacts().program->getPipeOutput(static_cast<Int>(index)).glDefineType;
|
||||||
}
|
}
|
||||||
GLenum GetAttribType(Uint index) const { return m_attribTypes[index]; }
|
GLenum GetAttribType(Uint index) const { return Artifacts().attribTypes[index]; }
|
||||||
const String& GetAttribName(Uint index) const { return m_attribs[index]; }
|
const String& GetAttribName(Uint index) const { return Artifacts().attribs[index]; }
|
||||||
GLenum GetActiveAttribType(Uint index) const { return m_program->getPipeInput(static_cast<Int>(index)).glDefineType; }
|
GLenum GetActiveAttribType(Uint index) const { return Artifacts().program->getPipeInput(static_cast<Int>(index)).glDefineType; }
|
||||||
GLint GetActiveAttribArraySize(Uint index) const { return m_program->getPipeInput(static_cast<Int>(index)).size; }
|
GLint GetActiveAttribArraySize(Uint index) const { return Artifacts().program->getPipeInput(static_cast<Int>(index)).size; }
|
||||||
const String& GetActiveAttribName(Uint index) const { return m_program->getPipeInput(static_cast<Int>(index)).name; }
|
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V names;
|
||||||
void* MapUBO() { return m_globalUboScratch.data(); }
|
// GL must keep reporting the GL spellings (glGetActiveAttrib and the program-input
|
||||||
const void* GetUBOData() const { return m_globalUboScratch.data(); }
|
// resource queries enumerate builtins).
|
||||||
Uint GetUBOSize() const { return static_cast<Uint>(m_globalUboScratch.size()); }
|
static const String& NormalizeBuiltinPipeInputName(const String& name) {
|
||||||
|
static const String kGlVertexId = "gl_VertexID";
|
||||||
|
static const String kGlInstanceId = "gl_InstanceID";
|
||||||
|
if (name == "gl_VertexIndex") return kGlVertexId;
|
||||||
|
if (name == "gl_InstanceIndex") return kGlInstanceId;
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
const String& GetActiveAttribName(Uint index) const {
|
||||||
|
return NormalizeBuiltinPipeInputName(Artifacts().program->getPipeInput(static_cast<Int>(index)).name);
|
||||||
|
}
|
||||||
|
void* MapUBO() { return Artifacts().globalUboScratch.data(); }
|
||||||
|
const void* GetUBOData() const { return Artifacts().globalUboScratch.data(); }
|
||||||
|
Uint GetUBOSize() const { return static_cast<Uint>(Artifacts().globalUboScratch.size()); }
|
||||||
// Content version of the CPU-side global-UBO shadow: writers bump it so backends
|
// Content version of the CPU-side global-UBO shadow: writers bump it so backends
|
||||||
// can skip re-uploading an unchanged UBO on every draw. ~0u is reserved as the
|
// can skip re-uploading an unchanged UBO on every draw. ~0u is reserved as the
|
||||||
// backends' "never uploaded" sentinel, so skip over it on wrap.
|
// backends' "never uploaded" sentinel, so skip over it on wrap.
|
||||||
Uint32 GetUBOContentVersion() const { return m_uboContentVersion; }
|
Uint32 GetUBOContentVersion() const { return m_uboContentVersion; }
|
||||||
void MarkUBOContentDirty() {
|
void MarkUBOContentDirty() const {
|
||||||
if (++m_uboContentVersion == ~0u) m_uboContentVersion = 0;
|
if (++m_uboContentVersion == ~0u) m_uboContentVersion = 0;
|
||||||
}
|
}
|
||||||
Uint32 GetBackendStateVersion() const { return m_backendStateVersion; }
|
Uint32 GetBackendStateVersion() const { return m_backendStateVersion; }
|
||||||
@@ -370,20 +429,20 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SetUniformSamplerOrImageUnitIndex(Uint location, Int unit) {
|
void SetUniformSamplerOrImageUnitIndex(Uint location, Int unit) {
|
||||||
if (location >= m_uniformSamplerOrImageUnitIndex.size() ||
|
if (location >= Artifacts().uniformSamplerOrImageUnitIndex.size() ||
|
||||||
m_uniformSamplerOrImageUnitIndex[location] == unit) {
|
Artifacts().uniformSamplerOrImageUnitIndex[location] == unit) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_uniformSamplerOrImageUnitIndex[location] = unit;
|
Artifacts().uniformSamplerOrImageUnitIndex[location] = unit;
|
||||||
++m_backendStateVersion;
|
++m_backendStateVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
Int GetUniformSamplerOrImageUnitIndex(Uint location) const {
|
Int GetUniformSamplerOrImageUnitIndex(Uint location) const {
|
||||||
return m_uniformSamplerOrImageUnitIndex[location];
|
return Artifacts().uniformSamplerOrImageUnitIndex[location];
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool GetDeleteStatus() const { return m_deleteStatus; }
|
Bool GetDeleteStatus() const { return m_deleteStatus; }
|
||||||
Bool GetLinkStatus() const { return m_linkStatus; }
|
Bool GetLinkStatus() const { return Artifacts().linkStatus; }
|
||||||
// GL_PROGRAM_BINARY_RETRIEVABLE_HINT. MobileGL exposes no program binary format
|
// GL_PROGRAM_BINARY_RETRIEVABLE_HINT. MobileGL exposes no program binary format
|
||||||
// (GL_NUM_PROGRAM_BINARY_FORMATS is 0), so the hint is pure state - which is all
|
// (GL_NUM_PROGRAM_BINARY_FORMATS is 0), so the hint is pure state - which is all
|
||||||
// ARB_get_program_binary requires of it.
|
// ARB_get_program_binary requires of it.
|
||||||
@@ -397,24 +456,32 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// glProgramBinary always fails here (there is no format it could accept) and the
|
// glProgramBinary always fails here (there is no format it could accept) and the
|
||||||
// spec then requires the program's LINK_STATUS to read FALSE.
|
// spec then requires the program's LINK_STATUS to read FALSE.
|
||||||
void MarkLinkFailedByProgramBinary() {
|
void MarkLinkFailedByProgramBinary() {
|
||||||
ResetLinkArtifacts();
|
// Before anything reads m_artifacts: a pending link would otherwise publish its
|
||||||
m_infoLog = "No program binary format is supported.";
|
// (possibly successful) result over the failure this call is required to install
|
||||||
|
// - and Artifacts() below would be the thing that let it. Cancel-not-join: GL
|
||||||
|
// gives glProgramBinary no reason to wait for a link it is about to invalidate.
|
||||||
|
CancelLink();
|
||||||
|
BumpLinkObservableVersions();
|
||||||
|
ResetLinkArtifacts(Artifacts());
|
||||||
|
Artifacts().infoLog = "No program binary format is supported.";
|
||||||
}
|
}
|
||||||
Bool GetValidateStatus() const { return m_validateStatus; }
|
Bool GetValidateStatus() const { return m_validateStatus; }
|
||||||
Int GetActiveAtomicCounterCount() const { return m_program->getNumAtomicCounters(); }
|
Int GetActiveAtomicCounterCount() const { return Artifacts().program->getNumAtomicCounters(); }
|
||||||
Int GetActiveAttributesCount() const { return m_program->getNumPipeInputs(); }
|
Int GetActiveAttributesCount() const { return Artifacts().program->getNumPipeInputs(); }
|
||||||
Int GetActiveUniformBlocksCount() const { return m_program->getNumUniformBlocks(); }
|
// GL-visible uniform blocks only: the synthesized MGL_GLOBAL_UBO the relaxed parse
|
||||||
GLuint GetComputeLocalSize(Uint dim) const { return m_program->getLocalSize(static_cast<Int>(dim)); }
|
// materializes for default-block uniforms is filtered out by DoReflection.
|
||||||
Int GetActiveAttributesMaxLength() const { return m_attribInNameMaxLength; }
|
Int GetActiveUniformBlocksCount() const { return static_cast<Int>(Artifacts().glBlockIndexToTProgram.size()); }
|
||||||
Int GetActiveUniformBlocksMaxNameLength() const { return m_uniformBlockNameMaxLength; }
|
GLuint GetComputeLocalSize(Uint dim) const { return Artifacts().program->getLocalSize(static_cast<Int>(dim)); }
|
||||||
|
Int GetActiveAttributesMaxLength() const { return Artifacts().attribInNameMaxLength; }
|
||||||
|
Int GetActiveUniformBlocksMaxNameLength() const { return Artifacts().uniformBlockNameMaxLength; }
|
||||||
Uint GetUniformBlockIndex(const char* name) const {
|
Uint GetUniformBlockIndex(const char* name) const {
|
||||||
auto it = m_uniformBlockIndexByName.find(name);
|
auto it = Artifacts().uniformBlockIndexByName.find(name);
|
||||||
if (it != m_uniformBlockIndexByName.end()) return it->second;
|
if (it != Artifacts().uniformBlockIndexByName.end()) return it->second;
|
||||||
// Instances of an arrayed block are reflected as "Block[0]".."Block[N-1]";
|
// Instances of an arrayed block are reflected as "Block[0]".."Block[N-1]";
|
||||||
// a bare "Block" query resolves to the first instance per GL semantics.
|
// a bare "Block" query resolves to the first instance per GL semantics.
|
||||||
const String suffixedName = String(name) + "[0]";
|
const String suffixedName = String(name) + "[0]";
|
||||||
it = m_uniformBlockIndexByName.find(suffixedName);
|
it = Artifacts().uniformBlockIndexByName.find(suffixedName);
|
||||||
if (it != m_uniformBlockIndexByName.end()) return it->second;
|
if (it != Artifacts().uniformBlockIndexByName.end()) return it->second;
|
||||||
return 0xFFFFFFFFu; // GL_INVALID_INDEX
|
return 0xFFFFFFFFu; // GL_INVALID_INDEX
|
||||||
}
|
}
|
||||||
Bool IsActiveUniformBlock(Uint index) const {
|
Bool IsActiveUniformBlock(Uint index) const {
|
||||||
@@ -427,11 +494,11 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// (like a std140 struct) occupies a vec4-rounded size, and that is what the
|
// (like a std140 struct) occupies a vec4-rounded size, and that is what the
|
||||||
// backend compiles: ES drivers reject draws whose bound UBO range is smaller
|
// backend compiles: ES drivers reject draws whose bound UBO range is smaller
|
||||||
// than the block (a block ending in ivec3 reported 12 while the driver needs 16).
|
// than the block (a block ending in ivec3 reported 12 while the driver needs 16).
|
||||||
return (m_program->getUniformBlock((Int)index).size + 15u) & ~15u;
|
return (Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[index]).size + 15u) & ~15u;
|
||||||
}
|
}
|
||||||
|
|
||||||
const String& GetUniformBlockName(Uint index) const {
|
const String& GetUniformBlockName(Uint index) const {
|
||||||
auto& ubo = m_program->getUniformBlock((Int)index);
|
auto& ubo = Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[index]);
|
||||||
return ubo.name;
|
return ubo.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,8 +510,8 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
if (name.empty() || name.back() != ']') return index;
|
if (name.empty() || name.back() != ']') return index;
|
||||||
const SizeT bracket = name.rfind('[');
|
const SizeT bracket = name.rfind('[');
|
||||||
if (bracket == String::npos) return index;
|
if (bracket == String::npos) return index;
|
||||||
const auto it = m_uniformBlockIndexByName.find(name.substr(0, bracket) + "[0]");
|
const auto it = Artifacts().uniformBlockIndexByName.find(name.substr(0, bracket) + "[0]");
|
||||||
if (it != m_uniformBlockIndexByName.end()) return it->second;
|
if (it != Artifacts().uniformBlockIndexByName.end()) return it->second;
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,31 +522,64 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
Int GetUniformBlockActiveUniformCount(Uint index) const {
|
Int GetUniformBlockActiveUniformCount(Uint index) const {
|
||||||
const Int ownerIndex = static_cast<Int>(GetUniformBlockMemberOwnerIndex(index));
|
const Int ownerIndex = static_cast<Int>(GetUniformBlockMemberOwnerIndex(index));
|
||||||
Int count = 0;
|
Int count = 0;
|
||||||
for (Uint uniformIndex = 0; uniformIndex < m_activeUniformCount; ++uniformIndex) {
|
for (Uint uniformIndex = 0; uniformIndex < Artifacts().activeUniformCount; ++uniformIndex) {
|
||||||
if (GetActiveUniformBlockIndex(uniformIndex) == ownerIndex) ++count;
|
if (GetActiveUniformBlockIndex(uniformIndex) == ownerIndex) ++count;
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool IsUniformBlockReferencedByStage(Uint index, EShLanguage stage) const {
|
Bool IsUniformBlockReferencedByStage(Uint index, EShLanguage stage) const {
|
||||||
const auto& ubo = m_program->getUniformBlock((Int)index);
|
const auto& ubo = Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[index]);
|
||||||
const auto stageMask = static_cast<EShLanguageMask>(1 << stage);
|
const auto stageMask = static_cast<EShLanguageMask>(1 << stage);
|
||||||
return (ubo.stages & stageMask) != 0;
|
return (ubo.stages & stageMask) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set by glUniformBlockBinding
|
// Set by glUniformBlockBinding
|
||||||
void SetUniformBlockBinding(Uint index, Uint binding) {
|
void SetUniformBlockBinding(Uint index, Uint binding) {
|
||||||
if (index >= m_uniformBlockBinding.size() || m_uniformBlockBinding[index] == static_cast<Int>(binding)) {
|
if (index >= Artifacts().uniformBlockBinding.size() || Artifacts().uniformBlockBinding[index] == static_cast<Int>(binding)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_uniformBlockBinding[index] = static_cast<Int>(binding);
|
Artifacts().uniformBlockBinding[index] = static_cast<Int>(binding);
|
||||||
++m_backendStateVersion;
|
++m_backendStateVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint GetUniformBlockBinding(Uint index) const { return m_uniformBlockBinding[index]; }
|
Uint GetUniformBlockBinding(Uint index) const { return Artifacts().uniformBlockBinding[index]; }
|
||||||
|
|
||||||
Vector<Vector<unsigned>>& GetGeneratedSpirv() { return m_generatedSpirv; }
|
// Set by glShaderStorageBlockBinding, keyed by the block's GL name rather than by any
|
||||||
const Vector<Vector<unsigned>>& GetGeneratedSpirv() const { return m_generatedSpirv; }
|
// index. A shader storage block has THREE index spaces - the frontend interface-query
|
||||||
|
// enumeration, DirectVulkan's SPIR-V descriptor order and DirectGLES's real-driver
|
||||||
|
// order - and the name is the only coordinate all three agree on. Absent from the map
|
||||||
|
// means "never rebound", and the shader's declared binding still stands.
|
||||||
|
void SetShaderStorageBlockBinding(const String& blockName, Uint binding) {
|
||||||
|
Artifacts().shaderStorageBlockBinding[blockName] = static_cast<Int>(binding);
|
||||||
|
}
|
||||||
|
// -1 when the block has never been rebound. `blockName` is the interface-query
|
||||||
|
// spelling; an arrayed block's elements ("B[0]", "B[1]") are separate GL resources
|
||||||
|
// with separate bindings, so they are separate keys.
|
||||||
|
Int GetShaderStorageBlockBindingOverride(const String& blockName) const {
|
||||||
|
const auto it = Artifacts().shaderStorageBlockBinding.find(blockName);
|
||||||
|
if (it != Artifacts().shaderStorageBlockBinding.end()) return it->second;
|
||||||
|
// A backend that collapses an arrayed block down to one resource knows it only by
|
||||||
|
// the bare block name; answer that with element zero's binding.
|
||||||
|
const auto zeroth = Artifacts().shaderStorageBlockBinding.find(blockName + "[0]");
|
||||||
|
return zeroth != Artifacts().shaderStorageBlockBinding.end() ? zeroth->second : -1;
|
||||||
|
}
|
||||||
|
// Every rebinding recorded so far, for a backend that has to REPLAY them onto a
|
||||||
|
// driver program it just (re)built. Empty for the overwhelming majority of programs -
|
||||||
|
// check .empty() before doing any per-block work.
|
||||||
|
const UnorderedMap<String, Int>& GetShaderStorageBlockBindingOverrides() const {
|
||||||
|
return Artifacts().shaderStorageBlockBinding;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector<Vector<unsigned>>& GetGeneratedSpirv() { return Artifacts().generatedSpirv; }
|
||||||
|
const Vector<Vector<unsigned>>& GetGeneratedSpirv() const { return Artifacts().generatedSpirv; }
|
||||||
|
|
||||||
|
// The linked glslang reflection itself, for the ONE consumer that needs resource
|
||||||
|
// lists no typed getter above exposes: the GL program-interface query layer
|
||||||
|
// (MG_Impl/GLImpl/Program/ProgramInterface.cpp), which has to enumerate buffer
|
||||||
|
// blocks, buffer variables, atomic counters and per-stage reference masks. Null
|
||||||
|
// until a link has succeeded. Read through the join gate like everything else.
|
||||||
|
const glslang::TProgram* GetReflection() const { return Artifacts().program.get(); }
|
||||||
|
|
||||||
Int GetShaderIndexByStage(ShaderStage stage) const {
|
Int GetShaderIndexByStage(ShaderStage stage) const {
|
||||||
auto it = std::find_if(m_shaders.begin(), m_shaders.end(), [stage](const SharedPtr<ShaderObject>& shader) {
|
auto it = std::find_if(m_shaders.begin(), m_shaders.end(), [stage](const SharedPtr<ShaderObject>& shader) {
|
||||||
@@ -501,41 +601,202 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// layout captures into; see NeedsScatteredTransformFeedbackCapture.
|
// layout captures into; see NeedsScatteredTransformFeedbackCapture.
|
||||||
Uint32 packedOffsetBytes = 0;
|
Uint32 packedOffsetBytes = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---- P1: everything a link PRODUCES, in one movable block ----
|
||||||
|
//
|
||||||
|
// The membership rule is mechanical, not editorial: this is exactly the field list
|
||||||
|
// ResetLinkArtifacts() clears (plus the four it forgot to - infoLog,
|
||||||
|
// linkedFragDataLocation/Index and the geometry strip-capture pair - which are just
|
||||||
|
// as much link output). Nothing else belongs here.
|
||||||
|
//
|
||||||
|
// Why a struct: once glLinkProgram runs on a worker (P1 stage 4) the worker writes
|
||||||
|
// its OWN LinkArtifacts and the GL thread publishes it with a single move, instead
|
||||||
|
// of thirty cross-thread field assignments. Until then this is a pure refactor.
|
||||||
|
//
|
||||||
|
// Access rule (invariant I5): the member below is private and reachable ONLY
|
||||||
|
// through ProgramObject::Artifacts(), which calls EnsureLinkJoined() first. That is
|
||||||
|
// what makes "every read of link output joins the pending link" a property the
|
||||||
|
// compiler checks rather than a review item - a new reader cannot spell the field
|
||||||
|
// without going through the gate.
|
||||||
|
struct LinkArtifacts {
|
||||||
|
SharedPtr<glslang::TProgram> program;
|
||||||
|
Vector<Vector<unsigned>> generatedSpirv;
|
||||||
|
|
||||||
|
// Attributes (Vertex in)
|
||||||
|
Vector<String> attribs;
|
||||||
|
Vector<GLenum> attribTypes;
|
||||||
|
|
||||||
|
// FragData (Frag out): the per-link snapshot of the explicit request maps.
|
||||||
|
UnorderedMap<String, Uint> linkedFragDataLocation;
|
||||||
|
UnorderedMap<String, Uint> linkedFragDataIndex;
|
||||||
|
|
||||||
|
// GL-facing index spaces (see the translation helpers above): GL active-uniform
|
||||||
|
// index <-> glslang TProgram uniform index, GL uniform-block index <-> TProgram
|
||||||
|
// block index. -1 marks a TProgram entry GL does not expose (dead default-block
|
||||||
|
// uniforms swept into MGL_GLOBAL_UBO by the relaxed parse, and that block itself).
|
||||||
|
Vector<Int> glUniformIndexToTProgram;
|
||||||
|
Vector<Int> tProgramUniformIndexToGl;
|
||||||
|
Vector<Int> glBlockIndexToTProgram;
|
||||||
|
Vector<Int> tProgramBlockIndexToGl;
|
||||||
|
// Per-link merged snapshot of the attached shaders' lexically extracted
|
||||||
|
// layout(location = N) default-block uniform qualifiers (the relaxed parse drops
|
||||||
|
// them from reflection; the DoReflection assigner restores them from here).
|
||||||
|
UnorderedMap<String, Int> linkedExplicitUniformLocations;
|
||||||
|
UnorderedMap<String, Uint> uniformLocations;
|
||||||
|
// Ordered by location,
|
||||||
|
// aka. uniformIndexInTProgram[loc] == "uniform index of TProgram at location `loc`"
|
||||||
|
Vector<Int> uniformIndexInTProgram;
|
||||||
|
// ditto. Will be set at glUniform1i
|
||||||
|
Vector<Int> uniformSamplerOrImageUnitIndex;
|
||||||
|
UnorderedMap<String, Uint> explicitOpaqueUniformBindings;
|
||||||
|
|
||||||
|
// Ordered by uniform block index
|
||||||
|
// index is DIFFERENT from binding!!!
|
||||||
|
//
|
||||||
|
// Let's define UniformBlockIndex == the order at glslang getUniformBlock()
|
||||||
|
// aka `i = glGetUniformBlockIndex(prog, "BlockName")` implies:
|
||||||
|
// `prog->getUniformBlock(i) == "BlockName"`
|
||||||
|
// These stuff are present for GL semantics, not for backend inspection
|
||||||
|
// These may change after-link (because GL spec decided to have `glUniformBlockBinding`)
|
||||||
|
UnorderedMap<String, Uint> uniformBlockIndexByName;
|
||||||
|
Vector<Int> uniformBlockBinding;
|
||||||
|
// glShaderStorageBlockBinding overrides, keyed by GL block name. See
|
||||||
|
// SetShaderStorageBlockBinding for why this one is by name and not by index.
|
||||||
|
UnorderedMap<String, Int> shaderStorageBlockBinding;
|
||||||
|
|
||||||
|
// Need to be reflected after linking of SPIR-V binary
|
||||||
|
Vector<Uint> uniformOffsets;
|
||||||
|
Vector<Uint> uniformSizesInBytes;
|
||||||
|
Vector<Uint8> globalUboScratch;
|
||||||
|
|
||||||
|
Uint activeUniformCount = 0;
|
||||||
|
Uint maxUniformLocation = 0;
|
||||||
|
Int uniformNameMaxLength = 0;
|
||||||
|
Int attribInNameMaxLength = 0;
|
||||||
|
Int uniformBlockNameMaxLength = 0;
|
||||||
|
|
||||||
|
String infoLog;
|
||||||
|
Bool linkStatus = false;
|
||||||
|
|
||||||
|
// Transform feedback: the linked snapshot (the request lives outside, on the
|
||||||
|
// GL-thread-owned side).
|
||||||
|
Vector<XfbVarying> xfbVaryings;
|
||||||
|
// The glTransformFeedbackVaryings request list exactly as this link consumed it,
|
||||||
|
// INCLUDING the gl_NextBuffer / gl_SkipComponentsN pseudo-varyings that
|
||||||
|
// xfbVaryings deliberately drops (they steer the capture layout and must never
|
||||||
|
// reach a backend's varying list). GL_TRANSFORM_FEEDBACK_VARYING enumerates the
|
||||||
|
// full request, pseudo-varyings and all, so the interface query needs its own copy.
|
||||||
|
Vector<String> xfbInterfaceNames;
|
||||||
|
Vector<Uint32> xfbStrides;
|
||||||
|
Vector<Uint32> gsStripTriangles;
|
||||||
|
Bool gsStripCaptureFixup = false;
|
||||||
|
GLenum gsInputPrimitive = GL_NONE;
|
||||||
|
GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||||
|
Int xfbVaryingNameMaxLength = 0;
|
||||||
|
Bool xfbNeedsScatteredCapture = false;
|
||||||
|
Uint32 xfbPackedStride = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- artifacts-only helpers, shared with ProgramLinkTask ----
|
||||||
|
// Static and taking the block explicitly, because from stage 4 the link BODY needs
|
||||||
|
// them while its artifacts still live on the job node, not on any ProgramObject. The
|
||||||
|
// member overloads above are the same functions read through the join gate.
|
||||||
|
|
||||||
|
// Clears every field one link produces, EXCEPT infoLog, linkedFragDataLocation/Index
|
||||||
|
// and the geometry strip-capture pair. That exception is load-bearing: the callers
|
||||||
|
// that survive (glProgramBinary's mandated failure, and the link body's own mid-link
|
||||||
|
// aborts) write infoLog immediately AFTER calling here. Link()'s prologue does not
|
||||||
|
// use this at all - it assigns a whole default-constructed LinkArtifacts, where the
|
||||||
|
// ordering is explicit and nothing is exempt.
|
||||||
|
static void ResetLinkArtifacts(LinkArtifacts& artifacts);
|
||||||
|
|
||||||
|
static Bool IsValidUniformLocation(const LinkArtifacts& artifacts, Int location) {
|
||||||
|
if (location < 0 || location > static_cast<Int>(artifacts.maxUniformLocation)) return false;
|
||||||
|
if (static_cast<SizeT>(location) >= artifacts.uniformIndexInTProgram.size()) return false;
|
||||||
|
const Int uniformIndexInProgram = artifacts.uniformIndexInTProgram[location];
|
||||||
|
return uniformIndexInProgram != glslang::TQualifier::layoutLocationEnd &&
|
||||||
|
uniformIndexInProgram >= 0 &&
|
||||||
|
uniformIndexInProgram < static_cast<Int>(artifacts.tProgramUniformIndexToGl.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number of active array elements (GL_UNIFORM_SIZE / GL_ARRAY_SIZE); 1 for a non-array.
|
||||||
|
// glslang's TObjectReflection.size only carries the element count for a NON-block array; for
|
||||||
|
// a block array member it reports 1, so take the count from the TType, which is authoritative
|
||||||
|
// for both. GL 3.3 core uniforms are always sized. Takes a TProgram uniform index (the space
|
||||||
|
// the artifacts' uniformIndexInTProgram stores).
|
||||||
|
static GLint GetUniformArraySizeByTIndex(const LinkArtifacts& artifacts, Int tIndex) {
|
||||||
|
const auto& uniform = artifacts.program->getUniform(tIndex);
|
||||||
|
const glslang::TType* type = uniform.getType();
|
||||||
|
if (type != nullptr && type->isSizedArray()) {
|
||||||
|
return type->getOuterArraySize();
|
||||||
|
}
|
||||||
|
return uniform.size < 1 ? 1 : uniform.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blocks until a pending link has published its artifacts. Public because a few call
|
||||||
|
// sites have to join without reading anything - see the explicit-join list (J1-J8) in
|
||||||
|
// the P1 design. GL thread only.
|
||||||
|
void JoinLink() const { EnsureLinkJoined(); }
|
||||||
|
|
||||||
|
// Drops a link that is still in flight, without waiting for it. Called at the points
|
||||||
|
// where the pending link's result stops being the answer to "what did this program
|
||||||
|
// link to": a re-link supersedes it, glProgramBinary must force LINK_STATUS false,
|
||||||
|
// and a destroyed program has no observers left.
|
||||||
|
//
|
||||||
|
// Deliberately NOT called by the "takes effect at the next link" setters
|
||||||
|
// (glBindAttribLocation, glBindFragDataLocation(Indexed), glTransformFeedbackVaryings,
|
||||||
|
// glProgramParameteri) NOR by glAttachShader/glDetachShader. Every one of those is
|
||||||
|
// defined by GL to leave the CURRENT link result alone, and the pending link already
|
||||||
|
// snapshotted its own inputs at enqueue, so it is computing exactly the answer GL
|
||||||
|
// requires. Cancelling on any of them would make
|
||||||
|
// glLinkProgram(p); <setter>; glGetProgramiv(p, GL_LINK_STATUS)
|
||||||
|
// report FALSE for a link that succeeded - and for the attach/detach pair it would
|
||||||
|
// additionally break glCreateShaderProgramv, which detaches immediately after linking.
|
||||||
|
void CancelLink();
|
||||||
|
|
||||||
|
// MUST NOT JOIN - this is what GL_COMPLETION_STATUS_KHR reads when the extension
|
||||||
|
// surface lands. "No job at all" counts as complete: there is nothing outstanding to
|
||||||
|
// wait for.
|
||||||
|
Bool IsLinkComplete() const { return m_pendingLink == nullptr || IsPendingLinkTerminal(); }
|
||||||
|
|
||||||
void SetTransformFeedbackVaryings(Vector<String>&& names, GLenum bufferMode) {
|
void SetTransformFeedbackVaryings(Vector<String>&& names, GLenum bufferMode) {
|
||||||
m_requestedXfbVaryings = Move(names);
|
m_requestedXfbVaryings = Move(names);
|
||||||
m_requestedXfbBufferMode = bufferMode;
|
m_requestedXfbBufferMode = bufferMode;
|
||||||
}
|
}
|
||||||
GLenum GetTransformFeedbackBufferMode() const { return m_xfbBufferMode; }
|
GLenum GetTransformFeedbackBufferMode() const { return Artifacts().xfbBufferMode; }
|
||||||
SizeT GetTransformFeedbackVaryingCount() const { return m_xfbVaryings.size(); }
|
SizeT GetTransformFeedbackVaryingCount() const { return Artifacts().xfbVaryings.size(); }
|
||||||
const XfbVarying* GetTransformFeedbackVarying(SizeT index) const {
|
const XfbVarying* GetTransformFeedbackVarying(SizeT index) const {
|
||||||
return index < m_xfbVaryings.size() ? &m_xfbVaryings[index] : nullptr;
|
return index < Artifacts().xfbVaryings.size() ? &Artifacts().xfbVaryings[index] : nullptr;
|
||||||
}
|
}
|
||||||
const Vector<XfbVarying>& GetTransformFeedbackVaryings() const { return m_xfbVaryings; }
|
const Vector<XfbVarying>& GetTransformFeedbackVaryings() const { return Artifacts().xfbVaryings; }
|
||||||
|
// The GL_TRANSFORM_FEEDBACK_VARYING resource list: every name the last successful
|
||||||
|
// link was asked to capture, in request order, pseudo-varyings included.
|
||||||
|
const Vector<String>& GetTransformFeedbackInterfaceNames() const { return Artifacts().xfbInterfaceNames; }
|
||||||
// Stride of one captured vertex in the given capture buffer slot.
|
// Stride of one captured vertex in the given capture buffer slot.
|
||||||
Uint32 GetTransformFeedbackStride(Uint32 bufferIndex) const {
|
Uint32 GetTransformFeedbackStride(Uint32 bufferIndex) const {
|
||||||
return bufferIndex < m_xfbStrides.size() ? m_xfbStrides[bufferIndex] : 0;
|
return bufferIndex < Artifacts().xfbStrides.size() ? Artifacts().xfbStrides[bufferIndex] : 0;
|
||||||
}
|
}
|
||||||
SizeT GetTransformFeedbackBufferCount() const { return m_xfbStrides.size(); }
|
SizeT GetTransformFeedbackBufferCount() const { return Artifacts().xfbStrides.size(); }
|
||||||
Int GetTransformFeedbackVaryingMaxLength() const { return m_xfbVaryingNameMaxLength; }
|
Int GetTransformFeedbackVaryingMaxLength() const { return Artifacts().xfbVaryingNameMaxLength; }
|
||||||
// True when the capture layout uses gl_SkipComponents / gl_NextBuffer
|
// True when the capture layout uses gl_SkipComponents / gl_NextBuffer
|
||||||
// (ARB_transform_feedback3), which no ES driver can express: it can only pack every
|
// (ARB_transform_feedback3), which no ES driver can express: it can only pack every
|
||||||
// captured varying into one record with no gaps. A backend that captures through
|
// captured varying into one record with no gaps. A backend that captures through
|
||||||
// such a driver has to capture into scratch storage and scatter the records into the
|
// such a driver has to capture into scratch storage and scatter the records into the
|
||||||
// application's buffers itself, using packedOffsetBytes as the source offset and
|
// application's buffers itself, using packedOffsetBytes as the source offset and
|
||||||
// (bufferIndex, offsetBytes, stride) as the destination.
|
// (bufferIndex, offsetBytes, stride) as the destination.
|
||||||
Bool NeedsScatteredTransformFeedbackCapture() const { return m_xfbNeedsScatteredCapture; }
|
Bool NeedsScatteredTransformFeedbackCapture() const { return Artifacts().xfbNeedsScatteredCapture; }
|
||||||
// Bytes one gap-free captured record occupies.
|
// Bytes one gap-free captured record occupies.
|
||||||
Uint32 GetTransformFeedbackPackedStride() const { return m_xfbPackedStride; }
|
Uint32 GetTransformFeedbackPackedStride() const { return Artifacts().xfbPackedStride; }
|
||||||
// True when the capture stage is a triangle-strip geometry shader with a
|
// True when the capture stage is a triangle-strip geometry shader with a
|
||||||
// statically-known emit sequence: the Vulkan capture order then needs the GL
|
// statically-known emit sequence: the Vulkan capture order then needs the GL
|
||||||
// odd-triangle vertex swap after EndTransformFeedback.
|
// odd-triangle vertex swap after EndTransformFeedback.
|
||||||
Bool HasGsTriangleStripCaptureFixup() const { return m_gsStripCaptureFixup; }
|
Bool HasGsTriangleStripCaptureFixup() const { return Artifacts().gsStripCaptureFixup; }
|
||||||
// Triangles per strip, in emission order, for ONE geometry invocation.
|
// Triangles per strip, in emission order, for ONE geometry invocation.
|
||||||
const Vector<Uint32>& GetGsStripTriangles() const { return m_gsStripTriangles; }
|
const Vector<Uint32>& GetGsStripTriangles() const { return Artifacts().gsStripTriangles; }
|
||||||
// GL_GEOMETRY_INPUT_TYPE of the linked geometry stage (GL_POINTS, GL_LINES,
|
// GL_GEOMETRY_INPUT_TYPE of the linked geometry stage (GL_POINTS, GL_LINES,
|
||||||
// GL_LINES_ADJACENCY, GL_TRIANGLES or GL_TRIANGLES_ADJACENCY), or GL_NONE when the
|
// GL_LINES_ADJACENCY, GL_TRIANGLES or GL_TRIANGLES_ADJACENCY), or GL_NONE when the
|
||||||
// program has no geometry stage. Draws must present a compatible primitive type.
|
// program has no geometry stage. Draws must present a compatible primitive type.
|
||||||
GLenum GetGeometryInputType() const { return m_gsInputPrimitive; }
|
GLenum GetGeometryInputType() const { return Artifacts().gsInputPrimitive; }
|
||||||
|
|
||||||
Uint GetExternalIndex() const { return m_externalIndex; }
|
Uint GetExternalIndex() const { return m_externalIndex; }
|
||||||
// Globally-unique, never-reused id for this program object's lifetime. Unlike the GL
|
// Globally-unique, never-reused id for this program object's lifetime. Unlike the GL
|
||||||
@@ -545,81 +806,74 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
Uint64 GetLifetimeId() const { return m_lifetimeId; }
|
Uint64 GetLifetimeId() const { return m_lifetimeId; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void ResetLinkArtifacts();
|
// ---- The one and only join gate for link output (P1 invariant I5) ----
|
||||||
void DoReflection();
|
// Blocks until a pending link has finished and its LinkArtifacts have been
|
||||||
// Resolves the requested transform feedback varyings against the linked
|
// published into m_artifacts. It exists so that the ~120 readers of link output are
|
||||||
// vertex stage; fails the link (GL semantics) on unknown or duplicate
|
// routed through it by the compiler rather than by review: m_artifacts is private
|
||||||
// names or exceeded capture limits.
|
// and Artifacts() is the only spelling that reaches it.
|
||||||
Bool ResolveTransformFeedbackVaryings();
|
//
|
||||||
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
|
// The fast path - no pending link - is one predictable branch and stays inline: it
|
||||||
void GenerateBinary();
|
// runs on every Artifacts() read (~1200 call sites project-wide) and the project
|
||||||
void WaitUntilGenerationCompleted() const;
|
// never builds with LTO (MOBILEGL_ENABLE_LTO=OFF), so an out-of-line body would be a
|
||||||
|
// real cross-TU call at every one of them. The blocking half is out of line.
|
||||||
|
void EnsureLinkJoined() const {
|
||||||
|
if (m_pendingLink) JoinPendingLink();
|
||||||
|
}
|
||||||
|
void JoinPendingLink() const;
|
||||||
|
// ProgramLinkTask is incomplete here, so IsLinkComplete()'s non-joining peek at the
|
||||||
|
// node's state goes through this out-of-line helper.
|
||||||
|
Bool IsPendingLinkTerminal() const;
|
||||||
|
|
||||||
|
LinkArtifacts& Artifacts() {
|
||||||
|
EnsureLinkJoined();
|
||||||
|
return m_artifacts;
|
||||||
|
}
|
||||||
|
const LinkArtifacts& Artifacts() const {
|
||||||
|
EnsureLinkJoined();
|
||||||
|
return m_artifacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL-thread-only companion to ResetLinkArtifacts (see its definition). Const because
|
||||||
|
// the publish half of the join calls it; see the mutable counters below.
|
||||||
|
void BumpLinkObservableVersions() const;
|
||||||
void AddDefaultFragmentShaderIfMissing();
|
void AddDefaultFragmentShaderIfMissing();
|
||||||
Bool ValidateFragmentOutputLocations();
|
|
||||||
|
|
||||||
static Uint64 AllocateLifetimeId();
|
static Uint64 AllocateLifetimeId();
|
||||||
|
|
||||||
|
// ---- GL-thread-owned state: never joins ----
|
||||||
|
// Most of this is never produced by a link at all. The three version counters
|
||||||
|
// (m_backendStateVersion / m_uboContentVersion / m_linkVersion) ARE
|
||||||
|
// link-observable, but they are bumped exclusively on the GL thread
|
||||||
|
// (BumpLinkObservableVersions in Link()'s prologue and glProgramBinary's
|
||||||
|
// failure path) - the link BODY, which stage 4 moves to a worker, never
|
||||||
|
// writes them.
|
||||||
const Uint m_externalIndex = 0;
|
const Uint m_externalIndex = 0;
|
||||||
const Uint64 m_lifetimeId = 0;
|
const Uint64 m_lifetimeId = 0;
|
||||||
|
// The attach lists are mutated only in Link()'s GL-thread prologue, which is why
|
||||||
|
// glGetAttachedShaders / GL_ATTACHED_SHADERS / the orphan-shader sweep need no join.
|
||||||
Vector<SharedPtr<ShaderObject>> m_shaders;
|
Vector<SharedPtr<ShaderObject>> m_shaders;
|
||||||
Vector<SharedPtr<ShaderObject>> m_detachedShaders; // Store detached shaders and remove on next link
|
Vector<SharedPtr<ShaderObject>> m_detachedShaders; // Store detached shaders and remove on next link
|
||||||
|
|
||||||
SharedPtr<glslang::TProgram> m_program;
|
// Link INPUTS (all "take effect at the next link" per GL): glBindAttribLocation,
|
||||||
|
// glBindFragDataLocation(Indexed), glTransformFeedbackVaryings, and the draw-buffer
|
||||||
Vector<Vector<unsigned>> m_generatedSpirv;
|
// count stamped in by the entry point. A pending link snapshots these at enqueue.
|
||||||
|
|
||||||
// Attributes (Vertex in)
|
|
||||||
UnorderedMap<String, Uint> m_explicitAttribLocations;
|
UnorderedMap<String, Uint> m_explicitAttribLocations;
|
||||||
Vector<String> m_attribs;
|
|
||||||
Vector<GLenum> m_attribTypes;
|
|
||||||
|
|
||||||
// FragData (Frag out)
|
|
||||||
UnorderedMap<String, Uint> m_explicitFragDataLocation;
|
UnorderedMap<String, Uint> m_explicitFragDataLocation;
|
||||||
UnorderedMap<String, Uint> m_linkedFragDataLocation;
|
|
||||||
// Dual-source blend color index per output name (glBindFragDataLocationIndexed); snapshotted
|
// Dual-source blend color index per output name (glBindFragDataLocationIndexed); snapshotted
|
||||||
// into the linked map at link time, like the location maps above.
|
// into the linked map at link time, like the location maps above.
|
||||||
UnorderedMap<String, Uint> m_explicitFragDataIndex;
|
UnorderedMap<String, Uint> m_explicitFragDataIndex;
|
||||||
UnorderedMap<String, Uint> m_linkedFragDataIndex;
|
|
||||||
Int m_maxFragmentOutputColorNumber = 8;
|
Int m_maxFragmentOutputColorNumber = 8;
|
||||||
|
Vector<String> m_requestedXfbVaryings;
|
||||||
|
GLenum m_requestedXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||||
|
|
||||||
// Uniforms
|
|
||||||
UnorderedMap<String, Uint> m_uniformLocations;
|
|
||||||
// Ordered by location,
|
|
||||||
// aka. m_uniformIndexInTProgram[loc] == "uniform index of TProgram at location `loc`"
|
|
||||||
Vector<Int> m_uniformIndexInTProgram;
|
|
||||||
// ditto. Will be set at glUniform1i
|
|
||||||
Vector<Int> m_uniformSamplerOrImageUnitIndex;
|
|
||||||
UnorderedMap<String, Uint> m_explicitOpaqueUniformBindings;
|
|
||||||
|
|
||||||
// Ordered by uniform block index
|
|
||||||
// index is DIFFERENT from binding!!!
|
|
||||||
//
|
|
||||||
// Let's define UniformBlockIndex == the order at glslang getUniformBlock()
|
|
||||||
// aka `i = glGetUniformBlockIndex(prog, "BlockName")` implies:
|
|
||||||
// `prog->getUniformBlock(i) == "BlockName"`
|
|
||||||
// These stuff are present for GL semantics, not for backend inspection
|
|
||||||
// These may change after-link (because GL spec decided to have `glUniformBlockBinding`)
|
|
||||||
UnorderedMap<String, Uint> m_uniformBlockIndexByName;
|
|
||||||
Vector<Int> m_uniformBlockBinding;
|
|
||||||
|
|
||||||
// Need to be reflected after linking of SPIR-V binary
|
|
||||||
Vector<Uint> m_uniformOffsets;
|
|
||||||
Vector<Uint> m_uniformSizesInBytes;
|
|
||||||
Vector<Uint8> m_globalUboScratch;
|
|
||||||
|
|
||||||
Uint m_activeUniformCount = 0;
|
|
||||||
Uint m_maxUniformLocation = 0;
|
|
||||||
Int m_uniformNameMaxLength = 0;
|
|
||||||
Int m_attribInNameMaxLength = 0;
|
|
||||||
Int m_uniformBlockNameMaxLength = 0;
|
|
||||||
|
|
||||||
String m_infoLog;
|
|
||||||
Bool m_deleteStatus = false;
|
Bool m_deleteStatus = false;
|
||||||
Bool m_linkStatus = false;
|
|
||||||
Bool m_binaryRetrievableHint = false;
|
Bool m_binaryRetrievableHint = false;
|
||||||
Bool m_separable = false;
|
Bool m_separable = false;
|
||||||
Bool m_validateStatus = true;
|
Bool m_validateStatus = true;
|
||||||
Uint32 m_backendStateVersion = 0;
|
// Mutable, like m_artifacts and for the same reason: publishing a pending link is a
|
||||||
|
// READ-side operation (the first gated getter is what pulls the result in), and the
|
||||||
|
// publish has to bump these. Still GL-thread-only - a worker never touches them.
|
||||||
|
mutable Uint32 m_backendStateVersion = 0;
|
||||||
|
|
||||||
// Backend-owned content-hash memo (see GetBackendHashMemo): valid only while
|
// Backend-owned content-hash memo (see GetBackendHashMemo): valid only while
|
||||||
// m_backendStateVersion matches. Several slots, not one: a backend may resolve the same
|
// m_backendStateVersion matches. Several slots, not one: a backend may resolve the same
|
||||||
@@ -635,20 +889,20 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
mutable Array<BackendHashMemoSlot, kBackendHashMemoSlotCount> m_backendHashMemoSlots{};
|
mutable Array<BackendHashMemoSlot, kBackendHashMemoSlotCount> m_backendHashMemoSlots{};
|
||||||
mutable SizeT m_backendHashMemoNextSlot = 0;
|
mutable SizeT m_backendHashMemoNextSlot = 0;
|
||||||
mutable Uint32 m_backendHashMemoVersion = ~0u;
|
mutable Uint32 m_backendHashMemoVersion = ~0u;
|
||||||
Uint32 m_uboContentVersion = 0;
|
mutable Uint32 m_uboContentVersion = 0;
|
||||||
Uint32 m_linkVersion = 0;
|
mutable Uint32 m_linkVersion = 0;
|
||||||
|
|
||||||
// Transform feedback: request (applies at next link) and linked snapshot.
|
// ---- Link OUTPUT ----
|
||||||
Vector<String> m_requestedXfbVaryings;
|
// Written by the link and by the post-link setters GL allows (glUniform1i's sampler
|
||||||
GLenum m_requestedXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
// unit, glUniformBlockBinding). Reachable only through Artifacts(); see LinkArtifacts.
|
||||||
Vector<XfbVarying> m_xfbVaryings;
|
//
|
||||||
Vector<Uint32> m_xfbStrides;
|
// Mutable because publishing is a READ-side operation: a const getter has to be able
|
||||||
Vector<Uint32> m_gsStripTriangles;
|
// to settle an outstanding link before answering it.
|
||||||
Bool m_gsStripCaptureFixup = false;
|
mutable LinkArtifacts m_artifacts;
|
||||||
GLenum m_gsInputPrimitive = GL_NONE;
|
|
||||||
GLenum m_xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
// The link job, from enqueue until the first observable read pulls its result. Null
|
||||||
Int m_xfbVaryingNameMaxLength = 0;
|
// means m_artifacts is already the answer - which is the state every reader outside
|
||||||
Bool m_xfbNeedsScatteredCapture = false;
|
// the pending window sees, and the whole reason the gate above is one branch.
|
||||||
Uint32 m_xfbPackedStride = 0;
|
mutable SharedPtr<ProgramLinkTask> m_pendingLink;
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_State::GLState
|
} // namespace MobileGL::MG_State::GLState
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
Uint ProgramState::CreateProgram() {
|
Uint ProgramState::CreateProgram() {
|
||||||
Uint programId = 0;
|
Uint programId = 0;
|
||||||
m_programIndexGenerator.Generate(1, &programId);
|
m_programShaderNameGenerator.Generate(1, &programId);
|
||||||
EnsureIndexAvail(programId, m_programObjects);
|
EnsureIndexAvail(programId, m_programObjects);
|
||||||
auto programObject = MakeShared<ProgramObject>(programId);
|
auto programObject = MakeShared<ProgramObject>(programId);
|
||||||
if (programObject == nullptr) return 0;
|
if (programObject == nullptr) return 0;
|
||||||
@@ -39,11 +39,19 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
void ProgramState::DestroyProgramSlot(const Uint program) {
|
void ProgramState::DestroyProgramSlot(const Uint program) {
|
||||||
auto& programObject = m_programObjects[program];
|
auto& programObject = m_programObjects[program];
|
||||||
|
// P1 join site J4/J5 (glDeleteProgram, and the deferred destroy UseProgram performs
|
||||||
|
// when a deletion-flagged program stops being current). The program's name is about
|
||||||
|
// to go, so nothing can observe its link any more: cancel-not-join, so a delete never
|
||||||
|
// blocks the GL thread on a worker. Explicit rather than left to ~ProgramObject,
|
||||||
|
// because the reset below only destroys the object if this table held the last
|
||||||
|
// reference - a program still bound as current, or still referenced by a pipeline,
|
||||||
|
// outlives it, and its link should stop the moment the name does.
|
||||||
|
programObject->CancelLink();
|
||||||
// Snapshot the attachments: deleting the program is a detach point for shaders
|
// Snapshot the attachments: deleting the program is a detach point for shaders
|
||||||
// that were flagged with glDeleteShader while still attached.
|
// that were flagged with glDeleteShader while still attached.
|
||||||
const Vector<SharedPtr<ShaderObject>> attachedShaders = programObject->GetAttachedShaders();
|
const Vector<SharedPtr<ShaderObject>> attachedShaders = programObject->GetAttachedShaders();
|
||||||
programObject.reset();
|
programObject.reset();
|
||||||
m_programIndexGenerator.Delete(program);
|
m_programShaderNameGenerator.Delete(program);
|
||||||
for (const auto& shader : attachedShaders) {
|
for (const auto& shader : attachedShaders) {
|
||||||
const Uint shaderName = shader->GetExternalIndex();
|
const Uint shaderName = shader->GetExternalIndex();
|
||||||
if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) {
|
if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) {
|
||||||
@@ -77,9 +85,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
Uint ProgramState::CreateShader(ShaderStage stage) {
|
Uint ProgramState::CreateShader(ShaderStage stage) {
|
||||||
Uint shaderId = 0;
|
Uint shaderId = 0;
|
||||||
m_shaderIndexGenerator.Generate(1, &shaderId);
|
m_programShaderNameGenerator.Generate(1, &shaderId);
|
||||||
EnsureIndexAvail(shaderId, m_shaderObjects);
|
EnsureIndexAvail(shaderId, m_shaderObjects);
|
||||||
auto shaderObject = MakeShared<ShaderObject>(stage, shaderId);
|
auto shaderObject =
|
||||||
|
MakeShared<ShaderObject>(stage, shaderId, m_shaderPreprocessCache, m_shaderCompileAdoptionMap);
|
||||||
if (shaderObject == nullptr) return 0;
|
if (shaderObject == nullptr) return 0;
|
||||||
m_shaderObjects[shaderId] = shaderObject;
|
m_shaderObjects[shaderId] = shaderObject;
|
||||||
return shaderId;
|
return shaderId;
|
||||||
@@ -91,6 +100,31 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return m_shaderObjects[shader];
|
return m_shaderObjects[shader];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ProgramState::JoinAllPendingWork() {
|
||||||
|
// Programs first: a link joins the compiles it depends on, so the shader pass that
|
||||||
|
// follows finds most of them already settled. The reverse order would be correct but
|
||||||
|
// would wait on each compile twice - once here, once inside the link's own prologue.
|
||||||
|
//
|
||||||
|
// A copy of each slot rather than a reference into the vector, and an index rather
|
||||||
|
// than an iterator: publishing a link replays deferred diagnostics, which reach
|
||||||
|
// pGLContext->RecordError. That does not touch these tables today, but it is a sink
|
||||||
|
// that can grow, and a reallocation underneath this loop would be a use-after-free
|
||||||
|
// that only shows up on the one GL call that walks the whole table. The copy costs a
|
||||||
|
// refcount bump on a path a mode switch takes at most once.
|
||||||
|
for (SizeT i = 0; i < m_programObjects.size(); ++i) {
|
||||||
|
const SharedPtr<ProgramObject> program = m_programObjects[i];
|
||||||
|
if (program) program->JoinLink();
|
||||||
|
}
|
||||||
|
for (SizeT i = 0; i < m_shaderObjects.size(); ++i) {
|
||||||
|
const SharedPtr<ShaderObject> shader = m_shaderObjects[i];
|
||||||
|
if (shader) shader->JoinCompile();
|
||||||
|
}
|
||||||
|
// The currently-used program is reachable through m_programObjects unless
|
||||||
|
// glDeleteProgram already freed its slot while it stayed current. Nothing else holds
|
||||||
|
// a GL-visible name for it, but a draw would still join it, so settle it here too.
|
||||||
|
if (m_currentProgram) m_currentProgram->JoinLink();
|
||||||
|
}
|
||||||
|
|
||||||
void ProgramState::MarkShaderObjectForDeletion(Uint shader) {
|
void ProgramState::MarkShaderObjectForDeletion(Uint shader) {
|
||||||
if (!CheckIndexAvail(shader, m_shaderObjects)) return;
|
if (!CheckIndexAvail(shader, m_shaderObjects)) return;
|
||||||
auto& shaderObject = m_shaderObjects[shader];
|
auto& shaderObject = m_shaderObjects[shader];
|
||||||
@@ -120,8 +154,15 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
auto& shaderObject = m_shaderObjects[shader];
|
auto& shaderObject = m_shaderObjects[shader];
|
||||||
if (shaderObject == nullptr || !shaderObject->GetDeleteStatus()) return;
|
if (shaderObject == nullptr || !shaderObject->GetDeleteStatus()) return;
|
||||||
if (ShaderHasGLVisibleAttachment(shaderObject)) return;
|
if (ShaderHasGLVisibleAttachment(shaderObject)) return;
|
||||||
|
// The name is about to go, so nothing can observe this shader's compile through THIS
|
||||||
|
// object any more and a job still in flight for it is pure waste - unless another
|
||||||
|
// shader object adopted the same node (stage 6) or a pending link pinned it, which is
|
||||||
|
// exactly what ReleaseCompileNode weighs before it cancels anything. Cancel-not-join
|
||||||
|
// either way: the job owns its inputs, so dropping the object out from under it is
|
||||||
|
// safe and the GL thread never blocks on a delete.
|
||||||
|
shaderObject->ReleaseCompileNode();
|
||||||
shaderObject.reset();
|
shaderObject.reset();
|
||||||
m_shaderIndexGenerator.Delete(shader);
|
m_programShaderNameGenerator.Delete(shader);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ProgramState::ValidateShaderObject(Uint shader) const {
|
Bool ProgramState::ValidateShaderObject(Uint shader) const {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
#include <MG_Util/Miscellany/IndexGenerator.h>
|
#include <MG_Util/Miscellany/IndexGenerator.h>
|
||||||
#include "ProgramObject.h"
|
#include "ProgramObject.h"
|
||||||
|
#include "ShaderCompileAdoptionMap.h"
|
||||||
|
#include "ShaderPreprocessCache.h"
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
class ProgramState {
|
class ProgramState {
|
||||||
@@ -33,6 +35,29 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
const SharedPtr<ProgramObject>& GetCurrentProgram() const { return m_currentProgram; }
|
const SharedPtr<ProgramObject>& GetCurrentProgram() const { return m_currentProgram; }
|
||||||
|
|
||||||
|
// Joins every outstanding compile and link this context still owns, publishing each
|
||||||
|
// one's artifacts through the ordinary gates. The single caller is
|
||||||
|
// glMaxShaderCompilerThreadsKHR(0): GL_KHR_parallel_shader_compile requires a zero
|
||||||
|
// count to leave nothing in flight, so that every subsequent
|
||||||
|
// GL_COMPLETION_STATUS_KHR reads GL_TRUE.
|
||||||
|
//
|
||||||
|
// NOT a teardown path and NOT ShaderCompilePool::StopAndDrain(): the pool keeps its
|
||||||
|
// threads and stays usable, because a later nonzero count has to bring asynchronous
|
||||||
|
// compilation straight back. Nodes belonging to objects this context has already
|
||||||
|
// dropped are not joined - nothing can observe them, and waiting on them would make
|
||||||
|
// a GL call's cost depend on garbage.
|
||||||
|
void JoinAllPendingWork();
|
||||||
|
|
||||||
|
// P0b layer 2. Exposed for tests and diagnostics; the GL frontend never touches it
|
||||||
|
// directly - shader objects reach it through the pointer they are handed at
|
||||||
|
// CreateShader().
|
||||||
|
ShaderPreprocessCache& GetShaderPreprocessCache() { return *m_shaderPreprocessCache; }
|
||||||
|
|
||||||
|
// P1 stage 6, same deal: exposed for tests and diagnostics only. Its adoption counter
|
||||||
|
// is the one number that says how many glCompileShader calls this context turned into
|
||||||
|
// no work at all; nothing in the GL frontend branches on it.
|
||||||
|
ShaderCompileAdoptionMap& GetShaderCompileAdoptionMap() { return *m_shaderCompileAdoptionMap; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Bool ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const;
|
Bool ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const;
|
||||||
// Frees the name slot and releases orphaned attached shaders; the immediate half
|
// Frees the name slot and releases orphaned attached shaders; the immediate half
|
||||||
@@ -52,10 +77,24 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
vec.resize(idx + 1);
|
vec.resize(idx + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
IndexGenerator<Uint> m_programIndexGenerator;
|
// Programs and shaders share one GL name space (GL 3.3 core 2.11: a shader
|
||||||
Vector<SharedPtr<ProgramObject>> m_programObjects;
|
// name passed where a program is expected must be recognized as a shader and
|
||||||
|
// rejected with INVALID_OPERATION, and vice versa). One generator for both
|
||||||
|
// object kinds keeps the names disjoint; the object tables stay separate.
|
||||||
|
IndexGenerator<Uint> m_programShaderNameGenerator;
|
||||||
|
|
||||||
IndexGenerator<Uint> m_shaderIndexGenerator;
|
// P0b layer 2: every shader object created here is handed shared ownership of this
|
||||||
|
// cache, so its lifetime no longer depends on member destruction order (P1: an
|
||||||
|
// in-flight compile job may outlive the context). The FIRST-member declaration is
|
||||||
|
// kept anyway - it costs nothing and documents the intent.
|
||||||
|
SharedPtr<ShaderPreprocessCache> m_shaderPreprocessCache = MakeShared<ShaderPreprocessCache>();
|
||||||
|
// P1 stage 6: the GL-thread-only index of adoptable compile nodes. Shared ownership
|
||||||
|
// for the same reason as the cache above - a ShaderObject held by a ProgramObject can
|
||||||
|
// outlive these tables, and its destructor releases a node - though unlike the cache
|
||||||
|
// no worker ever sees this one, which is why it carries no lock.
|
||||||
|
SharedPtr<ShaderCompileAdoptionMap> m_shaderCompileAdoptionMap = MakeShared<ShaderCompileAdoptionMap>();
|
||||||
|
|
||||||
|
Vector<SharedPtr<ProgramObject>> m_programObjects;
|
||||||
Vector<SharedPtr<ShaderObject>> m_shaderObjects;
|
Vector<SharedPtr<ShaderObject>> m_shaderObjects;
|
||||||
|
|
||||||
SharedPtr<ProgramObject> m_currentProgram;
|
SharedPtr<ProgramObject> m_currentProgram;
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ShaderCompileAdoptionMap.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 "ShaderCompileAdoptionMap.h"
|
||||||
|
|
||||||
|
#include "ShaderCompileTask.h"
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
SharedPtr<ShaderCompileTask> ShaderCompileAdoptionMap::FindAdoptable(const ShaderStage stage,
|
||||||
|
const Uint64 sourceHash, const String& source,
|
||||||
|
const Uint64 envFingerprint) {
|
||||||
|
const ShaderSourceKey key{.stage = stage,
|
||||||
|
.sourceHash = sourceHash,
|
||||||
|
.sourceLength = source.length(),
|
||||||
|
.envFingerprint = envFingerprint};
|
||||||
|
|
||||||
|
const auto it = m_entries.find(key);
|
||||||
|
if (it == m_entries.end()) return nullptr;
|
||||||
|
|
||||||
|
SharedPtr<ShaderCompileTask> node = it->second.lock();
|
||||||
|
// Expired (every shader object that held it has released it), settled as Cancelled
|
||||||
|
// (the enqueue lost a race with teardown, or the body threw), or CANCELLATION
|
||||||
|
// REQUESTED but not yet settled (a releaser fired Cancel() while a worker was still
|
||||||
|
// inside RunBody(), so the node is stuck at Running until the body returns - see
|
||||||
|
// JobNode::Run: once m_cancelled is set, the node is DOOMED to end up Cancelled no
|
||||||
|
// matter how the body finishes, it just has not gotten there yet). All three can
|
||||||
|
// never publish artifacts a caller may rely on, so all three are misses. Only the
|
||||||
|
// first two are dead weight worth pruning from the index here - a cancellation-
|
||||||
|
// requested-but-still-running node is still reachable from its own (about to
|
||||||
|
// release) ShaderObject and will get pruned once it actually settles, so leave the
|
||||||
|
// entry alone and just refuse to hand this node out.
|
||||||
|
if (!node || node->IsCancelled()) {
|
||||||
|
m_entries.erase(it);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (node->IsCancellationRequested()) return nullptr;
|
||||||
|
|
||||||
|
// Never let correctness ride on a 64-bit hash. Lengths already matched (they are part
|
||||||
|
// of the key), so this is a plain memcmp - and it is the ONLY thing that authorizes
|
||||||
|
// two GL shader names to share one compile.
|
||||||
|
if (*node->source != source) return nullptr;
|
||||||
|
|
||||||
|
++m_adoptionCount;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderCompileAdoptionMap::Register(const SharedPtr<ShaderCompileTask>& node) {
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
SweepIfCrowded();
|
||||||
|
// operator[] rather than a find/insert pair: an existing entry for this key is either
|
||||||
|
// a re-registration of the same source (the previous node expired or was cancelled)
|
||||||
|
// or an astronomically rare hash collision. The newcomer wins in both cases.
|
||||||
|
m_entries[ShaderSourceKey{.stage = node->stage,
|
||||||
|
.sourceHash = node->sourceHash,
|
||||||
|
.sourceLength = node->source->length(),
|
||||||
|
.envFingerprint = node->env->fingerprint}] = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderCompileAdoptionMap::Clear() {
|
||||||
|
m_entries.clear();
|
||||||
|
m_sweepThreshold = kMinSweepThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderCompileAdoptionMap::SweepIfCrowded() {
|
||||||
|
if (m_entries.size() < m_sweepThreshold) return;
|
||||||
|
|
||||||
|
// Collect first, erase after: FastSTL::unordered_map is open-addressed, so erasing
|
||||||
|
// through an iterator that the same loop is still advancing is not worth reasoning
|
||||||
|
// about on a path this cold.
|
||||||
|
Vector<ShaderSourceKey> dead;
|
||||||
|
for (const auto& entry : m_entries) {
|
||||||
|
const SharedPtr<ShaderCompileTask> node = entry.second.lock();
|
||||||
|
if (!node || node->IsCancelled()) dead.push_back(entry.first);
|
||||||
|
}
|
||||||
|
for (const ShaderSourceKey& key : dead) {
|
||||||
|
m_entries.erase(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Amortization: after a sweep the map holds exactly the nodes still reachable from
|
||||||
|
// some shader object, so letting it double before the next sweep makes the whole
|
||||||
|
// scheme O(1) per Register() while keeping the map O(live nodes).
|
||||||
|
m_sweepThreshold = std::max(kMinSweepThreshold, m_entries.size() * 2);
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ShaderCompileAdoptionMap.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 <MG_State/GLState/ProgramState/ShaderSourceKey.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
class ShaderCompileTask;
|
||||||
|
|
||||||
|
// P1 stage 6: the per-context index of compile job nodes that a NEW shader object may
|
||||||
|
// adopt instead of enqueueing a duplicate of.
|
||||||
|
//
|
||||||
|
// Why it is not the P0b preprocess cache. That cache only helps once a compile has
|
||||||
|
// FINISHED - it memoizes the source-only half of the pipeline, and a worker consults it
|
||||||
|
// from inside the job body. Under asynchronous compilation the dominant shape is
|
||||||
|
// different: a shaderpack load hands N different shader objects byte-identical source
|
||||||
|
// within the same GL-thread burst (measured across bsl/complementary/bliss, ~21% of all
|
||||||
|
// Compile() calls are such cross-object duplicates), and all N are enqueued before any of
|
||||||
|
// them completes. Every one of those workers then misses the cache, runs the whole
|
||||||
|
// pipeline, and races the others to insert the same entry. This map closes that window on
|
||||||
|
// the GL thread, at enqueue: the second object through takes the FIRST object's node.
|
||||||
|
//
|
||||||
|
// What "adopt" means: the two shader objects end up holding the same SharedPtr in their
|
||||||
|
// m_compiled. They are two distinct GL names with two distinct info-log/COMPILE_STATUS
|
||||||
|
// queries, but both queries read one set of artifacts - which is exactly right, because
|
||||||
|
// the pipeline is a pure function of the key below and the full source text. Nothing is
|
||||||
|
// copied and no worker ever waits (P1 invariant I4 is untouched: this only ever REMOVES
|
||||||
|
// work from the pool). The single consume-once resource, the glslang parse, is already
|
||||||
|
// guarded for sharing by ShaderCompileTask::ClaimParsedShader's CAS, which stage 4 built
|
||||||
|
// for exactly this shape - one node, several links.
|
||||||
|
//
|
||||||
|
// ---- Threading: GL thread only, and therefore lock-free ----
|
||||||
|
// Every entry point below is reached from glCompileShader (ShaderObject::Compile) and
|
||||||
|
// from nowhere else. That is one GL entry point on the application's context thread, so
|
||||||
|
// the map needs no mutex, unlike the preprocess cache which several workers hit at once.
|
||||||
|
// The weak pointers are the ONLY thing this class stores, precisely so it can never keep
|
||||||
|
// a node - or the artifacts a node owns - alive past its last real holder.
|
||||||
|
//
|
||||||
|
// ---- Lifetime and pruning ----
|
||||||
|
// WeakPtr, never SharedPtr: the map is an index, not an owner. An entry whose node has
|
||||||
|
// been released by every shader object simply expires, and a node that was CANCELLED
|
||||||
|
// carries no result at all, so both are treated as misses and pruned where they are
|
||||||
|
// found. Pruning is otherwise amortized: Register() sweeps the whole map whenever it has
|
||||||
|
// grown past twice its size at the last sweep, which bounds the map at O(live nodes)
|
||||||
|
// without a per-call cost.
|
||||||
|
class ShaderCompileAdoptionMap {
|
||||||
|
public:
|
||||||
|
// Never sweep below this: a shaderpack burst is a few hundred distinct sources, and
|
||||||
|
// an entry is a key plus a weak pointer.
|
||||||
|
static constexpr SizeT kMinSweepThreshold = 256;
|
||||||
|
|
||||||
|
// The adoptable node for this exact source under this exact environment, or null.
|
||||||
|
//
|
||||||
|
// A hit is honored only after the FULL source text has been compared byte for byte
|
||||||
|
// against the candidate node's own snapshot: the hash in the key is a lookup
|
||||||
|
// accelerator, never the answer (ShaderSourceKey). A node that has settled as
|
||||||
|
// Cancelled is never handed out - it published nothing, so adopting it would give the
|
||||||
|
// new object a compile that can never report anything but GL_FALSE. Nor is a node
|
||||||
|
// whose cancellation has merely been REQUESTED but not yet settled (still Running,
|
||||||
|
// with IsCancellationRequested() true): JobNode::Run forces such a node to Cancelled
|
||||||
|
// the moment its body returns regardless of how the body finished, so it is already
|
||||||
|
// doomed and handing it out would just move the same GL_FALSE-with-no-log outcome to
|
||||||
|
// a second, unrelated shader object.
|
||||||
|
//
|
||||||
|
// A COMPLETED node is adoptable, and deliberately so: the new object gets the right
|
||||||
|
// answer for zero work, which is the same deal the P0b cache offers one layer down.
|
||||||
|
SharedPtr<ShaderCompileTask> FindAdoptable(ShaderStage stage, Uint64 sourceHash, const String& source,
|
||||||
|
Uint64 envFingerprint);
|
||||||
|
|
||||||
|
// Indexes `node` as the adoptable one for its key. A key already present is
|
||||||
|
// overwritten: the newcomer is at least as fresh as whatever was there, and one entry
|
||||||
|
// per key keeps this a plain map.
|
||||||
|
void Register(const SharedPtr<ShaderCompileTask>& node);
|
||||||
|
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
// ---- diagnostics only; nothing in the GL frontend branches on these ----
|
||||||
|
// Monotonic count of nodes handed out by FindAdoptable, i.e. of glCompileShader calls
|
||||||
|
// that did NOT enqueue a job because an equivalent one already existed. Tests read it
|
||||||
|
// as a delta across a burst.
|
||||||
|
Uint64 GetAdoptionCount() const { return m_adoptionCount; }
|
||||||
|
SizeT GetEntryCount() const { return m_entries.size(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void SweepIfCrowded();
|
||||||
|
|
||||||
|
UnorderedMap<ShaderSourceKey, WeakPtr<ShaderCompileTask>, ShaderSourceKeyHasher> m_entries;
|
||||||
|
SizeT m_sweepThreshold = kMinSweepThreshold;
|
||||||
|
Uint64 m_adoptionCount = 0;
|
||||||
|
};
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.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 "ShaderCompileTask.h"
|
||||||
|
|
||||||
|
#include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
|
|
||||||
|
#include <glslang/Include/PoolAlloc.h>
|
||||||
|
|
||||||
|
#include <charconv>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct ComputeLocalSize {
|
||||||
|
MobileGL::Uint x = 1;
|
||||||
|
MobileGL::Uint y = 1;
|
||||||
|
MobileGL::Uint z = 1;
|
||||||
|
bool declared = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
static MobileGL::String StripGlslComments(const MobileGL::String& source) {
|
||||||
|
MobileGL::String result;
|
||||||
|
result.reserve(source.length());
|
||||||
|
|
||||||
|
bool inLineComment = false;
|
||||||
|
bool inBlockComment = false;
|
||||||
|
for (MobileGL::SizeT i = 0; i < source.length(); ++i) {
|
||||||
|
if (inLineComment) {
|
||||||
|
if (source[i] == '\n') {
|
||||||
|
inLineComment = false;
|
||||||
|
result.push_back(source[i]);
|
||||||
|
} else {
|
||||||
|
result.push_back(' ');
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inBlockComment) {
|
||||||
|
if (source[i] == '*' && i + 1 < source.length() && source[i + 1] == '/') {
|
||||||
|
inBlockComment = false;
|
||||||
|
result.append(" ");
|
||||||
|
++i;
|
||||||
|
} else {
|
||||||
|
result.push_back(source[i] == '\n' ? '\n' : ' ');
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source[i] == '/' && i + 1 < source.length()) {
|
||||||
|
if (source[i + 1] == '/') {
|
||||||
|
inLineComment = true;
|
||||||
|
result.append(" ");
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (source[i + 1] == '*') {
|
||||||
|
inBlockComment = true;
|
||||||
|
result.append(" ");
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push_back(source[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hoisted out of ParseComputeLocalSize: constructing a std::regex costs far more than
|
||||||
|
// running it over a small source, and it was being rebuilt on every compute compile. A
|
||||||
|
// const regex carries no mutable state, so sharing one instance across workers is safe.
|
||||||
|
static const std::regex kComputeLocalSizePattern(R"(local_size_([xyz])\s*=\s*([0-9]+))");
|
||||||
|
|
||||||
|
static ComputeLocalSize ParseComputeLocalSize(const MobileGL::String& source) {
|
||||||
|
ComputeLocalSize localSize;
|
||||||
|
const MobileGL::String uncommentedSource = StripGlslComments(source);
|
||||||
|
|
||||||
|
for (std::sregex_iterator it(uncommentedSource.begin(), uncommentedSource.end(), kComputeLocalSizePattern),
|
||||||
|
end;
|
||||||
|
it != end; ++it) {
|
||||||
|
const char axis = (*it)[1].str()[0];
|
||||||
|
// The [0-9]+ capture is unbounded, so `local_size_x = 99999999999999999999999`
|
||||||
|
// is a legal match. std::stoull would throw std::out_of_range on it and let the
|
||||||
|
// exception escape glCompileShader; std::from_chars reports the overflow instead.
|
||||||
|
// An overflowing literal saturates to UINT_MAX, which the device-limit check
|
||||||
|
// below rejects anyway - the same verdict a non-overflowing huge value gets.
|
||||||
|
const MobileGL::String digits = (*it)[2].str();
|
||||||
|
unsigned long long value = 0;
|
||||||
|
const std::from_chars_result parsed =
|
||||||
|
std::from_chars(digits.data(), digits.data() + digits.size(), value);
|
||||||
|
const MobileGL::Uint clampedValue = (parsed.ec != std::errc() || value > UINT_MAX)
|
||||||
|
? UINT_MAX
|
||||||
|
: static_cast<MobileGL::Uint>(value);
|
||||||
|
|
||||||
|
// TODO: Replace this literal layout scanner with parser/AST-backed validation so expressions and
|
||||||
|
// specialization-id layouts are handled consistently with glslang.
|
||||||
|
localSize.declared = true;
|
||||||
|
if (axis == 'x') {
|
||||||
|
localSize.x = clampedValue;
|
||||||
|
} else if (axis == 'y') {
|
||||||
|
localSize.y = clampedValue;
|
||||||
|
} else {
|
||||||
|
localSize.z = clampedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return localSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The device limits come from the CompileEnv snapshot, never from a live driver query.
|
||||||
|
// GL_MAX_COMPUTE_WORK_GROUP_SIZE is a real GLES call on the DirectGLES backend: issued
|
||||||
|
// off the context thread it would silently no-op and turn a legal local_size_z into
|
||||||
|
// COMPILE_STATUS=FALSE. CaptureCompileEnv() issues it once, on the GL thread.
|
||||||
|
static std::optional<MobileGL::String> ValidateComputeLocalSizeLimits(
|
||||||
|
const MobileGL::String& source, const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
|
||||||
|
const ComputeLocalSize localSize = ParseComputeLocalSize(source);
|
||||||
|
if (!localSize.declared) return std::nullopt;
|
||||||
|
|
||||||
|
if (localSize.x > env.maxComputeWorkGroupSize[0] || localSize.y > env.maxComputeWorkGroupSize[1] ||
|
||||||
|
localSize.z > env.maxComputeWorkGroupSize[2]) {
|
||||||
|
return "Compute shader local_size exceeds GL_MAX_COMPUTE_WORK_GROUP_SIZE.";
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsigned long long invocations = static_cast<unsigned long long>(localSize.x) * localSize.y * localSize.z;
|
||||||
|
if (invocations > env.maxComputeWorkGroupInvocations) {
|
||||||
|
return "Compute shader local_size product exceeds GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The half of a compile that depends on nothing but the source text, the stage and the
|
||||||
|
// environment snapshot: preprocessing, the two lexical rejections, and the two lexical
|
||||||
|
// side-channel extractions. Split out so P0b layer 2 can memoize exactly this and
|
||||||
|
// nothing else - the glslang parse stays per-object because its TShader is consume-once.
|
||||||
|
// Deliberately free of any per-object state so the memo is sound.
|
||||||
|
//
|
||||||
|
// The compute local-size verdict reads `env` rather than the live backend, and
|
||||||
|
// env.fingerprint is part of the P0b cache key, so a memo can never be returned against
|
||||||
|
// limits other than the ones it was computed against.
|
||||||
|
static MobileGL::MG_State::GLState::ShaderPreprocessResult RunSourceOnlyPipeline(
|
||||||
|
const MobileGL::ShaderStage stage, const MobileGL::String& source,
|
||||||
|
const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
|
||||||
|
using namespace MobileGL;
|
||||||
|
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
||||||
|
using MobileGL::MG_State::GLState::ShaderPreprocessOutcome;
|
||||||
|
|
||||||
|
MobileGL::MG_State::GLState::ShaderPreprocessResult result;
|
||||||
|
result.preprocessedSource = source;
|
||||||
|
PreprocessShaderSource(stage, result.preprocessedSource, env);
|
||||||
|
|
||||||
|
if (stage == ShaderStage::Compute) {
|
||||||
|
if (const std::optional<String> localSizeError =
|
||||||
|
ValidateComputeLocalSizeLimits(result.preprocessedSource, env)) {
|
||||||
|
result.outcome = ShaderPreprocessOutcome::ComputeLocalSizeRejected;
|
||||||
|
result.infoLog = *localSizeError;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const std::optional<String> reservedError = FindReservedIdentifierViolation(result.preprocessedSource)) {
|
||||||
|
result.outcome = ShaderPreprocessOutcome::ReservedIdentifierRejected;
|
||||||
|
result.infoLog = *reservedError;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The parse this feeds runs in the link-compatible configuration (Vulkan-client
|
||||||
|
// env with relaxed rules): the TShader it produces is what glLinkProgram links and
|
||||||
|
// what the backends' SPIR-V is generated from - there is no second, GL-client
|
||||||
|
// parse. The GL frontend semantics the relaxed parse cannot provide are restored
|
||||||
|
// on top: explicit default-block uniform locations through the lexical
|
||||||
|
// side-channels below, dead-uniform/global-UBO filtering in
|
||||||
|
// ProgramObject::DoReflection.
|
||||||
|
result.explicitUniformLocations = ExtractExplicitUniformLocations(result.preprocessedSource);
|
||||||
|
result.explicitOpaqueBindings = ExtractExplicitOpaqueBindings(result.preprocessedSource);
|
||||||
|
result.outcome = ShaderPreprocessOutcome::Preprocessed;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
// glslang has no "detach this thread" API in the vendored revision (there is no
|
||||||
|
// InitThread/DetachThread pair any more; thread attachment is implicit through
|
||||||
|
// thread_local state, and glslang::InitializeProcess() is process-wide, refcounted and
|
||||||
|
// mutex-guarded, so it needs no per-worker counterpart). The pool allocator is the part
|
||||||
|
// that needs undoing; see the declaration in ShaderCompileTask.h.
|
||||||
|
GlslangThreadAllocatorGuard::~GlslangThreadAllocatorGuard() { glslang::SetThreadPoolAllocator(nullptr); }
|
||||||
|
|
||||||
|
// Pure CPU work only. Everything this reads is either an input the node owns or a
|
||||||
|
// process-wide constant; everything it writes is `artifacts`. Do not add a GL/EGL call,
|
||||||
|
// a pActiveBackendObject read, or a pGLContext->RecordError() here - the first two are
|
||||||
|
// what CompileEnv exists to replace, and the third is why the design's section 6
|
||||||
|
// deferral mechanism (and JobNode's debug assert on it) exists.
|
||||||
|
void ShaderCompileTask::RunBody() {
|
||||||
|
// Own the failure rather than letting JobNode's backstop settle the node as
|
||||||
|
// Cancelled: an abandoned node publishes nothing, so the shader would report
|
||||||
|
// COMPILE_STATUS false with an EMPTY info log. GL models a failed compile as
|
||||||
|
// status + log, so turn a throw into exactly that - a completed job whose result
|
||||||
|
// is "this shader did not compile", with a log the application can read.
|
||||||
|
// (JobNode still catches: it is the last resort for anything below.)
|
||||||
|
try {
|
||||||
|
RunCompilePipeline();
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
artifacts = {};
|
||||||
|
artifacts.env = env;
|
||||||
|
artifacts.compileStatus = false;
|
||||||
|
artifacts.infoLog = std::format("Error: shader compilation failed: {}", e.what());
|
||||||
|
} catch (...) {
|
||||||
|
artifacts = {};
|
||||||
|
artifacts.env = env;
|
||||||
|
artifacts.compileStatus = false;
|
||||||
|
artifacts.infoLog = "Error: shader compilation failed: unknown exception";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderCompileTask::RunCompilePipeline() {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
const GlslangThreadAllocatorGuard glslangGuard;
|
||||||
|
|
||||||
|
const CompileEnv& compileEnv = *env;
|
||||||
|
artifacts.env = env;
|
||||||
|
|
||||||
|
// P0b layer 2: another shader object in this context may already have run the
|
||||||
|
// source-only half over byte-identical text under the same environment.
|
||||||
|
ShaderPreprocessResultPtr cached =
|
||||||
|
cache ? cache->Find(stage, sourceHash, *source, compileEnv.fingerprint) : nullptr;
|
||||||
|
SharedPtr<ShaderPreprocessResult> fresh;
|
||||||
|
if (!cached) fresh = MakeShared<ShaderPreprocessResult>(RunSourceOnlyPipeline(stage, *source, compileEnv));
|
||||||
|
const ShaderPreprocessResult& shared = cached ? *cached : *fresh;
|
||||||
|
const Bool shouldPopulateCache = !cached && cache != nullptr;
|
||||||
|
|
||||||
|
if (!shared.Preprocessed()) {
|
||||||
|
// Rejected lexically, or a glslang failure this context has already seen for
|
||||||
|
// this exact source (ParseFailed) - either way the parse can be skipped.
|
||||||
|
artifacts.infoLog = shared.infoLog;
|
||||||
|
if (shouldPopulateCache) {
|
||||||
|
cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderAttrib attrib{.shaderType = MG_Util::ConvertShaderStageToGLEnum(stage),
|
||||||
|
.sourceStr = shared.preprocessedSource,
|
||||||
|
.flags = 0,
|
||||||
|
.env = &compileEnv};
|
||||||
|
|
||||||
|
auto result = ShaderCompiler::CompileShader(attrib);
|
||||||
|
if (result) {
|
||||||
|
artifacts.compileStatus = true;
|
||||||
|
artifacts.shader = result.value();
|
||||||
|
// Copy, not move: `shared` may alias a cache entry that has to outlive us, and
|
||||||
|
// `fresh` is about to be handed to the cache.
|
||||||
|
artifacts.preprocessedSource = shared.preprocessedSource;
|
||||||
|
artifacts.explicitUniformLocations = shared.explicitUniformLocations;
|
||||||
|
artifacts.explicitOpaqueBindings = shared.explicitOpaqueBindings;
|
||||||
|
artifacts.infoLog.clear();
|
||||||
|
if (shouldPopulateCache) {
|
||||||
|
cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
artifacts.infoLog = result.error().log;
|
||||||
|
// Deferred, not logged here, for two reasons. MGLOG from a pool thread interleaves
|
||||||
|
// mid-line with the GL thread's own output and lands out of order relative to the
|
||||||
|
// glCompileShader that caused it; diagnostics.logLines is replayed by the join, on
|
||||||
|
// the GL thread, exactly where a serial implementation would have printed it.
|
||||||
|
// And a one-line summary rather than the old full source dump: a shaderpack stage
|
||||||
|
// is ~100KB, so the dump was the single largest thing this driver ever wrote to
|
||||||
|
// the log, for every failing shader. The info log is what names the offending
|
||||||
|
// line; the source is recoverable from the application.
|
||||||
|
const SizeT firstLineEnd = artifacts.infoLog.find('\n');
|
||||||
|
diagnostics.logLines.push_back(std::format(
|
||||||
|
"ShaderCompileTask: shader {} (stage {}) failed to compile; compileStatus = false. "
|
||||||
|
"Preprocessed source: {} bytes. First log line: {}",
|
||||||
|
externalIndex, static_cast<Int>(stage), shared.preprocessedSource.length(),
|
||||||
|
artifacts.infoLog.substr(0, firstLineEnd == String::npos ? artifacts.infoLog.length()
|
||||||
|
: firstLineEnd)));
|
||||||
|
if (shouldPopulateCache) {
|
||||||
|
fresh->outcome = ShaderPreprocessOutcome::ParseFailed;
|
||||||
|
fresh->infoLog = artifacts.infoLog;
|
||||||
|
fresh->explicitUniformLocations.clear();
|
||||||
|
fresh->explicitOpaqueBindings.clear();
|
||||||
|
cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedPtr<glslang::TShader> ShaderCompileTask::ClaimParsedShader(String& outReparseLog) const {
|
||||||
|
MOBILEGL_ASSERT(IsComplete(),
|
||||||
|
"ShaderCompileTask::ClaimParsedShader() on a job that has not completed; its artifacts "
|
||||||
|
"are still being written");
|
||||||
|
|
||||||
|
if (artifacts.shader) {
|
||||||
|
// The whole race, in one instruction. Acquire-release because the winner is about
|
||||||
|
// to hand the TShader to glslang's linker on a possibly different thread from the
|
||||||
|
// one that parsed it - the node's terminal transition already published the
|
||||||
|
// parse, and this orders the two claimants against each other.
|
||||||
|
Bool expected = false;
|
||||||
|
if (m_parseClaimed.compare_exchange_strong(expected, true, std::memory_order_acq_rel,
|
||||||
|
std::memory_order_acquire)) {
|
||||||
|
return artifacts.shader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either another link already consumed the stored parse (and mapIO mutated its
|
||||||
|
// intermediate), or there never was one. Re-parse the preprocessed source through the
|
||||||
|
// identical configuration; that costs one glslang parse, which is what GenerateBinary
|
||||||
|
// used to spend here on EVERY link rather than only on reuse.
|
||||||
|
//
|
||||||
|
// The guard is not optional on this path: from stage 4 this runs on a pool worker,
|
||||||
|
// and TShader::parse would leave that worker's TLS allocator pointing at a pool the
|
||||||
|
// GL thread is about to free. (ProgramLinkTask::RunBody holds one too; they nest
|
||||||
|
// harmlessly - both just reset the thread to its own default.)
|
||||||
|
const GlslangThreadAllocatorGuard glslangGuard;
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
ShaderAttrib attrib{.shaderType = MG_Util::ConvertShaderStageToGLEnum(stage),
|
||||||
|
.sourceStr = artifacts.preprocessedSource,
|
||||||
|
.flags = 0,
|
||||||
|
// Re-parse against the SAME environment the original parse used,
|
||||||
|
// not against whatever the backend reports now.
|
||||||
|
.env = artifacts.env.get()};
|
||||||
|
auto result = ShaderCompiler::CompileShader(attrib);
|
||||||
|
if (!result) {
|
||||||
|
// Should be unreachable: the same source parsed successfully at Compile().
|
||||||
|
outReparseLog = result.error().log;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return result.value();
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.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 <MG_Util/Async/JobNode.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||||
|
#include <MG_State/GLState/ProgramState/ShaderPreprocessCache.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
// glslang has no "detach this thread" API in the vendored revision, but TShader::parse
|
||||||
|
// leaves the calling thread's TLS pool allocator pointing at the shader's own pool and
|
||||||
|
// never restores it. Left there, the next allocation this thread makes - in an unrelated
|
||||||
|
// job, or in glslang code reached from a different object - would come out of a pool the
|
||||||
|
// GL thread may already have deleted with the TShader. SetThreadPoolAllocator(nullptr)
|
||||||
|
// reverts the thread to its own thread_local default and is the documented idiom.
|
||||||
|
//
|
||||||
|
// A scope guard, so it also runs when a body throws. Declared here rather than kept
|
||||||
|
// file-local because stage 4 gave it a second user: ProgramLinkTask's body parses (the
|
||||||
|
// claim-CAS loser's re-parse), links and emits SPIR-V, all on a pool thread.
|
||||||
|
struct GlslangThreadAllocatorGuard {
|
||||||
|
GlslangThreadAllocatorGuard() = default;
|
||||||
|
~GlslangThreadAllocatorGuard();
|
||||||
|
GlslangThreadAllocatorGuard(const GlslangThreadAllocatorGuard&) = delete;
|
||||||
|
GlslangThreadAllocatorGuard& operator=(const GlslangThreadAllocatorGuard&) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Everything one glCompileShader PRODUCES, in one block.
|
||||||
|
//
|
||||||
|
// This is exactly the set a single run of the compile pipeline writes, which is what
|
||||||
|
// makes "discard the artifacts" a complete invalidation and "move the artifacts" a
|
||||||
|
// complete publish. It lives on the job node rather than on ShaderObject: a worker fills
|
||||||
|
// it in, and the GL thread reads it through ShaderObject's join gate.
|
||||||
|
struct ShaderCompileArtifacts {
|
||||||
|
// The CompileEnv snapshot this compile ran against. Held so the consume-once
|
||||||
|
// re-parse in ClaimParsedShader() reproduces the original parse exactly, instead of
|
||||||
|
// re-reading whatever the backend says now.
|
||||||
|
SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> env;
|
||||||
|
SharedPtr<glslang::TShader> shader;
|
||||||
|
// The source the parse actually consumed (after PreprocessShaderSource), kept for
|
||||||
|
// ClaimParsedShader's re-parse so a later link never depends on the preprocessor
|
||||||
|
// being deterministic across backend-state changes.
|
||||||
|
String preprocessedSource;
|
||||||
|
UnorderedMap<String, Int> explicitUniformLocations;
|
||||||
|
UnorderedMap<String, Uint> explicitOpaqueBindings;
|
||||||
|
String infoLog;
|
||||||
|
Bool compileStatus = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The unit of asynchronous shader compilation: one glCompileShader's worth of pure CPU
|
||||||
|
// work - preprocess, the two lexical rejections, the two lexical extractions, and the
|
||||||
|
// glslang parse - with every input it needs owned by the node itself.
|
||||||
|
//
|
||||||
|
// That ownership is the whole point. The node reads no GL-thread state (the source is a
|
||||||
|
// SharedPtr<const String> snapshot, the device limits come from the CompileEnv snapshot,
|
||||||
|
// the P0b cross-object memo is shared-owned and internally locked) and writes nothing
|
||||||
|
// but its own `artifacts`. So a node whose ShaderObject was re-sourced, deleted, or
|
||||||
|
// destroyed while it was still running is safe to simply abandon - no wait, no
|
||||||
|
// synchronization with the GL thread beyond the node's own terminal state.
|
||||||
|
class ShaderCompileTask final : public MG_Util::Async::JobNode {
|
||||||
|
public:
|
||||||
|
ShaderCompileTask(const ShaderStage stage, SharedPtr<const String> source, const Uint64 sourceHash,
|
||||||
|
SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> env,
|
||||||
|
SharedPtr<ShaderPreprocessCache> cache, const Uint externalIndex)
|
||||||
|
: stage(stage), source(Move(source)), sourceHash(sourceHash), env(Move(env)), cache(Move(cache)),
|
||||||
|
externalIndex(externalIndex) {}
|
||||||
|
|
||||||
|
// ---- inputs: immutable after construction, all owned by the node ----
|
||||||
|
const ShaderStage stage;
|
||||||
|
// The exact text at enqueue. ShaderObject compares this pointer against its own
|
||||||
|
// m_source to decide whether its layer-1 memo is armed, which is why glShaderSource
|
||||||
|
// only ever swaps the pointer when the text genuinely differs.
|
||||||
|
const SharedPtr<const String> source;
|
||||||
|
const Uint64 sourceHash;
|
||||||
|
const SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> env;
|
||||||
|
// P0b layer 2, or null. Null is the "no context" case (the default fragment shader,
|
||||||
|
// the backends' internal blit/mipmap shaders) and doubles as the marker for
|
||||||
|
// "compile inline regardless of the async flag" - see ShaderObject::Compile().
|
||||||
|
const SharedPtr<ShaderPreprocessCache> cache;
|
||||||
|
const Uint externalIndex; // logs only
|
||||||
|
|
||||||
|
// ---- output: valid iff IsComplete(), immutable afterwards ----
|
||||||
|
ShaderCompileArtifacts artifacts;
|
||||||
|
|
||||||
|
// Hands out a link-consumable TShader, exactly once for the stored parse.
|
||||||
|
//
|
||||||
|
// glslang's mapIO mutates the TShader's aliased intermediate, so the parse this node
|
||||||
|
// produced may feed exactly ONE link; every later link (a relink, or the same shader
|
||||||
|
// attached to a second program) needs a fresh parse. The claim is a CAS on this
|
||||||
|
// shared node rather than a flag on the ShaderObject because from stage 4 the two
|
||||||
|
// callers can be two ProgramLinkTasks running on two workers: two programs sharing
|
||||||
|
// one shader, linked back to back. Copying the parse out and tracking consumed-ness
|
||||||
|
// per program would let both of them decide they were the first, run mapIO over the
|
||||||
|
// same intermediate twice, and ship silently corrupt SPIR-V.
|
||||||
|
//
|
||||||
|
// The CAS loser re-parses artifacts.preprocessedSource against THIS node's own
|
||||||
|
// CompileEnv (not against whatever the backend reports now), through the identical
|
||||||
|
// CompileShader path - so winner and loser produce byte-identical SPIR-V. Callable
|
||||||
|
// only once IsComplete() and compileStatus are true. Returns null only if that
|
||||||
|
// re-parse fails, and outReparseLog then carries its diagnostics.
|
||||||
|
//
|
||||||
|
// Const because the claim is the node's own synchronization, not a mutation of its
|
||||||
|
// published artifacts: a claim that is taken and then abandoned (its link was
|
||||||
|
// cancelled) costs one extra re-parse later and nothing else.
|
||||||
|
SharedPtr<glslang::TShader> ClaimParsedShader(String& outReparseLog) const;
|
||||||
|
|
||||||
|
// Sticky marker for "a ProgramLinkTask has this node in its input snapshot".
|
||||||
|
//
|
||||||
|
// It exists to keep a cancel from eating a result someone still needs. A pending link
|
||||||
|
// holds its dependencies by SharedPtr, so the NODE always outlives the ShaderObject -
|
||||||
|
// but Cancel() is not about lifetime, it discards the result. The reachable sequence
|
||||||
|
// is the ordinary one: compile, attach, glLinkProgram (enqueued), glDetachShader,
|
||||||
|
// glDeleteShader. The detach makes the shader GL-invisible, so the delete frees its
|
||||||
|
// name, and ReleaseShaderNameIfOrphaned would cancel a compile the enqueued link is
|
||||||
|
// waiting on - turning a link that must report GL_TRUE into GL_FALSE. Set on the GL
|
||||||
|
// thread in Link()'s prologue, read on the GL thread by
|
||||||
|
// ShaderObject::ReleaseCompileNode - which from stage 6 weighs it together with the
|
||||||
|
// adopter count below, because a node can now have both kinds of observer at once.
|
||||||
|
//
|
||||||
|
// Never cleared: the worst case is one stale node compiling to completion for nobody,
|
||||||
|
// which is exactly what the pre-stage-3 implementation always did.
|
||||||
|
void MarkLinkReferenced() { m_linkReferenced.store(true, std::memory_order_release); }
|
||||||
|
Bool IsLinkReferenced() const { return m_linkReferenced.load(std::memory_order_acquire); }
|
||||||
|
|
||||||
|
// ---- P1 stage 6: the adopter count ----
|
||||||
|
// How many live ShaderObjects currently hold this node in their m_compiled.
|
||||||
|
//
|
||||||
|
// It exists because stage 6 lets a node be SHARED: before it, a node had exactly one
|
||||||
|
// shader object, so "this object stopped caring" and "nothing can observe this
|
||||||
|
// result" were the same statement and ShaderObject::CancelCompile could cancel
|
||||||
|
// unconditionally. Once two GL shader names hold one node, that cancel would kill the
|
||||||
|
// other one's pending compile - a compile that must still report GL_TRUE. So a cancel
|
||||||
|
// is now authorized by TWO conditions, both checked by the releaser:
|
||||||
|
// * this release brings the count to zero (no shader object is left), AND
|
||||||
|
// * IsLinkReferenced() is false (no enqueued link took the node into its snapshot).
|
||||||
|
// The second is the stage-4 pin, unchanged; the first is what stage 6 adds.
|
||||||
|
//
|
||||||
|
// ---- Why a plain Int and not an atomic ----
|
||||||
|
// Every mutation is made from ShaderObject, and every ShaderObject mutation site is a
|
||||||
|
// GL entry point on the application's context thread: glCompileShader (adopt/create),
|
||||||
|
// glShaderSource with different text, glDeleteShader's orphan sweep, and
|
||||||
|
// ~ShaderObject. All of them are the SAME thread, so the count is never concurrently
|
||||||
|
// mutated and an atomic would only buy an unneeded lock prefix on the hottest compile
|
||||||
|
// path. Workers cannot touch it by construction: a job body's entire contract (see
|
||||||
|
// this class's header comment) is that it reads only the node's inputs and writes only
|
||||||
|
// `artifacts`, and a plain Int here makes that contract grep-checkable in a way an
|
||||||
|
// atomic would quietly hide.
|
||||||
|
//
|
||||||
|
// The CANCEL that the count authorizes still races the worker, and deliberately so -
|
||||||
|
// that is the settled cancel-not-join semantics from stage 3: JobNode::Cancel is
|
||||||
|
// cooperative and non-blocking, a node already running settles as Cancelled when its
|
||||||
|
// body returns, and a node that has already gone terminal ignores the request.
|
||||||
|
// Nothing about that changes here.
|
||||||
|
//
|
||||||
|
// Exactness under that race: ShaderObject::ReleaseCompileNode returns EARLY, without
|
||||||
|
// decrementing and without dropping its reference, when the node is already terminal
|
||||||
|
// (there is nothing left to stop). Terminality is sticky, so if a releaser observes a
|
||||||
|
// node as NON-terminal then no holder has ever taken that early return on it, and the
|
||||||
|
// count it reads is exactly the number of holders. If the worker finishes in the
|
||||||
|
// window between that observation and the Cancel(), the Cancel is a no-op on a
|
||||||
|
// terminal node - and the count was zero, so there was no other holder to harm.
|
||||||
|
void AddAdopter() { ++m_adopters; }
|
||||||
|
void ReleaseAdopter() {
|
||||||
|
MOBILEGL_ASSERT(m_adopters > 0,
|
||||||
|
"ShaderCompileTask adopter count underflow; a ShaderObject released a node it did not "
|
||||||
|
"hold (every release must pair with exactly one AddAdopter)");
|
||||||
|
--m_adopters;
|
||||||
|
}
|
||||||
|
Int AdopterCount() const { return m_adopters; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void RunBody() override;
|
||||||
|
// The real body; RunBody wraps it so a throw becomes a GL-visible compile failure.
|
||||||
|
void RunCompilePipeline();
|
||||||
|
|
||||||
|
mutable std::atomic<Bool> m_parseClaimed{false};
|
||||||
|
std::atomic<Bool> m_linkReferenced{false};
|
||||||
|
// GL-thread-owned; see AddAdopter above for why this is not an atomic.
|
||||||
|
Int m_adopters = 0;
|
||||||
|
};
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
@@ -7,197 +7,208 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#include "ShaderObject.h"
|
#include "ShaderObject.h"
|
||||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
#include "ShaderPreprocessCache.h"
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
#include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h>
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||||
#include <MG_Util/ShaderTranspiler/glslang/UniformTraverser.h>
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
|
|
||||||
namespace {
|
|
||||||
struct ComputeLocalSize {
|
|
||||||
MobileGL::Uint x = 1;
|
|
||||||
MobileGL::Uint y = 1;
|
|
||||||
MobileGL::Uint z = 1;
|
|
||||||
bool declared = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
static MobileGL::String StripGlslComments(const MobileGL::String& source) {
|
|
||||||
MobileGL::String result;
|
|
||||||
result.reserve(source.length());
|
|
||||||
|
|
||||||
bool inLineComment = false;
|
|
||||||
bool inBlockComment = false;
|
|
||||||
for (MobileGL::SizeT i = 0; i < source.length(); ++i) {
|
|
||||||
if (inLineComment) {
|
|
||||||
if (source[i] == '\n') {
|
|
||||||
inLineComment = false;
|
|
||||||
result.push_back(source[i]);
|
|
||||||
} else {
|
|
||||||
result.push_back(' ');
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inBlockComment) {
|
|
||||||
if (source[i] == '*' && i + 1 < source.length() && source[i + 1] == '/') {
|
|
||||||
inBlockComment = false;
|
|
||||||
result.append(" ");
|
|
||||||
++i;
|
|
||||||
} else {
|
|
||||||
result.push_back(source[i] == '\n' ? '\n' : ' ');
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source[i] == '/' && i + 1 < source.length()) {
|
|
||||||
if (source[i + 1] == '/') {
|
|
||||||
inLineComment = true;
|
|
||||||
result.append(" ");
|
|
||||||
++i;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (source[i + 1] == '*') {
|
|
||||||
inBlockComment = true;
|
|
||||||
result.append(" ");
|
|
||||||
++i;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push_back(source[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
static ComputeLocalSize ParseComputeLocalSize(const MobileGL::String& source) {
|
|
||||||
ComputeLocalSize localSize;
|
|
||||||
const MobileGL::String uncommentedSource = StripGlslComments(source);
|
|
||||||
const std::regex localSizePattern(R"(local_size_([xyz])\s*=\s*([0-9]+))");
|
|
||||||
|
|
||||||
for (std::sregex_iterator it(uncommentedSource.begin(), uncommentedSource.end(), localSizePattern), end;
|
|
||||||
it != end; ++it) {
|
|
||||||
const char axis = (*it)[1].str()[0];
|
|
||||||
const auto value = static_cast<unsigned long long>(std::stoull((*it)[2].str()));
|
|
||||||
const MobileGL::Uint clampedValue = value > UINT_MAX ? UINT_MAX : static_cast<MobileGL::Uint>(value);
|
|
||||||
|
|
||||||
// TODO: Replace this literal layout scanner with parser/AST-backed validation so expressions and
|
|
||||||
// specialization-id layouts are handled consistently with glslang.
|
|
||||||
localSize.declared = true;
|
|
||||||
if (axis == 'x') {
|
|
||||||
localSize.x = clampedValue;
|
|
||||||
} else if (axis == 'y') {
|
|
||||||
localSize.y = clampedValue;
|
|
||||||
} else {
|
|
||||||
localSize.z = clampedValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return localSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
static MobileGL::Uint GetComputeWorkGroupSizeLimit(MobileGL::Uint index) {
|
|
||||||
constexpr MobileGL::Uint kFrontendMinComputeWorkGroupSizes[] = {1024, 1024, 64};
|
|
||||||
MobileGL::Int backendValue = 0;
|
|
||||||
if (MobileGL::MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v) {
|
|
||||||
MobileGL::MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, index,
|
|
||||||
&backendValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Share these exposed compute limit helpers with GL_Getter.cpp instead of duplicating the frontend minima.
|
|
||||||
return std::max(static_cast<MobileGL::Uint>(std::max(backendValue, 0)),
|
|
||||||
kFrontendMinComputeWorkGroupSizes[index]);
|
|
||||||
}
|
|
||||||
|
|
||||||
static unsigned long long GetComputeWorkGroupInvocationLimit() {
|
|
||||||
constexpr unsigned long long kFrontendMaxComputeWorkGroupInvocations = 1024;
|
|
||||||
if (!MobileGL::MG_Backend::pActiveBackendObject) return kFrontendMaxComputeWorkGroupInvocations;
|
|
||||||
|
|
||||||
return std::max(static_cast<unsigned long long>(std::max(
|
|
||||||
MobileGL::MG_Backend::pActiveBackendObject->GetDynamicParameters()
|
|
||||||
.MaxComputeWorkGroupInvocations,
|
|
||||||
0)),
|
|
||||||
kFrontendMaxComputeWorkGroupInvocations);
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::optional<MobileGL::String> ValidateComputeLocalSizeLimits(const MobileGL::String& source) {
|
|
||||||
const ComputeLocalSize localSize = ParseComputeLocalSize(source);
|
|
||||||
if (!localSize.declared) return std::nullopt;
|
|
||||||
|
|
||||||
if (localSize.x > GetComputeWorkGroupSizeLimit(0) || localSize.y > GetComputeWorkGroupSizeLimit(1) ||
|
|
||||||
localSize.z > GetComputeWorkGroupSizeLimit(2)) {
|
|
||||||
return "Compute shader local_size exceeds GL_MAX_COMPUTE_WORK_GROUP_SIZE.";
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsigned long long invocations = static_cast<unsigned long long>(localSize.x) * localSize.y * localSize.z;
|
|
||||||
if (invocations > GetComputeWorkGroupInvocationLimit()) {
|
|
||||||
return "Compute shader local_size product exceeds GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
void ShaderObject::SetShaderSource(const String& source) {
|
void ShaderObject::SetShaderSource(const String& source) {
|
||||||
m_source = source;
|
// P0b layer 1. glShaderSource always REPLACES the source, but replacing it with a
|
||||||
m_shader.reset();
|
// byte-identical one cannot change what a compile would produce: the whole
|
||||||
m_compileStatus = false;
|
// pipeline (preprocess -> lexical checks -> glslang parse) is a pure function of
|
||||||
m_infoLog.clear();
|
// (stage, source, CompileEnv). So keeping the compiled state is not an optimization
|
||||||
|
// that changes observable behaviour - the COMPILE_STATUS, the info log and the
|
||||||
|
// reflection a caller can query are exactly what a real recompile would have
|
||||||
|
// rebuilt, byte for byte. A compile still IN FLIGHT is left running for the same
|
||||||
|
// reason: it is computing the right answer for text this object still holds.
|
||||||
|
if (SourceMatchesCompiledState(source)) return;
|
||||||
|
// The text genuinely changed, so whatever a running job is computing is now about
|
||||||
|
// an old source. Give up our claim on it - it owns its own copy of that old string,
|
||||||
|
// so swapping the pointer below cannot race its storage. Note "our claim", not "the
|
||||||
|
// job": another shader object may have adopted the same node and still be waiting for
|
||||||
|
// exactly this answer, which is what ReleaseCompileNode's count discipline protects.
|
||||||
|
ReleaseCompileNode();
|
||||||
|
m_source = MakeShared<const String>(source);
|
||||||
|
InvalidateCompiledState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShaderObject::SetShaderSource(String&& source) {
|
void ShaderObject::SetShaderSource(String&& source) {
|
||||||
m_source = Move(source);
|
if (SourceMatchesCompiledState(source)) return;
|
||||||
m_shader.reset();
|
ReleaseCompileNode();
|
||||||
m_compileStatus = false;
|
m_source = MakeShared<const String>(Move(source));
|
||||||
m_infoLog.clear();
|
InvalidateCompiledState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ShaderObject::SourceMatchesCompiledState(const String& candidate) const {
|
||||||
|
// The memo is armed exactly while a job exists that was built from the string this
|
||||||
|
// object still points at - pending or finished, success or failure.
|
||||||
|
if (!HasMemoizedCompile()) return false;
|
||||||
|
if (candidate.length() != m_source->length()) return false;
|
||||||
|
// Never let correctness ride on a hash: the answer is the full text comparison.
|
||||||
|
// (The stored hash on the node is a cache-lookup accelerator, not a substitute.)
|
||||||
|
return candidate == *m_source;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderObject::JoinPendingCompile() const {
|
||||||
|
MOBILEGL_ASSERT(!MG_Util::Async::ShaderCompilePool::IsPoolThread(),
|
||||||
|
"ShaderObject::EnsureCompileJoined() reached from a pool thread; a job body must never read "
|
||||||
|
"GL-thread-owned objects");
|
||||||
|
m_compiled->Wait();
|
||||||
|
m_compileJoined = true;
|
||||||
|
// Errors and worker-side log lines are raised HERE, on the GL thread, at the first
|
||||||
|
// join of the job that produced them - which for a single shader is trivially the
|
||||||
|
// order a serial implementation would have produced them in.
|
||||||
|
//
|
||||||
|
// ApplyDeferredDiagnostics DRAINS, so a node shared by several shader objects
|
||||||
|
// (stage 6) replays its worker-side log line exactly once, at whichever object joins
|
||||||
|
// first. That is the honest report - one compile ran - and it is log text only: the
|
||||||
|
// GL-observable half of a failure, COMPILE_STATUS and the info log, lives in
|
||||||
|
// `artifacts` and every sharer reads the identical copy of it.
|
||||||
|
MG_Util::Async::ApplyDeferredDiagnostics(*m_compiled);
|
||||||
|
// A node that settled as Cancelled published nothing. Dropping it here is what keeps
|
||||||
|
// the object's state machine to two reachable cases - "no job" and "a job that
|
||||||
|
// completed" - so every reader below can treat a live node as authoritative.
|
||||||
|
//
|
||||||
|
// Through DropCompileNode, not a bare reset: this object is letting the node go, so
|
||||||
|
// its adopter slot has to go with it. A node shared with another object stays alive
|
||||||
|
// and gets dropped once more when that object joins - once per holder, never twice
|
||||||
|
// for the same one, because DropCompileNode is null-guarded.
|
||||||
|
if (!m_compiled->IsComplete()) DropCompileNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderObject::AdoptCompileNode(SharedPtr<ShaderCompileTask> node) const {
|
||||||
|
// Never overwrite a hold without giving its slot back first.
|
||||||
|
DropCompileNode();
|
||||||
|
m_compiled = Move(node);
|
||||||
|
m_compiled->AddAdopter();
|
||||||
|
// Re-arm the join gate: whether this node was just created or just adopted from
|
||||||
|
// another object, THIS object has not pulled its result yet. (An adopted node may
|
||||||
|
// already be terminal - the join then only replays what is left of its diagnostics.)
|
||||||
|
m_compileJoined = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderObject::DropCompileNode() const {
|
||||||
|
if (!m_compiled) return;
|
||||||
|
m_compiled->ReleaseAdopter();
|
||||||
|
m_compiled.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderObject::InvalidateCompiledState() {
|
||||||
|
// The job node holds exactly what one Compile() produces, so discarding it IS the
|
||||||
|
// invalidation - and it re-arms nothing, so the next Compile() genuinely recompiles.
|
||||||
|
DropCompileNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderObject::ReleaseCompileNode() {
|
||||||
|
if (!m_compiled) return;
|
||||||
|
// Already terminal: there is nothing left to stop, so this is not a release at all -
|
||||||
|
// the node and this object's claim on it both stay. That early return is older than
|
||||||
|
// stage 6 and it is load-bearing: ProgramState::ReleaseShaderNameIfOrphaned calls
|
||||||
|
// this on a shader whose name is going away but whose object a ProgramObject may
|
||||||
|
// still hold, and dropping a COMPLETED compile there would turn that program's link
|
||||||
|
// into GL_FALSE.
|
||||||
|
if (m_compiled->IsTerminal()) return;
|
||||||
|
// Two independent claimants have to be checked before a cancel, and this object is
|
||||||
|
// authorized to cancel only if BOTH say the result has become unobservable.
|
||||||
|
//
|
||||||
|
// 1. Other shader objects. From stage 6 a node can be SHARED by several GL shader
|
||||||
|
// names that were handed byte-identical source; cancelling here would turn a
|
||||||
|
// compile they must still see as GL_TRUE into GL_FALSE. Only the releaser that
|
||||||
|
// takes the count to zero - i.e. the last holder - may cancel. See
|
||||||
|
// ShaderCompileTask::AddAdopter for why a plain Int is sound here and for the
|
||||||
|
// exactness argument under the worker race.
|
||||||
|
// 2. A pending LINK. An enqueued ProgramLinkTask holds the node in its input snapshot
|
||||||
|
// and a cancel would turn its link into GL_FALSE; reached by the ordinary
|
||||||
|
// link-then-detach-then-delete shader teardown. See MarkLinkReferenced. Never
|
||||||
|
// cleared, so this is a one-way pin.
|
||||||
|
//
|
||||||
|
// The cancel itself is cooperative and non-blocking, exactly as before: a node no
|
||||||
|
// worker has picked up settles immediately, a running one is flagged and settles when
|
||||||
|
// its body returns, writing only into itself the whole time.
|
||||||
|
if (m_compiled->AdopterCount() == 1 && !m_compiled->IsLinkReferenced()) m_compiled->Cancel();
|
||||||
|
DropCompileNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShaderObject::Compile() {
|
void ShaderObject::Compile() {
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
// P0b layer 1, as a tri-state: the memo is "the node in m_compiled was built from
|
||||||
String compileSource = m_source;
|
// the string m_source still points at". SetShaderSource only swaps that pointer when
|
||||||
MG_Util::ShaderTranspiler::PreprocessShaderSource(m_stage, compileSource);
|
// the text actually differs, so this is a pointer compare, and it covers Pending as
|
||||||
|
// well as Complete - a second glCompileShader on an in-flight object is a no-op, not
|
||||||
|
// a duplicate job racing to write the same fields.
|
||||||
|
//
|
||||||
|
// The failure case is covered too: the info log stays queryable because nothing is
|
||||||
|
// cleared. And if the stored TShader already fed a link, the no-op leaves
|
||||||
|
// preprocessedSource and both side-channel maps intact, which is precisely what
|
||||||
|
// ClaimParsedShader's on-demand re-parse needs - a real recompile would have handed
|
||||||
|
// the next link a fresh parse, the no-op hands it a fresh re-parse of the identical
|
||||||
|
// source instead. Same result, one parse either way.
|
||||||
|
if (HasMemoizedCompile()) return;
|
||||||
|
|
||||||
if (m_stage == ShaderStage::Compute) {
|
// Two reasons to stay on this thread, one rule. Without the async flag the whole
|
||||||
const std::optional<String> localSizeError = ValidateComputeLocalSizeLimits(compileSource);
|
// path must be byte-identical to the synchronous implementation, and a cache-less
|
||||||
if (localSizeError) {
|
// object is an internal shader that compiles and reads its status in the same
|
||||||
m_compileStatus = false;
|
// breath (see the constructor comment) - a job would only add a round trip.
|
||||||
m_shader.reset();
|
// AsyncShaderCompileActive(), not ...Enabled(): a glMaxShaderCompilerThreadsKHR(0)
|
||||||
m_infoLog = *localSizeError;
|
// has to put compilation back on this thread even though the extension is still
|
||||||
|
// advertised, and that is exactly what makes the GL_COMPLETION_STATUS_KHR the
|
||||||
|
// extension mandates after a zero count (immediately GL_TRUE) fall out for free.
|
||||||
|
//
|
||||||
|
// Hoisted above the node construction because stage 6 keys off it too: this same
|
||||||
|
// answer decides whether the adoption map is consulted at all, so a
|
||||||
|
// glMaxShaderCompilerThreadsKHR(0) and a flag-off build both bypass sharing exactly
|
||||||
|
// as they bypass the pool, and their behaviour stays byte-identical to pre-stage-6.
|
||||||
|
const Bool runOnPool = m_preprocessCache && MG_Util::Async::AsyncShaderCompileActive();
|
||||||
|
|
||||||
|
// The compile-environment snapshot is taken HERE, on the GL thread, and handed to
|
||||||
|
// the job. Everything the pipeline needs to know about the device comes through it,
|
||||||
|
// never through pActiveBackendObject - that is what makes the body movable.
|
||||||
|
const SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> env =
|
||||||
|
MG_Util::ShaderTranspiler::GetCurrentCompileEnv();
|
||||||
|
const Uint64 sourceHash = ShaderPreprocessCache::HashSource(*m_source);
|
||||||
|
|
||||||
|
// ---- P1 stage 6: adopt an equivalent compile instead of enqueueing a duplicate ----
|
||||||
|
// ~21% of all glCompileShader calls in the shaderpack corpus are a DIFFERENT shader
|
||||||
|
// object handed byte-identical source. P0b's memo only pays off once one of them has
|
||||||
|
// finished; under async they are all enqueued in the same burst, so without this each
|
||||||
|
// one runs the whole pipeline on its own worker. The map hands back the node the
|
||||||
|
// first of them created - in flight or already complete - and this object simply
|
||||||
|
// holds it too.
|
||||||
|
if (runOnPool && m_adoptionMap) {
|
||||||
|
if (SharedPtr<ShaderCompileTask> shared =
|
||||||
|
m_adoptionMap->FindAdoptable(m_stage, sourceHash, *m_source, env->fingerprint)) {
|
||||||
|
// Take the node's own source snapshot as ours. FindAdoptable just compared
|
||||||
|
// the two strings in full, so this changes nothing observable - but it is not
|
||||||
|
// optional: the layer-1 memo (HasMemoizedCompile) is a POINTER comparison
|
||||||
|
// against the node's snapshot, so leaving our own equal-but-distinct copy in
|
||||||
|
// place would make the very next glCompileShader on this object decide it had
|
||||||
|
// no memo and enqueue the duplicate this whole stage exists to avoid - and
|
||||||
|
// would make an identical glShaderSource re-source cancel a shared compile.
|
||||||
|
// It also collapses N copies of a ~100 KB shaderpack stage into one.
|
||||||
|
m_source = shared->source;
|
||||||
|
AdoptCompileNode(Move(shared));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::optional<String> reservedError =
|
AdoptCompileNode(MakeShared<ShaderCompileTask>(m_stage, m_source, sourceHash, env, m_preprocessCache,
|
||||||
MG_Util::ShaderTranspiler::FindReservedIdentifierViolation(compileSource);
|
m_externalIndex));
|
||||||
if (reservedError) {
|
|
||||||
m_compileStatus = false;
|
if (!runOnPool) {
|
||||||
m_shader.reset();
|
m_compiled->RunInline();
|
||||||
m_infoLog = *reservedError;
|
// Inline means the node is already terminal, so this join only replays
|
||||||
|
// diagnostics; it is here so the synchronous and asynchronous paths publish
|
||||||
|
// through the identical code.
|
||||||
|
EnsureCompileJoined();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Registered BEFORE the post, so the very next glCompileShader in this burst can
|
||||||
// Compile for OpenGL here, so that we can do validation and link
|
// adopt it however fast a worker picks it up. Registration is an index entry only -
|
||||||
// like a real OpenGL driver at linking stage
|
// the map holds a WeakPtr and never keeps a node alive.
|
||||||
// Will compile for other backends later.
|
if (m_adoptionMap) m_adoptionMap->Register(m_compiled);
|
||||||
ShaderAttrib attrib{.shaderType = MG_Util::ConvertShaderStageToGLEnum(m_stage),
|
MG_Util::Async::ShaderCompilePool::Get().Post(m_compiled);
|
||||||
.sourceStr = compileSource,
|
|
||||||
.flags = ShaderCompileBits::CompileForOpenGL};
|
|
||||||
|
|
||||||
auto result = ShaderCompiler::CompileShader(attrib);
|
|
||||||
if (result) {
|
|
||||||
m_compileStatus = true;
|
|
||||||
m_shader = result.value();
|
|
||||||
m_infoLog.clear();
|
|
||||||
} else {
|
|
||||||
m_compileStatus = false;
|
|
||||||
m_shader.reset();
|
|
||||||
m_infoLog = result.error().log;
|
|
||||||
MGLOG_D("ShaderObject::Compile: Shader %d compilation failed.\nSource:\n%s\nInfoLog:\n%s\nSetting "
|
|
||||||
"m_compileStatus = false as a result.",
|
|
||||||
m_externalIndex, compileSource.c_str(), m_infoLog.c_str());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShaderObject::MarkAsDeleted() {
|
void ShaderObject::MarkAsDeleted() {
|
||||||
|
|||||||
@@ -8,48 +8,229 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
|
#include <MG_State/GLState/ProgramState/ShaderStage.h>
|
||||||
|
#include <MG_State/GLState/ProgramState/ShaderCompileTask.h>
|
||||||
|
#include <MG_State/GLState/ProgramState/ShaderCompileAdoptionMap.h>
|
||||||
|
|
||||||
namespace MobileGL {
|
namespace MobileGL {
|
||||||
enum class ShaderStage {
|
|
||||||
Vertex,
|
|
||||||
TessControl,
|
|
||||||
TessEval,
|
|
||||||
Geometry,
|
|
||||||
Fragment,
|
|
||||||
Compute,
|
|
||||||
ShaderStageCount,
|
|
||||||
Unknown = -1
|
|
||||||
};
|
|
||||||
|
|
||||||
namespace MG_State::GLState {
|
namespace MG_State::GLState {
|
||||||
|
// The GL-visible shader name. It owns the source text and one compile job node; the
|
||||||
|
// job node owns everything a compile produces.
|
||||||
|
//
|
||||||
|
// Every member below is GL-thread-owned, and every read of worker-produced state
|
||||||
|
// goes through Compiled(), which joins first. That is invariant I5 of the P1 design:
|
||||||
|
// because Compiled() is the SOLE accessor of the node's artifacts, the compiler
|
||||||
|
// enumerates every reader for us and none can be forgotten.
|
||||||
class ShaderObject {
|
class ShaderObject {
|
||||||
public:
|
public:
|
||||||
ShaderObject(const ShaderStage stage, Uint externalIndex)
|
// `preprocessCache` is the owning context's cross-object memo (P0b layer 2).
|
||||||
: m_stage(stage), m_externalIndex(externalIndex) {}
|
// Null is fully supported and means two things at once: "no sharing", and
|
||||||
|
// "compile inline, never on a worker". Those coincide exactly - the only
|
||||||
|
// cache-less shader objects are the internal ones (ProgramObject's default
|
||||||
|
// fragment shader, the DirectVulkan blit and depth-mipmap shaders) and every one
|
||||||
|
// of them compiles and reads its status in the same breath, so a job would only
|
||||||
|
// add a round trip. Shared ownership rather than a raw pointer: a compile job
|
||||||
|
// outlives neither the object nor the context deterministically, and the cache
|
||||||
|
// has to stay alive for whoever is still reading it.
|
||||||
|
//
|
||||||
|
// `adoptionMap` is the same context's stage-6 index of adoptable compile nodes.
|
||||||
|
// It is non-null exactly when `preprocessCache` is (ProgramState hands both out
|
||||||
|
// together, and nobody else hands out either), which is what makes "no cache"
|
||||||
|
// keep meaning "compile inline, share nothing": an internal shader object has
|
||||||
|
// neither, so it neither adopts nor registers and its path is byte-identical to
|
||||||
|
// the pre-stage-6 one. GL-thread-only, so unlike the cache it carries no lock -
|
||||||
|
// shared ownership only because a ShaderObject may outlive the context's tables.
|
||||||
|
ShaderObject(const ShaderStage stage, Uint externalIndex,
|
||||||
|
SharedPtr<ShaderPreprocessCache> preprocessCache = nullptr,
|
||||||
|
SharedPtr<ShaderCompileAdoptionMap> adoptionMap = nullptr)
|
||||||
|
: m_stage(stage), m_externalIndex(externalIndex), m_preprocessCache(Move(preprocessCache)),
|
||||||
|
m_adoptionMap(Move(adoptionMap)) {}
|
||||||
|
// Cancel-not-join: the node owns its inputs, so an in-flight compile whose
|
||||||
|
// object just went away is safe to abandon where it stands. Nothing can observe
|
||||||
|
// its result any more - unless another shader object adopted the same node, or a
|
||||||
|
// link pinned it, which is precisely what ReleaseCompileNode() checks.
|
||||||
|
~ShaderObject() {
|
||||||
|
ReleaseCompileNode();
|
||||||
|
// ReleaseCompileNode KEEPS a node that has already gone terminal - there is
|
||||||
|
// nothing left to stop, so it is not a release at all. This object is going
|
||||||
|
// away regardless, so hand the adopter slot back here. That is what keeps
|
||||||
|
// ShaderCompileTask::AdopterCount() exactly "how many live ShaderObjects hold
|
||||||
|
// this node" instead of merely an upper bound.
|
||||||
|
DropCompileNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderObject(const ShaderObject&) = delete;
|
||||||
|
ShaderObject& operator=(const ShaderObject&) = delete;
|
||||||
|
|
||||||
void SetShaderSource(const String& source);
|
void SetShaderSource(const String& source);
|
||||||
void SetShaderSource(String&& source);
|
void SetShaderSource(String&& source);
|
||||||
void Compile();
|
void Compile();
|
||||||
|
// Gives up this object's claim on its compile node, cancelling the node only if
|
||||||
|
// this object was its LAST claimant. Called at the points where the object's
|
||||||
|
// compiled state stops being observable through THIS name: a real source change,
|
||||||
|
// and the release of an orphaned shader name.
|
||||||
|
//
|
||||||
|
// Named for what it does rather than for what it used to do: before stage 6 a
|
||||||
|
// node had exactly one shader object, so giving up the claim and cancelling the
|
||||||
|
// compile were the same act and this was CancelCompile(). They are not the same
|
||||||
|
// act any more - see ShaderCompileTask::AddAdopter for the count discipline and
|
||||||
|
// its single-threadedness argument. Never waits, in either case.
|
||||||
|
void ReleaseCompileNode();
|
||||||
void MarkAsDeleted();
|
void MarkAsDeleted();
|
||||||
|
|
||||||
|
// The compile job node itself, for ProgramObject::Link()'s input snapshot.
|
||||||
|
// DELIBERATELY DOES NOT JOIN, and that is the entire point of stage 4: the link
|
||||||
|
// takes the node as a dependency and is posted only once the node is terminal,
|
||||||
|
// so glLinkProgram never blocks on glCompileShader. Null means this object has
|
||||||
|
// never been compiled (or its last compile was abandoned), which the link reads
|
||||||
|
// as COMPILE_STATUS false - the same verdict the joining path produces.
|
||||||
|
//
|
||||||
|
// The caller must MarkLinkReferenced() whatever it keeps: from here on the node's
|
||||||
|
// result has an observer this object knows nothing about (see the marker's
|
||||||
|
// comment in ShaderCompileTask.h).
|
||||||
|
const SharedPtr<ShaderCompileTask>& CompiledNodeForLink() const { return m_compiled; }
|
||||||
|
|
||||||
Uint GetExternalIndex() const { return m_externalIndex; }
|
Uint GetExternalIndex() const { return m_externalIndex; }
|
||||||
ShaderStage GetShaderStage() const { return m_stage; }
|
ShaderStage GetShaderStage() const { return m_stage; }
|
||||||
const String& GetShaderSource() const { return m_source; }
|
// No join: the source is GL-thread-owned, and a worker only ever reads the
|
||||||
const SharedPtr<glslang::TShader>& GetCompiledShader() const { return m_shader; }
|
// immutable snapshot it was handed at enqueue.
|
||||||
const String& GetInfoLog() const { return m_infoLog; }
|
const String& GetShaderSource() const { return *m_source; }
|
||||||
const UnorderedMap<String, Uint>& GetUniformLocations() const { return m_uniforms; }
|
// The snapshot itself, for whoever needs to hand it to a job.
|
||||||
Bool GetCompileStatus() const { return m_compileStatus; }
|
const SharedPtr<const String>& GetShaderSourcePtr() const { return m_source; }
|
||||||
|
|
||||||
|
const SharedPtr<glslang::TShader>& GetCompiledShader() const { return Compiled().shader; }
|
||||||
|
const String& GetInfoLog() const { return Compiled().infoLog; }
|
||||||
|
// Explicit layout(location = N) qualifiers on this shader's default-block
|
||||||
|
// uniforms, captured lexically at Compile() because the relaxed parse drops
|
||||||
|
// them from reflection (see ExtractExplicitUniformLocations).
|
||||||
|
const UnorderedMap<String, Int>& GetExplicitUniformLocations() const {
|
||||||
|
return Compiled().explicitUniformLocations;
|
||||||
|
}
|
||||||
|
// Explicit layout(binding = N) on sampler/image uniforms - their initial
|
||||||
|
// texture/image units - captured lexically for the same reason (see
|
||||||
|
// ExtractExplicitOpaqueBindings).
|
||||||
|
const UnorderedMap<String, Uint>& GetExplicitOpaqueBindings() const {
|
||||||
|
return Compiled().explicitOpaqueBindings;
|
||||||
|
}
|
||||||
|
Bool GetCompileStatus() const { return Compiled().compileStatus; }
|
||||||
Bool GetDeleteStatus() const { return m_deleteStatus; }
|
Bool GetDeleteStatus() const { return m_deleteStatus; }
|
||||||
|
|
||||||
|
// Blocks until a pending compile has published its artifacts. Public for the
|
||||||
|
// sites that must join without reading anything - ProgramObject::Link's
|
||||||
|
// prologue, which needs every attached shader settled before it runs.
|
||||||
|
void JoinCompile() const { EnsureCompileJoined(); }
|
||||||
|
|
||||||
|
// True while this object holds the outcome (success OR failure) of a Compile()
|
||||||
|
// of exactly the source it currently holds - i.e. while the P0b layer-1 memo is
|
||||||
|
// armed and a glCompileShader would be a no-op. Diagnostics and tests only;
|
||||||
|
// nothing in the GL frontend branches on it.
|
||||||
|
//
|
||||||
|
// Tri-state, and deliberately NOT joining: an in-flight compile of the current
|
||||||
|
// source counts as memoized (a second glCompileShader must not enqueue a
|
||||||
|
// duplicate job), but asking that question must never block.
|
||||||
|
// A node that settled as Cancelled (the job body threw, or the enqueue failed)
|
||||||
|
// carries no result, so it must NOT satisfy the memo: otherwise a second
|
||||||
|
// glCompileShader on the same source enqueues nothing and the eventual join
|
||||||
|
// reports GL_FALSE forever. The synchronous path retries in exactly this case.
|
||||||
|
Bool HasMemoizedCompile() const {
|
||||||
|
return m_compiled != nullptr && m_compiled->source == m_source && !m_compiled->IsCancelled();
|
||||||
|
}
|
||||||
|
|
||||||
|
// MUST NOT JOIN - this is what GL_COMPLETION_STATUS_KHR will read when the
|
||||||
|
// extension surface lands. "No job at all" counts as complete: there is nothing
|
||||||
|
// outstanding to wait for.
|
||||||
|
Bool IsCompileComplete() const { return m_compiled == nullptr || m_compiled->IsTerminal(); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// ---- The one and only join gate for compile output (P1 invariant I5) ----
|
||||||
|
// The fast path - no job, or a job whose result this object has already pulled -
|
||||||
|
// is two predictable branches and stays inline: it runs on every Compiled() read
|
||||||
|
// and the project never builds with LTO, so an out-of-line body would be a real
|
||||||
|
// cross-TU call at each of those sites. The blocking half is out of line.
|
||||||
|
//
|
||||||
|
// The gate keys on "has this object pulled the job's result yet", NOT on "is the
|
||||||
|
// job terminal". Those differ in the case that matters: a worker can finish a
|
||||||
|
// compile before the GL thread ever looks at it, and the pull is where deferred
|
||||||
|
// diagnostics get replayed and an abandoned node gets dropped. Keying on
|
||||||
|
// terminality would silently skip both.
|
||||||
|
void EnsureCompileJoined() const {
|
||||||
|
if (m_compiled && !m_compileJoined) JoinPendingCompile();
|
||||||
|
}
|
||||||
|
void JoinPendingCompile() const;
|
||||||
|
|
||||||
|
// The artifacts of a compile that ran to completion. A node that was abandoned
|
||||||
|
// (cancelled at teardown, or whose body threw) never publishes: JoinPendingCompile
|
||||||
|
// drops it, so anything reachable here is either Complete or absent, and "absent"
|
||||||
|
// reads as the never-compiled defaults - COMPILE_STATUS false, empty info log,
|
||||||
|
// which is exactly what GL requires before the first glCompileShader.
|
||||||
|
static const ShaderCompileArtifacts& EmptyArtifacts() {
|
||||||
|
static const ShaderCompileArtifacts empty;
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
const ShaderCompileArtifacts& Compiled() const {
|
||||||
|
EnsureCompileJoined();
|
||||||
|
return m_compiled ? m_compiled->artifacts : EmptyArtifacts();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InvalidateCompiledState();
|
||||||
|
|
||||||
|
// ---- the ONLY two writers of m_compiled (P1 stage 6) ----
|
||||||
|
// Every adopter-count mutation lives in these two, which is what makes "exactly
|
||||||
|
// one AddAdopter per hold, exactly one ReleaseAdopter per hold" auditable rather
|
||||||
|
// than something review has to re-derive at each call site. DropCompileNode is
|
||||||
|
// null-guarded, so calling it on an object that already let go is a no-op and a
|
||||||
|
// double release is unrepresentable.
|
||||||
|
void AdoptCompileNode(SharedPtr<ShaderCompileTask> node) const;
|
||||||
|
void DropCompileNode() const;
|
||||||
|
|
||||||
|
// ---- P0b layer 1: per-object no-op recompile ----
|
||||||
|
// True iff `candidate` is byte-identical to the source that produced (or is
|
||||||
|
// producing) the compiled state this object currently holds.
|
||||||
|
Bool SourceMatchesCompiledState(const String& candidate) const;
|
||||||
|
|
||||||
|
// ---- GL-thread-owned state: never produced by a compile, so it never joins ----
|
||||||
|
const ShaderStage m_stage;
|
||||||
const Uint m_externalIndex = 0;
|
const Uint m_externalIndex = 0;
|
||||||
const ShaderStage m_stage;
|
// The pre-glShaderSource state, shared by every untouched object rather than
|
||||||
String m_source;
|
// allocated per glCreateShader.
|
||||||
SharedPtr<glslang::TShader> m_shader;
|
static const SharedPtr<const String>& EmptySource() {
|
||||||
UnorderedMap<String, Uint> m_uniforms;
|
static const SharedPtr<const String> empty = MakeShared<const String>();
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
// glShaderSource text, as an immutable snapshot. Never null. A job holds its own
|
||||||
|
// SharedPtr to the exact string it was given, so replacing the source under a
|
||||||
|
// running compile cannot race its storage - and the layer-1 memo collapses to a
|
||||||
|
// pointer comparison against the job's snapshot, because the setter only swaps
|
||||||
|
// the pointer when the text genuinely differs.
|
||||||
|
//
|
||||||
|
// Not necessarily unique to this object from stage 6 on: adopting a node also
|
||||||
|
// takes that node's source snapshot (see Compile()), so N shader objects sharing
|
||||||
|
// one compile share one copy of the text. The string is immutable and shared-
|
||||||
|
// owned, so that is invisible to every reader.
|
||||||
|
SharedPtr<const String> m_source = EmptySource();
|
||||||
|
|
||||||
|
// P0b layer 2: the owning context's cross-object memo, or null. Internally
|
||||||
|
// locked, because several workers hit it at once.
|
||||||
|
const SharedPtr<ShaderPreprocessCache> m_preprocessCache;
|
||||||
|
// P1 stage 6: the owning context's index of adoptable compile nodes, or null.
|
||||||
|
// Touched only from Compile(), i.e. only on the GL thread, so it carries no lock.
|
||||||
|
const SharedPtr<ShaderCompileAdoptionMap> m_adoptionMap;
|
||||||
|
|
||||||
String m_infoLog;
|
|
||||||
Bool m_deleteStatus = false;
|
Bool m_deleteStatus = false;
|
||||||
Bool m_compileStatus = false;
|
|
||||||
|
// ---- Compile OUTPUT ---- pending OR completed; reachable only through Compiled().
|
||||||
|
// Mutable because the join is a read-side operation: a const getter has to be
|
||||||
|
// able to settle an outstanding job before answering.
|
||||||
|
//
|
||||||
|
// SHARED from stage 6 on: several shader objects holding byte-identical source
|
||||||
|
// under the same CompileEnv point at one node. Every read below still goes
|
||||||
|
// through the same join gate, and a second joiner finds the node already
|
||||||
|
// terminal, so nothing about the read path changes - only the release path does
|
||||||
|
// (ReleaseCompileNode).
|
||||||
|
mutable SharedPtr<ShaderCompileTask> m_compiled;
|
||||||
|
// Exactly-once latch for the pull above. Armed with every new job node, set by
|
||||||
|
// the one join that consumes it.
|
||||||
|
mutable Bool m_compileJoined = false;
|
||||||
};
|
};
|
||||||
} // namespace MG_State::GLState
|
} // namespace MG_State::GLState
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.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 "ShaderPreprocessCache.h"
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
ShaderPreprocessResultPtr ShaderPreprocessCache::Find(const ShaderStage stage, const Uint64 sourceHash,
|
||||||
|
const String& source, const Uint64 envFingerprint) const {
|
||||||
|
const Key key{.stage = stage,
|
||||||
|
.sourceHash = sourceHash,
|
||||||
|
.sourceLength = source.length(),
|
||||||
|
.envFingerprint = envFingerprint};
|
||||||
|
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
const auto it = m_index.find(key);
|
||||||
|
if (it == m_index.end()) return nullptr;
|
||||||
|
|
||||||
|
// Never let correctness ride on a 64-bit hash: confirm the hit byte for byte.
|
||||||
|
// Lengths already matched (they are part of the key), so this is a plain memcmp.
|
||||||
|
const Entry& entry = *it->second;
|
||||||
|
if (entry.originalSource != source) return nullptr;
|
||||||
|
|
||||||
|
// A copy of the SharedPtr, taken under the lock: the payload now outlives any
|
||||||
|
// eviction the caller races with.
|
||||||
|
return entry.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderPreprocessCache::Insert(const ShaderStage stage, const Uint64 sourceHash, const String& source,
|
||||||
|
const Uint64 envFingerprint, ShaderPreprocessResultPtr result) {
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
const SizeT entryBytes = EntryBytes(source, *result);
|
||||||
|
// A single source bigger than the whole budget would evict every other entry and
|
||||||
|
// then itself; refuse it instead of thrashing the cache empty.
|
||||||
|
if (entryBytes > kMaxStoredSourceBytes) return;
|
||||||
|
|
||||||
|
const Key key{.stage = stage,
|
||||||
|
.sourceHash = sourceHash,
|
||||||
|
.sourceLength = source.length(),
|
||||||
|
.envFingerprint = envFingerprint};
|
||||||
|
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
if (const auto existing = m_index.find(key); existing != m_index.end()) {
|
||||||
|
// Either a re-insert of the same source (harmless) or a genuine hash collision
|
||||||
|
// with a different source. Both are resolved by letting the newcomer win: one
|
||||||
|
// entry per key keeps the index a plain map, and a collision is astronomically
|
||||||
|
// rare enough that the loser simply misses.
|
||||||
|
EraseEntryLocked(existing->second);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_entries.push_back(Entry{.key = key, .originalSource = source, .result = Move(result)});
|
||||||
|
m_index[key] = std::prev(m_entries.end());
|
||||||
|
m_storedSourceBytes += entryBytes;
|
||||||
|
|
||||||
|
EvictUntilWithinBudgetLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderPreprocessCache::Clear() {
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
m_entries.clear();
|
||||||
|
m_index.clear();
|
||||||
|
m_storedSourceBytes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderPreprocessCache::EraseEntryLocked(const EntryList::iterator it) {
|
||||||
|
const SizeT bytes = EntryBytes(it->originalSource, *it->result);
|
||||||
|
m_storedSourceBytes = bytes > m_storedSourceBytes ? 0 : m_storedSourceBytes - bytes;
|
||||||
|
m_index.erase(it->key);
|
||||||
|
m_entries.erase(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderPreprocessCache::EvictUntilWithinBudgetLocked() {
|
||||||
|
// FIFO: the oldest insertion goes first. Insert() already refuses entries larger
|
||||||
|
// than the byte budget, so this loop always terminates with at least the entry
|
||||||
|
// that was just added still resident.
|
||||||
|
while (!m_entries.empty() &&
|
||||||
|
(m_entries.size() > kMaxEntries || m_storedSourceBytes > kMaxStoredSourceBytes)) {
|
||||||
|
EraseEntryLocked(m_entries.begin());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.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 <list>
|
||||||
|
#include <mutex>
|
||||||
|
// Deliberately NOT ShaderObject.h: ShaderCompileTask.h needs this header, and ShaderObject.h
|
||||||
|
// needs ShaderCompileTask.h. Only ShaderStage was ever used from there.
|
||||||
|
#include <MG_State/GLState/ProgramState/ShaderStage.h>
|
||||||
|
#include <MG_State/GLState/ProgramState/ShaderSourceKey.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
// Where the shared, source-only half of ShaderObject::Compile() stopped. The two
|
||||||
|
// rejection verdicts are kept apart (rather than collapsed into "failed") so a hit
|
||||||
|
// reproduces the original diagnosis, not just the original info log.
|
||||||
|
enum class ShaderPreprocessOutcome : Uint8 {
|
||||||
|
// The source-only half ran clean; preprocessedSource and both maps are valid.
|
||||||
|
Preprocessed,
|
||||||
|
// ValidateComputeLocalSizeLimits rejected it (compute only).
|
||||||
|
ComputeLocalSizeRejected,
|
||||||
|
// FindReservedIdentifierViolation rejected it.
|
||||||
|
ReservedIdentifierRejected,
|
||||||
|
// The source-only half was clean but glslang rejected the preprocessed source.
|
||||||
|
// Memoizing this saves the parse itself on every later object with that source.
|
||||||
|
ParseFailed,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Everything ShaderObject::Compile() derives from the source text alone, i.e.
|
||||||
|
// everything that is identical for two shader objects holding byte-identical source.
|
||||||
|
struct ShaderPreprocessResult {
|
||||||
|
ShaderPreprocessOutcome outcome = ShaderPreprocessOutcome::Preprocessed;
|
||||||
|
// Valid unless the preprocessor itself never ran; kept even for the rejection
|
||||||
|
// outcomes because that is the text the diagnostics refer to.
|
||||||
|
String preprocessedSource;
|
||||||
|
UnorderedMap<String, Int> explicitUniformLocations;
|
||||||
|
UnorderedMap<String, Uint> explicitOpaqueBindings;
|
||||||
|
// The compile info log to publish; empty when outcome == Preprocessed.
|
||||||
|
String infoLog;
|
||||||
|
|
||||||
|
Bool Preprocessed() const { return outcome == ShaderPreprocessOutcome::Preprocessed; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cache hits hand out shared ownership, not a raw pointer into the entry list. That is
|
||||||
|
// what makes the cache safe once compiles run concurrently: a reader keeps its payload
|
||||||
|
// alive across any eviction, and a 107 KB preprocessedSource is never copied on a hit.
|
||||||
|
using ShaderPreprocessResultPtr = SharedPtr<const ShaderPreprocessResult>;
|
||||||
|
|
||||||
|
// P0b layer 2: a per-context, bounded memo of the source-only half of shader
|
||||||
|
// compilation, keyed by (stage, xxhash64(source), source length).
|
||||||
|
//
|
||||||
|
// Motivation: in the Iris shader-pack corpus ~21% of every glCompileShader in a trace
|
||||||
|
// is a *different* shader object holding byte-identical source (packs glue the same
|
||||||
|
// common/composite GLSL into many program stages), so the preprocess + reserved-
|
||||||
|
// identifier scan + explicit-location/binding extraction runs over the same megabytes
|
||||||
|
// again and again. Layer 1 (in ShaderObject) covers the same object recompiled with
|
||||||
|
// unchanged source; this covers the cross-object case.
|
||||||
|
//
|
||||||
|
// What is NOT cached: the glslang parse. glslang's TShader is consume-once (mapIO
|
||||||
|
// mutates the aliased intermediate at link), so every shader object still needs its
|
||||||
|
// own parse; only the text-processing half is shared.
|
||||||
|
//
|
||||||
|
// Correctness: the 64-bit hash is a lookup accelerator only. Every hit re-compares the
|
||||||
|
// full stored original source with memcmp before it is honored, so a hash collision
|
||||||
|
// degrades to a miss, never to a wrong answer. That is why the full original text is
|
||||||
|
// stored rather than a prefix/suffix digest - the cache is bounded, so the cost is.
|
||||||
|
//
|
||||||
|
// Eviction: FIFO (insertion order), bounded by BOTH an entry count and a stored-source
|
||||||
|
// byte budget, whichever binds first. FIFO rather than LRU because shader-pack loading
|
||||||
|
// is a burst of mostly-distinct sources whose reuse clusters around insertion time;
|
||||||
|
// LRU's extra list splice on every hit buys nothing measurable here, and FIFO keeps
|
||||||
|
// Find() a genuinely const, read-only operation.
|
||||||
|
class ShaderPreprocessCache {
|
||||||
|
public:
|
||||||
|
static constexpr SizeT kMaxEntries = 128;
|
||||||
|
static constexpr SizeT kMaxStoredSourceBytes = 8u * 1024u * 1024u;
|
||||||
|
|
||||||
|
// Returns the memoized result for this exact source under this exact compile
|
||||||
|
// environment, or null on a miss. The returned SharedPtr owns its payload, so it
|
||||||
|
// stays valid for as long as the caller holds it - across Insert(), Clear(), and
|
||||||
|
// across the destruction of the cache itself.
|
||||||
|
//
|
||||||
|
// envFingerprint joins the key because the source-only pipeline's compute
|
||||||
|
// local-size verdict is computed against CompileEnv's device limits: a memo must
|
||||||
|
// never outlive the environment it was computed against (memo-hazard rule).
|
||||||
|
ShaderPreprocessResultPtr Find(ShaderStage stage, Uint64 sourceHash, const String& source,
|
||||||
|
Uint64 envFingerprint) const;
|
||||||
|
|
||||||
|
// Memoizes `result` for this source. A source whose own storage cost already
|
||||||
|
// exceeds the byte budget is simply not cached (caching it would evict everything
|
||||||
|
// else and then itself).
|
||||||
|
void Insert(ShaderStage stage, Uint64 sourceHash, const String& source, Uint64 envFingerprint,
|
||||||
|
ShaderPreprocessResultPtr result);
|
||||||
|
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
static Uint64 HashSource(const String& source) {
|
||||||
|
return static_cast<Uint64>(XXH64(source.data(), source.length(), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeT GetEntryCount() const {
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
return m_entries.size();
|
||||||
|
}
|
||||||
|
SizeT GetStoredSourceBytes() const {
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
return m_storedSourceBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Shared with ShaderCompileAdoptionMap so the two per-context memos cannot key
|
||||||
|
// themselves on different notions of "the same compile" - see ShaderSourceKey.h.
|
||||||
|
using Key = ShaderSourceKey;
|
||||||
|
using KeyHasher = ShaderSourceKeyHasher;
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
Key key;
|
||||||
|
// The full original (pre-preprocess) source, kept so a hit can be confirmed by
|
||||||
|
// comparison instead of trusting the hash.
|
||||||
|
String originalSource;
|
||||||
|
ShaderPreprocessResultPtr result;
|
||||||
|
};
|
||||||
|
|
||||||
|
using EntryList = std::list<Entry>;
|
||||||
|
|
||||||
|
static SizeT EntryBytes(const String& source, const ShaderPreprocessResult& result) {
|
||||||
|
return source.length() + result.preprocessedSource.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EvictUntilWithinBudgetLocked();
|
||||||
|
|
||||||
|
void EraseEntryLocked(EntryList::iterator it);
|
||||||
|
|
||||||
|
// P1: every public entry point takes this. The lock alone would NOT have been
|
||||||
|
// enough - the old Find() handed back a raw pointer into an entry that a
|
||||||
|
// concurrent Insert()'s FIFO eviction could erase while the caller was still
|
||||||
|
// reading it. Shared ownership of the payload is what closes that hole; the mutex
|
||||||
|
// only protects the containers below.
|
||||||
|
mutable std::mutex m_mutex;
|
||||||
|
EntryList m_entries; // front = oldest (FIFO victim)
|
||||||
|
UnorderedMap<Key, EntryList::iterator, KeyHasher> m_index;
|
||||||
|
SizeT m_storedSourceBytes = 0;
|
||||||
|
};
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ShaderSourceKey.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 <MG_State/GLState/ProgramState/ShaderStage.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
// The identity of "one glCompileShader's worth of input" - the tuple that decides
|
||||||
|
// whether two compiles must produce byte-identical results. Shared by the two
|
||||||
|
// per-context memos keyed on it, so that neither can drift from the other:
|
||||||
|
// * P0b's ShaderPreprocessCache, which memoizes the source-only half of a compile;
|
||||||
|
// * P1 stage 6's ShaderCompileAdoptionMap, which shares the job NODE itself.
|
||||||
|
//
|
||||||
|
// The 64-bit source hash is a LOOKUP ACCELERATOR ONLY. Every user of this key confirms
|
||||||
|
// a candidate hit with a full byte comparison of the stored source before honoring it,
|
||||||
|
// so a hash collision degrades to a miss and never to a wrong answer. That rule is not
|
||||||
|
// negotiable - see the memo-hazard notes on ShaderPreprocessCache.
|
||||||
|
//
|
||||||
|
// envFingerprint is part of the identity because the pipeline's compute local-size
|
||||||
|
// verdict is computed against CompileEnv's device limits: a memo must never be handed
|
||||||
|
// back under an environment other than the one it was computed against.
|
||||||
|
struct ShaderSourceKey {
|
||||||
|
ShaderStage stage = ShaderStage::Unknown;
|
||||||
|
Uint64 sourceHash = 0;
|
||||||
|
SizeT sourceLength = 0;
|
||||||
|
Uint64 envFingerprint = 0;
|
||||||
|
|
||||||
|
Bool operator==(const ShaderSourceKey& other) const {
|
||||||
|
return stage == other.stage && sourceHash == other.sourceHash &&
|
||||||
|
sourceLength == other.sourceLength && envFingerprint == other.envFingerprint;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ShaderSourceKeyHasher {
|
||||||
|
SizeT operator()(const ShaderSourceKey& key) const {
|
||||||
|
// The source hash already spreads well; fold the three discriminators in so
|
||||||
|
// that same-hash-different-stage/length/env keys land in different buckets.
|
||||||
|
Uint64 mixed = key.sourceHash;
|
||||||
|
mixed ^= static_cast<Uint64>(key.sourceLength) + 0x9e3779b97f4a7c15ull + (mixed << 6) + (mixed >> 2);
|
||||||
|
mixed ^= static_cast<Uint64>(static_cast<Int>(key.stage)) * 0xff51afd7ed558ccdull;
|
||||||
|
mixed ^= key.envFingerprint + 0x9e3779b97f4a7c15ull + (mixed << 6) + (mixed >> 2);
|
||||||
|
return static_cast<SizeT>(mixed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ShaderStage.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
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
// Split out of ShaderObject.h so the compile pipeline's headers form a DAG:
|
||||||
|
// ShaderStage.h <- ShaderPreprocessCache.h <- ShaderCompileTask.h <- ShaderObject.h.
|
||||||
|
// Every existing includer of ShaderObject.h still sees this type unchanged.
|
||||||
|
enum class ShaderStage {
|
||||||
|
Vertex,
|
||||||
|
TessControl,
|
||||||
|
TessEval,
|
||||||
|
Geometry,
|
||||||
|
Fragment,
|
||||||
|
Compute,
|
||||||
|
ShaderStageCount,
|
||||||
|
Unknown = -1
|
||||||
|
};
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
#include "SamplerObject.h"
|
#include "SamplerObject.h"
|
||||||
|
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
namespace MobileGL {
|
namespace MobileGL {
|
||||||
@@ -22,81 +24,91 @@ namespace MobileGL {
|
|||||||
SamplerObject::SamplerObject(Uint externalIndex)
|
SamplerObject::SamplerObject(Uint externalIndex)
|
||||||
: m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {}
|
: m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {}
|
||||||
|
|
||||||
|
void SamplerObject::BumpVersion() {
|
||||||
|
++m_version;
|
||||||
|
// Every setter early-outs on an unchanged value, so this only runs on a real
|
||||||
|
// parameter change. The generation is bumped for ALL parameters, not just filter
|
||||||
|
// ones that feed mipmap-completeness: a backend memo of the resolved per-unit
|
||||||
|
// bindings must never miss an invalidation, and over-invalidating on a wrap-mode
|
||||||
|
// write costs one re-resolve.
|
||||||
|
if (pGLContext) pGLContext->BumpSamplingResolutionGeneration();
|
||||||
|
}
|
||||||
|
|
||||||
void SamplerObject::SetWrapS(SamplerWrapMode mode) {
|
void SamplerObject::SetWrapS(SamplerWrapMode mode) {
|
||||||
if (mode == m_samplerParameters.wrapS) return;
|
if (mode == m_samplerParameters.wrapS) return;
|
||||||
|
|
||||||
m_samplerParameters.wrapS = mode;
|
m_samplerParameters.wrapS = mode;
|
||||||
++m_version;
|
BumpVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SamplerObject::SetWrapT(SamplerWrapMode mode) {
|
void SamplerObject::SetWrapT(SamplerWrapMode mode) {
|
||||||
if (mode == m_samplerParameters.wrapT) return;
|
if (mode == m_samplerParameters.wrapT) return;
|
||||||
|
|
||||||
m_samplerParameters.wrapT = mode;
|
m_samplerParameters.wrapT = mode;
|
||||||
++m_version;
|
BumpVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SamplerObject::SetWrapR(SamplerWrapMode mode) {
|
void SamplerObject::SetWrapR(SamplerWrapMode mode) {
|
||||||
if (mode == m_samplerParameters.wrapR) return;
|
if (mode == m_samplerParameters.wrapR) return;
|
||||||
|
|
||||||
m_samplerParameters.wrapR = mode;
|
m_samplerParameters.wrapR = mode;
|
||||||
++m_version;
|
BumpVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SamplerObject::SetMinFilter(SamplerFilterMode mode) {
|
void SamplerObject::SetMinFilter(SamplerFilterMode mode) {
|
||||||
if (mode == m_samplerParameters.minFilter) return;
|
if (mode == m_samplerParameters.minFilter) return;
|
||||||
|
|
||||||
m_samplerParameters.minFilter = mode;
|
m_samplerParameters.minFilter = mode;
|
||||||
++m_version;
|
BumpVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SamplerObject::SetMagFilter(SamplerFilterMode mode) {
|
void SamplerObject::SetMagFilter(SamplerFilterMode mode) {
|
||||||
if (mode == m_samplerParameters.magFilter) return;
|
if (mode == m_samplerParameters.magFilter) return;
|
||||||
|
|
||||||
m_samplerParameters.magFilter = mode;
|
m_samplerParameters.magFilter = mode;
|
||||||
++m_version;
|
BumpVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SamplerObject::SetMipmapMode(SamplerMipmapMode mode) {
|
void SamplerObject::SetMipmapMode(SamplerMipmapMode mode) {
|
||||||
if (mode == m_samplerParameters.mipmapMode) return;
|
if (mode == m_samplerParameters.mipmapMode) return;
|
||||||
|
|
||||||
m_samplerParameters.mipmapMode = mode;
|
m_samplerParameters.mipmapMode = mode;
|
||||||
++m_version;
|
BumpVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SamplerObject::SetLodRange(Float minLod, Float maxLod) {
|
void SamplerObject::SetLodRange(Float minLod, Float maxLod) {
|
||||||
if (minLod == m_samplerParameters.minLod && maxLod == m_samplerParameters.maxLod) return;
|
if (minLod == m_samplerParameters.minLod && maxLod == m_samplerParameters.maxLod) return;
|
||||||
m_samplerParameters.minLod = minLod;
|
m_samplerParameters.minLod = minLod;
|
||||||
m_samplerParameters.maxLod = maxLod;
|
m_samplerParameters.maxLod = maxLod;
|
||||||
++m_version;
|
BumpVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SamplerObject::SetLodBias(Float bias) {
|
void SamplerObject::SetLodBias(Float bias) {
|
||||||
if (bias == m_samplerParameters.lodBias) return;
|
if (bias == m_samplerParameters.lodBias) return;
|
||||||
|
|
||||||
m_samplerParameters.lodBias = bias;
|
m_samplerParameters.lodBias = bias;
|
||||||
++m_version;
|
BumpVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SamplerObject::SetMaxAnisotropy(Float maxAnisotropy) {
|
void SamplerObject::SetMaxAnisotropy(Float maxAnisotropy) {
|
||||||
if (maxAnisotropy == m_samplerParameters.maxAnisotropy) return;
|
if (maxAnisotropy == m_samplerParameters.maxAnisotropy) return;
|
||||||
|
|
||||||
m_samplerParameters.maxAnisotropy = maxAnisotropy;
|
m_samplerParameters.maxAnisotropy = maxAnisotropy;
|
||||||
++m_version;
|
BumpVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SamplerObject::SetSamplerCompareFunc(SamplerCompareFunc func) {
|
void SamplerObject::SetSamplerCompareFunc(SamplerCompareFunc func) {
|
||||||
if (func == m_samplerParameters.compareFunc) return;
|
if (func == m_samplerParameters.compareFunc) return;
|
||||||
|
|
||||||
m_samplerParameters.compareFunc = func;
|
m_samplerParameters.compareFunc = func;
|
||||||
++m_version;
|
BumpVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SamplerObject::SetCompareMode(SamplerCompareMode mode) {
|
void SamplerObject::SetCompareMode(SamplerCompareMode mode) {
|
||||||
if (mode == m_samplerParameters.compareMode) return;
|
if (mode == m_samplerParameters.compareMode) return;
|
||||||
|
|
||||||
m_samplerParameters.compareMode = mode;
|
m_samplerParameters.compareMode = mode;
|
||||||
++m_version;
|
BumpVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
SamplerWrapMode SamplerObject::GetWrapS() const {
|
SamplerWrapMode SamplerObject::GetWrapS() const {
|
||||||
@@ -153,7 +165,7 @@ namespace MobileGL {
|
|||||||
m_samplerParameters.borderColorUI =
|
m_samplerParameters.borderColorUI =
|
||||||
UintVec4(static_cast<Uint32>(color.x()), static_cast<Uint32>(color.y()),
|
UintVec4(static_cast<Uint32>(color.x()), static_cast<Uint32>(color.y()),
|
||||||
static_cast<Uint32>(color.z()), static_cast<Uint32>(color.w()));
|
static_cast<Uint32>(color.z()), static_cast<Uint32>(color.w()));
|
||||||
++m_version;
|
BumpVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SamplerObject::SetBorderColorI(const IntVec4& color) {
|
void SamplerObject::SetBorderColorI(const IntVec4& color) {
|
||||||
@@ -166,7 +178,7 @@ namespace MobileGL {
|
|||||||
m_samplerParameters.borderColor =
|
m_samplerParameters.borderColor =
|
||||||
FloatVec4(static_cast<Float>(color.x()), static_cast<Float>(color.y()),
|
FloatVec4(static_cast<Float>(color.x()), static_cast<Float>(color.y()),
|
||||||
static_cast<Float>(color.z()), static_cast<Float>(color.w()));
|
static_cast<Float>(color.z()), static_cast<Float>(color.w()));
|
||||||
++m_version;
|
BumpVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SamplerObject::SetBorderColorUI(const UintVec4& color) {
|
void SamplerObject::SetBorderColorUI(const UintVec4& color) {
|
||||||
@@ -179,7 +191,7 @@ namespace MobileGL {
|
|||||||
m_samplerParameters.borderColor =
|
m_samplerParameters.borderColor =
|
||||||
FloatVec4(static_cast<Float>(color.x()), static_cast<Float>(color.y()),
|
FloatVec4(static_cast<Float>(color.x()), static_cast<Float>(color.y()),
|
||||||
static_cast<Float>(color.z()), static_cast<Float>(color.w()));
|
static_cast<Float>(color.z()), static_cast<Float>(color.w()));
|
||||||
++m_version;
|
BumpVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
const FloatVec4& SamplerObject::GetBorderColor() const {
|
const FloatVec4& SamplerObject::GetBorderColor() const {
|
||||||
|
|||||||
@@ -128,6 +128,12 @@ namespace MobileGL {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
static Uint64 AllocateLifetimeId();
|
static Uint64 AllocateLifetimeId();
|
||||||
|
// The ONLY way m_version may move. Besides marking this object's parameters
|
||||||
|
// dirty for the backends it bumps the context-wide sampling-resolution
|
||||||
|
// generation: MIN_FILTER decides whether a lookup reads the mip chain, which
|
||||||
|
// decides whether a bound texture is mipmap-complete, which decides whether a
|
||||||
|
// backend binds it on its unit at all.
|
||||||
|
void BumpVersion();
|
||||||
|
|
||||||
const Uint m_externalIndex;
|
const Uint m_externalIndex;
|
||||||
const Uint64 m_lifetimeId;
|
const Uint64 m_lifetimeId;
|
||||||
|
|||||||
@@ -11,6 +11,23 @@
|
|||||||
namespace MobileGL {
|
namespace MobileGL {
|
||||||
namespace MG_State {
|
namespace MG_State {
|
||||||
namespace GLState {
|
namespace GLState {
|
||||||
|
namespace {
|
||||||
|
// Overlapping OR abutting ([lo, hi) intervals meeting edge-to-edge) in
|
||||||
|
// every axis: merging abutting boxes keeps scanline/tile write patterns
|
||||||
|
// as one rect instead of a picket fence.
|
||||||
|
Bool RegionsTouch(const MipmapDirtyRegion& a, const MipmapDirtyRegion& b) {
|
||||||
|
return a.lo.x() <= b.hi.x() && b.lo.x() <= a.hi.x() && a.lo.y() <= b.hi.y() &&
|
||||||
|
b.lo.y() <= a.hi.y() && a.lo.z() <= b.hi.z() && b.lo.z() <= a.hi.z();
|
||||||
|
}
|
||||||
|
|
||||||
|
MipmapDirtyRegion RegionUnion(const MipmapDirtyRegion& a, const MipmapDirtyRegion& b) {
|
||||||
|
return {IntVec3{std::min(a.lo.x(), b.lo.x()), std::min(a.lo.y(), b.lo.y()),
|
||||||
|
std::min(a.lo.z(), b.lo.z())},
|
||||||
|
IntVec3{std::max(a.hi.x(), b.hi.x()), std::max(a.hi.y(), b.hi.y()),
|
||||||
|
std::max(a.hi.z(), b.hi.z())}};
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
SizeT MipmapStorage::GetLevelCount() const {
|
SizeT MipmapStorage::GetLevelCount() const {
|
||||||
return m_data.size();
|
return m_data.size();
|
||||||
}
|
}
|
||||||
@@ -28,6 +45,7 @@ namespace MobileGL {
|
|||||||
m_texelSizes.resize(requiredLevelCount);
|
m_texelSizes.resize(requiredLevelCount);
|
||||||
m_isDirty.resize(requiredLevelCount, false);
|
m_isDirty.resize(requiredLevelCount, false);
|
||||||
m_dirtyRegions.resize(requiredLevelCount);
|
m_dirtyRegions.resize(requiredLevelCount);
|
||||||
|
m_dirtyRects.resize(requiredLevelCount);
|
||||||
m_compressedData.resize(requiredLevelCount);
|
m_compressedData.resize(requiredLevelCount);
|
||||||
m_compressedFormats.resize(requiredLevelCount, GL_NONE);
|
m_compressedFormats.resize(requiredLevelCount, GL_NONE);
|
||||||
}
|
}
|
||||||
@@ -44,6 +62,11 @@ namespace MobileGL {
|
|||||||
std::max(input.texelSize.z(), 1)}}
|
std::max(input.texelSize.z(), 1)}}
|
||||||
: MipmapDirtyRegion{};
|
: MipmapDirtyRegion{};
|
||||||
}
|
}
|
||||||
|
// The rect list mirrors the union box's reset: whatever rects were
|
||||||
|
// pending measured the OLD extents. Empty list = union box tells all.
|
||||||
|
if (level < m_dirtyRects.size()) {
|
||||||
|
m_dirtyRects[level].clear();
|
||||||
|
}
|
||||||
auto& data = m_data[level];
|
auto& data = m_data[level];
|
||||||
data.resize(input.byteSize, 0);
|
data.resize(input.byteSize, 0);
|
||||||
|
|
||||||
@@ -94,6 +117,7 @@ namespace MobileGL {
|
|||||||
m_texelSizes.resize(levelCount);
|
m_texelSizes.resize(levelCount);
|
||||||
m_isDirty.resize(levelCount);
|
m_isDirty.resize(levelCount);
|
||||||
m_dirtyRegions.resize(levelCount);
|
m_dirtyRegions.resize(levelCount);
|
||||||
|
m_dirtyRects.resize(levelCount);
|
||||||
m_compressedData.resize(levelCount);
|
m_compressedData.resize(levelCount);
|
||||||
m_compressedFormats.resize(levelCount);
|
m_compressedFormats.resize(levelCount);
|
||||||
}
|
}
|
||||||
@@ -142,6 +166,13 @@ namespace MobileGL {
|
|||||||
m_dirtyRegions[level] = {};
|
m_dirtyRegions[level] = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Both directions collapse the rect list to "just the union box": a
|
||||||
|
// whole-level dirty IS the union box, a clean level has nothing to say.
|
||||||
|
// clear() keeps the vector's capacity, so per-frame streaming levels
|
||||||
|
// allocate their slots once and reuse them.
|
||||||
|
if (level < m_dirtyRects.size()) {
|
||||||
|
m_dirtyRects[level].clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MipmapStorage::IsDirty(Uint level) const {
|
bool MipmapStorage::IsDirty(Uint level) const {
|
||||||
@@ -158,6 +189,20 @@ namespace MobileGL {
|
|||||||
std::min(offset.y() + size.y(), levelSize.y()),
|
std::min(offset.y() + size.y(), levelSize.y()),
|
||||||
std::min(offset.z() + std::max(size.z(), 1), std::max(levelSize.z(), 1))};
|
std::min(offset.z() + std::max(size.z(), 1), std::max(levelSize.z(), 1))};
|
||||||
if (incoming.Empty()) return;
|
if (incoming.Empty()) return;
|
||||||
|
// Rect list first, while the union box still holds only the PREVIOUS
|
||||||
|
// writes: a level that is already dirty with an empty list is in the
|
||||||
|
// "union box tells all" resting state, so that box seeds the list
|
||||||
|
// before the incoming rect refines it.
|
||||||
|
if (level < m_dirtyRects.size()) {
|
||||||
|
auto& rects = m_dirtyRects[level];
|
||||||
|
if (!m_isDirty[level]) {
|
||||||
|
rects.clear(); // stale-safety; MarkDirty(false) already cleared it
|
||||||
|
} else if (rects.empty() && level < m_dirtyRegions.size() &&
|
||||||
|
!m_dirtyRegions[level].Empty()) {
|
||||||
|
rects.push_back(m_dirtyRegions[level]);
|
||||||
|
}
|
||||||
|
InsertDirtyRect(level, incoming);
|
||||||
|
}
|
||||||
if (level < m_dirtyRegions.size()) {
|
if (level < m_dirtyRegions.size()) {
|
||||||
MipmapDirtyRegion& region = m_dirtyRegions[level];
|
MipmapDirtyRegion& region = m_dirtyRegions[level];
|
||||||
if (m_isDirty[level] && !region.Empty()) {
|
if (m_isDirty[level] && !region.Empty()) {
|
||||||
@@ -174,10 +219,82 @@ namespace MobileGL {
|
|||||||
m_isDirty[level] = true;
|
m_isDirty[level] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MipmapStorage::InsertDirtyRect(Uint level, MipmapDirtyRegion incoming) {
|
||||||
|
auto& rects = m_dirtyRects[level];
|
||||||
|
if (rects.capacity() < kMaxDirtyRects) {
|
||||||
|
rects.reserve(kMaxDirtyRects);
|
||||||
|
}
|
||||||
|
// Cascade-merge: absorb every rect the incoming touches. The absorbed
|
||||||
|
// union can reach rects a smaller box did not, so rescan until stable;
|
||||||
|
// every merge shrinks the list, so this terminates. Swap-with-back keeps
|
||||||
|
// removal O(1) - the list is unordered by design.
|
||||||
|
Bool merged = true;
|
||||||
|
while (merged) {
|
||||||
|
merged = false;
|
||||||
|
for (SizeT i = 0; i < rects.size(); ++i) {
|
||||||
|
if (RegionsTouch(rects[i], incoming)) {
|
||||||
|
incoming = RegionUnion(rects[i], incoming);
|
||||||
|
rects[i] = rects.back();
|
||||||
|
rects.pop_back();
|
||||||
|
merged = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rects.size() < kMaxDirtyRects) {
|
||||||
|
rects.push_back(incoming);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Full: fold the incoming rect into the neighbour whose box grows least
|
||||||
|
// (least new area dragged into the upload), then re-insert the grown
|
||||||
|
// box - it may now touch others. The removal above guarantees the
|
||||||
|
// recursion appends on the second pass at the latest.
|
||||||
|
SizeT best = 0;
|
||||||
|
SizeT bestGrowth = ~static_cast<SizeT>(0);
|
||||||
|
for (SizeT i = 0; i < rects.size(); ++i) {
|
||||||
|
const SizeT growth = RegionUnion(rects[i], incoming).TexelCount() - rects[i].TexelCount();
|
||||||
|
if (growth < bestGrowth) {
|
||||||
|
bestGrowth = growth;
|
||||||
|
best = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
incoming = RegionUnion(rects[best], incoming);
|
||||||
|
rects[best] = rects.back();
|
||||||
|
rects.pop_back();
|
||||||
|
InsertDirtyRect(level, incoming);
|
||||||
|
}
|
||||||
|
|
||||||
MipmapDirtyRegion MipmapStorage::GetDirtyRegion(Uint level) const {
|
MipmapDirtyRegion MipmapStorage::GetDirtyRegion(Uint level) const {
|
||||||
if (level >= m_dirtyRegions.size()) return {};
|
if (level >= m_dirtyRegions.size()) return {};
|
||||||
return m_dirtyRegions[level];
|
return m_dirtyRegions[level];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SizeT MipmapStorage::GetDirtyRects(Uint level, MipmapDirtyRegion* outRects, SizeT maxRects) const {
|
||||||
|
if (outRects == nullptr || level >= m_dirtyRects.size() || level >= m_dirtyRegions.size()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const auto& rects = m_dirtyRects[level];
|
||||||
|
// 0 or 1 rects: the union box already says exactly this. More than the
|
||||||
|
// caller can take: never truncate - a dropped rect is a dropped write.
|
||||||
|
if (rects.size() < 2 || rects.size() > maxRects) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// Total-bytes accounting: when the scattered rects add up to most of
|
||||||
|
// the union box anyway (>= 3/4), one driver call on the box beats many
|
||||||
|
// calls moving nearly the same bytes.
|
||||||
|
SizeT summedArea = 0;
|
||||||
|
for (const auto& rect : rects) {
|
||||||
|
summedArea += rect.TexelCount();
|
||||||
|
}
|
||||||
|
const SizeT unionArea = m_dirtyRegions[level].TexelCount();
|
||||||
|
if (summedArea * 4 >= unionArea * 3) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
for (SizeT i = 0; i < rects.size(); ++i) {
|
||||||
|
outRects[i] = rects[i];
|
||||||
|
}
|
||||||
|
return rects.size();
|
||||||
|
}
|
||||||
} // namespace GLState
|
} // namespace GLState
|
||||||
} // namespace MG_State
|
} // namespace MG_State
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ namespace MobileGL {
|
|||||||
return lo.x() <= 0 && lo.y() <= 0 && lo.z() <= 0 && hi.x() >= levelSize.x() &&
|
return lo.x() <= 0 && lo.y() <= 0 && lo.z() <= 0 && hi.x() >= levelSize.x() &&
|
||||||
hi.y() >= levelSize.y() && hi.z() >= std::max(levelSize.z(), 1);
|
hi.y() >= levelSize.y() && hi.z() >= std::max(levelSize.z(), 1);
|
||||||
}
|
}
|
||||||
|
SizeT TexelCount() const {
|
||||||
|
if (Empty()) return 0;
|
||||||
|
return static_cast<SizeT>(hi.x() - lo.x()) * static_cast<SizeT>(hi.y() - lo.y()) *
|
||||||
|
static_cast<SizeT>(hi.z() - lo.z());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class MipmapStorage {
|
class MipmapStorage {
|
||||||
@@ -54,6 +59,28 @@ namespace MobileGL {
|
|||||||
// Meaningful only while IsDirty(level).
|
// Meaningful only while IsDirty(level).
|
||||||
MipmapDirtyRegion GetDirtyRegion(Uint level) const;
|
MipmapDirtyRegion GetDirtyRegion(Uint level) const;
|
||||||
|
|
||||||
|
// Behind the union box, the level keeps up to kMaxDirtyRects pairwise
|
||||||
|
// disjoint rects recording WHERE the writes actually landed. A frame of
|
||||||
|
// ~100 scattered sprite updates in a big atlas has a union box that
|
||||||
|
// covers nearly the whole level while the touched texels are ~5% of it;
|
||||||
|
// the union box stays the source of truth (every write funnels through
|
||||||
|
// MarkDirty/MarkDirtyRegion into BOTH representations), backends OPT IN
|
||||||
|
// to the list purely as an upload-size refinement. 96 slots because the
|
||||||
|
// pattern this exists for is Minecraft's ~100 sprites/frame: a 16-slot
|
||||||
|
// list forced into far-apart merges was measured at >90% of the union
|
||||||
|
// box's area on exactly that pattern, i.e. worthless. Inserts merge any
|
||||||
|
// touching/overlapping rect (cascading, so the list stays disjoint);
|
||||||
|
// when full, the incoming rect folds into the neighbour whose box grows
|
||||||
|
// least and the list degrades gracefully toward the union box.
|
||||||
|
static constexpr SizeT kMaxDirtyRects = 96;
|
||||||
|
// Copies the level's dirty rects into outRects and returns how many were
|
||||||
|
// written. 0 means "upload the union box instead" and covers every
|
||||||
|
// reason at once: tracking unavailable, a single rect (identical to the
|
||||||
|
// union box by construction), more rects than maxRects, or a summed
|
||||||
|
// area so close to the union box's that one big upload beats many small
|
||||||
|
// ones (fewer driver calls wins when the bytes are nearly equal).
|
||||||
|
SizeT GetDirtyRects(Uint level, MipmapDirtyRegion* outRects, SizeT maxRects) const;
|
||||||
|
|
||||||
// The bytes an application handed to glCompressedTexImage*, kept verbatim beside the
|
// The bytes an application handed to glCompressedTexImage*, kept verbatim beside the
|
||||||
// (uncompressed) texel shadow rather than in place of it. GL 4.6 core 8.11 requires
|
// (uncompressed) texel shadow rather than in place of it. GL 4.6 core 8.11 requires
|
||||||
// glGetCompressedTexImage to return the image *as stored*, and no backend here has a
|
// glGetCompressedTexImage to return the image *as stored*, and no backend here has a
|
||||||
@@ -70,10 +97,22 @@ namespace MobileGL {
|
|||||||
const void* MapCompressedData(Uint level) const;
|
const void* MapCompressedData(Uint level) const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
// Insert one clamped, non-empty write box, keeping the list disjoint
|
||||||
|
// and bounded (see kMaxDirtyRects).
|
||||||
|
void InsertDirtyRect(Uint level, MipmapDirtyRegion incoming);
|
||||||
|
|
||||||
Vector<IntVec3> m_texelSizes;
|
Vector<IntVec3> m_texelSizes;
|
||||||
Vector<Vector<Uint8>> m_data;
|
Vector<Vector<Uint8>> m_data;
|
||||||
Vector<bool> m_isDirty;
|
Vector<bool> m_isDirty;
|
||||||
Vector<MipmapDirtyRegion> m_dirtyRegions;
|
Vector<MipmapDirtyRegion> m_dirtyRegions;
|
||||||
|
// Per level, the disjoint rect list behind m_dirtyRegions' union box.
|
||||||
|
// An EMPTY list is the common resting state and always means "the union
|
||||||
|
// box is the whole story" - clean levels, whole-level dirties and
|
||||||
|
// respecifies all just clear it, so plain full-level uploads never pay
|
||||||
|
// a heap allocation; the first scattered MarkDirtyRegion on an
|
||||||
|
// already-dirty level seeds the list from the union box accumulated so
|
||||||
|
// far and refines from there.
|
||||||
|
Vector<Vector<MipmapDirtyRegion>> m_dirtyRects;
|
||||||
Vector<Vector<Uint8>> m_compressedData;
|
Vector<Vector<Uint8>> m_compressedData;
|
||||||
Vector<GLenum> m_compressedFormats;
|
Vector<GLenum> m_compressedFormats;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -84,6 +84,12 @@ namespace MobileGL {
|
|||||||
return m_storage[targetIndex].GetDirtyRegion(level);
|
return m_storage[targetIndex].GetDirtyRegion(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SizeT GetDirtyRects(Uint targetIndex, Uint level, MipmapDirtyRegion* outRects,
|
||||||
|
SizeT maxRects) const {
|
||||||
|
MOBILEGL_ASSERT(targetIndex < TargetCount, "GetDirtyRects: target invalid");
|
||||||
|
return m_storage[targetIndex].GetDirtyRects(level, outRects, maxRects);
|
||||||
|
}
|
||||||
|
|
||||||
void SetCompressedImage(Uint targetIndex, Uint level, GLenum internalFormat, const void* data,
|
void SetCompressedImage(Uint targetIndex, Uint level, GLenum internalFormat, const void* data,
|
||||||
SizeT size) {
|
SizeT size) {
|
||||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "SetCompressedImage: target invalid");
|
MOBILEGL_ASSERT(targetIndex < TargetCount, "SetCompressedImage: target invalid");
|
||||||
|
|||||||
@@ -25,6 +25,18 @@ namespace MobileGL {
|
|||||||
return s_nextTextureLifetimeId.fetch_add(1, std::memory_order_relaxed);
|
return s_nextTextureLifetimeId.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TextureObjectBase::BumpShapeVersion() {
|
||||||
|
++m_shapeVersion;
|
||||||
|
// Shape is what mipmap-completeness is computed from, and completeness decides
|
||||||
|
// whether a backend binds this texture on its unit at all. Nothing else tells a
|
||||||
|
// backend memo of the resolved per-unit bindings that the answer moved - no bind
|
||||||
|
// changed and the texel content may be untouched. Proxy textures (used only to
|
||||||
|
// answer PROXY queries) are never bound, so their shape churn costs a memo
|
||||||
|
// invalidation for nothing; that is accepted rather than filtered, because a
|
||||||
|
// missed bump renders wrong pixels while a spare bump only costs one re-resolve.
|
||||||
|
if (pGLContext) pGLContext->BumpSamplingResolutionGeneration();
|
||||||
|
}
|
||||||
|
|
||||||
TextureObjectBase::TextureObjectBase(TextureTarget target, Uint externalIndex)
|
TextureObjectBase::TextureObjectBase(TextureTarget target, Uint externalIndex)
|
||||||
: m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()), m_target(target) {
|
: m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()), m_target(target) {
|
||||||
m_sampler = MakeShared<SamplerObject>(0);
|
m_sampler = MakeShared<SamplerObject>(0);
|
||||||
@@ -85,7 +97,7 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_internalFormat = format;
|
m_internalFormat = format;
|
||||||
++m_shapeVersion;
|
BumpShapeVersion();
|
||||||
++m_textureParamsVersion;
|
++m_textureParamsVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +209,7 @@ namespace MobileGL {
|
|||||||
m_levelRange.y() = m_levelRange.x();
|
m_levelRange.y() = m_levelRange.x();
|
||||||
}
|
}
|
||||||
++m_textureParamsVersion;
|
++m_textureParamsVersion;
|
||||||
++m_shapeVersion;
|
BumpShapeVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextureObjectBase::SetMaxLevel(Uint maxLevel) {
|
void TextureObjectBase::SetMaxLevel(Uint maxLevel) {
|
||||||
@@ -208,7 +220,7 @@ namespace MobileGL {
|
|||||||
|
|
||||||
m_levelRange.y() = maxLevel;
|
m_levelRange.y() = maxLevel;
|
||||||
++m_textureParamsVersion;
|
++m_textureParamsVersion;
|
||||||
++m_shapeVersion;
|
BumpShapeVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool TextureObjectBase::IsImmutable() const {
|
Bool TextureObjectBase::IsImmutable() const {
|
||||||
@@ -291,12 +303,12 @@ namespace MobileGL {
|
|||||||
|
|
||||||
void TextureObjectWithOneMipmap::AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
void TextureObjectWithOneMipmap::AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||||
MipmapInput input) {
|
MipmapInput input) {
|
||||||
++m_shapeVersion;
|
BumpShapeVersion();
|
||||||
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextureObjectWithOneMipmap::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
|
void TextureObjectWithOneMipmap::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
|
||||||
++m_shapeVersion;
|
BumpShapeVersion();
|
||||||
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
|
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,6 +345,13 @@ namespace MobileGL {
|
|||||||
return m_textureStorage.GetDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
return m_textureStorage.GetDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SizeT TextureObjectWithOneMipmap::GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||||
|
MipmapDirtyRegion* outRects,
|
||||||
|
SizeT maxRects) const {
|
||||||
|
return m_textureStorage.GetDirtyRects(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
||||||
|
outRects, maxRects);
|
||||||
|
}
|
||||||
|
|
||||||
void TextureObjectWithOneMipmap::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
void TextureObjectWithOneMipmap::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||||
GLenum internalFormat, const void* data, SizeT size) {
|
GLenum internalFormat, const void* data, SizeT size) {
|
||||||
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
||||||
|
|||||||
@@ -121,6 +121,12 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
static Uint64 AllocateLifetimeId();
|
static Uint64 AllocateLifetimeId();
|
||||||
|
// The ONLY way m_shapeVersion may move. Besides invalidating this object's own
|
||||||
|
// completeness memo it bumps the context-wide sampling-resolution generation, which is
|
||||||
|
// what a backend memo of the resolved per-unit bindings watches: completeness decides
|
||||||
|
// whether a bound texture reaches its native target at all, and a shape change is
|
||||||
|
// otherwise invisible to such a memo (no bind moved).
|
||||||
|
void BumpShapeVersion();
|
||||||
|
|
||||||
const Uint m_externalIndex;
|
const Uint m_externalIndex;
|
||||||
const Uint64 m_lifetimeId;
|
const Uint64 m_lifetimeId;
|
||||||
@@ -180,6 +186,20 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
const IntVec3 size = GetMipmapTexelSize(uploadTarget, mipmapLevel);
|
const IntVec3 size = GetMipmapTexelSize(uploadTarget, mipmapLevel);
|
||||||
return {IntVec3{0, 0, 0}, IntVec3{size.x(), size.y(), std::max(size.z(), 1)}};
|
return {IntVec3{0, 0, 0}, IntVec3{size.x(), size.y(), std::max(size.z(), 1)}};
|
||||||
}
|
}
|
||||||
|
// Scatter detail behind GetStorageDirtyRegion: up to maxRects disjoint rects
|
||||||
|
// that together cover every dirty texel, so ~100 sprite writes in a big atlas
|
||||||
|
// need not be uploaded as one atlas-sized box. Returns how many rects were
|
||||||
|
// written to outRects; 0 means "no list, upload the union box" and is always a
|
||||||
|
// safe answer - this base fallback keeps whole-level semantics for storage
|
||||||
|
// classes that do not track rects, and backends OPT IN by calling this.
|
||||||
|
virtual SizeT GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||||
|
MipmapDirtyRegion* outRects, SizeT maxRects) const {
|
||||||
|
(void)uploadTarget;
|
||||||
|
(void)mipmapLevel;
|
||||||
|
(void)outRects;
|
||||||
|
(void)maxRects;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// The compressed image a glCompressedTexImage* call shadowed for this level, kept verbatim
|
// The compressed image a glCompressedTexImage* call shadowed for this level, kept verbatim
|
||||||
// next to the texel data rather than instead of it - see MipmapStorage. The texel shadow
|
// next to the texel data rather than instead of it - see MipmapStorage. The texel shadow
|
||||||
@@ -251,6 +271,8 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
|
void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
|
||||||
IntVec3 size) override;
|
IntVec3 size) override;
|
||||||
MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||||
|
SizeT GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapDirtyRegion* outRects,
|
||||||
|
SizeT maxRects) const override;
|
||||||
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat,
|
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat,
|
||||||
const void* data, SizeT size) override;
|
const void* data, SizeT size) override;
|
||||||
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ namespace MobileGL {
|
|||||||
|
|
||||||
void TextureObject2DCube::AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
void TextureObject2DCube::AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||||
MipmapInput input) {
|
MipmapInput input) {
|
||||||
++m_shapeVersion;
|
BumpShapeVersion();
|
||||||
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextureObject2DCube::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
|
void TextureObject2DCube::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
|
||||||
++m_shapeVersion;
|
BumpShapeVersion();
|
||||||
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
|
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +69,12 @@ namespace MobileGL {
|
|||||||
return m_textureStorage.GetDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
return m_textureStorage.GetDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SizeT TextureObject2DCube::GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||||
|
MipmapDirtyRegion* outRects, SizeT maxRects) const {
|
||||||
|
return m_textureStorage.GetDirtyRects(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
||||||
|
outRects, maxRects);
|
||||||
|
}
|
||||||
|
|
||||||
void TextureObject2DCube::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
void TextureObject2DCube::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||||
GLenum internalFormat, const void* data, SizeT size) {
|
GLenum internalFormat, const void* data, SizeT size) {
|
||||||
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ namespace MobileGL {
|
|||||||
IntVec3 size) override;
|
IntVec3 size) override;
|
||||||
MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
|
MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
|
||||||
Uint mipmapLevel) const override;
|
Uint mipmapLevel) const override;
|
||||||
|
SizeT GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||||
|
MipmapDirtyRegion* outRects, SizeT maxRects) const override;
|
||||||
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||||
GLenum internalFormat, const void* data, SizeT size) override;
|
GLenum internalFormat, const void* data, SizeT size) override;
|
||||||
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#include "TextureState.h"
|
#include "TextureState.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include "Defines.h"
|
#include "Defines.h"
|
||||||
#include "TextureEnum.h"
|
#include "TextureEnum.h"
|
||||||
#include "TextureObject.h"
|
#include "TextureObject.h"
|
||||||
@@ -18,6 +20,12 @@
|
|||||||
#include "TextureObjectStubs.h"
|
#include "TextureObjectStubs.h"
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
static std::atomic<Uint64> s_nextTextureStateContextId = 1;
|
||||||
|
|
||||||
|
Uint64 TextureState::AllocateContextId() {
|
||||||
|
return s_nextTextureStateContextId.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
static SharedPtr<ITextureObject> MakeTextureObjectForTarget(Uint index, TextureTarget target) {
|
static SharedPtr<ITextureObject> MakeTextureObjectForTarget(Uint index, TextureTarget target) {
|
||||||
switch (target) {
|
switch (target) {
|
||||||
case TextureTarget::Texture1D:
|
case TextureTarget::Texture1D:
|
||||||
@@ -50,7 +58,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextureState::TextureState() : m_indexGenerator(1024, 1) {
|
TextureState::TextureState() : m_contextId(AllocateContextId()), m_indexGenerator(1024, 1) {
|
||||||
// GL 3.3 core 3.8: each target owns one default texture object (name 0) per context,
|
// GL 3.3 core 3.8: each target owns one default texture object (name 0) per context,
|
||||||
// shared across all texture units, and it is the initial binding of every unit/target
|
// shared across all texture units, and it is the initial binding of every unit/target
|
||||||
// slot. It is created outside m_textureObjects so name-based paths (glIsTexture,
|
// slot. It is created outside m_textureObjects so name-based paths (glIsTexture,
|
||||||
|
|||||||
@@ -84,8 +84,33 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
Uint64 GetTextureBindGeneration() const { return m_textureBindGeneration; }
|
Uint64 GetTextureBindGeneration() const { return m_textureBindGeneration; }
|
||||||
void BumpTextureBindGeneration() { ++m_textureBindGeneration; }
|
void BumpTextureBindGeneration() { ++m_textureBindGeneration; }
|
||||||
|
|
||||||
|
// Sibling of the bind generation for everything that changes WHICH native texture a
|
||||||
|
// backend ends up putting on a unit WITHOUT any binding moving. Two families feed it:
|
||||||
|
// a texture's SHAPE (internal format, stored level set, level range - all that
|
||||||
|
// mipmap-completeness is computed from) and any sampler object's parameters (MIN_FILTER
|
||||||
|
// decides whether the mip chain is read at all, and an incomplete-for-the-filter texture
|
||||||
|
// is deliberately left unbound so it samples as (0,0,0,1)). Deliberately coarse - ANY
|
||||||
|
// texture, ANY sampler - so that no mutation can slip past a per-unit binding memo; the
|
||||||
|
// setters that feed it all early-out when the value is unchanged, so the redundant
|
||||||
|
// glTexParameteri calls applications issue every frame do not churn it. Kept separate
|
||||||
|
// from the bind generation because the sampled texture SET is unaffected by these, and
|
||||||
|
// the Vulkan backend's set memo keys on that one.
|
||||||
|
Uint64 GetSamplingResolutionGeneration() const { return m_samplingResolutionGeneration; }
|
||||||
|
void BumpSamplingResolutionGeneration() { ++m_samplingResolutionGeneration; }
|
||||||
|
|
||||||
|
// Globally-unique, never-reused id of THIS texture state, i.e. of the context that owns
|
||||||
|
// it. Both generations above restart at 0 with a new context, so a backend memo keyed on
|
||||||
|
// them alone would accept a destroyed-and-recreated context whose counters happen to line
|
||||||
|
// up - and the heap address is no help, since a context freed and remade lands on it
|
||||||
|
// again (the unit tests do exactly that between cases).
|
||||||
|
Uint64 GetContextId() const { return m_contextId; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
static Uint64 AllocateContextId();
|
||||||
|
|
||||||
|
const Uint64 m_contextId;
|
||||||
Uint64 m_textureBindGeneration = 0;
|
Uint64 m_textureBindGeneration = 0;
|
||||||
|
Uint64 m_samplingResolutionGeneration = 0;
|
||||||
Int m_maxTouchedUnit = -1;
|
Int m_maxTouchedUnit = -1;
|
||||||
Int m_activeTextureUnit = 0;
|
Int m_activeTextureUnit = 0;
|
||||||
Array<TextureUnit, MAX_TEXTURE_IMAGE_UNITS> m_textureUnits;
|
Array<TextureUnit, MAX_TEXTURE_IMAGE_UNITS> m_textureUnits;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
#include "TextureUnit.h"
|
#include "TextureUnit.h"
|
||||||
|
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
TextureUnit::TextureUnit() : m_sampler(nullptr) {
|
TextureUnit::TextureUnit() : m_sampler(nullptr) {
|
||||||
for (int i = 0; i < (int)TextureTarget::TextureTargetCount; ++i) {
|
for (int i = 0; i < (int)TextureTarget::TextureTargetCount; ++i) {
|
||||||
@@ -24,7 +26,17 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TextureUnit::SetSamplerObject(const SharedPtr<SamplerObject>& sampler) {
|
void TextureUnit::SetSamplerObject(const SharedPtr<SamplerObject>& sampler) {
|
||||||
|
if (m_sampler == sampler) return;
|
||||||
|
|
||||||
m_sampler = sampler;
|
m_sampler = sampler;
|
||||||
|
// Which sampler object a unit carries is part of "what is bound at this unit": it
|
||||||
|
// overrides the texture's own sampler state, so it selects the filter that decides a
|
||||||
|
// bound texture's mipmap-completeness. glBindSampler already bumps the generation
|
||||||
|
// through NoteUnitTouched, but glDeleteSamplers unbinds the deleted object from every
|
||||||
|
// unit straight through here (GLContext::MarkSamplerObjectForDeletion) and would
|
||||||
|
// otherwise leave a backend memo of the resolved per-unit bindings replaying the
|
||||||
|
// deleted sampler.
|
||||||
|
if (pGLContext) pGLContext->BumpTextureBindGeneration();
|
||||||
}
|
}
|
||||||
|
|
||||||
const SharedPtr<SamplerObject>& TextureUnit::GetSamplerObject() const {
|
const SharedPtr<SamplerObject>& TextureUnit::GetSamplerObject() const {
|
||||||
|
|||||||
@@ -8,7 +8,18 @@
|
|||||||
|
|
||||||
#include "VertexArrayObject.h"
|
#include "VertexArrayObject.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
// Starts at 1 so a zero-initialized memo slot can never carry a live object's id.
|
||||||
|
// Atomic because VAOs are GL-thread-only today but the counter costs nothing to
|
||||||
|
// make safe, and a duplicate id would resurrect exactly the bug it exists to kill.
|
||||||
|
static std::atomic<Uint64> s_nextVertexArrayLifetimeId{1};
|
||||||
|
|
||||||
|
Uint64 VertexArrayObject::AllocateLifetimeId() {
|
||||||
|
return s_nextVertexArrayLifetimeId.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
VertexArrayObject::VertexArrayObject(Uint externIndex) : m_externalIndex(externIndex) {
|
VertexArrayObject::VertexArrayObject(Uint externIndex) : m_externalIndex(externIndex) {
|
||||||
for (int index = 0; index < MAX_VERTEX_ATTRIBS; ++index) {
|
for (int index = 0; index < MAX_VERTEX_ATTRIBS; ++index) {
|
||||||
auto& attr = m_attributes[index];
|
auto& attr = m_attributes[index];
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user