mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-13 14:48:32 +09:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3025284a6e |
+2
-116
@@ -1,4 +1,4 @@
|
|||||||
name: Test
|
name: Test
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -83,8 +83,6 @@ jobs:
|
|||||||
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
|
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
|
||||||
-DMOBILEGL_BUILD_TEST=ON \
|
-DMOBILEGL_BUILD_TEST=ON \
|
||||||
-DMOBILEGL_BUILD_BENCHMARK=ON \
|
-DMOBILEGL_BUILD_BENCHMARK=ON \
|
||||||
-DMOBILEGL_BUILD_INTEGRATION_TEST=ON \
|
|
||||||
-DMOBILEGL_ITEST_VK_ICD=/usr/share/vulkan/icd.d/lvp_icd.json \
|
|
||||||
-DMOBILEGL_BUILD_TRACE_REPLAY=OFF \
|
-DMOBILEGL_BUILD_TRACE_REPLAY=OFF \
|
||||||
-DBENCHMARK_DOWNLOAD_DEPENDENCIES=ON \
|
-DBENCHMARK_DOWNLOAD_DEPENDENCIES=ON \
|
||||||
-DBENCHMARK_ENABLE_TESTING=OFF \
|
-DBENCHMARK_ENABLE_TESTING=OFF \
|
||||||
@@ -112,7 +110,6 @@ jobs:
|
|||||||
"${BUILD_DIR}/CTestTestfile.cmake" \
|
"${BUILD_DIR}/CTestTestfile.cmake" \
|
||||||
"${BUILD_DIR}/MobileGL/MG_Test" \
|
"${BUILD_DIR}/MobileGL/MG_Test" \
|
||||||
"${BUILD_DIR}/MobileGL/MG_Benchmark" \
|
"${BUILD_DIR}/MobileGL/MG_Benchmark" \
|
||||||
"${BUILD_DIR}/MobileGL/MG_IntegrationTest" \
|
|
||||||
"${SHARED_LIBS[@]}"
|
"${SHARED_LIBS[@]}"
|
||||||
|
|
||||||
- name: Upload Linux runtime
|
- name: Upload Linux runtime
|
||||||
@@ -162,102 +159,12 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
working-directory: build-linux
|
working-directory: build-linux
|
||||||
run: |
|
run: |
|
||||||
ulimit -c unlimited
|
|
||||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
|
||||||
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
|
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
|
||||||
ctest -V -L unit --no-tests=error
|
ctest -V -L unit --no-tests=error
|
||||||
else
|
else
|
||||||
ctest --output-on-failure -L unit --no-tests=error
|
ctest --output-on-failure -L unit --no-tests=error
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload core dumps
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: unit-core-dumps
|
|
||||||
path: /tmp/core.*
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
integration:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build-linux
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Get CMake
|
|
||||||
uses: lukka/get-cmake@v4.3.3
|
|
||||||
|
|
||||||
- name: Install runtime dependencies
|
|
||||||
# Same set as the benchmark job, for the same reason: the scenarios bring
|
|
||||||
# up real headless EGL (llvmpipe) and Vulkan (lavapipe) contexts, and
|
|
||||||
# libegl-mesa0 - the EGL vendor library behind glvnd's libegl1 dispatch -
|
|
||||||
# only arrives as a Recommends.
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libvulkan1 libegl1 libegl-mesa0 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
|
|
||||||
|
|
||||||
- name: Download Linux runtime
|
|
||||||
uses: actions/download-artifact@v8
|
|
||||||
with:
|
|
||||||
name: mobilegl-linux-runtime
|
|
||||||
path: .
|
|
||||||
|
|
||||||
- name: Unpack Linux runtime
|
|
||||||
run: tar -xzf mobilegl-linux-runtime.tgz
|
|
||||||
|
|
||||||
- name: Normalize CTest command paths
|
|
||||||
run: |
|
|
||||||
python - <<'PY'
|
|
||||||
from pathlib import Path
|
|
||||||
import re
|
|
||||||
|
|
||||||
for path in Path('build-linux').rglob('CTestTestfile.cmake'):
|
|
||||||
text = path.read_text()
|
|
||||||
text = re.sub(r'"[^"]*/cmake-[^"]*/bin/cmake"', '"cmake"', text)
|
|
||||||
path.write_text(text)
|
|
||||||
PY
|
|
||||||
|
|
||||||
- name: Integration scenarios
|
|
||||||
working-directory: build-linux
|
|
||||||
# REQUIRE_GPU makes a driverless runner FAIL instead of skipping every
|
|
||||||
# scenario - an all-skip run is otherwise indistinguishable from a pass,
|
|
||||||
# which is how a five-month-old draw-dropping bug survived unseen until
|
|
||||||
# this lane existed.
|
|
||||||
#
|
|
||||||
# The lavapipe ICD pin lives in the build-linux configure
|
|
||||||
# (-DMOBILEGL_ITEST_VK_ICD), NOT here: the configure bakes it into each
|
|
||||||
# test's ctest ENVIRONMENT property, and a property entry OVERRIDES the
|
|
||||||
# job environment - a VK_ICD_FILENAMES exported here would be silently
|
|
||||||
# ignored while looking like it works. This lane runs on lavapipe
|
|
||||||
# deterministically, not on whichever of the eight Mesa ICDs a GPU-less
|
|
||||||
# runner enumerates first.
|
|
||||||
#
|
|
||||||
# Cores are armed so that any crash - the harness pre-flight child's
|
|
||||||
# included - leaves /tmp/core.*, which the failure-only step below ships
|
|
||||||
# as an artifact. Analyzing a downloaded core against the runtime
|
|
||||||
# artifact's binary in an ubuntu-24.04 userspace reproduces the exact
|
|
||||||
# crash stack without burning a CI round on an in-workflow debugger.
|
|
||||||
env:
|
|
||||||
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
|
||||||
run: |
|
|
||||||
ulimit -c unlimited
|
|
||||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
|
||||||
if [ "${{ secrets.ACTIONS_STEP_DEBUG }}" = "true" ]; then
|
|
||||||
ctest -V -L integration-gpu --no-tests=error
|
|
||||||
else
|
|
||||||
ctest --output-on-failure -L integration-gpu --no-tests=error
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload core dumps
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: integration-core-dumps
|
|
||||||
path: /tmp/core.*
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
benchmark:
|
benchmark:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build-linux
|
needs: build-linux
|
||||||
@@ -301,18 +208,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Benchmark
|
- name: Benchmark
|
||||||
working-directory: build-linux
|
working-directory: build-linux
|
||||||
run: |
|
run: ctest -V -C Release -L benchmark --no-tests=error
|
||||||
ulimit -c unlimited
|
|
||||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
|
||||||
ctest -V -C Release -L benchmark --no-tests=error
|
|
||||||
|
|
||||||
- name: Upload core dumps
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: benchmark-core-dumps
|
|
||||||
path: /tmp/core.*
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
build-retrace:
|
build-retrace:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -560,8 +456,6 @@ jobs:
|
|||||||
- name: Retrace and validate
|
- name: Retrace and validate
|
||||||
working-directory: build-retrace/tools/trace_replay
|
working-directory: build-retrace/tools/trace_replay
|
||||||
run: |
|
run: |
|
||||||
ulimit -c unlimited
|
|
||||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
|
||||||
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
|
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
|
||||||
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
|
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
|
||||||
fi
|
fi
|
||||||
@@ -576,14 +470,6 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
|
ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
|
||||||
|
|
||||||
- name: Upload core dumps
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: retrace-core-dumps-${{ matrix.backend }}-${{ matrix.case }}
|
|
||||||
path: /tmp/core.*
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
- name: Upload actual image
|
- name: Upload actual image
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
|
|||||||
@@ -34,3 +34,6 @@
|
|||||||
[submodule "3rdparty/asio"]
|
[submodule "3rdparty/asio"]
|
||||||
path = 3rdparty/asio
|
path = 3rdparty/asio
|
||||||
url = https://github.com/chriskohlhoff/asio.git
|
url = https://github.com/chriskohlhoff/asio.git
|
||||||
|
[submodule "3rdparty/libfork"]
|
||||||
|
path = 3rdparty/libfork
|
||||||
|
url = https://github.com/ConorWilliams/libfork.git
|
||||||
|
|||||||
+1
Submodule 3rdparty/libfork added at 9b2b844a5f
+7
-3
@@ -205,8 +205,6 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
|
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
|
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
||||||
@@ -298,7 +296,6 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
|
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
|
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp
|
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp
|
|
||||||
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
|
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
|
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp
|
MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp
|
||||||
@@ -376,6 +373,13 @@ set(MOBILEGL_INCLUDE_DIR
|
|||||||
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
|
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
|
||||||
# pimpl so no consumer target needs this path.
|
# pimpl so no consumer target needs this path.
|
||||||
${CMAKE_SOURCE_DIR}/3rdparty/asio/asio/include
|
${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
|
||||||
|
|||||||
+6
-18
@@ -66,11 +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_VALIDATE_SPIRV: test suites like SpirvPassTest exercise
|
// - MOBILEGL_ASYNC_POOL: a ShaderCompilePool is constructed by binaries that never call
|
||||||
// ShaderCompiler without ever running MobileGL::Initialize(), and every
|
// MobileGL::Initialize() and so never run MG_ConfigLoader::Init - MG_Test's
|
||||||
// Initialize() re-runs MG_ConfigLoader::Init, which would clobber a
|
// JobNodeTest builds pools directly, and it is the suite that runs the whole async
|
||||||
// programmatic override stored here (see ShaderCompiler.cpp,
|
// matrix against both execution engines. Mirroring it here would resolve to the
|
||||||
// SpirvValidationEnabled).
|
// 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;
|
||||||
@@ -141,19 +142,6 @@ namespace MobileGL::MG_Config {
|
|||||||
// MOBILEGL_ASYNC_SHADER_COMPILE_THREADS: shader-compile worker count. 0 (unset) means
|
// 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.
|
// auto, which is min(4, big cores); an explicit value is honoured as given.
|
||||||
Uint32 AsyncShaderCompileThreads = 0;
|
Uint32 AsyncShaderCompileThreads = 0;
|
||||||
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS: while a compile job is still in flight,
|
|
||||||
// glGetShaderiv(GL_COMPILE_STATUS) answers GL_TRUE and the shader info log reads
|
|
||||||
// empty, WITHOUT joining the job (latched per compile - see
|
|
||||||
// ShaderObject::TakeOptimisticCompileAnswer). A deliberate, bounded spec violation:
|
|
||||||
// a real failure still fails the program link with the compile log quoted. It
|
|
||||||
// exists for applications that compile hundreds of shaders serially and read the
|
|
||||||
// status right after each glCompileShader - Iris's shader-pack load - where those
|
|
||||||
// per-shader joins are what serializes the batch on its main path (Iris's gbuffer
|
|
||||||
// phase issues no program-level query between programs; program-level LINK_STATUS
|
|
||||||
// and the program info log still join truthfully, so paths that check each link
|
|
||||||
// immediately stay serial by their own construction). Off by default; never
|
|
||||||
// advertise it.
|
|
||||||
QuirkOverride AsyncOptimisticShaderStatus = QuirkOverride::Auto;
|
|
||||||
};
|
};
|
||||||
extern FeaturesTable Features;
|
extern FeaturesTable Features;
|
||||||
} // namespace MobileGL::MG_Config
|
} // namespace MobileGL::MG_Config
|
||||||
|
|||||||
@@ -183,8 +183,6 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
||||||
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
|
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
|
||||||
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
|
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
|
||||||
features.AsyncOptimisticShaderStatus =
|
|
||||||
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void InitBackendType() {
|
inline void InitBackendType() {
|
||||||
|
|||||||
@@ -37,19 +37,6 @@
|
|||||||
#define MOBILEGL_WGL_API MOBILEGL_API
|
#define MOBILEGL_WGL_API MOBILEGL_API
|
||||||
|
|
||||||
// ====================== MobileGL configurations ======================= //
|
// ====================== MobileGL configurations ======================= //
|
||||||
// The numeric log levels live here, not only in Log.h: MOBILEGL_ASSERT below compares
|
|
||||||
// MOBILEGL_LOG_ACTIVE_LEVEL against MOBILEGL_LOG_LEVEL_DEBUG, and in a translation unit
|
|
||||||
// that includes Defines.h without Log.h both tokens would silently evaluate to 0 in the
|
|
||||||
// preprocessor conditional - enabling the assert in exactly the INFO-level builds it is
|
|
||||||
// documented to be compiled out of. Log.h redefines them identically, which is legal.
|
|
||||||
#ifndef MOBILEGL_LOG_LEVEL_DEBUG
|
|
||||||
#define MOBILEGL_LOG_LEVEL_DEBUG 0
|
|
||||||
#define MOBILEGL_LOG_LEVEL_WARN 1
|
|
||||||
#define MOBILEGL_LOG_LEVEL_ERROR 2
|
|
||||||
#define MOBILEGL_LOG_LEVEL_INFO 3
|
|
||||||
#define MOBILEGL_LOG_LEVEL_FATAL 4
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL
|
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL
|
||||||
#define MOBILEGL_LOG_ACTIVE_LEVEL MOBILEGL_LOG_LEVEL_INFO
|
#define MOBILEGL_LOG_ACTIVE_LEVEL MOBILEGL_LOG_LEVEL_INFO
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1214,7 +1214,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (g_unitTextureSyncListValid &&
|
if (g_unitTextureSyncListValid &&
|
||||||
g_unitTextureSyncListContextId == keys.contextId &&
|
g_unitTextureSyncListContextId == keys.contextId &&
|
||||||
g_unitTextureSyncListMaxUnit == maxTouchedUnit &&
|
g_unitTextureSyncListMaxUnit == maxTouchedUnit &&
|
||||||
g_unitTextureSyncListContextGeneration == g_backendContextGeneration &&
|
g_unitTextureSyncListContextGeneration == g_textureContextGeneration &&
|
||||||
g_unitTextureSyncListEpoch == unitBindingsEpoch &&
|
g_unitTextureSyncListEpoch == unitBindingsEpoch &&
|
||||||
g_unitTextureSyncListSamplingGeneration == samplingGeneration &&
|
g_unitTextureSyncListSamplingGeneration == samplingGeneration &&
|
||||||
PairingsIntact(g_unitTextureSyncList)) {
|
PairingsIntact(g_unitTextureSyncList)) {
|
||||||
@@ -1246,7 +1246,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
g_unitTextureSyncListContextId = keys.contextId;
|
g_unitTextureSyncListContextId = keys.contextId;
|
||||||
g_unitTextureSyncListMaxUnit = maxTouchedUnit;
|
g_unitTextureSyncListMaxUnit = maxTouchedUnit;
|
||||||
g_unitTextureSyncListContextGeneration = g_backendContextGeneration;
|
g_unitTextureSyncListContextGeneration = g_textureContextGeneration;
|
||||||
g_unitTextureSyncListEpoch = unitBindingsEpoch;
|
g_unitTextureSyncListEpoch = unitBindingsEpoch;
|
||||||
g_unitTextureSyncListSamplingGeneration = samplingGeneration;
|
g_unitTextureSyncListSamplingGeneration = samplingGeneration;
|
||||||
g_unitTextureSyncListValid = true;
|
g_unitTextureSyncListValid = true;
|
||||||
@@ -1274,7 +1274,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_fboTextureSyncListSlotVersion == fboSlotVersion &&
|
g_fboTextureSyncListSlotVersion == fboSlotVersion &&
|
||||||
g_fboTextureSyncListObjectVersion == fboObjectVersion &&
|
g_fboTextureSyncListObjectVersion == fboObjectVersion &&
|
||||||
g_fboTextureSyncListContextId == keys.contextId &&
|
g_fboTextureSyncListContextId == keys.contextId &&
|
||||||
g_fboTextureSyncListContextGeneration == g_backendContextGeneration &&
|
g_fboTextureSyncListContextGeneration == g_textureContextGeneration &&
|
||||||
PairingsIntact(g_fboTextureSyncList);
|
PairingsIntact(g_fboTextureSyncList);
|
||||||
if (fboListValid) {
|
if (fboListValid) {
|
||||||
for (const auto& entry : g_fboTextureSyncList) {
|
for (const auto& entry : g_fboTextureSyncList) {
|
||||||
@@ -1302,7 +1302,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_fboTextureSyncListSlotVersion = fboSlotVersion;
|
g_fboTextureSyncListSlotVersion = fboSlotVersion;
|
||||||
g_fboTextureSyncListObjectVersion = fboObjectVersion;
|
g_fboTextureSyncListObjectVersion = fboObjectVersion;
|
||||||
g_fboTextureSyncListContextId = keys.contextId;
|
g_fboTextureSyncListContextId = keys.contextId;
|
||||||
g_fboTextureSyncListContextGeneration = g_backendContextGeneration;
|
g_fboTextureSyncListContextGeneration = g_textureContextGeneration;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
g_fboTextureSyncListFbo = nullptr;
|
g_fboTextureSyncListFbo = nullptr;
|
||||||
@@ -2028,9 +2028,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_currentDrawFrontendProgram = nullptr;
|
g_currentDrawFrontendProgram = nullptr;
|
||||||
g_currentDrawBackendProgram = nullptr;
|
g_currentDrawBackendProgram = nullptr;
|
||||||
|
|
||||||
// ... || !GetSpirvStatus(): see BackendProgramObjectImpl::SyncToBackend - a
|
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
||||||
// program whose SPIR-V never arrived is linked but not drawable.
|
|
||||||
if (!currentProgram || !currentProgram->GetLinkStatus() || !currentProgram->GetSpirvStatus()) {
|
|
||||||
g_GLESFuncs.glUseProgram(0);
|
g_GLESFuncs.glUseProgram(0);
|
||||||
g_lastUsedBackendProgramId = 0;
|
g_lastUsedBackendProgramId = 0;
|
||||||
return;
|
return;
|
||||||
@@ -2423,7 +2421,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
static_cast<SizeT>(maxTouchedUnit + 1) * sizeof(SamplerImpl::g_boundSamplersCache[0]);
|
static_cast<SizeT>(maxTouchedUnit + 1) * sizeof(SamplerImpl::g_boundSamplersCache[0]);
|
||||||
if (g_unitSamplerWalkValid && g_unitSamplerWalkContextId == keys.contextId &&
|
if (g_unitSamplerWalkValid && g_unitSamplerWalkContextId == keys.contextId &&
|
||||||
g_unitSamplerWalkEpoch == keys.unitBindingsEpoch && g_unitSamplerWalkMaxUnit == maxTouchedUnit &&
|
g_unitSamplerWalkEpoch == keys.unitBindingsEpoch && g_unitSamplerWalkMaxUnit == maxTouchedUnit &&
|
||||||
g_unitSamplerWalkContextGeneration == g_backendContextGeneration &&
|
g_unitSamplerWalkContextGeneration == TextureImpl::g_textureContextGeneration &&
|
||||||
std::memcmp(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes) == 0) {
|
std::memcmp(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes) == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2444,7 +2442,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_unitSamplerWalkContextId = keys.contextId;
|
g_unitSamplerWalkContextId = keys.contextId;
|
||||||
g_unitSamplerWalkEpoch = keys.unitBindingsEpoch;
|
g_unitSamplerWalkEpoch = keys.unitBindingsEpoch;
|
||||||
g_unitSamplerWalkMaxUnit = maxTouchedUnit;
|
g_unitSamplerWalkMaxUnit = maxTouchedUnit;
|
||||||
g_unitSamplerWalkContextGeneration = g_backendContextGeneration;
|
g_unitSamplerWalkContextGeneration = TextureImpl::g_textureContextGeneration;
|
||||||
std::memcpy(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes);
|
std::memcpy(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes);
|
||||||
g_unitSamplerWalkValid = true;
|
g_unitSamplerWalkValid = true;
|
||||||
}
|
}
|
||||||
@@ -2554,7 +2552,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
memo.programBackendStateVersion ==
|
memo.programBackendStateVersion ==
|
||||||
(currentProgram ? currentProgram->GetBackendStateVersion() : 0) &&
|
(currentProgram ? currentProgram->GetBackendStateVersion() : 0) &&
|
||||||
memo.programLinked == (currentProgram && currentProgram->GetLinkStatus()) &&
|
memo.programLinked == (currentProgram && currentProgram->GetLinkStatus()) &&
|
||||||
memo.contextGeneration == g_backendContextGeneration;
|
memo.contextGeneration == TextureImpl::g_textureContextGeneration;
|
||||||
// Short-circuited: the shadow compare is only meaningful once the key (and with it the
|
// Short-circuited: the shadow compare is only meaningful once the key (and with it the
|
||||||
// snapshotted row count) matches.
|
// snapshotted row count) matches.
|
||||||
if (!keysMatch || std::memcmp(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(),
|
if (!keysMatch || std::memcmp(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(),
|
||||||
@@ -2569,7 +2567,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
memo.programLifetimeId = currentProgram ? currentProgram->GetLifetimeId() : 0;
|
memo.programLifetimeId = currentProgram ? currentProgram->GetLifetimeId() : 0;
|
||||||
memo.programBackendStateVersion = currentProgram ? currentProgram->GetBackendStateVersion() : 0;
|
memo.programBackendStateVersion = currentProgram ? currentProgram->GetBackendStateVersion() : 0;
|
||||||
memo.programLinked = currentProgram && currentProgram->GetLinkStatus();
|
memo.programLinked = currentProgram && currentProgram->GetLinkStatus();
|
||||||
memo.contextGeneration = g_backendContextGeneration;
|
memo.contextGeneration = TextureImpl::g_textureContextGeneration;
|
||||||
std::memcpy(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), shadowBytes);
|
std::memcpy(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), shadowBytes);
|
||||||
memo.valid = true;
|
memo.valid = true;
|
||||||
}
|
}
|
||||||
@@ -2591,7 +2589,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
static void BindCurrentProgramWithResources(
|
static void BindCurrentProgramWithResources(
|
||||||
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
|
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
|
||||||
const TextureImpl::DrawTextureSyncKeys& keys) {
|
const TextureImpl::DrawTextureSyncKeys& keys) {
|
||||||
if (currentProgram && currentProgram->GetLinkStatus() && currentProgram->GetSpirvStatus()) {
|
if (currentProgram && currentProgram->GetLinkStatus()) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
@@ -2744,7 +2742,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
samplerPassMemo.unitBindingsEpoch == keys.unitBindingsEpoch &&
|
samplerPassMemo.unitBindingsEpoch == keys.unitBindingsEpoch &&
|
||||||
samplerPassMemo.samplingGeneration == keys.samplingGeneration &&
|
samplerPassMemo.samplingGeneration == keys.samplingGeneration &&
|
||||||
samplerPassMemo.backendStateVersion == programBackendStateVersion &&
|
samplerPassMemo.backendStateVersion == programBackendStateVersion &&
|
||||||
samplerPassMemo.textureContextGeneration == g_backendContextGeneration;
|
samplerPassMemo.textureContextGeneration == TextureImpl::g_textureContextGeneration;
|
||||||
if (samplerPassClean) {
|
if (samplerPassClean) {
|
||||||
for (Uint i = 0; i < samplerPassMemo.count; ++i) {
|
for (Uint i = 0; i < samplerPassMemo.count; ++i) {
|
||||||
if (SamplerImpl::g_boundSamplersCache[samplerPassMemo.units[i]] !=
|
if (SamplerImpl::g_boundSamplersCache[samplerPassMemo.units[i]] !=
|
||||||
@@ -2843,7 +2841,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
samplerPassMemo.unitBindingsEpoch = keys.unitBindingsEpoch;
|
samplerPassMemo.unitBindingsEpoch = keys.unitBindingsEpoch;
|
||||||
samplerPassMemo.samplingGeneration = keys.samplingGeneration;
|
samplerPassMemo.samplingGeneration = keys.samplingGeneration;
|
||||||
samplerPassMemo.backendStateVersion = programBackendStateVersion;
|
samplerPassMemo.backendStateVersion = programBackendStateVersion;
|
||||||
samplerPassMemo.textureContextGeneration = g_backendContextGeneration;
|
samplerPassMemo.textureContextGeneration = TextureImpl::g_textureContextGeneration;
|
||||||
samplerPassMemo.valid = true;
|
samplerPassMemo.valid = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2861,7 +2859,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// is pinned for the duration. Prefers the per-draw stash those preparations wrote.
|
// is pinned for the duration. Prefers the per-draw stash those preparations wrote.
|
||||||
static PrgramImpl::BackendProgramObjectImpl* GetCurrentBackendProgram() {
|
static PrgramImpl::BackendProgramObjectImpl* GetCurrentBackendProgram() {
|
||||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||||
if (!currentProgram || !currentProgram->GetLinkStatus() || !currentProgram->GetSpirvStatus()) {
|
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
if (PrgramImpl::g_currentDrawFrontendProgram == currentProgram.get()) {
|
if (PrgramImpl::g_currentDrawFrontendProgram == currentProgram.get()) {
|
||||||
@@ -3019,7 +3017,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
TextureImpl::SyncImageTextureBindings();
|
TextureImpl::SyncImageTextureBindings();
|
||||||
PrgramImpl::SyncCurrentProgram(currentProgram);
|
PrgramImpl::SyncCurrentProgram(currentProgram);
|
||||||
|
|
||||||
if (!currentProgram || !currentProgram->GetLinkStatus() || !currentProgram->GetSpirvStatus()) {
|
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
||||||
g_GLESFuncs.glUseProgram(0);
|
g_GLESFuncs.glUseProgram(0);
|
||||||
PrgramImpl::g_lastUsedBackendProgramId = 0;
|
PrgramImpl::g_lastUsedBackendProgramId = 0;
|
||||||
return;
|
return;
|
||||||
@@ -3605,12 +3603,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s_resolveContextGeneration != g_backendContextGeneration) {
|
if (s_resolveContextGeneration != TextureImpl::g_textureContextGeneration) {
|
||||||
// The ids belonged to a dead context; the context reclaimed them with it.
|
// The ids belonged to a dead context; the context reclaimed them with it.
|
||||||
s_resolveFramebuffer = 0;
|
s_resolveFramebuffer = 0;
|
||||||
s_resolveRenderbuffer = 0;
|
s_resolveRenderbuffer = 0;
|
||||||
s_resolveFormat = 0;
|
s_resolveFormat = 0;
|
||||||
s_resolveContextGeneration = g_backendContextGeneration;
|
s_resolveContextGeneration = TextureImpl::g_textureContextGeneration;
|
||||||
}
|
}
|
||||||
if (s_resolveFramebuffer == 0) {
|
if (s_resolveFramebuffer == 0) {
|
||||||
g_GLESFuncs.glGenFramebuffers(1, &s_resolveFramebuffer);
|
g_GLESFuncs.glGenFramebuffers(1, &s_resolveFramebuffer);
|
||||||
@@ -3758,7 +3756,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Bool EnsureResources() {
|
static Bool EnsureResources() {
|
||||||
if (s_contextGeneration != g_backendContextGeneration) {
|
if (s_contextGeneration != TextureImpl::g_textureContextGeneration) {
|
||||||
// The ids belonged to a dead context; the context reclaimed them with it.
|
// The ids belonged to a dead context; the context reclaimed them with it.
|
||||||
s_framebuffer = 0;
|
s_framebuffer = 0;
|
||||||
s_texture = 0;
|
s_texture = 0;
|
||||||
@@ -3769,7 +3767,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
s_depthProgram = 0;
|
s_depthProgram = 0;
|
||||||
s_stencilProgram = 0;
|
s_stencilProgram = 0;
|
||||||
s_programsFailed = false;
|
s_programsFailed = false;
|
||||||
s_contextGeneration = g_backendContextGeneration;
|
s_contextGeneration = TextureImpl::g_textureContextGeneration;
|
||||||
}
|
}
|
||||||
if (s_programsFailed) {
|
if (s_programsFailed) {
|
||||||
return false;
|
return false;
|
||||||
@@ -7482,7 +7480,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
PixelStoreImpl::InvalidatePackStateCache();
|
PixelStoreImpl::InvalidatePackStateCache();
|
||||||
// Texture ids belong to the dying context; wrappers destroyed later must
|
// Texture ids belong to the dying context; wrappers destroyed later must
|
||||||
// not glDeleteTextures a recycled name in a successor context.
|
// not glDeleteTextures a recycled name in a successor context.
|
||||||
++g_backendContextGeneration;
|
++TextureImpl::g_textureContextGeneration;
|
||||||
g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release);
|
g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release);
|
||||||
// Outstanding fence handles now refer to a dead context; treat them as
|
// Outstanding fence handles now refer to a dead context; treat them as
|
||||||
// signaled from here on.
|
// signaled from here on.
|
||||||
|
|||||||
@@ -33,8 +33,6 @@
|
|||||||
#include <regex>
|
#include <regex>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectGLES {
|
namespace MobileGL::MG_Backend::DirectGLES {
|
||||||
Uint g_backendContextGeneration = 1;
|
|
||||||
|
|
||||||
constexpr Bool PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC = false;
|
constexpr Bool PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC = false;
|
||||||
constexpr const char* BASE_INSTANCE_UNIFORM_NAME = "mg_BaseInstance";
|
constexpr const char* BASE_INSTANCE_UNIFORM_NAME = "mg_BaseInstance";
|
||||||
constexpr const char* DRAW_ID_UNIFORM_NAME = "mg_DrawID";
|
constexpr const char* DRAW_ID_UNIFORM_NAME = "mg_DrawID";
|
||||||
@@ -1648,7 +1646,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
||||||
m_contextGeneration = g_backendContextGeneration;
|
m_contextGeneration = g_textureContextGeneration;
|
||||||
if (m_backendTextureId == 0) {
|
if (m_backendTextureId == 0) {
|
||||||
MGLOG_E("Failed to generate texture object.");
|
MGLOG_E("Failed to generate texture object.");
|
||||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||||
@@ -1675,7 +1673,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteTextures) {
|
if (m_contextGeneration == g_textureContextGeneration && g_GLESFuncs.glDeleteTextures) {
|
||||||
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
||||||
}
|
}
|
||||||
m_backendTextureId = 0;
|
m_backendTextureId = 0;
|
||||||
@@ -1714,7 +1712,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void BackendTextureObject::RecreateBackendTexture() {
|
void BackendTextureObject::RecreateBackendTexture() {
|
||||||
if (m_backendTextureId != 0) {
|
if (m_backendTextureId != 0) {
|
||||||
ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId);
|
ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId);
|
||||||
if (m_contextGeneration == g_backendContextGeneration) {
|
if (m_contextGeneration == g_textureContextGeneration) {
|
||||||
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
||||||
}
|
}
|
||||||
for (auto& unitCache : g_boundTexturesCache) {
|
for (auto& unitCache : g_boundTexturesCache) {
|
||||||
@@ -1727,7 +1725,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
||||||
m_contextGeneration = g_backendContextGeneration;
|
m_contextGeneration = g_textureContextGeneration;
|
||||||
if (m_backendTextureId == 0) {
|
if (m_backendTextureId == 0) {
|
||||||
MGLOG_E("Failed to regenerate texture object.");
|
MGLOG_E("Failed to regenerate texture object.");
|
||||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||||
@@ -2811,14 +2809,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// TextureStorageType is {Mipmap, Buffer}, both handled above, so this is a
|
THROW_UNIMPL_EXCEPTION;
|
||||||
// backstop for a state object that grew a new storage kind. Skipping the upload
|
|
||||||
// renders wrong; throwing unwinds through the C GL ABI and kills the process.
|
|
||||||
MGLOG_I("DirectGLES texture sync: no upload path for storage type %d on texture %u; "
|
|
||||||
"skipping this sync",
|
|
||||||
static_cast<int>(stateTextureObject->GetStorageType()),
|
|
||||||
stateTextureObject->GetExternalIndex());
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
||||||
@@ -3083,6 +3074,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Uint g_activeTextureUnit = 0;
|
Uint g_activeTextureUnit = 0;
|
||||||
|
Uint g_textureContextGeneration = 1;
|
||||||
Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
|
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;
|
||||||
@@ -3101,7 +3093,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
m_backendColorSlots[i] = GL_COLOR_ATTACHMENT0 + i;
|
m_backendColorSlots[i] = GL_COLOR_ATTACHMENT0 + i;
|
||||||
}
|
}
|
||||||
g_GLESFuncs.glGenFramebuffers(1, &m_backendFBOId);
|
g_GLESFuncs.glGenFramebuffers(1, &m_backendFBOId);
|
||||||
m_contextGeneration = g_backendContextGeneration;
|
|
||||||
if (m_backendFBOId == 0) {
|
if (m_backendFBOId == 0) {
|
||||||
MGLOG_E("Failed to generate framebuffer object.");
|
MGLOG_E("Failed to generate framebuffer object.");
|
||||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||||
@@ -3110,22 +3101,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BackendFramebufferObject::~BackendFramebufferObject() {
|
|
||||||
if (InProcessTeardown()) {
|
|
||||||
return; // see InProcessTeardown(): the driver may be unloaded already
|
|
||||||
}
|
|
||||||
if (m_backendFBOId == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Scrub the binding shadow whether or not the id can still be deleted: a
|
|
||||||
// recycled name must never satisfy the shadow's dedup.
|
|
||||||
NoteFramebufferIdDeleted(m_backendFBOId);
|
|
||||||
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteFramebuffers) {
|
|
||||||
g_GLESFuncs.glDeleteFramebuffers(1, &m_backendFBOId);
|
|
||||||
}
|
|
||||||
m_backendFBOId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BackendFramebufferObject::Bind(FramebufferTarget target) const {
|
void BackendFramebufferObject::Bind(FramebufferTarget target) const {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
@@ -3181,17 +3156,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return g_driverFBOBindings[idx];
|
return g_driverFBOBindings[idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
void NoteFramebufferIdDeleted(Uint id) {
|
|
||||||
if (id == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (SizeT idx = 0; idx < g_driverFBOBindings.size(); ++idx) {
|
|
||||||
if (g_driverFBOBindingKnown[idx] && g_driverFBOBindings[idx] == id) {
|
|
||||||
g_driverFBOBindings[idx] = 0; // glDeleteFramebuffers reverts a bound FBO to 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void InvalidateFramebufferBindingCache() {
|
void InvalidateFramebufferBindingCache() {
|
||||||
g_driverFBOBindings = {0, 0};
|
g_driverFBOBindings = {0, 0};
|
||||||
g_driverFBOBindingKnown = {false, false};
|
g_driverFBOBindingKnown = {false, false};
|
||||||
@@ -4192,14 +4156,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSpirvStatus() as well as GetLinkStatus(): a program whose phase-B job was
|
if (!stateProgramObject->GetLinkStatus()) {
|
||||||
// cancelled (teardown) or whose optimizer run failed is fully linked and fully
|
MGLOG_E("Program object is not linked, skipping backend sync. State program ID: %u",
|
||||||
// queryable, but has no SPIR-V to build a driver program out of. GL cannot retract
|
|
||||||
// a LINK_STATUS it already reported true, so "linked but not drawable" is the
|
|
||||||
// answer, and this is where the ES backend expresses it.
|
|
||||||
if (!stateProgramObject->GetLinkStatus() || !stateProgramObject->GetSpirvStatus()) {
|
|
||||||
MGLOG_E("Program object is not linked or has no generated SPIR-V, skipping backend sync. State "
|
|
||||||
"program ID: %u",
|
|
||||||
stateProgramObject->GetExternalIndex());
|
stateProgramObject->GetExternalIndex());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4386,28 +4344,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
log.back() = '\0';
|
log.back() = '\0';
|
||||||
MGLOG_E("Shader compilation failed for backend ID %u: %s", backendShaderId, log.data());
|
MGLOG_E("Shader compilation failed for backend ID %u: %s", backendShaderId, log.data());
|
||||||
m_backendProgramUsable = false;
|
m_backendProgramUsable = false;
|
||||||
// Nothing will ever attach this one, so nothing else can free it.
|
|
||||||
g_GLESFuncs.glDeleteShader(backendShaderId);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
MGLOG_D("Attaching shader ID: %u to program %u", backendShaderId, m_backendProgramId);
|
MGLOG_D("Attaching shader ID: %u to program %u", backendShaderId, m_backendProgramId);
|
||||||
g_GLESFuncs.glAttachShader(m_backendProgramId, backendShaderId);
|
g_GLESFuncs.glAttachShader(m_backendProgramId, backendShaderId);
|
||||||
// Hand the shader's lifetime to the program, immediately and unconditionally.
|
|
||||||
//
|
|
||||||
// glDeleteShader only FLAGS a shader; the driver frees it when it is attached to
|
|
||||||
// nothing. Flagging it here is what makes the program own it, so deleting the
|
|
||||||
// program (or the detach loop above, on a relink) is what actually frees it.
|
|
||||||
// Without this call every program build leaked its shader objects for the process
|
|
||||||
// lifetime, and a relink leaked them twice - the detach loop above dropped the
|
|
||||||
// program's reference to shaders nothing had flagged, so they became unreachable
|
|
||||||
// AND undeletable. The GL swizzle conformance test builds 1,296 programs per case,
|
|
||||||
// so a handful of cases left tens of thousands of live driver shaders behind and
|
|
||||||
// the driver started mis-serving them (KHR-GL33/GL40.texture_swizzle.smoke_*).
|
|
||||||
// Same class of defect as the missing framebuffer/renderbuffer/sampler destructors
|
|
||||||
// fixed in Wave 1, and the last of that family: this is the one backend GL object
|
|
||||||
// MobileGL creates without an owning wrapper to destroy it.
|
|
||||||
g_GLESFuncs.glDeleteShader(backendShaderId);
|
|
||||||
|
|
||||||
MGLOG_D("Processed shader source length: %zu", source.length());
|
MGLOG_D("Processed shader source length: %zu", source.length());
|
||||||
}
|
}
|
||||||
@@ -4616,7 +4557,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
g_GLESFuncs.glGenSamplers(1, &m_backendSamplerId);
|
g_GLESFuncs.glGenSamplers(1, &m_backendSamplerId);
|
||||||
m_contextGeneration = g_backendContextGeneration;
|
|
||||||
if (m_backendSamplerId == 0) {
|
if (m_backendSamplerId == 0) {
|
||||||
MGLOG_E("Failed to generate sampler object.");
|
MGLOG_E("Failed to generate sampler object.");
|
||||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||||
@@ -4625,26 +4565,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BackendSamplerObject::~BackendSamplerObject() {
|
|
||||||
if (InProcessTeardown()) {
|
|
||||||
return; // see InProcessTeardown(): the driver may be unloaded already
|
|
||||||
}
|
|
||||||
if (m_backendSamplerId == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Scrub the unit shadow whether or not the id can still be deleted - the next
|
|
||||||
// twin can land on this heap address and would otherwise false-skip its Bind.
|
|
||||||
for (auto& boundSampler : g_boundSamplersCache) {
|
|
||||||
if (boundSampler == this) {
|
|
||||||
boundSampler = nullptr; // glDeleteSamplers unbinds from every unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteSamplers) {
|
|
||||||
g_GLESFuncs.glDeleteSamplers(1, &m_backendSamplerId);
|
|
||||||
}
|
|
||||||
m_backendSamplerId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BackendSamplerObject::SyncToBackend(
|
void BackendSamplerObject::SyncToBackend(
|
||||||
const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject) {
|
const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
@@ -4760,28 +4680,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
g_GLESFuncs.glGenRenderbuffers(1, &m_backendRBOId);
|
g_GLESFuncs.glGenRenderbuffers(1, &m_backendRBOId);
|
||||||
m_contextGeneration = g_backendContextGeneration;
|
|
||||||
if (m_backendRBOId == 0) {
|
if (m_backendRBOId == 0) {
|
||||||
MGLOG_E("Failed to generate renderbuffer object.");
|
MGLOG_E("Failed to generate renderbuffer object.");
|
||||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BackendRenderbufferObject::~BackendRenderbufferObject() {
|
|
||||||
if (InProcessTeardown()) {
|
|
||||||
return; // see InProcessTeardown(): the driver may be unloaded already
|
|
||||||
}
|
|
||||||
if (m_backendRBOId == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// No driver-level renderbuffer-binding shadow exists (Bind() always issues the
|
|
||||||
// call), so there is nothing to scrub here - only the id to release.
|
|
||||||
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteRenderbuffers) {
|
|
||||||
g_GLESFuncs.glDeleteRenderbuffers(1, &m_backendRBOId);
|
|
||||||
}
|
|
||||||
m_backendRBOId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BackendRenderbufferObject::Bind() const {
|
void BackendRenderbufferObject::Bind() const {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
|
|||||||
@@ -36,14 +36,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Bool InProcessTeardown();
|
Bool InProcessTeardown();
|
||||||
void EnsureProcessTeardownSentinel();
|
void EnsureProcessTeardownSentinel();
|
||||||
|
|
||||||
// Generation of the backend ES context that owns the driver ids currently handed
|
|
||||||
// out. Bumped exactly once per DestroyEGLContext. Every backend twin that owns a
|
|
||||||
// driver name (texture, framebuffer, renderbuffer, sampler) stamps this at
|
|
||||||
// construction and compares it in its destructor: a twin outliving its context
|
|
||||||
// must NOT glDelete* its id, because a successor context may already have recycled
|
|
||||||
// that name and the delete would take out a live object of the new context.
|
|
||||||
extern Uint g_backendContextGeneration;
|
|
||||||
|
|
||||||
// Which optional pieces of state a draw needs synchronized before it is issued.
|
// 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
|
// Index/indirect buffer syncs and the instancing-related work are skipped for
|
||||||
// draws that provably cannot read them.
|
// draws that provably cannot read them.
|
||||||
@@ -665,20 +657,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
||||||
g_boundTexturesCache;
|
g_boundTexturesCache;
|
||||||
extern Uint g_activeTextureUnit;
|
extern Uint g_activeTextureUnit;
|
||||||
|
// Bumped when the backend ES context is destroyed; texture ids stamped with
|
||||||
|
// an older generation belong to a dead context and must not be deleted.
|
||||||
|
extern Uint g_textureContextGeneration;
|
||||||
} // namespace TextureImpl
|
} // namespace TextureImpl
|
||||||
|
|
||||||
namespace FramebufferImpl {
|
namespace FramebufferImpl {
|
||||||
class BackendFramebufferObject {
|
class BackendFramebufferObject {
|
||||||
public:
|
public:
|
||||||
BackendFramebufferObject();
|
BackendFramebufferObject();
|
||||||
// Deletes the driver framebuffer and scrubs the binding shadow. Without it every
|
|
||||||
// frontend glDeleteFramebuffers leaked one ES framebuffer for the process lifetime;
|
|
||||||
// an app that creates a framebuffer per readback (GL CTS packed_pixels does ~3300
|
|
||||||
// per case) walked the driver into hundreds of megabytes of dead framebuffers and
|
|
||||||
// out of the resources a later attachment needs.
|
|
||||||
~BackendFramebufferObject();
|
|
||||||
BackendFramebufferObject(const BackendFramebufferObject&) = delete;
|
|
||||||
BackendFramebufferObject& operator=(const BackendFramebufferObject&) = delete;
|
|
||||||
void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,
|
void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,
|
||||||
FramebufferTarget asTarget);
|
FramebufferTarget asTarget);
|
||||||
// Apply only this FBO's read buffer (glReadBuffer) to the backend. Split out so it can
|
// Apply only this FBO's read buffer (glReadBuffer) to the backend. Split out so it can
|
||||||
@@ -693,7 +680,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
Uint m_backendFBOId = 0;
|
Uint m_backendFBOId = 0;
|
||||||
Uint m_contextGeneration = 0;
|
|
||||||
|
|
||||||
/* this will save buffers in its original form,
|
/* this will save buffers in its original form,
|
||||||
reversion, absence or not consecutive are all allowed, as long as GL spec allows it
|
reversion, absence or not consecutive are all allowed, as long as GL spec allows it
|
||||||
@@ -835,10 +821,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void BindFramebufferId(GLenum fbTarget, Uint id);
|
void BindFramebufferId(GLenum fbTarget, Uint id);
|
||||||
Uint CurrentFramebufferBinding(FramebufferTarget target);
|
Uint CurrentFramebufferBinding(FramebufferTarget target);
|
||||||
void InvalidateFramebufferBindingCache();
|
void InvalidateFramebufferBindingCache();
|
||||||
// A driver framebuffer id is about to be deleted: ES reverts every target that
|
|
||||||
// currently binds it to 0, so the binding shadow has to follow or the next
|
|
||||||
// BindFramebufferId(0) would be deduped away and leave the deleted name bound.
|
|
||||||
void NoteFramebufferIdDeleted(Uint id);
|
|
||||||
} // namespace FramebufferImpl
|
} // namespace FramebufferImpl
|
||||||
|
|
||||||
// Shared scratch framebuffers for the readback/copy/blit emulation paths, with a
|
// Shared scratch framebuffers for the readback/copy/blit emulation paths, with a
|
||||||
@@ -1105,19 +1087,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
class BackendSamplerObject {
|
class BackendSamplerObject {
|
||||||
public:
|
public:
|
||||||
BackendSamplerObject();
|
BackendSamplerObject();
|
||||||
// Deletes the driver sampler and clears the units whose binding shadow still names
|
|
||||||
// this twin (a recycled heap address would otherwise false-skip a later Bind).
|
|
||||||
// Frontend glDeleteSamplers used to leak the backend id for the process lifetime.
|
|
||||||
~BackendSamplerObject();
|
|
||||||
BackendSamplerObject(const BackendSamplerObject&) = delete;
|
|
||||||
BackendSamplerObject& operator=(const BackendSamplerObject&) = delete;
|
|
||||||
void SyncToBackend(const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject);
|
void SyncToBackend(const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject);
|
||||||
void Bind(Uint unit);
|
void Bind(Uint unit);
|
||||||
Uint GetBackendSamplerId() const;
|
Uint GetBackendSamplerId() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Uint m_backendSamplerId = 0;
|
Uint m_backendSamplerId = 0;
|
||||||
Uint m_contextGeneration = 0;
|
|
||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
SamplerParameters m_cacheSamplerParameters;
|
SamplerParameters m_cacheSamplerParameters;
|
||||||
Uint16 m_syncedSamplerVersion = 0;
|
Uint16 m_syncedSamplerVersion = 0;
|
||||||
@@ -1135,18 +1110,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
class BackendRenderbufferObject {
|
class BackendRenderbufferObject {
|
||||||
public:
|
public:
|
||||||
BackendRenderbufferObject();
|
BackendRenderbufferObject();
|
||||||
// Deletes the driver renderbuffer; frontend glDeleteRenderbuffers used to leak it
|
|
||||||
// (with its whole image allocation) for the process lifetime.
|
|
||||||
~BackendRenderbufferObject();
|
|
||||||
BackendRenderbufferObject(const BackendRenderbufferObject&) = delete;
|
|
||||||
BackendRenderbufferObject& operator=(const BackendRenderbufferObject&) = delete;
|
|
||||||
void SyncToBackend(const SharedPtr<MG_State::GLState::RenderbufferObject>& stateRBOObject);
|
void SyncToBackend(const SharedPtr<MG_State::GLState::RenderbufferObject>& stateRBOObject);
|
||||||
Uint GetBackendRenderbufferId() const { return m_backendRBOId; }
|
Uint GetBackendRenderbufferId() const { return m_backendRBOId; }
|
||||||
void Bind() const;
|
void Bind() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Uint m_backendRBOId = 0;
|
Uint m_backendRBOId = 0;
|
||||||
Uint m_contextGeneration = 0;
|
|
||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
TextureInternalFormat m_cacheInternalFormat = TextureInternalFormat::Unknown;
|
TextureInternalFormat m_cacheInternalFormat = TextureInternalFormat::Unknown;
|
||||||
Int m_cacheWidth = 0;
|
Int m_cacheWidth = 0;
|
||||||
|
|||||||
@@ -768,48 +768,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS.
|
// the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS.
|
||||||
m_dynamicParameters.MaxVertexAttribs = std::min(
|
m_dynamicParameters.MaxVertexAttribs = std::min(
|
||||||
m_vulkanCaps.MaxVertexAttribs, static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
|
m_vulkanCaps.MaxVertexAttribs, static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
|
||||||
// Vulkan descriptor limits are not GL limits, and a GL application reads an advertised
|
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_vulkanCaps.MaxComputeShaderStorageBlocks;
|
||||||
// limit as an amount it may actually USE. Adreno answers the per-stage/per-set descriptor
|
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_vulkanCaps.MaxCombinedShaderStorageBlocks;
|
||||||
// queries at descriptor-indexing scale - the same driver whose
|
m_dynamicParameters.MaxComputeUniformBlocks = m_vulkanCaps.MaxComputeUniformBlocks;
|
||||||
// GL_MAX_SHADER_STORAGE_BLOCK_SIZE is clamped from 2147483647 further down - so
|
|
||||||
// KHR-GL44.multi_bind.dispatch_bind_buffers_base read GL_MAX_COMPUTE_UNIFORM_BLOCKS,
|
|
||||||
// created that many buffers and spliced that many UBO declarations into a single compute
|
|
||||||
// shader: ~14 s of allocation, then death on std::bad_alloc. Its sibling
|
|
||||||
// dispatch_bind_buffers_range hard-codes 4 buffers and passes, which is the clean
|
|
||||||
// discriminator. Every ceiling below is far above what any desktop driver advertises for
|
|
||||||
// these (84-96 for the binding families) and far below a descriptor-indexing count, so it
|
|
||||||
// can only lower a limit that was never usable in the first place. The zero floor is not
|
|
||||||
// decoration: a driver reporting UINT32_MAX used to arrive here as -1.
|
|
||||||
const auto clampLimit = [](const char* name, Int reported, Int ceiling) {
|
|
||||||
const Int clamped = std::min(std::max(reported, 0), ceiling);
|
|
||||||
if (clamped != reported) {
|
|
||||||
MGLOG_I("DirectVulkan: clamped %s from %d to %d", name, reported, clamped);
|
|
||||||
}
|
|
||||||
return clamped;
|
|
||||||
};
|
|
||||||
// GL 4.6 required minimums, for the record: MAX_COMPUTE_UNIFORM_BLOCKS 12,
|
|
||||||
// MAX_COMPUTE/COMBINED_SHADER_STORAGE_BLOCKS 8, MAX_SHADER_STORAGE_BUFFER_BINDINGS 8,
|
|
||||||
// MAX_UNIFORM_BUFFER_BINDINGS 84, MAX_TEXTURE_BUFFER_SIZE 65536.
|
|
||||||
constexpr Int kMaxAdvertisedBufferBlocks = 256;
|
|
||||||
constexpr Int kMaxAdvertisedTextureBufferSize = 1 << 27; // texels; what desktop GL reports
|
|
||||||
m_dynamicParameters.MaxComputeShaderStorageBlocks =
|
|
||||||
clampLimit("GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS", m_vulkanCaps.MaxComputeShaderStorageBlocks,
|
|
||||||
kMaxAdvertisedBufferBlocks);
|
|
||||||
m_dynamicParameters.MaxCombinedShaderStorageBlocks =
|
|
||||||
clampLimit("GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", m_vulkanCaps.MaxCombinedShaderStorageBlocks,
|
|
||||||
kMaxAdvertisedBufferBlocks);
|
|
||||||
m_dynamicParameters.MaxComputeUniformBlocks =
|
|
||||||
clampLimit("GL_MAX_COMPUTE_UNIFORM_BLOCKS", m_vulkanCaps.MaxComputeUniformBlocks,
|
|
||||||
kMaxAdvertisedBufferBlocks);
|
|
||||||
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
|
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
|
||||||
m_dynamicParameters.MaxShaderStorageBufferBindings =
|
m_dynamicParameters.MaxShaderStorageBufferBindings = m_vulkanCaps.MaxShaderStorageBufferBindings;
|
||||||
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
|
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
|
||||||
kMaxAdvertisedBufferBlocks);
|
|
||||||
m_dynamicParameters.MaxTextureBufferSize = clampLimit(
|
|
||||||
"GL_MAX_TEXTURE_BUFFER_SIZE", m_vulkanCaps.MaxTextureBufferSize, kMaxAdvertisedTextureBufferSize);
|
|
||||||
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
|
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
|
||||||
m_dynamicParameters.MaxUniformBufferBindings = clampLimit(
|
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
|
||||||
"GL_MAX_UNIFORM_BUFFER_BINDINGS", m_vulkanCaps.MaxUniformBufferBindings, kMaxAdvertisedBufferBlocks);
|
|
||||||
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
|
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
|
||||||
m_dynamicParameters.MaxImageUnits = std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
|
m_dynamicParameters.MaxImageUnits = std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
|
||||||
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
|
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
|
||||||
|
|||||||
@@ -966,29 +966,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (drawcount <= 0) {
|
if (drawcount <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// With no element-array buffer bound, every indices[i] is a client pointer into a
|
|
||||||
// separate CPU allocation, not an offset into one shared buffer. The batched payload
|
|
||||||
// below cannot express that: it carries ONE index-buffer view for the whole batch and
|
|
||||||
// turns each pointer into a firstIndex relative to it. Replay the sub-draws through
|
|
||||||
// the single-draw entry point instead - it snapshots each client range into its own
|
|
||||||
// transient slice, which is exactly what the unrolled draws this must match do.
|
|
||||||
// (The batch used to be built this way; the shared-view rewrite that added
|
|
||||||
// MultiDrawIndexedCmd left the client-memory shape addressing a view whose byte
|
|
||||||
// offset is a hardcoded 0, so UploadAndBindIndexBuffer saw a null client pointer,
|
|
||||||
// declined the whole batch and painted nothing.)
|
|
||||||
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
|
||||||
if (vao.GetIndexBufferBindingSlot().GetBoundObject() == nullptr) {
|
|
||||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
|
||||||
if (count[i] <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
DrawElementsBaseVertex(mode, count[i], type, indices[i],
|
|
||||||
basevertex != nullptr ? basevertex[i] : 0);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiDrawIndexedCmd payload{};
|
MultiDrawIndexedCmd payload{};
|
||||||
payload.mode = mode;
|
payload.mode = mode;
|
||||||
payload.indexBufferView.indexType = type;
|
payload.indexBufferView.indexType = type;
|
||||||
|
|||||||
@@ -111,11 +111,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IsValid()) {
|
if (buffer.IsValid()) {
|
||||||
// Outgrown, not dead: every BufferSlice handed out from this frame's arena so far
|
|
||||||
// still names it, and those slices stay in service until the frame slot is rewound
|
|
||||||
// (VkBufferResource::transientSlice, the converted-vertex-stream cache, the draw
|
|
||||||
// memos). The release therefore has to survive every mid-frame reclaim and land on
|
|
||||||
// the next ResetFrame of this slot - see VkBufferManager::CollectAllDeferredReleases.
|
|
||||||
m_deferredReleases[frameIndex].push_back(std::move(buffer));
|
m_deferredReleases[frameIndex].push_back(std::move(buffer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -252,19 +252,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
VkPipeline pipeline = CreatePipeline(payload);
|
VkPipeline pipeline = CreatePipeline(payload);
|
||||||
// A failed creation must never be memoized. Caching VK_NULL_HANDLE served the null back for
|
|
||||||
// the rest of the process, so one transient driver rejection turned every later draw with
|
|
||||||
// the same state into a vkCmdBindPipeline(VK_NULL_HANDLE) - the SIGSEGV behind 9 of the 15
|
|
||||||
// CTS process deaths. Retrying costs one failed vkCreateGraphicsPipelines per draw, which
|
|
||||||
// is the correct price for a broken pipeline and is bounded by the draw itself being
|
|
||||||
// skipped.
|
|
||||||
if (pipeline == VK_NULL_HANDLE) {
|
|
||||||
MGLOG_I("PipelineFactory::GetOrCreatePipeline: creation failed for hash=0x%llx "
|
|
||||||
"programHash=0x%llx; not caching the failure",
|
|
||||||
static_cast<unsigned long long>(hash),
|
|
||||||
static_cast<unsigned long long>(payload.programHash));
|
|
||||||
return VK_NULL_HANDLE;
|
|
||||||
}
|
|
||||||
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass,
|
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass,
|
||||||
m_frameCounter});
|
m_frameCounter});
|
||||||
return pipeline;
|
return pipeline;
|
||||||
@@ -520,35 +507,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
MGLOG_F("PipelineFactory::CreatePipeline vertex input: bindingCount=%u attributeCount=%u",
|
MGLOG_F("PipelineFactory::CreatePipeline vertex input: bindingCount=%u attributeCount=%u",
|
||||||
payload.vertexInputState->vertexBindingDescriptionCount,
|
payload.vertexInputState->vertexBindingDescriptionCount,
|
||||||
payload.vertexInputState->vertexAttributeDescriptionCount);
|
payload.vertexInputState->vertexAttributeDescriptionCount);
|
||||||
// The driver's own answer is VK_ERROR_UNKNOWN, i.e. no information at all, so the only
|
|
||||||
// way to work out WHICH shader it choked on (the open sampler-array-in-struct
|
|
||||||
// investigation) is to name the modules. MGLOG_I, not _D/_E: this must survive in the
|
|
||||||
// INFO-level builds that CTS actually runs against.
|
|
||||||
if (payload.stageSpirvDigests) {
|
|
||||||
for (SizeT i = 0; i < payload.stageSpirvDigests->size(); ++i) {
|
|
||||||
const auto& digest = (*payload.stageSpirvDigests)[i];
|
|
||||||
MGLOG_I("PipelineFactory::CreatePipeline spirv[%zu]: stage=0x%x words=%u bytes=%zu "
|
|
||||||
"hash=0x%llx",
|
|
||||||
i, digest.stage, digest.wordCount,
|
|
||||||
static_cast<SizeT>(digest.wordCount) * sizeof(Uint32),
|
|
||||||
static_cast<unsigned long long>(digest.hash));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
MGLOG_I("PipelineFactory::CreatePipeline: no SPIR-V digests attached to the payload");
|
|
||||||
}
|
|
||||||
if (payload.stages) {
|
|
||||||
for (SizeT i = 0; i < payload.stages->size(); ++i) {
|
|
||||||
const auto& stage = (*payload.stages)[i];
|
|
||||||
// VkShaderModule is a non-dispatchable handle: a pointer on 64-bit but a
|
|
||||||
// plain uint64_t on 32-bit ABIs, where a cast to const void* is ill-formed
|
|
||||||
// (broke the armeabi-v7a build). Print it as the 64-bit value it is.
|
|
||||||
MGLOG_I("PipelineFactory::CreatePipeline stage[%zu]: stage=0x%x module=0x%llx entry=%s "
|
|
||||||
"specialization=%d",
|
|
||||||
i, static_cast<Uint32>(stage.stage),
|
|
||||||
static_cast<unsigned long long>(reinterpret_cast<Uint64>(stage.module)),
|
|
||||||
stage.pName ? stage.pName : "(null)", stage.pSpecializationInfo ? 1 : 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
|
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
|
||||||
const auto& attachment = payload.colorBlendAttachments[i];
|
const auto& attachment = payload.colorBlendAttachments[i];
|
||||||
MGLOG_F("PipelineFactory::CreatePipeline colorAttachment[%u]: blend=%d colorWriteMask=0x%x srcColor=%d dstColor=%d colorOp=%d srcAlpha=%d dstAlpha=%d alphaOp=%d",
|
MGLOG_F("PipelineFactory::CreatePipeline colorAttachment[%u]: blend=%d colorWriteMask=0x%x srcColor=%d dstColor=%d colorOp=%d srcAlpha=%d dstAlpha=%d alphaOp=%d",
|
||||||
|
|||||||
@@ -14,16 +14,6 @@
|
|||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
// Enough of a fingerprint to identify the exact module the driver rejected without keeping the
|
|
||||||
// SPIR-V alive for every program in the cache: a driver that answers VK_ERROR_UNKNOWN tells us
|
|
||||||
// nothing, so the log has to carry the shader's identity itself. Diagnostic only - never part
|
|
||||||
// of any pipeline or program hash.
|
|
||||||
struct ShaderStageSpirvDigest {
|
|
||||||
Uint32 stage = 0; // VkShaderStageFlagBits
|
|
||||||
Uint32 wordCount = 0;
|
|
||||||
Uint64 hash = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
class PipelineFactory {
|
class PipelineFactory {
|
||||||
public:
|
public:
|
||||||
using HashType = Uint64;
|
using HashType = Uint64;
|
||||||
@@ -72,8 +62,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
|
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
|
||||||
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
|
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
|
||||||
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
|
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
|
||||||
// Diagnostic only; may be null. Read solely from the pipeline-creation failure path.
|
|
||||||
const Vector<ShaderStageSpirvDigest>* stageSpirvDigests = nullptr;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit PipelineFactory(VkDevice device, const VulkanRendererConfig& config);
|
explicit PipelineFactory(VkDevice device, const VulkanRendererConfig& config);
|
||||||
|
|||||||
@@ -12,10 +12,7 @@
|
|||||||
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
||||||
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
|
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
|
||||||
#include "MG_Util/ShaderTranspiler/Types.h"
|
#include "MG_Util/ShaderTranspiler/Types.h"
|
||||||
#include <algorithm>
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <map>
|
|
||||||
#include <utility>
|
|
||||||
#include <spirv-tools/libspirv.h>
|
#include <spirv-tools/libspirv.h>
|
||||||
#include <spirv-tools/optimizer.hpp>
|
#include <spirv-tools/optimizer.hpp>
|
||||||
#include <source/opt/build_module.h>
|
#include <source/opt/build_module.h>
|
||||||
@@ -375,20 +372,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
spv_diagnostic diagnostic = nullptr;
|
spv_diagnostic diagnostic = nullptr;
|
||||||
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
|
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
|
||||||
if (result != SPV_SUCCESS) {
|
|
||||||
// MGLOG_I, not E: at the INFO compile level of the CI/test lanes that arm
|
|
||||||
// the validation switch, MGLOG_E is compiled out (Log.h orders
|
|
||||||
// DEBUG < WARN < ERROR < INFO) and the VUID would never reach a log. The
|
|
||||||
// latch is what a test harness asserts on.
|
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
|
|
||||||
MGLOG_I(
|
|
||||||
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d index=%zu msg=%s",
|
|
||||||
static_cast<Int>(shaderStage),
|
|
||||||
programExternalIndex,
|
|
||||||
static_cast<Int>(result),
|
|
||||||
diagnostic != nullptr ? diagnostic->position.index : 0,
|
|
||||||
diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<null>");
|
|
||||||
}
|
|
||||||
MOBILEGL_ASSERT(
|
MOBILEGL_ASSERT(
|
||||||
result == SPV_SUCCESS,
|
result == SPV_SUCCESS,
|
||||||
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d line=%zu column=%zu index=%zu msg=%s",
|
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d line=%zu column=%zu index=%zu msg=%s",
|
||||||
@@ -940,189 +923,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ProgramFactory::CompileOptionFlags m_transformFlags;
|
ProgramFactory::CompileOptionFlags m_transformFlags;
|
||||||
};
|
};
|
||||||
|
|
||||||
// gl_FragCoord back into GL's window space, for default-framebuffer draws only.
|
|
||||||
//
|
|
||||||
// Vulkan's gl_FragCoord.y is the framebuffer ROW being written - not a value the
|
|
||||||
// viewport rect can move independently of placement. The default framebuffer's image is
|
|
||||||
// stored display-side-up and the vertex stage compensates by negating gl_Position.y, so
|
|
||||||
// for every default-FBO draw the framebuffer row of a fragment is exactly
|
|
||||||
// `height - y_GL` (the viewport terms cancel: yf_VK = H - yf_GL for any viewport rect).
|
|
||||||
// A shader that reads gl_FragCoord therefore sees a flipped Y, and once the viewport
|
|
||||||
// rect started being converted to the stored orientation it also sees a Y that is
|
|
||||||
// OUTSIDE the range GL promises - a 32-pixel-tall viewport at GL y=0 reports 224..255 on
|
|
||||||
// a 256-tall surface. GL CTS shader_image_load_store writes imageStore(image,
|
|
||||||
// ivec2(gl_FragCoord.xy)) into an image exactly the size of that viewport, so every
|
|
||||||
// store fell outside the image and the test read back zeroes.
|
|
||||||
//
|
|
||||||
// The rewrite redirects every read of the builtin to a Private copy initialised once at
|
|
||||||
// entry, which is exact for all access forms (whole-vector loads, `.y` access chains,
|
|
||||||
// OpCopyMemory) and leaves the builtin itself - and its decorations - untouched.
|
|
||||||
class GlFragCoordYFlipPass final : public spvtools::opt::Pass {
|
|
||||||
public:
|
|
||||||
const char* name() const override { return "mobilegl-fragcoord-y-flip"; }
|
|
||||||
explicit GlFragCoordYFlipPass(Uint32 framebufferHeight) : m_framebufferHeight(framebufferHeight) {}
|
|
||||||
|
|
||||||
Status Process() override {
|
|
||||||
using namespace spvtools::opt;
|
|
||||||
if (m_framebufferHeight == 0) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
Instruction* entryPoint = nullptr;
|
|
||||||
for (auto& candidate : get_module()->entry_points()) {
|
|
||||||
if (candidate.NumInOperands() >= 2 &&
|
|
||||||
static_cast<spv::ExecutionModel>(candidate.GetSingleWordInOperand(0)) ==
|
|
||||||
spv::ExecutionModel::Fragment) {
|
|
||||||
entryPoint = &candidate;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!entryPoint) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
const Uint32 builtinVarId = FindFragCoordVariable();
|
|
||||||
if (builtinVarId == 0) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
Instruction* builtinVar = context()->get_def_use_mgr()->GetDef(builtinVarId);
|
|
||||||
if (!builtinVar || builtinVar->opcode() != spv::Op::OpVariable) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
// The builtin is `Input vec4`; take the vector and component types from its own
|
|
||||||
// pointer type rather than assuming float32x4, so a module that spells it
|
|
||||||
// differently declines instead of miscompiling.
|
|
||||||
Instruction* inputPtrType = context()->get_def_use_mgr()->GetDef(builtinVar->type_id());
|
|
||||||
if (!inputPtrType || inputPtrType->opcode() != spv::Op::OpTypePointer) {
|
|
||||||
return Status::SuccessWithoutChange;
|
|
||||||
}
|
|
||||||
const Uint32 vectorTypeId = inputPtrType->GetSingleWordInOperand(1);
|
|
||||||
Instruction* vectorType = context()->get_def_use_mgr()->GetDef(vectorTypeId);
|
|
||||||
if (!vectorType || vectorType->opcode() != spv::Op::OpTypeVector ||
|
|
||||||
vectorType->GetSingleWordInOperand(1) != 4) {
|
|
||||||
return Status::SuccessWithoutChange;
|
|
||||||
}
|
|
||||||
const Uint32 floatTypeId = vectorType->GetSingleWordInOperand(0);
|
|
||||||
auto* floatType = context()->get_type_mgr()->GetType(floatTypeId);
|
|
||||||
if (!floatType || !floatType->AsFloat() || floatType->AsFloat()->width() != 32) {
|
|
||||||
return Status::SuccessWithoutChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto heightBits = std::bit_cast<Uint32>(static_cast<float>(m_framebufferHeight));
|
|
||||||
const auto* heightConst = context()->get_constant_mgr()->GetConstant(floatType, {heightBits});
|
|
||||||
auto* heightInst = context()->get_constant_mgr()->GetDefiningInstruction(heightConst);
|
|
||||||
if (!heightInst) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
auto* function = context()->GetFunction(entryPoint->GetSingleWordInOperand(1));
|
|
||||||
if (!function || function->begin() == function->end()) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
const Uint32 privatePtrTypeId =
|
|
||||||
context()->get_type_mgr()->FindPointerToType(vectorTypeId, spv::StorageClass::Private);
|
|
||||||
if (privatePtrTypeId == 0) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
const Uint32 copyVarId = context()->TakeNextId();
|
|
||||||
if (copyVarId == 0) return Status::SuccessWithoutChange;
|
|
||||||
auto copyVar = std::make_unique<Instruction>(
|
|
||||||
context(), spv::Op::OpVariable, privatePtrTypeId, copyVarId,
|
|
||||||
std::initializer_list<Operand>{
|
|
||||||
{SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast<Uint32>(spv::StorageClass::Private)}}});
|
|
||||||
context()->AddGlobalValue(std::move(copyVar));
|
|
||||||
|
|
||||||
// Redirect the reads BEFORE emitting the initialiser, so the initialiser's own
|
|
||||||
// load of the builtin is not rewritten into a load of the (still empty) copy.
|
|
||||||
if (!RedirectReads(builtinVarId, copyVarId)) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
auto& entryBlock = *function->begin();
|
|
||||||
auto insertPoint = entryBlock.begin();
|
|
||||||
while (insertPoint != entryBlock.end() && insertPoint->opcode() == spv::Op::OpVariable) {
|
|
||||||
++insertPoint;
|
|
||||||
}
|
|
||||||
if (insertPoint == entryBlock.end()) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
InstructionBuilder builder(context(), &*insertPoint,
|
|
||||||
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
|
||||||
auto* raw = builder.AddLoad(vectorTypeId, builtinVarId);
|
|
||||||
if (!raw) return Status::SuccessWithoutChange;
|
|
||||||
auto* x = builder.AddCompositeExtract(floatTypeId, raw->result_id(), {0});
|
|
||||||
auto* y = builder.AddCompositeExtract(floatTypeId, raw->result_id(), {1});
|
|
||||||
auto* z = builder.AddCompositeExtract(floatTypeId, raw->result_id(), {2});
|
|
||||||
auto* w = builder.AddCompositeExtract(floatTypeId, raw->result_id(), {3});
|
|
||||||
if (!x || !y || !z || !w) return Status::SuccessWithoutChange;
|
|
||||||
auto* flippedY =
|
|
||||||
builder.AddBinaryOp(floatTypeId, spv::Op::OpFSub, heightInst->result_id(), y->result_id());
|
|
||||||
if (!flippedY) return Status::SuccessWithoutChange;
|
|
||||||
auto* corrected = builder.AddCompositeConstruct(
|
|
||||||
vectorTypeId, {x->result_id(), flippedY->result_id(), z->result_id(), w->result_id()});
|
|
||||||
if (!corrected) return Status::SuccessWithoutChange;
|
|
||||||
if (!builder.AddStore(copyVarId, corrected->result_id())) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
// SPIR-V 1.4 widened the entry-point interface to every global the entry point
|
|
||||||
// statically uses, Private included; earlier versions accept Input/Output only,
|
|
||||||
// so listing it there would be invalid.
|
|
||||||
if (get_module()->version() >= 0x00010400u) {
|
|
||||||
entryPoint->AddOperand({SPV_OPERAND_TYPE_ID, {copyVarId}});
|
|
||||||
context()->AnalyzeUses(entryPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisDefUse |
|
|
||||||
spvtools::opt::IRContext::kAnalysisInstrToBlockMapping);
|
|
||||||
return Status::SuccessWithChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
Uint32 FindFragCoordVariable() const {
|
|
||||||
for (const auto& annotation : get_module()->annotations()) {
|
|
||||||
if (annotation.opcode() != spv::Op::OpDecorate) continue;
|
|
||||||
if (annotation.NumInOperands() < 3) continue;
|
|
||||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
|
|
||||||
spv::Decoration::BuiltIn) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2)) != spv::BuiltIn::FragCoord) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return annotation.GetSingleWordInOperand(0);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Every instruction that reads through the builtin's POINTER gets the copy instead.
|
|
||||||
// Decorations, names and the entry-point interface keep naming the builtin.
|
|
||||||
Bool RedirectReads(Uint32 builtinVarId, Uint32 copyVarId) {
|
|
||||||
using namespace spvtools::opt;
|
|
||||||
Bool ok = true;
|
|
||||||
Vector<Instruction*> users;
|
|
||||||
context()->get_def_use_mgr()->ForEachUser(builtinVarId, [&](Instruction* user) {
|
|
||||||
switch (user->opcode()) {
|
|
||||||
case spv::Op::OpLoad:
|
|
||||||
case spv::Op::OpAccessChain:
|
|
||||||
case spv::Op::OpInBoundsAccessChain:
|
|
||||||
case spv::Op::OpPtrAccessChain:
|
|
||||||
case spv::Op::OpInBoundsPtrAccessChain:
|
|
||||||
case spv::Op::OpCopyMemory:
|
|
||||||
case spv::Op::OpCopyMemorySized:
|
|
||||||
users.push_back(user);
|
|
||||||
break;
|
|
||||||
case spv::Op::OpStore:
|
|
||||||
// gl_FragCoord is read-only; a store through it means this is not the
|
|
||||||
// module we think it is.
|
|
||||||
ok = false;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!ok) return false;
|
|
||||||
for (Instruction* user : users) {
|
|
||||||
for (Uint32 i = 0; i < user->NumInOperands(); ++i) {
|
|
||||||
auto& operand = user->GetInOperand(i);
|
|
||||||
if (operand.type == SPV_OPERAND_TYPE_ID && !operand.words.empty() &&
|
|
||||||
operand.words[0] == builtinVarId) {
|
|
||||||
operand.words[0] = copyVarId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
context()->AnalyzeUses(user);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint32 m_framebufferHeight = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Decorates the module's captured varyings for VK_EXT_transform_feedback:
|
// Decorates the module's captured varyings for VK_EXT_transform_feedback:
|
||||||
// user outputs get XfbBuffer/XfbStride/Offset directly; a captured
|
// user outputs get XfbBuffer/XfbStride/Offset directly; a captured
|
||||||
// gl_Position (a gl_PerVertex member) is mirrored into a dedicated output
|
// gl_Position (a gl_PerVertex member) is mirrored into a dedicated output
|
||||||
@@ -1134,15 +934,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
std::string name;
|
std::string name;
|
||||||
Uint32 bufferIndex = 0;
|
Uint32 bufferIndex = 0;
|
||||||
Uint32 offsetBytes = 0;
|
Uint32 offsetBytes = 0;
|
||||||
// Set when the capture names a member of an output interface block
|
|
||||||
// ("Block.member"): the decoration target is then the block's struct TYPE,
|
|
||||||
// decorated per member, not the variable. `name` keeps the GL spelling and
|
|
||||||
// is useless for the id lookup, so the instance name is carried separately.
|
|
||||||
std::string blockInstanceName;
|
|
||||||
std::string blockName;
|
|
||||||
Int blockMemberIndex = -1;
|
|
||||||
Int blockMemberElement = -1; // array element of that member, -1 = the whole member
|
|
||||||
Uint32 byteSize = 0;
|
|
||||||
};
|
};
|
||||||
const char* name() const override { return "mobilegl-xfb-capture-decorate"; }
|
const char* name() const override { return "mobilegl-xfb-capture-decorate"; }
|
||||||
XfbCaptureDecoratePass(Vector<CapturedVarying> varyings, Vector<Uint32> strides)
|
XfbCaptureDecoratePass(Vector<CapturedVarying> varyings, Vector<Uint32> strides)
|
||||||
@@ -1174,33 +965,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
decorationManager->AddDecorationVal(targetId, static_cast<Uint32>(spv::Decoration::Offset),
|
decorationManager->AddDecorationVal(targetId, static_cast<Uint32>(spv::Decoration::Offset),
|
||||||
offsetBytes);
|
offsetBytes);
|
||||||
};
|
};
|
||||||
// SPIR-V puts XfbBuffer/XfbStride/Offset on the struct MEMBER when the
|
|
||||||
// captured varying lives in an interface block (SPIR-V 1.6 §3.20 lists all
|
|
||||||
// three as member-decoratable); Offset in particular is illegal on the block
|
|
||||||
// variable once the type is decorated Block.
|
|
||||||
const auto decorateMemberForXfb = [&](Uint32 structTypeId, Uint32 memberIndex, Uint32 bufferIndex,
|
|
||||||
Uint32 offsetBytes) {
|
|
||||||
const Uint32 stride = bufferIndex < m_strides.size() ? m_strides[bufferIndex] : 0;
|
|
||||||
decorationManager->AddMemberDecoration(structTypeId, memberIndex,
|
|
||||||
static_cast<Uint32>(spv::Decoration::XfbBuffer),
|
|
||||||
bufferIndex);
|
|
||||||
decorationManager->AddMemberDecoration(structTypeId, memberIndex,
|
|
||||||
static_cast<Uint32>(spv::Decoration::XfbStride), stride);
|
|
||||||
decorationManager->AddMemberDecoration(structTypeId, memberIndex,
|
|
||||||
static_cast<Uint32>(spv::Decoration::Offset), offsetBytes);
|
|
||||||
};
|
|
||||||
|
|
||||||
// A member array captured element by element ("Block.attrib[0]" .. "[15]")
|
|
||||||
// is one SPIR-V member, so its captures collapse into a single decoration
|
|
||||||
// placed at the first element's offset - the rest follow from the member's
|
|
||||||
// own layout. Collected first so the group is complete before it decorates.
|
|
||||||
struct MemberGroup {
|
|
||||||
Uint32 bufferIndex = 0;
|
|
||||||
Uint32 minOffset = 0;
|
|
||||||
Uint32 elementBytes = 0;
|
|
||||||
Vector<Uint32> offsets;
|
|
||||||
};
|
|
||||||
std::map<std::pair<Uint32, Uint32>, MemberGroup> memberGroups;
|
|
||||||
|
|
||||||
Bool modified = false;
|
Bool modified = false;
|
||||||
Bool needsPositionMirror = false;
|
Bool needsPositionMirror = false;
|
||||||
@@ -1213,41 +977,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
positionOffset = varying.offsetBytes;
|
positionOffset = varying.offsetBytes;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (varying.blockMemberIndex >= 0) {
|
|
||||||
// glslang names the block's instance variable and its struct type
|
|
||||||
// separately; an anonymous instance leaves only the type named, so
|
|
||||||
// both spellings are tried before giving up.
|
|
||||||
Uint32 structTypeId = 0;
|
|
||||||
if (const auto it = idsByName.find(varying.blockInstanceName); it != idsByName.end()) {
|
|
||||||
structTypeId = BlockStructTypeOf(it->second);
|
|
||||||
}
|
|
||||||
if (structTypeId == 0) {
|
|
||||||
if (const auto it = idsByName.find(varying.blockName); it != idsByName.end()) {
|
|
||||||
const spvtools::opt::Instruction* def = context()->get_def_use_mgr()->GetDef(it->second);
|
|
||||||
if (def != nullptr && def->opcode() == spv::Op::OpTypeStruct) {
|
|
||||||
structTypeId = it->second;
|
|
||||||
} else if (def != nullptr && def->opcode() == spv::Op::OpVariable) {
|
|
||||||
structTypeId = BlockStructTypeOf(it->second);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (structTypeId == 0) {
|
|
||||||
MGLOG_E("XfbCaptureDecoratePass: no SPIR-V interface block '%s' (instance '%s') for "
|
|
||||||
"capture '%s'",
|
|
||||||
varying.blockName.c_str(), varying.blockInstanceName.c_str(),
|
|
||||||
varying.name.c_str());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
auto& group =
|
|
||||||
memberGroups[{structTypeId, static_cast<Uint32>(varying.blockMemberIndex)}];
|
|
||||||
if (group.offsets.empty() || varying.offsetBytes < group.minOffset) {
|
|
||||||
group.minOffset = varying.offsetBytes;
|
|
||||||
}
|
|
||||||
group.bufferIndex = varying.bufferIndex;
|
|
||||||
group.elementBytes = varying.byteSize;
|
|
||||||
group.offsets.push_back(varying.offsetBytes);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const auto idIt = idsByName.find(varying.name);
|
const auto idIt = idsByName.find(varying.name);
|
||||||
if (idIt == idsByName.end()) {
|
if (idIt == idsByName.end()) {
|
||||||
MGLOG_E("XfbCaptureDecoratePass: no SPIR-V variable named '%s'", varying.name.c_str());
|
MGLOG_E("XfbCaptureDecoratePass: no SPIR-V variable named '%s'", varying.name.c_str());
|
||||||
@@ -1257,25 +986,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
modified = true;
|
modified = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto& [key, group] : memberGroups) {
|
|
||||||
// The single Offset can only stand for the whole group when the group's
|
|
||||||
// captures are a gap-free ascending run - that is what SPIR-V lays the
|
|
||||||
// member's elements out as. Anything else still gets a best-effort
|
|
||||||
// decoration, but say so, because the capture layout will not match GL.
|
|
||||||
std::sort(group.offsets.begin(), group.offsets.end());
|
|
||||||
for (SizeT i = 1; i < group.offsets.size(); ++i) {
|
|
||||||
if (group.elementBytes == 0 ||
|
|
||||||
group.offsets[i] != group.offsets[i - 1] + group.elementBytes) {
|
|
||||||
MGLOG_I("XfbCaptureDecoratePass: block member %u of type %%%u is captured with a "
|
|
||||||
"non-contiguous element set; the capture layout will differ from GL's",
|
|
||||||
key.second, key.first);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
decorateMemberForXfb(key.first, key.second, group.bufferIndex, group.minOffset);
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needsPositionMirror) {
|
if (needsPositionMirror) {
|
||||||
modified |= MirrorPositionForCapture(entryFunctionId, *entryPoint, positionBufferIndex,
|
modified |= MirrorPositionForCapture(entryFunctionId, *entryPoint, positionBufferIndex,
|
||||||
positionOffset, decorateForXfb);
|
positionOffset, decorateForXfb);
|
||||||
@@ -1297,27 +1007,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// The struct type an interface-block variable points at, peeling an array of
|
|
||||||
// block instances on the way. 0 when the id is not a block variable at all.
|
|
||||||
Uint32 BlockStructTypeOf(Uint32 variableId) {
|
|
||||||
auto* defUse = context()->get_def_use_mgr();
|
|
||||||
const spvtools::opt::Instruction* variable = defUse->GetDef(variableId);
|
|
||||||
if (variable == nullptr || variable->opcode() != spv::Op::OpVariable) return 0;
|
|
||||||
const spvtools::opt::Instruction* pointer = defUse->GetDef(variable->type_id());
|
|
||||||
if (pointer == nullptr || pointer->opcode() != spv::Op::OpTypePointer) return 0;
|
|
||||||
Uint32 pointeeId = pointer->GetSingleWordInOperand(1);
|
|
||||||
for (const spvtools::opt::Instruction* pointee = defUse->GetDef(pointeeId); pointee != nullptr;
|
|
||||||
pointee = defUse->GetDef(pointeeId)) {
|
|
||||||
if (pointee->opcode() == spv::Op::OpTypeStruct) return pointeeId;
|
|
||||||
if (pointee->opcode() != spv::Op::OpTypeArray &&
|
|
||||||
pointee->opcode() != spv::Op::OpTypeRuntimeArray) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
pointeeId = pointee->GetSingleWordInOperand(0);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename DecorateFn>
|
template <typename DecorateFn>
|
||||||
Bool MirrorPositionForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint,
|
Bool MirrorPositionForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint,
|
||||||
Uint32 bufferIndex, Uint32 offsetBytes, const DecorateFn& decorateForXfb) {
|
Uint32 bufferIndex, Uint32 offsetBytes, const DecorateFn& decorateForXfb) {
|
||||||
@@ -1574,10 +1263,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||||
spvtools::OptimizerOptions options;
|
spvtools::OptimizerOptions options;
|
||||||
// Always off: the optimizer's input validator conflates "input invalid" with
|
// Matches the position-fix pass: this build of spirv-tools asserts rather than
|
||||||
// "transform failed", and this call site fails open. Validating lanes check the
|
// reporting, so validation stays off in the shipping path.
|
||||||
// FINAL module via ValidateTransformedSpirv, which latches instead of rerouting
|
|
||||||
// control flow.
|
|
||||||
options.set_run_validator(false);
|
options.set_run_validator(false);
|
||||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
||||||
const char* message) {
|
const char* message) {
|
||||||
@@ -1598,35 +1285,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
|
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool TransformSpirvForFragCoordYFlip(const Vector<Uint>& input, Vector<Uint>& output,
|
|
||||||
Uint32 framebufferHeight) {
|
|
||||||
if (input.empty()) {
|
|
||||||
output.clear();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (framebufferHeight == 0) {
|
|
||||||
output = input;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
|
||||||
spvtools::OptimizerOptions options;
|
|
||||||
options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling
|
|
||||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
|
||||||
const char* message) {
|
|
||||||
MGLOG_E("Vulkan: fragcoord y-flip pass: %s", message != nullptr ? message : "");
|
|
||||||
});
|
|
||||||
optimizer.RegisterPass(
|
|
||||||
spvtools::Optimizer::PassToken(MakeUnique<GlFragCoordYFlipPass>(framebufferHeight)));
|
|
||||||
|
|
||||||
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
|
||||||
if (!success) {
|
|
||||||
MGLOG_E("Vulkan: failed to run the gl_FragCoord y-flip pass; keeping the original module");
|
|
||||||
output = input;
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool TransformSpirvForXfbCapture(const Vector<Uint>& input, Vector<Uint>& output,
|
Bool TransformSpirvForXfbCapture(const Vector<Uint>& input, Vector<Uint>& output,
|
||||||
const MG_State::GLState::ProgramObject& program) {
|
const MG_State::GLState::ProgramObject& program) {
|
||||||
if (input.empty()) {
|
if (input.empty()) {
|
||||||
@@ -1636,9 +1294,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<XfbCaptureDecoratePass::CapturedVarying> varyings;
|
Vector<XfbCaptureDecoratePass::CapturedVarying> varyings;
|
||||||
varyings.reserve(program.GetTransformFeedbackVaryingCount());
|
varyings.reserve(program.GetTransformFeedbackVaryingCount());
|
||||||
for (const auto& varying : program.GetTransformFeedbackVaryings()) {
|
for (const auto& varying : program.GetTransformFeedbackVaryings()) {
|
||||||
varyings.push_back({varying.name, varying.bufferIndex, varying.offsetBytes,
|
varyings.push_back({varying.name, varying.bufferIndex, varying.offsetBytes});
|
||||||
varying.blockInstanceName, varying.blockName, varying.blockMemberIndex,
|
|
||||||
varying.blockMemberElement, varying.byteSize});
|
|
||||||
}
|
}
|
||||||
Vector<Uint32> strides;
|
Vector<Uint32> strides;
|
||||||
strides.reserve(program.GetTransformFeedbackBufferCount());
|
strides.reserve(program.GetTransformFeedbackBufferCount());
|
||||||
@@ -1648,7 +1304,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||||
spvtools::OptimizerOptions options;
|
spvtools::OptimizerOptions options;
|
||||||
options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling
|
options.set_run_validator(false);
|
||||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
||||||
const char* message) {
|
const char* message) {
|
||||||
MGLOG_E("Vulkan: xfb capture pass: %s", message != nullptr ? message : "");
|
MGLOG_E("Vulkan: xfb capture pass: %s", message != nullptr ? message : "");
|
||||||
@@ -1678,11 +1334,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||||
spvtools::OptimizerOptions options;
|
spvtools::OptimizerOptions options;
|
||||||
options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling
|
options.set_run_validator(false);
|
||||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
|
||||||
const char* message) {
|
|
||||||
MGLOG_E("Vulkan: position fix pass: %s", message != nullptr ? message : "");
|
|
||||||
});
|
|
||||||
optimizer.RegisterPass(CreateGlToVulkanPositionFixPass(transformFlags));
|
optimizer.RegisterPass(CreateGlToVulkanPositionFixPass(transformFlags));
|
||||||
|
|
||||||
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
||||||
@@ -2100,12 +1752,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
XXHASH_VERIFY(XXH64_update(m_hashState, spv.data(), spv.size() * sizeof(Uint)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, spv.data(), spv.size() * sizeof(Uint)));
|
||||||
}
|
}
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &flags, sizeof(CompileOptionFlags)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &flags, sizeof(CompileOptionFlags)));
|
||||||
// Only FragCoordYFlip variants bake the height in, so mixing it unconditionally would
|
|
||||||
// re-key every program in the cache on a resize for no reason.
|
|
||||||
if (flags & CompileOptionBit::FragCoordYFlip) {
|
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &m_defaultFramebufferHeight,
|
|
||||||
sizeof(m_defaultFramebufferHeight)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include UBO block bindings in hash so different binding configurations produce different entries
|
// Include UBO block bindings in hash so different binding configurations produce different entries
|
||||||
const Uint32 blockCount = static_cast<Uint32>(program.GetActiveUniformBlocksCount());
|
const Uint32 blockCount = static_cast<Uint32>(program.GetActiveUniformBlocksCount());
|
||||||
@@ -2578,16 +2224,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
entry.storageBlockNameByBinding[binding] = uniformName;
|
entry.storageBlockNameByBinding[binding] = uniformName;
|
||||||
entry.storageBlockIndexByBinding[binding] = static_cast<Int>(blockIndex);
|
entry.storageBlockIndexByBinding[binding] = static_cast<Int>(blockIndex);
|
||||||
|
|
||||||
// A block INSTANCE array is ONE Vulkan binding carrying `count`
|
|
||||||
// descriptors, while GL assigns its elements consecutive binding points
|
|
||||||
// starting at the declared one (GL 4.6 core 7.8). Recording only element 0 -
|
|
||||||
// which is all this used to do - left the layout claiming descriptorCount 1,
|
|
||||||
// so every element past the first read a descriptor nobody wrote and
|
|
||||||
// `b[1].data.length()` answered from an unconstrained buffer instead of its
|
|
||||||
// own bound range (KHR-GL43.shader_storage_buffer_object.-
|
|
||||||
// advanced-unsizedArrayLength-*).
|
|
||||||
entry.bindingDescriptorCounts[binding] = static_cast<Uint16>(std::max<Uint32>(1u, sampler->count));
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2724,36 +2360,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramFactory::SetDefaultFramebufferHeight(Uint32 height) {
|
|
||||||
if (m_defaultFramebufferHeight == height) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_defaultFramebufferHeight = height;
|
|
||||||
// Both memos key on (program, flags) alone, so neither can tell the two heights apart:
|
|
||||||
// drop the lookup memo, and bump the structure epoch so every caller holding a
|
|
||||||
// VkProgramObject* re-runs GetOrCreateProgram and lands on the new hash. The cached
|
|
||||||
// entries themselves stay - they are keyed by a hash that now includes the old height,
|
|
||||||
// so they can only be reached again if that height comes back, and the frame-boundary
|
|
||||||
// sweep retires them otherwise.
|
|
||||||
m_lastLookup = {};
|
|
||||||
++m_cacheStructureEpoch;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProgramFactory::VkProgramObject& ProgramFactory::GetOrCreateProgram(
|
const ProgramFactory::VkProgramObject& ProgramFactory::GetOrCreateProgram(
|
||||||
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) {
|
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) {
|
||||||
// Hashing the full SPIR-V of every stage is far too expensive to repeat per draw;
|
// Hashing the full SPIR-V of every stage is far too expensive to repeat per draw;
|
||||||
// reuse the program's memoized hash while its backend state version is unchanged.
|
// reuse the program's memoized hash while its backend state version is unchanged.
|
||||||
// The memo keys on the flags word, which ComputeHash is no longer a pure function of:
|
|
||||||
// a FragCoordYFlip variant also depends on the baked default-framebuffer height, so
|
|
||||||
// that height rides in the free high half of the key. Flags occupy the low bits, and a
|
|
||||||
// height cannot exceed the 16 bits a swapchain extent fits in.
|
|
||||||
const Uint memoKey = (flags & CompileOptionBit::FragCoordYFlip)
|
|
||||||
? (flags.GetRaw() | (m_defaultFramebufferHeight << 16))
|
|
||||||
: flags.GetRaw();
|
|
||||||
HashType hash = 0;
|
HashType hash = 0;
|
||||||
if (!program.GetBackendHashMemo(memoKey, hash)) {
|
if (!program.GetBackendHashMemo(flags.GetRaw(), hash)) {
|
||||||
hash = ComputeHash(program, flags);
|
hash = ComputeHash(program, flags);
|
||||||
program.SetBackendHashMemo(memoKey, hash);
|
program.SetBackendHashMemo(flags.GetRaw(), hash);
|
||||||
}
|
}
|
||||||
auto it = m_cache.find(hash);
|
auto it = m_cache.find(hash);
|
||||||
if (it != m_cache.end()) {
|
if (it != m_cache.end()) {
|
||||||
@@ -2806,14 +2420,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((flags & ProgramFactory::CompileOptionBit::FragCoordYFlip) && shaders[i] &&
|
|
||||||
shaders[i]->GetShaderStage() == ShaderStage::Fragment) {
|
|
||||||
Vector<Uint> fragCoordSpirv;
|
|
||||||
if (TransformSpirvForFragCoordYFlip(moduleSpirvs[i], fragCoordSpirv, m_defaultFramebufferHeight)) {
|
|
||||||
moduleSpirvs[i] = Move(fragCoordSpirv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vulkan's SPIR-V environment has no rectangle image dimension, so a
|
// Vulkan's SPIR-V environment has no rectangle image dimension, so a
|
||||||
// GL_TEXTURE_RECTANGLE lookup has to become the 2D one the texture is really
|
// GL_TEXTURE_RECTANGLE lookup has to become the 2D one the texture is really
|
||||||
// stored as - which addresses [0,1] where the application addressed texels.
|
// stored as - which addresses [0,1] where the application addressed texels.
|
||||||
@@ -2919,12 +2525,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||||
ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex());
|
ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex());
|
||||||
#else
|
|
||||||
// Final module the driver receives; also checked in the INFO-level CI/test
|
|
||||||
// lanes, where the DEBUG gate above is compiled out.
|
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvValidationEnabled()) {
|
|
||||||
ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex());
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||||
@@ -2942,9 +2542,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
entry.modules.push_back(module);
|
entry.modules.push_back(module);
|
||||||
entry.stages.push_back(stage);
|
entry.stages.push_back(stage);
|
||||||
entry.stageSpirvDigests.push_back(ShaderStageSpirvDigest{
|
|
||||||
static_cast<Uint32>(stage.stage), static_cast<Uint32>(moduleSpv.size()),
|
|
||||||
XXH64(moduleSpv.data(), moduleSpv.size() * sizeof(Uint), 0)});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reflect and create layout as part of the program object
|
// Reflect and create layout as part of the program object
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "../VkIncludes.h"
|
#include "../VkIncludes.h"
|
||||||
#include "PipelineFactory.h"
|
|
||||||
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
||||||
#include "MG_State/GLState/ProgramState/ShaderObject.h"
|
#include "MG_State/GLState/ProgramState/ShaderObject.h"
|
||||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||||
@@ -53,13 +52,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// recorded while GL transform feedback is active, so plain draws keep the
|
// recorded while GL transform feedback is active, so plain draws keep the
|
||||||
// undecorated variant.
|
// undecorated variant.
|
||||||
XfbCapture = 1 << 6,
|
XfbCapture = 1 << 6,
|
||||||
// Rewrites the fragment stage's gl_FragCoord reads to GL's bottom-left window
|
|
||||||
// origin. Vulkan's gl_FragCoord.y IS the framebuffer row being written, and the
|
|
||||||
// default framebuffer's image is stored in display (top-left) order, so a shader
|
|
||||||
// that reads gl_FragCoord there sees `height - y_GL`. Set together with
|
|
||||||
// PositionYFlip (the two are the same fact about the same draws) except under a
|
|
||||||
// quarter turn, which this renderer does not convert rectangles for either.
|
|
||||||
FragCoordYFlip = 1 << 7,
|
|
||||||
};
|
};
|
||||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||||
using HashType = Uint64;
|
using HashType = Uint64;
|
||||||
@@ -70,9 +62,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
HashType hash = 0;
|
HashType hash = 0;
|
||||||
Vector<VkPipelineShaderStageCreateInfo> stages;
|
Vector<VkPipelineShaderStageCreateInfo> stages;
|
||||||
Vector<VkShaderModule> modules;
|
Vector<VkShaderModule> modules;
|
||||||
// Parallel to stages; identifies the exact module bytes handed to the driver when a
|
|
||||||
// pipeline creation fails. Sixteen bytes per stage instead of keeping the SPIR-V.
|
|
||||||
Vector<ShaderStageSpirvDigest> stageSpirvDigests;
|
|
||||||
|
|
||||||
// Layout data (previously in separate VkProgramLayout)
|
// Layout data (previously in separate VkProgramLayout)
|
||||||
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
|
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
@@ -274,17 +263,6 @@ 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);
|
||||||
|
|
||||||
// The default framebuffer's current image height, baked as a literal into every
|
|
||||||
// FragCoordYFlip variant (there is no push-constant or specialization channel here, and
|
|
||||||
// adding one for a value that changes only on swapchain recreation would cost the draw
|
|
||||||
// path more than a recompile costs a resize). It is therefore part of those variants'
|
|
||||||
// identity: ComputeHash mixes it in when the bit is set, so a height change re-keys them
|
|
||||||
// and leaves every other program's hash untouched. Setting a NEW height also bumps the
|
|
||||||
// cache-structure epoch, because a caller holding a memoised VkProgramObject* would
|
|
||||||
// otherwise keep using a module compiled against the old height.
|
|
||||||
void SetDefaultFramebufferHeight(Uint32 height);
|
|
||||||
Uint32 GetDefaultFramebufferHeight() const { return m_defaultFramebufferHeight; }
|
|
||||||
|
|
||||||
// Bumped whenever m_cache's STRUCTURE changes (any insert or erase): the cache is
|
// 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.
|
// 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
|
// A caller that memoised a VkProgramObject* may keep dereferencing it only while
|
||||||
@@ -342,9 +320,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// True only when the logical device enabled both
|
// True only when the logical device enabled both
|
||||||
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
||||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||||
// See SetDefaultFramebufferHeight. 0 means "not known yet"; the FragCoordYFlip bit is
|
|
||||||
// never set before the swapchain exists, so no variant can be compiled against it.
|
|
||||||
Uint32 m_defaultFramebufferHeight = 0;
|
|
||||||
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;
|
||||||
|
|||||||
@@ -677,7 +677,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
Bool UniformManager::ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
Bool UniformManager::ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
Uint32 binding, Uint32 element,
|
Uint32 binding,
|
||||||
VkDescriptorBufferInfo& outBufferInfo) const {
|
VkDescriptorBufferInfo& outBufferInfo) const {
|
||||||
outBufferInfo = {};
|
outBufferInfo = {};
|
||||||
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageBufferDescriptor: buffer manager is null");
|
MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageBufferDescriptor: buffer manager is null");
|
||||||
@@ -688,11 +688,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Int blockIndex = programObj.storageBlockIndexByBinding[binding];
|
const Int blockIndex = programObj.storageBlockIndexByBinding[binding];
|
||||||
MOBILEGL_ASSERT(blockIndex >= 0, "ResolveStorageBufferDescriptor: no SSBO block mapped to binding %u",
|
MOBILEGL_ASSERT(blockIndex >= 0, "ResolveStorageBufferDescriptor: no SSBO block mapped to binding %u",
|
||||||
binding);
|
binding);
|
||||||
// A block instance array declares one block whose elements take consecutive GL binding
|
|
||||||
// points from the declared one (GL 4.6 core 7.8), and the reflection collapses the whole
|
|
||||||
// array to that one block - so the element index IS the offset from its binding.
|
|
||||||
const GLuint frontendBinding =
|
const GLuint frontendBinding =
|
||||||
GetShaderStorageBlockBinding(program, static_cast<GLuint>(blockIndex)) + element;
|
GetShaderStorageBlockBinding(program, static_cast<GLuint>(blockIndex));
|
||||||
const Uint32 bindingPointCount =
|
const Uint32 bindingPointCount =
|
||||||
static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::ShaderStorage));
|
static_cast<Uint32>(MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::ShaderStorage));
|
||||||
MOBILEGL_ASSERT(frontendBinding < bindingPointCount,
|
MOBILEGL_ASSERT(frontendBinding < bindingPointCount,
|
||||||
@@ -1419,17 +1416,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
dynamicOffsets.clear();
|
dynamicOffsets.clear();
|
||||||
// Arrayed UBO bindings contribute extra buffer infos and dynamic offsets; reserve for
|
// Arrayed UBO bindings contribute extra buffer infos and dynamic offsets; reserve for
|
||||||
// the worst case so the pBufferInfo pointers taken below never dangle on reallocation.
|
// the worst case so the pBufferInfo pointers taken below never dangle on reallocation.
|
||||||
// Arrayed SSBO bindings contribute extra buffer infos too (but no dynamic offsets).
|
|
||||||
Uint32 uboArrayExtra = 0;
|
Uint32 uboArrayExtra = 0;
|
||||||
for (const auto& arrayEntry : programObj.arrayedUniformBlockIndicesByBinding) {
|
for (const auto& arrayEntry : programObj.arrayedUniformBlockIndicesByBinding) {
|
||||||
uboArrayExtra += static_cast<Uint32>(arrayEntry.second.size()) - 1u;
|
uboArrayExtra += static_cast<Uint32>(arrayEntry.second.size()) - 1u;
|
||||||
}
|
}
|
||||||
Uint32 ssboArrayExtra = 0;
|
|
||||||
for (const Uint16 count : programObj.bindingDescriptorCounts) {
|
|
||||||
if (count > 1) ssboArrayExtra += static_cast<Uint32>(count) - 1u;
|
|
||||||
}
|
|
||||||
writes.reserve(m_maxBindings);
|
writes.reserve(m_maxBindings);
|
||||||
bufferInfos.reserve(m_maxBindings + uboArrayExtra + ssboArrayExtra);
|
bufferInfos.reserve(m_maxBindings + uboArrayExtra);
|
||||||
imageInfos.reserve(m_maxBindings);
|
imageInfos.reserve(m_maxBindings);
|
||||||
texelBufferViews.reserve(m_maxBindings);
|
texelBufferViews.reserve(m_maxBindings);
|
||||||
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
|
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
|
||||||
@@ -1501,30 +1493,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
write.pTexelBufferView = &texelBufferViews.back();
|
write.pTexelBufferView = &texelBufferViews.back();
|
||||||
writes.push_back(write);
|
writes.push_back(write);
|
||||||
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageBuffer) {
|
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageBuffer) {
|
||||||
// One write per binding, but `descriptorCount` buffer infos: a GLSL block
|
VkDescriptorBufferInfo bufferInfo{};
|
||||||
// instance array occupies a single binding whose elements each come from their
|
if (!ResolveStorageBufferDescriptor(program, programObj, binding, bufferInfo)) {
|
||||||
// own GL binding point.
|
MGLOG_E(
|
||||||
const Uint32 descriptorCount =
|
"UniformDescriptorBinder::BindProgramUniformBuffers failed: storage buffer binding %u has no valid descriptor",
|
||||||
binding < programObj.bindingDescriptorCounts.size()
|
binding);
|
||||||
? std::max<Uint32>(1, programObj.bindingDescriptorCounts[binding])
|
return false;
|
||||||
: 1u;
|
|
||||||
const SizeT firstBufferInfoIndex = bufferInfos.size();
|
|
||||||
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
|
||||||
VkDescriptorBufferInfo bufferInfo{};
|
|
||||||
if (!ResolveStorageBufferDescriptor(program, programObj, binding, element, bufferInfo)) {
|
|
||||||
MGLOG_E(
|
|
||||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: storage buffer binding %u "
|
|
||||||
"element %u has no valid descriptor",
|
|
||||||
binding, element);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
bufferInfos.push_back(bufferInfo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bufferInfos.push_back(bufferInfo);
|
||||||
fastRebindKindsEligible = false;
|
fastRebindKindsEligible = false;
|
||||||
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||||
write.descriptorCount = descriptorCount;
|
write.pBufferInfo = &bufferInfos.back();
|
||||||
write.pBufferInfo = &bufferInfos[firstBufferInfoIndex];
|
|
||||||
writes.push_back(write);
|
writes.push_back(write);
|
||||||
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageImage) {
|
} else if (kind == ProgramFactory::DescriptorBindingKind::StorageImage) {
|
||||||
VkDescriptorImageInfo imageInfo{};
|
VkDescriptorImageInfo imageInfo{};
|
||||||
|
|||||||
@@ -166,12 +166,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
Uint32 frameIndex, VkBufferView& outBufferView);
|
Uint32 frameIndex, VkBufferView& outBufferView);
|
||||||
// `element` indexes a block INSTANCE array's descriptors; it is 0 for every ordinary
|
|
||||||
// block. Each element resolves through its own GL storage block, and so its own GL
|
|
||||||
// binding point, buffer and glBindBufferRange window.
|
|
||||||
Bool ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
Bool ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
Uint32 element, VkDescriptorBufferInfo& outBufferInfo) const;
|
VkDescriptorBufferInfo& outBufferInfo) const;
|
||||||
Bool ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer,
|
Bool ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer,
|
||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
|
|||||||
@@ -161,23 +161,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VkBufferManager::CollectAllDeferredReleases() {
|
void VkBufferManager::CollectAllDeferredReleases() {
|
||||||
// Per-resource releases only. Every one of them was deferred behind a BumpSliceEpoch,
|
|
||||||
// so no memo can still name the handle, and the caller has proved the GPU is idle.
|
|
||||||
//
|
|
||||||
// The transient arena's releases are deliberately NOT collected here. A buffer lands
|
|
||||||
// there when the arena outgrows it mid-frame (BufferArena::EnsureCapacity), and at
|
|
||||||
// that moment every slice already handed out from this frame's arena still names it -
|
|
||||||
// VkBufferResource::transientSlice above all, which AcquireStreamedSlice keeps
|
|
||||||
// serving for the whole frame serial on the strength of transientFrameSerial alone.
|
|
||||||
// Nothing bumps the slice epoch for those other resources, so freeing the buffer
|
|
||||||
// here left the streamed memo handing a destroyed VkBuffer to vkCmdBindIndexBuffer
|
|
||||||
// (llvmpipe then faulted inside the draw; the Create/Flywheel indirect retrace died
|
|
||||||
// exactly this way). Mid-frame drains do not advance m_frameSerial, so they must not
|
|
||||||
// free arena storage either: the arena's own ResetFrame/BeginFrame is the point where
|
|
||||||
// the slot's slices stop being reachable, and that is where these releases land.
|
|
||||||
for (Uint32 frameIndex = 0; frameIndex < m_deferredBufferReleases.size(); ++frameIndex) {
|
for (Uint32 frameIndex = 0; frameIndex < m_deferredBufferReleases.size(); ++frameIndex) {
|
||||||
CollectDeferredReleases(frameIndex);
|
CollectDeferredReleases(frameIndex);
|
||||||
}
|
}
|
||||||
|
for (Uint32 frameIndex = 0; frameIndex < m_transientUploadArena.GetFrameCount(); ++frameIndex) {
|
||||||
|
m_transientUploadArena.CollectDeferredReleases(frameIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VkBufferManager::NotifyDeviceIdle() {
|
void VkBufferManager::NotifyDeviceIdle() {
|
||||||
|
|||||||
@@ -102,11 +102,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Recreate all per-frame transient arenas
|
// Recreate all per-frame transient arenas
|
||||||
Bool RecreateTransientArenas(Uint32 frameCount);
|
Bool RecreateTransientArenas(Uint32 frameCount);
|
||||||
void BeginFrame(Uint32 frameIndex);
|
void BeginFrame(Uint32 frameIndex);
|
||||||
// Drains every frame slot's deferred buffer/resource releases. Only valid when
|
// Drains every frame slot's deferred buffer/resource releases (and the
|
||||||
// the caller has proven every queue submission complete; used by the present-less
|
// transient arena's parked superseded blocks). Only valid when the
|
||||||
// frame-boundary drain. Deliberately does NOT touch the transient arena's parked
|
// caller has proven every queue submission complete; used by the
|
||||||
// superseded blocks: those are still named by this frame's slices (see the
|
// present-less frame-boundary drain.
|
||||||
// definition), and only a frame rewind retires them.
|
|
||||||
void CollectAllDeferredReleases();
|
void CollectAllDeferredReleases();
|
||||||
// All previously submitted GPU work has completed (vkDeviceWaitIdle).
|
// All previously submitted GPU work has completed (vkDeviceWaitIdle).
|
||||||
void NotifyDeviceIdle();
|
void NotifyDeviceIdle();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -211,15 +211,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
GLsizei height, GLenum format, GLenum type, void* pixels);
|
GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||||
// Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
|
// Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
|
||||||
// expects command recording to be active and any render pass already ended.
|
// expects command recording to be active and any render pass already ended.
|
||||||
//
|
|
||||||
// `defaultFramebufferOrientation` is set only when the source is the swapchain's
|
|
||||||
// depth/stencil image, which this renderer stores display-side-up: the copy rect then
|
|
||||||
// has to be mapped out of GL's bottom-origin space and the copied rows re-oriented on
|
|
||||||
// the way back, exactly as the colour ReadPixels path does.
|
|
||||||
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
||||||
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
|
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
|
||||||
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||||
void* pixels, Bool defaultFramebufferOrientation = false);
|
void* pixels);
|
||||||
// Same-extent depth blit between images of different depth formats: host
|
// Same-extent depth blit between images of different depth formats: host
|
||||||
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
|
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
|
||||||
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
|
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
|
||||||
@@ -368,31 +363,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 samplerBinding = 0;
|
Uint32 samplerBinding = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// A single-sample staging image for multisample-resolve blits that also have to change
|
|
||||||
// orientation. vkCmdResolveImage cannot flip (it takes one offset per side, not the
|
|
||||||
// invertible pair vkCmdBlitImage takes), so a resolve into or out of the default
|
|
||||||
// framebuffer used to land the mirrored band. Resolving here first and then blitting from
|
|
||||||
// here separates the two operations, and each one then does only what it can express.
|
|
||||||
//
|
|
||||||
// Pooled rather than created per blit: the CTS runs hundreds of these back to back, and
|
|
||||||
// create-destroy per call would both cost allocations and, worse, need per-call deferred
|
|
||||||
// destruction to outlive the recording. It grows to the largest extent asked for and is
|
|
||||||
// reused; format changes recreate it.
|
|
||||||
struct MultisampleResolveScratchImage {
|
|
||||||
VkImage image = VK_NULL_HANDLE;
|
|
||||||
VmaAllocation allocation = VK_NULL_HANDLE;
|
|
||||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
|
||||||
VkExtent2D extent = {0, 0};
|
|
||||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
||||||
};
|
|
||||||
MultisampleResolveScratchImage m_msResolveScratch;
|
|
||||||
// Returns a scratch image at least `extent` in size with exactly `format`, transitioned to
|
|
||||||
// TRANSFER_DST and ready to be resolved into. Null image on failure (the caller then falls
|
|
||||||
// back to the direct resolve).
|
|
||||||
Bool AcquireMultisampleResolveScratchImage(VkCommandBuffer commandBuffer, VkFormat format,
|
|
||||||
VkExtent2D extent);
|
|
||||||
void DestroyMultisampleResolveScratchImage();
|
|
||||||
|
|
||||||
struct DeferredDepthMipmapCleanup {
|
struct DeferredDepthMipmapCleanup {
|
||||||
Vector<VkImageView> imageViews;
|
Vector<VkImageView> imageViews;
|
||||||
Vector<VkFramebuffer> framebuffers;
|
Vector<VkFramebuffer> framebuffers;
|
||||||
@@ -475,13 +445,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void* m_platformDisplay = nullptr;
|
void* m_platformDisplay = nullptr;
|
||||||
void* m_platformLibrary = nullptr;
|
void* m_platformLibrary = nullptr;
|
||||||
void* m_platformCloseDisplay = nullptr;
|
void* m_platformCloseDisplay = nullptr;
|
||||||
// Whether the loader exposes VK_EXT_headless_surface, detected once in
|
// Some real ICDs (e.g. NVIDIA's proprietary Linux driver) don't implement
|
||||||
// CreateInstance() from the enumerated instance extensions. On desktop an
|
// VK_EXT_headless_surface at all. Detected once in CreateInstance() from the
|
||||||
// offscreen surface REQUIRES it: false is a clean, loud bring-up failure, never
|
// enumerated instance extensions; when false, CreateSurface() falls back to a
|
||||||
// a substituted window. (Android is the one exception and has its own path -
|
// hidden Xlib window instead of vkCreateHeadlessSurfaceEXT.
|
||||||
// no Mali/Adreno driver seen so far exposes the extension, so a windowless
|
|
||||||
// context is given an AImageReader ANativeWindow that is never displayed.)
|
|
||||||
Bool m_headlessSurfaceSupported = true;
|
Bool m_headlessSurfaceSupported = true;
|
||||||
|
// Set when CreateSurface() had to create its own Xlib window for the fallback
|
||||||
|
// above (rather than being handed one by the caller), so Shutdown() knows it
|
||||||
|
// owns that window and must destroy it.
|
||||||
|
Bool m_ownsFallbackXlibWindow = false;
|
||||||
// Android has the same shortfall: no Mali/Adreno driver seen so far exposes
|
// Android has the same shortfall: no Mali/Adreno driver seen so far exposes
|
||||||
// VK_EXT_headless_surface, so a windowless (EGL pbuffer) context gets an
|
// VK_EXT_headless_surface, so a windowless (EGL pbuffer) context gets an
|
||||||
// AImageReader's ANativeWindow to hand the WSI instead. Nothing is ever
|
// AImageReader's ANativeWindow to hand the WSI instead. Nothing is ever
|
||||||
@@ -1148,18 +1120,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool MaterializePendingClearForRenderbuffer(
|
Bool MaterializePendingClearForRenderbuffer(
|
||||||
VkCommandBuffer commandBuffer,
|
VkCommandBuffer commandBuffer,
|
||||||
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
|
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
|
||||||
// The default framebuffer's twin of the two above. It cannot go through
|
|
||||||
// MaterializePendingClearForTexture: the default FBO's colour attachment is a
|
|
||||||
// placeholder texture object, and syncing THAT would clear a texture image nobody
|
|
||||||
// presents instead of the acquired swapchain image.
|
|
||||||
Bool MaterializePendingClearForDefaultFramebuffer(VkCommandBuffer commandBuffer,
|
|
||||||
MG_State::GLState::FramebufferObject& fbo,
|
|
||||||
FramebufferAttachmentType attachmentType);
|
|
||||||
// Its depth/stencil half: a different image (the swapchain's depth/stencil twin), a
|
|
||||||
// different clear command and per-aspect masking.
|
|
||||||
Bool MaterializePendingDepthStencilClearForDefaultFramebuffer(
|
|
||||||
VkCommandBuffer commandBuffer, const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
|
||||||
const ClearAttachmentPayload& payload);
|
|
||||||
VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
|
VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
|
||||||
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
|
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
|
||||||
MG_State::GLState::ITextureObject& texture,
|
MG_State::GLState::ITextureObject& texture,
|
||||||
|
|||||||
@@ -1491,8 +1491,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// offset and size, which is also how glBindBuffersRange spells "reset this element"
|
// offset and size, which is also how glBindBuffersRange spells "reset this element"
|
||||||
// (a NULL buffers array, or a zero entry inside one).
|
// (a NULL buffers array, or a zero entry inside one).
|
||||||
static Bool ValidateBufferRangeOffsetAndSize(GLenum target, GLintptr offset, GLsizeiptr size,
|
static Bool ValidateBufferRangeOffsetAndSize(GLenum target, GLintptr offset, GLsizeiptr size,
|
||||||
const char* funcName, Bool hasBuffer = true) {
|
const char* funcName) {
|
||||||
if (hasBuffer && size <= 0) {
|
if (size <= 0) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||||
@@ -1527,27 +1527,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// GL 4.6 core 6.1.1 constrains the OFFSET to a multiple of four for both
|
// A transform feedback capture binding is addressed in 32-bit components, so BOTH the
|
||||||
// TRANSFORM_FEEDBACK_BUFFER and ATOMIC_COUNTER_BUFFER (the atomic-counter one has no
|
// offset and the size must be multiples of 4.
|
||||||
// queryable alignment pname, which is why it was missing here), and the SIZE only for
|
if (target == GL_TRANSFORM_FEEDBACK_BUFFER && ((offset % 4) != 0 || (size % 4) != 0)) {
|
||||||
// transform feedback, whose capture is written in whole 32-bit components. Extending the
|
|
||||||
// size rule to atomic counters as well breaks a legal bind: the conformance suite splits
|
|
||||||
// MAX_ATOMIC_COUNTER_BUFFER_SIZE evenly across the binding points and that quotient is
|
|
||||||
// not required to land on four.
|
|
||||||
if ((target == GL_TRANSFORM_FEEDBACK_BUFFER || target == GL_ATOMIC_COUNTER_BUFFER) && (offset % 4) != 0) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidValue,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
|
||||||
std::format("offset ({}) must be a multiple of 4 for {}.", offset,
|
|
||||||
MG_Util::ConvertGLEnumToString(target))));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (target == GL_TRANSFORM_FEEDBACK_BUFFER && hasBuffer && (size % 4) != 0) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
"MG_Impl/GLImpl", funcName,
|
"MG_Impl/GLImpl", funcName,
|
||||||
std::format("size ({}) must be a multiple of 4 for GL_TRANSFORM_FEEDBACK_BUFFER.", size)));
|
std::format("offset ({}) and size ({}) must both be multiples of 4 for "
|
||||||
|
"GL_TRANSFORM_FEEDBACK_BUFFER.",
|
||||||
|
offset, size)));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -1559,12 +1548,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
||||||
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
||||||
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return;
|
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return;
|
||||||
// The target's alignment rules are a property of the BINDING POINT, not of the buffer,
|
if (buffer != 0 && !ValidateBufferRangeOffsetAndSize(target, offset, size, __func__)) return;
|
||||||
// so they apply even when buffer is zero - which is exactly how
|
|
||||||
// KHR-GL43.shader_storage_buffer_object.negative-api-bind probes the SSBO alignment
|
|
||||||
// (glBindBufferRange(SHADER_STORAGE_BUFFER, 0, 0, alignment - 1, 0)). Only the size
|
|
||||||
// rules need a buffer, since buffer 0 detaches the binding point and ignores size.
|
|
||||||
if (!ValidateBufferRangeOffsetAndSize(target, offset, size, __func__, /*hasBuffer: */ buffer != 0)) 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,
|
||||||
@@ -1748,30 +1732,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return BufferImpl::ValidateBufferBindingPointRange(bufferTarget, first, count, funcName);
|
return BufferImpl::ValidateBufferBindingPointRange(bufferTarget, first, count, funcName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ARB_multi_bind states the equivalence to a loop of single binds "except that ... buffers
|
|
||||||
// will not be created if they do not exist": glBindBuffer instantiates a name glGenBuffers
|
|
||||||
// merely reserved, glBindBuffers* must refuse it and raise INVALID_OPERATION instead
|
|
||||||
// (KHR-GL44.multi_bind.errors_bind_buffers).
|
|
||||||
//
|
|
||||||
// Deliberately PER ELEMENT, not all-or-nothing: the equivalence the extension defines is a
|
|
||||||
// loop, so a bad entry costs its own binding point and nothing else. Rejecting the whole
|
|
||||||
// call instead cost multi_bind.functional_bind_buffers_base its bindings.
|
|
||||||
static Bool IsExistingBufferForMultiBind(GLuint buffer, GLsizei index, const char* funcName) {
|
|
||||||
if (buffer == 0 || MG_State::pGLContext->ValidateBufferObject(buffer)) return true;
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>(
|
|
||||||
"MG_Impl/GLImpl", funcName,
|
|
||||||
std::format("buffers[{}] ({}) is not the name of an existing buffer object.", index, buffer)));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
|
||||||
for (GLsizei i = 0; i < count; ++i) {
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
const GLuint buffer = buffers ? buffers[i] : 0;
|
BindBufferBase_State(target, first + i, buffers ? buffers[i] : 0);
|
||||||
if (!IsExistingBufferForMultiBind(buffer, i, __func__)) continue;
|
|
||||||
BindBufferBase_State(target, first + i, buffer);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1785,7 +1749,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const GLsizeiptr* sizes) {
|
const GLsizeiptr* sizes) {
|
||||||
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
|
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
|
||||||
for (GLsizei i = 0; i < count; ++i) {
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
if (buffers && !IsExistingBufferForMultiBind(buffers[i], i, __func__)) continue;
|
|
||||||
if (!buffers || buffers[i] == 0) {
|
if (!buffers || buffers[i] == 0) {
|
||||||
BindBufferBase_State(target, first + i, 0);
|
BindBufferBase_State(target, first + i, 0);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -493,12 +493,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DispatchComputeIndirect(GLintptr indirect) {
|
void DispatchComputeIndirect(GLintptr indirect) {
|
||||||
// Argument and binding validation runs FIRST. Both are properties of the call and of GL
|
auto dispatchComputeIndirect = MG_Backend::gBackendFunctionsTable.GL.DispatchComputeIndirect;
|
||||||
// state, so a context whose backend cannot dispatch at all must still report the
|
if (!dispatchComputeIndirect) {
|
||||||
// argument error the spec names rather than masking every one of them with
|
MG_State::pGLContext->RecordError(
|
||||||
// "unsupported" - which is what put GL_INVALID_OPERATION where
|
ErrorCode::InvalidOperation,
|
||||||
// KHR-GL43.compute_shader.api-indirect expects GL_INVALID_VALUE.
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
//
|
"Backend does not support indirect compute dispatch."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||||
// GL 4.6 core 19: `indirect` is a byte offset into GL_DISPATCH_INDIRECT_BUFFER -
|
// 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.
|
// negative or misaligned is INVALID_VALUE, nothing bound is INVALID_OPERATION.
|
||||||
if (indirect < 0 || (indirect % 4) != 0) {
|
if (indirect < 0 || (indirect % 4) != 0) {
|
||||||
@@ -517,29 +520,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
"No buffer is bound to GL_DISPATCH_INDIRECT_BUFFER."));
|
"No buffer is bound to GL_DISPATCH_INDIRECT_BUFFER."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// ...and the same INVALID_OPERATION covers "the command would source data beyond the end
|
|
||||||
// of the bound buffer object" (GL 4.6 core 19): the dispatch reads three uints starting
|
|
||||||
// at `indirect`.
|
|
||||||
constexpr SizeT kDispatchIndirectCommandSize = 3 * sizeof(Uint32);
|
|
||||||
if (static_cast<SizeT>(indirect) + kDispatchIndirectCommandSize > indirectBuffer->GetSize()) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>(
|
|
||||||
"MG_Impl/GLImpl", __func__,
|
|
||||||
std::format("indirect ({}) + 12 bytes runs past the end of the {}-byte buffer bound to "
|
|
||||||
"GL_DISPATCH_INDIRECT_BUFFER.",
|
|
||||||
indirect, indirectBuffer->GetSize())));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto dispatchComputeIndirect = MG_Backend::gBackendFunctionsTable.GL.DispatchComputeIndirect;
|
|
||||||
if (!dispatchComputeIndirect) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
|
||||||
"Backend does not support indirect compute dispatch."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
|
||||||
dispatchComputeIndirect(indirect);
|
dispatchComputeIndirect(indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,69 +580,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
|
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ARB_indirect_parameters / GL 4.6 core 10.4: `drawcount` is a byte offset into the buffer
|
|
||||||
// bound to PARAMETER_BUFFER and holds one uint draw count. Three errors have to be raised
|
|
||||||
// before the call reaches a backend, and none of them was
|
|
||||||
// (KHR-GL46.indirect_parameters_tests.MultiDraw{Arrays,Elements}IndirectCount):
|
|
||||||
// * drawcount not a multiple of four INVALID_VALUE
|
|
||||||
// * nothing bound to PARAMETER_BUFFER, or the uint at `drawcount`
|
|
||||||
// lies past its end INVALID_OPERATION
|
|
||||||
// * maxdrawcount commands from `indirect` run past the end of the
|
|
||||||
// buffer bound to DRAW_INDIRECT_BUFFER INVALID_OPERATION
|
|
||||||
static Bool ValidateIndirectCountDraw(GLintptr indirect, GLintptr drawcount, GLsizei maxdrawcount,
|
|
||||||
GLsizei stride, SizeT commandSize, const char* funcName) {
|
|
||||||
if (drawcount < 0 || (drawcount % 4) != 0) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidValue,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
|
||||||
"drawcount must be non-negative and a multiple of four."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const auto& parameterBuffer =
|
|
||||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
|
||||||
if (!parameterBuffer ||
|
|
||||||
static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
|
||||||
"No buffer is bound to GL_PARAMETER_BUFFER, or drawcount runs past "
|
|
||||||
"the end of the one that is."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (maxdrawcount < 0 || stride < 0 || indirect < 0) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidValue,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
|
||||||
"indirect, maxdrawcount and stride must all be non-negative."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const SizeT effectiveStride = stride != 0 ? static_cast<SizeT>(stride) : commandSize;
|
|
||||||
const auto& indirectBuffer =
|
|
||||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
|
||||||
// A zero maxdrawcount sources nothing, so it cannot run past anything.
|
|
||||||
const SizeT requiredBytes =
|
|
||||||
maxdrawcount == 0 ? 0
|
|
||||||
: static_cast<SizeT>(indirect) +
|
|
||||||
static_cast<SizeT>(maxdrawcount - 1) * effectiveStride + commandSize;
|
|
||||||
if (!indirectBuffer || requiredBytes > indirectBuffer->GetSize()) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
|
||||||
"maxdrawcount commands would be sourced from beyond the end of the "
|
|
||||||
"buffer bound to GL_DRAW_INDIRECT_BUFFER."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
||||||
GLsizei maxdrawcount, GLsizei stride) {
|
GLsizei maxdrawcount, GLsizei stride) {
|
||||||
// Argument validation before the backend-availability check: see DispatchComputeIndirect.
|
|
||||||
// DrawElementsIndirectCommand: count, instanceCount, firstIndex, baseVertex, baseInstance.
|
|
||||||
if (!ValidateIndirectCountDraw(reinterpret_cast<GLintptr>(indirect), drawcount, maxdrawcount, stride,
|
|
||||||
5 * sizeof(Uint32), __func__)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto multiDrawElementsIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount;
|
auto multiDrawElementsIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount;
|
||||||
if (!multiDrawElementsIndirectCount) {
|
if (!multiDrawElementsIndirectCount) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -676,12 +595,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
|
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
|
||||||
GLsizei maxdrawcount, GLsizei stride) {
|
GLsizei maxdrawcount, GLsizei stride) {
|
||||||
// Argument validation before the backend-availability check: see DispatchComputeIndirect.
|
|
||||||
// DrawArraysIndirectCommand: count, instanceCount, first, baseInstance.
|
|
||||||
if (!ValidateIndirectCountDraw(reinterpret_cast<GLintptr>(indirect), drawcount, maxdrawcount, stride,
|
|
||||||
4 * sizeof(Uint32), __func__)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto multiDrawArraysIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount;
|
auto multiDrawArraysIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount;
|
||||||
if (!multiDrawArraysIndirectCount) {
|
if (!multiDrawArraysIndirectCount) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
|
|||||||
@@ -961,30 +961,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The one indexed pname whose value genuinely needs 64 bits: a vertex buffer binding
|
auto getInteger64i = MG_Backend::gBackendFunctionsTable.GL.GetInteger64i_v;
|
||||||
// offset is an intptr, so taking the 32-bit route below would truncate it.
|
if (!getInteger64i) {
|
||||||
if (target == GL_VERTEX_BINDING_OFFSET) {
|
*data = 0;
|
||||||
if (index >= VertexArrayImpl::GetMaxVertexAttribBindings()) {
|
MG_State::pGLContext->RecordError(
|
||||||
MG_State::pGLContext->RecordError(
|
ErrorCode::InvalidOperation,
|
||||||
ErrorCode::InvalidValue,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support indexed integer queries."));
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
|
||||||
"Vertex buffer binding index is out of range."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
|
||||||
*data = vao ? static_cast<GLint64>(vao->GetBindingPoint(index).Offset) : 0;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
getInteger64i(target, index, data);
|
||||||
// Everything else is 32-bit indexed state that the glGetIntegeri_v pname table already
|
|
||||||
// owns, and GL 4.6 core 22.1 says every indexed query answers every indexed pname.
|
|
||||||
// Handing the leftovers straight to the backend instead made glGetInteger64i_v disagree
|
|
||||||
// with glGetIntegeri_v on the very same pname - GL_MAX_COMPUTE_WORK_GROUP_COUNT read
|
|
||||||
// back 0 while the 32-bit view said 65535 (KHR-GL43.compute_shader.max), because a
|
|
||||||
// frontend-only value simply is not in the driver's table.
|
|
||||||
GLint values[4] = {};
|
|
||||||
GetIntegeri_v(target, index, values);
|
|
||||||
*data = static_cast<GLint64>(values[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetInteger64v(GLenum pname, GLint64* params) {
|
void GetInteger64v(GLenum pname, GLint64* params) {
|
||||||
|
|||||||
@@ -744,21 +744,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
CopyStr(bufSize, length, infoLog, log.c_str(), (GLsizei)log.length());
|
CopyStr(bufSize, length, infoLog, log.c_str(), (GLsizei)log.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS: while the compile job is still in flight -
|
|
||||||
// and, via the latch below, for the rest of that node's life once any query was
|
|
||||||
// answered this way - GL_COMPILE_STATUS reads GL_TRUE and the info log reads empty,
|
|
||||||
// WITHOUT joining. The latch (TakeOptimisticCompileAnswer) is what makes the three
|
|
||||||
// sites tell ONE story: without it, a job settling between an application's info-log
|
|
||||||
// read and its status read would produce the torn pair "GL_FALSE with an empty log",
|
|
||||||
// and an application that aborts on that never reaches the link join that carries the
|
|
||||||
// real diagnostic. A failure hidden here still fails the program link, with the
|
|
||||||
// compile log quoted in the program info log (ProgramLinkTask::ConsumeShaders), which
|
|
||||||
// is where the serial compile-then-check applications this exists for do their error
|
|
||||||
// handling.
|
|
||||||
static Bool AnswerCompileOptimistically(const SharedPtr<MG_State::GLState::ShaderObject>& shaderObject) {
|
|
||||||
return MG_Util::Async::OptimisticShaderStatusActive() && shaderObject->TakeOptimisticCompileAnswer();
|
|
||||||
}
|
|
||||||
|
|
||||||
void GetShaderiv_State(GLuint shader, GLenum pname, GLint* params) {
|
void GetShaderiv_State(GLuint shader, GLenum pname, GLint* params) {
|
||||||
auto& shaderObject = TryToGetShaderObject(shader);
|
auto& shaderObject = TryToGetShaderObject(shader);
|
||||||
if (!shaderObject) return;
|
if (!shaderObject) return;
|
||||||
@@ -771,20 +756,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = shaderObject->GetDeleteStatus();
|
*params = shaderObject->GetDeleteStatus();
|
||||||
break;
|
break;
|
||||||
case GL_COMPILE_STATUS:
|
case GL_COMPILE_STATUS:
|
||||||
if (AnswerCompileOptimistically(shaderObject)) {
|
|
||||||
*params = GL_TRUE;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
*params = shaderObject->GetCompileStatus();
|
*params = shaderObject->GetCompileStatus();
|
||||||
break;
|
break;
|
||||||
case GL_INFO_LOG_LENGTH:
|
case GL_INFO_LOG_LENGTH:
|
||||||
// Not cosmetic: LWJGL's one-argument glGetShaderInfoLog convenience overload
|
|
||||||
// sizes its buffer from this query, so a joining answer here would defeat the
|
|
||||||
// non-joining GetShaderInfoLog below.
|
|
||||||
if (AnswerCompileOptimistically(shaderObject)) {
|
|
||||||
*params = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
*params = shaderObject->GetInfoLog().empty() ? 0 : (GLint)shaderObject->GetInfoLog().length() + 1;
|
*params = shaderObject->GetInfoLog().empty() ? 0 : (GLint)shaderObject->GetInfoLog().length() + 1;
|
||||||
break;
|
break;
|
||||||
case GL_SHADER_SOURCE_LENGTH:
|
case GL_SHADER_SOURCE_LENGTH:
|
||||||
@@ -810,15 +784,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
auto& shaderObject = TryToGetShaderObject(shader);
|
auto& shaderObject = TryToGetShaderObject(shader);
|
||||||
if (!shaderObject) return;
|
if (!shaderObject) return;
|
||||||
|
|
||||||
// See AnswerCompileOptimistically: an in-flight compile reads as an empty log. The
|
|
||||||
// cost is a lost compile WARNING (a successful compile whose log the application
|
|
||||||
// reads exactly once, now, and never after the join) - accepted as part of the
|
|
||||||
// opt-in.
|
|
||||||
if (AnswerCompileOptimistically(shaderObject)) {
|
|
||||||
CopyStr(bufSize, length, infoLog, "", 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto& log = shaderObject->GetInfoLog();
|
const auto& log = shaderObject->GetInfoLog();
|
||||||
CopyStr(bufSize, length, infoLog, log.c_str(), (GLsizei)log.length());
|
CopyStr(bufSize, length, infoLog, log.c_str(), (GLsizei)log.length());
|
||||||
}
|
}
|
||||||
@@ -1120,9 +1085,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (!programObject.IsUniformOpaqueAtLocation(location)) {
|
if (!programObject.IsUniformOpaqueAtLocation(location)) {
|
||||||
MGLOG_D("%s: program = %d, location = %d, maxLocation = %d", __func__, programObject.GetExternalIndex(),
|
MGLOG_D("%s: program = %d, location = %d, maxLocation = %d", __func__, programObject.GetExternalIndex(),
|
||||||
location, programObject.GetMaxUniformLocation());
|
location, programObject.GetMaxUniformLocation());
|
||||||
// Everything up to and including the clamp is phase-A data (the uniform's GL type
|
|
||||||
// decides its size), so it is answered without joining anything.
|
|
||||||
const SizeT size = programObject.GetUniformSizesInBytes(location);
|
const SizeT size = programObject.GetUniformSizesInBytes(location);
|
||||||
|
const Uint offset = programObject.GetUniformOffset(location);
|
||||||
|
char* pUBO = static_cast<char*>(programObject.MapUBO());
|
||||||
|
const SizeT uboSize = programObject.GetUBOSize();
|
||||||
SizeT writeSize = ItemCount * sizeof(T);
|
SizeT writeSize = ItemCount * sizeof(T);
|
||||||
if (size < writeSize) {
|
if (size < writeSize) {
|
||||||
// Metadata bug: degrade to a clamped copy instead of killing the process.
|
// Metadata bug: degrade to a clamped copy instead of killing the process.
|
||||||
@@ -1131,18 +1097,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
__func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size);
|
__func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size);
|
||||||
writeSize = size;
|
writeSize = size;
|
||||||
}
|
}
|
||||||
// The uniform shadow's LAYOUT is phase-B data, so a write that lands while the
|
|
||||||
// SPIR-V job is still running is recorded and replayed at its publish instead of
|
|
||||||
// joining it. This is the hot path for a shaderpack that sets its uniforms
|
|
||||||
// immediately after glLinkProgram. BufferUniformWrite declines (and we fall
|
|
||||||
// through, joining) only past its size budget.
|
|
||||||
if (programObject.IsSpirvPending() &&
|
|
||||||
programObject.BufferUniformWrite(location, byteOffsetInsideUniform, value, writeSize)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const Uint offset = programObject.GetUniformOffset(location);
|
|
||||||
char* pUBO = static_cast<char*>(programObject.MapUBO());
|
|
||||||
const SizeT uboSize = programObject.GetUBOSize();
|
|
||||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||||
offset + byteOffsetInsideUniform + writeSize > uboSize) {
|
offset + byteOffsetInsideUniform + writeSize > uboSize) {
|
||||||
// Should not happen: linking gives every settable uniform backing
|
// Should not happen: linking gives every settable uniform backing
|
||||||
@@ -2704,15 +2658,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
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) {
|
||||||
// Every early-out below reports "nothing was written", and it has to say so before it can
|
|
||||||
// take one: callers legitimately leave *length uninitialised and then loop to it. The CTS
|
|
||||||
// does exactly that (gl4cProgramInterfaceQueryTests.cpp:2172 declares `GLsizei length;` and
|
|
||||||
// walks `for (i = 0; i < length; ++i)` over a 1000-entry stack array), so an untouched
|
|
||||||
// *length turned every error path here into a stack overrun inside the caller -
|
|
||||||
// KHR-GL43.program_interface_query.subroutines-vertex read 0x20202020 entries and died on
|
|
||||||
// both backends. The success path overwrites this with the real count.
|
|
||||||
if (length) *length = 0;
|
|
||||||
|
|
||||||
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
|
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
|
||||||
if (!programObject) return;
|
if (!programObject) return;
|
||||||
if (!ProgramInterface::IsInterfaceEnum(programInterface)) {
|
if (!ProgramInterface::IsInterfaceEnum(programInterface)) {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
#include "GL_RenderState.h"
|
#include "GL_RenderState.h"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/Converters/GLToMG/RenderStateEnumConverter.h>
|
#include <MG_Util/Converters/GLToMG/RenderStateEnumConverter.h>
|
||||||
@@ -381,18 +380,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL 4.6 core 22.1: glGetBooleani_v answers EVERY indexed state, not just the indexed
|
*data = IsEnabledi_State(target, index);
|
||||||
// capabilities - a non-boolean value simply reads back as "is it non-zero". Routing the
|
|
||||||
// non-capability enums to the pname table glGetIntegeri_v already owns is what makes
|
|
||||||
// that true; without it a query like glGetBooleani_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 0)
|
|
||||||
// came back GL_INVALID_ENUM (KHR-GL43.compute_shader.max).
|
|
||||||
if (MG_Util::ConvertGLEnumToCapabilityInput(target) != CapabilityInput::Unknown) {
|
|
||||||
*data = IsEnabledi_State(target, index);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
GLint values[4] = {};
|
|
||||||
GetIntegeri_v(target, index, values);
|
|
||||||
*data = values[0] != 0 ? GL_TRUE : GL_FALSE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GLboolean IsEnabled_State(GLenum cap) {
|
GLboolean IsEnabled_State(GLenum cap) {
|
||||||
|
|||||||
@@ -336,22 +336,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ARB_multi_bind adds one rule the single-bind path does not have: "samplers will not be
|
|
||||||
// created if they do not exist", so a name that is not an existing sampler OBJECT is
|
|
||||||
// INVALID_OPERATION here (KHR-GL44.multi_bind.errors_bind_samplers). Per element, not
|
|
||||||
// all-or-nothing - the extension defines glBindSamplers as a loop, so a bad entry costs
|
|
||||||
// its own texture unit and leaves the rest of the range bound.
|
|
||||||
for (GLsizei i = 0; i < count; ++i) {
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
const GLuint sampler = samplers ? samplers[i] : 0;
|
BindSampler_State(first + i, samplers ? samplers[i] : 0);
|
||||||
if (sampler != 0 && !MG_State::pGLContext->ValidateSamplerObject(sampler)) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>(
|
|
||||||
"MG_Impl/GLImpl", "BindSamplers",
|
|
||||||
std::format("samplers[{}] ({}) is not the name of an existing sampler object.", i, sampler)));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
BindSampler_State(first + i, sampler);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -613,23 +613,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||||
"Compressed texture formats are not supported."));
|
"Compressed texture formats are not supported."));
|
||||||
}
|
}
|
||||||
|
|
||||||
// glGetTexLevelParameter{i,f}v answers WIDTH/HEIGHT/DEPTH out of the mipmap chain. The only
|
|
||||||
// other storage type the state layer knows is GL_TEXTURE_BUFFER (TextureStorageType is
|
|
||||||
// {Mipmap, Buffer}), whose level geometry this stack does not track yet. Report that instead
|
|
||||||
// of throwing: THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and takes
|
|
||||||
// the process down, which is never an acceptable answer to a query - see the same reasoning
|
|
||||||
// above for the compressed-format path.
|
|
||||||
void RecordUnsupportedLevelQueryStorage(const char* caller, GLenum pname) {
|
|
||||||
MGLOG_I("%s: glGetTexLevelParameter(pname=%s) is not implemented for texture-buffer "
|
|
||||||
"storage; recording GL_INVALID_OPERATION instead of terminating",
|
|
||||||
caller, MG_Util::ConvertGLEnumToString(pname).c_str());
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>(
|
|
||||||
"MG_Impl/GLImpl", caller,
|
|
||||||
"Level queries are not supported for texture-buffer storage."));
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByName(GLuint texture, const char* caller) {
|
const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByName(GLuint texture, const char* caller) {
|
||||||
@@ -2927,8 +2910,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
THROW_UNIMPL_EXCEPTION;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -2942,8 +2924,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
THROW_UNIMPL_EXCEPTION;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -2957,8 +2938,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
THROW_UNIMPL_EXCEPTION;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3065,8 +3045,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
THROW_UNIMPL_EXCEPTION;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3080,8 +3059,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
THROW_UNIMPL_EXCEPTION;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3095,8 +3073,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
THROW_UNIMPL_EXCEPTION;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -3426,10 +3403,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GET_SRC_INTERNAL_FORMAT(readBufferType);
|
GET_SRC_INTERNAL_FORMAT(readBufferType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The validator has already recorded GL_INVALID_OPERATION; just decline. Throwing
|
if (!TextureImpl::ValidateBaseInternalFormatMatch(internalFormat, srcInternalFormat)) THROW_UNIMPL_EXCEPTION;
|
||||||
// here unwound a C++ exception through the C GL ABI and killed the process (see the
|
|
||||||
// same reasoning at :604-609).
|
|
||||||
if (!TextureImpl::ValidateCopyTexImageBaseFormatSubset(internalFormat, srcInternalFormat)) return false;
|
|
||||||
|
|
||||||
GLenum outInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(srcInternalFormat);
|
GLenum outInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(srcInternalFormat);
|
||||||
GLenum realInternalFormat = GL_RGBA8;
|
GLenum realInternalFormat = GL_RGBA8;
|
||||||
@@ -3452,13 +3426,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void CopyTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
void CopyTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
||||||
GLint border) {
|
GLint border) {
|
||||||
// 1D textures are not implemented by this backend set. Record the error the way every
|
// TODO: implement
|
||||||
// other unsupported entry point does - throwing unwinds through the C GL ABI and kills
|
THROW_UNIMPL_EXCEPTION;
|
||||||
// the process, which is never an acceptable answer to an unsupported call.
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "CopyTexImage1D",
|
|
||||||
"1D textures are not supported by this implementation"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
|
void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
|
||||||
@@ -4110,46 +4079,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
||||||
}
|
}
|
||||||
|
|
||||||
// No block-compressed format is defined for a three-dimensional image, so glTexStorage3D on
|
|
||||||
// TEXTURE_3D must reject one - and with INVALID_OPERATION, not the INVALID_ENUM an unknown
|
|
||||||
// sized format gets (GL 4.6 core 8.19 / Khronos bug 11239, KHR-GLxx.texture_storage
|
|
||||||
// .compressed_data). Written against the enum ranges rather than a name list because the
|
|
||||||
// families are contiguous and MobileGL's own internal-format enum drops the ones it cannot
|
|
||||||
// carry, which would make this check silently narrower than the API surface.
|
|
||||||
static Bool IsCompressedGLInternalFormat(GLenum internalformat) {
|
|
||||||
switch (internalformat) {
|
|
||||||
case 0x8225: // GL_COMPRESSED_RED
|
|
||||||
case 0x8226: // GL_COMPRESSED_RG
|
|
||||||
case 0x84ED: // GL_COMPRESSED_RGB
|
|
||||||
case 0x84EE: // GL_COMPRESSED_RGBA
|
|
||||||
case 0x8C48: // GL_COMPRESSED_SRGB
|
|
||||||
case 0x8C49: // GL_COMPRESSED_SRGB_ALPHA
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return (internalformat >= 0x83F0 && internalformat <= 0x83F3) || // S3TC / DXT
|
|
||||||
(internalformat >= 0x8DBB && internalformat <= 0x8DBE) || // RGTC
|
|
||||||
(internalformat >= 0x8E8C && internalformat <= 0x8E8F) || // BPTC
|
|
||||||
(internalformat >= 0x9270 && internalformat <= 0x9279) || // ETC2 / EAC
|
|
||||||
(internalformat >= 0x93B0 && internalformat <= 0x93BD) || // ASTC LDR
|
|
||||||
(internalformat >= 0x93D0 && internalformat <= 0x93DD); // ASTC sRGB
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
|
void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
|
||||||
GLsizei depth) {
|
GLsizei depth) {
|
||||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||||
if (!textureObject) return;
|
if (!textureObject) return;
|
||||||
if (textureObject->GetTarget() == TextureTarget::Texture3D &&
|
|
||||||
IsCompressedGLInternalFormat(internalformat)) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>(
|
|
||||||
"MG_Impl/GLImpl", __func__,
|
|
||||||
std::format("{} is a compressed internal format and cannot back GL_TEXTURE_3D storage.",
|
|
||||||
MG_Util::ConvertGLEnumToString(internalformat))));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||||
if (!ValidateTextureStorageInternalFormat(textureInternalFormat, __func__)) return;
|
if (!ValidateTextureStorageInternalFormat(textureInternalFormat, __func__)) return;
|
||||||
if (!ValidateTextureStorageShape(textureObject, 3, levels, width, height, depth, __func__)) return;
|
if (!ValidateTextureStorageShape(textureObject, 3, levels, width, height, depth, __func__)) return;
|
||||||
|
|||||||
@@ -424,81 +424,19 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
|
||||||
// Component set of an UNSIZED base internal format, as the bitmask GL 4.6 SS 8.6
|
|
||||||
// reasons about. Colour components are independent bits so "subset" is a plain
|
|
||||||
// mask test; depth and stencil are their own components and never satisfy a
|
|
||||||
// colour request (or each other).
|
|
||||||
enum : Uint32 {
|
|
||||||
kComponentR = 1u << 0,
|
|
||||||
kComponentG = 1u << 1,
|
|
||||||
kComponentB = 1u << 2,
|
|
||||||
kComponentA = 1u << 3,
|
|
||||||
kComponentDepth = 1u << 4,
|
|
||||||
kComponentStencil = 1u << 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
Uint32 BaseFormatComponents(TextureInternalFormat unsizedFormat) {
|
|
||||||
switch (unsizedFormat) {
|
|
||||||
case TextureInternalFormat::Red:
|
|
||||||
return kComponentR;
|
|
||||||
case TextureInternalFormat::RG:
|
|
||||||
return kComponentR | kComponentG;
|
|
||||||
case TextureInternalFormat::RGB:
|
|
||||||
return kComponentR | kComponentG | kComponentB;
|
|
||||||
case TextureInternalFormat::RGBA:
|
|
||||||
return kComponentR | kComponentG | kComponentB | kComponentA;
|
|
||||||
case TextureInternalFormat::DepthComponent:
|
|
||||||
return kComponentDepth;
|
|
||||||
case TextureInternalFormat::DepthStencil:
|
|
||||||
return kComponentDepth | kComponentStencil;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2) {
|
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2) {
|
||||||
const auto unsizedFormat1 = MG_Util::ConvertInternalFormatToUnsized(format1);
|
auto unsizedFormat1 = MG_Util::ConvertInternalFormatToUnsized(format1);
|
||||||
const auto unsizedFormat2 = MG_Util::ConvertInternalFormatToUnsized(format2);
|
auto unsizedFormat2 = MG_Util::ConvertInternalFormatToUnsized(format2);
|
||||||
if (unsizedFormat1 != unsizedFormat2) {
|
if (unsizedFormat1 != unsizedFormat2) {
|
||||||
// The 3-argument GenericErrorInfo constructor used to be spelled as a single
|
|
||||||
// std::format() call whose format string was the component name, so every
|
|
||||||
// diagnostic collapsed to the literal "MG_Impl/GLImpl". Format the message, then
|
|
||||||
// hand over component/function/message separately.
|
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
"MG_Impl/GLImpl", "ValidateBaseInternalFormatMatch",
|
std::format("MG_Impl/GLImpl", "ValidateBaseInternalFormatMatch",
|
||||||
std::format("The base internal format of the two formats do not match ({} vs. {})",
|
"The base internal format of the two formats do not match ({} vs. {})",
|
||||||
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat1),
|
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat1).c_str(),
|
||||||
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat2))));
|
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat2).c_str())));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
} // namespace TextureImpl
|
||||||
|
|
||||||
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat) {
|
|
||||||
const auto unsizedDest = MG_Util::ConvertInternalFormatToUnsized(destFormat);
|
|
||||||
const auto unsizedSrc = MG_Util::ConvertInternalFormatToUnsized(srcFormat);
|
|
||||||
// GL 4.6 SS 8.6: glCopyTexImage* may request a SUBSET of the read buffer's components,
|
|
||||||
// not an exact match - GL_RGB from an RGBA8 framebuffer is textbook legal and is what
|
|
||||||
// Minecraft and its mods do. glCopyTexImage2D used to run the exact-match predicate
|
|
||||||
// above and turn its rejection into an uncaught exception through the C GL ABI, so the
|
|
||||||
// app died rather than seeing a GL error.
|
|
||||||
const Uint32 destComponents = BaseFormatComponents(unsizedDest);
|
|
||||||
const Uint32 srcComponents = BaseFormatComponents(unsizedSrc);
|
|
||||||
if (destComponents == 0 || srcComponents == 0 || (destComponents & ~srcComponents) != 0) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>(
|
|
||||||
"MG_Impl/GLImpl", "ValidateCopyTexImageBaseFormatSubset",
|
|
||||||
std::format("the read buffer's base internal format {} does not provide every component of "
|
|
||||||
"the requested internal format {}",
|
|
||||||
MG_Util::ConvertTextureInternalFormatToString(unsizedSrc),
|
|
||||||
MG_Util::ConvertTextureInternalFormatToString(unsizedDest))));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
||||||
|
|||||||
@@ -40,9 +40,5 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
TextureTarget target);
|
TextureTarget target);
|
||||||
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
|
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
|
||||||
Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0);
|
Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0);
|
||||||
// Exact base-format equality - what glCopyImageSubData's format compatibility needs.
|
|
||||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2);
|
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2);
|
||||||
// GL 4.6 SS 8.6 subset rule for glCopyTexImage*: the read buffer must supply every component
|
|
||||||
// the requested internalformat asks for, but may supply more.
|
|
||||||
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat);
|
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
||||||
|
|||||||
@@ -179,28 +179,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return vao;
|
return vao;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The ARB_vertex_attrib_binding entry points that take no vertex array name modify the
|
|
||||||
// *bound* vertex array, and in a core profile the default vertex array (name 0) is not
|
|
||||||
// one: every one of them is INVALID_OPERATION there (GL 4.6 core 10.3.1, and the tail of
|
|
||||||
// each KHR-GL4x.vertex_attrib_binding.negative-* case checks exactly this). MobileGL
|
|
||||||
// keeps a real object at name 0 for the compatibility paths, so GetBoundVertexArray
|
|
||||||
// never returns null and the rule has to be spelled out - behind the same gate the VAO-0
|
|
||||||
// draw rule already uses (MOBILEGL_RELAXED_SEMANTICS, plus "the context never asked for
|
|
||||||
// a core profile"), so applications that legitimately run relaxed keep working.
|
|
||||||
static SharedPtr<MG_State::GLState::VertexArrayObject> GetBoundVertexArrayForBindingApi(const char* funcName) {
|
|
||||||
auto vao = GetBoundVertexArrayOrError(funcName);
|
|
||||||
if (!vao) return nullptr;
|
|
||||||
if (vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>(
|
|
||||||
"MG_Impl/GLImpl", funcName,
|
|
||||||
"The default vertex array object cannot be modified in a core profile."));
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return vao;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool ValidateVertexAttribPname(GLenum pname) {
|
static bool ValidateVertexAttribPname(GLenum pname) {
|
||||||
switch (pname) {
|
switch (pname) {
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_ENABLED:
|
case GL_VERTEX_ATTRIB_ARRAY_ENABLED:
|
||||||
@@ -966,7 +944,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
params[0] = static_cast<GLfloat>(attr->Size);
|
params[0] = static_cast<GLfloat>(attr->Size);
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||||
params[0] = static_cast<GLfloat>(attr->LegacyStride);
|
params[0] = static_cast<GLfloat>(attr->Stride);
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||||
params[0] = static_cast<GLfloat>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
|
params[0] = static_cast<GLfloat>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
|
||||||
@@ -1036,7 +1014,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
params[0] = static_cast<GLdouble>(attr->Size);
|
params[0] = static_cast<GLdouble>(attr->Size);
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||||
params[0] = static_cast<GLdouble>(attr->LegacyStride);
|
params[0] = static_cast<GLdouble>(attr->Stride);
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||||
params[0] = static_cast<GLdouble>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
|
params[0] = static_cast<GLdouble>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
|
||||||
@@ -1101,11 +1079,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_VERTEX_ATTRIB_ARRAY_SIZE:
|
case GL_VERTEX_ATTRIB_ARRAY_SIZE:
|
||||||
params[0] = attr->Size;
|
params[0] = attr->Size;
|
||||||
return;
|
return;
|
||||||
// The legacy shadow, not the resolved draw stride: GL 4.6 core table 23.3 defines this
|
|
||||||
// as the last glVertexAttrib*Pointer argument, which glBindVertexBuffer must not
|
|
||||||
// overwrite even though it does overwrite what the backend actually reads.
|
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||||
params[0] = attr->LegacyStride;
|
params[0] = attr->Stride;
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||||
params[0] = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
|
params[0] = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
|
||||||
@@ -1163,7 +1138,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto& attr = vao->GetAttribute(index);
|
const auto& attr = vao->GetAttribute(index);
|
||||||
*pointer = reinterpret_cast<void*>(attr.LegacyPointer);
|
*pointer = reinterpret_cast<void*>(attr.Offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetVertexAttribIiv(GLuint index, GLenum pname, GLint* params) {
|
void GetVertexAttribIiv(GLuint index, GLenum pname, GLint* params) {
|
||||||
@@ -1247,7 +1222,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*param = static_cast<GLint>(attr.Size);
|
*param = static_cast<GLint>(attr.Size);
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||||
*param = static_cast<GLint>(attr.LegacyStride);
|
*param = static_cast<GLint>(attr.Stride);
|
||||||
return;
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||||
*param = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr.Type));
|
*param = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr.Type));
|
||||||
@@ -1319,14 +1294,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void BindVertexBuffer(GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride) {
|
void BindVertexBuffer(GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride) {
|
||||||
auto vao = GetBoundVertexArrayForBindingApi("BindVertexBuffer");
|
auto vao = GetBoundVertexArrayOrError("BindVertexBuffer");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
VertexBufferBinding_State(vao, bindingindex, buffer, offset, stride, "BindVertexBuffer");
|
VertexBufferBinding_State(vao, bindingindex, buffer, offset, stride, "BindVertexBuffer");
|
||||||
}
|
}
|
||||||
|
|
||||||
void BindVertexBuffers(GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets,
|
void BindVertexBuffers(GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets,
|
||||||
const GLsizei* strides) {
|
const GLsizei* strides) {
|
||||||
auto vao = GetBoundVertexArrayForBindingApi("BindVertexBuffers");
|
auto vao = GetBoundVertexArrayOrError("BindVertexBuffers");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
if (!ValidateVertexBindingRange(first, count, "BindVertexBuffers")) return;
|
if (!ValidateVertexBindingRange(first, count, "BindVertexBuffers")) return;
|
||||||
for (GLsizei i = 0; i < count; ++i) {
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
@@ -1340,21 +1315,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VertexAttribFormat(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset) {
|
void VertexAttribFormat(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset) {
|
||||||
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribFormat");
|
auto vao = GetBoundVertexArrayOrError("VertexAttribFormat");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
VertexAttribFormatSeparate_State(vao, attribindex, size, type, normalized, relativeoffset, false,
|
VertexAttribFormatSeparate_State(vao, attribindex, size, type, normalized, relativeoffset, false,
|
||||||
"VertexAttribFormat");
|
"VertexAttribFormat");
|
||||||
}
|
}
|
||||||
|
|
||||||
void VertexAttribIFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
void VertexAttribIFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
||||||
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribIFormat");
|
auto vao = GetBoundVertexArrayOrError("VertexAttribIFormat");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
VertexAttribFormatSeparate_State(vao, attribindex, size, type, GL_FALSE, relativeoffset, true,
|
VertexAttribFormatSeparate_State(vao, attribindex, size, type, GL_FALSE, relativeoffset, true,
|
||||||
"VertexAttribIFormat");
|
"VertexAttribIFormat");
|
||||||
}
|
}
|
||||||
|
|
||||||
void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
||||||
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribLFormat");
|
auto vao = GetBoundVertexArrayOrError("VertexAttribLFormat");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset);
|
VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset);
|
||||||
}
|
}
|
||||||
@@ -1366,7 +1341,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex) {
|
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex) {
|
||||||
auto vao = GetBoundVertexArrayForBindingApi("VertexAttribBinding");
|
auto vao = GetBoundVertexArrayOrError("VertexAttribBinding");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
|
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
|
||||||
if (!ValidateVertexBindingIndex(bindingindex, "VertexAttribBinding")) return;
|
if (!ValidateVertexBindingIndex(bindingindex, "VertexAttribBinding")) return;
|
||||||
@@ -1374,7 +1349,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VertexBindingDivisor(GLuint bindingindex, GLuint divisor) {
|
void VertexBindingDivisor(GLuint bindingindex, GLuint divisor) {
|
||||||
auto vao = GetBoundVertexArrayForBindingApi("VertexBindingDivisor");
|
auto vao = GetBoundVertexArrayOrError("VertexBindingDivisor");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
if (!ValidateVertexBindingIndex(bindingindex, "VertexBindingDivisor")) return;
|
if (!ValidateVertexBindingIndex(bindingindex, "VertexBindingDivisor")) return;
|
||||||
vao->SetBindingDivisor(bindingindex, divisor);
|
vao->SetBindingDivisor(bindingindex, divisor);
|
||||||
|
|||||||
@@ -53,14 +53,6 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/AsyncCompileScenario.cpp
|
Scenarios/AsyncCompileScenario.cpp
|
||||||
Scenarios/XfbAfterClipDistanceScenario.cpp
|
Scenarios/XfbAfterClipDistanceScenario.cpp
|
||||||
Scenarios/ThreeChannelAttachmentScenario.cpp
|
Scenarios/ThreeChannelAttachmentScenario.cpp
|
||||||
Scenarios/PipelineFailureScenario.cpp
|
|
||||||
Scenarios/AdvertisedLimitsScenario.cpp
|
|
||||||
Scenarios/PixelStoreSweepScenario.cpp
|
|
||||||
Scenarios/FragCoordOriginScenario.cpp
|
|
||||||
Scenarios/ClearThenReadPixelsScenario.cpp
|
|
||||||
Scenarios/DepthStencilReadbackScenario.cpp
|
|
||||||
Scenarios/SsboArrayLengthScenario.cpp
|
|
||||||
Scenarios/SwizzleAccessRoutineScenario.cpp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||||
@@ -177,24 +169,25 @@ endif()
|
|||||||
option(MOBILEGL_ITEST_REQUIRE_GPU
|
option(MOBILEGL_ITEST_REQUIRE_GPU
|
||||||
"Fail (rather than skip) the integration scenarios when the headless harness is unusable" OFF)
|
"Fail (rather than skip) the integration scenarios when the headless harness is unusable" OFF)
|
||||||
|
|
||||||
# No EGL_PLATFORM knob here on purpose. The harness pins EGL_PLATFORM=surfaceless
|
# DirectGLES asks the system EGL for a pbuffer config, and on Mesa the default
|
||||||
# itself before its first EGL call (HeadlessGL.cpp, EnsureHeadlessPlatform) so a
|
# platform is not X11 unless it is said out loud (run_driver_bench.sh sets the
|
||||||
# developer's machine and a CI runner take the SAME path whether or not a window
|
# same variable). Wrong platform here is not a soft failure: eglCreatePbuffer
|
||||||
# system happens to be running. This used to inject "x11", which is how the lane
|
# fails and every scenario skips.
|
||||||
# came up green on a workstation with WSLg and died on a runner with no X server.
|
if (UNIX AND NOT APPLE AND NOT ANDROID)
|
||||||
#
|
set(MOBILEGL_ITEST_EGL_PLATFORM "x11" CACHE STRING
|
||||||
# A build-system knob would not just be redundant, it would be a trap: `set(...
|
"EGL_PLATFORM for the integration tests (empty: leave the loader alone)")
|
||||||
# CACHE ...)` does not rewrite an existing cache, so every build directory
|
else()
|
||||||
# configured before this change would keep injecting EGL_PLATFORM=x11 and go on
|
set(MOBILEGL_ITEST_EGL_PLATFORM "" CACHE STRING
|
||||||
# binding to a window system - silently, and only on the machines that have one.
|
"EGL_PLATFORM for the integration tests (empty: leave the loader alone)")
|
||||||
# Someone reproducing a platform-specific bug sets EGL_PLATFORM in their own
|
endif()
|
||||||
# environment, which the harness still honours.
|
|
||||||
|
|
||||||
set(MGL_ITEST_COMMON_ENV "")
|
set(MGL_ITEST_COMMON_ENV "")
|
||||||
if (MOBILEGL_ITEST_EGL_VENDOR)
|
if (MOBILEGL_ITEST_EGL_VENDOR)
|
||||||
list(APPEND MGL_ITEST_COMMON_ENV "__EGL_VENDOR_LIBRARY_FILENAMES=${MOBILEGL_ITEST_EGL_VENDOR}")
|
list(APPEND MGL_ITEST_COMMON_ENV "__EGL_VENDOR_LIBRARY_FILENAMES=${MOBILEGL_ITEST_EGL_VENDOR}")
|
||||||
endif()
|
endif()
|
||||||
unset(MOBILEGL_ITEST_EGL_PLATFORM CACHE) # see above: an old cache must not resurrect x11
|
if (MOBILEGL_ITEST_EGL_PLATFORM)
|
||||||
|
list(APPEND MGL_ITEST_COMMON_ENV "EGL_PLATFORM=${MOBILEGL_ITEST_EGL_PLATFORM}")
|
||||||
|
endif()
|
||||||
if (MOBILEGL_ITEST_REQUIRE_GPU)
|
if (MOBILEGL_ITEST_REQUIRE_GPU)
|
||||||
list(APPEND MGL_ITEST_COMMON_ENV "MOBILEGL_ITEST_REQUIRE_GPU=1")
|
list(APPEND MGL_ITEST_COMMON_ENV "MOBILEGL_ITEST_REQUIRE_GPU=1")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -74,40 +74,6 @@ namespace MGITest {
|
|||||||
std::string renderer;
|
std::string renderer;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The harness is headless BY CONSTRUCTION, on every machine: it must never
|
|
||||||
// reach a window system, not even where one happens to be running. This is
|
|
||||||
// not a CI accommodation - it is what keeps a developer's run and a CI run
|
|
||||||
// the same run. The lane was wired up green on a workstation and immediately
|
|
||||||
// died on the runner precisely because the workstation had a DISPLAY (WSLg)
|
|
||||||
// and took Mesa's x11 platform, while the runner has none; that divergence
|
|
||||||
// is the bug, and pinning the platform here is the fix for it.
|
|
||||||
//
|
|
||||||
// Mesa selects its EGL platform from EGL_PLATFORM at loader time, so this
|
|
||||||
// has to run before the first EGL call in the process (see EnsureHeadless
|
|
||||||
// callers). surfaceless is the platform with no window-system dependency at
|
|
||||||
// all; the surface this file then creates is still a pbuffer, which every
|
|
||||||
// platform supports and which the amendment to this rule requires as the
|
|
||||||
// fallback shape. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
|
|
||||||
// driver that consults them directly cannot reintroduce the dependency
|
|
||||||
// behind EGL's back. Desktop-only file: MG_IntegrationTest never builds
|
|
||||||
// for Android, so no device path is affected.
|
|
||||||
void EnsureHeadlessPlatform() {
|
|
||||||
#if defined(__linux__) && !defined(__ANDROID__)
|
|
||||||
static bool done = false;
|
|
||||||
if (done) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
done = true;
|
|
||||||
// An explicit EGL_PLATFORM from the operator still wins: pinning a
|
|
||||||
// platform is exactly how someone reproduces a platform-specific bug.
|
|
||||||
if (std::getenv("EGL_PLATFORM") == nullptr) {
|
|
||||||
setenv("EGL_PLATFORM", "surfaceless", 1);
|
|
||||||
}
|
|
||||||
unsetenv("DISPLAY");
|
|
||||||
unsetenv("WAYLAND_DISPLAY");
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
// THE bring-up, in one function so the pre-flight child and the parent run
|
// 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
|
// literally the same sequence - a pre-flight that tests something narrower
|
||||||
// than what the parent will do is exactly the kind of "predictive" check
|
// than what the parent will do is exactly the kind of "predictive" check
|
||||||
@@ -116,9 +82,6 @@ namespace MGITest {
|
|||||||
// Returns 0 on success, or the 1-based index of the step that failed, and
|
// Returns 0 on success, or the 1-based index of the step that failed, and
|
||||||
// fills outReason either way.
|
// fills outReason either way.
|
||||||
int RunEglBringUp(EglBringUp& out, std::string& outReason) {
|
int RunEglBringUp(EglBringUp& out, std::string& outReason) {
|
||||||
// Belt and braces: the pre-flight child and the parent both enter here,
|
|
||||||
// and neither may be the first to touch EGL without this having run.
|
|
||||||
EnsureHeadlessPlatform();
|
|
||||||
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||||
if (display == EGL_NO_DISPLAY) {
|
if (display == EGL_NO_DISPLAY) {
|
||||||
outReason = WithEglError("eglGetDisplay(EGL_DEFAULT_DISPLAY) returned EGL_NO_DISPLAY");
|
outReason = WithEglError("eglGetDisplay(EGL_DEFAULT_DISPLAY) returned EGL_NO_DISPLAY");
|
||||||
@@ -236,10 +199,11 @@ namespace MGITest {
|
|||||||
}
|
}
|
||||||
if (child == 0) {
|
if (child == 0) {
|
||||||
close(channel[0]);
|
close(channel[0]);
|
||||||
// No core suppression here, deliberately: when the child dies on a
|
// The child is EXPECTED to die on a signal on an unusable
|
||||||
// signal, the core IS the diagnosis (an rlimit that used to sit here
|
// platform; that is the measurement. Do not let each such
|
||||||
// made a CI-only crash undebuggable). Machines that do not want
|
// measurement drop a core file next to the test binary.
|
||||||
// cores control that with the usual ulimit/core_pattern knobs.
|
const rlimit noCore{0, 0};
|
||||||
|
setrlimit(RLIMIT_CORE, &noCore);
|
||||||
std::fprintf(stderr, "[itest] pre-flight child: attempting a full EGL bring-up\n");
|
std::fprintf(stderr, "[itest] pre-flight child: attempting a full EGL bring-up\n");
|
||||||
EglBringUp local;
|
EglBringUp local;
|
||||||
std::string reason;
|
std::string reason;
|
||||||
@@ -320,19 +284,9 @@ namespace MGITest {
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace {
|
|
||||||
bool EnvFlag(const char* name) {
|
|
||||||
const char* value = std::getenv(name);
|
|
||||||
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
bool RequireGpu() {
|
bool RequireGpu() {
|
||||||
return EnvFlag("MOBILEGL_ITEST_REQUIRE_GPU");
|
const char* value = std::getenv("MOBILEGL_ITEST_REQUIRE_GPU");
|
||||||
}
|
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
||||||
|
|
||||||
bool RequireHardwareGpu() {
|
|
||||||
return EnvFlag("MOBILEGL_ITEST_REQUIRE_HARDWARE_GPU");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::ostream& operator<<(std::ostream& os, const Rgba8& c) {
|
std::ostream& operator<<(std::ostream& os, const Rgba8& c) {
|
||||||
@@ -436,10 +390,6 @@ namespace MGITest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HeadlessGL::HeadlessGL() {
|
HeadlessGL::HeadlessGL() {
|
||||||
// Before anything else in this process can reach EGL, and in particular
|
|
||||||
// before the pre-flight forks - the child must measure the same platform
|
|
||||||
// the parent will use.
|
|
||||||
EnsureHeadlessPlatform();
|
|
||||||
m_backendName = EnvOr("MOBILEGL_BACKEND_TYPE", "<unset>");
|
m_backendName = EnvOr("MOBILEGL_BACKEND_TYPE", "<unset>");
|
||||||
m_usable = BringUp();
|
m_usable = BringUp();
|
||||||
}
|
}
|
||||||
@@ -601,13 +551,9 @@ namespace MGITest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Image ReadPixels(int width, int height) {
|
Image ReadPixels(int width, int height) {
|
||||||
return ReadPixelsRect(0, 0, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
Image ReadPixelsRect(int x, int y, int width, int height) {
|
|
||||||
Image image(width, height);
|
Image image(width, height);
|
||||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||||
glReadPixels(x, y, width, height, GL_RGBA, GL_UNSIGNED_BYTE, image.Data());
|
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, image.Data());
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,15 +41,6 @@ namespace MGITest {
|
|||||||
// a job that ran everything.
|
// a job that ran everything.
|
||||||
bool RequireGpu();
|
bool RequireGpu();
|
||||||
|
|
||||||
// True when MOBILEGL_ITEST_REQUIRE_HARDWARE_GPU is set: additionally asserts
|
|
||||||
// that the context did NOT land on a software rasterizer. Deliberately a
|
|
||||||
// SEPARATE switch from RequireGpu - a GPU-less CI runner is a supported and
|
|
||||||
// intended configuration for these scenarios (they pin backend draw logic,
|
|
||||||
// which llvmpipe/lavapipe execute faithfully), so CI wants the falsifiability
|
|
||||||
// of REQUIRE_GPU without the hardware demand. Use this one only where a vendor
|
|
||||||
// pin silently degrading to software would invalidate the measurement.
|
|
||||||
bool RequireHardwareGpu();
|
|
||||||
|
|
||||||
struct Rgba8 {
|
struct Rgba8 {
|
||||||
std::uint8_t r = 0, g = 0, b = 0, a = 0;
|
std::uint8_t r = 0, g = 0, b = 0, a = 0;
|
||||||
|
|
||||||
@@ -184,18 +175,11 @@ namespace MGITest {
|
|||||||
|
|
||||||
void ClearTo(float r, float g, float b, float a);
|
void ClearTo(float r, float g, float b, float a);
|
||||||
|
|
||||||
// Reads back the whole currently bound READ framebuffer.
|
// 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);
|
Image ReadPixels(int width, int height);
|
||||||
|
|
||||||
// A PARTIAL glReadPixels. Row 0 of the returned image is GL row `y` of the
|
|
||||||
// framebuffer, i.e. the bottom row of the requested rect - the same
|
|
||||||
// convention ReadPixels uses, just with an origin. This is the shape the
|
|
||||||
// conformance suite reads in (a random sub-rect of the default
|
|
||||||
// framebuffer), and the shape DirectVulkan's default-FBO readback used to
|
|
||||||
// hand back in Vulkan row order because its re-orientation only ran on an
|
|
||||||
// exact full-extent read.
|
|
||||||
Image ReadPixelsRect(int x, int y, int width, int height);
|
|
||||||
|
|
||||||
// Drains any GL error queue and returns the first error, or 0.
|
// Drains any GL error queue and returns the first error, or 0.
|
||||||
unsigned int FirstGLError();
|
unsigned int FirstGLError();
|
||||||
const char* GLErrorName(unsigned int error);
|
const char* GLErrorName(unsigned int error);
|
||||||
|
|||||||
@@ -45,18 +45,12 @@ namespace MGITest {
|
|||||||
}
|
}
|
||||||
GTEST_SKIP() << "no usable GPU/display/ICD for backend " << gl.BackendName() << ": " << gl.SkipReason();
|
GTEST_SKIP() << "no usable GPU/display/ICD for backend " << gl.BackendName() << ": " << gl.SkipReason();
|
||||||
}
|
}
|
||||||
if (RequireHardwareGpu() && LooksLikeSoftwareRasterizer(gl.RendererString())) {
|
if (RequireGpu() && LooksLikeSoftwareRasterizer(gl.RendererString())) {
|
||||||
// Only when hardware was asked for BY NAME. REQUIRE_GPU means "an
|
// "Ran on llvmpipe" must not be able to pass as "ran on the GPU":
|
||||||
// unusable harness is a failure, not a silent skip" - it is the
|
// a misconfigured vendor pin silently lands on the software
|
||||||
// falsifiability switch, and CI is exactly where it belongs. But CI
|
// rasterizer, and REQUIRE_GPU exists precisely to make that loud.
|
||||||
// runners have no GPU, so folding "must not be llvmpipe" into the
|
FAIL() << "MOBILEGL_ITEST_REQUIRE_GPU is set but the context landed on a software rasterizer: "
|
||||||
// same switch made the CI lane unpassable by construction: the
|
<< gl.RendererString();
|
||||||
// scenarios pin backend draw logic, which a software rasterizer
|
|
||||||
// executes just as faithfully. Landing on llvmpipe/lavapipe there is
|
|
||||||
// the intended configuration, not a misconfiguration. A vendor pin
|
|
||||||
// that must not silently degrade sets REQUIRE_HARDWARE_GPU.
|
|
||||||
FAIL() << "MOBILEGL_ITEST_REQUIRE_HARDWARE_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
|
// A scenario starts from a clean slate but shares the context (and so
|
||||||
// the renderer's memos) with every other scenario in this process -
|
// the renderer's memos) with every other scenario in this process -
|
||||||
|
|||||||
@@ -26,14 +26,8 @@ namespace {
|
|||||||
const MGITest::HeadlessGL& gl = MGITest::HeadlessGL::Get();
|
const MGITest::HeadlessGL& gl = MGITest::HeadlessGL::Get();
|
||||||
std::fprintf(stderr, "MobileGL integration scenarios: backend=%s\n", gl.BackendName().c_str());
|
std::fprintf(stderr, "MobileGL integration scenarios: backend=%s\n", gl.BackendName().c_str());
|
||||||
if (gl.Usable()) {
|
if (gl.Usable()) {
|
||||||
// EGL_PLATFORM is echoed because it is the invariant this harness
|
std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless)\n",
|
||||||
// rests on: the run is headless on every machine, so a run that
|
gl.RendererString().c_str(), gl.Width(), gl.Height());
|
||||||
// silently bound to a workstation's window system is a different
|
|
||||||
// run from CI's and must be visible as one in the log.
|
|
||||||
const char* eglPlatform = std::getenv("EGL_PLATFORM");
|
|
||||||
std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless, EGL_PLATFORM=%s)\n",
|
|
||||||
gl.RendererString().c_str(), gl.Width(), gl.Height(),
|
|
||||||
eglPlatform != nullptr ? eglPlatform : "<unset>");
|
|
||||||
} else if (MGITest::RequireGpu()) {
|
} else if (MGITest::RequireGpu()) {
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
" FAILING every scenario (MOBILEGL_ITEST_REQUIRE_GPU is set): %s\n",
|
" FAILING every scenario (MOBILEGL_ITEST_REQUIRE_GPU is set): %s\n",
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp
|
|
||||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
||||||
// Licensed under the GNU Lesser General Public License v3.0:
|
|
||||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
||||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// End of Source File Header
|
|
||||||
//
|
|
||||||
// "The limit we advertise is a promise, and an application will hold us to it."
|
|
||||||
//
|
|
||||||
// DirectVulkan copied Vulkan descriptor limits straight into the GL limit table. Those are not
|
|
||||||
// the same quantity: Adreno answers maxPerStageDescriptorUniformBuffers at descriptor-indexing
|
|
||||||
// scale, and GL_MAX_COMPUTE_UNIFORM_BLOCKS is a count an app will allocate. KHR-GL44.multi_bind
|
|
||||||
// .dispatch_bind_buffers_base does exactly that - createsO(limit) buffers and splices O(limit)
|
|
||||||
// UBO declarations into one compute shader - and spent ~14 s allocating before dying on
|
|
||||||
// std::bad_alloc. Its sibling dispatch_bind_buffers_range hard-codes 4 buffers and passes.
|
|
||||||
//
|
|
||||||
// Two failure modes, one table:
|
|
||||||
// - too LARGE: an unusable promise (the OOM above).
|
|
||||||
// - too SMALL or negative: a uint32 limit that lost its top bit on the way to a signed Int -
|
|
||||||
// UINT32_MAX arrived as -1, which every downstream std::min then accepted as "small enough".
|
|
||||||
// A conformant GL 4.x implementation may never advertise below the spec minimum either.
|
|
||||||
//
|
|
||||||
// Every bound below is checked on BOTH backends, because the loader casts are shared and the
|
|
||||||
// DirectGLES lane is the control: it takes its limits from a driver that already reports GL
|
|
||||||
// quantities, so an entry that only fails on DirectVulkan is a translation bug and one that
|
|
||||||
// fails on both is a table bug.
|
|
||||||
|
|
||||||
#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 {
|
|
||||||
|
|
||||||
struct LimitBound {
|
|
||||||
GLenum pname;
|
|
||||||
const char* name;
|
|
||||||
// The GL 4.x required minimum. A value below this is a conformance failure in its own
|
|
||||||
// right, and is what a sign-flipped uint32 looks like.
|
|
||||||
int minimum;
|
|
||||||
// The largest value this implementation is willing to promise. Chosen well above every
|
|
||||||
// desktop driver's answer, so it can only catch a descriptor-scale number.
|
|
||||||
int ceiling;
|
|
||||||
};
|
|
||||||
|
|
||||||
const std::vector<LimitBound>& BufferLimitTable() {
|
|
||||||
static const std::vector<LimitBound> table = {
|
|
||||||
{GL_MAX_UNIFORM_BUFFER_BINDINGS, "GL_MAX_UNIFORM_BUFFER_BINDINGS", 36, 256},
|
|
||||||
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", 12, 256},
|
|
||||||
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS", 8, 256},
|
|
||||||
{GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", 8, 256},
|
|
||||||
{GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", 8, 256},
|
|
||||||
{GL_MAX_TEXTURE_BUFFER_SIZE, "GL_MAX_TEXTURE_BUFFER_SIZE", 65536, 1 << 27},
|
|
||||||
{GL_MAX_UNIFORM_BLOCK_SIZE, "GL_MAX_UNIFORM_BLOCK_SIZE", 16384, 1 << 30},
|
|
||||||
// Already clamped before this campaign; in the table so a regression there is
|
|
||||||
// caught by the same case.
|
|
||||||
{GL_MAX_SHADER_STORAGE_BLOCK_SIZE, "GL_MAX_SHADER_STORAGE_BLOCK_SIZE", 1 << 24, 512 * 1024 * 1024},
|
|
||||||
{GL_MAX_TEXTURE_IMAGE_UNITS, "GL_MAX_TEXTURE_IMAGE_UNITS", 16, 32},
|
|
||||||
{GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, "GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS", 48, 192},
|
|
||||||
};
|
|
||||||
return table;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AdvertisedLimitsScenario : public ScenarioTest {};
|
|
||||||
|
|
||||||
TEST_F(AdvertisedLimitsScenario, EveryBufferLimitIsWithinItsAdvertisedRange) {
|
|
||||||
for (const LimitBound& bound : BufferLimitTable()) {
|
|
||||||
GLint value = -424242;
|
|
||||||
glGetIntegerv(bound.pname, &value);
|
|
||||||
const unsigned int error = FirstGLError();
|
|
||||||
EXPECT_EQ(error, GLenum(GL_NO_ERROR))
|
|
||||||
<< bound.name << " is not answerable: " << GLErrorName(error);
|
|
||||||
if (error != GL_NO_ERROR) continue;
|
|
||||||
|
|
||||||
EXPECT_GE(value, bound.minimum)
|
|
||||||
<< bound.name << " = " << value << " is below the GL required minimum "
|
|
||||||
<< bound.minimum << " (a negative or tiny value here is a uint32 limit that lost "
|
|
||||||
"its top bit on the way to a signed Int)";
|
|
||||||
EXPECT_LE(value, bound.ceiling)
|
|
||||||
<< bound.name << " = " << value << " exceeds the ceiling " << bound.ceiling
|
|
||||||
<< " this implementation is willing to promise - an application that allocates "
|
|
||||||
"what we advertise will run out of memory";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The OOM case in isolation, because it is the one with a known CTS victim and the one a
|
|
||||||
// future refactor is most likely to reintroduce by copying the Vulkan limit back.
|
|
||||||
TEST_F(AdvertisedLimitsScenario, ComputeUniformBlocksIsAnAmountAnApplicationCouldActuallyAllocate) {
|
|
||||||
GLint blocks = -1;
|
|
||||||
glGetIntegerv(GL_MAX_COMPUTE_UNIFORM_BLOCKS, &blocks);
|
|
||||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
||||||
EXPECT_GE(blocks, 12);
|
|
||||||
EXPECT_LE(blocks, 256) << "KHR-GL44.multi_bind.dispatch_bind_buffers_base creates one GL buffer "
|
|
||||||
"and one UBO declaration per advertised block";
|
|
||||||
|
|
||||||
GLint blockSize = -1;
|
|
||||||
glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &blockSize);
|
|
||||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
||||||
EXPECT_GT(blockSize, 0);
|
|
||||||
// GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS is derived from the product of these two,
|
|
||||||
// so their product has to stay representable.
|
|
||||||
EXPECT_LE(static_cast<long long>(blocks) * blockSize,
|
|
||||||
static_cast<long long>(2147483647))
|
|
||||||
<< "blocks(" << blocks << ") * blockSize(" << blockSize << ") overflows the GLint the "
|
|
||||||
"derived component limits are computed in";
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -157,22 +157,6 @@ void main() {
|
|||||||
const QuirkOverride m_saved;
|
const QuirkOverride m_saved;
|
||||||
};
|
};
|
||||||
|
|
||||||
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS, forced in-process for the same reason
|
|
||||||
// as AsyncModeScope: one ctest run asserts the quirk against the ambient default.
|
|
||||||
class OptimisticStatusScope {
|
|
||||||
public:
|
|
||||||
explicit OptimisticStatusScope(const QuirkOverride mode)
|
|
||||||
: m_saved(MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus) {
|
|
||||||
MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus = mode;
|
|
||||||
}
|
|
||||||
~OptimisticStatusScope() { MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus = m_saved; }
|
|
||||||
OptimisticStatusScope(const OptimisticStatusScope&) = delete;
|
|
||||||
OptimisticStatusScope& operator=(const OptimisticStatusScope&) = delete;
|
|
||||||
|
|
||||||
private:
|
|
||||||
const QuirkOverride m_saved;
|
|
||||||
};
|
|
||||||
|
|
||||||
// glMaxShaderCompilerThreadsKHR writes process-wide state; a scenario that calls
|
// 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.
|
// it has to put the pool back or it changes how every scenario after it compiles.
|
||||||
class CompilerThreadScope {
|
class CompilerThreadScope {
|
||||||
@@ -479,81 +463,5 @@ void main() {
|
|||||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Iris two-phase shape end to end on a real driver, with the optimistic-status
|
|
||||||
// quirk on: phase 1 compiles each stage and reads its log then its status (both
|
|
||||||
// answered optimistically), links, detaches and deletes the shaders for every
|
|
||||||
// program with no program-level read anywhere; phase 2 then checks every link and
|
|
||||||
// draws every program. Deliberately NOT built on the harness CompileProgram(),
|
|
||||||
// whose status read would join and collapse the phase-1 overlap this exists to
|
|
||||||
// exercise. What the unit suite cannot see - worker-produced artifacts the backend
|
|
||||||
// then mis-renders - shows up here as a wrong quadrant signature.
|
|
||||||
TEST_F(AsyncCompileScenario, IrisShapedTwoPhaseBatchRendersCorrectly) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
constexpr int kPrograms = 12;
|
|
||||||
|
|
||||||
// Distinct per program (so neither the source memo nor the adoption map turns
|
|
||||||
// a compile into a no-op) but a pure pass-through at runtime: the bulk sits in
|
|
||||||
// a branch a zero-initialised uniform never takes.
|
|
||||||
const auto fragmentSource = [](const int index) {
|
|
||||||
std::string source = "#version 330 core\nin vec3 vColor;\nout vec4 oColor;\n";
|
|
||||||
source += "uniform float uGate" + std::to_string(index) + ";\n";
|
|
||||||
source += "void main() {\n oColor = vec4(vColor, 1.0);\n";
|
|
||||||
source += " if (uGate" + std::to_string(index) + " > 1e30) {\n float acc = 1.0;\n";
|
|
||||||
for (int i = 0; i < 60; ++i) {
|
|
||||||
source += " acc = acc * 1.0001 + sin(acc + " + std::to_string(i) + ".0);\n";
|
|
||||||
}
|
|
||||||
source += " oColor = vec4(acc);\n }\n}\n";
|
|
||||||
return source;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::vector<GLuint> programs;
|
|
||||||
{
|
|
||||||
const AsyncModeScope async(true);
|
|
||||||
const OptimisticStatusScope quirk(QuirkOverride::ForceOn);
|
|
||||||
const CompilerThreadScope threads;
|
|
||||||
glMaxShaderCompilerThreadsKHR(1);
|
|
||||||
|
|
||||||
for (int i = 0; i < kPrograms; ++i) {
|
|
||||||
m_sources.push_back(fragmentSource(i));
|
|
||||||
const char* fsText = m_sources.back().c_str();
|
|
||||||
|
|
||||||
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
|
||||||
glShaderSource(vs, 1, &kVertexSource, nullptr);
|
|
||||||
glCompileShader(vs);
|
|
||||||
(void)ShaderInfoLog(vs); // Iris's exact order: the log first...
|
|
||||||
(void)ShaderCompileStatus(vs); // ...then the status; both optimistic.
|
|
||||||
|
|
||||||
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
|
|
||||||
glShaderSource(fs, 1, &fsText, nullptr);
|
|
||||||
glCompileShader(fs);
|
|
||||||
(void)ShaderInfoLog(fs);
|
|
||||||
(void)ShaderCompileStatus(fs);
|
|
||||||
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
glAttachShader(program, vs);
|
|
||||||
glAttachShader(program, fs);
|
|
||||||
glBindAttribLocation(program, 0, "aPos");
|
|
||||||
glBindAttribLocation(program, 1, "aColor");
|
|
||||||
glLinkProgram(program);
|
|
||||||
glDetachShader(program, vs);
|
|
||||||
glDetachShader(program, fs);
|
|
||||||
glDeleteShader(vs);
|
|
||||||
glDeleteShader(fs);
|
|
||||||
programs.push_back(program);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < kPrograms; ++i) {
|
|
||||||
const GLuint program = programs[static_cast<std::size_t>(i)];
|
|
||||||
GLint linked = GL_FALSE;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
ASSERT_EQ(linked, GL_TRUE) << "program " << i;
|
|
||||||
const Image image = DrawFrameWith(program);
|
|
||||||
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
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -1,335 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ClearThenReadPixelsScenario.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 CLEAR OF THE DEFAULT FRAMEBUFFER IS VISIBLE TO glReadPixels WITH NO DRAW BETWEEN.
|
|
||||||
//
|
|
||||||
// DirectVulkan parks a glClear as a pending clear and folds it into the next render pass's
|
|
||||||
// loadOp. When nothing is drawn after the clear there is no render pass, and the readback path
|
|
||||||
// used to materialize pending clears only for USER framebuffers - so a readback right after a
|
|
||||||
// clear of the DEFAULT framebuffer blitted the untouched swapchain image and handed back the
|
|
||||||
// previous frame's colour.
|
|
||||||
//
|
|
||||||
// That is the whole of KHR-GL40.draw_indirect.negative-* (12 Magma failures): each case clears,
|
|
||||||
// issues a draw that correctly raises INVALID_OPERATION and therefore never executes, then reads
|
|
||||||
// the frame back expecting (0,0,0,0) and gets the previous case's (0.1,0.2,0.3,1). The staleness
|
|
||||||
// cannot appear in one frame, so the scenario paints a frame first and clears in the next.
|
|
||||||
//
|
|
||||||
// The alpha assertion is the second half of the same census finding: a cleared default
|
|
||||||
// framebuffer read back (0,0,0,1) where (0,0,0,0) was written, because the clear was routed
|
|
||||||
// through the default FBO's placeholder attachment, whose format can lack alpha, rather than
|
|
||||||
// through the swapchain image that actually has one.
|
|
||||||
//
|
|
||||||
// DirectGLES is the built-in control: a native GL driver has no deferred-clear model at all, so
|
|
||||||
// a failure there would mean the scenario, not the backend.
|
|
||||||
|
|
||||||
#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); }
|
|
||||||
)";
|
|
||||||
|
|
||||||
// The colour KHR-GL40.draw_indirect's fshSimple paints, so a stale readback shows up as
|
|
||||||
// the same value the conformance log reports.
|
|
||||||
constexpr const char* kFS = R"(#version 330 core
|
|
||||||
out vec4 o_color;
|
|
||||||
void main() { o_color = vec4(0.1, 0.2, 0.3, 1.0); }
|
|
||||||
)";
|
|
||||||
|
|
||||||
class ClearThenReadPixelsScenario : public ScenarioTest {};
|
|
||||||
|
|
||||||
void DrawFullViewportQuad(unsigned int program) {
|
|
||||||
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
|
||||||
GLuint vao = 0, vbo = 0;
|
|
||||||
glGenVertexArrays(1, &vao);
|
|
||||||
glBindVertexArray(vao);
|
|
||||||
glGenBuffers(1, &vbo);
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
|
||||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
|
||||||
glEnableVertexAttribArray(0);
|
|
||||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
|
||||||
glUseProgram(program);
|
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
|
||||||
glBindVertexArray(0);
|
|
||||||
glDeleteBuffers(1, &vbo);
|
|
||||||
glDeleteVertexArrays(1, &vao);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST_F(ClearThenReadPixelsScenario, ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
HeadlessGL& gl = Gl();
|
|
||||||
const int width = gl.Width();
|
|
||||||
const int height = gl.Height();
|
|
||||||
ASSERT_GE(width, 8);
|
|
||||||
ASSERT_GE(height, 8);
|
|
||||||
|
|
||||||
std::string error;
|
|
||||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
|
||||||
ASSERT_NE(program, 0u) << error;
|
|
||||||
|
|
||||||
// Frame 1: paint the whole default framebuffer, so there IS something stale to return.
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
glViewport(0, 0, width, height);
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
|
||||||
glDisable(GL_DEPTH_TEST);
|
|
||||||
ClearTo(1.0f, 1.0f, 1.0f, 1.0f);
|
|
||||||
DrawFullViewportQuad(program);
|
|
||||||
{
|
|
||||||
const Image painted = ReadPixels(width, height);
|
|
||||||
const Rgba8 centre = painted.At(width / 2, height / 2);
|
|
||||||
ASSERT_NEAR(centre.r, 26, 2) << "the setup frame did not paint; the staleness test would be vacuous";
|
|
||||||
ASSERT_NEAR(centre.g, 51, 2);
|
|
||||||
ASSERT_NEAR(centre.b, 77, 2);
|
|
||||||
}
|
|
||||||
gl.EndFrame();
|
|
||||||
|
|
||||||
// Frame 2: clear to transparent black and read back with NO draw at all.
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
glViewport(0, 0, width, height);
|
|
||||||
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
|
|
||||||
const Image cleared = ReadPixels(width, height);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
int nonZero = 0;
|
|
||||||
int firstX = -1;
|
|
||||||
int firstY = -1;
|
|
||||||
Rgba8 firstOffender{};
|
|
||||||
for (int y = 0; y < height; ++y) {
|
|
||||||
for (int x = 0; x < width; ++x) {
|
|
||||||
const Rgba8 pixel = cleared.At(x, y);
|
|
||||||
if (pixel.r == 0 && pixel.g == 0 && pixel.b == 0 && pixel.a == 0) continue;
|
|
||||||
if (nonZero == 0) {
|
|
||||||
firstX = x;
|
|
||||||
firstY = y;
|
|
||||||
firstOffender = pixel;
|
|
||||||
}
|
|
||||||
++nonZero;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EXPECT_EQ(nonZero, 0) << "glClear(0,0,0,0) followed by glReadPixels with no draw returned " << nonZero
|
|
||||||
<< " of " << (width * height) << " non-zero pixels; first at (" << firstX << ", "
|
|
||||||
<< firstY << ") = (" << static_cast<int>(firstOffender.r) << ", "
|
|
||||||
<< static_cast<int>(firstOffender.g) << ", " << static_cast<int>(firstOffender.b)
|
|
||||||
<< ", " << static_cast<int>(firstOffender.a) << ")";
|
|
||||||
|
|
||||||
gl.EndFrame();
|
|
||||||
glDeleteProgram(program);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The same claim for a sub-rect read, which is the shape the conformance suite uses most and
|
|
||||||
// the one whose orientation handling is separate (see OrientationScenario).
|
|
||||||
TEST_F(ClearThenReadPixelsScenario, ClearWithNoDrawIsVisibleToASubRectReadback) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
HeadlessGL& gl = Gl();
|
|
||||||
const int width = gl.Width();
|
|
||||||
const int height = gl.Height();
|
|
||||||
ASSERT_GE(width, 8);
|
|
||||||
ASSERT_GE(height, 8);
|
|
||||||
|
|
||||||
std::string error;
|
|
||||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
|
||||||
ASSERT_NE(program, 0u) << error;
|
|
||||||
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
glViewport(0, 0, width, height);
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
|
||||||
glDisable(GL_DEPTH_TEST);
|
|
||||||
DrawFullViewportQuad(program);
|
|
||||||
gl.EndFrame();
|
|
||||||
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
glViewport(0, 0, width, height);
|
|
||||||
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
|
|
||||||
const int rectWidth = width / 2;
|
|
||||||
const int rectHeight = height / 2;
|
|
||||||
const Image cleared = ReadPixelsRect(width / 4, height / 4, rectWidth, rectHeight);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
int nonZero = 0;
|
|
||||||
for (int y = 0; y < rectHeight; ++y) {
|
|
||||||
for (int x = 0; x < rectWidth; ++x) {
|
|
||||||
const Rgba8 pixel = cleared.At(x, y);
|
|
||||||
if (pixel.r != 0 || pixel.g != 0 || pixel.b != 0 || pixel.a != 0) ++nonZero;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EXPECT_EQ(nonZero, 0) << nonZero << " of " << (rectWidth * rectHeight)
|
|
||||||
<< " pixels in a sub-rect read after a draw-free clear were not zero";
|
|
||||||
|
|
||||||
gl.EndFrame();
|
|
||||||
glDeleteProgram(program);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The other half of the same rule, and the one the first version of this fix got wrong: a
|
|
||||||
// parked clear must be executed BEFORE whatever writes the framebuffer next, not whenever the
|
|
||||||
// readback happens to notice it. Minecraft clears the default framebuffer, renders the world
|
|
||||||
// into its own framebuffer and blits the result out; nothing in between opens a render pass on
|
|
||||||
// the default framebuffer, so the clear stays parked across the whole frame. Materializing it
|
|
||||||
// at readback time therefore ran it AFTER the blit and returned a blank frame - which is what
|
|
||||||
// took every DirectVulkan retrace to ssim 0.000005.
|
|
||||||
TEST_F(ClearThenReadPixelsScenario, ABlitIntoTheDefaultFramebufferSurvivesAnEarlierClear) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
HeadlessGL& gl = Gl();
|
|
||||||
const int width = gl.Width();
|
|
||||||
const int height = gl.Height();
|
|
||||||
|
|
||||||
std::string error;
|
|
||||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
|
||||||
ASSERT_NE(program, 0u) << error;
|
|
||||||
|
|
||||||
// Paint a source framebuffer, exactly as a game renders its world off-screen.
|
|
||||||
ColorFbo source = MakeColorFbo(width, height);
|
|
||||||
ASSERT_NE(source.fbo, 0u);
|
|
||||||
BindFbo(source);
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
|
||||||
glDisable(GL_DEPTH_TEST);
|
|
||||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
|
||||||
DrawFullViewportQuad(program);
|
|
||||||
|
|
||||||
// Clear the DEFAULT framebuffer, then blit the source over it. The clear is white so a
|
|
||||||
// frame that lost the blit is unmistakable, and the blit's colour is fshSimple's.
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
glViewport(0, 0, width, height);
|
|
||||||
ClearTo(1.0f, 1.0f, 1.0f, 1.0f);
|
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, source.fbo);
|
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
|
||||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
const Image blitted = ReadPixels(width, height);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
const Rgba8 centre = blitted.At(width / 2, height / 2);
|
|
||||||
EXPECT_NEAR(centre.r, 26, 2) << "the blit into the default framebuffer did not survive the clear that "
|
|
||||||
"preceded it; read back rgba(" << static_cast<int>(centre.r) << ", "
|
|
||||||
<< static_cast<int>(centre.g) << ", " << static_cast<int>(centre.b) << ", "
|
|
||||||
<< static_cast<int>(centre.a) << ")";
|
|
||||||
EXPECT_NEAR(centre.g, 51, 2);
|
|
||||||
EXPECT_NEAR(centre.b, 77, 2);
|
|
||||||
|
|
||||||
DestroyColorFbo(source);
|
|
||||||
gl.EndFrame();
|
|
||||||
glDeleteProgram(program);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A MULTISAMPLE-RESOLVE blit into the default framebuffer has to change orientation like any
|
|
||||||
// other, but vkCmdResolveImage takes one offset per side and cannot invert an axis, so it used
|
|
||||||
// to land the mirrored band. The renderer now resolves into a single-sample scratch image and
|
|
||||||
// blits from there. The source is painted in two horizontal bands so the mirror is visible;
|
|
||||||
// a full-extent uniform blit is a fixed point of the flip and would prove nothing.
|
|
||||||
TEST_F(ClearThenReadPixelsScenario, AMultisampleResolveBlitIntoTheDefaultFramebufferKeepsItsOrientation) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
HeadlessGL& gl = Gl();
|
|
||||||
const int width = gl.Width();
|
|
||||||
const int height = gl.Height();
|
|
||||||
ASSERT_GE(height, 8);
|
|
||||||
|
|
||||||
GLint maxSamples = 0;
|
|
||||||
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
|
|
||||||
if (maxSamples < 2) {
|
|
||||||
GTEST_SKIP() << "GL_MAX_SAMPLES is " << maxSamples << "; this needs a multisample renderbuffer";
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint fbo = 0, rbo = 0;
|
|
||||||
glGenFramebuffers(1, &fbo);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
||||||
glGenRenderbuffers(1, &rbo);
|
|
||||||
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
|
|
||||||
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 2, GL_RGBA8, width, height);
|
|
||||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo);
|
|
||||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
|
||||||
glDeleteRenderbuffers(1, &rbo);
|
|
||||||
glDeleteFramebuffers(1, &fbo);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
GTEST_SKIP() << "no complete 2x multisample RGBA8 renderbuffer on this driver";
|
|
||||||
}
|
|
||||||
glViewport(0, 0, width, height);
|
|
||||||
|
|
||||||
// Bottom half red, top half blue - via scissored clears, so no shader is involved.
|
|
||||||
glEnable(GL_SCISSOR_TEST);
|
|
||||||
glScissor(0, 0, width, height / 2);
|
|
||||||
ClearTo(1.0f, 0.0f, 0.0f, 1.0f);
|
|
||||||
glScissor(0, height / 2, width, height - height / 2);
|
|
||||||
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
|
||||||
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
glViewport(0, 0, width, height);
|
|
||||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
|
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
|
||||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
const Image resolved = ReadPixels(width, height);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
const Rgba8 bottom = resolved.At(width / 2, height / 4);
|
|
||||||
const Rgba8 top = resolved.At(width / 2, height - 1 - height / 4);
|
|
||||||
EXPECT_GT(bottom.r, 200) << "the bottom band should be red after the resolve, got rgba("
|
|
||||||
<< static_cast<int>(bottom.r) << ", " << static_cast<int>(bottom.g) << ", "
|
|
||||||
<< static_cast<int>(bottom.b) << ") - blue there means the resolve landed "
|
|
||||||
<< "in the mirrored band";
|
|
||||||
EXPECT_LT(bottom.b, 60);
|
|
||||||
EXPECT_GT(top.b, 200) << "the top band should be blue after the resolve, got rgba("
|
|
||||||
<< static_cast<int>(top.r) << ", " << static_cast<int>(top.g) << ", "
|
|
||||||
<< static_cast<int>(top.b) << ")";
|
|
||||||
EXPECT_LT(top.r, 60);
|
|
||||||
|
|
||||||
glDeleteRenderbuffers(1, &rbo);
|
|
||||||
glDeleteFramebuffers(1, &fbo);
|
|
||||||
gl.EndFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The same ordering claim for the path that DOES open a render pass. It passes today (the
|
|
||||||
// render pass folds the clear into its loadOp and pops it), and it is here so a future change
|
|
||||||
// to the pending-clear lifecycle cannot quietly reverse clear and draw.
|
|
||||||
TEST_F(ClearThenReadPixelsScenario, ADrawIntoTheDefaultFramebufferSurvivesAnEarlierClear) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
HeadlessGL& gl = Gl();
|
|
||||||
const int width = gl.Width();
|
|
||||||
const int height = gl.Height();
|
|
||||||
|
|
||||||
std::string error;
|
|
||||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
|
||||||
ASSERT_NE(program, 0u) << error;
|
|
||||||
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
glViewport(0, 0, width, height);
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
|
||||||
glDisable(GL_DEPTH_TEST);
|
|
||||||
ClearTo(1.0f, 1.0f, 1.0f, 1.0f);
|
|
||||||
DrawFullViewportQuad(program);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
const Image painted = ReadPixels(width, height);
|
|
||||||
const Rgba8 centre = painted.At(width / 2, height / 2);
|
|
||||||
EXPECT_NEAR(centre.r, 26, 2) << "the draw did not survive the clear that preceded it";
|
|
||||||
EXPECT_NEAR(centre.g, 51, 2);
|
|
||||||
EXPECT_NEAR(centre.b, 77, 2);
|
|
||||||
|
|
||||||
gl.EndFrame();
|
|
||||||
glDeleteProgram(program);
|
|
||||||
}
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackScenario.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 - glReadPixels OF DEPTH AND STENCIL FROM THE DEFAULT FRAMEBUFFER.
|
|
||||||
//
|
|
||||||
// DirectVulkan's depth/stencil readback used to decline the default framebuffer outright
|
|
||||||
// (`ReadDepthStencilPixels` returned at its first line) because that framebuffer's depth and
|
|
||||||
// stencil "attachments" are placeholder texture objects backing no image - the real one is the
|
|
||||||
// swapchain's depth/stencil twin. Declining meant the call raised no GL error and wrote NOTHING,
|
|
||||||
// so the caller kept whatever its buffer already held.
|
|
||||||
//
|
|
||||||
// That silence is what the framebuffer_blit family trips over. Every one of its cases begins by
|
|
||||||
// clearing the default framebuffer's depth and stencil and reading them straight back as a
|
|
||||||
// sanity check, into a local pre-initialised to 0.2 (depth) and 50 (stencil); an untouched
|
|
||||||
// buffer therefore reports "expected DEPTH[0.25] but got DEPTH[0.2]" and "expected STENCIL[1] but
|
|
||||||
// got STENCIL[50]" - the exact strings in the 15 Magma failures - long before any blit happens.
|
|
||||||
// A test that only checked "no GL error" would pass against the broken path, so every case here
|
|
||||||
// poisons its destination with a value the correct answer cannot be.
|
|
||||||
//
|
|
||||||
// The orientation case is the second half. This renderer stores the default framebuffer
|
|
||||||
// display-side-up and converts GL rects on their way in, so the depth copy needs the same rect
|
|
||||||
// mapping and row re-ordering the colour readback got in the M-1 fix; without them a
|
|
||||||
// vertically-varying depth buffer reads back mirrored, which no full-extent uniform-value test
|
|
||||||
// can see.
|
|
||||||
//
|
|
||||||
// Depth/stencil readback through a USER framebuffer already worked and is asserted here too, as
|
|
||||||
// the built-in control: it shares ReadDepthStencilImageToClient with the default-framebuffer
|
|
||||||
// path, so it is what says a failure is about the default framebuffer specifically.
|
|
||||||
|
|
||||||
#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 {
|
|
||||||
|
|
||||||
// Values no correct read can produce, so "the backend wrote nothing" fails loudly instead
|
|
||||||
// of passing on whatever happened to be in the variable. These are the CTS's own poison
|
|
||||||
// values, which is why its logs report exactly them.
|
|
||||||
constexpr float kDepthPoison = 0.2f;
|
|
||||||
constexpr int kStencilPoison = 50;
|
|
||||||
|
|
||||||
class DepthStencilReadbackScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
// DirectGLES reads depth and stencil back through the ES driver, which has no
|
|
||||||
// guaranteed path for either (GL_NV_read_depth / GL_NV_read_stencil are optional and
|
|
||||||
// absent on both the Adreno device and Mesa's ES). That gap is tracked separately as
|
|
||||||
// the packed_depth_stencil cluster and needs a shader-sampling emulation, not this
|
|
||||||
// change; asserting it here would only pin a known-missing feature.
|
|
||||||
bool BackendReadsDepthStencil() const { return Gl().BackendName() == "DirectVulkan"; }
|
|
||||||
|
|
||||||
float ReadDepthAt(int x, int y) const {
|
|
||||||
float depth = kDepthPoison;
|
|
||||||
glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);
|
|
||||||
return depth;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ReadStencilAt(int x, int y) const {
|
|
||||||
int stencil = kStencilPoison;
|
|
||||||
glReadPixels(x, y, 1, 1, GL_STENCIL_INDEX, GL_INT, &stencil);
|
|
||||||
return stencil;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// A depth buffer whose value depends on the row: bottom half `bottom`, top half `top`.
|
|
||||||
// Built with a scissored clear rather than a draw so the test stays independent of
|
|
||||||
// depth-test and shader behaviour.
|
|
||||||
void ClearDepthInBands(int width, int height, float bottom, float top) {
|
|
||||||
glEnable(GL_SCISSOR_TEST);
|
|
||||||
glScissor(0, 0, width, height / 2);
|
|
||||||
glClearDepth(bottom);
|
|
||||||
glClear(GL_DEPTH_BUFFER_BIT);
|
|
||||||
glScissor(0, height / 2, width, height - height / 2);
|
|
||||||
glClearDepth(top);
|
|
||||||
glClear(GL_DEPTH_BUFFER_BIT);
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST_F(DepthStencilReadbackScenario, DefaultFramebufferDepthClearIsVisibleToReadPixels) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
if (!BackendReadsDepthStencil()) {
|
|
||||||
GTEST_SKIP() << "backend " << Gl().BackendName()
|
|
||||||
<< " has no depth readback path (ES lacks GL_NV_read_depth); see the packed_depth_stencil "
|
|
||||||
"cluster";
|
|
||||||
}
|
|
||||||
HeadlessGL& gl = Gl();
|
|
||||||
const int width = gl.Width();
|
|
||||||
const int height = gl.Height();
|
|
||||||
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
glViewport(0, 0, width, height);
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
|
||||||
glDepthMask(GL_TRUE);
|
|
||||||
glClearDepth(0.25);
|
|
||||||
glClear(GL_DEPTH_BUFFER_BIT);
|
|
||||||
|
|
||||||
const float centre = ReadDepthAt(width / 2, height / 2);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
EXPECT_NEAR(centre, 0.25f, 1.0f / 4096.0f)
|
|
||||||
<< "glReadPixels(GL_DEPTH_COMPONENT) of the default framebuffer returned " << centre
|
|
||||||
<< (std::fabs(centre - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");
|
|
||||||
|
|
||||||
gl.EndFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(DepthStencilReadbackScenario, DefaultFramebufferStencilClearIsVisibleToReadPixels) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
if (!BackendReadsDepthStencil()) {
|
|
||||||
GTEST_SKIP() << "backend " << Gl().BackendName()
|
|
||||||
<< " has no stencil readback path (ES lacks GL_NV_read_stencil); see the "
|
|
||||||
"packed_depth_stencil cluster";
|
|
||||||
}
|
|
||||||
HeadlessGL& gl = Gl();
|
|
||||||
const int width = gl.Width();
|
|
||||||
const int height = gl.Height();
|
|
||||||
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
glViewport(0, 0, width, height);
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
|
||||||
glStencilMask(0xFFu);
|
|
||||||
glClearStencil(3);
|
|
||||||
glClear(GL_STENCIL_BUFFER_BIT);
|
|
||||||
|
|
||||||
const int centre = ReadStencilAt(width / 2, height / 2);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
EXPECT_EQ(centre, 3) << "glReadPixels(GL_STENCIL_INDEX) of the default framebuffer returned " << centre
|
|
||||||
<< (centre == kStencilPoison ? " - the destination was never written at all" : "");
|
|
||||||
|
|
||||||
gl.EndFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The orientation half: a depth buffer that varies with the row must read back in GL's
|
|
||||||
// bottom-up order. A full-extent uniform clear is a fixed point of the flip, so only a banded
|
|
||||||
// buffer can tell the two apart.
|
|
||||||
TEST_F(DepthStencilReadbackScenario, DefaultFramebufferDepthReadbackKeepsTheGLRowOrder) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
if (!BackendReadsDepthStencil()) {
|
|
||||||
GTEST_SKIP() << "backend " << Gl().BackendName() << " has no depth readback path";
|
|
||||||
}
|
|
||||||
HeadlessGL& gl = Gl();
|
|
||||||
const int width = gl.Width();
|
|
||||||
const int height = gl.Height();
|
|
||||||
ASSERT_GE(height, 8);
|
|
||||||
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
glViewport(0, 0, width, height);
|
|
||||||
glDepthMask(GL_TRUE);
|
|
||||||
ClearDepthInBands(width, height, /*bottom=*/0.25f, /*top=*/0.75f);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
const float bottom = ReadDepthAt(width / 2, height / 4);
|
|
||||||
const float top = ReadDepthAt(width / 2, height - 1 - height / 4);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
EXPECT_NEAR(bottom, 0.25f, 1.0f / 4096.0f)
|
|
||||||
<< "GL row " << (height / 4) << " is in the bottom band and was cleared to 0.25, but read back " << bottom
|
|
||||||
<< " (0.75 there means the readback is upside down)";
|
|
||||||
EXPECT_NEAR(top, 0.75f, 1.0f / 4096.0f)
|
|
||||||
<< "GL row " << (height - 1 - height / 4) << " is in the top band and was cleared to 0.75, but read back "
|
|
||||||
<< top << " (0.25 there means the readback is upside down)";
|
|
||||||
|
|
||||||
gl.EndFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
// A depth blit INTO the default framebuffer has to convert its rect out of GL's bottom-origin
|
|
||||||
// space, exactly as the colour blit does. The colour path had that conversion and the
|
|
||||||
// depth path did not, so a scissored depth blit landed in the mirrored band - which is the
|
|
||||||
// whole of KHR-GL*.framebuffer_blit.scissor_blit once the readback above works well enough to
|
|
||||||
// see it (before that the test died on the poison values and never reached the blit).
|
|
||||||
TEST_F(DepthStencilReadbackScenario, AScissoredDepthBlitIntoTheDefaultFramebufferLandsInTheScissorBox) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
if (!BackendReadsDepthStencil()) {
|
|
||||||
GTEST_SKIP() << "backend " << Gl().BackendName() << " has no depth readback path";
|
|
||||||
}
|
|
||||||
HeadlessGL& gl = Gl();
|
|
||||||
const int width = gl.Width();
|
|
||||||
const int height = gl.Height();
|
|
||||||
ASSERT_GE(width, 8);
|
|
||||||
ASSERT_GE(height, 8);
|
|
||||||
|
|
||||||
// Source: a user framebuffer whose depth is uniformly 0.75.
|
|
||||||
GLuint fbo = 0, colorTex = 0, depthTex = 0;
|
|
||||||
glGenFramebuffers(1, &fbo);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
||||||
glGenTextures(1, &colorTex);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, colorTex);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTex, 0);
|
|
||||||
glGenTextures(1, &depthTex);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, depthTex);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, width, height, 0, GL_DEPTH_STENCIL,
|
|
||||||
GL_UNSIGNED_INT_24_8, nullptr);
|
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, depthTex, 0);
|
|
||||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
|
||||||
glViewport(0, 0, width, height);
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
|
||||||
glDepthMask(GL_TRUE);
|
|
||||||
glClearDepth(0.75);
|
|
||||||
glClear(GL_DEPTH_BUFFER_BIT);
|
|
||||||
|
|
||||||
// Destination: the default framebuffer, depth 0 everywhere.
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
glViewport(0, 0, width, height);
|
|
||||||
glClearDepth(0.0);
|
|
||||||
glClear(GL_DEPTH_BUFFER_BIT);
|
|
||||||
|
|
||||||
// Blit the whole rect, but scissored to the BOTTOM-LEFT quadrant in GL coordinates.
|
|
||||||
glEnable(GL_SCISSOR_TEST);
|
|
||||||
glScissor(0, 0, width / 2, height / 2);
|
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
|
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
|
||||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
const float inside = ReadDepthAt(width / 4, height / 4);
|
|
||||||
const float above = ReadDepthAt(width / 4, height - 1 - height / 4);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
EXPECT_NEAR(inside, 0.75f, 1.0f / 4096.0f)
|
|
||||||
<< "GL (" << (width / 4) << ", " << (height / 4) << ") is inside the scissor box and should hold the "
|
|
||||||
<< "blitted 0.75, but read back " << inside;
|
|
||||||
EXPECT_NEAR(above, 0.0f, 1.0f / 4096.0f)
|
|
||||||
<< "GL (" << (width / 4) << ", " << (height - 1 - height / 4)
|
|
||||||
<< ") is ABOVE the scissor box and must still hold the cleared 0.0, but read back " << above
|
|
||||||
<< " (0.75 there means the depth blit landed in the mirrored band)";
|
|
||||||
|
|
||||||
glDeleteTextures(1, &depthTex);
|
|
||||||
glDeleteTextures(1, &colorTex);
|
|
||||||
glDeleteFramebuffers(1, &fbo);
|
|
||||||
gl.EndFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The control: the same read against a user framebuffer, which never went through the
|
|
||||||
// declined path. It is what makes a failure above specific to the default framebuffer.
|
|
||||||
TEST_F(DepthStencilReadbackScenario, UserFramebufferDepthClearIsVisibleToReadPixels) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
if (!BackendReadsDepthStencil()) {
|
|
||||||
GTEST_SKIP() << "backend " << Gl().BackendName() << " has no depth readback path";
|
|
||||||
}
|
|
||||||
HeadlessGL& gl = Gl();
|
|
||||||
const int width = 64;
|
|
||||||
const int height = 48;
|
|
||||||
|
|
||||||
GLuint fbo = 0, colorTex = 0, depthTex = 0;
|
|
||||||
glGenFramebuffers(1, &fbo);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
||||||
glGenTextures(1, &colorTex);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, colorTex);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTex, 0);
|
|
||||||
glGenTextures(1, &depthTex);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, depthTex);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, width, height, 0, GL_DEPTH_STENCIL,
|
|
||||||
GL_UNSIGNED_INT_24_8, nullptr);
|
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, depthTex, 0);
|
|
||||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
glViewport(0, 0, width, height);
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
|
||||||
glDepthMask(GL_TRUE);
|
|
||||||
glStencilMask(0xFFu);
|
|
||||||
glClearDepth(0.5);
|
|
||||||
glClearStencil(7);
|
|
||||||
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
|
||||||
|
|
||||||
const float depth = ReadDepthAt(width / 2, height / 2);
|
|
||||||
const int stencil = ReadStencilAt(width / 2, height / 2);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
EXPECT_NEAR(depth, 0.5f, 1.0f / 4096.0f) << "user-framebuffer depth readback returned " << depth;
|
|
||||||
EXPECT_EQ(stencil, 7) << "user-framebuffer stencil readback returned " << stencil;
|
|
||||||
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
glDeleteTextures(1, &depthTex);
|
|
||||||
glDeleteTextures(1, &colorTex);
|
|
||||||
glDeleteFramebuffers(1, &fbo);
|
|
||||||
gl.EndFrame();
|
|
||||||
}
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/FragCoordOriginScenario.cpp
|
|
||||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
||||||
// Licensed under the GNU Lesser General Public License v3.0:
|
|
||||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
||||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// End of Source File Header
|
|
||||||
//
|
|
||||||
// Scenario - gl_FragCoord ON THE DEFAULT FRAMEBUFFER CARRIES GL'S WINDOW ORIGIN.
|
|
||||||
//
|
|
||||||
// GL measures gl_FragCoord.y from the BOTTOM of the window. Vulkan's gl_FragCoord.y is the
|
|
||||||
// framebuffer ROW being written, and DirectVulkan stores the default framebuffer display-side-up
|
|
||||||
// (compensating for vertices by negating gl_Position.y), so a fragment's reported Y there was
|
|
||||||
// `height - y_GL` - flipped, and for a viewport that does not span the full height, outside the
|
|
||||||
// range GL promises entirely. GL CTS
|
|
||||||
// `KHR-GL42.shader_image_load_store.basic-{allTargets-atomic,glsl-earlyFragTests,glsl-misc}`
|
|
||||||
// caught it: each sets a small viewport at GL y=0 and does
|
|
||||||
// `imageStore(image, ivec2(gl_FragCoord.xy), ...)` into an image exactly that size, so on a
|
|
||||||
// 256-tall surface every store addressed rows 224..255 of a 32-row image and was dropped.
|
|
||||||
//
|
|
||||||
// The shader here paints each row with its own GL window Y, which is the whole claim in one
|
|
||||||
// value: row j of the readback must be j, for a full-height viewport and for a half-height one
|
|
||||||
// (the case where a flip and an offset can no longer hide each other). DirectGLES is the
|
|
||||||
// built-in control - a native GL driver gets this right by construction, so a failure there
|
|
||||||
// would mean the test, not the backend.
|
|
||||||
|
|
||||||
#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* kVS = R"(#version 330 core
|
|
||||||
in vec2 aPos;
|
|
||||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
|
||||||
)";
|
|
||||||
|
|
||||||
// floor(gl_FragCoord.y) is the fragment's window row; 1/255 steps survive an RGBA8
|
|
||||||
// round trip exactly, so the readback byte IS the row the shader believes it is on.
|
|
||||||
constexpr const char* kFS = R"(#version 330 core
|
|
||||||
out vec4 o_color;
|
|
||||||
void main() { o_color = vec4(floor(gl_FragCoord.y) / 255.0, 0.0, 0.0, 1.0); }
|
|
||||||
)";
|
|
||||||
|
|
||||||
class FragCoordOriginScenario : public ScenarioTest {};
|
|
||||||
|
|
||||||
// A quad covering the whole viewport, drawn with attribute 0 = aPos.
|
|
||||||
void DrawFullViewportQuad(unsigned int program) {
|
|
||||||
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
|
||||||
GLuint vao = 0, vbo = 0;
|
|
||||||
glGenVertexArrays(1, &vao);
|
|
||||||
glBindVertexArray(vao);
|
|
||||||
glGenBuffers(1, &vbo);
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
|
||||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
|
||||||
glEnableVertexAttribArray(0);
|
|
||||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
|
||||||
glUseProgram(program);
|
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
|
||||||
glBindVertexArray(0);
|
|
||||||
glDeleteBuffers(1, &vbo);
|
|
||||||
glDeleteVertexArrays(1, &vao);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paints `viewportHeight` rows starting at GL y=0 and returns the red byte of each row.
|
|
||||||
std::vector<int> RowsPaintedWithTheirOwnWindowY(unsigned int program, int width, int viewportHeight) {
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
glViewport(0, 0, width, viewportHeight);
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
|
||||||
glDisable(GL_DEPTH_TEST);
|
|
||||||
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
|
||||||
DrawFullViewportQuad(program);
|
|
||||||
|
|
||||||
const Image image = ReadPixelsRect(0, 0, width, viewportHeight);
|
|
||||||
std::vector<int> rows;
|
|
||||||
rows.reserve(static_cast<std::size_t>(viewportHeight));
|
|
||||||
for (int y = 0; y < viewportHeight; ++y) {
|
|
||||||
rows.push_back(image.At(width / 2, y).r);
|
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
::testing::AssertionResult RowsAreTheirOwnIndex(const std::vector<int>& rows, const char* when) {
|
|
||||||
for (std::size_t y = 0; y < rows.size(); ++y) {
|
|
||||||
if (rows[y] != static_cast<int>(y)) {
|
|
||||||
return ::testing::AssertionFailure()
|
|
||||||
<< when << ": GL window row " << y << " reported gl_FragCoord.y = " << rows[y]
|
|
||||||
<< " (expected " << y << "). Rows 0.." << (rows.size() - 1) << " read back as ["
|
|
||||||
<< rows.front() << " .. " << rows.back() << "].";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ::testing::AssertionSuccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST_F(FragCoordOriginScenario, DefaultFramebufferFragCoordCountsFromTheBottom) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
HeadlessGL& gl = Gl();
|
|
||||||
// 1/255 steps only stay distinguishable while the row index fits in a byte.
|
|
||||||
const int width = gl.Width();
|
|
||||||
const int fullHeight = std::min(gl.Height(), 256);
|
|
||||||
ASSERT_GE(fullHeight, 8) << "the harness surface is too small to tell rows apart";
|
|
||||||
|
|
||||||
std::string error;
|
|
||||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
|
||||||
ASSERT_NE(program, 0u) << error;
|
|
||||||
|
|
||||||
// Full height first: this one passed even before the fix (a flip alone maps the row set
|
|
||||||
// onto itself), so it is the control that the shader and the readback agree at all.
|
|
||||||
EXPECT_TRUE(RowsAreTheirOwnIndex(RowsPaintedWithTheirOwnWindowY(program, width, fullHeight),
|
|
||||||
"full-height viewport"));
|
|
||||||
|
|
||||||
// Half height at GL y=0: the case the CTS failures were made of. A backend that reports
|
|
||||||
// the stored row here answers `height - y` for every row - off the bottom of the range,
|
|
||||||
// not merely reversed within it.
|
|
||||||
const int halfHeight = fullHeight / 2;
|
|
||||||
EXPECT_TRUE(RowsAreTheirOwnIndex(RowsPaintedWithTheirOwnWindowY(program, width, halfHeight),
|
|
||||||
"half-height viewport at GL y=0"));
|
|
||||||
|
|
||||||
glUseProgram(0);
|
|
||||||
glDeleteProgram(program);
|
|
||||||
glViewport(0, 0, gl.Width(), gl.Height());
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -55,7 +55,6 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -102,39 +101,6 @@ void main() {
|
|||||||
// "every single pixel" an achievable (and therefore useful) demand.
|
// "every single pixel" an achievable (and therefore useful) demand.
|
||||||
constexpr int kQuadrantInset = 2;
|
constexpr int kQuadrantInset = 2;
|
||||||
|
|
||||||
// A deliberately asymmetric sub-rect of the 128x96 surface: neither centred nor
|
|
||||||
// full-extent in either axis, mirroring the conformance suite's randomised
|
|
||||||
// sub-viewport geometry (glcShaderRenderCase.cpp:735-741). Asymmetry is the whole
|
|
||||||
// point - y == H - y - h is exactly the case an unconverted Y origin gets right by
|
|
||||||
// accident, and it is the only case the shipped code ever exercised.
|
|
||||||
// correct band = GL rows [13, 55)
|
|
||||||
// mirrored band = GL rows [41, 83) (what H-y-h produces)
|
|
||||||
constexpr int kSubX = 17;
|
|
||||||
constexpr int kSubY = 13;
|
|
||||||
constexpr int kSubW = 60;
|
|
||||||
constexpr int kSubH = 42;
|
|
||||||
|
|
||||||
Image CropRect(const Image& source, int x0, int y0, int width, int height) {
|
|
||||||
Image out(width, height);
|
|
||||||
const std::size_t rowBytes = static_cast<std::size_t>(width) * 4;
|
|
||||||
for (int y = 0; y < height; ++y) {
|
|
||||||
const std::uint8_t* sourceRow =
|
|
||||||
source.Data() + (static_cast<std::size_t>(y0 + y) * source.Width() + x0) * 4;
|
|
||||||
std::memcpy(out.Data() + static_cast<std::size_t>(y) * rowBytes, sourceRow, rowBytes);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
Image VFlip(const Image& source) {
|
|
||||||
Image out(source.Width(), source.Height());
|
|
||||||
const std::size_t rowBytes = static_cast<std::size_t>(source.Width()) * 4;
|
|
||||||
for (int y = 0; y < source.Height(); ++y) {
|
|
||||||
std::memcpy(out.Data() + static_cast<std::size_t>(y) * rowBytes,
|
|
||||||
source.Data() + static_cast<std::size_t>(source.Height() - 1 - y) * rowBytes, rowBytes);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Vertex {
|
struct Vertex {
|
||||||
float x, y;
|
float x, y;
|
||||||
float r, g, b;
|
float r, g, b;
|
||||||
@@ -411,174 +377,5 @@ void main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ sub-rect / M-1 ----
|
|
||||||
//
|
|
||||||
// Everything above reads the FULL extent of its target, which is the one case
|
|
||||||
// DirectVulkan's default-framebuffer readback ever re-oriented: the remap at
|
|
||||||
// VulkanRenderer.cpp:2042 had no rect parameters at all, so :8278 gated it on
|
|
||||||
// `width == swapchainExtent.width && height == swapchainExtent.height` and fell back to a
|
|
||||||
// raw copy otherwise. Meanwhile the viewport (:422), the scissor (:506-546) and the
|
|
||||||
// ReadPixels copy offset (:8238) all used the GL bottom-origin Y verbatim as a Vulkan
|
|
||||||
// top-origin Y.
|
|
||||||
//
|
|
||||||
// In the conformance suite those defects CANCEL in placement - the draw lands in Vulkan
|
|
||||||
// rows [y, y+h) and the readback copies the same rows back - and compose into an exact
|
|
||||||
// vertical flip of a correct image. That is 1,759 of Magma's 1,793 non-pass cases, and
|
|
||||||
// image forensics over all 861 gl33 failures found 861 vertical flips and nothing else.
|
|
||||||
// Taken apart, they are two independent user-visible bugs, so they are tested apart:
|
|
||||||
// SubViewportDraw pins placement with a full-extent read, SubRectReadback pins the
|
|
||||||
// readback rect after a full-viewport draw, and SubViewportSubRectRoundTrip is the CTS
|
|
||||||
// shape where the two cancel.
|
|
||||||
|
|
||||||
// Placement: a sub-viewport draw must land in GL rows [y0, y0+h), not mirrored about the
|
|
||||||
// surface centre. Read back full-extent, which is the path that already worked, so a
|
|
||||||
// failure here can only be the viewport's Y origin.
|
|
||||||
TEST_F(OrientationScenario, SubViewportDrawLandsWhereGLPutsIt) {
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
|
||||||
glViewport(kSubX, kSubY, kSubW, kSubH);
|
|
||||||
DrawQuadrants();
|
|
||||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
|
||||||
|
|
||||||
const Image whole = ReadPixels(Gl().Width(), Gl().Height());
|
|
||||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
||||||
|
|
||||||
const Image placed = CropRect(whole, kSubX, kSubY, kSubW, kSubH);
|
|
||||||
EXPECT_EQ(placed.QuadrantSignature(), kUprightSignature)
|
|
||||||
<< "the sub-viewport draw is not upright inside its own rect";
|
|
||||||
ExpectUprightQuadrants(placed, "sub-viewport draw, cropped out of a full-extent read");
|
|
||||||
|
|
||||||
// Nothing may have been painted outside the viewport. This is what catches the
|
|
||||||
// mirrored placement: the drawn band would sit at GL rows [41, 83) instead.
|
|
||||||
EXPECT_TRUE(RegionIsMostly(whole, 0, Gl().Width() - 1, 0, kSubY - 2, "black", 0.0,
|
|
||||||
"below the sub-viewport"));
|
|
||||||
EXPECT_TRUE(RegionIsMostly(whole, 0, Gl().Width() - 1, kSubY + kSubH + 1, Gl().Height() - 1, "black",
|
|
||||||
0.0, "above the sub-viewport"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Readback: a full-viewport draw read back through a sub-rect must return the requested
|
|
||||||
// band, in GL row order. Band and orientation are asserted separately so that fixing only
|
|
||||||
// one of the two cannot pass this case.
|
|
||||||
TEST_F(OrientationScenario, SubRectReadbackReturnsTheRequestedBandUpright) {
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
|
||||||
DrawQuadrants();
|
|
||||||
|
|
||||||
const Image whole = ReadPixels(Gl().Width(), Gl().Height());
|
|
||||||
ASSERT_EQ(whole.QuadrantSignature(), kUprightSignature)
|
|
||||||
<< "the full-extent read is already wrong, so nothing below can be trusted";
|
|
||||||
|
|
||||||
const Image sub = ReadPixelsRect(kSubX, kSubY, kSubW, kSubH);
|
|
||||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
||||||
ASSERT_EQ(sub.Width(), kSubW);
|
|
||||||
ASSERT_EQ(sub.Height(), kSubH);
|
|
||||||
|
|
||||||
const Image requestedBand = CropRect(whole, kSubX, kSubY, kSubW, kSubH);
|
|
||||||
const Image mirroredBand = CropRect(whole, kSubX, Gl().Height() - kSubY - kSubH, kSubW, kSubH);
|
|
||||||
|
|
||||||
// The geometry has to be able to see both mistakes; if a future surface size made the
|
|
||||||
// band symmetric these assertions would be vacuous, so say so loudly instead.
|
|
||||||
ASSERT_FALSE(requestedBand == VFlip(requestedBand))
|
|
||||||
<< "the chosen sub-rect is vertically symmetric - it cannot detect a row flip";
|
|
||||||
ASSERT_FALSE(requestedBand == mirroredBand)
|
|
||||||
<< "the chosen sub-rect equals its mirror band - it cannot detect a wrong band";
|
|
||||||
|
|
||||||
EXPECT_FALSE(sub == VFlip(requestedBand))
|
|
||||||
<< "ORIENTATION: the requested band came back with its rows in Vulkan (top-first) order";
|
|
||||||
EXPECT_FALSE(sub == mirroredBand || sub == VFlip(mirroredBand))
|
|
||||||
<< "BAND: the read returned GL rows [H-y-h, H-y) instead of [y, y+h)";
|
|
||||||
EXPECT_TRUE(sub == requestedBand)
|
|
||||||
<< "the sub-rect readback differs from the same rect of the full-extent read in "
|
|
||||||
<< sub.ByteDiffCount(requestedBand) << " bytes";
|
|
||||||
}
|
|
||||||
|
|
||||||
// The exact conformance-suite shape: an asymmetric sub-viewport draw read back through the
|
|
||||||
// very same sub-rect. The placement and readback errors cancel, leaving an image that is
|
|
||||||
// correct in every pixel VALUE and vertically flipped - which is precisely the 861-case
|
|
||||||
// signature. One assertion, and it pins all of them.
|
|
||||||
TEST_F(OrientationScenario, SubViewportSubRectRoundTripIsUpright) {
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
|
||||||
glViewport(kSubX, kSubY, kSubW, kSubH);
|
|
||||||
DrawQuadrants();
|
|
||||||
const Image sub = ReadPixelsRect(kSubX, kSubY, kSubW, kSubH);
|
|
||||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
|
||||||
|
|
||||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
||||||
EXPECT_EQ(sub.QuadrantSignature(), kUprightSignature)
|
|
||||||
<< "sub-viewport draw + same-rect readback came back flipped - this is the shape "
|
|
||||||
"behind KHR-GL33/GL40.shaders.* (861 cases each)";
|
|
||||||
ExpectUprightQuadrants(sub, "sub-viewport draw read back through the same sub-rect");
|
|
||||||
}
|
|
||||||
|
|
||||||
// The same conversion, on the other rect consumer that reads the default framebuffer.
|
|
||||||
// glBlitFramebuffer already converted its DESTINATION rect when the draw framebuffer was
|
|
||||||
// the default one (ApplyNativeBlitDefaultFramebufferTransform), but never its SOURCE rect,
|
|
||||||
// so a blit OUT of the default framebuffer took the mirrored band and wrote it upside
|
|
||||||
// down. Blitting a sub-rect and comparing against the same sub-rect of a direct read pins
|
|
||||||
// both halves at once.
|
|
||||||
TEST_F(OrientationScenario, BlitOutOfTheDefaultFramebufferKeepsBandAndOrientation) {
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
|
||||||
DrawQuadrants();
|
|
||||||
const Image whole = ReadPixels(Gl().Width(), Gl().Height());
|
|
||||||
ASSERT_EQ(whole.QuadrantSignature(), kUprightSignature)
|
|
||||||
<< "the full-extent read is already wrong, so nothing below can be trusted";
|
|
||||||
|
|
||||||
BindFbo(m_offscreen);
|
|
||||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_offscreen.fbo);
|
|
||||||
glBlitFramebuffer(kSubX, kSubY, kSubX + kSubW, kSubY + kSubH, kSubX, kSubY, kSubX + kSubW,
|
|
||||||
kSubY + kSubH, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
|
||||||
const unsigned int blitError = FirstGLError();
|
|
||||||
if (blitError != GL_NO_ERROR) {
|
|
||||||
GTEST_SKIP() << "this backend refused the default-framebuffer blit: "
|
|
||||||
<< GLErrorName(blitError);
|
|
||||||
}
|
|
||||||
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, m_offscreen.fbo);
|
|
||||||
const Image blitted = ReadPixels(m_offscreen.width, m_offscreen.height);
|
|
||||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
||||||
|
|
||||||
const Image landed = CropRect(blitted, kSubX, kSubY, kSubW, kSubH);
|
|
||||||
const Image expected = CropRect(whole, kSubX, kSubY, kSubW, kSubH);
|
|
||||||
EXPECT_FALSE(landed == VFlip(expected))
|
|
||||||
<< "ORIENTATION: the blitted band arrived upside down";
|
|
||||||
EXPECT_TRUE(landed == expected)
|
|
||||||
<< "the blitted sub-rect differs from the same sub-rect of a direct read in "
|
|
||||||
<< landed.ByteDiffCount(expected) << " bytes";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Negative control. A non-default framebuffer is already self-consistent - no
|
|
||||||
// gl_Position.y negation, GL row 0 IS Vulkan row 0 - so none of the fixes above may touch
|
|
||||||
// it. If this ever starts failing, the default-FBO remap has leaked into the FBO path.
|
|
||||||
TEST_F(OrientationScenario, FboSubRectReadbackAndSubViewportAreUnaffected) {
|
|
||||||
BindFbo(m_offscreen);
|
|
||||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
|
||||||
DrawQuadrants();
|
|
||||||
|
|
||||||
const Image whole = ReadPixels(m_offscreen.width, m_offscreen.height);
|
|
||||||
const Image sub = ReadPixelsRect(kSubX, kSubY, kSubW, kSubH);
|
|
||||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
||||||
EXPECT_TRUE(sub == CropRect(whole, kSubX, kSubY, kSubW, kSubH))
|
|
||||||
<< "an FBO sub-rect readback differs from the same rect of its full-extent read in "
|
|
||||||
<< sub.ByteDiffCount(CropRect(whole, kSubX, kSubY, kSubW, kSubH)) << " bytes";
|
|
||||||
|
|
||||||
BindFbo(m_offscreen);
|
|
||||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
|
||||||
glViewport(kSubX, kSubY, kSubW, kSubH);
|
|
||||||
DrawQuadrants();
|
|
||||||
glViewport(0, 0, m_offscreen.width, m_offscreen.height);
|
|
||||||
const Image placedWhole = ReadPixels(m_offscreen.width, m_offscreen.height);
|
|
||||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
||||||
EXPECT_EQ(CropRect(placedWhole, kSubX, kSubY, kSubW, kSubH).QuadrantSignature(), kUprightSignature)
|
|
||||||
<< "an FBO sub-viewport draw must land in GL rows [y0, y0+h) upright";
|
|
||||||
EXPECT_TRUE(RegionIsMostly(placedWhole, 0, m_offscreen.width - 1, 0, kSubY - 2, "black", 0.0,
|
|
||||||
"below an FBO sub-viewport"));
|
|
||||||
EXPECT_TRUE(RegionIsMostly(placedWhole, 0, m_offscreen.width - 1, kSubY + kSubH + 1,
|
|
||||||
m_offscreen.height - 1, "black", 0.0, "above an FBO sub-viewport"));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PipelineFailureScenario.cpp
|
|
||||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
||||||
// Licensed under the GNU Lesser General Public License v3.0:
|
|
||||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
||||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// End of Source File Header
|
|
||||||
//
|
|
||||||
// "The draw had no pipeline, so we bound null."
|
|
||||||
//
|
|
||||||
// DirectVulkan's SetupDraw called GetOrCreatePipeline - a function that DOCUMENTS a
|
|
||||||
// VK_NULL_HANDLE return - and passed the result straight to vkCmdBindPipeline. When the
|
|
||||||
// Adreno driver answered vkCreateGraphicsPipelines with VK_ERROR_UNKNOWN, the next
|
|
||||||
// instruction dereferenced null inside the driver: SIGSEGV at fault addr 0x8, and that one
|
|
||||||
// shape accounted for 9 of the 15 process deaths in the 2026-08-10 GL-CTS run
|
|
||||||
// (KHR-GL33/GL40.shaders.struct.uniform.sampler_array_vertex, six
|
|
||||||
// KHR-GL42.shader_image_load_store cases, one shader_storage_buffer_object case).
|
|
||||||
//
|
|
||||||
// It was made permanent by a second defect: PipelineFactory memoized the failure, so the
|
|
||||||
// null was served for the rest of the process. Every later draw with the same state died
|
|
||||||
// too, which is why a single bad program took whole CTS groups down with it.
|
|
||||||
//
|
|
||||||
// What this scenario pins, on both backends:
|
|
||||||
// 1. The GL program shape the CTS crashed on (an array of structs each containing a
|
|
||||||
// sampler, sampled from the VERTEX stage) draws without killing the process.
|
|
||||||
// 2. It draws AGAIN and produces the identical image. A second draw is the only thing
|
|
||||||
// that can tell a working pipeline apart from a poisoned cache entry: if the first
|
|
||||||
// creation had failed and been memoized, the second draw is where the null would be
|
|
||||||
// served back.
|
|
||||||
//
|
|
||||||
// A deterministic driver-side pipeline-creation FAILURE is not reachable from the GL API on
|
|
||||||
// the llvmpipe/lavapipe lanes - both accept every pipeline these scenarios can describe - so
|
|
||||||
// the guard itself is proven structurally (PipelineFactory returns before it can emplace a
|
|
||||||
// VK_NULL_HANDLE, SetupDraw returns false before it can bind one) and this scenario holds
|
|
||||||
// the surrounding path honest.
|
|
||||||
|
|
||||||
#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 {
|
|
||||||
|
|
||||||
// Lifted from KHR-GL33.shaders.struct.uniform.sampler_array_vertex (the QPA records the
|
|
||||||
// source verbatim): an array of structs, each carrying an opaque sampler, sampled in the
|
|
||||||
// vertex stage. The fragment sibling of this case only FAILS on Magma; only the vertex one
|
|
||||||
// takes the process down, so the stage matters and is kept.
|
|
||||||
constexpr const char* kSamplerArrayVertexSource = R"(#version 330 core
|
|
||||||
struct S {
|
|
||||||
float a;
|
|
||||||
vec3 b;
|
|
||||||
sampler2D c;
|
|
||||||
};
|
|
||||||
uniform S s[2];
|
|
||||||
in vec2 aPos;
|
|
||||||
out vec4 vColor;
|
|
||||||
void main() {
|
|
||||||
vec2 coords = aPos * 0.5 + 0.5;
|
|
||||||
vColor = vec4(texture(s[1].c, coords * s[0].b.xy + s[1].b.z).rgb, s[0].a);
|
|
||||||
gl_Position = vec4(aPos, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
constexpr const char* kPassthroughFragmentSource = R"(#version 330 core
|
|
||||||
in vec4 vColor;
|
|
||||||
out vec4 oColor;
|
|
||||||
void main() {
|
|
||||||
oColor = vColor;
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
struct Vertex {
|
|
||||||
float x, y;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::vector<Vertex> FullscreenTriangleStrip() {
|
|
||||||
return {{-1.0f, -1.0f}, {1.0f, -1.0f}, {-1.0f, 1.0f}, {1.0f, 1.0f}};
|
|
||||||
}
|
|
||||||
|
|
||||||
class PipelineFailureScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
|
|
||||||
std::string error;
|
|
||||||
m_program = CompileProgram(kSamplerArrayVertexSource, kPassthroughFragmentSource, &error);
|
|
||||||
ASSERT_NE(m_program, 0u) << error;
|
|
||||||
|
|
||||||
const std::vector<Vertex> vertices = FullscreenTriangleStrip();
|
|
||||||
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));
|
|
||||||
glBindVertexArray(0);
|
|
||||||
|
|
||||||
// A solid red 2x2 texture, so the sampled colour is the same wherever the
|
|
||||||
// (deliberately degenerate) coordinates land.
|
|
||||||
const unsigned char red[] = {255, 0, 0, 255, 255, 0, 0, 255,
|
|
||||||
255, 0, 0, 255, 255, 0, 0, 255};
|
|
||||||
glGenTextures(1, &m_texture);
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, red);
|
|
||||||
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);
|
|
||||||
|
|
||||||
glUseProgram(m_program);
|
|
||||||
const int samplerLocation = glGetUniformLocation(m_program, "s[1].c");
|
|
||||||
if (samplerLocation >= 0) glUniform1i(samplerLocation, 0);
|
|
||||||
const int alphaLocation = glGetUniformLocation(m_program, "s[0].a");
|
|
||||||
if (alphaLocation >= 0) glUniform1f(alphaLocation, 1.0f);
|
|
||||||
glUseProgram(0);
|
|
||||||
|
|
||||||
m_target = MakeColorFbo(Gl().Width(), Gl().Height());
|
|
||||||
ASSERT_NE(m_target.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_target);
|
|
||||||
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
|
||||||
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
|
||||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
|
||||||
if (m_program != 0) glDeleteProgram(m_program);
|
|
||||||
}
|
|
||||||
|
|
||||||
Image DrawOnce() {
|
|
||||||
BindFbo(m_target);
|
|
||||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
|
||||||
glDisable(GL_DEPTH_TEST);
|
|
||||||
glDisable(GL_BLEND);
|
|
||||||
glUseProgram(m_program);
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
|
||||||
glBindVertexArray(m_vao);
|
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, m_vertexCount);
|
|
||||||
glBindVertexArray(0);
|
|
||||||
return ReadPixels(m_target.width, m_target.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned int m_program = 0;
|
|
||||||
unsigned int m_vao = 0;
|
|
||||||
unsigned int m_vbo = 0;
|
|
||||||
unsigned int m_texture = 0;
|
|
||||||
int m_vertexCount = 0;
|
|
||||||
ColorFbo m_target;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reaching the assertion at all is most of the point: the shipped code SIGSEGV'd inside
|
|
||||||
// the driver on this draw.
|
|
||||||
TEST_F(PipelineFailureScenario, SamplerArrayInAStructDrawsWithoutKillingTheProcess) {
|
|
||||||
const Image drawn = DrawOnce();
|
|
||||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
||||||
EXPECT_TRUE(RegionIsMostly(drawn, 2, drawn.Width() - 3, 2, drawn.Height() - 3, "red", 0.0,
|
|
||||||
"sampler-array-in-struct draw"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The second draw is what a poisoned cache entry cannot survive: a memoized
|
|
||||||
// VK_NULL_HANDLE is served on every subsequent lookup, so a run that dies (or silently
|
|
||||||
// stops drawing) on the second draw and not the first is exactly the "failed pipeline was
|
|
||||||
// cached" defect.
|
|
||||||
TEST_F(PipelineFailureScenario, TheSameDrawRepeatsIdenticallyWithNoPoisonedPipelineCache) {
|
|
||||||
const Image first = DrawOnce();
|
|
||||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the first draw already errored";
|
|
||||||
Gl().EndFrame();
|
|
||||||
const Image second = DrawOnce();
|
|
||||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the second draw errored";
|
|
||||||
|
|
||||||
EXPECT_TRUE(RegionIsMostly(second, 2, second.Width() - 3, 2, second.Height() - 3, "red", 0.0,
|
|
||||||
"second draw"));
|
|
||||||
EXPECT_TRUE(second == first) << "the second draw differs from the first in "
|
|
||||||
<< second.ByteDiffCount(first) << " bytes - the pipeline the second "
|
|
||||||
"draw resolved is not the one the first draw used";
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PixelStoreSweepScenario.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 - PIXEL-STORE MODES RESTORE, and FRAMEBUFFER CHURN STAYS EXACT.
|
|
||||||
//
|
|
||||||
// Both cases here replay the shape of KHR-GL3x.packed_pixels.varied_rectangle, the single
|
|
||||||
// heaviest polluter in the GL CTS: for each of 46 (pixel-store mode, value) pairs it uploads a
|
|
||||||
// gradient into a fresh texture, attaches that texture to a FRESH framebuffer, reads it back and
|
|
||||||
// deletes both - ~3300 texture+framebuffer pairs per test case.
|
|
||||||
//
|
|
||||||
// What that found: DirectGLES had no destructor for BackendFramebufferObject (nor for the
|
|
||||||
// renderbuffer and sampler twins), so every frontend glDeleteFramebuffers leaked one driver
|
|
||||||
// framebuffer for the process lifetime. On an Adreno 830 the CTS run walked the driver to 1.2 GB
|
|
||||||
// of dead objects, and from that point on EVERY readback through a freshly attached framebuffer
|
|
||||||
// came back with someone else's pixels - which is what made ~1,500 otherwise-correct cases fail
|
|
||||||
// depending only on how much ran before them. The unit-level pin for the missing destructors is
|
|
||||||
// MG_Test/SanityTest.cpp (DirectGLESBackendFramebuffer/Renderbuffer/Sampler); this file pins the
|
|
||||||
// end-to-end behaviour they protect.
|
|
||||||
//
|
|
||||||
// The mode sweep is the second half of the same story: 46 modes are set and reset per case, so a
|
|
||||||
// mode that fails to restore is indistinguishable from the leak in a full-batch CTS run. The
|
|
||||||
// assertion here is RESTORATION - after every single mode is set and put back, a readback at
|
|
||||||
// default state must be byte-identical to one taken before the sweep ever started.
|
|
||||||
//
|
|
||||||
// Backend-agnostic on purpose: both bugs this guards against are frontend/backend bookkeeping,
|
|
||||||
// and DirectVulkan is the built-in control.
|
|
||||||
|
|
||||||
#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 {
|
|
||||||
|
|
||||||
// Small enough that the table's row lengths (10, 15) and image heights are all >= the
|
|
||||||
// image, which is the shape the CTS uses (its gradient is 7x3).
|
|
||||||
constexpr int kTexSize = 8;
|
|
||||||
// Every buffer handed to GL is this big regardless of the image size: with row length 15,
|
|
||||||
// two skipped rows/pixels and alignment 8 the driver strides well past the natural image
|
|
||||||
// extent, and a tight buffer would be an out-of-bounds access rather than a test. (It was:
|
|
||||||
// the first version of this scenario passed its assertions and then segfaulted at
|
|
||||||
// teardown, because glReadPixels had written past a 1 KiB destination.)
|
|
||||||
constexpr std::size_t kScratchBytes = 64 * 1024;
|
|
||||||
|
|
||||||
// Every pixel-store mode GL 4.0 has, so a reset provably covers the whole state and not
|
|
||||||
// just the subset a particular test happened to touch.
|
|
||||||
struct PixelStoreMode {
|
|
||||||
GLenum name;
|
|
||||||
GLint defaultValue;
|
|
||||||
};
|
|
||||||
const PixelStoreMode kAllModes[] = {
|
|
||||||
{GL_UNPACK_SWAP_BYTES, 0}, {GL_UNPACK_LSB_FIRST, 0}, {GL_UNPACK_ROW_LENGTH, 0},
|
|
||||||
{GL_UNPACK_IMAGE_HEIGHT, 0}, {GL_UNPACK_SKIP_ROWS, 0}, {GL_UNPACK_SKIP_PIXELS, 0},
|
|
||||||
{GL_UNPACK_SKIP_IMAGES, 0}, {GL_UNPACK_ALIGNMENT, 4}, {GL_PACK_SWAP_BYTES, 0},
|
|
||||||
{GL_PACK_LSB_FIRST, 0}, {GL_PACK_ROW_LENGTH, 0}, {GL_PACK_IMAGE_HEIGHT, 0},
|
|
||||||
{GL_PACK_SKIP_ROWS, 0}, {GL_PACK_SKIP_PIXELS, 0}, {GL_PACK_SKIP_IMAGES, 0},
|
|
||||||
{GL_PACK_ALIGNMENT, 4},
|
|
||||||
};
|
|
||||||
|
|
||||||
// The CTS table verbatim (glcPackedPixelsTests.cpp VariedRectangleTest::iterate): 32
|
|
||||||
// common cases plus the 14 core-only ones ES has no equivalent for and MobileGL therefore
|
|
||||||
// honours on the CPU. IMAGE_WIDTH_1/2 and IMAGE_HEIGHT_1/2 are the CTS's 10 and 15.
|
|
||||||
struct SweepCase {
|
|
||||||
GLenum mode;
|
|
||||||
GLint value;
|
|
||||||
};
|
|
||||||
const SweepCase kSweep[] = {
|
|
||||||
{GL_UNPACK_ROW_LENGTH, 0}, {GL_UNPACK_ROW_LENGTH, 10}, {GL_UNPACK_ROW_LENGTH, 15},
|
|
||||||
{GL_UNPACK_SKIP_ROWS, 0}, {GL_UNPACK_SKIP_ROWS, 1}, {GL_UNPACK_SKIP_ROWS, 2},
|
|
||||||
{GL_UNPACK_SKIP_PIXELS, 0}, {GL_UNPACK_SKIP_PIXELS, 1}, {GL_UNPACK_SKIP_PIXELS, 2},
|
|
||||||
{GL_UNPACK_ALIGNMENT, 1}, {GL_UNPACK_ALIGNMENT, 2}, {GL_UNPACK_ALIGNMENT, 4},
|
|
||||||
{GL_UNPACK_ALIGNMENT, 8}, {GL_UNPACK_IMAGE_HEIGHT, 0}, {GL_UNPACK_IMAGE_HEIGHT, 10},
|
|
||||||
{GL_UNPACK_IMAGE_HEIGHT, 15}, {GL_UNPACK_SKIP_IMAGES, 0}, {GL_UNPACK_SKIP_IMAGES, 1},
|
|
||||||
{GL_UNPACK_SKIP_IMAGES, 2}, {GL_PACK_ROW_LENGTH, 0}, {GL_PACK_ROW_LENGTH, 10},
|
|
||||||
{GL_PACK_ROW_LENGTH, 15}, {GL_PACK_SKIP_ROWS, 0}, {GL_PACK_SKIP_ROWS, 1},
|
|
||||||
{GL_PACK_SKIP_ROWS, 2}, {GL_PACK_SKIP_PIXELS, 0}, {GL_PACK_SKIP_PIXELS, 1},
|
|
||||||
{GL_PACK_SKIP_PIXELS, 2}, {GL_PACK_ALIGNMENT, 1}, {GL_PACK_ALIGNMENT, 2},
|
|
||||||
{GL_PACK_ALIGNMENT, 4}, {GL_PACK_ALIGNMENT, 8},
|
|
||||||
// core-only, no ES equivalent
|
|
||||||
{GL_UNPACK_SWAP_BYTES, GL_FALSE}, {GL_UNPACK_SWAP_BYTES, GL_TRUE},
|
|
||||||
{GL_UNPACK_LSB_FIRST, GL_FALSE}, {GL_UNPACK_LSB_FIRST, GL_TRUE},
|
|
||||||
{GL_PACK_SWAP_BYTES, GL_FALSE}, {GL_PACK_SWAP_BYTES, GL_TRUE},
|
|
||||||
{GL_PACK_LSB_FIRST, GL_FALSE}, {GL_PACK_LSB_FIRST, GL_TRUE},
|
|
||||||
{GL_PACK_IMAGE_HEIGHT, 0}, {GL_PACK_IMAGE_HEIGHT, 10},
|
|
||||||
{GL_PACK_IMAGE_HEIGHT, 15}, {GL_PACK_SKIP_IMAGES, 0},
|
|
||||||
{GL_PACK_SKIP_IMAGES, 1}, {GL_PACK_SKIP_IMAGES, 2},
|
|
||||||
};
|
|
||||||
|
|
||||||
std::size_t ImageBytes(int size) { return static_cast<std::size_t>(size) * size * 4; }
|
|
||||||
|
|
||||||
// Padded to kScratchBytes so it is safe to hand to an upload running under any of the
|
|
||||||
// sweep's stride/skip settings.
|
|
||||||
std::vector<std::uint8_t> MakeGradient(int size, unsigned seed) {
|
|
||||||
std::vector<std::uint8_t> pixels(kScratchBytes, 0);
|
|
||||||
for (int y = 0; y < size; ++y) {
|
|
||||||
for (int x = 0; x < size; ++x) {
|
|
||||||
const std::size_t base = (static_cast<std::size_t>(y) * size + x) * 4;
|
|
||||||
pixels[base + 0] = static_cast<std::uint8_t>((x * 11 + seed) & 0xFF);
|
|
||||||
pixels[base + 1] = static_cast<std::uint8_t>((y * 13 + seed) & 0xFF);
|
|
||||||
pixels[base + 2] = static_cast<std::uint8_t>((x * y + seed) & 0xFF);
|
|
||||||
pixels[base + 3] = 0xFF;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pixels;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ResetAllPixelStoreModes() {
|
|
||||||
for (const PixelStoreMode& mode : kAllModes) {
|
|
||||||
glPixelStorei(mode.name, mode.defaultValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The one operation the CTS repeats: a fresh texture, a fresh framebuffer, one readback,
|
|
||||||
// both deleted. Returns the readback; `outStatus` carries the completeness answer so a
|
|
||||||
// caller can tell an incomplete framebuffer apart from wrong pixels.
|
|
||||||
std::vector<std::uint8_t> UploadAndReadBack(const std::vector<std::uint8_t>& source, int size,
|
|
||||||
GLenum* outStatus) {
|
|
||||||
GLuint texture = 0;
|
|
||||||
glGenTextures(1, &texture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, texture);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, source.data());
|
|
||||||
|
|
||||||
GLuint fbo = 0;
|
|
||||||
glGenFramebuffers(1, &fbo);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
|
||||||
*outStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
|
||||||
|
|
||||||
std::vector<std::uint8_t> read(kScratchBytes, 0);
|
|
||||||
if (*outStatus == GL_FRAMEBUFFER_COMPLETE) {
|
|
||||||
glReadPixels(0, 0, size, size, GL_RGBA, GL_UNSIGNED_BYTE, read.data());
|
|
||||||
}
|
|
||||||
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
glDeleteFramebuffers(1, &fbo);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
glDeleteTextures(1, &texture);
|
|
||||||
return read;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Index of the first differing byte within the image, or `bytes` when they agree.
|
|
||||||
std::size_t FirstDifference(const std::vector<std::uint8_t>& a, const std::vector<std::uint8_t>& b,
|
|
||||||
std::size_t bytes) {
|
|
||||||
for (std::size_t i = 0; i < bytes; ++i) {
|
|
||||||
if (a[i] != b[i]) return i;
|
|
||||||
}
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
class PixelStoreSweepScenario : public ScenarioTest {};
|
|
||||||
class FramebufferChurnScenario : public ScenarioTest {};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
// Every mode in the CTS table is set, exercised and put back; the readback at default state
|
|
||||||
// afterwards must be bit-identical to the one taken before the sweep. A mode that silently
|
|
||||||
// fails to restore corrupts every later case in the batch, which is exactly how the CTS
|
|
||||||
// failures presented (the FIRST sub-case, at default state, is what failed).
|
|
||||||
TEST_F(PixelStoreSweepScenario, DefaultStateSurvivesTheFullModeSweep) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
|
|
||||||
ResetAllPixelStoreModes();
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << "resetting the pixel-store modes must be legal on a GL 4.0 context";
|
|
||||||
|
|
||||||
const std::vector<std::uint8_t> gradient = MakeGradient(kTexSize, 0);
|
|
||||||
GLenum status = 0;
|
|
||||||
const std::vector<std::uint8_t> baseline = UploadAndReadBack(gradient, kTexSize, &status);
|
|
||||||
ASSERT_EQ(status, static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
const std::vector<std::uint8_t> scratchSource(kScratchBytes, 0x5A);
|
|
||||||
|
|
||||||
for (const SweepCase& sweep : kSweep) {
|
|
||||||
glPixelStorei(sweep.mode, sweep.value);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << "glPixelStorei(0x" << std::hex << sweep.mode << std::dec << ", "
|
|
||||||
<< sweep.value << ") must be accepted";
|
|
||||||
|
|
||||||
// Exercise the mode: an upload and a readback that both run with it in force.
|
|
||||||
GLenum sweepStatus = 0;
|
|
||||||
(void)UploadAndReadBack(scratchSource, kTexSize, &sweepStatus);
|
|
||||||
|
|
||||||
ResetAllPixelStoreModes();
|
|
||||||
|
|
||||||
GLenum afterStatus = 0;
|
|
||||||
const std::vector<std::uint8_t> after = UploadAndReadBack(gradient, kTexSize, &afterStatus);
|
|
||||||
ASSERT_EQ(afterStatus, static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
|
||||||
const std::size_t diff = FirstDifference(baseline, after, ImageBytes(kTexSize));
|
|
||||||
ASSERT_EQ(diff, ImageBytes(kTexSize))
|
|
||||||
<< "default-state readback changed after setting and resetting 0x" << std::hex << sweep.mode
|
|
||||||
<< std::dec << " = " << sweep.value << "; first differing byte " << diff << " (baseline "
|
|
||||||
<< static_cast<int>(baseline[diff]) << ", now " << static_cast<int>(after[diff]) << ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
// And the modes themselves must read back as the defaults the reset asked for.
|
|
||||||
for (const PixelStoreMode& mode : kAllModes) {
|
|
||||||
GLint value = -1;
|
|
||||||
glGetIntegerv(mode.name, &value);
|
|
||||||
EXPECT_EQ(value, mode.defaultValue)
|
|
||||||
<< "pixel-store mode 0x" << std::hex << mode.name << std::dec << " did not return to its default";
|
|
||||||
}
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The leak regression. Each iteration is one complete CTS inner step, and every readback has
|
|
||||||
// to be exactly the gradient THIS iteration uploaded - never the previous one's. Before the
|
|
||||||
// missing destructors were added, the driver-side framebuffer count grew without bound here.
|
|
||||||
TEST_F(FramebufferChurnScenario, RepeatedFramebufferReadbackStaysExact) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
|
|
||||||
ResetAllPixelStoreModes();
|
|
||||||
constexpr int kSize = 8;
|
|
||||||
constexpr int kIterations = 1024;
|
|
||||||
|
|
||||||
for (int i = 0; i < kIterations; ++i) {
|
|
||||||
// A distinct gradient per iteration: a stale attachment or a recycled driver name
|
|
||||||
// reads back the PREVIOUS iteration's image, which a constant fill could not tell
|
|
||||||
// apart from a correct read.
|
|
||||||
const std::vector<std::uint8_t> gradient = MakeGradient(kSize, static_cast<unsigned>(i * 7 + 1));
|
|
||||||
GLenum status = 0;
|
|
||||||
const std::vector<std::uint8_t> read = UploadAndReadBack(gradient, kSize, &status);
|
|
||||||
ASSERT_EQ(status, static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE)) << "iteration " << i;
|
|
||||||
const std::size_t diff = FirstDifference(gradient, read, ImageBytes(kSize));
|
|
||||||
ASSERT_EQ(diff, ImageBytes(kSize))
|
|
||||||
<< "iteration " << i << " read back a different image than it uploaded; first differing byte "
|
|
||||||
<< diff << " (uploaded " << static_cast<int>(gradient[diff]) << ", read "
|
|
||||||
<< static_cast<int>(read[diff]) << ")";
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << "iteration " << i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SsboArrayLengthScenario.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 - length() ON AN SSBO's UNSIZED ARRAY.
|
|
||||||
//
|
|
||||||
// GLSL's `arr.length()` on the trailing runtime array of a shader storage block is not a compile
|
|
||||||
// time constant: it is (bound range - the array's byte offset inside the block) / array stride,
|
|
||||||
// evaluated against whatever the descriptor actually covers. Three separate pieces of MobileGL
|
|
||||||
// have to agree for that to come out right - the byte offsets the block layout was compiled with,
|
|
||||||
// the buffer the frontend binding resolves to, and the offset/size a glBindBufferRange asked for -
|
|
||||||
// and a defect in any one of them shows up only as a wrong integer, never as an error.
|
|
||||||
//
|
|
||||||
// KHR-GL43.shader_storage_buffer_object.advanced-unsizedArrayLength-* (28 Magma failures, all 28
|
|
||||||
// passing on Espryt) reports exactly that: lengths too large by roughly the size of the members
|
|
||||||
// preceding the array. The cases here are the same shape, reduced to what can be asserted in one
|
|
||||||
// dispatch: a block with no preamble, a block with one, a two-element ARRAY OF BLOCKS (which
|
|
||||||
// consumes two consecutive bindings and is where the conformance failures concentrate), and the
|
|
||||||
// two glBindBufferRange forms.
|
|
||||||
//
|
|
||||||
// Every length is written into one output SSBO and read back, so a failure names the block and
|
|
||||||
// prints the number the shader saw.
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "../Harness/HeadlessGL.h"
|
|
||||||
#include "../Harness/ScenarioFixture.h"
|
|
||||||
|
|
||||||
#ifdef GLAPI
|
|
||||||
#undef GLAPI
|
|
||||||
#endif
|
|
||||||
#define GL_GLEXT_PROTOTYPES
|
|
||||||
#include <GL/gl.h>
|
|
||||||
#include <GL/glcorearb.h>
|
|
||||||
#undef GL_GLEXT_PROTOTYPES
|
|
||||||
|
|
||||||
namespace MGITest {
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// Bindings 0..3 are inputs (2 and 3 are the block array), 4 is the output.
|
|
||||||
constexpr const char* kComputeSource = R"(#version 430 core
|
|
||||||
layout(local_size_x = 1) in;
|
|
||||||
layout(std430, binding = 0) readonly buffer Input0 {
|
|
||||||
ivec4 g_input0[];
|
|
||||||
};
|
|
||||||
layout(std430, binding = 1) readonly buffer Input1 {
|
|
||||||
ivec4 pad1;
|
|
||||||
ivec4 data[];
|
|
||||||
} g_input1;
|
|
||||||
layout(std430, binding = 2) readonly buffer Input23 {
|
|
||||||
ivec4 data[];
|
|
||||||
} g_input23[2];
|
|
||||||
layout(std430, binding = 4) buffer Output {
|
|
||||||
int g_length[];
|
|
||||||
};
|
|
||||||
void main() {
|
|
||||||
g_length[0] = g_input0.length();
|
|
||||||
g_length[1] = g_input1.data.length();
|
|
||||||
g_length[2] = g_input23[0].data.length();
|
|
||||||
g_length[3] = g_input23[1].data.length();
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
constexpr int kElementBytes = 16; // ivec4, std430
|
|
||||||
|
|
||||||
class SsboArrayLengthScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
GLint blocks = 0;
|
|
||||||
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &blocks);
|
|
||||||
if (blocks < 5) {
|
|
||||||
GTEST_SKIP() << "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS is " << blocks << "; this needs 5";
|
|
||||||
}
|
|
||||||
m_program = CompileComputeProgram(kComputeSource);
|
|
||||||
ASSERT_NE(m_program, 0u) << m_buildLog;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
if (!Ready()) return;
|
|
||||||
if (!m_buffers.empty()) glDeleteBuffers(static_cast<GLsizei>(m_buffers.size()), m_buffers.data());
|
|
||||||
if (m_program != 0) glDeleteProgram(m_program);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned int CompileComputeProgram(const char* source) {
|
|
||||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
|
||||||
glShaderSource(shader, 1, &source, nullptr);
|
|
||||||
glCompileShader(shader);
|
|
||||||
GLint compiled = 0;
|
|
||||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
|
||||||
if (compiled == GL_FALSE) {
|
|
||||||
char log[2048] = {};
|
|
||||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
|
||||||
m_buildLog = std::string("compute shader did not compile: ") + log;
|
|
||||||
glDeleteShader(shader);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
glAttachShader(program, shader);
|
|
||||||
glLinkProgram(program);
|
|
||||||
glDeleteShader(shader);
|
|
||||||
GLint linked = 0;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
if (linked == GL_FALSE) {
|
|
||||||
char log[2048] = {};
|
|
||||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
|
||||||
m_buildLog = std::string("compute program did not link: ") + log;
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A buffer of `elements` ivec4s, filled with a recognisable pattern.
|
|
||||||
GLuint MakeStorageBuffer(int elements) {
|
|
||||||
std::vector<int> contents(static_cast<std::size_t>(elements) * 4, 41);
|
|
||||||
GLuint buffer = 0;
|
|
||||||
glGenBuffers(1, &buffer);
|
|
||||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
|
||||||
glBufferData(GL_SHADER_STORAGE_BUFFER,
|
|
||||||
static_cast<GLsizeiptr>(elements) * kElementBytes, contents.data(), GL_DYNAMIC_COPY);
|
|
||||||
m_buffers.push_back(buffer);
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatches once and returns the four lengths the shader observed.
|
|
||||||
std::vector<int> RunAndReadLengths(GLuint outputBuffer) {
|
|
||||||
glUseProgram(m_program);
|
|
||||||
glDispatchCompute(1, 1, 1);
|
|
||||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
|
||||||
std::vector<int> lengths(4, -1);
|
|
||||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, outputBuffer);
|
|
||||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
|
|
||||||
static_cast<GLsizeiptr>(lengths.size() * sizeof(int)), lengths.data());
|
|
||||||
return lengths;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned int m_program = 0;
|
|
||||||
std::string m_buildLog;
|
|
||||||
std::vector<GLuint> m_buffers;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
// glBindBufferBase everywhere: the plain case, and the one that pins the block array.
|
|
||||||
TEST_F(SsboArrayLengthScenario, WholeBufferBindingsReportTheElementCount) {
|
|
||||||
if (!Ready() || IsSkipped()) return;
|
|
||||||
|
|
||||||
// input1 carries one ivec4 of preamble before its runtime array, so a length that ignores
|
|
||||||
// the member offset comes back one too large there and only there.
|
|
||||||
const GLuint input0 = MakeStorageBuffer(7);
|
|
||||||
const GLuint input1 = MakeStorageBuffer(1 + 5);
|
|
||||||
const GLuint input2 = MakeStorageBuffer(3);
|
|
||||||
const GLuint input3 = MakeStorageBuffer(4);
|
|
||||||
const GLuint output = MakeStorageBuffer(4);
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, input0);
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, input1);
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, input2);
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, input3);
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, output);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
const std::vector<int> lengths = RunAndReadLengths(output);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
EXPECT_EQ(lengths[0], 7) << "Input0 (no preamble, 7 elements) reported length " << lengths[0];
|
|
||||||
EXPECT_EQ(lengths[1], 5) << "Input1 (1 ivec4 of preamble, 6 elements of storage) reported length "
|
|
||||||
<< lengths[1] << "; 6 means the array's byte offset inside the block was ignored";
|
|
||||||
EXPECT_EQ(lengths[2], 3) << "Input23[0] (binding 2, 3 elements) reported length " << lengths[2];
|
|
||||||
EXPECT_EQ(lengths[3], 4) << "Input23[1] (binding 3, 4 elements) reported length " << lengths[3]
|
|
||||||
<< "; a block array's second element must resolve to the NEXT binding";
|
|
||||||
}
|
|
||||||
|
|
||||||
// glBindBufferRange with a non-zero offset: length() must see only the bound window.
|
|
||||||
TEST_F(SsboArrayLengthScenario, RangeBindingsReportTheBoundWindow) {
|
|
||||||
if (!Ready() || IsSkipped()) return;
|
|
||||||
|
|
||||||
GLint alignment = 1;
|
|
||||||
glGetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &alignment);
|
|
||||||
if (alignment > 2 * kElementBytes) {
|
|
||||||
GTEST_SKIP() << "GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT is " << alignment
|
|
||||||
<< "; a two-element offset cannot be expressed";
|
|
||||||
}
|
|
||||||
|
|
||||||
const GLuint input0 = MakeStorageBuffer(7);
|
|
||||||
const GLuint input1 = MakeStorageBuffer(1 + 5);
|
|
||||||
const GLuint input2 = MakeStorageBuffer(3);
|
|
||||||
const GLuint input3 = MakeStorageBuffer(4);
|
|
||||||
const GLuint output = MakeStorageBuffer(4);
|
|
||||||
// Input0: window starts two elements in, so 5 remain.
|
|
||||||
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, input0, 2 * kElementBytes, 5 * kElementBytes);
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, input1);
|
|
||||||
// Both elements of the block array get a window, so a failure says whether the array's
|
|
||||||
// FIRST element is handled and only the later ones are lost, or neither is.
|
|
||||||
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 2, input2, 0, 2 * kElementBytes);
|
|
||||||
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 3, input3, 0, 2 * kElementBytes);
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, output);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
const std::vector<int> lengths = RunAndReadLengths(output);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
EXPECT_EQ(lengths[0], 5) << "Input0 bound as [2 elements, 5 elements) reported length " << lengths[0]
|
|
||||||
<< "; 7 means glBindBufferRange's offset/size never reached the descriptor";
|
|
||||||
EXPECT_EQ(lengths[2], 2) << "Input23[0] bound as [0, 2 elements) reported length " << lengths[2];
|
|
||||||
EXPECT_EQ(lengths[3], 2) << "Input23[1] bound as [0, 2 elements) reported length " << lengths[3];
|
|
||||||
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, input0);
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, input3);
|
|
||||||
}
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SwizzleAccessRoutineScenario.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 - EVERY TEXTURE ACCESS ROUTINE READS THE SAME TEXEL OUT OF A usampler2DArray.
|
|
||||||
//
|
|
||||||
// KHR-GL33/GL40.texture_swizzle.smoke_access_idx_* sweeps the fourteen GLSL texture access
|
|
||||||
// routines against a 1x1x1 GL_RGBA32UI GL_TEXTURE_2D_ARRAY and asserts the fetched channel. On
|
|
||||||
// Espryt, `texture` and `textureGrad` pass while `textureLod`, `textureOffset`, `texelFetch`,
|
|
||||||
// `texelFetchOffset` and `textureLodOffset` fail - 21 cases per version, 42 across GL33 and GL40.
|
|
||||||
// The discriminator is the important part: the swizzle state is IDENTICAL across all of them, so
|
|
||||||
// swizzle delivery is not the defect; what differs is only how the routine is spelled, i.e. what
|
|
||||||
// SPIRV-Cross has to emit into ESSL for it.
|
|
||||||
//
|
|
||||||
// This scenario is that discriminator, reduced to something that fails in milliseconds: one draw
|
|
||||||
// per access routine against the same texture and the same swizzle, all reading the same texel.
|
|
||||||
// A routine that disagrees with the others is the defect, and the failure message names it.
|
|
||||||
//
|
|
||||||
// The shader shape is copied from the conformance test rather than idealised - including its
|
|
||||||
// `int(0)` level-of-detail argument, which is a desktop-GLSL implicit int->float conversion that
|
|
||||||
// ESSL does not have, and its zero offsets. Both are exactly the things a GLSL -> SPIR-V -> ESSL
|
|
||||||
// round trip can lose.
|
|
||||||
//
|
|
||||||
// DirectVulkan is the built-in control: it consumes the SPIR-V directly and never runs the ESSL
|
|
||||||
// emission, so a failure there would mean the scenario, not the backend.
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#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 {
|
|
||||||
|
|
||||||
// The conformance test's own source texel, one recognisable value per channel.
|
|
||||||
constexpr std::uint32_t kSourceTexel[4] = {0x3FFFFFFFu, 0x7FFFFFFFu, 0xBFFFFFFFu, 0xFFFFFFFFu};
|
|
||||||
|
|
||||||
constexpr int kOutputWidth = 8;
|
|
||||||
constexpr int kOutputHeight = 8;
|
|
||||||
|
|
||||||
// The blank vertex shader the smoke test uses: a full-viewport strip with no attributes.
|
|
||||||
constexpr const char* kVertexSource = R"(#version 330 core
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
switch (gl_VertexID)
|
|
||||||
{
|
|
||||||
case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
|
||||||
case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
|
||||||
case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break;
|
|
||||||
case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
struct AccessRoutine {
|
|
||||||
const char* name; // as it appears in the conformance case name
|
|
||||||
const char* callText; // the whole TEXTURE_ACCESS(sampler, ARGUMENTS) expression
|
|
||||||
};
|
|
||||||
|
|
||||||
// Spelled exactly as gl3cTextureSwizzleTests.cpp's prepareArguments builds them for
|
|
||||||
// GL_TEXTURE_2D_ARRAY: three coordinates, `int(0)` for the level, ivec2 offsets.
|
|
||||||
constexpr AccessRoutine kRoutines[] = {
|
|
||||||
{"texture", "texture(smp, vec3(0, 0, 0))"},
|
|
||||||
{"textureLod", "textureLod(smp, vec3(0, 0, 0), int(0))"},
|
|
||||||
{"textureOffset", "textureOffset(smp, vec3(0, 0, 0), ivec2(0, 0))"},
|
|
||||||
{"texelFetch", "texelFetch(smp, ivec3(0, 0, 0), int(0))"},
|
|
||||||
{"texelFetchOffset", "texelFetchOffset(smp, ivec3(0, 0, 0), int(0), ivec2(0, 0))"},
|
|
||||||
{"textureLodOffset", "textureLodOffset(smp, vec3(0, 0, 0), int(0), ivec2(0, 0))"},
|
|
||||||
{"textureGrad", "textureGrad(smp, vec3(0, 0, 0), vec2(0, 0), vec2(0, 0))"},
|
|
||||||
{"textureGradOffset", "textureGradOffset(smp, vec3(0, 0, 0), vec2(0, 0), vec2(0, 0), ivec2(0, 0))"},
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr const char* kChannels[4] = {"x", "y", "z", "w"};
|
|
||||||
|
|
||||||
std::string FragmentSource(const AccessRoutine& routine, int channel) {
|
|
||||||
return std::string("#version 330 core\n\nuniform usampler2DArray smp;\n\nout uint out_color;\n\n"
|
|
||||||
"void main()\n{\n uint result = ") +
|
|
||||||
routine.callText + "." + kChannels[channel] + ";\n\n out_color = result;\n}\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
class SwizzleAccessRoutineScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
|
|
||||||
// 1x1x1 RGBA32UI 2D array. Integer textures are not filterable, so NEAREST is
|
|
||||||
// mandatory, and a single level means every LOD argument must resolve to 0.
|
|
||||||
glGenTextures(1, &m_sourceTexture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D_ARRAY, m_sourceTexture);
|
|
||||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA32UI, 1, 1, 1);
|
|
||||||
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, 1, 1, 1, GL_RGBA_INTEGER, GL_UNSIGNED_INT,
|
|
||||||
kSourceTexel);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << "source texture setup left a GL error behind";
|
|
||||||
|
|
||||||
// 8x8 R32UI render target, read back with glReadPixels.
|
|
||||||
glGenTextures(1, &m_outputTexture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, m_outputTexture);
|
|
||||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32UI, kOutputWidth, kOutputHeight);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
||||||
glGenFramebuffers(1, &m_fbo);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_outputTexture, 0);
|
|
||||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
|
||||||
glGenVertexArrays(1, &m_vao);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << "output framebuffer setup left a GL error behind";
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
if (!Ready()) return;
|
|
||||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
|
||||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
|
||||||
if (m_outputTexture != 0) glDeleteTextures(1, &m_outputTexture);
|
|
||||||
if (m_sourceTexture != 0) glDeleteTextures(1, &m_sourceTexture);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SetSwizzle(GLenum r, GLenum g, GLenum b, GLenum a) {
|
|
||||||
glBindTexture(GL_TEXTURE_2D_ARRAY, m_sourceTexture);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_SWIZZLE_R, static_cast<GLint>(r));
|
|
||||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_SWIZZLE_G, static_cast<GLint>(g));
|
|
||||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_SWIZZLE_B, static_cast<GLint>(b));
|
|
||||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_SWIZZLE_A, static_cast<GLint>(a));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Renders one access routine into the 8x8 target and returns every texel it wrote.
|
|
||||||
// Returns an empty vector (with a gtest failure already recorded) if the program did
|
|
||||||
// not build.
|
|
||||||
std::vector<std::uint32_t> Render(const AccessRoutine& routine, int channel) {
|
|
||||||
const std::string fragment = FragmentSource(routine, channel);
|
|
||||||
std::string error;
|
|
||||||
const unsigned int program = CompileProgram(kVertexSource, fragment.c_str(), &error);
|
|
||||||
if (program == 0) {
|
|
||||||
ADD_FAILURE() << routine.name << " channel " << kChannels[channel]
|
|
||||||
<< ": program did not build: " << error << "\n--- source ---\n"
|
|
||||||
<< fragment;
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
|
||||||
glViewport(0, 0, kOutputWidth, kOutputHeight);
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
|
||||||
glDisable(GL_DEPTH_TEST);
|
|
||||||
const GLuint clearValue[4] = {0xDEADBEEFu, 0u, 0u, 0u};
|
|
||||||
glClearBufferuiv(GL_COLOR, 0, clearValue);
|
|
||||||
|
|
||||||
glUseProgram(program);
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
|
||||||
glBindTexture(GL_TEXTURE_2D_ARRAY, m_sourceTexture);
|
|
||||||
const GLint location = glGetUniformLocation(program, "smp");
|
|
||||||
glUniform1i(location, 0);
|
|
||||||
glBindVertexArray(m_vao);
|
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
|
||||||
glBindVertexArray(0);
|
|
||||||
|
|
||||||
std::vector<std::uint32_t> texels(static_cast<std::size_t>(kOutputWidth) * kOutputHeight, 0);
|
|
||||||
glReadPixels(0, 0, kOutputWidth, kOutputHeight, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
|
|
||||||
glUseProgram(0);
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return texels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Asserts every texel equals `expected`, naming the routine and the first offender.
|
|
||||||
void ExpectAllTexels(const AccessRoutine& routine, int channel, std::uint32_t expected,
|
|
||||||
const std::vector<std::uint32_t>& texels) {
|
|
||||||
if (texels.empty()) return;
|
|
||||||
std::size_t offenders = 0;
|
|
||||||
std::uint32_t firstBad = 0;
|
|
||||||
std::size_t firstIndex = 0;
|
|
||||||
for (std::size_t i = 0; i < texels.size(); ++i) {
|
|
||||||
if (texels[i] == expected) continue;
|
|
||||||
if (offenders == 0) {
|
|
||||||
firstBad = texels[i];
|
|
||||||
firstIndex = i;
|
|
||||||
}
|
|
||||||
++offenders;
|
|
||||||
}
|
|
||||||
EXPECT_EQ(offenders, 0u)
|
|
||||||
<< routine.name << "(...)." << kChannels[channel] << " returned 0x" << std::hex << firstBad
|
|
||||||
<< " instead of 0x" << expected << std::dec << " at texel " << firstIndex << " (" << offenders
|
|
||||||
<< " of " << texels.size() << " wrong)";
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint m_sourceTexture = 0;
|
|
||||||
GLuint m_outputTexture = 0;
|
|
||||||
GLuint m_fbo = 0;
|
|
||||||
GLuint m_vao = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
// Identity swizzle: every routine must fetch the channel it was asked for. This is the
|
|
||||||
// scenario's floor - it does not involve swizzling at all, so a failure here is purely about
|
|
||||||
// how the access routine itself survives the trip to the backend.
|
|
||||||
TEST_F(SwizzleAccessRoutineScenario, EveryAccessRoutineFetchesTheSameTexelUnderTheIdentitySwizzle) {
|
|
||||||
if (!Ready() || IsSkipped()) return;
|
|
||||||
SetSwizzle(GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
for (const AccessRoutine& routine : kRoutines) {
|
|
||||||
for (int channel = 0; channel < 4; ++channel) {
|
|
||||||
const std::vector<std::uint32_t> texels = Render(routine, channel);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u) << routine.name << " left a GL error behind";
|
|
||||||
ExpectAllTexels(routine, channel, kSourceTexel[channel], texels);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Gl().EndFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
// A real swizzle, applied to every routine. Reversing the channels means a routine that
|
|
||||||
// silently drops the swizzle returns the UNSWIZZLED texel rather than nothing, so the
|
|
||||||
// failure distinguishes "swizzle lost" from "fetch broken".
|
|
||||||
TEST_F(SwizzleAccessRoutineScenario, EveryAccessRoutineSeesAReversedSwizzle) {
|
|
||||||
if (!Ready() || IsSkipped()) return;
|
|
||||||
SetSwizzle(GL_ALPHA, GL_BLUE, GL_GREEN, GL_RED);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
const std::uint32_t expected[4] = {kSourceTexel[3], kSourceTexel[2], kSourceTexel[1], kSourceTexel[0]};
|
|
||||||
for (const AccessRoutine& routine : kRoutines) {
|
|
||||||
for (int channel = 0; channel < 4; ++channel) {
|
|
||||||
const std::vector<std::uint32_t> texels = Render(routine, channel);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u) << routine.name << " left a GL error behind";
|
|
||||||
ExpectAllTexels(routine, channel, expected[channel], texels);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Gl().EndFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Program churn: the shape that made the conformance suite fail, reduced.
|
|
||||||
//
|
|
||||||
// The swizzle smoke test builds one program per swizzle combination - 1,296 per case - and
|
|
||||||
// DirectGLES created a driver shader object per attached shader without ever calling
|
|
||||||
// glDeleteShader. glDeleteShader only FLAGS a shader for deletion (the driver frees it once
|
|
||||||
// nothing has it attached), so without that call the program's own deletion could not free
|
|
||||||
// them either: eight cases left ~20,000 live driver shaders behind, the Adreno ES driver
|
|
||||||
// passed its ceiling, and it began mis-serving shaders - first the sampling variants with the
|
|
||||||
// most image operands (textureLod/texelFetch/*Offset), while plain texture/textureGrad still
|
|
||||||
// worked. On device this loop plus a value check is the whole defect.
|
|
||||||
//
|
|
||||||
// HONEST LIMIT OF THIS TEST: llvmpipe has no such ceiling, so this passes here whether or not
|
|
||||||
// the leak is present - it cannot fail on the CI lane. It is a standing guard for the SHAPE
|
|
||||||
// (build many programs, keep reading the right texel) and the place to raise the iteration
|
|
||||||
// count if a driver ceiling ever needs reproducing; the leak itself is pinned by device
|
|
||||||
// measurement (VmRSS flat at ~137 MB across the 32-case family, against 132 -> 154 MB and
|
|
||||||
// still climbing before the fix).
|
|
||||||
TEST_F(SwizzleAccessRoutineScenario, RepeatedProgramBuildsKeepFetchingTheSameTexel) {
|
|
||||||
if (!Ready() || IsSkipped()) return;
|
|
||||||
SetSwizzle(GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
// One routine from each side of the device's failure order, so a ceiling that takes the
|
|
||||||
// vulnerable one down first is still caught.
|
|
||||||
const AccessRoutine& plain = kRoutines[0]; // texture
|
|
||||||
const AccessRoutine& explicitLod = kRoutines[1]; // textureLod
|
|
||||||
constexpr int kIterations = 200;
|
|
||||||
|
|
||||||
for (int i = 0; i < kIterations; ++i) {
|
|
||||||
const AccessRoutine& routine = (i % 2 == 0) ? plain : explicitLod;
|
|
||||||
const int channel = i % 4;
|
|
||||||
const std::vector<std::uint32_t> texels = Render(routine, channel);
|
|
||||||
if (::testing::Test::HasFailure()) return; // a build failure repeats 200 times; say it once
|
|
||||||
ExpectAllTexels(routine, channel, kSourceTexel[channel], texels);
|
|
||||||
if (::testing::Test::HasFailure()) {
|
|
||||||
ADD_FAILURE() << "diverged at iteration " << i << " of " << kIterations;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u) << "the churn loop left a GL error behind";
|
|
||||||
Gl().EndFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
// GL_ONE and GL_ZERO, which the conformance table spells as the literal values 1 and 0 and
|
|
||||||
// which the backend has to synthesise rather than fetch.
|
|
||||||
TEST_F(SwizzleAccessRoutineScenario, EveryAccessRoutineSeesConstantSwizzleSources) {
|
|
||||||
if (!Ready() || IsSkipped()) return;
|
|
||||||
SetSwizzle(GL_ONE, GL_ZERO, GL_ONE, GL_ZERO);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
const std::uint32_t expected[4] = {1u, 0u, 1u, 0u};
|
|
||||||
for (const AccessRoutine& routine : kRoutines) {
|
|
||||||
for (int channel = 0; channel < 4; ++channel) {
|
|
||||||
const std::vector<std::uint32_t> texels = Render(routine, channel);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u) << routine.name << " left a GL error behind";
|
|
||||||
ExpectAllTexels(routine, channel, expected[channel], texels);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Gl().EndFrame();
|
|
||||||
}
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -380,14 +380,8 @@ namespace MobileGL::MG_State {
|
|||||||
// inside the same draw when it finally touched an artifact, and cache under a
|
// 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 the publish had already superseded. Settling here means every
|
||||||
// version a backend reads during a draw describes the program it is drawing.
|
// version a backend reads during a draw describes the program it is drawing.
|
||||||
// Two null checks in steady state.
|
// One null check in steady state.
|
||||||
//
|
currentProgram->JoinLink();
|
||||||
// BOTH phases, and that is not optional: the phase-B publish bumps those same
|
|
||||||
// versions, so joining only phase A here would leave exactly the hazard this
|
|
||||||
// site exists to close - a backend samples a version, then trips the phase-B
|
|
||||||
// gate through GetGeneratedSpirv() deeper inside the same draw, and memoizes
|
|
||||||
// under a version the publish has already superseded.
|
|
||||||
currentProgram->JoinLinkAndSpirv();
|
|
||||||
return currentProgram;
|
return currentProgram;
|
||||||
}
|
}
|
||||||
if (m_boundProgramPipeline == 0) return nullProgram;
|
if (m_boundProgramPipeline == 0) return nullProgram;
|
||||||
@@ -404,7 +398,7 @@ namespace MobileGL::MG_State {
|
|||||||
// programs. In steady state this is a null check per stage.
|
// programs. In steady state this is a null check per stage.
|
||||||
for (SizeT stage = 0; stage < static_cast<SizeT>(ShaderStage::ShaderStageCount); ++stage) {
|
for (SizeT stage = 0; stage < static_cast<SizeT>(ShaderStage::ShaderStageCount); ++stage) {
|
||||||
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
|
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
|
||||||
if (stageProgram) stageProgram->JoinLinkAndSpirv();
|
if (stageProgram) stageProgram->JoinLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto signature = pipeline->ComputeDrawProgramSignature();
|
const auto signature = pipeline->ComputeDrawProgramSignature();
|
||||||
@@ -436,9 +430,8 @@ namespace MobileGL::MG_State {
|
|||||||
composite->Link(true);
|
composite->Link(true);
|
||||||
// P1 join site J2. The draw that asked for this program is the very next thing to
|
// 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
|
// happen, so enqueueing the composite's link buys nothing and only moves the wait
|
||||||
// to whichever backend accessor happens to touch its artifacts first. Both phases,
|
// to whichever backend accessor happens to touch its artifacts first.
|
||||||
// for the same reason: the backend is about to read its SPIR-V.
|
composite->JoinLink();
|
||||||
composite->JoinLinkAndSpirv();
|
|
||||||
pipeline->SetCachedDrawProgram(signature, Move(composite));
|
pipeline->SetCachedDrawProgram(signature, Move(composite));
|
||||||
return pipeline->GetCachedDrawProgram(signature);
|
return pipeline->GetCachedDrawProgram(signature);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -361,53 +361,29 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- everything below this line up to GenerateSpirv() is the GL query surface ----
|
// SPIR-V must be generated BEFORE buildReflection touches artifacts.program:
|
||||||
//
|
// reflection's live-variable analysis mutates the intermediates in ways that
|
||||||
// ORDERING NOTE (rewritten 2026-08-10; the constraint it records was RETESTED, not
|
// change subsequent GlslangToSpv output (observed: catastrophic uniform
|
||||||
// dropped on a hunch). This block used to insist that SPIR-V be generated BEFORE
|
// misbinding on DirectVulkan for UBO-heavy content). The old two-link pipeline
|
||||||
// buildReflection touches artifacts.program, on the grounds that reflection's
|
// never ran buildReflection on the SPIR-V-producing program; this order keeps
|
||||||
// live-variable analysis mutates the shared intermediates in ways that change
|
// that property with the single link. The glUniform*-to-scratch routing
|
||||||
// subsequent GlslangToSpv output - "observed: catastrophic uniform misbinding on
|
// tables, in contrast, are sized and keyed by reflection results, so they are
|
||||||
// DirectVulkan for UBO-heavy content", recorded with commit 0d052719.
|
// built strictly AFTER DoReflection. (Everything else on the reflection
|
||||||
//
|
// surface - locations, sampler units, block bindings/sizes - was measured
|
||||||
// Re-measured on the glslang pin this tree vendors, with the same method 0d052719
|
// identical in either order.)
|
||||||
// used (per-module SPIR-V hashes, both orders, byte-compared): 636 modules across
|
MGLOG_D("ProgramObject %u: Starting SPIR-V generation", in.externalIndex);
|
||||||
// 320 programs - the whole extracted trace corpus (BSL, Complementary Reimagined,
|
GenerateSpirv();
|
||||||
// IterationRP, Create/Flywheel) plus adversarial synthetics - came out BYTE-IDENTICAL
|
|
||||||
// in both orders, pre-optimize and post-optimize alike. glslang's code structure
|
|
||||||
// agrees: reflection.cpp performs no AST write (no getWritableType, no const_cast, no
|
|
||||||
// qualifier assignment) and GlslangToSpv takes a const TIntermediate&.
|
|
||||||
//
|
|
||||||
// Confirmed a third time ON DEVICE, 2026-08-11, and this one closes the gap the
|
|
||||||
// desktop A/B could not: the corpus replays captured SOURCES, so it never reproduced
|
|
||||||
// Iris's glBindAttribLocation-before-link flow, which is what drives the io-resolver
|
|
||||||
// that assigns vertex-input Locations. A Complementary Reimagined pack load on an
|
|
||||||
// Adreno 830 was dumped at the pipeline the driver rejects (programHash
|
|
||||||
// 0x4a7e9a37fb49caa1) under BOTH orders and under the pre-split build 6ea94877: all
|
|
||||||
// three dumps are the same bytes (md5 39ffa10d5186a4d37be82d0b42297a8d). The order
|
|
||||||
// does not perturb SPIR-V on this pin, including on the exact flow 0d052719 feared.
|
|
||||||
//
|
|
||||||
// Not a licence to stop measuring: 0d052719's observation was real once, and the
|
|
||||||
// method (per-module hashes, both orders) is cheap. Re-run it on any glslang bump.
|
|
||||||
//
|
|
||||||
// So the order is now the other way round, and deliberately: reflection, fragment
|
|
||||||
// output validation and transform-feedback resolution are what the GL query surface
|
|
||||||
// is made of, and they are also the only remaining ways a link can FAIL, so running
|
|
||||||
// them first is what lets LINK_STATUS and every query behind it become final without
|
|
||||||
// waiting for SPIR-V (and stops a program that fails validation from paying for
|
|
||||||
// ~68 s/pack-load of SPIR-V generation it is about to throw away).
|
|
||||||
//
|
|
||||||
// What has NOT changed: the routing tables are sized and keyed by reflection results
|
|
||||||
// AND read the OPTIMIZED SPIR-V, so BuildGlobalUboRouting still runs strictly after
|
|
||||||
// both DoReflection and GenerateSpirv.
|
|
||||||
MGLOG_D("ProgramObject %u: Starting reflection", in.externalIndex);
|
MGLOG_D("ProgramObject %u: Starting reflection", in.externalIndex);
|
||||||
if (!DoReflection(env)) {
|
if (!DoReflection(env)) {
|
||||||
DeferLog(std::format("ProgramObject {}: Link failed during reflection: {}", in.externalIndex,
|
DeferLog(std::format("ProgramObject {}: Link failed during reflection: {}", in.externalIndex,
|
||||||
artifacts.infoLog));
|
artifacts.infoLog));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
MGLOG_D("ProgramObject %u: Reflection done (linkStatus=%d)", in.externalIndex, (int)artifacts.linkStatus);
|
|
||||||
|
|
||||||
|
MGLOG_D("ProgramObject %u: Building global-UBO routing tables", in.externalIndex);
|
||||||
|
BuildGlobalUboRouting();
|
||||||
|
MGLOG_D("ProgramObject %u: Reflection done (linkStatus=%d)", in.externalIndex, (int)artifacts.linkStatus);
|
||||||
if (!ValidateFragmentOutputLocations()) {
|
if (!ValidateFragmentOutputLocations()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -417,52 +393,13 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
in.externalIndex, artifacts.infoLog));
|
in.externalIndex, artifacts.infoLog));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
MGLOG_D("ProgramObject %u: Binary generation finished (generatedSpirv size=%zu)", in.externalIndex,
|
||||||
// ---- past this point the link cannot fail any more ----
|
artifacts.generatedSpirv.size());
|
||||||
// Everything left is SPIR-V work, and it belongs to phase B. Hand it what it needs
|
|
||||||
// and stop: from the join's point of view this program is now fully linked.
|
|
||||||
//
|
|
||||||
// The TShaders move rather than copy - `attrib` borrowed them into the TProgram as
|
|
||||||
// raw pointers and this node is now their owner of record, for as long as phase B
|
|
||||||
// (which holds this node) needs the intermediates hanging off them.
|
|
||||||
spirvHandoff.shaders = Move(attrib.shaders);
|
|
||||||
spirvHandoff.shaderTypes.resize(in.shaders.size());
|
|
||||||
for (SizeT i = 0; i < in.shaders.size(); i++) {
|
|
||||||
spirvHandoff.shaderTypes[i] = MG_Util::ConvertShaderStageToGLEnum(in.shaders[i].stage);
|
|
||||||
}
|
|
||||||
// Copied, not referenced: `artifacts` is MOVED out of this node by the join, and
|
|
||||||
// phase B runs after that. Measured at ~20 us per program, which is noise against the
|
|
||||||
// ~450 ms phase B spends on the same program.
|
|
||||||
spirvHandoff.reflection.program = artifacts.program;
|
|
||||||
spirvHandoff.reflection.uniformLocations = artifacts.uniformLocations;
|
|
||||||
spirvHandoff.reflection.uniformIndexInTProgram = artifacts.uniformIndexInTProgram;
|
|
||||||
spirvHandoff.reflection.tProgramUniformIndexToGl = artifacts.tProgramUniformIndexToGl;
|
|
||||||
spirvHandoff.reflection.maxUniformLocation = artifacts.maxUniformLocation;
|
|
||||||
spirvHandoff.ready = true;
|
|
||||||
MGLOG_D("ProgramObject %u: phase A done, %zu module(s) handed to the SPIR-V job", in.externalIndex,
|
|
||||||
spirvHandoff.shaderTypes.size());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ProgramLinkTask::ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders) {
|
Bool ProgramLinkTask::ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders) {
|
||||||
outShaders.assign(in.shaders.size(), nullptr);
|
outShaders.assign(in.shaders.size(), nullptr);
|
||||||
|
|
||||||
// GL 4.6 core 7.3: a compute shader may only be linked with other compute shaders -
|
|
||||||
// the compute pipeline has no other stages to link against, so a program that mixes
|
|
||||||
// them must fail to link (KHR-GL43.compute_shader.api-program).
|
|
||||||
{
|
|
||||||
Bool hasCompute = false;
|
|
||||||
Bool hasNonCompute = false;
|
|
||||||
for (const LinkShaderInput& input : in.shaders) {
|
|
||||||
(input.stage == ShaderStage::Compute ? hasCompute : hasNonCompute) = true;
|
|
||||||
}
|
|
||||||
if (hasCompute && hasNonCompute) {
|
|
||||||
artifacts.infoLog =
|
|
||||||
"A compute shader cannot be linked with shaders of any other stage.";
|
|
||||||
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (SizeT i = 0; i < in.shaders.size(); i++) {
|
for (SizeT i = 0; i < in.shaders.size(); i++) {
|
||||||
const LinkShaderInput& input = in.shaders[i];
|
const LinkShaderInput& input = in.shaders[i];
|
||||||
const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(input.stage);
|
const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(input.stage);
|
||||||
@@ -471,13 +408,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
MG_Util::ConvertGLEnumToString(shaderType).c_str());
|
MG_Util::ConvertGLEnumToString(shaderType).c_str());
|
||||||
|
|
||||||
if (!compiled.compileStatus) {
|
if (!compiled.compileStatus) {
|
||||||
// The compile log LEADS the quoted source, and that order is load-bearing:
|
|
||||||
// under MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS this string is the
|
|
||||||
// application's ONLY compile diagnostic (the per-shader queries answered
|
|
||||||
// optimistically), and applications read it through a bounded buffer -
|
|
||||||
// Iris uses 32768 bytes - so the actionable text must come before the
|
|
||||||
// potentially-100KB source dump. The full source stays: the device log is
|
|
||||||
// where a failing pack gets debugged from.
|
|
||||||
artifacts.infoLog =
|
artifacts.infoLog =
|
||||||
std::format("Linking a {} with compilation error, linking will now terminate. Shader error "
|
std::format("Linking a {} with compilation error, linking will now terminate. Shader error "
|
||||||
"log:\n{}\nShader src:\n{}",
|
"log:\n{}\nShader src:\n{}",
|
||||||
@@ -906,6 +836,183 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ProgramLinkTask::GenerateSpirv() {
|
||||||
|
/* As we passed first stage compilation/linking,
|
||||||
|
* we'll assume all the operations here should
|
||||||
|
* pass. We may be able to employ some optimizations
|
||||||
|
* here without the burden of error reporting.
|
||||||
|
*/
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
MGLOG_D("ProgramObject %u: GenerateSpirv - start", in.externalIndex);
|
||||||
|
|
||||||
|
// The shaders were parsed once, in the link-compatible (relaxed Vulkan-rules)
|
||||||
|
// configuration, and artifacts.program linked those parses - so artifacts.program IS
|
||||||
|
// the program the backends consume. Generate SPIR-V straight from its
|
||||||
|
// intermediates; the full re-parse + re-link that used to live here (one
|
||||||
|
// glslang pass per shader per link) is gone.
|
||||||
|
Vector<GLenum> shaderTypes(in.shaders.size());
|
||||||
|
for (SizeT i = 0; i < in.shaders.size(); i++) {
|
||||||
|
shaderTypes[i] = MG_Util::ConvertShaderStageToGLEnum(in.shaders[i].stage);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgramBinaryAttrib binaryAttrib{
|
||||||
|
.shaderTypes = shaderTypes,
|
||||||
|
.program = *artifacts.program,
|
||||||
|
};
|
||||||
|
MGLOG_D("ProgramObject %u: GenerateSpirv - requesting SPIR-V binary from program", in.externalIndex);
|
||||||
|
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||||
|
if (!binaryResult) {
|
||||||
|
DeferLog(std::format("ProgramObject {}: GenerateSpirv - GetSpirvBinaryFromProgram failed",
|
||||||
|
in.externalIndex));
|
||||||
|
}
|
||||||
|
MOBILEGL_ASSERT(binaryResult, "GetSpirvBinaryFromProgram failed");
|
||||||
|
artifacts.generatedSpirv = Move(binaryResult.value());
|
||||||
|
MGLOG_D("ProgramObject %u: GenerateSpirv - generated %zu SPIR-V modules", in.externalIndex,
|
||||||
|
artifacts.generatedSpirv.size());
|
||||||
|
|
||||||
|
// Linked SPIR-V generated, sanitize and optimize it
|
||||||
|
for (auto& spv : artifacts.generatedSpirv) {
|
||||||
|
auto success = ShaderCompiler::SanitizeAndOptimizeBinary(spv, spv);
|
||||||
|
MOBILEGL_ASSERT(success, "SanitizeBinary failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramLinkTask::BuildGlobalUboRouting() {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
Vector<GLenum> shaderTypes(in.shaders.size());
|
||||||
|
for (SizeT i = 0; i < in.shaders.size(); i++) {
|
||||||
|
shaderTypes[i] = MG_Util::ConvertShaderStageToGLEnum(in.shaders[i].stage);
|
||||||
|
}
|
||||||
|
|
||||||
|
artifacts.uniformSizesInBytes.clear();
|
||||||
|
artifacts.uniformOffsets.clear();
|
||||||
|
artifacts.globalUboScratch.clear();
|
||||||
|
// kInvalidUniformOffset marks locations that end up without global-UBO backing
|
||||||
|
// (e.g. the optimizer eliminated every use of the uniform); the fallback pass
|
||||||
|
// below gives those locations tail storage so glUniform* always has a target.
|
||||||
|
artifacts.uniformOffsets.resize(artifacts.maxUniformLocation + 1, ProgramObject::kInvalidUniformOffset);
|
||||||
|
artifacts.uniformSizesInBytes.resize(artifacts.maxUniformLocation + 1, 0);
|
||||||
|
for (SizeT i = 0; i < artifacts.generatedSpirv.size(); i++) {
|
||||||
|
auto& spv = artifacts.generatedSpirv[i];
|
||||||
|
|
||||||
|
auto shaderType = shaderTypes[i];
|
||||||
|
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - parsing SPIR-V meta data for module %zu "
|
||||||
|
"(shaderType=%u, wordCount=%zu)",
|
||||||
|
in.externalIndex, i, shaderType, spv.size());
|
||||||
|
SpvcSession session(spv, SessionUsageBit::Reflection);
|
||||||
|
auto result = session.ParseMetaData();
|
||||||
|
if (result < 0) {
|
||||||
|
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - SpvcSession::ParseMetaData failed for module %zu, "
|
||||||
|
"err = %d%s",
|
||||||
|
in.externalIndex, i, result,
|
||||||
|
(result == SPVC_ERROR_INVALID_SPIRV ? ". Probably no global UBO?" : ""));
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
auto& meta = session.GetMetadata();
|
||||||
|
auto size = meta.globalUboSize;
|
||||||
|
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - SPIR-V meta: uboSize=%zu plainUniformCount=%zu "
|
||||||
|
"plainUniformOffsets=%zu",
|
||||||
|
in.externalIndex, meta.globalUboSize, meta.plainUniformMemberSizesInBytes.size(),
|
||||||
|
meta.plainUniformOffsetsInUBO.size());
|
||||||
|
if (size == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (artifacts.globalUboScratch.size() < size) {
|
||||||
|
artifacts.globalUboScratch.resize(size);
|
||||||
|
}
|
||||||
|
for (const auto& [name, offset] : meta.plainUniformOffsetsInUBO) {
|
||||||
|
// SPIRV-Reflect leaf names never carry a "[0]" suffix; frontend
|
||||||
|
// reflection keys arrays as "arr[0]" (GL naming), so retry with the
|
||||||
|
// suffix before declaring the uniform unbacked.
|
||||||
|
auto locationIt = artifacts.uniformLocations.find(name);
|
||||||
|
if (locationIt == artifacts.uniformLocations.end()) {
|
||||||
|
locationIt = artifacts.uniformLocations.find(name + "[0]");
|
||||||
|
}
|
||||||
|
if (locationIt == artifacts.uniformLocations.end()) {
|
||||||
|
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' offset=%u but not found in "
|
||||||
|
"uniformLocations",
|
||||||
|
in.externalIndex, name.c_str(), offset);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const Uint baseLocation = locationIt->second;
|
||||||
|
if (!ProgramObject::IsValidUniformLocation(artifacts, static_cast<Int>(baseLocation))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Int uniformIndex = artifacts.uniformIndexInTProgram[baseLocation];
|
||||||
|
const GLint arraySize = ProgramObject::GetUniformArraySizeByTIndex(artifacts, uniformIndex);
|
||||||
|
SizeT memberSize = 0;
|
||||||
|
const auto sizeIt = meta.plainUniformMemberSizesInBytes.find(name);
|
||||||
|
if (sizeIt != meta.plainUniformMemberSizesInBytes.end()) {
|
||||||
|
memberSize = sizeIt->second;
|
||||||
|
}
|
||||||
|
Uint arrayStride = 0;
|
||||||
|
const auto strideIt = meta.plainUniformArrayStridesInUBO.find(name);
|
||||||
|
if (strideIt != meta.plainUniformArrayStridesInUBO.end()) {
|
||||||
|
arrayStride = strideIt->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array uniforms span one location per element (see DoReflection);
|
||||||
|
// give each element its real byte offset inside the UBO.
|
||||||
|
const GLint elementCount = (arraySize > 1 && arrayStride == 0) ? 1 : std::max(arraySize, 1);
|
||||||
|
for (GLint element = 0; element < elementCount; ++element) {
|
||||||
|
const Uint location = baseLocation + static_cast<Uint>(element);
|
||||||
|
if (location > artifacts.maxUniformLocation ||
|
||||||
|
artifacts.uniformIndexInTProgram[location] != uniformIndex) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
artifacts.uniformOffsets[location] = offset + static_cast<Uint>(element) * arrayStride;
|
||||||
|
const SizeT consumed = static_cast<SizeT>(element) * arrayStride;
|
||||||
|
artifacts.uniformSizesInBytes[location] = memberSize > consumed ? memberSize - consumed : 0;
|
||||||
|
}
|
||||||
|
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' offset=%u stride=%u size=%zu assigned "
|
||||||
|
"to locations %u..%u",
|
||||||
|
in.externalIndex, name.c_str(), offset, arrayStride, memberSize, baseLocation,
|
||||||
|
baseLocation + static_cast<Uint>(elementCount) - 1);
|
||||||
|
}
|
||||||
|
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - finished parsing module %zu metadata",
|
||||||
|
in.externalIndex, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback pass: a linked program's active non-opaque uniforms must accept
|
||||||
|
// glUniform*/glGetUniform* even when the optimized SPIR-V no longer contains
|
||||||
|
// them (AggressiveDCE can remove a dead loop together with the only loads of a
|
||||||
|
// uniform -- or the entire global UBO, leaving the scratch unallocated). Hand
|
||||||
|
// such locations CPU-side storage at the (16-byte aligned) tail of the shadow
|
||||||
|
// buffer; backends bind at least the SPIR-V-declared UBO range, and the GPU
|
||||||
|
// never reads these bytes, so this only keeps the GL-visible state coherent.
|
||||||
|
for (Uint location = 0; location <= artifacts.maxUniformLocation; ++location) {
|
||||||
|
if (artifacts.uniformOffsets[location] != ProgramObject::kInvalidUniformOffset) continue;
|
||||||
|
if (!ProgramObject::IsValidUniformLocation(artifacts, static_cast<Int>(location))) continue;
|
||||||
|
const auto& uniform = artifacts.program->getUniform(artifacts.uniformIndexInTProgram[location]);
|
||||||
|
const glslang::TType* type = uniform.getType();
|
||||||
|
if (type != nullptr && type->isOpaque()) continue;
|
||||||
|
if (uniform.index >= 0 && uniform.index < artifacts.program->getNumUniformBlocks() &&
|
||||||
|
std::strstr(artifacts.program->getUniformBlock(uniform.index).name.c_str(),
|
||||||
|
MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) == nullptr) {
|
||||||
|
// Member of a named uniform block: not settable through glUniform*, so it
|
||||||
|
// needs no global-UBO shadow storage.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// std140-style slot: the matrix upload paths write column vectors at
|
||||||
|
// 16-byte strides, so a matrix slot must cover cols * 16 bytes.
|
||||||
|
SizeT slotSize = MG_Util::GetGLTypeSize(uniform.glDefineType);
|
||||||
|
if (type != nullptr && type->isMatrix()) {
|
||||||
|
slotSize = static_cast<SizeT>(type->getMatrixCols()) * 16u;
|
||||||
|
}
|
||||||
|
slotSize = (slotSize + 15u) & ~static_cast<SizeT>(15u);
|
||||||
|
const SizeT slotOffset = (artifacts.globalUboScratch.size() + 15u) & ~static_cast<SizeT>(15u);
|
||||||
|
artifacts.globalUboScratch.resize(slotOffset + slotSize, 0);
|
||||||
|
artifacts.uniformOffsets[location] = static_cast<Uint>(slotOffset);
|
||||||
|
artifacts.uniformSizesInBytes[location] = slotSize;
|
||||||
|
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' location %u has no UBO backing in the "
|
||||||
|
"generated SPIR-V (optimized out?); allocated %zu fallback bytes at scratch offset %zu",
|
||||||
|
in.externalIndex, uniform.name.c_str(), location, slotSize, slotOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Bool ProgramLinkTask::ValidateFragmentOutputLocations() {
|
Bool ProgramLinkTask::ValidateFragmentOutputLocations() {
|
||||||
if (!artifacts.program) return false;
|
if (!artifacts.program) return false;
|
||||||
|
|
||||||
@@ -1047,80 +1154,21 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// GL 4.6 core 11.1.2.1 (and the resource-name rule of 7.3.1.1): a member of
|
|
||||||
// an output interface block is named "<BLOCK name>.<member>" - the block's
|
|
||||||
// TYPE name, never the instance name, and that holds for an anonymous
|
|
||||||
// instance too. glslang's linker object for such a block is the *instance*
|
|
||||||
// symbol ("vs_out", or "anon@N" when there is none), so the head of the
|
|
||||||
// dotted path has to be matched against getType().getTypeName() instead of
|
|
||||||
// getName(). Without this every capture of a block member resolved to
|
|
||||||
// nothing and the link failed with "is not an output of the vertex stage".
|
|
||||||
String blockName;
|
|
||||||
String memberName;
|
|
||||||
if (const SizeT dot = declaredName.find('.'); dot != String::npos) {
|
|
||||||
blockName = declaredName.substr(0, dot);
|
|
||||||
memberName = declaredName.substr(dot + 1);
|
|
||||||
// An array of block instances is spelled "<block>[i].<member>"; every
|
|
||||||
// instance shares one member list, so the subscript only has to go.
|
|
||||||
if (!blockName.empty() && blockName.back() == ']') {
|
|
||||||
const SizeT bracket = blockName.rfind('[');
|
|
||||||
if (bracket != String::npos) blockName.resize(bracket);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto* node : linkerObjects->getSequence()) {
|
for (const auto* node : linkerObjects->getSequence()) {
|
||||||
const glslang::TIntermSymbol* symbol = node->getAsSymbolNode();
|
const glslang::TIntermSymbol* symbol = node->getAsSymbolNode();
|
||||||
if (symbol == nullptr || symbol->getType().getQualifier().storage != glslang::EvqVaryingOut) {
|
if (symbol == nullptr || symbol->getType().getQualifier().storage != glslang::EvqVaryingOut) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const glslang::TType& symbolType = symbol->getType();
|
if (symbol->getName() != declaredName.c_str()) {
|
||||||
const glslang::TType* capturedType = nullptr;
|
continue;
|
||||||
if (memberName.empty()) {
|
|
||||||
if (symbol->getName() != declaredName.c_str()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
capturedType = &symbolType;
|
|
||||||
} else {
|
|
||||||
if (symbolType.getBasicType() != glslang::EbtBlock) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// The spec spelling is the block name; the instance name is accepted
|
|
||||||
// as a fallback so a request written the (common, non-conformant)
|
|
||||||
// instance-qualified way resolves instead of failing the whole link.
|
|
||||||
if (symbolType.getTypeName() != blockName.c_str() &&
|
|
||||||
symbol->getName() != blockName.c_str()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const glslang::TTypeList* members = symbolType.getStruct();
|
|
||||||
if (members == nullptr) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (SizeT m = 0; m < members->size(); ++m) {
|
|
||||||
const glslang::TType* memberType = (*members)[m].type;
|
|
||||||
if (memberType == nullptr || memberType->getFieldName() != memberName.c_str()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
capturedType = memberType;
|
|
||||||
varying.blockMemberIndex = static_cast<Int>(m);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (capturedType == nullptr) {
|
|
||||||
// Right block, wrong member: no other linker object can match.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
varying.blockName = symbolType.getTypeName().c_str();
|
|
||||||
varying.blockInstanceName = symbol->getName().c_str();
|
|
||||||
}
|
}
|
||||||
resolved = ResolveXfbSymbolType(*capturedType, varying.type, varying.size, bytesPerElement);
|
resolved = ResolveXfbSymbolType(symbol->getType(), varying.type, varying.size, bytesPerElement);
|
||||||
if (resolved && singleElement) {
|
if (resolved && singleElement) {
|
||||||
if (static_cast<Int>(element) >= varying.size) {
|
if (static_cast<Int>(element) >= varying.size) {
|
||||||
resolved = false;
|
resolved = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
varying.size = 1;
|
varying.size = 1;
|
||||||
if (varying.blockMemberIndex >= 0) {
|
|
||||||
varying.blockMemberElement = static_cast<Int>(element);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,16 +29,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
SharedPtr<const ShaderCompileTask> compiled;
|
SharedPtr<const ShaderCompileTask> compiled;
|
||||||
};
|
};
|
||||||
|
|
||||||
// PHASE A of one glLinkProgram: the half that decides what GL can be asked about the
|
// The unit of asynchronous linking: one glLinkProgram's worth of pure CPU work - glslang
|
||||||
// program - glslang link + mapIO, the GL-facing reflection surface, fragment-output
|
// link + mapIO, SPIR-V generation and optimization, the GL-facing reflection surface, the
|
||||||
// validation and transform-feedback resolution - with every input it needs snapshotted at
|
// global-UBO routing tables, fragment-output validation and transform-feedback
|
||||||
// enqueue.
|
// resolution - with every input it needs snapshotted at enqueue.
|
||||||
//
|
|
||||||
// Every one of the eight ways a link can fail lives here, so once this node has published
|
|
||||||
// through EnsureLinkJoined() the program's LINK_STATUS, info log and entire query surface
|
|
||||||
// are FINAL and truthful. SPIR-V generation, spirv-opt and the global-UBO routing tables
|
|
||||||
// moved to ProgramSpirvTask, which chains behind this node and is joined by only five
|
|
||||||
// getters (see ProgramObject::EnsureSpirvJoined).
|
|
||||||
//
|
//
|
||||||
// Same ownership rule as ShaderCompileTask: the body reads nothing but `in` (all of it
|
// 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
|
// owned or immutable) and writes nothing but `artifacts`. No GL call, no
|
||||||
@@ -46,13 +40,11 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// through the CompileEnv snapshot and diagnostics are deferred to the join.
|
// 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
|
// ONE LINK IS ONE HANDLER. RunBody() runs start to finish inside a single pool handler
|
||||||
// and is the only place `artifacts` is written. Splitting it across handlers to
|
// and is the only place `artifacts` is written. Do not split it across handlers to
|
||||||
// "pipeline" the reflection half would let a cancel land between the halves and publish a
|
// "pipeline" the reflection half: the intermediates that GlslangToSpv and buildReflection
|
||||||
// program whose SPIR-V and reflection describe different things - so any such split has
|
// share are mutated in a strict order (see the GenerateSpirv-before-DoReflection comment
|
||||||
// to be structural: the first half must publish a LINK_STATUS and a query surface that
|
// in Run()), and a second handler would let a cancel land between them and publish a
|
||||||
// are already final, and a lost second half must degrade to "linked but not drawable",
|
// program whose SPIR-V and reflection describe different things.
|
||||||
// never to a half-published program. (The intermediates' ordering constraint that used to
|
|
||||||
// be quoted here is retested and no longer binding; see the ordering note in RunBody.)
|
|
||||||
class ProgramLinkTask final : public MG_Util::Async::JobNode {
|
class ProgramLinkTask final : public MG_Util::Async::JobNode {
|
||||||
public:
|
public:
|
||||||
// ---- inputs, snapshotted on the GL thread in ProgramObject::Link()'s prologue ----
|
// ---- inputs, snapshotted on the GL thread in ProgramObject::Link()'s prologue ----
|
||||||
@@ -76,49 +68,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// Moved (never copied) into the ProgramObject by EnsureLinkJoined().
|
// Moved (never copied) into the ProgramObject by EnsureLinkJoined().
|
||||||
ProgramObject::LinkArtifacts artifacts;
|
ProgramObject::LinkArtifacts artifacts;
|
||||||
|
|
||||||
// ---- output: everything ProgramSpirvTask needs to run without this node's
|
|
||||||
// artifacts, filled at the tail of a successful RunBody() ----
|
|
||||||
//
|
|
||||||
// THIS IS NOT `artifacts` AND MUST NOT BE MERGED INTO IT. The GL thread MOVES
|
|
||||||
// `artifacts` out of this node at the join, and phase B runs on a worker afterwards -
|
|
||||||
// so phase B may read `spirvHandoff` and `in` (neither is ever touched by the join)
|
|
||||||
// and this node's JobState, and nothing else on it. Reading `artifacts` or
|
|
||||||
// `diagnostics` from phase B would race the publish.
|
|
||||||
struct SpirvHandoff {
|
|
||||||
// MANDATORY, and the reason this struct exists at all: TProgram::addShader stores
|
|
||||||
// a RAW TShader*, and for the one-shader-per-stage case getIntermediate() returns
|
|
||||||
// the TShader's own intermediate rather than a copy. These used to die when
|
|
||||||
// RunBody() returned, which was safe only because nothing called getIntermediate()
|
|
||||||
// afterwards. GlslangToSpv does exactly that, so phase B has to own them.
|
|
||||||
//
|
|
||||||
// MEMORY NOTE: this is the one thing the split makes live LONGER than it used to -
|
|
||||||
// a glslang arena per stage, megabytes for a shaderpack, now alive from the end of
|
|
||||||
// phase A until phase B runs instead of dying with the link body, so a deep
|
|
||||||
// phase-B backlog holds one arena per queued program. Phase B clears this vector
|
|
||||||
// as soon as GlslangToSpv returns, but read that call site's comment before
|
|
||||||
// relying on it: for the COMMON case (a shader linked into exactly one program)
|
|
||||||
// the compile node co-owns the same TShader and phase A pins that node, so the
|
|
||||||
// clear frees nothing and only the re-parsed CAS-loser shaders are actually
|
|
||||||
// released. If peak RSS ever becomes the binding constraint on a pack load, THIS
|
|
||||||
// is the field to attack - by bounding the backlog, by releasing the compile
|
|
||||||
// node's own reference at claim time, or by moving GlslangToSpv back into phase A.
|
|
||||||
Vector<SharedPtr<glslang::TShader>> shaders;
|
|
||||||
// GL enum per entry of `in.shaders`, in the same order (GetSpirvBinaryFromProgram
|
|
||||||
// walks it to pick the intermediates).
|
|
||||||
Vector<GLenum> shaderTypes;
|
|
||||||
// The reflection slice BuildGlobalUboRouting consumes: {program, uniformLocations,
|
|
||||||
// uniformIndexInTProgram, tProgramUniformIndexToGl, maxUniformLocation}. Carried
|
|
||||||
// as a LinkArtifacts with only those five fields set, so the routing pass can keep
|
|
||||||
// calling ProgramObject::IsValidUniformLocation / GetUniformArraySizeByTIndex
|
|
||||||
// unchanged. The SharedPtr copy of `program` is also what keeps the TProgram alive
|
|
||||||
// for phase B after the join has moved `artifacts` away.
|
|
||||||
ProgramObject::LinkArtifacts reflection;
|
|
||||||
|
|
||||||
// The one flag phase B tests before doing anything: false means this link never
|
|
||||||
// reached the tail of RunBody (it failed, or was cancelled mid-body).
|
|
||||||
Bool ready = false;
|
|
||||||
} spirvHandoff;
|
|
||||||
|
|
||||||
// Posts this job once every compile in `deps` is terminal - and not one moment
|
// 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
|
// 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
|
// on another job, or the pool could deadlock with all its workers waiting on each
|
||||||
@@ -145,6 +94,8 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
Bool ValidateFragmentOutputLocations();
|
Bool ValidateFragmentOutputLocations();
|
||||||
Bool ResolveTransformFeedbackVaryings();
|
Bool ResolveTransformFeedbackVaryings();
|
||||||
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
|
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
|
||||||
|
void GenerateSpirv();
|
||||||
|
void BuildGlobalUboRouting();
|
||||||
|
|
||||||
// Worker-side MGLOG replacement: appended to diagnostics.logLines and replayed by the
|
// 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.
|
// join, on the GL thread, where a serial implementation would have printed it.
|
||||||
|
|||||||
@@ -8,9 +8,7 @@
|
|||||||
|
|
||||||
#include "ProgramObject.h"
|
#include "ProgramObject.h"
|
||||||
#include "ProgramLinkTask.h"
|
#include "ProgramLinkTask.h"
|
||||||
#include "ProgramSpirvTask.h"
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstring>
|
|
||||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||||
@@ -70,129 +68,12 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
Bool ProgramObject::IsPendingLinkTerminal() const { return m_pendingLink->IsTerminal(); }
|
Bool ProgramObject::IsPendingLinkTerminal() const { return m_pendingLink->IsTerminal(); }
|
||||||
|
|
||||||
Bool ProgramObject::IsPendingSpirvTerminal() const { return m_pendingSpirv->IsTerminal(); }
|
|
||||||
|
|
||||||
void ProgramObject::JoinPendingSpirv() const {
|
|
||||||
MOBILEGL_ASSERT(!MG_Util::Async::ShaderCompilePool::IsPoolThread(),
|
|
||||||
"ProgramObject::EnsureSpirvJoined() reached from a pool thread; a job body must never read "
|
|
||||||
"GL-thread-owned objects");
|
|
||||||
|
|
||||||
// Move the node out FIRST, for the same reason JoinPendingLink does: everything below
|
|
||||||
// runs GL-thread-only code that reads program state, and with m_pendingSpirv still set
|
|
||||||
// that would re-enter this function.
|
|
||||||
const SharedPtr<ProgramSpirvTask> pending = Move(m_pendingSpirv);
|
|
||||||
m_pendingSpirv.reset();
|
|
||||||
|
|
||||||
pending->Wait();
|
|
||||||
if (pending->IsComplete()) {
|
|
||||||
m_spirv = Move(pending->artifacts);
|
|
||||||
}
|
|
||||||
// A node that settled as Cancelled published nothing, so m_spirv stays empty with
|
|
||||||
// spirvStatus false: linked, queryable, not drawable. Nothing to repair.
|
|
||||||
|
|
||||||
// Before the version bump, and before any caller can read the shadow: the writes the
|
|
||||||
// application made while the layout did not exist yet.
|
|
||||||
ReplayBufferedUniformWrites();
|
|
||||||
|
|
||||||
// The THIRD version bump of this link (enqueue, phase-A publish, phase-B publish), and
|
|
||||||
// it is mandatory for exactly the reason the phase-A one is (see JoinPendingLink): a
|
|
||||||
// backend memo taken during the A->B window - when the program was already answering
|
|
||||||
// as linked but had no SPIR-V and no uniform shadow - must not survive the arrival of
|
|
||||||
// either. The memos at risk are keyed on (lifetimeId, backendStateVersion).
|
|
||||||
BumpLinkObservableVersions();
|
|
||||||
|
|
||||||
MG_Util::Async::ApplyDeferredDiagnostics(*pending);
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool ProgramObject::BufferUniformWrite(const Uint location, const SizeT byteOffsetInUniform, const void* source,
|
|
||||||
const SizeT byteSize) {
|
|
||||||
if (source == nullptr || byteSize == 0) return true; // nothing to record, nothing to join for
|
|
||||||
if (m_pendingUniformBytes.size() + byteSize > kMaxBufferedUniformBytes) {
|
|
||||||
// Pressure valve: stop growing and let the caller take the join. Say so once per
|
|
||||||
// program, because the interesting fact is WHICH program did it.
|
|
||||||
MGLOG_D("ProgramObject %u: buffered uniform writes exceeded %zu bytes during the SPIR-V window; the "
|
|
||||||
"write joins instead",
|
|
||||||
m_externalIndex, kMaxBufferedUniformBytes);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const SizeT dataOffset = m_pendingUniformBytes.size();
|
|
||||||
m_pendingUniformBytes.resize(dataOffset + byteSize);
|
|
||||||
std::memcpy(m_pendingUniformBytes.data() + dataOffset, source, byteSize);
|
|
||||||
m_pendingUniformWrites.push_back(PendingUniformWrite{.location = location,
|
|
||||||
.byteOffsetInUniform =
|
|
||||||
static_cast<Uint>(byteOffsetInUniform),
|
|
||||||
.byteSize = static_cast<Uint>(byteSize),
|
|
||||||
.dataOffset = static_cast<Uint>(dataOffset)});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProgramObject::ReplayBufferedUniformWrites() const {
|
|
||||||
if (m_pendingUniformWrites.empty()) {
|
|
||||||
m_pendingUniformBytes.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drain into locals first: MarkUBOContentDirty below is a plain counter bump, but a
|
|
||||||
// future reader of this function should not be able to observe a half-drained buffer.
|
|
||||||
Vector<PendingUniformWrite> writes;
|
|
||||||
Vector<Uint8> bytes;
|
|
||||||
writes.swap(m_pendingUniformWrites);
|
|
||||||
bytes.swap(m_pendingUniformBytes);
|
|
||||||
|
|
||||||
if (m_spirv.globalUboScratch.empty() || m_spirv.uniformOffsets.empty()) {
|
|
||||||
// Phase B produced nothing (cancelled at teardown, or a relink superseded it).
|
|
||||||
// The program is not drawable, so there is nowhere for these to land and nothing
|
|
||||||
// that could observe them.
|
|
||||||
MGLOG_D("ProgramObject %u: dropping %zu buffered uniform write(s); the SPIR-V job published no shadow",
|
|
||||||
m_externalIndex, writes.size());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint8* const scratch = m_spirv.globalUboScratch.data();
|
|
||||||
const SizeT uboSize = m_spirv.globalUboScratch.size();
|
|
||||||
for (const PendingUniformWrite& write : writes) {
|
|
||||||
if (write.location >= m_spirv.uniformOffsets.size()) continue;
|
|
||||||
const Uint offset = m_spirv.uniformOffsets[write.location];
|
|
||||||
if (offset == kInvalidUniformOffset ||
|
|
||||||
static_cast<SizeT>(offset) + write.byteOffsetInUniform + write.byteSize > uboSize) {
|
|
||||||
// Same verdict the live write path reaches for a uniform without backing
|
|
||||||
// storage: log and drop, rather than fault.
|
|
||||||
MGLOG_E("ProgramObject %u: buffered uniform write at location %u has no backing storage "
|
|
||||||
"(offset=%u size=%u uboSize=%zu); dropping write",
|
|
||||||
m_externalIndex, write.location, offset, write.byteSize, uboSize);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Uint8* const destination = scratch + offset + write.byteOffsetInUniform;
|
|
||||||
const Uint8* const sourceBytes = bytes.data() + write.dataOffset;
|
|
||||||
// The same bytes-equal dedupe the live path applies, per record and in order, so
|
|
||||||
// the "an identical write does not move the content version" property survives
|
|
||||||
// the detour byte for byte.
|
|
||||||
if (std::memcmp(destination, sourceBytes, write.byteSize) == 0) continue;
|
|
||||||
std::memcpy(destination, sourceBytes, write.byteSize);
|
|
||||||
MarkUBOContentDirty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProgramObject::CancelLink() {
|
void ProgramObject::CancelLink() {
|
||||||
// Phase B first: it is chained behind phase A, so cancelling A would otherwise run A's
|
|
||||||
// continuation and post a node this call is about to abandon anyway. Cancelling it up
|
|
||||||
// front makes that continuation a no-op.
|
|
||||||
//
|
|
||||||
// Cooperative and non-blocking, both of them. A node that no worker has picked up
|
|
||||||
// settles immediately; one that is running is flagged and settles when its body
|
|
||||||
// returns, writing only into itself the whole time. Either way nothing waits, and each
|
|
||||||
// node keeps its own inputs alive for as long as it needs them.
|
|
||||||
if (m_pendingSpirv) {
|
|
||||||
m_pendingSpirv->Cancel();
|
|
||||||
m_pendingSpirv.reset();
|
|
||||||
// Buffered writes belong to the link that is being abandoned. A relink resets
|
|
||||||
// every uniform to its initial value anyway (GL 4.6 core 7.6), and the other two
|
|
||||||
// callers are destruction and glProgramBinary's mandated failure, so there is
|
|
||||||
// nothing left that could want them.
|
|
||||||
m_pendingUniformWrites.clear();
|
|
||||||
m_pendingUniformBytes.clear();
|
|
||||||
}
|
|
||||||
if (!m_pendingLink) return;
|
if (!m_pendingLink) return;
|
||||||
|
// Cooperative and non-blocking. A node that no worker has picked up settles
|
||||||
|
// immediately; one that is running is flagged and settles when its body returns,
|
||||||
|
// writing only into itself the whole time. Either way nothing waits, and the node
|
||||||
|
// keeps its own inputs alive for as long as it needs them.
|
||||||
m_pendingLink->Cancel();
|
m_pendingLink->Cancel();
|
||||||
m_pendingLink.reset();
|
m_pendingLink.reset();
|
||||||
}
|
}
|
||||||
@@ -222,12 +103,8 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// function has ever cleared, and its callers depend on that (they write infoLog
|
// function has ever cleared, and its callers depend on that (they write infoLog
|
||||||
// immediately AFTER calling here). Link()'s prologue does not use this - it assigns a
|
// immediately AFTER calling here). Link()'s prologue does not use this - it assigns a
|
||||||
// whole default-constructed block, where the ordering is explicit.
|
// whole default-constructed block, where the ordering is explicit.
|
||||||
// Phase-B output (generatedSpirv / uniformOffsets / globalUboScratch) is NOT cleared
|
|
||||||
// here and is not in LinkArtifacts at all: the link body calls this on its own block,
|
|
||||||
// where no phase-B output exists yet. The two GL-thread callers that also have to
|
|
||||||
// discard phase-B output say so themselves (MarkLinkFailedByProgramBinary clears
|
|
||||||
// m_spirv; Link()'s prologue assigns a fresh one).
|
|
||||||
artifacts.program.reset();
|
artifacts.program.reset();
|
||||||
|
artifacts.generatedSpirv.clear();
|
||||||
artifacts.uniformLocations.clear();
|
artifacts.uniformLocations.clear();
|
||||||
artifacts.glUniformIndexToTProgram.clear();
|
artifacts.glUniformIndexToTProgram.clear();
|
||||||
artifacts.tProgramUniformIndexToGl.clear();
|
artifacts.tProgramUniformIndexToGl.clear();
|
||||||
@@ -240,6 +117,9 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
artifacts.uniformBlockIndexByName.clear();
|
artifacts.uniformBlockIndexByName.clear();
|
||||||
artifacts.uniformBlockBinding.clear();
|
artifacts.uniformBlockBinding.clear();
|
||||||
artifacts.shaderStorageBlockBinding.clear();
|
artifacts.shaderStorageBlockBinding.clear();
|
||||||
|
artifacts.uniformOffsets.clear();
|
||||||
|
artifacts.uniformSizesInBytes.clear();
|
||||||
|
artifacts.globalUboScratch.clear();
|
||||||
artifacts.attribs.clear();
|
artifacts.attribs.clear();
|
||||||
artifacts.attribTypes.clear();
|
artifacts.attribTypes.clear();
|
||||||
artifacts.activeUniformCount = 0;
|
artifacts.activeUniformCount = 0;
|
||||||
@@ -358,7 +238,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// is what every gated reader sees, so it has to be the complete "not linked" state -
|
// is what every gated reader sees, so it has to be the complete "not linked" state -
|
||||||
// including the fields ResetLinkArtifacts deliberately preserves for its own callers.
|
// including the fields ResetLinkArtifacts deliberately preserves for its own callers.
|
||||||
m_artifacts = {};
|
m_artifacts = {};
|
||||||
m_spirv = {};
|
|
||||||
|
|
||||||
// ---- GL-thread-owned mutations ----
|
// ---- GL-thread-owned mutations ----
|
||||||
// Remove detached shaders first
|
// Remove detached shaders first
|
||||||
@@ -413,33 +292,17 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
task->in.shaders.push_back({shader->GetShaderStage(), shader->GetShaderSourcePtr(), node});
|
task->in.shaders.push_back({shader->GetShaderStage(), shader->GetShaderSourcePtr(), node});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase B of the same link: SPIR-V generation, spirv-opt and the global-UBO routing
|
|
||||||
// tables. Created here, alongside phase A, so that from this instant the program has
|
|
||||||
// BOTH pending nodes and every cancel site (this prologue, ~ProgramObject,
|
|
||||||
// glProgramBinary's failure) drops both through the one CancelLink().
|
|
||||||
auto spirvTask = MakeShared<ProgramSpirvTask>();
|
|
||||||
m_pendingLink = task;
|
m_pendingLink = task;
|
||||||
m_pendingSpirv = spirvTask;
|
|
||||||
|
|
||||||
// Flag off - or glMaxShaderCompilerThreadsKHR(0), see AsyncShaderCompileActive():
|
// Flag off - or glMaxShaderCompilerThreadsKHR(0), see AsyncShaderCompileActive():
|
||||||
// byte-identical to the synchronous implementation. RunInline() executes the same
|
// byte-identical to the synchronous implementation. RunInline() executes the same
|
||||||
// bodies on this thread, in the same order, and the join below publishes through the
|
// body on this thread and the join below publishes through the same code, so the two
|
||||||
// same code, so the two modes differ only in WHICH thread ran them.
|
// modes differ only in WHICH thread ran RunBody().
|
||||||
//
|
|
||||||
// Deliberately NOT expressed as SubmitAfter here: its continuation posts to the pool,
|
|
||||||
// and in this mode the pool is merely unused rather than stopped - the work would
|
|
||||||
// silently move off-thread in the one mode whose whole contract is that it does not.
|
|
||||||
if (!MG_Util::Async::AsyncShaderCompileActive()) {
|
if (!MG_Util::Async::AsyncShaderCompileActive()) {
|
||||||
task->RunInline();
|
task->RunInline();
|
||||||
spirvTask->RunInlineAfter(task);
|
EnsureLinkJoined();
|
||||||
EnsureSpirvJoined();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// The chain edge FIRST, while phase A is still Pending, so registering it is a plain
|
|
||||||
// list append rather than an inline continuation on this thread. If SubmitAfter below
|
|
||||||
// then fails to post phase A it cancels it, and that cancel fires this edge, which
|
|
||||||
// cancels phase B - nothing is left stranded either way.
|
|
||||||
spirvTask->SubmitAfter(task);
|
|
||||||
task->SubmitAfter(deps);
|
task->SubmitAfter(deps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// ProgramLinkTask.h includes THIS header (it outputs a LinkArtifacts), so including it
|
// ProgramLinkTask.h includes THIS header (it outputs a LinkArtifacts), so including it
|
||||||
// back would be circular. The destructor is therefore out of line.
|
// back would be circular. The destructor is therefore out of line.
|
||||||
class ProgramLinkTask;
|
class ProgramLinkTask;
|
||||||
// Phase B of the same link: SPIR-V generation, spirv-opt and the global-UBO routing
|
|
||||||
// tables. Chained behind the ProgramLinkTask, forward-declared for the same reason.
|
|
||||||
class ProgramSpirvTask;
|
|
||||||
|
|
||||||
class ProgramObject {
|
class ProgramObject {
|
||||||
public:
|
public:
|
||||||
@@ -306,25 +303,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// 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;
|
||||||
// PHASE B (joins the SPIR-V job; see EnsureSpirvJoined).
|
Uint GetUniformOffset(Uint location) const { return Artifacts().uniformOffsets[location]; }
|
||||||
//
|
|
||||||
// BOUNDS-CHECKED, and that is not defensive padding - it is the load-bearing half of
|
|
||||||
// the "linked but not drawable" contract. A phase B that settles CANCELLED rather than
|
|
||||||
// Complete (its body threw, the pool failed to enqueue it, or teardown cancelled it
|
|
||||||
// while phase A had already published) publishes nothing, so the shadow is a
|
|
||||||
// default-constructed SpirvArtifacts with an EMPTY uniformOffsets - while LINK_STATUS
|
|
||||||
// stays GL_TRUE, because GL gives no way to retract one, and IsValidUniformLocation()
|
|
||||||
// keeps answering true out of phase-A reflection. Every glUniform*/glGetUniform* call
|
|
||||||
// site reaches this getter BEFORE its own kInvalidUniformOffset / null-scratch guard,
|
|
||||||
// so an unchecked operator[] here would be a null dereference on the query surface
|
|
||||||
// this design promises stays answerable. Reporting kInvalidUniformOffset instead hands
|
|
||||||
// each of those sites exactly the value their existing guard already handles - the
|
|
||||||
// same value the routing pass itself uses for a uniform the optimizer deleted.
|
|
||||||
Uint GetUniformOffset(Uint location) const {
|
|
||||||
const SpirvArtifacts& spirv = Spirv();
|
|
||||||
return location < spirv.uniformOffsets.size() ? spirv.uniformOffsets[location]
|
|
||||||
: kInvalidUniformOffset;
|
|
||||||
}
|
|
||||||
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) {
|
||||||
@@ -402,14 +381,9 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
const String& GetActiveAttribName(Uint index) const {
|
const String& GetActiveAttribName(Uint index) const {
|
||||||
return NormalizeBuiltinPipeInputName(Artifacts().program->getPipeInput(static_cast<Int>(index)).name);
|
return NormalizeBuiltinPipeInputName(Artifacts().program->getPipeInput(static_cast<Int>(index)).name);
|
||||||
}
|
}
|
||||||
// PHASE B, all three (see EnsureSpirvJoined): the shadow buffer's layout is decided
|
void* MapUBO() { return Artifacts().globalUboScratch.data(); }
|
||||||
// by the OPTIMIZED SPIR-V, so it does not exist until the SPIR-V job has settled - and
|
const void* GetUBOData() const { return Artifacts().globalUboScratch.data(); }
|
||||||
// never exists at all for a program whose SPIR-V job settled cancelled. These three
|
Uint GetUBOSize() const { return static_cast<Uint>(Artifacts().globalUboScratch.size()); }
|
||||||
// degrade to nullptr/nullptr/0 in that case, which is exactly the "no backing storage"
|
|
||||||
// shape every caller already tests for (see GetUniformOffset's note).
|
|
||||||
void* MapUBO() { return Spirv().globalUboScratch.data(); }
|
|
||||||
const void* GetUBOData() const { return Spirv().globalUboScratch.data(); }
|
|
||||||
Uint GetUBOSize() const { return static_cast<Uint>(Spirv().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.
|
||||||
@@ -417,25 +391,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
void MarkUBOContentDirty() const {
|
void MarkUBOContentDirty() const {
|
||||||
if (++m_uboContentVersion == ~0u) m_uboContentVersion = 0;
|
if (++m_uboContentVersion == ~0u) m_uboContentVersion = 0;
|
||||||
}
|
}
|
||||||
// ---- glUniform* inside the phase-A -> phase-B window ----
|
|
||||||
//
|
|
||||||
// True while the program is fully linked and fully queryable but its uniform shadow's
|
|
||||||
// LAYOUT (which the optimized SPIR-V decides) does not exist yet. A non-opaque
|
|
||||||
// glUniform* write in that window is RECORDED rather than joined, and replayed into
|
|
||||||
// the shadow at the phase-B publish - so a pack that sets its uniforms immediately
|
|
||||||
// after glLinkProgram never waits for SPIR-V.
|
|
||||||
//
|
|
||||||
// Nothing can observe the difference: the only route to those bytes is glGetUniform*
|
|
||||||
// (and a draw), and both of those go through the phase-B gate, which replays first.
|
|
||||||
// The OPAQUE branch of glUniform* is deliberately not buffered - a sampler unit is
|
|
||||||
// phase-A state (uniformSamplerOrImageUnitIndex), so glUniform1i(samplerLoc, unit)
|
|
||||||
// right after a link stays a zero-join operation, which is exactly what Iris does.
|
|
||||||
Bool IsSpirvPending() const { return m_pendingSpirv != nullptr; }
|
|
||||||
// Records one write. Returns false if it declined to buffer - the caller must then
|
|
||||||
// perform the write directly (which joins). Declining is the pressure valve for an
|
|
||||||
// application that writes megabytes of uniforms into a single pending window.
|
|
||||||
Bool BufferUniformWrite(Uint location, SizeT byteOffsetInUniform, const void* source, SizeT byteSize);
|
|
||||||
|
|
||||||
Uint32 GetBackendStateVersion() const { return m_backendStateVersion; }
|
Uint32 GetBackendStateVersion() const { return m_backendStateVersion; }
|
||||||
// Bumped only by (re)linking — lets backends detect that every piece of
|
// Bumped only by (re)linking — lets backends detect that every piece of
|
||||||
// link-derived reflection (locations, block order, UBO layout) is stale.
|
// link-derived reflection (locations, block order, UBO layout) is stale.
|
||||||
@@ -508,35 +463,15 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
CancelLink();
|
CancelLink();
|
||||||
BumpLinkObservableVersions();
|
BumpLinkObservableVersions();
|
||||||
ResetLinkArtifacts(Artifacts());
|
ResetLinkArtifacts(Artifacts());
|
||||||
// ResetLinkArtifacts is a LinkArtifacts-only operation (the link body calls it on
|
|
||||||
// its own block, where no phase-B output exists yet), so the phase-B half is
|
|
||||||
// cleared here. CancelLink() above already dropped the pending SPIR-V job, so
|
|
||||||
// this cannot be racing a publish.
|
|
||||||
m_spirv = {};
|
|
||||||
Artifacts().infoLog = "No program binary format is supported.";
|
Artifacts().infoLog = "No program binary format is supported.";
|
||||||
}
|
}
|
||||||
Bool GetValidateStatus() const { return m_validateStatus; }
|
Bool GetValidateStatus() const { return m_validateStatus; }
|
||||||
// Artifacts().program is null until a link produces reflection, and glGetProgramiv is
|
Int GetActiveAtomicCounterCount() const { return Artifacts().program->getNumAtomicCounters(); }
|
||||||
// perfectly legal on a program that never linked (GL 4.6 sec. 7.3: the queried state is
|
Int GetActiveAttributesCount() const { return Artifacts().program->getNumPipeInputs(); }
|
||||||
// simply its initial value, zero). Dereferencing it there took the process down with a
|
|
||||||
// SIGSEGV inside glslang::TProgram::getNumPipeInputs - KHR-GL30.api.coverage does exactly
|
|
||||||
// this after a failed glGetAttribLocation, and reached it as soon as the CopyTexImage2D
|
|
||||||
// throw ahead of it stopped killing the run first.
|
|
||||||
Int GetActiveAtomicCounterCount() const {
|
|
||||||
const auto& program = Artifacts().program;
|
|
||||||
return program ? program->getNumAtomicCounters() : 0;
|
|
||||||
}
|
|
||||||
Int GetActiveAttributesCount() const {
|
|
||||||
const auto& program = Artifacts().program;
|
|
||||||
return program ? program->getNumPipeInputs() : 0;
|
|
||||||
}
|
|
||||||
// GL-visible uniform blocks only: the synthesized MGL_GLOBAL_UBO the relaxed parse
|
// GL-visible uniform blocks only: the synthesized MGL_GLOBAL_UBO the relaxed parse
|
||||||
// materializes for default-block uniforms is filtered out by DoReflection.
|
// materializes for default-block uniforms is filtered out by DoReflection.
|
||||||
Int GetActiveUniformBlocksCount() const { return static_cast<Int>(Artifacts().glBlockIndexToTProgram.size()); }
|
Int GetActiveUniformBlocksCount() const { return static_cast<Int>(Artifacts().glBlockIndexToTProgram.size()); }
|
||||||
GLuint GetComputeLocalSize(Uint dim) const {
|
GLuint GetComputeLocalSize(Uint dim) const { return Artifacts().program->getLocalSize(static_cast<Int>(dim)); }
|
||||||
const auto& program = Artifacts().program;
|
|
||||||
return program ? program->getLocalSize(static_cast<Int>(dim)) : 0;
|
|
||||||
}
|
|
||||||
Int GetActiveAttributesMaxLength() const { return Artifacts().attribInNameMaxLength; }
|
Int GetActiveAttributesMaxLength() const { return Artifacts().attribInNameMaxLength; }
|
||||||
Int GetActiveUniformBlocksMaxNameLength() const { return Artifacts().uniformBlockNameMaxLength; }
|
Int GetActiveUniformBlocksMaxNameLength() const { return Artifacts().uniformBlockNameMaxLength; }
|
||||||
Uint GetUniformBlockIndex(const char* name) const {
|
Uint GetUniformBlockIndex(const char* name) const {
|
||||||
@@ -636,15 +571,8 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return Artifacts().shaderStorageBlockBinding;
|
return Artifacts().shaderStorageBlockBinding;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PHASE B (see EnsureSpirvJoined). Empty for a program whose SPIR-V job was
|
Vector<Vector<unsigned>>& GetGeneratedSpirv() { return Artifacts().generatedSpirv; }
|
||||||
// cancelled; GetSpirvStatus() below is how a backend tells that apart from a program
|
const Vector<Vector<unsigned>>& GetGeneratedSpirv() const { return Artifacts().generatedSpirv; }
|
||||||
// that never linked.
|
|
||||||
Vector<Vector<unsigned>>& GetGeneratedSpirv() { return Spirv().generatedSpirv; }
|
|
||||||
const Vector<Vector<unsigned>>& GetGeneratedSpirv() const { return Spirv().generatedSpirv; }
|
|
||||||
// Whether phase B produced usable SPIR-V. Joins, like the four getters above: a
|
|
||||||
// backend asks this exactly where it used to ask GetLinkStatus(), i.e. right before
|
|
||||||
// it builds or draws with the program.
|
|
||||||
Bool GetSpirvStatus() const { return Spirv().spirvStatus; }
|
|
||||||
|
|
||||||
// The linked glslang reflection itself, for the ONE consumer that needs resource
|
// The linked glslang reflection itself, for the ONE consumer that needs resource
|
||||||
// lists no typed getter above exposes: the GL program-interface query layer
|
// lists no typed getter above exposes: the GL program-interface query layer
|
||||||
@@ -672,21 +600,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// Offset within the gap-free record a backend that cannot express the GL
|
// Offset within the gap-free record a backend that cannot express the GL
|
||||||
// layout captures into; see NeedsScatteredTransformFeedbackCapture.
|
// layout captures into; see NeedsScatteredTransformFeedbackCapture.
|
||||||
Uint32 packedOffsetBytes = 0;
|
Uint32 packedOffsetBytes = 0;
|
||||||
|
|
||||||
// GL 4.6 core 11.1.2.1 / 7.3.1.1: a member of an output interface block is
|
|
||||||
// captured under "<block name>.<member>". `name` keeps that GL spelling (it is
|
|
||||||
// what the interface queries and the ESSL backend's driver-side capture list
|
|
||||||
// need, since SPIRV-Cross re-emits the block under its own type name), while
|
|
||||||
// the three fields below carry what a SPIR-V backend needs instead: the
|
|
||||||
// decoration target is the block's *instance* variable and the member index
|
|
||||||
// inside it. blockMemberIndex < 0 means "not a block member".
|
|
||||||
String blockInstanceName;
|
|
||||||
String blockName;
|
|
||||||
Int blockMemberIndex = -1;
|
|
||||||
// Which element of an arrayed block member this capture names, -1 for "the
|
|
||||||
// member as a whole". SPIR-V cannot decorate a single array element, so a
|
|
||||||
// backend needs the element index to tell a full run from a partial one.
|
|
||||||
Int blockMemberElement = -1;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- P1: everything a link PRODUCES, in one movable block ----
|
// ---- P1: everything a link PRODUCES, in one movable block ----
|
||||||
@@ -707,6 +620,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// without going through the gate.
|
// without going through the gate.
|
||||||
struct LinkArtifacts {
|
struct LinkArtifacts {
|
||||||
SharedPtr<glslang::TProgram> program;
|
SharedPtr<glslang::TProgram> program;
|
||||||
|
Vector<Vector<unsigned>> generatedSpirv;
|
||||||
|
|
||||||
// Attributes (Vertex in)
|
// Attributes (Vertex in)
|
||||||
Vector<String> attribs;
|
Vector<String> attribs;
|
||||||
@@ -750,6 +664,11 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// SetShaderStorageBlockBinding for why this one is by name and not by index.
|
// SetShaderStorageBlockBinding for why this one is by name and not by index.
|
||||||
UnorderedMap<String, Int> shaderStorageBlockBinding;
|
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 activeUniformCount = 0;
|
||||||
Uint maxUniformLocation = 0;
|
Uint maxUniformLocation = 0;
|
||||||
Int uniformNameMaxLength = 0;
|
Int uniformNameMaxLength = 0;
|
||||||
@@ -778,35 +697,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
Uint32 xfbPackedStride = 0;
|
Uint32 xfbPackedStride = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- everything phase B of a link produces, in one movable block ----
|
|
||||||
//
|
|
||||||
// The membership rule is the same mechanical one LinkArtifacts uses: this is exactly
|
|
||||||
// what ProgramSpirvTask writes, which is what makes moving it THE publish. It is
|
|
||||||
// deliberately NOT part of LinkArtifacts, and that separation is what routes the five
|
|
||||||
// readers of SPIR-V-derived data through their own join gate by compiler rather than
|
|
||||||
// by review - m_spirv is private and Spirv() is the only spelling that reaches it.
|
|
||||||
//
|
|
||||||
// Why these three and nothing else: `generatedSpirv` has no GL-thread reader at all
|
|
||||||
// (every consumer is a backend draw/prepare path), and `uniformOffsets` +
|
|
||||||
// `globalUboScratch` are the ONLY things glUniform*/glGetUniform* need that are
|
|
||||||
// derived from the OPTIMIZED SPIR-V rather than from glslang reflection - spirv-opt
|
|
||||||
// runs in place and can delete a uniform, or the whole global UBO, so the offsets
|
|
||||||
// cannot be lifted out of glslang's reflection instead.
|
|
||||||
struct SpirvArtifacts {
|
|
||||||
Vector<Vector<unsigned>> generatedSpirv;
|
|
||||||
// Byte offset of each uniform location inside globalUboScratch, or
|
|
||||||
// kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass.
|
|
||||||
Vector<Uint> uniformOffsets;
|
|
||||||
Vector<Uint8> globalUboScratch;
|
|
||||||
// False for a program whose SPIR-V was never produced (phase B cancelled at
|
|
||||||
// teardown or by a relink) or whose optimizer run failed. GL has no way to
|
|
||||||
// retract a LINK_STATUS it already reported true, so such a program stays
|
|
||||||
// "linked" and every reflection answer it has given stays correct - it is simply
|
|
||||||
// not drawable, which the backends already express through their link-status
|
|
||||||
// gates.
|
|
||||||
Bool spirvStatus = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---- artifacts-only helpers, shared with ProgramLinkTask ----
|
// ---- artifacts-only helpers, shared with ProgramLinkTask ----
|
||||||
// Static and taking the block explicitly, because from stage 4 the link BODY needs
|
// 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
|
// them while its artifacts still live on the job node, not on any ProgramObject. The
|
||||||
@@ -846,20 +736,9 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// Blocks until a pending link has published its artifacts. Public because a few call
|
// 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
|
// sites have to join without reading anything - see the explicit-join list (J1-J8) in
|
||||||
// the P1 design. GL thread only.
|
// the P1 design. GL thread only.
|
||||||
//
|
|
||||||
// PHASE A ONLY. After this returns, LINK_STATUS and the whole GL query surface are
|
|
||||||
// final and truthful, but the SPIR-V and the uniform shadow may still be in flight.
|
|
||||||
void JoinLink() const { EnsureLinkJoined(); }
|
void JoinLink() const { EnsureLinkJoined(); }
|
||||||
|
|
||||||
// Both phases. The draw path uses this, and must: the backends sample lifetimeId /
|
// Drops a link that is still in flight, without waiting for it. Called at the points
|
||||||
// backendStateVersion / the UBO content version OUTSIDE the gate, so a draw that
|
|
||||||
// joined only phase A would sample a version, join phase B later inside the same draw
|
|
||||||
// (through GetGeneratedSpirv), and memoize under a version the phase-B publish had
|
|
||||||
// already superseded - the exact lost-invalidation hazard J1 exists to prevent.
|
|
||||||
void JoinLinkAndSpirv() const { EnsureSpirvJoined(); }
|
|
||||||
|
|
||||||
// Drops BOTH phases of a link that is still in flight, without waiting for either.
|
|
||||||
// Called at the points
|
|
||||||
// where the pending link's result stops being the answer to "what did this program
|
// 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,
|
// link to": a re-link supersedes it, glProgramBinary must force LINK_STATUS false,
|
||||||
// and a destroyed program has no observers left.
|
// and a destroyed program has no observers left.
|
||||||
@@ -878,15 +757,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// MUST NOT JOIN - this is what GL_COMPLETION_STATUS_KHR reads when the extension
|
// 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
|
// surface lands. "No job at all" counts as complete: there is nothing outstanding to
|
||||||
// wait for.
|
// wait for.
|
||||||
//
|
Bool IsLinkComplete() const { return m_pendingLink == nullptr || IsPendingLinkTerminal(); }
|
||||||
// BOTH phases, deliberately: an application that polls GL_COMPLETION_STATUS_KHR and
|
|
||||||
// then draws must not be told "done" while the SPIR-V is still being generated, or
|
|
||||||
// the draw it was cleared for is the thing that blocks.
|
|
||||||
Bool IsLinkComplete() const { return IsPhaseALinkComplete() && IsSpirvComplete(); }
|
|
||||||
// Phase A alone, for the callers that only care about the query surface (and for the
|
|
||||||
// tests that pin the two phases apart).
|
|
||||||
Bool IsPhaseALinkComplete() const { return m_pendingLink == nullptr || IsPendingLinkTerminal(); }
|
|
||||||
Bool IsSpirvComplete() const { return m_pendingSpirv == nullptr || IsPendingSpirvTerminal(); }
|
|
||||||
|
|
||||||
void SetTransformFeedbackVaryings(Vector<String>&& names, GLenum bufferMode) {
|
void SetTransformFeedbackVaryings(Vector<String>&& names, GLenum bufferMode) {
|
||||||
m_requestedXfbVaryings = Move(names);
|
m_requestedXfbVaryings = Move(names);
|
||||||
@@ -953,40 +824,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// node's state goes through this out-of-line helper.
|
// node's state goes through this out-of-line helper.
|
||||||
Bool IsPendingLinkTerminal() const;
|
Bool IsPendingLinkTerminal() const;
|
||||||
|
|
||||||
// ---- the second join gate: phase-B (SPIR-V) output only ----
|
|
||||||
// Phase A FIRST, always. Two reasons: the phase-B publish replays the uniform writes
|
|
||||||
// that were buffered during its window, and those need the phase-A reflection to
|
|
||||||
// validate against; and a caller that reaches a phase-B getter without having settled
|
|
||||||
// phase A would otherwise leave the link half-published.
|
|
||||||
//
|
|
||||||
// Same inline/out-of-line split as the phase-A gate, for the same reason: the five
|
|
||||||
// getters behind this one include the per-draw uniform upload path.
|
|
||||||
void EnsureSpirvJoined() const {
|
|
||||||
if (m_pendingLink) JoinPendingLink();
|
|
||||||
if (m_pendingSpirv) JoinPendingSpirv();
|
|
||||||
}
|
|
||||||
void JoinPendingSpirv() const;
|
|
||||||
Bool IsPendingSpirvTerminal() const;
|
|
||||||
|
|
||||||
// One buffered non-opaque glUniform* write. `dataOffset` indexes m_pendingUniformBytes,
|
|
||||||
// which is one append-only blob rather than a per-record allocation.
|
|
||||||
struct PendingUniformWrite {
|
|
||||||
Uint location = 0;
|
|
||||||
Uint byteOffsetInUniform = 0;
|
|
||||||
Uint byteSize = 0;
|
|
||||||
Uint dataOffset = 0;
|
|
||||||
};
|
|
||||||
// Replays the buffer into the freshly published shadow, in write order, and drains it.
|
|
||||||
// Each record re-does the bounds check and the bytes-equal dedupe the live write path
|
|
||||||
// performs, so "an identical write does not move the content version" survives the
|
|
||||||
// detour exactly - and a record that really does change bytes moves the version, which
|
|
||||||
// is what makes a backend re-upload the UBO it cached during the window.
|
|
||||||
void ReplayBufferedUniformWrites() const;
|
|
||||||
// Past this, BufferUniformWrite declines and the write joins instead. Sized so an
|
|
||||||
// ordinary pack load never reaches it (a pending window is one program's worth of
|
|
||||||
// uniforms) while a pathological writer cannot grow the heap without bound.
|
|
||||||
static constexpr SizeT kMaxBufferedUniformBytes = 4u << 20;
|
|
||||||
|
|
||||||
LinkArtifacts& Artifacts() {
|
LinkArtifacts& Artifacts() {
|
||||||
EnsureLinkJoined();
|
EnsureLinkJoined();
|
||||||
return m_artifacts;
|
return m_artifacts;
|
||||||
@@ -995,14 +832,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
EnsureLinkJoined();
|
EnsureLinkJoined();
|
||||||
return m_artifacts;
|
return m_artifacts;
|
||||||
}
|
}
|
||||||
SpirvArtifacts& Spirv() {
|
|
||||||
EnsureSpirvJoined();
|
|
||||||
return m_spirv;
|
|
||||||
}
|
|
||||||
const SpirvArtifacts& Spirv() const {
|
|
||||||
EnsureSpirvJoined();
|
|
||||||
return m_spirv;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GL-thread-only companion to ResetLinkArtifacts (see its definition). Const because
|
// GL-thread-only companion to ResetLinkArtifacts (see its definition). Const because
|
||||||
// the publish half of the join calls it; see the mutable counters below.
|
// the publish half of the join calls it; see the mutable counters below.
|
||||||
@@ -1070,22 +899,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// Mutable because publishing is a READ-side operation: a const getter has to be able
|
// Mutable because publishing is a READ-side operation: a const getter has to be able
|
||||||
// to settle an outstanding link before answering it.
|
// to settle an outstanding link before answering it.
|
||||||
mutable LinkArtifacts m_artifacts;
|
mutable LinkArtifacts m_artifacts;
|
||||||
// Phase-B output. Same mutability argument as m_artifacts, reached only through
|
|
||||||
// Spirv().
|
|
||||||
mutable SpirvArtifacts m_spirv;
|
|
||||||
|
|
||||||
// The link job, from enqueue until the first observable read pulls its result. Null
|
// The link job, from enqueue until the first observable read pulls its result. Null
|
||||||
// means m_artifacts is already the answer - which is the state every reader outside
|
// means m_artifacts is already the answer - which is the state every reader outside
|
||||||
// the pending window sees, and the whole reason the gate above is one branch.
|
// the pending window sees, and the whole reason the gate above is one branch.
|
||||||
mutable SharedPtr<ProgramLinkTask> m_pendingLink;
|
mutable SharedPtr<ProgramLinkTask> m_pendingLink;
|
||||||
// The SPIR-V job, chained behind m_pendingLink. Null means m_spirv is already the
|
|
||||||
// answer. A program can be in the window where m_pendingLink is already null (phase A
|
|
||||||
// published, the query surface is live) while this is still set.
|
|
||||||
mutable SharedPtr<ProgramSpirvTask> m_pendingSpirv;
|
|
||||||
// glUniform* writes taken while m_pendingSpirv was set, in call order, plus their
|
|
||||||
// bytes. Drained by the phase-B publish and cleared by every cancel site (a relink's
|
|
||||||
// uniforms are not the previous link's uniforms).
|
|
||||||
mutable Vector<PendingUniformWrite> m_pendingUniformWrites;
|
|
||||||
mutable Vector<Uint8> m_pendingUniformBytes;
|
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_State::GLState
|
} // namespace MobileGL::MG_State::GLState
|
||||||
|
|||||||
@@ -1,318 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.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 "ProgramSpirvTask.h"
|
|
||||||
|
|
||||||
#include <MG_State/GLState/ProgramState/ShaderCompileTask.h> // GlslangThreadAllocatorGuard
|
|
||||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
|
||||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
|
||||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
|
||||||
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
|
||||||
void ProgramSpirvTask::DeferLog(String line) { diagnostics.logLines.push_back(Move(line)); }
|
|
||||||
|
|
||||||
void ProgramSpirvTask::SubmitAfter(const SharedPtr<ProgramLinkTask>& phaseA) {
|
|
||||||
MOBILEGL_ASSERT(phaseA != nullptr, "ProgramSpirvTask::SubmitAfter: the phase-A node is missing");
|
|
||||||
m_phaseA = phaseA;
|
|
||||||
|
|
||||||
auto self = std::static_pointer_cast<ProgramSpirvTask>(shared_from_this());
|
|
||||||
// ONE dependency, so no counter and no guard slot: the whole race
|
|
||||||
// ProgramLinkTask::SubmitAfter's +1 exists to close (a dependency settling while the
|
|
||||||
// remaining edges are still being registered) cannot arise with a single edge.
|
|
||||||
//
|
|
||||||
// Runs inline, right here, if phase A is already terminal.
|
|
||||||
phaseA->OnTerminal([self, phaseA] {
|
|
||||||
// "Dependency did not complete, publish nothing" - the same collapse
|
|
||||||
// ProgramLinkTask::CompiledArtifacts() performs for an abandoned compile. Note
|
|
||||||
// this reads the HANDOFF, never phaseA->artifacts: the GL thread may already be
|
|
||||||
// moving those out (see the class comment).
|
|
||||||
if (!phaseA->IsComplete() || !phaseA->spirvHandoff.ready) {
|
|
||||||
self->Cancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// A cancel that landed before phase A settled (relink, glDeleteProgram, teardown).
|
|
||||||
// Posting would only make a worker pick up a node that immediately falls out of
|
|
||||||
// Run() again.
|
|
||||||
if (self->IsCancellationRequested()) {
|
|
||||||
self->Cancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Non-throwing by construction, and it has to be: this is a JobNode continuation,
|
|
||||||
// so on the pool side it runs inside an Asio handler. Post() contains its own
|
|
||||||
// allocation failures, and the catch below CANCELS rather than swallowing - a
|
|
||||||
// phase B that is never posted is a GL thread blocked forever in
|
|
||||||
// EnsureSpirvJoined(), which is far worse than a program reported as not drawable.
|
|
||||||
try {
|
|
||||||
MG_Util::Async::ShaderCompilePool::Get().Post(self);
|
|
||||||
} catch (...) {
|
|
||||||
self->Cancel();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProgramSpirvTask::RunInlineAfter(const SharedPtr<ProgramLinkTask>& phaseA) {
|
|
||||||
MOBILEGL_ASSERT(phaseA != nullptr, "ProgramSpirvTask::RunInlineAfter: the phase-A node is missing");
|
|
||||||
MOBILEGL_ASSERT(phaseA->IsTerminal(),
|
|
||||||
"ProgramSpirvTask::RunInlineAfter: phase A has not settled; the inline path must run the "
|
|
||||||
"two bodies in order on the same thread");
|
|
||||||
m_phaseA = phaseA;
|
|
||||||
RunInline();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pure CPU work only, on a pool worker (or on the GL thread in the inline mode).
|
|
||||||
// Everything this reads is either owned by this node or published by a terminal phase A;
|
|
||||||
// everything it writes is `artifacts` (and diagnostics). Same prohibitions as
|
|
||||||
// ProgramLinkTask::RunBody - no GL/EGL call, no pActiveBackendObject read, no
|
|
||||||
// pGLContext->RecordError().
|
|
||||||
void ProgramSpirvTask::RunBody() {
|
|
||||||
// glslang leaves this worker's TLS pool allocator pointing at the last arena it
|
|
||||||
// touched; reset it on the way out so an unrelated later job cannot allocate out of a
|
|
||||||
// pool that has since been freed. Declared FIRST so it is destroyed LAST - the phase-A
|
|
||||||
// release below drops the TShaders (and their pools) and must happen inside it.
|
|
||||||
const GlslangThreadAllocatorGuard glslangGuard;
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
// Drop phase A - and with it the TShaders, the TProgram reference and phase A's whole
|
|
||||||
// input snapshot - the moment this body is done, rather than at some later join. For a
|
|
||||||
// pack load that is the difference between W glslang arenas alive and all of them.
|
|
||||||
struct PhaseAReleaser {
|
|
||||||
SharedPtr<ProgramLinkTask>& node;
|
|
||||||
~PhaseAReleaser() { node.reset(); }
|
|
||||||
} const phaseAReleaser{m_phaseA};
|
|
||||||
|
|
||||||
if (!m_phaseA) return;
|
|
||||||
// Non-const: the TShaders are dropped below, the moment GlslangToSpv is finished with
|
|
||||||
// them. This is safe by ownership rather than by locking - phase A is terminal and
|
|
||||||
// therefore immutable to everyone else, the GL-thread join touches only `artifacts`
|
|
||||||
// and `diagnostics`, and this node is the sole reader of the handoff.
|
|
||||||
ProgramLinkTask::SpirvHandoff& handoff = m_phaseA->spirvHandoff;
|
|
||||||
const Uint externalIndex = m_phaseA->in.externalIndex;
|
|
||||||
if (!handoff.ready || !handoff.reflection.program) {
|
|
||||||
// Phase A did not reach its tail (it failed the link, or was cancelled mid-body).
|
|
||||||
// Publish nothing; spirvStatus stays false.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MGLOG_D("ProgramObject %u: Starting SPIR-V generation", externalIndex);
|
|
||||||
GenerateSpirv(handoff, externalIndex);
|
|
||||||
// GlslangToSpv was the only consumer of the parsed ASTs; everything after this point
|
|
||||||
// works on the SPIR-V and on the TProgram's own self-contained reflection pool. Drop
|
|
||||||
// them here rather than at the end of the body, which is ~87% of this node's runtime
|
|
||||||
// earlier (spirv-opt plus routing).
|
|
||||||
//
|
|
||||||
// WHAT THIS ACTUALLY FREES, precisely - it is LESS than "the glslang arenas", and the
|
|
||||||
// difference matters for the peak-RSS story:
|
|
||||||
// * CAS-LOSER shaders (the re-parse in ShaderCompileTask::ClaimParsedShader, i.e.
|
|
||||||
// the 2nd..Nth link of a shared shader): freed here in full. The handoff is their
|
|
||||||
// ONLY owner.
|
|
||||||
// * CAS-WINNER shaders (the common case - one shader object linked into one
|
|
||||||
// program, which is every program of an Iris pack load): NOT freed here. The
|
|
||||||
// winner branch returns a COPY of ShaderCompileTask::artifacts.shader
|
|
||||||
// (ShaderCompileTask.cpp:320) and the node never releases its own reference, while
|
|
||||||
// phase A holds that node through in.shaders[i].compiled for its whole life - and
|
|
||||||
// phase A lives until PhaseAReleaser fires at the end of this body. So the
|
|
||||||
// refcount goes 2 -> 1 here and the arena dies where it would have died anyway.
|
|
||||||
//
|
|
||||||
// Making it free the winner's arena too means releasing whatever pins the TShader
|
|
||||||
// inside the compile node, and neither obvious route is safe as a drive-by: moving out
|
|
||||||
// of artifacts.shader at claim time races ShaderObject::GetCompiledShader() on the GL
|
|
||||||
// thread and breaks JobNode's "a terminal node is immutable" invariant, and dropping
|
|
||||||
// phase A's in.shaders[i].compiled reference only helps when nothing else holds the
|
|
||||||
// node (the adoption map is a WeakPtr index, so it would also change which nodes stay
|
|
||||||
// adoptable). Both belong in a change that can be reviewed against the consume-once
|
|
||||||
// and adoption semantics on their own terms.
|
|
||||||
handoff.shaders.clear();
|
|
||||||
|
|
||||||
MGLOG_D("ProgramObject %u: Building global-UBO routing tables", externalIndex);
|
|
||||||
BuildGlobalUboRouting(handoff, externalIndex);
|
|
||||||
MGLOG_D("ProgramObject %u: Binary generation finished (generatedSpirv size=%zu)", externalIndex,
|
|
||||||
artifacts.generatedSpirv.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProgramSpirvTask::GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, const Uint externalIndex) {
|
|
||||||
/* As we passed first stage compilation/linking,
|
|
||||||
* we'll assume all the operations here should
|
|
||||||
* pass. We may be able to employ some optimizations
|
|
||||||
* here without the burden of error reporting.
|
|
||||||
*/
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
MGLOG_D("ProgramObject %u: GenerateSpirv - start", externalIndex);
|
|
||||||
|
|
||||||
// The shaders were parsed once, in the link-compatible (relaxed Vulkan-rules)
|
|
||||||
// configuration, and the handoff's program linked those parses - so it IS the program
|
|
||||||
// the backends consume. Generate SPIR-V straight from its intermediates, which the
|
|
||||||
// handoff's TShaders keep alive.
|
|
||||||
ProgramBinaryAttrib binaryAttrib{
|
|
||||||
.shaderTypes = handoff.shaderTypes,
|
|
||||||
.program = *handoff.reflection.program,
|
|
||||||
};
|
|
||||||
MGLOG_D("ProgramObject %u: GenerateSpirv - requesting SPIR-V binary from program", externalIndex);
|
|
||||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
|
||||||
if (!binaryResult) {
|
|
||||||
DeferLog(std::format("ProgramObject {}: GenerateSpirv - GetSpirvBinaryFromProgram failed", externalIndex));
|
|
||||||
MOBILEGL_ASSERT(binaryResult, "GetSpirvBinaryFromProgram failed");
|
|
||||||
return; // spirvStatus stays false: linked, but not drawable.
|
|
||||||
}
|
|
||||||
artifacts.generatedSpirv = Move(binaryResult.value());
|
|
||||||
MGLOG_D("ProgramObject %u: GenerateSpirv - generated %zu SPIR-V modules", externalIndex,
|
|
||||||
artifacts.generatedSpirv.size());
|
|
||||||
|
|
||||||
// Linked SPIR-V generated, sanitize and optimize it
|
|
||||||
Bool allOptimized = true;
|
|
||||||
{
|
|
||||||
for (auto& spv : artifacts.generatedSpirv) {
|
|
||||||
auto success = ShaderCompiler::SanitizeAndOptimizeBinary(spv, spv);
|
|
||||||
if (!success) {
|
|
||||||
// The one genuine phase-B failure mode: one of the seven optimizer passes
|
|
||||||
// reported failure, so `spv` is whatever the run left behind. A fordebug
|
|
||||||
// build trips the assert below; a release build used to hand that binary
|
|
||||||
// to the backend regardless. It no longer does - the program keeps its
|
|
||||||
// (truthful) LINK_STATUS and its whole query surface, and the routing
|
|
||||||
// tables below still give every settable uniform storage so glUniform*
|
|
||||||
// and glGetUniform* keep working, but spirvStatus stays false and the
|
|
||||||
// backends refuse to build or draw with it.
|
|
||||||
allOptimized = false;
|
|
||||||
DeferLog(std::format("ProgramObject {}: SanitizeAndOptimizeBinary failed; the program is linked "
|
|
||||||
"and queryable but not drawable",
|
|
||||||
externalIndex));
|
|
||||||
}
|
|
||||||
MOBILEGL_ASSERT(success, "SanitizeBinary failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
artifacts.spirvStatus = allOptimized;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProgramSpirvTask::BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff,
|
|
||||||
const Uint externalIndex) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
// The phase-A reflection slice this pass keys off. Carried in the handoff rather than
|
|
||||||
// read off the phase-A node's artifacts, which the join has very likely already moved.
|
|
||||||
const ProgramObject::LinkArtifacts& reflection = handoff.reflection;
|
|
||||||
|
|
||||||
artifacts.uniformOffsets.clear();
|
|
||||||
artifacts.globalUboScratch.clear();
|
|
||||||
// kInvalidUniformOffset marks locations that end up without global-UBO backing
|
|
||||||
// (e.g. the optimizer eliminated every use of the uniform); the fallback pass
|
|
||||||
// below gives those locations tail storage so glUniform* always has a target.
|
|
||||||
artifacts.uniformOffsets.resize(reflection.maxUniformLocation + 1, ProgramObject::kInvalidUniformOffset);
|
|
||||||
for (SizeT i = 0; i < artifacts.generatedSpirv.size(); i++) {
|
|
||||||
auto& spv = artifacts.generatedSpirv[i];
|
|
||||||
|
|
||||||
auto shaderType = i < handoff.shaderTypes.size() ? handoff.shaderTypes[i] : GLenum{0};
|
|
||||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - parsing SPIR-V meta data for module %zu "
|
|
||||||
"(shaderType=%u, wordCount=%zu)",
|
|
||||||
externalIndex, i, shaderType, spv.size());
|
|
||||||
SpvcSession session(spv, SessionUsageBit::Reflection);
|
|
||||||
auto result = session.ParseMetaData();
|
|
||||||
if (result < 0) {
|
|
||||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - SpvcSession::ParseMetaData failed for module %zu, "
|
|
||||||
"err = %d%s",
|
|
||||||
externalIndex, i, result,
|
|
||||||
(result == SPVC_ERROR_INVALID_SPIRV ? ". Probably no global UBO?" : ""));
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
auto& meta = session.GetMetadata();
|
|
||||||
auto size = meta.globalUboSize;
|
|
||||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - SPIR-V meta: uboSize=%zu plainUniformCount=%zu "
|
|
||||||
"plainUniformOffsets=%zu",
|
|
||||||
externalIndex, meta.globalUboSize, meta.plainUniformMemberSizesInBytes.size(),
|
|
||||||
meta.plainUniformOffsetsInUBO.size());
|
|
||||||
if (size == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (artifacts.globalUboScratch.size() < size) {
|
|
||||||
artifacts.globalUboScratch.resize(size);
|
|
||||||
}
|
|
||||||
for (const auto& [name, offset] : meta.plainUniformOffsetsInUBO) {
|
|
||||||
// SPIRV-Reflect leaf names never carry a "[0]" suffix; frontend
|
|
||||||
// reflection keys arrays as "arr[0]" (GL naming), so retry with the
|
|
||||||
// suffix before declaring the uniform unbacked.
|
|
||||||
auto locationIt = reflection.uniformLocations.find(name);
|
|
||||||
if (locationIt == reflection.uniformLocations.end()) {
|
|
||||||
locationIt = reflection.uniformLocations.find(name + "[0]");
|
|
||||||
}
|
|
||||||
if (locationIt == reflection.uniformLocations.end()) {
|
|
||||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' offset=%u but not found in "
|
|
||||||
"uniformLocations",
|
|
||||||
externalIndex, name.c_str(), offset);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const Uint baseLocation = locationIt->second;
|
|
||||||
if (!ProgramObject::IsValidUniformLocation(reflection, static_cast<Int>(baseLocation))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Int uniformIndex = reflection.uniformIndexInTProgram[baseLocation];
|
|
||||||
const GLint arraySize = ProgramObject::GetUniformArraySizeByTIndex(reflection, uniformIndex);
|
|
||||||
Uint arrayStride = 0;
|
|
||||||
const auto strideIt = meta.plainUniformArrayStridesInUBO.find(name);
|
|
||||||
if (strideIt != meta.plainUniformArrayStridesInUBO.end()) {
|
|
||||||
arrayStride = strideIt->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Array uniforms span one location per element (see DoReflection);
|
|
||||||
// give each element its real byte offset inside the UBO.
|
|
||||||
const GLint elementCount = (arraySize > 1 && arrayStride == 0) ? 1 : std::max(arraySize, 1);
|
|
||||||
for (GLint element = 0; element < elementCount; ++element) {
|
|
||||||
const Uint location = baseLocation + static_cast<Uint>(element);
|
|
||||||
if (location > reflection.maxUniformLocation ||
|
|
||||||
reflection.uniformIndexInTProgram[location] != uniformIndex) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
artifacts.uniformOffsets[location] = offset + static_cast<Uint>(element) * arrayStride;
|
|
||||||
}
|
|
||||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' offset=%u stride=%u assigned "
|
|
||||||
"to locations %u..%u",
|
|
||||||
externalIndex, name.c_str(), offset, arrayStride, baseLocation,
|
|
||||||
baseLocation + static_cast<Uint>(elementCount) - 1);
|
|
||||||
}
|
|
||||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - finished parsing module %zu metadata",
|
|
||||||
externalIndex, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback pass: a linked program's active non-opaque uniforms must accept
|
|
||||||
// glUniform*/glGetUniform* even when the optimized SPIR-V no longer contains
|
|
||||||
// them (AggressiveDCE can remove a dead loop together with the only loads of a
|
|
||||||
// uniform -- or the entire global UBO, leaving the scratch unallocated). Hand
|
|
||||||
// such locations CPU-side storage at the (16-byte aligned) tail of the shadow
|
|
||||||
// buffer; backends bind at least the SPIR-V-declared UBO range, and the GPU
|
|
||||||
// never reads these bytes, so this only keeps the GL-visible state coherent.
|
|
||||||
for (Uint location = 0; location <= reflection.maxUniformLocation; ++location) {
|
|
||||||
if (artifacts.uniformOffsets[location] != ProgramObject::kInvalidUniformOffset) continue;
|
|
||||||
if (!ProgramObject::IsValidUniformLocation(reflection, static_cast<Int>(location))) continue;
|
|
||||||
const auto& uniform = reflection.program->getUniform(reflection.uniformIndexInTProgram[location]);
|
|
||||||
const glslang::TType* type = uniform.getType();
|
|
||||||
if (type != nullptr && type->isOpaque()) continue;
|
|
||||||
if (uniform.index >= 0 && uniform.index < reflection.program->getNumUniformBlocks() &&
|
|
||||||
std::strstr(reflection.program->getUniformBlock(uniform.index).name.c_str(),
|
|
||||||
MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) == nullptr) {
|
|
||||||
// Member of a named uniform block: not settable through glUniform*, so it
|
|
||||||
// needs no global-UBO shadow storage.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// std140-style slot: the matrix upload paths write column vectors at
|
|
||||||
// 16-byte strides, so a matrix slot must cover cols * 16 bytes.
|
|
||||||
SizeT slotSize = MG_Util::GetGLTypeSize(uniform.glDefineType);
|
|
||||||
if (type != nullptr && type->isMatrix()) {
|
|
||||||
slotSize = static_cast<SizeT>(type->getMatrixCols()) * 16u;
|
|
||||||
}
|
|
||||||
slotSize = (slotSize + 15u) & ~static_cast<SizeT>(15u);
|
|
||||||
const SizeT slotOffset = (artifacts.globalUboScratch.size() + 15u) & ~static_cast<SizeT>(15u);
|
|
||||||
artifacts.globalUboScratch.resize(slotOffset + slotSize, 0);
|
|
||||||
artifacts.uniformOffsets[location] = static_cast<Uint>(slotOffset);
|
|
||||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' location %u has no UBO backing in the "
|
|
||||||
"generated SPIR-V (optimized out?); allocated %zu fallback bytes at scratch offset %zu",
|
|
||||||
externalIndex, uniform.name.c_str(), location, slotSize, slotOffset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace MobileGL::MG_State::GLState
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.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/ProgramLinkTask.h>
|
|
||||||
#include <MG_Util/Async/JobNode.h>
|
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
|
||||||
// PHASE B of one glLinkProgram: GlslangToSpv, spirv-opt, and the SPIRV-Cross pass that
|
|
||||||
// builds the glUniform*-to-scratch routing tables. Chained behind exactly one
|
|
||||||
// ProgramLinkTask and joined by exactly five ProgramObject getters (GetGeneratedSpirv,
|
|
||||||
// GetUniformOffset, MapUBO, GetUBOData, GetUBOSize), so ~120 other getters and the whole
|
|
||||||
// GL query surface stay on the phase-A gate and answer without waiting for any of this.
|
|
||||||
//
|
|
||||||
// ---- what this node may read, and what it may not ----
|
|
||||||
// It holds the phase-A node by SharedPtr and reads `phaseA->spirvHandoff` plus
|
|
||||||
// `phaseA->in`. It must NEVER read `phaseA->artifacts` or `phaseA->diagnostics`: the GL
|
|
||||||
// thread MOVES the artifacts out of the node at the phase-A join and DRAINS the
|
|
||||||
// diagnostics there, and both of those can happen while this body runs. The handoff exists
|
|
||||||
// precisely so this node has a copy of everything it needs that the join does not touch.
|
|
||||||
// (The general JobNode rule - a terminal node is immutable, so its outputs need no further
|
|
||||||
// synchronization - covers everything except the two members the join consumes.)
|
|
||||||
//
|
|
||||||
// ---- lifetime ----
|
|
||||||
// The handoff owns the Vector<SharedPtr<glslang::TShader>>, and that is mandatory rather
|
|
||||||
// than tidy: glslang::TProgram stores raw TShader* and, for the one-shader-per-stage case,
|
|
||||||
// BORROWS each stage's TIntermediate from its TShader. GlslangToSpv reads exactly those
|
|
||||||
// intermediates. Before the split the shaders died when ProgramLinkTask::RunBody returned,
|
|
||||||
// which was safe only because nothing called getIntermediate() afterwards.
|
|
||||||
//
|
|
||||||
// ---- failure ----
|
|
||||||
// A cancel (relink, teardown, program destruction) or an optimizer failure publishes
|
|
||||||
// spirvStatus = false rather than a half-built program. GL cannot retract a LINK_STATUS it
|
|
||||||
// already reported true, so such a program stays linked and fully queryable; it is just
|
|
||||||
// not drawable, which the backends express through their existing link-status gates.
|
|
||||||
class ProgramSpirvTask final : public MG_Util::Async::JobNode {
|
|
||||||
public:
|
|
||||||
// ---- output: valid iff IsComplete(), immutable afterwards ----
|
|
||||||
// Moved (never copied) into the ProgramObject by EnsureSpirvJoined().
|
|
||||||
ProgramObject::SpirvArtifacts artifacts;
|
|
||||||
|
|
||||||
// Posts this job when `phaseA` goes terminal - and not one moment earlier, so the body
|
|
||||||
// never waits on anything (invariant I4: no job body may block on another job). A
|
|
||||||
// single dependency needs no counter, just the one continuation; it runs inline right
|
|
||||||
// here if `phaseA` is already terminal, which is the same case
|
|
||||||
// ProgramLinkTask::SubmitAfter already reasons about.
|
|
||||||
//
|
|
||||||
// GL thread only, and only after the caller has stored a SharedPtr to this node: the
|
|
||||||
// continuation takes shared_from_this().
|
|
||||||
void SubmitAfter(const SharedPtr<ProgramLinkTask>& phaseA);
|
|
||||||
|
|
||||||
// The async-off / glMaxShaderCompilerThreadsKHR(0) path: run the body on the calling
|
|
||||||
// thread, right now, against an ALREADY-TERMINAL phase A. Deliberately not routed
|
|
||||||
// through SubmitAfter, whose continuation would Post() to a pool that is merely
|
|
||||||
// unused rather than stopped - that would move the work off-thread in the one mode
|
|
||||||
// whose contract is "byte-identical to the synchronous implementation".
|
|
||||||
void RunInlineAfter(const SharedPtr<ProgramLinkTask>& phaseA);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void RunBody() override;
|
|
||||||
|
|
||||||
void GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
|
|
||||||
void BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
|
|
||||||
|
|
||||||
// Worker-side MGLOG replacement, replayed by the join on the GL thread. Same reason as
|
|
||||||
// ProgramLinkTask::DeferLog.
|
|
||||||
void DeferLog(String line);
|
|
||||||
|
|
||||||
SharedPtr<ProgramLinkTask> m_phaseA;
|
|
||||||
};
|
|
||||||
} // namespace MobileGL::MG_State::GLState
|
|
||||||
@@ -111,13 +111,9 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// that can grow, and a reallocation underneath this loop would be a use-after-free
|
// 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
|
// 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.
|
// refcount bump on a path a mode switch takes at most once.
|
||||||
// BOTH phases per program. This is the glMaxShaderCompilerThreadsKHR(0) path, whose
|
|
||||||
// contract is that nothing is outstanding when it returns - a program left with its
|
|
||||||
// SPIR-V job in flight would make the very next GL_COMPLETION_STATUS_KHR read GL_FALSE
|
|
||||||
// in a mode the extension says cannot have anything pending.
|
|
||||||
for (SizeT i = 0; i < m_programObjects.size(); ++i) {
|
for (SizeT i = 0; i < m_programObjects.size(); ++i) {
|
||||||
const SharedPtr<ProgramObject> program = m_programObjects[i];
|
const SharedPtr<ProgramObject> program = m_programObjects[i];
|
||||||
if (program) program->JoinLinkAndSpirv();
|
if (program) program->JoinLink();
|
||||||
}
|
}
|
||||||
for (SizeT i = 0; i < m_shaderObjects.size(); ++i) {
|
for (SizeT i = 0; i < m_shaderObjects.size(); ++i) {
|
||||||
const SharedPtr<ShaderObject> shader = m_shaderObjects[i];
|
const SharedPtr<ShaderObject> shader = m_shaderObjects[i];
|
||||||
@@ -126,7 +122,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// The currently-used program is reachable through m_programObjects unless
|
// The currently-used program is reachable through m_programObjects unless
|
||||||
// glDeleteProgram already freed its slot while it stayed current. Nothing else holds
|
// 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.
|
// a GL-visible name for it, but a draw would still join it, so settle it here too.
|
||||||
if (m_currentProgram) m_currentProgram->JoinLinkAndSpirv();
|
if (m_currentProgram) m_currentProgram->JoinLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramState::MarkShaderObjectForDeletion(Uint shader) {
|
void ProgramState::MarkShaderObjectForDeletion(Uint shader) {
|
||||||
|
|||||||
@@ -88,19 +88,12 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// another object, THIS object has not pulled its result yet. (An adopted node may
|
// 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.)
|
// already be terminal - the join then only replays what is left of its diagnostics.)
|
||||||
m_compileJoined = false;
|
m_compileJoined = false;
|
||||||
// A new compile is a new story: whatever the optimistic getters promised about the
|
|
||||||
// previous node does not carry over.
|
|
||||||
m_optimisticAnswerLatched = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShaderObject::DropCompileNode() const {
|
void ShaderObject::DropCompileNode() const {
|
||||||
if (!m_compiled) return;
|
if (!m_compiled) return;
|
||||||
m_compiled->ReleaseAdopter();
|
m_compiled->ReleaseAdopter();
|
||||||
m_compiled.reset();
|
m_compiled.reset();
|
||||||
// No node means IsCompileComplete() is trivially true and the truthful answers are
|
|
||||||
// "not compiled"; a stale latch would keep reporting a compile that no longer
|
|
||||||
// exists as GL_TRUE.
|
|
||||||
m_optimisticAnswerLatched = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShaderObject::InvalidateCompiledState() {
|
void ShaderObject::InvalidateCompiledState() {
|
||||||
|
|||||||
@@ -116,10 +116,8 @@ namespace MobileGL {
|
|||||||
Bool GetDeleteStatus() const { return m_deleteStatus; }
|
Bool GetDeleteStatus() const { return m_deleteStatus; }
|
||||||
|
|
||||||
// Blocks until a pending compile has published its artifacts. Public for the
|
// Blocks until a pending compile has published its artifacts. Public for the
|
||||||
// sites that must join without reading anything - ProgramState::
|
// sites that must join without reading anything - ProgramObject::Link's
|
||||||
// JoinAllPendingWork, the glMaxShaderCompilerThreadsKHR(0) path that settles
|
// prologue, which needs every attached shader settled before it runs.
|
||||||
// every outstanding job. glLinkProgram deliberately does NOT come through
|
|
||||||
// here: its prologue takes the nodes unjoined via CompiledNodeForLink().
|
|
||||||
void JoinCompile() const { EnsureCompileJoined(); }
|
void JoinCompile() const { EnsureCompileJoined(); }
|
||||||
|
|
||||||
// True while this object holds the outcome (success OR failure) of a Compile()
|
// True while this object holds the outcome (success OR failure) of a Compile()
|
||||||
@@ -143,23 +141,6 @@ namespace MobileGL {
|
|||||||
// outstanding to wait for.
|
// outstanding to wait for.
|
||||||
Bool IsCompileComplete() const { return m_compiled == nullptr || m_compiled->IsTerminal(); }
|
Bool IsCompileComplete() const { return m_compiled == nullptr || m_compiled->IsTerminal(); }
|
||||||
|
|
||||||
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS's one-story-per-compile memory. The
|
|
||||||
// three optimistic getter sites in GL_Program ask THIS instead of a raw
|
|
||||||
// IsCompileComplete() peek, and the difference is the latch: without it, a job
|
|
||||||
// that settles between two adjacent queries hands the application a torn pair -
|
|
||||||
// an empty info log from the optimistic read, then the real GL_FALSE from the
|
|
||||||
// truthful one - and an application that aborts on that status never reaches
|
|
||||||
// the link join that quotes the real log. So the first optimistic answer
|
|
||||||
// latches: until the next AdoptCompileNode/DropCompileNode this object keeps
|
|
||||||
// answering optimistically even after the job settles, and a real failure
|
|
||||||
// surfaces exactly once, at the link. Returns whether the caller should answer
|
|
||||||
// optimistically; the caller has already checked the quirk is active.
|
|
||||||
Bool TakeOptimisticCompileAnswer() const {
|
|
||||||
if (!m_optimisticAnswerLatched && IsCompileComplete()) return false;
|
|
||||||
m_optimisticAnswerLatched = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// ---- The one and only join gate for compile output (P1 invariant I5) ----
|
// ---- 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 -
|
// The fast path - no job, or a job whose result this object has already pulled -
|
||||||
@@ -250,10 +231,6 @@ namespace MobileGL {
|
|||||||
// Exactly-once latch for the pull above. Armed with every new job node, set by
|
// Exactly-once latch for the pull above. Armed with every new job node, set by
|
||||||
// the one join that consumes it.
|
// the one join that consumes it.
|
||||||
mutable Bool m_compileJoined = false;
|
mutable Bool m_compileJoined = false;
|
||||||
// TakeOptimisticCompileAnswer's memory: this object has answered a compile
|
|
||||||
// query optimistically for the current node. Cleared wherever the node
|
|
||||||
// changes hands (AdoptCompileNode) or goes away (DropCompileNode).
|
|
||||||
mutable Bool m_optimisticAnswerLatched = false;
|
|
||||||
};
|
};
|
||||||
} // namespace MG_State::GLState
|
} // namespace MG_State::GLState
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#include "RenderState.h"
|
#include "RenderState.h"
|
||||||
#include "MG_Util/Debug/Log.h"
|
|
||||||
#include "MG_Util/Types.h"
|
#include "MG_Util/Types.h"
|
||||||
|
|
||||||
namespace MobileGL {
|
namespace MobileGL {
|
||||||
@@ -269,14 +268,9 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RenderState::SetCapabilityIndexed(CapabilityInput cap, Uint index, Bool enabled) {
|
void RenderState::SetCapabilityIndexed(CapabilityInput cap, Uint index, Bool enabled) {
|
||||||
// Only for BlendState currently. The GL entry points (glEnablei/glDisablei) already
|
// Only for BlendState currently
|
||||||
// reject every non-GL_BLEND target with GL_INVALID_ENUM before reaching here, so this
|
|
||||||
// is a backstop - but it must stay a backstop: THROW_UNIMPL_EXCEPTION unwinds a C++
|
|
||||||
// exception through the C GL ABI and terminates the process.
|
|
||||||
if (cap != CapabilityInput::Blend) {
|
if (cap != CapabilityInput::Blend) {
|
||||||
MGLOG_I("RenderState::SetCapabilityIndexed: indexed capability state exists only for "
|
THROW_UNIMPL_EXCEPTION;
|
||||||
"GL_BLEND (cap=%d, index=%u); ignoring",
|
|
||||||
static_cast<int>(cap), index);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
|
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
|
||||||
@@ -290,13 +284,9 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Bool RenderState::IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const {
|
Bool RenderState::IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const {
|
||||||
// Only for BlendState currently - same backstop reasoning as SetCapabilityIndexed:
|
// Only for BlendState currently
|
||||||
// glIsEnabledi has already answered GL_INVALID_ENUM/GL_FALSE for anything else, and a
|
|
||||||
// query must never be able to terminate the process.
|
|
||||||
if (cap != CapabilityInput::Blend) {
|
if (cap != CapabilityInput::Blend) {
|
||||||
MGLOG_I("RenderState::IsCapabilityEnabledIndexed: indexed capability state exists only "
|
THROW_UNIMPL_EXCEPTION;
|
||||||
"for GL_BLEND (cap=%d, index=%u); reporting disabled",
|
|
||||||
static_cast<int>(cap), index);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
|
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
attr.Normalized = false;
|
attr.Normalized = false;
|
||||||
attr.Stride = 0;
|
attr.Stride = 0;
|
||||||
attr.Offset = 0;
|
attr.Offset = 0;
|
||||||
attr.LegacyStride = 0;
|
|
||||||
attr.LegacyPointer = 0;
|
|
||||||
attr.Buffer = nullptr;
|
attr.Buffer = nullptr;
|
||||||
|
|
||||||
BumpAttributeFormatVersion(index);
|
BumpAttributeFormatVersion(index);
|
||||||
@@ -63,19 +61,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
void VertexArrayObject::SetAttributeFormat(Uint index, int size, DataType type, Bool normalized, int stride,
|
void VertexArrayObject::SetAttributeFormat(Uint index, int size, DataType type, Bool normalized, int stride,
|
||||||
SizeT offset, Bool isInteger, Bool isBgra) {
|
SizeT offset, Bool isInteger, Bool isBgra) {
|
||||||
if (index >= MAX_VERTEX_ATTRIBS) return;
|
if (index >= MAX_VERTEX_ATTRIBS) return;
|
||||||
if (size < 1 || size > 4) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The classic pointer-style API takes back full ownership of the resolved fields.
|
// The classic pointer-style API takes back full ownership of the resolved fields.
|
||||||
m_attributeUsesBindingModel[index] = false;
|
m_attributeUsesBindingModel[index] = false;
|
||||||
|
|
||||||
// The legacy query shadows: written here and nowhere else, so a later binding-model
|
|
||||||
// mutation cannot leak into VERTEX_ATTRIB_ARRAY_STRIDE / _POINTER. They are pure
|
|
||||||
// query state, so they carry no version bump of their own.
|
|
||||||
m_attributes[index].LegacyStride = stride;
|
|
||||||
m_attributes[index].LegacyPointer = offset;
|
|
||||||
|
|
||||||
if (m_attributes[index].Size == size && m_attributes[index].Type == type &&
|
if (m_attributes[index].Size == size && m_attributes[index].Type == type &&
|
||||||
m_attributes[index].Normalized == normalized && m_attributes[index].Stride == stride &&
|
m_attributes[index].Normalized == normalized && m_attributes[index].Stride == stride &&
|
||||||
m_attributes[index].Offset == offset && m_attributes[index].IsInteger == isInteger &&
|
m_attributes[index].Offset == offset && m_attributes[index].IsInteger == isInteger &&
|
||||||
@@ -83,6 +72,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (size < 1 || size > 4) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto& attr = m_attributes[index];
|
auto& attr = m_attributes[index];
|
||||||
attr.Size = size;
|
attr.Size = size;
|
||||||
attr.Type = type;
|
attr.Type = type;
|
||||||
@@ -118,12 +111,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
binding.Offset = offset;
|
binding.Offset = offset;
|
||||||
binding.Stride = effectiveStride;
|
binding.Stride = effectiveStride;
|
||||||
binding.Divisor = m_attributes[index].Divisor;
|
binding.Divisor = m_attributes[index].Divisor;
|
||||||
|
|
||||||
// Other attributes may already be pointed at this binding point through
|
|
||||||
// glVertexAttribBinding; they see the new buffer/offset/stride too (basic-state3
|
|
||||||
// checks exactly that after a glVertexAttribPointer). They are not adopted into the
|
|
||||||
// binding model here - only the ones already in it re-resolve.
|
|
||||||
ResolveAttributesForBinding(index, /*adopt: */ false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void VertexArrayObject::BindAttributeBuffer(Uint index, const SharedPtr<BufferObject>& buffer) {
|
void VertexArrayObject::BindAttributeBuffer(Uint index, const SharedPtr<BufferObject>& buffer) {
|
||||||
@@ -160,24 +147,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
void VertexArrayObject::SetAttributeDivisor(Uint index, Uint divisor) {
|
void VertexArrayObject::SetAttributeDivisor(Uint index, Uint divisor) {
|
||||||
if (index >= MAX_VERTEX_ATTRIBS) return;
|
if (index >= MAX_VERTEX_ATTRIBS) return;
|
||||||
// GL 4.6 core 10.3.2 defines VertexAttribDivisor(i, d) as
|
// glVertexAttribDivisor is VertexBindingDivisor on the attribute's own binding point
|
||||||
// VertexAttribBinding(i, i); VertexBindingDivisor(i, d)
|
// (GL 4.6 core 10.3.2), so the binding-point view has to follow the resolved attribute.
|
||||||
// - the binding is RE-POINTED at i, it is not merely written through when it already
|
if (index < MAX_VERTEX_ATTRIB_BINDINGS && m_attributeBindingIndex[index] == index) {
|
||||||
// happens to be i. Guarding the write on "binding == index" (which is what this did)
|
|
||||||
// left an attribute that glVertexAttribBinding had moved elsewhere pointing at the old
|
|
||||||
// binding, so the next resolve restored that binding's divisor and the new one was
|
|
||||||
// lost (KHR-GL4x.vertex_attrib_binding.basic-state4).
|
|
||||||
//
|
|
||||||
// What is deliberately NOT copied from VertexAttribBinding is the adoption into the
|
|
||||||
// binding model: an attribute configured the classic way keeps its pointer-resolved
|
|
||||||
// stride/offset, exactly as before. The binding point mirrors that state already
|
|
||||||
// (MirrorPointerIntoBinding), so nothing observable differs - and adopting it here
|
|
||||||
// would silently swap the raw pointer stride for the effective one under every
|
|
||||||
// application that calls glVertexAttribDivisor after glVertexAttribPointer.
|
|
||||||
if (index < MAX_VERTEX_ATTRIB_BINDINGS) {
|
|
||||||
m_attributeBindingIndex[index] = index;
|
|
||||||
m_bindingPoints[index].Divisor = divisor;
|
m_bindingPoints[index].Divisor = divisor;
|
||||||
ResolveAttributesForBinding(index, /*adopt: */ false);
|
|
||||||
}
|
}
|
||||||
if (m_attributes[index].Divisor == divisor) return;
|
if (m_attributes[index].Divisor == divisor) return;
|
||||||
m_attributes[index].Divisor = divisor;
|
m_attributes[index].Divisor = divisor;
|
||||||
@@ -191,6 +164,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
void VertexArrayObject::ResolveAttributeFromBinding(Uint attribIndex) {
|
void VertexArrayObject::ResolveAttributeFromBinding(Uint attribIndex) {
|
||||||
if (attribIndex >= MAX_VERTEX_ATTRIBS) return;
|
if (attribIndex >= MAX_VERTEX_ATTRIBS) return;
|
||||||
|
if (!m_attributeUsesBindingModel[attribIndex]) return;
|
||||||
|
|
||||||
const Uint bindingIndex = m_attributeBindingIndex[attribIndex];
|
const Uint bindingIndex = m_attributeBindingIndex[attribIndex];
|
||||||
if (bindingIndex >= MAX_VERTEX_ATTRIB_BINDINGS) return;
|
if (bindingIndex >= MAX_VERTEX_ATTRIB_BINDINGS) return;
|
||||||
@@ -198,24 +172,11 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
auto& attr = m_attributes[attribIndex];
|
auto& attr = m_attributes[attribIndex];
|
||||||
|
|
||||||
// VERTEX_ATTRIB_ARRAY_DIVISOR is not independent per-attribute state: it IS the divisor
|
|
||||||
// of the binding point the attribute is attached to (GL 4.6 core 10.3.2), whichever API
|
|
||||||
// configured the attribute. glVertexBindingDivisor therefore has to reach a classic
|
|
||||||
// pointer-configured attribute as well - basic-state4 alternates the two spellings on
|
|
||||||
// the same attribute and expects each to win in turn.
|
|
||||||
if (attr.Divisor != binding.Divisor) {
|
|
||||||
attr.Divisor = binding.Divisor;
|
|
||||||
BumpAttributeFormatVersion(attribIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Everything else stays owned by whichever API configured the attribute: a classic
|
|
||||||
// glVertexAttrib*Pointer attribute keeps its pointer-resolved stride and offset.
|
|
||||||
if (!m_attributeUsesBindingModel[attribIndex]) return;
|
|
||||||
|
|
||||||
const SizeT resolvedOffset = binding.Offset + m_attributeRelativeOffset[attribIndex];
|
const SizeT resolvedOffset = binding.Offset + m_attributeRelativeOffset[attribIndex];
|
||||||
if (attr.Stride != binding.Stride || attr.Offset != resolvedOffset) {
|
if (attr.Stride != binding.Stride || attr.Offset != resolvedOffset || attr.Divisor != binding.Divisor) {
|
||||||
attr.Stride = binding.Stride;
|
attr.Stride = binding.Stride;
|
||||||
attr.Offset = resolvedOffset;
|
attr.Offset = resolvedOffset;
|
||||||
|
attr.Divisor = binding.Divisor;
|
||||||
BumpAttributeFormatVersion(attribIndex);
|
BumpAttributeFormatVersion(attribIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,14 +186,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VertexArrayObject::ResolveAttributesForBinding(Uint bindingIndex, Bool adopt) {
|
|
||||||
for (Uint attribIndex = 0; attribIndex < MAX_VERTEX_ATTRIBS; ++attribIndex) {
|
|
||||||
if (m_attributeBindingIndex[attribIndex] != bindingIndex) continue;
|
|
||||||
if (adopt) m_attributeUsesBindingModel[attribIndex] = true;
|
|
||||||
ResolveAttributeFromBinding(attribIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void VertexArrayObject::SetBindingBuffer(Uint bindingIndex, const SharedPtr<BufferObject>& buffer, SizeT offset,
|
void VertexArrayObject::SetBindingBuffer(Uint bindingIndex, const SharedPtr<BufferObject>& buffer, SizeT offset,
|
||||||
int stride) {
|
int stride) {
|
||||||
if (bindingIndex >= MAX_VERTEX_ATTRIB_BINDINGS) return;
|
if (bindingIndex >= MAX_VERTEX_ATTRIB_BINDINGS) return;
|
||||||
@@ -242,10 +195,15 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
binding.Offset = offset;
|
binding.Offset = offset;
|
||||||
binding.Stride = stride;
|
binding.Stride = stride;
|
||||||
|
|
||||||
// Binding a vertex buffer to a binding point adopts every attribute currently mapped to
|
for (Uint attribIndex = 0; attribIndex < MAX_VERTEX_ATTRIBS; ++attribIndex) {
|
||||||
// that binding point into the binding model (the default mapping is attribute i ->
|
if (m_attributeBindingIndex[attribIndex] == bindingIndex) {
|
||||||
// binding i, which matches the GL 4.3 rules for state mixing).
|
// Binding a vertex buffer to a binding point adopts every attribute currently
|
||||||
ResolveAttributesForBinding(bindingIndex, /*adopt: */ true);
|
// mapped to that binding point into the binding model (the default mapping is
|
||||||
|
// attribute i -> binding i, which matches the GL 4.3 rules for state mixing).
|
||||||
|
m_attributeUsesBindingModel[attribIndex] = true;
|
||||||
|
ResolveAttributeFromBinding(attribIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VertexArrayObject::SetBindingDivisor(Uint bindingIndex, Uint divisor) {
|
void VertexArrayObject::SetBindingDivisor(Uint bindingIndex, Uint divisor) {
|
||||||
@@ -253,7 +211,11 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
m_bindingPoints[bindingIndex].Divisor = divisor;
|
m_bindingPoints[bindingIndex].Divisor = divisor;
|
||||||
|
|
||||||
ResolveAttributesForBinding(bindingIndex, /*adopt: */ false);
|
for (Uint attribIndex = 0; attribIndex < MAX_VERTEX_ATTRIBS; ++attribIndex) {
|
||||||
|
if (m_attributeBindingIndex[attribIndex] == bindingIndex && m_attributeUsesBindingModel[attribIndex]) {
|
||||||
|
ResolveAttributeFromBinding(attribIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VertexArrayObject::SetAttributeBinding(Uint attribIndex, Uint bindingIndex) {
|
void VertexArrayObject::SetAttributeBinding(Uint attribIndex, Uint bindingIndex) {
|
||||||
|
|||||||
@@ -32,16 +32,6 @@ namespace MobileGL {
|
|||||||
Bool IsBgra = false;
|
Bool IsBgra = false;
|
||||||
Uint Divisor = 0;
|
Uint Divisor = 0;
|
||||||
SharedPtr<BufferObject> Buffer;
|
SharedPtr<BufferObject> Buffer;
|
||||||
|
|
||||||
// GL 4.6 core table 23.3: VERTEX_ATTRIB_ARRAY_STRIDE and _POINTER are the
|
|
||||||
// arguments of the last glVertexAttrib*Pointer call on this attribute,
|
|
||||||
// reported verbatim, and NOTHING else writes them - not glVertexAttribFormat,
|
|
||||||
// not glBindVertexBuffer. Stride/Offset above are the *resolved* draw inputs
|
|
||||||
// and the binding model does overwrite those, so the two views have to be
|
|
||||||
// stored apart or the binding-model sequence reports a legacy state it never
|
|
||||||
// set (KHR-GL4x.vertex_attrib_binding.basic-state3).
|
|
||||||
int LegacyStride = 0;
|
|
||||||
SizeT LegacyPointer = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ARB_vertex_attrib_binding separate binding point. Attributes configured through the
|
// ARB_vertex_attrib_binding separate binding point. Attributes configured through the
|
||||||
@@ -50,8 +40,7 @@ namespace MobileGL {
|
|||||||
struct VertexBufferBindingPoint {
|
struct VertexBufferBindingPoint {
|
||||||
SharedPtr<BufferObject> Buffer;
|
SharedPtr<BufferObject> Buffer;
|
||||||
SizeT Offset = 0;
|
SizeT Offset = 0;
|
||||||
// GL 4.6 core table 23.4: the initial VERTEX_BINDING_STRIDE is 16, not 0.
|
int Stride = 0;
|
||||||
int Stride = 16;
|
|
||||||
Uint Divisor = 0;
|
Uint Divisor = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -196,10 +185,6 @@ namespace MobileGL {
|
|||||||
void BumpAttributeBufferVersion(Uint index);
|
void BumpAttributeBufferVersion(Uint index);
|
||||||
void BumpAttributeSwitchVersion(Uint index);
|
void BumpAttributeSwitchVersion(Uint index);
|
||||||
void ResolveAttributeFromBinding(Uint attribIndex);
|
void ResolveAttributeFromBinding(Uint attribIndex);
|
||||||
// Re-resolve every attribute currently pointed at `bindingIndex`. `adopt` turns
|
|
||||||
// the ones that are not in the binding model yet into binding-model attributes
|
|
||||||
// first (what glBindVertexBuffer does, GL 4.3 rules for state mixing).
|
|
||||||
void ResolveAttributesForBinding(Uint bindingIndex, Bool adopt);
|
|
||||||
|
|
||||||
// The default mapping is attribute i -> binding point i. Keep it an iota over
|
// The default mapping is attribute i -> binding point i. Keep it an iota over
|
||||||
// MAX_VERTEX_ATTRIBS rather than a literal list: a literal list silently leaves the
|
// MAX_VERTEX_ATTRIBS rather than a literal list: a literal list silently leaves the
|
||||||
|
|||||||
@@ -117,26 +117,11 @@ void main() { fragColor = thisIdentifierWasNeverDeclared; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The non-joining view of the program, i.e. what GL_COMPLETION_STATUS_KHR will report.
|
// The non-joining view of the program, i.e. what GL_COMPLETION_STATUS_KHR will report.
|
||||||
// BOTH phases: a program whose SPIR-V job is still in flight is not finished, even though
|
|
||||||
// its whole GL query surface already answers.
|
|
||||||
Bool LinkIsSettled(const GLuint program) {
|
Bool LinkIsSettled(const GLuint program) {
|
||||||
const auto& object = MG_State::pGLContext->GetProgramObject(program);
|
const auto& object = MG_State::pGLContext->GetProgramObject(program);
|
||||||
return object == nullptr || object->IsLinkComplete();
|
return object == nullptr || object->IsLinkComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase A alone: the half that decides LINK_STATUS, the info log, and every reflection
|
|
||||||
// query. This is what a read of LINK_STATUS is required to settle.
|
|
||||||
Bool PhaseALinkIsSettled(const GLuint program) {
|
|
||||||
const auto& object = MG_State::pGLContext->GetProgramObject(program);
|
|
||||||
return object == nullptr || object->IsPhaseALinkComplete();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase B alone: SPIR-V + the uniform shadow's layout.
|
|
||||||
Bool SpirvIsSettled(const GLuint program) {
|
|
||||||
const auto& object = MG_State::pGLContext->GetProgramObject(program);
|
|
||||||
return object == nullptr || object->IsSpirvComplete();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enqueues `count` distinct heavy compiles without reading anything back, so the pool is
|
// Enqueues `count` distinct heavy compiles without reading anything back, so the pool is
|
||||||
// left with a real backlog for the caller to race against.
|
// left with a real backlog for the caller to race against.
|
||||||
Vector<GLuint> SaturatePool(const int count, Vector<String>& sourceStorage) {
|
Vector<GLuint> SaturatePool(const int count, Vector<String>& sourceStorage) {
|
||||||
@@ -531,63 +516,11 @@ TEST_F(AsyncLinkTest, LinkProgramReturnsBeforeTheWorkIsDone) {
|
|||||||
|
|
||||||
for (const GLuint program : programs) {
|
for (const GLuint program : programs) {
|
||||||
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE) << QueryProgramInfoLog(program);
|
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE) << QueryProgramInfoLog(program);
|
||||||
// PHASE A only. Reading LINK_STATUS settles the half that decides it, and no more -
|
EXPECT_TRUE(LinkIsSettled(program)) << "reading LINK_STATUS must have joined";
|
||||||
// the SPIR-V job may well still be running, which is the entire point of the split.
|
|
||||||
EXPECT_TRUE(PhaseALinkIsSettled(program)) << "reading LINK_STATUS must have joined phase A";
|
|
||||||
}
|
}
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The other half of the previous case, and the property the two-phase split exists for:
|
|
||||||
// LINK_STATUS is answerable without the SPIR-V, so a run of LINK_STATUS reads over a
|
|
||||||
// backlog must leave SPIR-V jobs outstanding rather than draining them one by one.
|
|
||||||
TEST_F(AsyncLinkTest, ReadingLinkStatusDoesNotSettleTheSpirvJob) {
|
|
||||||
const AsyncModeScope async(true);
|
|
||||||
MG_Util::Async::ShaderCompilePool::Get().SetMaxConcurrency(1);
|
|
||||||
constexpr int kPrograms = 24;
|
|
||||||
|
|
||||||
const GLuint vs = MakeShader(GL_VERTEX_SHADER, kVs);
|
|
||||||
Vector<GLuint> programs;
|
|
||||||
Vector<String> sources;
|
|
||||||
for (int i = 0; i < kPrograms; ++i) {
|
|
||||||
sources.push_back(MakeBulkySource(7900 + i));
|
|
||||||
const char* text = sources.back().c_str();
|
|
||||||
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
|
||||||
ShaderSource(fs, 1, &text, nullptr);
|
|
||||||
CompileShader(fs);
|
|
||||||
const GLuint program = CreateProgram();
|
|
||||||
AttachShader(program, vs);
|
|
||||||
AttachShader(program, fs);
|
|
||||||
LinkProgram(program);
|
|
||||||
programs.push_back(program);
|
|
||||||
}
|
|
||||||
|
|
||||||
int spirvOutstanding = 0;
|
|
||||||
for (int i = 0; i < kPrograms; ++i) {
|
|
||||||
const GLuint program = programs[static_cast<SizeT>(i)];
|
|
||||||
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE) << QueryProgramInfoLog(program);
|
|
||||||
EXPECT_TRUE(PhaseALinkIsSettled(program)) << "reading LINK_STATUS must have joined phase A";
|
|
||||||
// Reflection has to answer here too, out of phase A and with no further join.
|
|
||||||
const String uniformName = "uSeed" + std::to_string(7900 + i);
|
|
||||||
EXPECT_GE(GetUniformLocation(program, uniformName.c_str()), 0) << uniformName;
|
|
||||||
if (!SpirvIsSettled(program)) ++spirvOutstanding;
|
|
||||||
}
|
|
||||||
EXPECT_GT(spirvOutstanding, 0) << "the whole GL query surface was answered and yet every SPIR-V job had "
|
|
||||||
"already been drained - the reads are joining phase B";
|
|
||||||
|
|
||||||
// And the SPIR-V gate really is a gate: touching it settles the job.
|
|
||||||
for (const GLuint program : programs) {
|
|
||||||
const auto& object = MG_State::pGLContext->GetProgramObject(program);
|
|
||||||
ASSERT_NE(object, nullptr);
|
|
||||||
EXPECT_GT(object->GetGeneratedSpirv().size(), 0u);
|
|
||||||
EXPECT_TRUE(SpirvIsSettled(program));
|
|
||||||
EXPECT_TRUE(LinkIsSettled(program));
|
|
||||||
}
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
MG_Util::Async::ShaderCompilePool::Get().SetMaxConcurrency(
|
|
||||||
MG_Util::Async::ShaderCompilePool::Get().GetThreadCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
// With the flag off, a link is finished by the time glLinkProgram returns. This is the guard
|
// With the flag off, a link is finished by the time glLinkProgram returns. This is the guard
|
||||||
// that keeps the default shippable.
|
// that keeps the default shippable.
|
||||||
TEST_F(AsyncLinkTest, LinkIsFullySynchronousWithAsyncOff) {
|
TEST_F(AsyncLinkTest, LinkIsFullySynchronousWithAsyncOff) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -49,22 +49,6 @@ add_executable(
|
|||||||
AsyncLinkTest.cpp
|
AsyncLinkTest.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(
|
|
||||||
OptimisticStatusTest
|
|
||||||
OptimisticStatusTest.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(OptimisticStatusTest PRIVATE
|
|
||||||
${MGL_ROOT}/include
|
|
||||||
${MGL_ROOT}/MobileGL
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
OptimisticStatusTest PRIVATE
|
|
||||||
GTest::gtest_main
|
|
||||||
${LINK_LIBRARIES}
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(AsyncLinkTest PRIVATE
|
target_include_directories(AsyncLinkTest PRIVATE
|
||||||
${MGL_ROOT}/include
|
${MGL_ROOT}/include
|
||||||
${MGL_ROOT}/MobileGL
|
${MGL_ROOT}/MobileGL
|
||||||
@@ -76,24 +60,6 @@ target_link_libraries(
|
|||||||
${LINK_LIBRARIES}
|
${LINK_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Its own binary, like the other async suites: its cases pin the compile pool down to one
|
|
||||||
# worker so a phase-B job really is still queued while the GL query surface is being read.
|
|
||||||
add_executable(
|
|
||||||
AsyncSpirvPhaseTest
|
|
||||||
AsyncSpirvPhaseTest.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(AsyncSpirvPhaseTest PRIVATE
|
|
||||||
${MGL_ROOT}/include
|
|
||||||
${MGL_ROOT}/MobileGL
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
AsyncSpirvPhaseTest PRIVATE
|
|
||||||
GTest::gtest_main
|
|
||||||
${LINK_LIBRARIES}
|
|
||||||
)
|
|
||||||
|
|
||||||
add_executable(
|
add_executable(
|
||||||
ShaderCompileAdoptionTest
|
ShaderCompileAdoptionTest
|
||||||
ShaderCompileAdoptionTest.cpp
|
ShaderCompileAdoptionTest.cpp
|
||||||
@@ -164,22 +130,6 @@ target_link_libraries(
|
|||||||
${LINK_LIBRARIES}
|
${LINK_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(
|
|
||||||
XfbBlockVaryingTest
|
|
||||||
XfbBlockVaryingTest.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(XfbBlockVaryingTest PRIVATE
|
|
||||||
${MGL_ROOT}/include
|
|
||||||
${MGL_ROOT}/MobileGL
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
XfbBlockVaryingTest PRIVATE
|
|
||||||
GTest::gtest_main
|
|
||||||
${LINK_LIBRARIES}
|
|
||||||
)
|
|
||||||
|
|
||||||
add_executable(
|
add_executable(
|
||||||
ProgramInterfaceTest
|
ProgramInterfaceTest
|
||||||
ProgramInterfaceTest.cpp
|
ProgramInterfaceTest.cpp
|
||||||
@@ -211,22 +161,15 @@ include(GoogleTest)
|
|||||||
gtest_discover_tests(ProgramUtilTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(ProgramUtilTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
gtest_discover_tests(ProgramTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(ProgramTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
gtest_discover_tests(ProgramInterfaceTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(ProgramInterfaceTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
gtest_discover_tests(XfbBlockVaryingTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
|
||||||
# Heavier than the rest of the unit suite by design: several cases deliberately saturate the
|
# Heavier than the rest of the unit suite by design: several cases deliberately saturate the
|
||||||
# compile pool so there is something in flight to race against.
|
# compile pool so there is something in flight to race against.
|
||||||
gtest_discover_tests(AsyncCompileTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
gtest_discover_tests(AsyncCompileTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||||
gtest_discover_tests(AsyncLinkTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
gtest_discover_tests(AsyncLinkTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||||
# Same reason: every case here links a batch against a one-worker pool so that phase B is
|
|
||||||
# genuinely outstanding while phase A is being interrogated.
|
|
||||||
gtest_discover_tests(AsyncSpirvPhaseTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
|
||||||
# Same reason: the stage-6 cases keep a backlog in flight so a release really can race a
|
# Same reason: the stage-6 cases keep a backlog in flight so a release really can race a
|
||||||
# worker, and the 48-object stress links every one of them.
|
# worker, and the 48-object stress links every one of them.
|
||||||
gtest_discover_tests(ShaderCompileAdoptionTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
gtest_discover_tests(ShaderCompileAdoptionTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||||
# Same reason: the GL_COMPLETION_STATUS_KHR cases saturate a one-worker pool on purpose.
|
# Same reason: the GL_COMPLETION_STATUS_KHR cases saturate a one-worker pool on purpose.
|
||||||
gtest_discover_tests(ParallelShaderCompileTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
gtest_discover_tests(ParallelShaderCompileTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||||
# Same reason: the optimistic-window cases need a saturated one-worker pool to observe an
|
|
||||||
# in-flight compile, and the two-phase replay links 48 programs across both flag states.
|
|
||||||
gtest_discover_tests(OptimisticStatusTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
|
||||||
gtest_discover_tests(AsyncTeardownTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
gtest_discover_tests(AsyncTeardownTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||||
# Same reason again: several cases leave A links outstanding while B compiles and links.
|
# Same reason again: several cases leave A links outstanding while B compiles and links.
|
||||||
gtest_discover_tests(XfbFrontendOrderInvarianceTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
gtest_discover_tests(XfbFrontendOrderInvarianceTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||||
|
|||||||
@@ -1,674 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Test/Program/OptimisticStatusTest.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
|
|
||||||
|
|
||||||
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS: while a compile job is in flight, the two
|
|
||||||
// per-shader queries that would join it - GL_COMPILE_STATUS and the info log - answer
|
|
||||||
// optimistically instead, and the first such answer latches for that compile's lifetime
|
|
||||||
// (ShaderObject::TakeOptimisticCompileAnswer). These cases pin the corners of that
|
|
||||||
// contract: the default still joins, the optimistic window really answers without
|
|
||||||
// joining, the latch keeps the three queries telling one story even after the job
|
|
||||||
// settles, a real failure still fails the program link with the compile log quoted, and
|
|
||||||
// the Iris-shaped two-phase batch produces reflection identical to the joining path.
|
|
||||||
//
|
|
||||||
// Determinism note: the cases that need "a compile that cannot have settled yet" do not
|
|
||||||
// race the pool - they occupy its single concurrency slot with a gate-blocked job
|
|
||||||
// (PoolBlocker), so the assertions are hard EXPECTs rather than skip-if-drained guesses.
|
|
||||||
// A quirk that silently reverts to joining DEADLOCKS such a case into its 300s ctest
|
|
||||||
// timeout instead of passing - ugly, but a failure, which is the point.
|
|
||||||
//
|
|
||||||
// Like AsyncCompileTest, every case drives the real GL entry points and flips the
|
|
||||||
// MG_Config::Features fields itself rather than reading the environment, so one binary
|
|
||||||
// asserts both flag states regardless of how the suite was launched.
|
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <chrono>
|
|
||||||
#include <condition_variable>
|
|
||||||
#include <mutex>
|
|
||||||
#include <string>
|
|
||||||
#include <thread>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "Config.h"
|
|
||||||
#include "Includes.h"
|
|
||||||
#include "Init.h"
|
|
||||||
#include "MG_Impl/GLImpl/Getter/GL_Getter.h"
|
|
||||||
#include "MG_Impl/GLImpl/Program/GL_Program.h"
|
|
||||||
#include "MG_State/GLState/Core.h"
|
|
||||||
#include "MG_Util/Async/JobNode.h"
|
|
||||||
#include "MG_Util/Async/ShaderCompilePool.h"
|
|
||||||
|
|
||||||
using namespace MobileGL;
|
|
||||||
using namespace MobileGL::MG_Impl::GLImpl;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
class AsyncModeScope {
|
|
||||||
public:
|
|
||||||
explicit AsyncModeScope(const Bool async) : m_saved(MG_Config::Features.AsyncShaderCompile) {
|
|
||||||
MG_Config::Features.AsyncShaderCompile =
|
|
||||||
async ? MG_Config::QuirkOverride::ForceOn : MG_Config::QuirkOverride::ForceOff;
|
|
||||||
}
|
|
||||||
~AsyncModeScope() { MG_Config::Features.AsyncShaderCompile = m_saved; }
|
|
||||||
AsyncModeScope(const AsyncModeScope&) = delete;
|
|
||||||
AsyncModeScope& operator=(const AsyncModeScope&) = delete;
|
|
||||||
|
|
||||||
private:
|
|
||||||
const MG_Config::QuirkOverride m_saved;
|
|
||||||
};
|
|
||||||
|
|
||||||
class OptimisticStatusScope {
|
|
||||||
public:
|
|
||||||
explicit OptimisticStatusScope(const MG_Config::QuirkOverride mode)
|
|
||||||
: m_saved(MG_Config::Features.AsyncOptimisticShaderStatus) {
|
|
||||||
MG_Config::Features.AsyncOptimisticShaderStatus = mode;
|
|
||||||
}
|
|
||||||
~OptimisticStatusScope() { MG_Config::Features.AsyncOptimisticShaderStatus = m_saved; }
|
|
||||||
OptimisticStatusScope(const OptimisticStatusScope&) = delete;
|
|
||||||
OptimisticStatusScope& operator=(const OptimisticStatusScope&) = delete;
|
|
||||||
|
|
||||||
private:
|
|
||||||
const MG_Config::QuirkOverride m_saved;
|
|
||||||
};
|
|
||||||
|
|
||||||
// glMaxShaderCompilerThreadsKHR writes PROCESS-wide state (the pool's concurrency budget
|
|
||||||
// and the suspension latch), so a case that touches it has to put both back or it
|
|
||||||
// poisons every case declared after it in this binary.
|
|
||||||
class CompilerThreadScope {
|
|
||||||
public:
|
|
||||||
CompilerThreadScope() = default;
|
|
||||||
~CompilerThreadScope() {
|
|
||||||
MG_Util::Async::SetAsyncShaderCompileSuspended(false);
|
|
||||||
MG_Util::Async::ShaderCompilePool::Get().SetMaxConcurrency(
|
|
||||||
MG_Util::Async::ShaderCompilePool::Get().GetThreadCount());
|
|
||||||
}
|
|
||||||
CompilerThreadScope(const CompilerThreadScope&) = delete;
|
|
||||||
CompilerThreadScope& operator=(const CompilerThreadScope&) = delete;
|
|
||||||
};
|
|
||||||
|
|
||||||
// A job that occupies a pool slot until released, holding everything queued behind it
|
|
||||||
// in a provably-unsettled state. Same gate idea as JobNodeTest's TestJob+Gate; waiting
|
|
||||||
// on a test-owned gate inside a body does not violate the pool's no-job-waits-on-job
|
|
||||||
// rule - there is no other JOB involved.
|
|
||||||
class PoolBlocker final : public MG_Util::Async::JobNode {
|
|
||||||
public:
|
|
||||||
void Release() {
|
|
||||||
{
|
|
||||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
|
||||||
m_open = true;
|
|
||||||
}
|
|
||||||
m_cv.notify_all();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void RunBody() override {
|
|
||||||
std::unique_lock<std::mutex> lock(m_mutex);
|
|
||||||
m_cv.wait(lock, [this] { return m_open; });
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::mutex m_mutex;
|
|
||||||
std::condition_variable m_cv;
|
|
||||||
Bool m_open = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Budget 1 + a blocked job in the only slot: from construction until Release(), no
|
|
||||||
// shader compile posted afterwards can run, let alone settle. The destructor releases
|
|
||||||
// and joins so no case can leak a wedged pool into the next one.
|
|
||||||
class BlockedPoolScope {
|
|
||||||
public:
|
|
||||||
BlockedPoolScope() : m_blocker(MakeShared<PoolBlocker>()) {
|
|
||||||
MaxShaderCompilerThreadsKHR(1);
|
|
||||||
MG_Util::Async::ShaderCompilePool::Get().Post(m_blocker);
|
|
||||||
}
|
|
||||||
~BlockedPoolScope() { Release(); }
|
|
||||||
|
|
||||||
void Release() {
|
|
||||||
m_blocker->Release();
|
|
||||||
m_blocker->Wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
BlockedPoolScope(const BlockedPoolScope&) = delete;
|
|
||||||
BlockedPoolScope& operator=(const BlockedPoolScope&) = delete;
|
|
||||||
|
|
||||||
private:
|
|
||||||
SharedPtr<PoolBlocker> m_blocker;
|
|
||||||
};
|
|
||||||
|
|
||||||
const char* kBrokenFs = R"(#version 460
|
|
||||||
layout(location = 0) out vec4 fragColor;
|
|
||||||
void main() { fragColor = thisIdentifierWasNeverDeclared; }
|
|
||||||
)";
|
|
||||||
|
|
||||||
// Expensive enough that a compile is not instantaneous, and distinct per index so the
|
|
||||||
// source-hash memo and the stage-6 adoption map never turn a second instance into a
|
|
||||||
// no-op. Callers pass disjoint seed ranges for the same reason - two calls in one case
|
|
||||||
// must never regenerate the same text.
|
|
||||||
String MakeBulkySource(const int index) {
|
|
||||||
String source = "#version 460\nlayout(location = 0) out vec4 fragColor;\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 += " fragColor = vec4(acc, acc, acc, 1.0);\n}\n";
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The two stages of one Iris-shaped program. Distinct per index (so nothing is memoized
|
|
||||||
// across programs) but IDENTICAL between the quirk-off and quirk-on replays of the same
|
|
||||||
// index, which is what makes the reflection comparison meaningful.
|
|
||||||
String MakeIrisVs(const int index) {
|
|
||||||
String source = "#version 460\nlayout(location = 0) in vec3 aPos;\n";
|
|
||||||
source += "uniform mat4 uModel" + std::to_string(index) + ";\n";
|
|
||||||
source += "uniform vec4 uTint;\nout vec4 vColor;\n";
|
|
||||||
source += "void main() {\n vColor = uTint;\n gl_Position = uModel" + std::to_string(index) +
|
|
||||||
" * vec4(aPos, 1.0);\n}\n";
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
String MakeIrisFs(const int index) {
|
|
||||||
String source = "#version 460\nlayout(location = 0) out vec4 fragColor;\nin vec4 vColor;\n";
|
|
||||||
source += "uniform float uSeed" + std::to_string(index) + ";\nuniform vec2 uOffset;\n";
|
|
||||||
source += "void main() {\n float acc = uSeed" + std::to_string(index) + " + uOffset.x;\n";
|
|
||||||
for (int i = 0; i < 40; ++i) {
|
|
||||||
source += " acc = acc * 1.0001 + sin(acc + " + std::to_string(i) + ".0);\n";
|
|
||||||
}
|
|
||||||
source += " fragColor = vColor + vec4(acc, uOffset.y, 0.0, 1.0);\n}\n";
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint MakeShader(const GLenum type, const char* source) {
|
|
||||||
const GLuint shader = CreateShader(type);
|
|
||||||
ShaderSource(shader, 1, &source, nullptr);
|
|
||||||
CompileShader(shader);
|
|
||||||
return shader;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLint QueryShaderCompletion(const GLuint shader) {
|
|
||||||
GLint status = -1;
|
|
||||||
GetShaderiv(shader, GL_COMPLETION_STATUS_KHR, &status);
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLint QueryCompileStatus(const GLuint shader) {
|
|
||||||
GLint status = GL_FALSE;
|
|
||||||
GetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLint QueryInfoLogLength(const GLuint shader) {
|
|
||||||
GLint length = -1;
|
|
||||||
GetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
|
||||||
return length;
|
|
||||||
}
|
|
||||||
|
|
||||||
String QueryShaderInfoLog(const GLuint shader) {
|
|
||||||
std::vector<GLchar> buffer(65536);
|
|
||||||
GLsizei written = 0;
|
|
||||||
GetShaderInfoLog(shader, (GLsizei)buffer.size(), &written, buffer.data());
|
|
||||||
return String(buffer.data(), static_cast<size_t>(written));
|
|
||||||
}
|
|
||||||
|
|
||||||
GLint QueryLinkStatus(const GLuint program) {
|
|
||||||
GLint status = GL_FALSE;
|
|
||||||
GetProgramiv(program, GL_LINK_STATUS, &status);
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLint QueryProgramCompletion(const GLuint program) {
|
|
||||||
GLint status = -1;
|
|
||||||
GetProgramiv(program, GL_COMPLETION_STATUS_KHR, &status);
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
String QueryProgramInfoLog(const GLuint program) {
|
|
||||||
// Iris reads through an explicit 32768-byte buffer; mirror that cap so the
|
|
||||||
// log-ordering contract is asserted through the same window the application has.
|
|
||||||
std::vector<GLchar> buffer(32768);
|
|
||||||
GLsizei written = 0;
|
|
||||||
GetProgramInfoLog(program, (GLsizei)buffer.size(), &written, buffer.data());
|
|
||||||
return String(buffer.data(), static_cast<size_t>(written));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enqueues `count` distinct heavy compiles without reading anything back. Seed bases
|
|
||||||
// must be disjoint across calls within one case (see MakeBulkySource).
|
|
||||||
Vector<GLuint> SaturatePool(const int count, const int seedBase, Vector<String>& sourceStorage) {
|
|
||||||
Vector<GLuint> shaders;
|
|
||||||
shaders.reserve(static_cast<SizeT>(count));
|
|
||||||
for (int i = 0; i < count; ++i) {
|
|
||||||
sourceStorage.push_back(MakeBulkySource(seedBase + i));
|
|
||||||
const char* text = sourceStorage.back().c_str();
|
|
||||||
const GLuint shader = CreateShader(GL_FRAGMENT_SHADER);
|
|
||||||
ShaderSource(shader, 1, &text, nullptr);
|
|
||||||
CompileShader(shader);
|
|
||||||
shaders.push_back(shader);
|
|
||||||
}
|
|
||||||
return shaders;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One program driven through Iris's exact phase-1 shape: create, source, compile, read
|
|
||||||
// the info log then the compile status (GlShader.createShader's order), attach, bind an
|
|
||||||
// attrib, link, detach, delete. NO program-level query of any kind.
|
|
||||||
GLuint RunIrisPhaseOne(const String& vsSource, const String& fsSource) {
|
|
||||||
const char* vsText = vsSource.c_str();
|
|
||||||
const char* fsText = fsSource.c_str();
|
|
||||||
|
|
||||||
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
|
|
||||||
ShaderSource(vs, 1, &vsText, nullptr);
|
|
||||||
CompileShader(vs);
|
|
||||||
(void)QueryShaderInfoLog(vs);
|
|
||||||
(void)QueryCompileStatus(vs);
|
|
||||||
|
|
||||||
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
|
||||||
ShaderSource(fs, 1, &fsText, nullptr);
|
|
||||||
CompileShader(fs);
|
|
||||||
(void)QueryShaderInfoLog(fs);
|
|
||||||
(void)QueryCompileStatus(fs);
|
|
||||||
|
|
||||||
const GLuint program = CreateProgram();
|
|
||||||
AttachShader(program, vs);
|
|
||||||
AttachShader(program, fs);
|
|
||||||
BindAttribLocation(program, 0, "aPos");
|
|
||||||
LinkProgram(program);
|
|
||||||
DetachShader(program, vs);
|
|
||||||
DetachShader(program, fs);
|
|
||||||
DeleteShader(vs);
|
|
||||||
DeleteShader(fs);
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 2, also in Iris's order: LINK_STATUS first, then the by-name location lookups,
|
|
||||||
// then the GL_ACTIVE_UNIFORMS enumeration ProgramUniforms$Builder.buildUniforms does.
|
|
||||||
struct ProgramReflection {
|
|
||||||
GLint linkStatus = GL_FALSE;
|
|
||||||
Vector<std::pair<String, GLint>> locations; // queried name -> location
|
|
||||||
Vector<std::tuple<String, GLenum, GLint, GLint>> activeUniforms; // name, type, size, location
|
|
||||||
};
|
|
||||||
|
|
||||||
ProgramReflection RunIrisPhaseTwo(const GLuint program, const Vector<String>& names) {
|
|
||||||
ProgramReflection out;
|
|
||||||
out.linkStatus = QueryLinkStatus(program);
|
|
||||||
|
|
||||||
for (const String& name : names) {
|
|
||||||
out.locations.emplace_back(name, GetUniformLocation(program, name.c_str()));
|
|
||||||
}
|
|
||||||
|
|
||||||
GLint activeCount = 0;
|
|
||||||
GetProgramiv(program, GL_ACTIVE_UNIFORMS, &activeCount);
|
|
||||||
for (GLint i = 0; i < activeCount; ++i) {
|
|
||||||
GLchar name[128] = {};
|
|
||||||
GLsizei written = 0;
|
|
||||||
GLint size = 0;
|
|
||||||
GLenum type = 0;
|
|
||||||
GetActiveUniform(program, (GLuint)i, (GLsizei)sizeof(name), &written, &size, &type, name);
|
|
||||||
const String nameStr(name, static_cast<size_t>(written));
|
|
||||||
out.activeUniforms.emplace_back(nameStr, type, size, GetUniformLocation(program, name));
|
|
||||||
}
|
|
||||||
// The enumeration order is an implementation detail; the SET is the contract.
|
|
||||||
std::sort(out.activeUniforms.begin(), out.activeUniforms.end());
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
class OptimisticStatusTest : public ::testing::Test {
|
|
||||||
protected:
|
|
||||||
void SetUp() override { MobileGL::Initialize(); }
|
|
||||||
};
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
// The default still joins
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// With the quirk unset (Auto = the shipped default), GL_COMPILE_STATUS on a pending compile
|
|
||||||
// must join it: after the query, the node is terminal. This is the case that guards the
|
|
||||||
// default against ever silently flipping. No blocker here - a blocked pool would turn the
|
|
||||||
// (correct) joining behaviour into a deadlock; a plain backlog only makes the pre-join
|
|
||||||
// state likely, and the assertion is valid either way.
|
|
||||||
TEST_F(OptimisticStatusTest, OffByDefaultTheStatusStillJoins) {
|
|
||||||
const AsyncModeScope async(true);
|
|
||||||
const OptimisticStatusScope quirk(MG_Config::QuirkOverride::Auto);
|
|
||||||
const CompilerThreadScope threads;
|
|
||||||
MaxShaderCompilerThreadsKHR(1);
|
|
||||||
|
|
||||||
Vector<String> backlog;
|
|
||||||
const Vector<GLuint> saturation = SaturatePool(8, 70000, backlog);
|
|
||||||
const Vector<GLuint> probes = SaturatePool(1, 71000, backlog);
|
|
||||||
const GLuint probe = probes[0];
|
|
||||||
|
|
||||||
EXPECT_EQ(QueryCompileStatus(probe), GL_TRUE);
|
|
||||||
EXPECT_EQ(QueryShaderCompletion(probe), GL_TRUE)
|
|
||||||
<< "GL_COMPILE_STATUS with the quirk off must have joined the job";
|
|
||||||
|
|
||||||
for (const GLuint shader : saturation) DeleteShader(shader);
|
|
||||||
DeleteShader(probe);
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
// The optimistic window, deterministically
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// A compile that provably cannot have settled (the pool's only slot is gate-blocked)
|
|
||||||
// answers GL_TRUE / length 0 / empty log, and GL_COMPLETION_STATUS_KHR still reads
|
|
||||||
// GL_FALSE after all three - i.e. none of them joined. Hard EXPECTs, no skip: if the
|
|
||||||
// quirk silently reverts to joining, the status read deadlocks against the blocked pool
|
|
||||||
// and the case fails by timeout.
|
|
||||||
TEST_F(OptimisticStatusTest, PendingCompileReportsTrueAndEmptyLogWithoutJoining) {
|
|
||||||
const AsyncModeScope async(true);
|
|
||||||
const OptimisticStatusScope quirk(MG_Config::QuirkOverride::ForceOn);
|
|
||||||
const CompilerThreadScope threads;
|
|
||||||
const BlockedPoolScope blocked;
|
|
||||||
|
|
||||||
Vector<String> storage;
|
|
||||||
const Vector<GLuint> probes = SaturatePool(1, 72000, storage);
|
|
||||||
const GLuint probe = probes[0];
|
|
||||||
|
|
||||||
EXPECT_EQ(QueryCompileStatus(probe), GL_TRUE) << "an in-flight compile must answer GL_TRUE";
|
|
||||||
EXPECT_EQ(QueryInfoLogLength(probe), 0) << "an in-flight compile must answer an empty log length";
|
|
||||||
EXPECT_TRUE(QueryShaderInfoLog(probe).empty()) << "an in-flight compile must answer an empty log";
|
|
||||||
EXPECT_EQ(QueryShaderCompletion(probe), GL_FALSE)
|
|
||||||
<< "the three reads above must not have joined the blocked job";
|
|
||||||
|
|
||||||
DeleteShader(probe);
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
// The latch: one story per compile
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// The torn-pair regression case. A broken shader's log and status are read while the job
|
|
||||||
// is provably in flight (optimistic empty/GL_TRUE), the job then settles, and the app
|
|
||||||
// re-reads: the latch must keep the answers optimistic - GL_TRUE, empty log - rather than
|
|
||||||
// flip to the real GL_FALSE next to the already-consumed empty log. The real failure then
|
|
||||||
// surfaces at the link, with the compile error inside the application's 32768-byte read
|
|
||||||
// window (the compile log leads the quoted source in ConsumeShaders' format).
|
|
||||||
TEST_F(OptimisticStatusTest, LatchKeepsOneStoryPerCompileAndTheLinkCarriesTheDiagnostic) {
|
|
||||||
const AsyncModeScope async(true);
|
|
||||||
const OptimisticStatusScope quirk(MG_Config::QuirkOverride::ForceOn);
|
|
||||||
const CompilerThreadScope threads;
|
|
||||||
|
|
||||||
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
|
|
||||||
const char* vsText =
|
|
||||||
"#version 460\nlayout(location = 0) in vec3 aPos;\nvoid main() { gl_Position = vec4(aPos, 1.0); }\n";
|
|
||||||
ShaderSource(vs, 1, &vsText, nullptr);
|
|
||||||
|
|
||||||
GLuint fs = 0;
|
|
||||||
{
|
|
||||||
const BlockedPoolScope blocked;
|
|
||||||
CompileShader(vs);
|
|
||||||
fs = MakeShader(GL_FRAGMENT_SHADER, kBrokenFs);
|
|
||||||
|
|
||||||
// Iris's order, while nothing can settle: log (empty), then status (GL_TRUE).
|
|
||||||
EXPECT_TRUE(QueryShaderInfoLog(fs).empty());
|
|
||||||
EXPECT_EQ(QueryCompileStatus(fs), GL_TRUE);
|
|
||||||
EXPECT_EQ(QueryShaderCompletion(fs), GL_FALSE);
|
|
||||||
} // blocker released and joined; the broken compile can now settle
|
|
||||||
|
|
||||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
|
||||||
while (QueryShaderCompletion(fs) == GL_FALSE) {
|
|
||||||
ASSERT_LT(std::chrono::steady_clock::now(), deadline) << "compile job never settled";
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Settled - but this shader already told the optimistic story, so it keeps telling it.
|
|
||||||
EXPECT_EQ(QueryCompileStatus(fs), GL_TRUE)
|
|
||||||
<< "the latch must keep a queried-while-pending compile optimistic after it settles";
|
|
||||||
EXPECT_EQ(QueryInfoLogLength(fs), 0);
|
|
||||||
EXPECT_TRUE(QueryShaderInfoLog(fs).empty());
|
|
||||||
|
|
||||||
// The truth arrives where the design routes it: at the link.
|
|
||||||
const GLuint program = CreateProgram();
|
|
||||||
AttachShader(program, vs);
|
|
||||||
AttachShader(program, fs);
|
|
||||||
LinkProgram(program);
|
|
||||||
EXPECT_EQ(QueryLinkStatus(program), GL_FALSE) << "a latched-over failure must still fail the link";
|
|
||||||
EXPECT_NE(QueryProgramInfoLog(program).find("thisIdentifierWasNeverDeclared"), String::npos)
|
|
||||||
<< "the compile error must lead the program info log, inside a 32768-byte window";
|
|
||||||
|
|
||||||
DeleteProgram(program);
|
|
||||||
DeleteShader(vs);
|
|
||||||
DeleteShader(fs);
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A shader whose FIRST query arrives after the job settled was never answered
|
|
||||||
// optimistically, so it owes no continuity: the truth comes straight back. (The
|
|
||||||
// completion poll does not engage the latch - it is the extension's own non-joining
|
|
||||||
// query and always tells the truth.)
|
|
||||||
TEST_F(OptimisticStatusTest, OnceTerminalAnUnqueriedShaderTellsTheTruth) {
|
|
||||||
const AsyncModeScope async(true);
|
|
||||||
const OptimisticStatusScope quirk(MG_Config::QuirkOverride::ForceOn);
|
|
||||||
|
|
||||||
const GLuint fs = MakeShader(GL_FRAGMENT_SHADER, kBrokenFs);
|
|
||||||
|
|
||||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
|
||||||
while (QueryShaderCompletion(fs) == GL_FALSE) {
|
|
||||||
ASSERT_LT(std::chrono::steady_clock::now(), deadline) << "compile job never settled";
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
EXPECT_EQ(QueryCompileStatus(fs), GL_FALSE) << "no optimistic answer was given, so no latch holds";
|
|
||||||
EXPECT_GT(QueryInfoLogLength(fs), 0);
|
|
||||||
EXPECT_NE(QueryShaderInfoLog(fs).find("thisIdentifierWasNeverDeclared"), String::npos);
|
|
||||||
DeleteShader(fs);
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recompiling resets the story: a latched optimistic answer must not survive a source
|
|
||||||
// change (the latch clears when the node changes hands or goes away).
|
|
||||||
TEST_F(OptimisticStatusTest, ANewCompileResetsTheLatch) {
|
|
||||||
const AsyncModeScope async(true);
|
|
||||||
const OptimisticStatusScope quirk(MG_Config::QuirkOverride::ForceOn);
|
|
||||||
const CompilerThreadScope threads;
|
|
||||||
|
|
||||||
GLuint fs = 0;
|
|
||||||
{
|
|
||||||
const BlockedPoolScope blocked;
|
|
||||||
fs = MakeShader(GL_FRAGMENT_SHADER, kBrokenFs);
|
|
||||||
EXPECT_EQ(QueryCompileStatus(fs), GL_TRUE); // latches
|
|
||||||
}
|
|
||||||
|
|
||||||
// New source, new compile, no query before it settles.
|
|
||||||
const char* goodFs = "#version 460\nlayout(location = 0) out vec4 fragColor;\n"
|
|
||||||
"void main() { fragColor = vec4(1.0); }\n";
|
|
||||||
ShaderSource(fs, 1, &goodFs, nullptr);
|
|
||||||
CompileShader(fs);
|
|
||||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
|
||||||
while (QueryShaderCompletion(fs) == GL_FALSE) {
|
|
||||||
ASSERT_LT(std::chrono::steady_clock::now(), deadline) << "recompile never settled";
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
||||||
}
|
|
||||||
EXPECT_EQ(QueryCompileStatus(fs), GL_TRUE);
|
|
||||||
EXPECT_TRUE(QueryShaderInfoLog(fs).empty());
|
|
||||||
DeleteShader(fs);
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
// Failure still fails, at the link, inside the application's read window
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// A broken fragment shader whose compile status was answered optimistically still fails
|
|
||||||
// its program link, and the compile error is readable through a 32768-byte
|
|
||||||
// glGetProgramInfoLog - the compile log LEADS the quoted source in ConsumeShaders'
|
|
||||||
// format, so even this >32KB shader source cannot push it out of the window.
|
|
||||||
TEST_F(OptimisticStatusTest, AFailingCompileStillFailsItsLink) {
|
|
||||||
const AsyncModeScope async(true);
|
|
||||||
const OptimisticStatusScope quirk(MG_Config::QuirkOverride::ForceOn);
|
|
||||||
|
|
||||||
// A >32KB broken fragment shader: the undeclared identifier sits at the top, then bulk.
|
|
||||||
String brokenSource = "#version 460\nlayout(location = 0) out vec4 fragColor;\n";
|
|
||||||
brokenSource += "void main() {\n float acc = thisIdentifierWasNeverDeclared;\n";
|
|
||||||
for (int i = 0; i < 900; ++i) {
|
|
||||||
brokenSource += " acc = acc * 1.0001 + sin(acc + " + std::to_string(i) + ".0) * cos(acc);\n";
|
|
||||||
}
|
|
||||||
brokenSource += " fragColor = vec4(acc);\n}\n";
|
|
||||||
ASSERT_GT(brokenSource.size(), 32768u);
|
|
||||||
|
|
||||||
const GLuint vs = MakeShader(GL_VERTEX_SHADER,
|
|
||||||
"#version 460\nlayout(location = 0) in vec3 aPos;\n"
|
|
||||||
"void main() { gl_Position = vec4(aPos, 1.0); }\n");
|
|
||||||
const char* brokenText = brokenSource.c_str();
|
|
||||||
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
|
||||||
ShaderSource(fs, 1, &brokenText, nullptr);
|
|
||||||
CompileShader(fs);
|
|
||||||
(void)QueryShaderInfoLog(fs);
|
|
||||||
(void)QueryCompileStatus(fs); // may latch optimistic GL_TRUE; must not matter
|
|
||||||
|
|
||||||
const GLuint program = CreateProgram();
|
|
||||||
AttachShader(program, vs);
|
|
||||||
AttachShader(program, fs);
|
|
||||||
LinkProgram(program);
|
|
||||||
|
|
||||||
EXPECT_EQ(QueryLinkStatus(program), GL_FALSE) << "a hidden compile failure must still fail the link";
|
|
||||||
const String log = QueryProgramInfoLog(program);
|
|
||||||
EXPECT_NE(log.find("thisIdentifierWasNeverDeclared"), String::npos)
|
|
||||||
<< "the compile error must be readable through a 32768-byte program info log window";
|
|
||||||
|
|
||||||
DeleteProgram(program);
|
|
||||||
DeleteShader(vs);
|
|
||||||
DeleteShader(fs);
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
// The Iris two-phase replay
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// THE LOAD-BEARING CASE. 24 programs through Iris's exact phase-1 shape (compile, read log
|
|
||||||
// then status per shader, link, detach, delete - no program query), then phase 2 (link
|
|
||||||
// status, by-name locations including an absent name, the active-uniform enumeration).
|
|
||||||
// Every location and every active-uniform record must equal what the identical sequence
|
|
||||||
// produces with the quirk off.
|
|
||||||
//
|
|
||||||
// Two determinism guards make this a real A/B rather than a tautology:
|
|
||||||
// * The quirk-on arm runs FIRST, against a cold preprocess cache, and the reference arm
|
|
||||||
// second - so it is the path under test that pays the full pipeline, not the control.
|
|
||||||
// * The quirk-on arm's phase 1 runs over a BLOCKED pool, and every program is then
|
|
||||||
// WITNESSED still-incomplete (GL_COMPLETION_STATUS_KHR == GL_FALSE) before the pool
|
|
||||||
// is released: proof that no phase-1 call joined, i.e. the quirk was really engaged.
|
|
||||||
// A quirk that silently reverts to joining deadlocks here and fails by timeout.
|
|
||||||
TEST_F(OptimisticStatusTest, IrisTwoPhaseReplayProducesIdenticalReflection) {
|
|
||||||
constexpr int kPrograms = 24;
|
|
||||||
|
|
||||||
Vector<ProgramReflection> reference;
|
|
||||||
Vector<ProgramReflection> optimistic;
|
|
||||||
|
|
||||||
for (const Bool quirkOn : {true, false}) {
|
|
||||||
const AsyncModeScope async(true);
|
|
||||||
const OptimisticStatusScope quirk(quirkOn ? MG_Config::QuirkOverride::ForceOn
|
|
||||||
: MG_Config::QuirkOverride::ForceOff);
|
|
||||||
const CompilerThreadScope threads;
|
|
||||||
|
|
||||||
Vector<String> vsSources, fsSources;
|
|
||||||
for (int i = 0; i < kPrograms; ++i) {
|
|
||||||
vsSources.push_back(MakeIrisVs(i));
|
|
||||||
fsSources.push_back(MakeIrisFs(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector<GLuint> programs;
|
|
||||||
if (quirkOn) {
|
|
||||||
const BlockedPoolScope blocked;
|
|
||||||
for (int i = 0; i < kPrograms; ++i) {
|
|
||||||
programs.push_back(RunIrisPhaseOne(vsSources[(SizeT)i], fsSources[(SizeT)i]));
|
|
||||||
}
|
|
||||||
// The witness: phase 1 finished with the pool blocked, so nothing can have
|
|
||||||
// settled and nothing can have been joined - every link must still be pending.
|
|
||||||
for (int i = 0; i < kPrograms; ++i) {
|
|
||||||
ASSERT_EQ(QueryProgramCompletion(programs[(SizeT)i]), GL_FALSE)
|
|
||||||
<< "program " << i << " settled under a blocked pool - a phase-1 call must have joined";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (int i = 0; i < kPrograms; ++i) {
|
|
||||||
programs.push_back(RunIrisPhaseOne(vsSources[(SizeT)i], fsSources[(SizeT)i]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector<ProgramReflection>& out = quirkOn ? optimistic : reference;
|
|
||||||
for (int i = 0; i < kPrograms; ++i) {
|
|
||||||
const Vector<String> names = {"uModel" + std::to_string(i), "uTint",
|
|
||||||
"uSeed" + std::to_string(i), "uOffset", "uDoesNotExist"};
|
|
||||||
out.push_back(RunIrisPhaseTwo(programs[(SizeT)i], names));
|
|
||||||
}
|
|
||||||
for (const GLuint program : programs) DeleteProgram(program);
|
|
||||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
ASSERT_EQ(reference.size(), optimistic.size());
|
|
||||||
for (SizeT i = 0; i < reference.size(); ++i) {
|
|
||||||
EXPECT_EQ(reference[i].linkStatus, GL_TRUE) << "program " << i;
|
|
||||||
EXPECT_EQ(optimistic[i].linkStatus, GL_TRUE) << "program " << i;
|
|
||||||
EXPECT_EQ(reference[i].locations, optimistic[i].locations)
|
|
||||||
<< "program " << i << ": by-name locations diverged under the quirk";
|
|
||||||
EXPECT_EQ(reference[i].activeUniforms, optimistic[i].activeUniforms)
|
|
||||||
<< "program " << i << ": active-uniform enumeration diverged under the quirk";
|
|
||||||
// The absent name answers -1 in both worlds.
|
|
||||||
EXPECT_EQ(reference[i].locations.back().second, -1) << "program " << i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
// The concurrency observable
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// The crisp A/B that phase 1 stopped joining. Quirk-on arm: the phase-1 shape over a
|
|
||||||
// blocked pool completes without joining anything - every shader is then provably still
|
|
||||||
// in flight (hard EXPECT; an inert quirk deadlocks and fails by timeout). Quirk-off arm:
|
|
||||||
// the same shape joins at every status read, so nothing is left in flight afterwards.
|
|
||||||
TEST_F(OptimisticStatusTest, PhaseOneIssuesNoCompileJoin) {
|
|
||||||
const AsyncModeScope async(true);
|
|
||||||
const CompilerThreadScope threads;
|
|
||||||
|
|
||||||
// Quirk on: nothing settles, nothing joins.
|
|
||||||
{
|
|
||||||
const OptimisticStatusScope quirk(MG_Config::QuirkOverride::ForceOn);
|
|
||||||
const BlockedPoolScope blocked;
|
|
||||||
Vector<String> storage;
|
|
||||||
Vector<GLuint> shaders;
|
|
||||||
for (int i = 0; i < 12; ++i) {
|
|
||||||
storage.push_back(MakeBulkySource(90000 + i));
|
|
||||||
const char* text = storage.back().c_str();
|
|
||||||
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
|
||||||
ShaderSource(fs, 1, &text, nullptr);
|
|
||||||
CompileShader(fs);
|
|
||||||
(void)QueryShaderInfoLog(fs);
|
|
||||||
(void)QueryCompileStatus(fs);
|
|
||||||
shaders.push_back(fs);
|
|
||||||
}
|
|
||||||
for (const GLuint shader : shaders) {
|
|
||||||
EXPECT_EQ(QueryShaderCompletion(shader), GL_FALSE)
|
|
||||||
<< "a phase-1 read joined a compile the blocked pool could not have run";
|
|
||||||
}
|
|
||||||
for (const GLuint shader : shaders) DeleteShader(shader);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quirk off: every status read joins its shader.
|
|
||||||
{
|
|
||||||
const OptimisticStatusScope quirk(MG_Config::QuirkOverride::ForceOff);
|
|
||||||
MaxShaderCompilerThreadsKHR(1);
|
|
||||||
Vector<String> storage;
|
|
||||||
Vector<GLuint> shaders;
|
|
||||||
for (int i = 0; i < 12; ++i) {
|
|
||||||
storage.push_back(MakeBulkySource(80000 + i));
|
|
||||||
const char* text = storage.back().c_str();
|
|
||||||
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
|
||||||
ShaderSource(fs, 1, &text, nullptr);
|
|
||||||
CompileShader(fs);
|
|
||||||
(void)QueryShaderInfoLog(fs);
|
|
||||||
(void)QueryCompileStatus(fs);
|
|
||||||
shaders.push_back(fs);
|
|
||||||
}
|
|
||||||
for (const GLuint shader : shaders) {
|
|
||||||
EXPECT_EQ(QueryShaderCompletion(shader), GL_TRUE)
|
|
||||||
<< "with the quirk off every per-shader status read must have joined";
|
|
||||||
}
|
|
||||||
for (const GLuint shader : shaders) DeleteShader(shader);
|
|
||||||
}
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
@@ -239,15 +239,7 @@ TEST_F(ParallelShaderCompileTest, ProgramCompletionStatusReportsFalseWithoutJoin
|
|||||||
|
|
||||||
for (const GLuint program : programs) {
|
for (const GLuint program : programs) {
|
||||||
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE);
|
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE);
|
||||||
// GL_COMPLETION_STATUS_KHR spans BOTH phases of a link, so reading GL_LINK_STATUS -
|
EXPECT_EQ(QueryProgramCompletion(program), GL_TRUE) << "GL_LINK_STATUS must have joined";
|
||||||
// which is answered out of phase A - is no longer enough to turn it GL_TRUE. That is
|
|
||||||
// deliberate: an application that polls completion and then draws must not be told
|
|
||||||
// "done" while the SPIR-V is still being generated, or the draw it was cleared for is
|
|
||||||
// the thing that blocks. Settling both phases is what makes the query true.
|
|
||||||
const auto& object = MG_State::pGLContext->GetProgramObject(program);
|
|
||||||
ASSERT_NE(object, nullptr);
|
|
||||||
object->JoinLinkAndSpirv();
|
|
||||||
EXPECT_EQ(QueryProgramCompletion(program), GL_TRUE) << "a full join must have settled both phases";
|
|
||||||
}
|
}
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
}
|
}
|
||||||
@@ -341,54 +333,6 @@ TEST_F(ParallelShaderCompileTest, ZeroCompilerThreadsJoinsEverythingAndCompilesI
|
|||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The same obligation, but for LINKS that are already in flight when the zero count arrives -
|
|
||||||
// and specifically for BOTH phases of one. A link is two chained jobs now (ProgramLinkTask,
|
|
||||||
// then ProgramSpirvTask), and GL_COMPLETION_STATUS_KHR spans both, so
|
|
||||||
// ProgramState::JoinAllPendingWork has to settle both or this query reads GL_FALSE in the one
|
|
||||||
// mode the extension says cannot have anything pending. The case above creates its program
|
|
||||||
// AFTER the zero count, so it links inline and cannot see this; here the programs are linked
|
|
||||||
// against a saturated pool BEFORE it.
|
|
||||||
TEST_F(ParallelShaderCompileTest, ZeroCompilerThreadsJoinsPendingLinksAndTheirSpirvJobs) {
|
|
||||||
const AsyncModeScope async(true);
|
|
||||||
const CompilerThreadScope threads;
|
|
||||||
MaxShaderCompilerThreadsKHR(1);
|
|
||||||
|
|
||||||
// A backlog first, so the links below cannot all drain before the zero count lands.
|
|
||||||
Vector<String> sources;
|
|
||||||
(void)EnqueueBacklog(24, 5000, sources);
|
|
||||||
|
|
||||||
Vector<GLuint> programs;
|
|
||||||
for (int i = 0; i < 8; ++i) {
|
|
||||||
sources.push_back(MakeBulkySource(5100 + i));
|
|
||||||
const char* text = sources.back().c_str();
|
|
||||||
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
|
||||||
ShaderSource(fs, 1, &text, nullptr);
|
|
||||||
CompileShader(fs);
|
|
||||||
const GLuint vs = MakeShader(GL_VERTEX_SHADER, kVs);
|
|
||||||
CompileShader(vs); // this file's MakeShader only sources; it does not compile
|
|
||||||
const GLuint program = CreateProgram();
|
|
||||||
AttachShader(program, vs);
|
|
||||||
AttachShader(program, fs);
|
|
||||||
LinkProgram(program);
|
|
||||||
programs.push_back(program);
|
|
||||||
}
|
|
||||||
|
|
||||||
int outstanding = 0;
|
|
||||||
for (const GLuint program : programs) {
|
|
||||||
if (QueryProgramCompletion(program) == GL_FALSE) ++outstanding;
|
|
||||||
}
|
|
||||||
|
|
||||||
MaxShaderCompilerThreadsKHR(0);
|
|
||||||
|
|
||||||
for (const GLuint program : programs) {
|
|
||||||
EXPECT_EQ(QueryProgramCompletion(program), GL_TRUE)
|
|
||||||
<< "glMaxShaderCompilerThreadsKHR(0) must leave neither link phase in flight";
|
|
||||||
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE);
|
|
||||||
}
|
|
||||||
EXPECT_GT(outstanding, 0) << "every link had drained before the zero count; this case proved nothing";
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...and a later NONZERO count is what lifts it. Nothing else does: not a new context, not a
|
// ...and a later NONZERO count is what lifts it. Nothing else does: not a new context, not a
|
||||||
// join, not eglInitialize. That is the documented contract, so it gets an assertion.
|
// join, not eglInitialize. That is the documented contract, so it gets an assertion.
|
||||||
TEST_F(ParallelShaderCompileTest, NonzeroCompilerThreadsRestoresAsynchronousCompilation) {
|
TEST_F(ParallelShaderCompileTest, NonzeroCompilerThreadsRestoresAsynchronousCompilation) {
|
||||||
|
|||||||
@@ -1100,119 +1100,4 @@ void main() { color = u + v; }
|
|||||||
EXPECT_EQ(viaActiveUniformBlockiv, 5);
|
EXPECT_EQ(viaActiveUniformBlockiv, 5);
|
||||||
EXPECT_EQ(TakeError(), GL_NO_ERROR);
|
EXPECT_EQ(TakeError(), GL_NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------- queries on an unlinked program ----
|
|
||||||
// glGetProgramiv is legal on a program that has never linked - GL 4.6 sec. 7.3 says the
|
|
||||||
// queried state simply has its initial value - but the reflection-backed pnames read
|
|
||||||
// Artifacts().program, which is null until a link produces one. That dereference was a
|
|
||||||
// SIGSEGV inside glslang::TProgram::getNumPipeInputs, and KHR-GL30.api.coverage walks into it
|
|
||||||
// (it queries GL_ACTIVE_ATTRIBUTES right after a glGetAttribLocation that failed). It only
|
|
||||||
// became reachable once the glCopyTexImage2D throw ahead of it in the same case stopped
|
|
||||||
// killing the run first.
|
|
||||||
TEST_F(ProgramInterfaceTest, ReflectionQueriesOnAnUnlinkedProgramAnswerZero) {
|
|
||||||
const GLuint neverLinked = CreateProgram();
|
|
||||||
ASSERT_NE(neverLinked, 0u);
|
|
||||||
ClearErrors();
|
|
||||||
|
|
||||||
for (const GLenum pname : {GL_ACTIVE_ATTRIBUTES, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, GL_ACTIVE_UNIFORMS,
|
|
||||||
GL_ACTIVE_UNIFORM_MAX_LENGTH, GL_ACTIVE_UNIFORM_BLOCKS,
|
|
||||||
GL_ACTIVE_ATOMIC_COUNTER_BUFFERS}) {
|
|
||||||
GLint value = -1;
|
|
||||||
GetProgramiv(neverLinked, pname, &value);
|
|
||||||
ClearErrors();
|
|
||||||
EXPECT_GE(value, 0) << "pname 0x" << std::hex << pname << " left its output untouched";
|
|
||||||
}
|
|
||||||
|
|
||||||
// A program that was linked and FAILED is the shape api.coverage actually hits.
|
|
||||||
const GLuint brokenSource = MakeProgram("#version 430\nvoid main() { this is not glsl }\n", kSimpleFs);
|
|
||||||
LinkProgram(brokenSource);
|
|
||||||
ClearErrors();
|
|
||||||
GLint linked = GL_TRUE;
|
|
||||||
GetProgramiv(brokenSource, GL_LINK_STATUS, &linked);
|
|
||||||
ASSERT_EQ(linked, GL_FALSE) << "the shader was supposed to fail to compile";
|
|
||||||
ClearErrors();
|
|
||||||
|
|
||||||
GLint attributes = -1;
|
|
||||||
GetProgramiv(brokenSource, GL_ACTIVE_ATTRIBUTES, &attributes);
|
|
||||||
ClearErrors();
|
|
||||||
EXPECT_EQ(attributes, 0);
|
|
||||||
|
|
||||||
// GL_COMPUTE_WORK_GROUP_SIZE is GL_INVALID_OPERATION on a program that has not linked (GL
|
|
||||||
// 4.6 sec. 7.13), so it is allowed to leave the output alone - but it still reaches
|
|
||||||
// GetComputeLocalSize(), and it may not do so through a null reflection.
|
|
||||||
GLint localSize[3] = {-1, -1, -1};
|
|
||||||
GetProgramiv(brokenSource, GL_COMPUTE_WORK_GROUP_SIZE, localSize);
|
|
||||||
const GLenum computeError = TakeError();
|
|
||||||
ClearErrors();
|
|
||||||
EXPECT_TRUE(computeError == GL_INVALID_OPERATION || (localSize[0] == 0 && localSize[1] == 0 &&
|
|
||||||
localSize[2] == 0))
|
|
||||||
<< "either the query is refused, or it answers the initial value - never both untouched "
|
|
||||||
"and unreported";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------- length on every path ----
|
|
||||||
// glGetProgramResourceiv's *length is the caller's only signal for how many entries params
|
|
||||||
// holds, and callers are entitled to leave it uninitialised: the CTS declares `GLsizei
|
|
||||||
// length;` next to a 1000-entry stack array and then loops `for (i = 0; i < length; ++i)`
|
|
||||||
// (gl4cProgramInterfaceQueryTests.cpp:2172). Leaving it untouched on an error path therefore
|
|
||||||
// does not "return nothing" - it hands the caller whatever was on its stack and makes it walk
|
|
||||||
// that far. KHR-GL43.program_interface_query.subroutines-vertex read 0x20202020 (" ")
|
|
||||||
// entries and took the process down on BOTH backends. So: zero on every exit, real count on
|
|
||||||
// success. Poisoning with the exact CTS-observed value keeps the assertion honest.
|
|
||||||
TEST_F(ProgramInterfaceTest, GetProgramResourceivReportsLengthOnEveryExitPath) {
|
|
||||||
const GLuint p = MakeProgram(kSimpleVs, kSimpleFs);
|
|
||||||
BindAttribLocation(p, 0, "position");
|
|
||||||
BindFragDataLocation(p, 0, "color");
|
|
||||||
LinkProgram(p);
|
|
||||||
ExpectLinked(p);
|
|
||||||
ClearErrors();
|
|
||||||
|
|
||||||
constexpr GLsizei kPoison = 0x20202020;
|
|
||||||
constexpr GLsizei kBufSize = 16;
|
|
||||||
GLint params[kBufSize] = {};
|
|
||||||
|
|
||||||
const GLenum nameLengthProp = GL_NAME_LENGTH;
|
|
||||||
const GLenum compatibleSubroutinesProp = GL_COMPATIBLE_SUBROUTINES;
|
|
||||||
const GLenum notAProp = GL_TEXTURE_2D;
|
|
||||||
|
|
||||||
const auto lengthAfter = [&](GLuint program, GLenum iface, GLuint index, GLsizei propCount,
|
|
||||||
const GLenum* props, GLsizei bufSize, GLint* out) {
|
|
||||||
GLsizei length = kPoison;
|
|
||||||
GetProgramResourceiv(program, iface, index, propCount, props, bufSize, &length, out);
|
|
||||||
ClearErrors();
|
|
||||||
return length;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The case that actually crashed: no subroutine reflection exists, so the query errors
|
|
||||||
// out - and the caller then trusts *length.
|
|
||||||
EXPECT_EQ(lengthAfter(p, GL_VERTEX_SUBROUTINE_UNIFORM, 0, 1, &compatibleSubroutinesProp, kBufSize, params), 0)
|
|
||||||
<< "GL_VERTEX_SUBROUTINE_UNIFORM";
|
|
||||||
// Not a program name.
|
|
||||||
EXPECT_EQ(lengthAfter(p + 4242, GL_UNIFORM, 0, 1, &nameLengthProp, kBufSize, params), 0) << "bad program";
|
|
||||||
// Not an interface enum.
|
|
||||||
EXPECT_EQ(lengthAfter(p, GL_TEXTURE_2D, 0, 1, &nameLengthProp, kBufSize, params), 0) << "bad interface";
|
|
||||||
// propCount <= 0, bufSize < 0.
|
|
||||||
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 0, 0, &nameLengthProp, kBufSize, params), 0) << "propCount 0";
|
|
||||||
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 0, 1, &nameLengthProp, -1, params), 0) << "negative bufSize";
|
|
||||||
// props == nullptr.
|
|
||||||
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 0, 1, nullptr, kBufSize, params), 0) << "null props";
|
|
||||||
// A prop this command does not know at all.
|
|
||||||
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 0, 1, ¬AProp, kBufSize, params), 0) << "unknown prop";
|
|
||||||
// A prop it knows but this interface does not carry.
|
|
||||||
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 0, 1, &compatibleSubroutinesProp, kBufSize, params), 0)
|
|
||||||
<< "prop/interface mismatch";
|
|
||||||
// Index past the end of a real interface.
|
|
||||||
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 9999, 1, &nameLengthProp, kBufSize, params), 0) << "bad index";
|
|
||||||
// Nowhere to put the values.
|
|
||||||
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 0, 1, &nameLengthProp, kBufSize, nullptr), 0) << "null params";
|
|
||||||
|
|
||||||
// ...and the success path still reports the count it actually wrote.
|
|
||||||
const GLuint outputIndex = GetProgramResourceIndex(p, GL_PROGRAM_OUTPUT, "color");
|
|
||||||
ASSERT_NE(outputIndex, GL_INVALID_INDEX);
|
|
||||||
GLsizei length = kPoison;
|
|
||||||
GetProgramResourceiv(p, GL_PROGRAM_OUTPUT, outputIndex, 1, &nameLengthProp, kBufSize, &length, params);
|
|
||||||
EXPECT_EQ(TakeError(), GL_NO_ERROR);
|
|
||||||
EXPECT_EQ(length, 1);
|
|
||||||
EXPECT_EQ(params[0], 6) << "GL_NAME_LENGTH counts the terminator";
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -9,11 +9,8 @@
|
|||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <map>
|
|
||||||
#include <set>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "Includes.h"
|
#include "Includes.h"
|
||||||
#include "Init.h"
|
#include "Init.h"
|
||||||
@@ -2652,430 +2649,3 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheHonorsByteBudget) {
|
|||||||
EXPECT_EQ(cache.GetEntryCount(), before);
|
EXPECT_EQ(cache.GetEntryCount(), before);
|
||||||
EXPECT_EQ(cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(oversized), oversized, kEnvA), nullptr);
|
EXPECT_EQ(cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(oversized), oversized, kEnvA), nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every vertex input that reaches SPIR-V must carry a Location decoration - including the
|
|
||||||
// declarations glslang's io-mapper considers INACTIVE.
|
|
||||||
//
|
|
||||||
// The shape is Iris's: seven attributes, only some of them bound through
|
|
||||||
// glBindAttribLocation (ProgramAttrib::explicitVertexInLocations), and at least one neither
|
|
||||||
// bound nor referenced. GL says only active inputs get generic attribute locations, so the
|
|
||||||
// resolver deliberately does not RESERVE a slot for a dead one - but it must still RESOLVE a
|
|
||||||
// location for it, because glslang emits an OpVariable for every declared global (the entry
|
|
||||||
// point's interface comes from the linker objects) and SPIR-V requires every non-built-in
|
|
||||||
// Input to be decorated (VUID-StandaloneSpirv-Location-04916).
|
|
||||||
//
|
|
||||||
// This test drives the FRONTEND rather than the GL entry points on purpose: it checks the RAW
|
|
||||||
// GlslangToSpv output, before SanitizeAndOptimizeBinary. A GL-level test cannot see the defect
|
|
||||||
// for an unreferenced attribute, because AggressiveDCE deletes the offending variable on its
|
|
||||||
// way to the backend - and yet the real victim (Iris' mc_midTexCoord, Adreno 830,
|
|
||||||
// programHash 0x4a7e9a37fb49caa1) survived DCE and killed the pipeline with VK_ERROR_UNKNOWN.
|
|
||||||
TEST_F(ProgramUtilTest, PartiallyBoundVertexInputsAllReceiveALocation) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
const String vertexSource = R"(#version 460 core
|
|
||||||
in vec3 a_Position;
|
|
||||||
in vec4 a_Color;
|
|
||||||
in vec2 a_TexCoord;
|
|
||||||
in vec2 mc_midTexCoord;
|
|
||||||
in vec4 mc_Entity;
|
|
||||||
in vec3 iris_Normal;
|
|
||||||
in vec4 a_Unreferenced;
|
|
||||||
out vec4 v_Color;
|
|
||||||
void main() {
|
|
||||||
v_Color = a_Color + vec4(a_TexCoord, 0.0, 0.0) + vec4(mc_midTexCoord, 0.0, 0.0) + mc_Entity
|
|
||||||
+ vec4(iris_Normal, 0.0);
|
|
||||||
gl_Position = vec4(a_Position, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
ShaderAttrib shaderAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = vertexSource};
|
|
||||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
|
||||||
ASSERT_TRUE(shaderResult) << shaderResult.error().log;
|
|
||||||
|
|
||||||
// PARTIALLY bound, and deliberately not a dense 0..N run - exactly what Iris does.
|
|
||||||
// mc_midTexCoord and a_Unreferenced are left unbound (FastSTL's map has no
|
|
||||||
// initializer-list constructor, hence the explicit inserts).
|
|
||||||
UnorderedMap<String, Uint> explicitVertexIns;
|
|
||||||
explicitVertexIns["a_Position"] = 0;
|
|
||||||
explicitVertexIns["a_Color"] = 1;
|
|
||||||
explicitVertexIns["a_TexCoord"] = 2;
|
|
||||||
explicitVertexIns["iris_Normal"] = 10;
|
|
||||||
explicitVertexIns["mc_Entity"] = 11;
|
|
||||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()},
|
|
||||||
.explicitVertexInLocations = explicitVertexIns};
|
|
||||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
|
||||||
ASSERT_TRUE(programResult) << programResult.error().log;
|
|
||||||
|
|
||||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_VERTEX_SHADER}, .program = *programResult.value()};
|
|
||||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
|
||||||
ASSERT_TRUE(binaryResult) << binaryResult.error().log;
|
|
||||||
ASSERT_EQ(binaryResult->size(), 1u);
|
|
||||||
const auto& vertexBinary = binaryResult->front();
|
|
||||||
|
|
||||||
// The authoritative check - this is the same validator whose VUID the driver enforces.
|
|
||||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
|
||||||
String validatorMessages;
|
|
||||||
tools.SetMessageConsumer([&validatorMessages](spv_message_level_t, const char*, const spv_position_t&,
|
|
||||||
const char* message) {
|
|
||||||
if (message != nullptr) validatorMessages += String(message) + "\n";
|
|
||||||
});
|
|
||||||
EXPECT_TRUE(tools.Validate(vertexBinary))
|
|
||||||
<< "the raw vertex module is not valid SPIR-V; Adreno rejects the whole pipeline for this "
|
|
||||||
<< "while lavapipe tolerates it:\n"
|
|
||||||
<< validatorMessages;
|
|
||||||
|
|
||||||
// ...and, independently of the validator, every non-built-in Input carries a UNIQUE location.
|
|
||||||
constexpr unsigned kOpDecorate = 71, kOpVariable = 59;
|
|
||||||
constexpr unsigned kDecorationBuiltIn = 11, kDecorationLocation = 30;
|
|
||||||
constexpr unsigned kStorageClassInput = 1;
|
|
||||||
std::map<unsigned, unsigned> locationById;
|
|
||||||
std::set<unsigned> builtInIds;
|
|
||||||
std::vector<unsigned> inputIds;
|
|
||||||
for (SizeT i = 5; i < vertexBinary.size();) { // 5-word header
|
|
||||||
const unsigned wordCount = vertexBinary[i] >> 16;
|
|
||||||
const unsigned opcode = vertexBinary[i] & 0xFFFFu;
|
|
||||||
ASSERT_GT(wordCount, 0u) << "malformed SPIR-V instruction stream";
|
|
||||||
if (i + wordCount > vertexBinary.size()) break;
|
|
||||||
if (opcode == kOpDecorate && wordCount >= 4 && vertexBinary[i + 2] == kDecorationLocation) {
|
|
||||||
locationById[vertexBinary[i + 1]] = vertexBinary[i + 3];
|
|
||||||
} else if (opcode == kOpDecorate && wordCount >= 3 && vertexBinary[i + 2] == kDecorationBuiltIn) {
|
|
||||||
builtInIds.insert(vertexBinary[i + 1]);
|
|
||||||
} else if (opcode == kOpVariable && wordCount >= 4 && vertexBinary[i + 3] == kStorageClassInput) {
|
|
||||||
inputIds.push_back(vertexBinary[i + 2]);
|
|
||||||
}
|
|
||||||
i += wordCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::set<unsigned> usedLocations;
|
|
||||||
SizeT checked = 0;
|
|
||||||
for (const unsigned id : inputIds) {
|
|
||||||
if (builtInIds.count(id) != 0) continue;
|
|
||||||
const auto it = locationById.find(id);
|
|
||||||
ASSERT_NE(it, locationById.end())
|
|
||||||
<< "vertex input id " << id << " reached SPIR-V with no Location decoration";
|
|
||||||
EXPECT_TRUE(usedLocations.insert(it->second).second)
|
|
||||||
<< "two vertex inputs were assigned location " << it->second;
|
|
||||||
++checked;
|
|
||||||
}
|
|
||||||
EXPECT_GE(checked, 7u) << "expected all seven declared inputs to be present in the raw module";
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
// Storage-class census of module-scope OpVariables plus an OpFunctionCall count -
|
|
||||||
// everything the dead-interface-elimination tests need to see, nothing more.
|
|
||||||
struct SpirvVariableCensus {
|
|
||||||
SizeT inputCount = 0;
|
|
||||||
SizeT outputCount = 0;
|
|
||||||
SizeT privateCount = 0;
|
|
||||||
SizeT functionCallCount = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
SpirvVariableCensus TakeVariableCensus(const Vector<Uint32>& spirv) {
|
|
||||||
constexpr unsigned kOpVariable = 59, kOpFunctionCall = 57;
|
|
||||||
constexpr unsigned kStorageClassInput = 1, kStorageClassPrivate = 6, kStorageClassOutput = 3;
|
|
||||||
SpirvVariableCensus census;
|
|
||||||
for (SizeT i = 5; i < spirv.size();) { // 5-word header
|
|
||||||
const unsigned wordCount = spirv[i] >> 16;
|
|
||||||
const unsigned opcode = spirv[i] & 0xFFFFu;
|
|
||||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
|
||||||
if (opcode == kOpVariable && wordCount >= 4) {
|
|
||||||
switch (spirv[i + 3]) {
|
|
||||||
case kStorageClassInput: ++census.inputCount; break;
|
|
||||||
case kStorageClassOutput: ++census.outputCount; break;
|
|
||||||
case kStorageClassPrivate: ++census.privateCount; break;
|
|
||||||
default: break;
|
|
||||||
}
|
|
||||||
} else if (opcode == kOpFunctionCall) {
|
|
||||||
++census.functionCallCount;
|
|
||||||
}
|
|
||||||
i += wordCount;
|
|
||||||
}
|
|
||||||
return census;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The exact Iris shim shape that shipped an invalid module for a month: a declared
|
|
||||||
// vertex input whose only use is the initializer of a file-scope global nothing ever
|
|
||||||
// reads, in a shader whose main() still contains calls (which is what used to make
|
|
||||||
// ADCE keep the whole chain alive).
|
|
||||||
constexpr const char* kDeadPrivateChainVertexSource = R"(#version 460 core
|
|
||||||
in vec3 a_Position;
|
|
||||||
in vec2 mc_midTexCoord;
|
|
||||||
out vec4 v_Color;
|
|
||||||
vec4 iris_MidTex = vec4(mc_midTexCoord * (1.0 / 32768.0), 0.0, 1.0);
|
|
||||||
vec4 helperTint();
|
|
||||||
void main() {
|
|
||||||
v_Color = helperTint();
|
|
||||||
gl_Position = vec4(a_Position, 1.0);
|
|
||||||
}
|
|
||||||
vec4 helperTint() { return vec4(1.0); }
|
|
||||||
)";
|
|
||||||
|
|
||||||
Vector<Uint32> CompileVertexToRawSpirv(const String& source) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
ShaderAttrib shaderAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = source};
|
|
||||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
|
||||||
if (!shaderResult) {
|
|
||||||
ADD_FAILURE() << shaderResult.error().log;
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
|
||||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
|
||||||
if (!programResult) {
|
|
||||||
ADD_FAILURE() << programResult.error().log;
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_VERTEX_SHADER},
|
|
||||||
.program = *programResult.value()};
|
|
||||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
|
||||||
if (!binaryResult || binaryResult->size() != 1u) {
|
|
||||||
ADD_FAILURE() << (binaryResult ? "unexpected module count"
|
|
||||||
: binaryResult.error().log);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return binaryResult->front();
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SpirvValidationScope {
|
|
||||||
bool previous;
|
|
||||||
explicit SpirvValidationScope(bool enabled)
|
|
||||||
: previous(MG_Util::ShaderTranspiler::ShaderCompiler::SpirvValidationEnabled()) {
|
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::SetSpirvValidationEnabled(enabled);
|
|
||||||
}
|
|
||||||
~SpirvValidationScope() {
|
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::SetSpirvValidationEnabled(previous);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST_F(ProgramUtilTest, DeadPrivateChainVertexInputIsEliminatedFromOptimizedBinary) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
const Vector<Uint32> raw = CompileVertexToRawSpirv(kDeadPrivateChainVertexSource);
|
|
||||||
ASSERT_FALSE(raw.empty());
|
|
||||||
|
|
||||||
const SpirvVariableCensus before = TakeVariableCensus(raw);
|
|
||||||
// Preconditions that make this module exercise the ADCE conservatism gate: the dead
|
|
||||||
// input is present, its Private sink is present, and main() still contains a call.
|
|
||||||
// Four Inputs, not two: the frontend always emits gl_VertexIndex/gl_InstanceIndex
|
|
||||||
// built-ins alongside a_Position and mc_midTexCoord.
|
|
||||||
ASSERT_EQ(before.inputCount, 4u)
|
|
||||||
<< "expected a_Position, mc_midTexCoord, gl_VertexIndex and gl_InstanceIndex in the raw module";
|
|
||||||
ASSERT_GE(before.privateCount, 1u);
|
|
||||||
ASSERT_GE(before.functionCallCount, 1u)
|
|
||||||
<< "helperTint() was inlined by the frontend; this test no longer covers the "
|
|
||||||
<< "entry-point-with-calls shape it exists for";
|
|
||||||
|
|
||||||
Vector<Uint32> optimized;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
|
||||||
|
|
||||||
const SpirvVariableCensus after = TakeVariableCensus(optimized);
|
|
||||||
EXPECT_EQ(after.inputCount, 1u)
|
|
||||||
<< "mc_midTexCoord feeds only a never-read Private global and must not reach the driver";
|
|
||||||
|
|
||||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
|
||||||
String validatorMessages;
|
|
||||||
tools.SetMessageConsumer([&validatorMessages](spv_message_level_t, const char*,
|
|
||||||
const spv_position_t&, const char* message) {
|
|
||||||
if (message != nullptr) validatorMessages += String(message) + "\n";
|
|
||||||
});
|
|
||||||
EXPECT_TRUE(tools.Validate(optimized)) << validatorMessages;
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(ProgramUtilTest, DeclaredButUnwrittenOutputSurvivesOptimization) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
// Chocapic-class packs declare varyings some variants never write while the paired
|
|
||||||
// fragment shader still reads them. The OpVariable (and its Location) must survive the
|
|
||||||
// chain on both backends: Espryt's ESSL link would otherwise fail with "varying not
|
|
||||||
// declared in vertex shader", and Magma's stage-interface contract breaks the same way.
|
|
||||||
// ADCE guarantees this only while remove_outputs stays false - this test freezes that.
|
|
||||||
const Vector<Uint32> raw = CompileVertexToRawSpirv(R"(#version 460 core
|
|
||||||
in vec3 a_Position;
|
|
||||||
out vec4 v_Written;
|
|
||||||
out vec4 v_NeverWritten;
|
|
||||||
void main() {
|
|
||||||
v_Written = vec4(1.0);
|
|
||||||
gl_Position = vec4(a_Position, 1.0);
|
|
||||||
}
|
|
||||||
)");
|
|
||||||
ASSERT_FALSE(raw.empty());
|
|
||||||
// v_Written, v_NeverWritten, and the gl_PerVertex block are all Output-storage variables.
|
|
||||||
const SpirvVariableCensus before = TakeVariableCensus(raw);
|
|
||||||
ASSERT_GE(before.outputCount, 3u);
|
|
||||||
|
|
||||||
Vector<Uint32> optimized;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
|
||||||
EXPECT_EQ(TakeVariableCensus(optimized).outputCount, before.outputCount)
|
|
||||||
<< "a declared-but-unwritten output was deleted; a fragment stage reading it now "
|
|
||||||
<< "fails to link (ES) or breaks the Vulkan stage interface";
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(ProgramUtilTest, ValidationLatchFlagsInvalidModuleWithoutChangingResults) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
// Only LIVE inputs, so the chain cannot heal the module by deleting them: both
|
|
||||||
// survive to the output, undecorated, and the output is invalid SPIR-V.
|
|
||||||
Vector<Uint32> raw = CompileVertexToRawSpirv(R"(#version 460 core
|
|
||||||
in vec3 a_Position;
|
|
||||||
in vec4 a_Color;
|
|
||||||
out vec4 v_Color;
|
|
||||||
void main() {
|
|
||||||
v_Color = a_Color;
|
|
||||||
gl_Position = vec4(a_Position, 1.0);
|
|
||||||
}
|
|
||||||
)");
|
|
||||||
ASSERT_FALSE(raw.empty());
|
|
||||||
|
|
||||||
// Strip every Input Location decoration - the exact defect class the
|
|
||||||
// TMglGlslIoResolver used to ship ([VUID-StandaloneSpirv-Location-04916]).
|
|
||||||
constexpr unsigned kOpDecorate = 71, kOpVariable = 59;
|
|
||||||
constexpr unsigned kDecorationLocation = 30, kStorageClassInput = 1;
|
|
||||||
std::set<unsigned> inputIds;
|
|
||||||
for (SizeT i = 5; i < raw.size();) {
|
|
||||||
const unsigned wordCount = raw[i] >> 16;
|
|
||||||
const unsigned opcode = raw[i] & 0xFFFFu;
|
|
||||||
ASSERT_GT(wordCount, 0u);
|
|
||||||
if (i + wordCount > raw.size()) break;
|
|
||||||
if (opcode == kOpVariable && wordCount >= 4 && raw[i + 3] == kStorageClassInput) {
|
|
||||||
inputIds.insert(raw[i + 2]);
|
|
||||||
}
|
|
||||||
i += wordCount;
|
|
||||||
}
|
|
||||||
SizeT strippedCount = 0;
|
|
||||||
for (SizeT i = 5; i < raw.size();) {
|
|
||||||
const unsigned wordCount = raw[i] >> 16;
|
|
||||||
const unsigned opcode = raw[i] & 0xFFFFu;
|
|
||||||
if (wordCount == 0 || i + wordCount > raw.size()) break;
|
|
||||||
if (opcode == kOpDecorate && wordCount >= 4 && raw[i + 2] == kDecorationLocation &&
|
|
||||||
inputIds.count(raw[i + 1]) != 0) {
|
|
||||||
raw.erase(raw.begin() + static_cast<std::ptrdiff_t>(i),
|
|
||||||
raw.begin() + static_cast<std::ptrdiff_t>(i + wordCount));
|
|
||||||
++strippedCount;
|
|
||||||
continue; // do not advance: the next instruction moved into place
|
|
||||||
}
|
|
||||||
i += wordCount;
|
|
||||||
}
|
|
||||||
ASSERT_GE(strippedCount, 2u) << "expected to strip both live inputs' Location decorations";
|
|
||||||
|
|
||||||
Vector<Uint32> optimized;
|
|
||||||
{
|
|
||||||
// The armed lane: control flow is IDENTICAL to shipping (the wrapper still
|
|
||||||
// succeeds - fail-open call sites downstream must not see a different world),
|
|
||||||
// and the failure latch is the signal. This is the catch that took a device
|
|
||||||
// bisect to find when the validator was off everywhere.
|
|
||||||
SpirvValidationScope validationOn(true);
|
|
||||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
|
||||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
|
||||||
EXPECT_GT(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
|
||||||
<< "an invalid optimized module must bump the validation-failure latch";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// The shipping configuration: same result, no validation, latch untouched.
|
|
||||||
SpirvValidationScope validationOff(false);
|
|
||||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
|
||||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
|
||||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
// OpTypeImage: result id (+1), sampled type (+2), dim (+3). Dim::Rect == 4.
|
|
||||||
SizeT CountRectImageTypes(const Vector<Uint32>& spirv) {
|
|
||||||
constexpr unsigned kOpTypeImage = 25, kDimRect = 4;
|
|
||||||
SizeT count = 0;
|
|
||||||
for (SizeT i = 5; i < spirv.size();) {
|
|
||||||
const unsigned wordCount = spirv[i] >> 16;
|
|
||||||
const unsigned opcode = spirv[i] & 0xFFFFu;
|
|
||||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
|
||||||
if (opcode == kOpTypeImage && wordCount >= 4 && spirv[i + 3] == kDimRect) {
|
|
||||||
++count;
|
|
||||||
}
|
|
||||||
i += wordCount;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// True when any OpDecorate Location targets a UniformConstant/Uniform-storage
|
|
||||||
// variable ([VUID-StandaloneSpirv-Location-06672]).
|
|
||||||
bool AnyLocationOnUniformStorage(const Vector<Uint32>& spirv) {
|
|
||||||
constexpr unsigned kOpDecorate = 71, kOpVariable = 59, kDecorationLocation = 30;
|
|
||||||
constexpr unsigned kStorageUniformConstant = 0, kStorageUniform = 2;
|
|
||||||
std::set<unsigned> locatedIds;
|
|
||||||
for (SizeT i = 5; i < spirv.size();) {
|
|
||||||
const unsigned wordCount = spirv[i] >> 16;
|
|
||||||
const unsigned opcode = spirv[i] & 0xFFFFu;
|
|
||||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
|
||||||
if (opcode == kOpDecorate && wordCount >= 4 && spirv[i + 2] == kDecorationLocation) {
|
|
||||||
locatedIds.insert(spirv[i + 1]);
|
|
||||||
}
|
|
||||||
i += wordCount;
|
|
||||||
}
|
|
||||||
for (SizeT i = 5; i < spirv.size();) {
|
|
||||||
const unsigned wordCount = spirv[i] >> 16;
|
|
||||||
const unsigned opcode = spirv[i] & 0xFFFFu;
|
|
||||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
|
||||||
if (opcode == kOpVariable && wordCount >= 4 &&
|
|
||||||
(spirv[i + 3] == kStorageUniformConstant || spirv[i + 3] == kStorageUniform) &&
|
|
||||||
locatedIds.count(spirv[i + 2]) != 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
i += wordCount;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST_F(ProgramUtilTest, RectangleSamplerModuleLeavesTheChainVulkanLegal) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
// Dim::Rect is invalid under every Vulkan environment; the lowering used to run
|
|
||||||
// only in the backends, i.e. AFTER the chain whose output the validating lanes
|
|
||||||
// check. It now runs inside the chain, so the driver-bound bytes are rect-free.
|
|
||||||
const Vector<Uint32> raw = CompileVertexToRawSpirv(R"(#version 460 core
|
|
||||||
in vec3 a_Position;
|
|
||||||
uniform sampler2DRect uRect;
|
|
||||||
out vec4 v_Color;
|
|
||||||
void main() {
|
|
||||||
v_Color = texture(uRect, a_Position.xy);
|
|
||||||
gl_Position = vec4(a_Position, 1.0);
|
|
||||||
}
|
|
||||||
)");
|
|
||||||
ASSERT_FALSE(raw.empty());
|
|
||||||
ASSERT_GE(CountRectImageTypes(raw), 1u) << "glslang no longer emits Dim::Rect for sampler2DRect";
|
|
||||||
|
|
||||||
SpirvValidationScope validationOn(true);
|
|
||||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
|
||||||
Vector<Uint32> optimized;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
|
||||||
EXPECT_EQ(CountRectImageTypes(optimized), 0u);
|
|
||||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
|
||||||
<< "a rectangle module must leave the chain valid, not latched as a failure";
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(ProgramUtilTest, ExplicitSamplerLocationIsStrippedFromTheOptimizedBinary) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
// glslang's relaxed GL path keeps layout(location=N) on the UniformConstant
|
|
||||||
// variable, which Vulkan forbids; nothing downstream reads it (GL locations come
|
|
||||||
// from phase-A reflection, Vulkan bindings go by name).
|
|
||||||
const Vector<Uint32> raw = CompileVertexToRawSpirv(R"(#version 460 core
|
|
||||||
in vec3 a_Position;
|
|
||||||
layout(location = 5) uniform sampler2D uTex;
|
|
||||||
out vec4 v_Color;
|
|
||||||
void main() {
|
|
||||||
v_Color = texture(uTex, a_Position.xy);
|
|
||||||
gl_Position = vec4(a_Position, 1.0);
|
|
||||||
}
|
|
||||||
)");
|
|
||||||
ASSERT_FALSE(raw.empty());
|
|
||||||
ASSERT_TRUE(AnyLocationOnUniformStorage(raw))
|
|
||||||
<< "glslang no longer keeps the explicit uniform location; the strip pass may be obsolete";
|
|
||||||
|
|
||||||
SpirvValidationScope validationOn(true);
|
|
||||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
|
||||||
Vector<Uint32> optimized;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
|
||||||
EXPECT_FALSE(AnyLocationOnUniformStorage(optimized));
|
|
||||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
|
||||||
<< "the stripped module must validate clean";
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,222 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Test/Program/XfbBlockVaryingTest.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
|
|
||||||
|
|
||||||
// Transform-feedback capture of a member of an output interface block.
|
|
||||||
//
|
|
||||||
// GL 4.6 core 11.1.2.1 names such a varying "<BLOCK name>.<member>" - the block's TYPE
|
|
||||||
// name, never the instance name - which is exactly what KHR-GL4x.vertex_attrib_binding
|
|
||||||
// (gl4cVertexAttribBindingTests.cpp:419-437, `out StageData { vec4 attrib[16]; } vs_out;`
|
|
||||||
// captured as "StageData.attrib[0]".."[15]") relies on. The resolver used to match the
|
|
||||||
// requested name against glslang's linker-object symbol name, which for a block is the
|
|
||||||
// INSTANCE ("vs_out"), so every one of those captures came back unresolved and the link
|
|
||||||
// failed with "is not an output of the vertex stage" + GL_INVALID_VALUE.
|
|
||||||
//
|
|
||||||
// GPU-free: everything asserted here is a property of the link, not of any driver.
|
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "Includes.h"
|
|
||||||
#include "Init.h"
|
|
||||||
#include "MG_Impl/GLImpl/Getter/GL_Getter.h"
|
|
||||||
#include "MG_Impl/GLImpl/Program/GL_Program.h"
|
|
||||||
#include "MG_State/GLState/Core.h"
|
|
||||||
|
|
||||||
using namespace MobileGL;
|
|
||||||
using namespace MobileGL::MG_Impl::GLImpl;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
class XfbBlockVaryingTest : public ::testing::Test {
|
|
||||||
protected:
|
|
||||||
void SetUp() override { MobileGL::Initialize(); }
|
|
||||||
};
|
|
||||||
|
|
||||||
GLuint MakeVsOnlyProgram(const char* vs) {
|
|
||||||
const GLuint program = CreateProgram();
|
|
||||||
const GLuint shader = CreateShader(GL_VERTEX_SHADER);
|
|
||||||
ShaderSource(shader, 1, &vs, nullptr);
|
|
||||||
CompileShader(shader);
|
|
||||||
GLint compiled = GL_FALSE;
|
|
||||||
GetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
|
||||||
EXPECT_EQ(compiled, GL_TRUE) << [&] {
|
|
||||||
char log[4096] = "";
|
|
||||||
GetShaderInfoLog(shader, sizeof(log), nullptr, log);
|
|
||||||
return std::string(log);
|
|
||||||
}();
|
|
||||||
AttachShader(program, shader);
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string LinkLog(GLuint program) {
|
|
||||||
char log[4096] = "";
|
|
||||||
GetProgramInfoLog(program, sizeof(log), nullptr, log);
|
|
||||||
return std::string(log);
|
|
||||||
}
|
|
||||||
|
|
||||||
GLint Programiv(GLuint program, GLenum pname) {
|
|
||||||
GLint value = -1;
|
|
||||||
GetProgramiv(program, pname, &value);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct VaryingRecord {
|
|
||||||
std::string name;
|
|
||||||
GLsizei size = 0;
|
|
||||||
GLenum type = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
VaryingRecord Varying(GLuint program, GLuint index) {
|
|
||||||
VaryingRecord record;
|
|
||||||
GLchar buffer[256] = {'\0'};
|
|
||||||
GLsizei length = 0;
|
|
||||||
GetTransformFeedbackVarying(program, index, sizeof(buffer), &length, &record.size, &record.type, buffer);
|
|
||||||
record.name.assign(buffer, buffer + (length < 0 ? 0 : length));
|
|
||||||
return record;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClearErrors() {
|
|
||||||
for (int i = 0; i < 32 && GetError() != GL_NO_ERROR; ++i) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The CTS shader, narrowed to two elements so the expectations stay readable.
|
|
||||||
const char* kNamedBlockVs = R"(#version 430 core
|
|
||||||
layout(location = 0) in vec4 vs_in_attrib[2];
|
|
||||||
out StageData {
|
|
||||||
vec4 attrib[2];
|
|
||||||
} vs_out;
|
|
||||||
void main() {
|
|
||||||
for (int i = 0; i < vs_in_attrib.length(); ++i) {
|
|
||||||
vs_out.attrib[i] = vs_in_attrib[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
TEST_F(XfbBlockVaryingTest, CapturesBlockMemberElementsByBlockTypeName) {
|
|
||||||
ClearErrors();
|
|
||||||
const GLuint program = MakeVsOnlyProgram(kNamedBlockVs);
|
|
||||||
const GLchar* const varyings[2] = {"StageData.attrib[0]", "StageData.attrib[1]"};
|
|
||||||
TransformFeedbackVaryings(program, 2, varyings, GL_INTERLEAVED_ATTRIBS);
|
|
||||||
LinkProgram(program);
|
|
||||||
|
|
||||||
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program);
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
EXPECT_EQ(Programiv(program, GL_TRANSFORM_FEEDBACK_VARYINGS), 2);
|
|
||||||
EXPECT_EQ(Programiv(program, GL_TRANSFORM_FEEDBACK_BUFFER_MODE), GL_INTERLEAVED_ATTRIBS);
|
|
||||||
|
|
||||||
for (GLuint i = 0; i < 2; ++i) {
|
|
||||||
const VaryingRecord record = Varying(program, i);
|
|
||||||
EXPECT_EQ(record.name, std::string("StageData.attrib[") + std::to_string(i) + "]");
|
|
||||||
// One element of the member array, not the whole array.
|
|
||||||
EXPECT_EQ(record.size, 1) << "index " << i;
|
|
||||||
EXPECT_EQ(record.type, static_cast<GLenum>(GL_FLOAT_VEC4)) << "index " << i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The whole member, no subscript: the array size has to survive.
|
|
||||||
TEST_F(XfbBlockVaryingTest, CapturesAWholeBlockMemberArray) {
|
|
||||||
ClearErrors();
|
|
||||||
const GLuint program = MakeVsOnlyProgram(kNamedBlockVs);
|
|
||||||
const GLchar* const varyings[1] = {"StageData.attrib"};
|
|
||||||
TransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS);
|
|
||||||
LinkProgram(program);
|
|
||||||
|
|
||||||
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program);
|
|
||||||
const VaryingRecord record = Varying(program, 0);
|
|
||||||
EXPECT_EQ(record.name, "StageData.attrib");
|
|
||||||
EXPECT_EQ(record.size, 2);
|
|
||||||
EXPECT_EQ(record.type, static_cast<GLenum>(GL_FLOAT_VEC4));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Members of an anonymous instance are named the same way - the block name is still
|
|
||||||
// what identifies them, and there is no instance name to fall back on.
|
|
||||||
TEST_F(XfbBlockVaryingTest, CapturesAnonymousInstanceBlockMember) {
|
|
||||||
ClearErrors();
|
|
||||||
const GLuint program = MakeVsOnlyProgram(R"(#version 430 core
|
|
||||||
layout(location = 0) in vec4 vs_in_attrib;
|
|
||||||
out StageData {
|
|
||||||
vec4 color;
|
|
||||||
vec2 uv;
|
|
||||||
};
|
|
||||||
void main() {
|
|
||||||
color = vs_in_attrib;
|
|
||||||
uv = vs_in_attrib.xy;
|
|
||||||
}
|
|
||||||
)");
|
|
||||||
const GLchar* const varyings[2] = {"StageData.color", "StageData.uv"};
|
|
||||||
TransformFeedbackVaryings(program, 2, varyings, GL_INTERLEAVED_ATTRIBS);
|
|
||||||
LinkProgram(program);
|
|
||||||
|
|
||||||
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program);
|
|
||||||
EXPECT_EQ(Varying(program, 0).type, static_cast<GLenum>(GL_FLOAT_VEC4));
|
|
||||||
EXPECT_EQ(Varying(program, 1).type, static_cast<GLenum>(GL_FLOAT_VEC2));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The instance-qualified spelling is not what the spec asks for, but it is what a lot of
|
|
||||||
// application code writes; resolving it too costs nothing and keeps those links alive.
|
|
||||||
TEST_F(XfbBlockVaryingTest, AlsoAcceptsTheInstanceQualifiedSpelling) {
|
|
||||||
ClearErrors();
|
|
||||||
const GLuint program = MakeVsOnlyProgram(kNamedBlockVs);
|
|
||||||
const GLchar* const varyings[1] = {"vs_out.attrib[1]"};
|
|
||||||
TransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS);
|
|
||||||
LinkProgram(program);
|
|
||||||
|
|
||||||
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program);
|
|
||||||
EXPECT_EQ(Varying(program, 0).size, 1);
|
|
||||||
EXPECT_EQ(Varying(program, 0).type, static_cast<GLenum>(GL_FLOAT_VEC4));
|
|
||||||
}
|
|
||||||
|
|
||||||
// A dotted path that resolves to nothing must still fail the link, and say so - the
|
|
||||||
// fix must not turn "unknown member" into a silently dropped capture.
|
|
||||||
TEST_F(XfbBlockVaryingTest, RejectsAnUnknownBlockMember) {
|
|
||||||
ClearErrors();
|
|
||||||
const GLuint program = MakeVsOnlyProgram(kNamedBlockVs);
|
|
||||||
const GLchar* const varyings[1] = {"StageData.missing"};
|
|
||||||
TransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS);
|
|
||||||
LinkProgram(program);
|
|
||||||
|
|
||||||
EXPECT_EQ(Programiv(program, GL_LINK_STATUS), GL_FALSE);
|
|
||||||
EXPECT_NE(LinkLog(program).find("StageData.missing"), std::string::npos) << LinkLog(program);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(XfbBlockVaryingTest, RejectsAnUnknownBlock) {
|
|
||||||
ClearErrors();
|
|
||||||
const GLuint program = MakeVsOnlyProgram(kNamedBlockVs);
|
|
||||||
const GLchar* const varyings[1] = {"NoSuchBlock.attrib[0]"};
|
|
||||||
TransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS);
|
|
||||||
LinkProgram(program);
|
|
||||||
|
|
||||||
EXPECT_EQ(Programiv(program, GL_LINK_STATUS), GL_FALSE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plain (non-block) outputs must keep resolving exactly as before.
|
|
||||||
TEST_F(XfbBlockVaryingTest, StillResolvesPlainOutputs) {
|
|
||||||
ClearErrors();
|
|
||||||
const GLuint program = MakeVsOnlyProgram(R"(#version 430 core
|
|
||||||
layout(location = 0) in vec4 vs_in_attrib;
|
|
||||||
out vec4 plain[2];
|
|
||||||
out vec3 single;
|
|
||||||
void main() {
|
|
||||||
plain[0] = vs_in_attrib;
|
|
||||||
plain[1] = vs_in_attrib;
|
|
||||||
single = vs_in_attrib.xyz;
|
|
||||||
}
|
|
||||||
)");
|
|
||||||
const GLchar* const varyings[3] = {"plain[1]", "single", "gl_Position"};
|
|
||||||
TransformFeedbackVaryings(program, 3, varyings, GL_INTERLEAVED_ATTRIBS);
|
|
||||||
LinkProgram(program);
|
|
||||||
|
|
||||||
ASSERT_EQ(Programiv(program, GL_LINK_STATUS), GL_TRUE) << LinkLog(program);
|
|
||||||
EXPECT_EQ(Varying(program, 0).size, 1);
|
|
||||||
EXPECT_EQ(Varying(program, 0).type, static_cast<GLenum>(GL_FLOAT_VEC4));
|
|
||||||
EXPECT_EQ(Varying(program, 1).type, static_cast<GLenum>(GL_FLOAT_VEC3));
|
|
||||||
EXPECT_EQ(Varying(program, 2).type, static_cast<GLenum>(GL_FLOAT_VEC4));
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
@@ -1822,170 +1822,13 @@ TEST(DirectGLESBackendTexture, DestructorDeletesIdAndScrubsBindingCache) {
|
|||||||
// A wrapper whose context died must NOT delete a foreign (recycled) name.
|
// A wrapper whose context died must NOT delete a foreign (recycled) name.
|
||||||
{
|
{
|
||||||
auto backendTexture = MobileGL::MakeShared<TextureImpl::BackendTextureObject>();
|
auto backendTexture = MobileGL::MakeShared<TextureImpl::BackendTextureObject>();
|
||||||
++g_backendContextGeneration;
|
++TextureImpl::g_textureContextGeneration;
|
||||||
backendTexture.reset();
|
backendTexture.reset();
|
||||||
--g_backendContextGeneration; // restore for later tests
|
--TextureImpl::g_textureContextGeneration; // restore for later tests
|
||||||
EXPECT_EQ(deleted.size(), 1u);
|
EXPECT_EQ(deleted.size(), 1u);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- DirectGLES backend twins release their driver ids --------------------------------------
|
|
||||||
// Framebuffers, renderbuffers and samplers had no destructor at all: every frontend object the
|
|
||||||
// application deleted leaked its ES twin for the whole process lifetime. An application that
|
|
||||||
// creates a framebuffer per readback (GL CTS packed_pixels.varied_rectangle makes ~3300 of them
|
|
||||||
// per case) walked the driver into a gigabyte of dead framebuffers, and past that point every
|
|
||||||
// readback through a freshly attached framebuffer came back with stale pixels.
|
|
||||||
namespace {
|
|
||||||
struct TwinDeletionSinks {
|
|
||||||
MobileGL::Vector<GLuint> framebuffers;
|
|
||||||
MobileGL::Vector<GLuint> renderbuffers;
|
|
||||||
MobileGL::Vector<GLuint> samplers;
|
|
||||||
};
|
|
||||||
|
|
||||||
TwinDeletionSinks* g_twinDeletionSinks = nullptr;
|
|
||||||
GLuint g_nextTwinDriverId = 900;
|
|
||||||
|
|
||||||
void TW_GenFramebuffers(GLsizei count, GLuint* ids) {
|
|
||||||
for (GLsizei i = 0; i < count; ++i) ids[i] = g_nextTwinDriverId++;
|
|
||||||
}
|
|
||||||
void TW_DeleteFramebuffers(GLsizei count, const GLuint* ids) {
|
|
||||||
if (!g_twinDeletionSinks) return;
|
|
||||||
for (GLsizei i = 0; i < count; ++i) g_twinDeletionSinks->framebuffers.push_back(ids[i]);
|
|
||||||
}
|
|
||||||
void TW_GenRenderbuffers(GLsizei count, GLuint* ids) {
|
|
||||||
for (GLsizei i = 0; i < count; ++i) ids[i] = g_nextTwinDriverId++;
|
|
||||||
}
|
|
||||||
void TW_DeleteRenderbuffers(GLsizei count, const GLuint* ids) {
|
|
||||||
if (!g_twinDeletionSinks) return;
|
|
||||||
for (GLsizei i = 0; i < count; ++i) g_twinDeletionSinks->renderbuffers.push_back(ids[i]);
|
|
||||||
}
|
|
||||||
void TW_GenSamplers(GLsizei count, GLuint* ids) {
|
|
||||||
for (GLsizei i = 0; i < count; ++i) ids[i] = g_nextTwinDriverId++;
|
|
||||||
}
|
|
||||||
void TW_DeleteSamplers(GLsizei count, const GLuint* ids) {
|
|
||||||
if (!g_twinDeletionSinks) return;
|
|
||||||
for (GLsizei i = 0; i < count; ++i) g_twinDeletionSinks->samplers.push_back(ids[i]);
|
|
||||||
}
|
|
||||||
void TW_BindFramebuffer(GLenum target, GLuint framebuffer) {
|
|
||||||
SG_Log("BindFramebuffer:" + std::to_string(target) + ":" + std::to_string(framebuffer));
|
|
||||||
}
|
|
||||||
void TW_BindSampler(GLuint, GLuint) {}
|
|
||||||
void TW_BindRenderbuffer(GLenum, GLuint) {}
|
|
||||||
|
|
||||||
// Installs a table that can create and destroy all three twin kinds, and unwinds it (plus the
|
|
||||||
// recording pointer) even when an assertion aborts the test body.
|
|
||||||
struct ScopedBackendTwinMocks {
|
|
||||||
ScopedBackendTwinMocks(): previousFunctions(MobileGL::MG_Backend::DirectGLES::g_GLESFuncs) {
|
|
||||||
MobileGL::MG_Backend::DirectGLES::FramebufferImpl::InvalidateFramebufferBindingCache();
|
|
||||||
MobileGL::MG_External::GLESFunctionsTable functions{};
|
|
||||||
functions.glGenFramebuffers = TW_GenFramebuffers;
|
|
||||||
functions.glDeleteFramebuffers = TW_DeleteFramebuffers;
|
|
||||||
functions.glBindFramebuffer = TW_BindFramebuffer;
|
|
||||||
functions.glGenRenderbuffers = TW_GenRenderbuffers;
|
|
||||||
functions.glDeleteRenderbuffers = TW_DeleteRenderbuffers;
|
|
||||||
functions.glBindRenderbuffer = TW_BindRenderbuffer;
|
|
||||||
functions.glGenSamplers = TW_GenSamplers;
|
|
||||||
functions.glDeleteSamplers = TW_DeleteSamplers;
|
|
||||||
functions.glBindSampler = TW_BindSampler;
|
|
||||||
functions.glGetError = SG_NoError;
|
|
||||||
MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(functions);
|
|
||||||
g_twinDeletionSinks = &sinks;
|
|
||||||
g_stateGuardLog = &log;
|
|
||||||
}
|
|
||||||
|
|
||||||
~ScopedBackendTwinMocks() {
|
|
||||||
g_stateGuardLog = nullptr;
|
|
||||||
g_twinDeletionSinks = nullptr;
|
|
||||||
MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(previousFunctions);
|
|
||||||
MobileGL::MG_Backend::DirectGLES::FramebufferImpl::InvalidateFramebufferBindingCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
ScopedBackendTwinMocks(const ScopedBackendTwinMocks&) = delete;
|
|
||||||
ScopedBackendTwinMocks& operator=(const ScopedBackendTwinMocks&) = delete;
|
|
||||||
|
|
||||||
TwinDeletionSinks sinks;
|
|
||||||
StateGuardCallLog log;
|
|
||||||
MobileGL::MG_External::GLESFunctionsTable previousFunctions;
|
|
||||||
};
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST(DirectGLESBackendFramebuffer, DestructorDeletesIdAndScrubsBindingShadow) {
|
|
||||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
|
||||||
ScopedBackendTwinMocks mocks;
|
|
||||||
|
|
||||||
GLuint id = 0;
|
|
||||||
{
|
|
||||||
auto backendFBO = MobileGL::MakeShared<FramebufferImpl::BackendFramebufferObject>();
|
|
||||||
id = backendFBO->GetBackendFramebufferId();
|
|
||||||
ASSERT_NE(id, 0u);
|
|
||||||
backendFBO->Bind(MobileGL::FramebufferTarget::Draw);
|
|
||||||
ASSERT_EQ(FramebufferImpl::CurrentFramebufferBinding(MobileGL::FramebufferTarget::Draw), id);
|
|
||||||
}
|
|
||||||
ASSERT_EQ(mocks.sinks.framebuffers.size(), 1u);
|
|
||||||
EXPECT_EQ(mocks.sinks.framebuffers[0], id);
|
|
||||||
// ES reverts every target bound to a deleted framebuffer to 0. The shadow has to follow, or
|
|
||||||
// the next BindFramebufferId(0) is deduped away and the driver keeps the dead name bound.
|
|
||||||
EXPECT_EQ(FramebufferImpl::CurrentFramebufferBinding(MobileGL::FramebufferTarget::Draw), 0u);
|
|
||||||
|
|
||||||
// A twin whose context died must NOT delete a name a successor context may have recycled.
|
|
||||||
{
|
|
||||||
auto backendFBO = MobileGL::MakeShared<FramebufferImpl::BackendFramebufferObject>();
|
|
||||||
++g_backendContextGeneration;
|
|
||||||
backendFBO.reset();
|
|
||||||
--g_backendContextGeneration; // restore for later tests
|
|
||||||
EXPECT_EQ(mocks.sinks.framebuffers.size(), 1u);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(DirectGLESBackendRenderbuffer, DestructorDeletesId) {
|
|
||||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
|
||||||
ScopedBackendTwinMocks mocks;
|
|
||||||
|
|
||||||
GLuint id = 0;
|
|
||||||
{
|
|
||||||
auto backendRBO = MobileGL::MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
|
|
||||||
id = backendRBO->GetBackendRenderbufferId();
|
|
||||||
ASSERT_NE(id, 0u);
|
|
||||||
}
|
|
||||||
ASSERT_EQ(mocks.sinks.renderbuffers.size(), 1u);
|
|
||||||
EXPECT_EQ(mocks.sinks.renderbuffers[0], id);
|
|
||||||
|
|
||||||
{
|
|
||||||
auto backendRBO = MobileGL::MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
|
|
||||||
++g_backendContextGeneration;
|
|
||||||
backendRBO.reset();
|
|
||||||
--g_backendContextGeneration;
|
|
||||||
EXPECT_EQ(mocks.sinks.renderbuffers.size(), 1u);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(DirectGLESBackendSampler, DestructorDeletesIdAndScrubsUnitCache) {
|
|
||||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
|
||||||
ScopedBackendTwinMocks mocks;
|
|
||||||
|
|
||||||
GLuint id = 0;
|
|
||||||
{
|
|
||||||
auto backendSampler = MobileGL::MakeShared<SamplerImpl::BackendSamplerObject>();
|
|
||||||
id = backendSampler->GetBackendSamplerId();
|
|
||||||
ASSERT_NE(id, 0u);
|
|
||||||
backendSampler->Bind(3);
|
|
||||||
ASSERT_EQ(SamplerImpl::g_boundSamplersCache[3], backendSampler.get());
|
|
||||||
}
|
|
||||||
ASSERT_EQ(mocks.sinks.samplers.size(), 1u);
|
|
||||||
EXPECT_EQ(mocks.sinks.samplers[0], id);
|
|
||||||
// glDeleteSamplers unbinds from every unit, and the next twin can land on this heap
|
|
||||||
// address - a stale row would false-skip its Bind.
|
|
||||||
EXPECT_EQ(SamplerImpl::g_boundSamplersCache[3], nullptr);
|
|
||||||
|
|
||||||
{
|
|
||||||
auto backendSampler = MobileGL::MakeShared<SamplerImpl::BackendSamplerObject>();
|
|
||||||
++g_backendContextGeneration;
|
|
||||||
backendSampler.reset();
|
|
||||||
--g_backendContextGeneration;
|
|
||||||
EXPECT_EQ(mocks.sinks.samplers.size(), 1u);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(DirectGLESStateGuards, DefaultFramebufferBindGoesThroughShadow) {
|
TEST(DirectGLESStateGuards, DefaultFramebufferBindGoesThroughShadow) {
|
||||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||||
ScopedStateGuardMocks mocks;
|
ScopedStateGuardMocks mocks;
|
||||||
|
|||||||
@@ -25,53 +25,3 @@ endif()
|
|||||||
|
|
||||||
include(GoogleTest)
|
include(GoogleTest)
|
||||||
gtest_discover_tests(ObjectLifetimeIdTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(ObjectLifetimeIdTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
|
||||||
add_executable(
|
|
||||||
RenderStateTest
|
|
||||||
RenderStateTest.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(RenderStateTest PRIVATE
|
|
||||||
${MGL_ROOT}/include
|
|
||||||
${MGL_ROOT}/MobileGL
|
|
||||||
${MGL_ROOT}/3rdparty/xxHash
|
|
||||||
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
|
|
||||||
${MGL_ROOT}/3rdparty/SPIRV-Reflect
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
RenderStateTest PRIVATE
|
|
||||||
GTest::gtest_main
|
|
||||||
${LINK_LIBRARIES}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (MSVC)
|
|
||||||
target_compile_options(RenderStateTest PRIVATE /Zc:preprocessor)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
gtest_discover_tests(RenderStateTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
|
||||||
|
|
||||||
add_executable(
|
|
||||||
NegativeApiErrorsTest
|
|
||||||
NegativeApiErrorsTest.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(NegativeApiErrorsTest PRIVATE
|
|
||||||
${MGL_ROOT}/include
|
|
||||||
${MGL_ROOT}/MobileGL
|
|
||||||
${MGL_ROOT}/3rdparty/xxHash
|
|
||||||
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
|
|
||||||
${MGL_ROOT}/3rdparty/SPIRV-Reflect
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
NegativeApiErrorsTest PRIVATE
|
|
||||||
GTest::gtest_main
|
|
||||||
${LINK_LIBRARIES}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (MSVC)
|
|
||||||
target_compile_options(NegativeApiErrorsTest PRIVATE /Zc:preprocessor)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
gtest_discover_tests(NegativeApiErrorsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
|
||||||
|
|||||||
@@ -1,304 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Test/State/NegativeApiErrorsTest.cpp
|
|
||||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
||||||
// Licensed under the GNU Lesser General Public License v3.0:
|
|
||||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
||||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// End of Source File Header
|
|
||||||
|
|
||||||
// The negative-path GL errors the conformance suite checks and MobileGL used to answer
|
|
||||||
// GL_NO_ERROR to. Every row here is a call the spec requires to fail, lifted from the CTS case
|
|
||||||
// that found it:
|
|
||||||
// * KHR-GL44.multi_bind.errors_bind_buffers / .errors_bind_samplers - ARB_multi_bind's
|
|
||||||
// "buffers/samplers will not be created if they do not exist" rule, plus the atomic-counter
|
|
||||||
// offset alignment the single-bind path never had.
|
|
||||||
// * KHR-GL43.shader_storage_buffer_object.negative-api-bind - the SSBO offset alignment is a
|
|
||||||
// property of the binding point and applies with buffer 0 too.
|
|
||||||
// * KHR-GL46.indirect_parameters_tests.MultiDraw{Arrays,Elements}IndirectCount - the three
|
|
||||||
// errors that guard a parameter-buffer draw.
|
|
||||||
// * KHR-GL43.compute_shader.api-indirect / .api-program.
|
|
||||||
// * KHR-GLxx.texture_storage.compressed_data - compressed formats on TEXTURE_3D.
|
|
||||||
// Plus the indexed-getter parity RC-7b is about: glGetBooleani_v / glGetInteger64i_v /
|
|
||||||
// glGetFloati_v / glGetDoublei_v must answer every pname glGetIntegeri_v answers.
|
|
||||||
//
|
|
||||||
// GPU-free: all of it is frontend validation.
|
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "Includes.h"
|
|
||||||
#include "Init.h"
|
|
||||||
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
|
||||||
#include <MG_Impl/GLImpl/Drawing/GL_Drawing.h>
|
|
||||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
|
||||||
#include <MG_Impl/GLImpl/Program/GL_Program.h>
|
|
||||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
|
||||||
#include <MG_Impl/GLImpl/Sampler/GL_Sampler.h>
|
|
||||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
|
||||||
#include <MG_Impl/GLImpl/VertexArray/GL_VertexArray.h>
|
|
||||||
#include <MG_State/GLState/Core.h>
|
|
||||||
|
|
||||||
using namespace MobileGL;
|
|
||||||
using namespace MobileGL::MG_Impl::GLImpl;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
class NegativeApiErrorsTest : public ::testing::Test {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
MobileGL::Initialize();
|
|
||||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
|
|
||||||
}
|
|
||||||
|
|
||||||
static void DrainErrors() {
|
|
||||||
for (int i = 0; i < 16 && GetError() != GL_NO_ERROR; ++i) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static GLuint MakeBuffer(GLenum target, GLsizeiptr size) {
|
|
||||||
GLuint buffer = 0;
|
|
||||||
GenBuffers(1, &buffer);
|
|
||||||
BindBuffer(target, buffer);
|
|
||||||
BufferData(target, size, nullptr, GL_STATIC_DRAW);
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One table row: run the call, assert exactly the expected error, leave nothing pending.
|
|
||||||
struct Row {
|
|
||||||
const char* what;
|
|
||||||
std::function<void()> call;
|
|
||||||
GLenum expected;
|
|
||||||
};
|
|
||||||
|
|
||||||
static void RunRows(const std::vector<Row>& rows) {
|
|
||||||
for (const Row& row : rows) {
|
|
||||||
DrainErrors();
|
|
||||||
row.call();
|
|
||||||
EXPECT_EQ(GetError(), row.expected) << row.what;
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TEST_F(NegativeApiErrorsTest, MultiBindRejectsNamesThatAreNotObjectsYet) {
|
|
||||||
const GLuint buffer = MakeBuffer(GL_UNIFORM_BUFFER, 1024);
|
|
||||||
// Reserved by glGenBuffers but never turned into an object: legal for glBindBuffer,
|
|
||||||
// which creates it, and illegal for glBindBuffersBase, which must not.
|
|
||||||
GLuint reservedOnly = 0;
|
|
||||||
GenBuffers(1, &reservedOnly);
|
|
||||||
ASSERT_NE(reservedOnly, 0u);
|
|
||||||
ASSERT_EQ(IsBuffer(reservedOnly), GL_FALSE);
|
|
||||||
|
|
||||||
// glGenSamplers, unlike glGenBuffers, creates the objects outright, so a sampler name is
|
|
||||||
// only "not an existing object" once it has been deleted.
|
|
||||||
GLuint deadSampler = 0;
|
|
||||||
GenSamplers(1, &deadSampler);
|
|
||||||
ASSERT_NE(deadSampler, 0u);
|
|
||||||
DeleteSamplers(1, &deadSampler);
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
const GLuint mixedBuffers[2] = {buffer, reservedOnly};
|
|
||||||
const GLuint samplers[1] = {deadSampler};
|
|
||||||
const GLintptr offsets[2] = {0, 0};
|
|
||||||
const GLsizeiptr sizes[2] = {256, 256};
|
|
||||||
|
|
||||||
RunRows({
|
|
||||||
{"glBindBuffersBase with a reserved-but-uncreated name",
|
|
||||||
[&] { BindBuffersBase(GL_UNIFORM_BUFFER, 0, 2, mixedBuffers); }, GL_INVALID_OPERATION},
|
|
||||||
{"glBindBuffersRange with a reserved-but-uncreated name",
|
|
||||||
[&] { BindBuffersRange(GL_UNIFORM_BUFFER, 0, 2, mixedBuffers, offsets, sizes); },
|
|
||||||
GL_INVALID_OPERATION},
|
|
||||||
{"glBindSamplers with a deleted sampler name", [&] { BindSamplers(0, 1, samplers); },
|
|
||||||
GL_INVALID_OPERATION},
|
|
||||||
});
|
|
||||||
|
|
||||||
// ARB_multi_bind defines these as a LOOP of single binds, so the bad entry costs its own
|
|
||||||
// binding point and the good one still binds - only the error is new.
|
|
||||||
GLint bound = -1;
|
|
||||||
GetIntegeri_v(GL_UNIFORM_BUFFER_BINDING, 0, &bound);
|
|
||||||
EXPECT_EQ(static_cast<GLuint>(bound), buffer) << "a rejected element must not take the valid ones with it";
|
|
||||||
GetIntegeri_v(GL_UNIFORM_BUFFER_BINDING, 1, &bound);
|
|
||||||
EXPECT_EQ(bound, 0) << "the rejected element must not have bound anything";
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(NegativeApiErrorsTest, BufferRangeOffsetAlignmentAppliesToTheBindingPoint) {
|
|
||||||
GLint ssboAlignment = 0;
|
|
||||||
GetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &ssboAlignment);
|
|
||||||
ASSERT_GT(ssboAlignment, 1) << "the alignment rule is untestable at alignment 1";
|
|
||||||
const GLuint atomicBuffer = MakeBuffer(GL_ATOMIC_COUNTER_BUFFER, 1024);
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
RunRows({
|
|
||||||
// buffer 0 detaches the binding point, but the target's alignment rule still holds.
|
|
||||||
{"glBindBufferRange(SHADER_STORAGE_BUFFER, buffer 0, misaligned offset)",
|
|
||||||
[&] { BindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, 0, ssboAlignment - 1, 0); }, GL_INVALID_VALUE},
|
|
||||||
// An atomic counter binding is addressed in 32-bit counters; it has no queryable
|
|
||||||
// alignment pname, which is how its rule went missing.
|
|
||||||
{"glBindBufferRange(ATOMIC_COUNTER_BUFFER, offset 3)",
|
|
||||||
[&] { BindBufferRange(GL_ATOMIC_COUNTER_BUFFER, 0, atomicBuffer, 3, 16); }, GL_INVALID_VALUE},
|
|
||||||
});
|
|
||||||
|
|
||||||
// ...and the aligned form still works.
|
|
||||||
DrainErrors();
|
|
||||||
BindBufferRange(GL_ATOMIC_COUNTER_BUFFER, 0, atomicBuffer, 4, 16);
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(NegativeApiErrorsTest, DispatchComputeIndirectChecksTheBoundBufferExtent) {
|
|
||||||
// Six uints: an indirect dispatch reads three, so offset 16 runs off the end.
|
|
||||||
const GLuint dispatchBuffer = MakeBuffer(GL_DISPATCH_INDIRECT_BUFFER, 6 * sizeof(GLuint));
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
RunRows({
|
|
||||||
{"glDispatchComputeIndirect(-2)", [] { DispatchComputeIndirect(-2); }, GL_INVALID_VALUE},
|
|
||||||
{"glDispatchComputeIndirect(3)", [] { DispatchComputeIndirect(3); }, GL_INVALID_VALUE},
|
|
||||||
{"glDispatchComputeIndirect(16) past the end of a 24-byte buffer",
|
|
||||||
[] { DispatchComputeIndirect(16); }, GL_INVALID_OPERATION},
|
|
||||||
{"glDispatchComputeIndirect(0) with nothing bound",
|
|
||||||
[&] {
|
|
||||||
BindBuffer(GL_DISPATCH_INDIRECT_BUFFER, 0);
|
|
||||||
DispatchComputeIndirect(0);
|
|
||||||
},
|
|
||||||
GL_INVALID_OPERATION},
|
|
||||||
});
|
|
||||||
static_cast<void>(dispatchBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(NegativeApiErrorsTest, IndirectParameterDrawsCheckBothBuffers) {
|
|
||||||
// Two DrawArraysIndirectCommands (16 bytes each) and a roomy parameter buffer.
|
|
||||||
MakeBuffer(GL_DRAW_INDIRECT_BUFFER, 2 * 4 * sizeof(GLuint));
|
|
||||||
const GLuint parameterBuffer = MakeBuffer(GL_PARAMETER_BUFFER, 200);
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
RunRows({
|
|
||||||
{"glMultiDrawArraysIndirectCount with drawcount 2 (not a multiple of four)",
|
|
||||||
[] { MultiDrawArraysIndirectCount(GL_TRIANGLE_STRIP, nullptr, 2, 1, 0); }, GL_INVALID_VALUE},
|
|
||||||
{"glMultiDrawArraysIndirectCount with maxdrawcount past the indirect buffer",
|
|
||||||
[] { MultiDrawArraysIndirectCount(GL_TRIANGLE_STRIP, nullptr, 0, 4, 0); }, GL_INVALID_OPERATION},
|
|
||||||
{"glMultiDrawElementsIndirectCount with drawcount 2",
|
|
||||||
[] { MultiDrawElementsIndirectCount(GL_TRIANGLE_STRIP, GL_UNSIGNED_BYTE, nullptr, 2, 1, 0); },
|
|
||||||
GL_INVALID_VALUE},
|
|
||||||
{"glMultiDrawArraysIndirectCount with no parameter buffer bound",
|
|
||||||
[&] {
|
|
||||||
BindBuffer(GL_PARAMETER_BUFFER, 0);
|
|
||||||
MultiDrawArraysIndirectCount(GL_TRIANGLE_STRIP, nullptr, 0, 2, 0);
|
|
||||||
},
|
|
||||||
GL_INVALID_OPERATION},
|
|
||||||
});
|
|
||||||
static_cast<void>(parameterBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(NegativeApiErrorsTest, TexStorage3DRejectsCompressedFormatsOnTexture3D) {
|
|
||||||
GLuint texture = 0;
|
|
||||||
GenTextures(1, &texture);
|
|
||||||
BindTexture(GL_TEXTURE_3D, texture);
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
RunRows({
|
|
||||||
{"glTexStorage3D(TEXTURE_3D, GL_COMPRESSED_RED_RGTC1)",
|
|
||||||
[] { TexStorage3D(GL_TEXTURE_3D, 1, 0x8DBB /* GL_COMPRESSED_RED_RGTC1 */, 8, 8, 8); },
|
|
||||||
GL_INVALID_OPERATION},
|
|
||||||
{"glTexStorage3D(TEXTURE_3D, GL_COMPRESSED_RG_RGTC2)",
|
|
||||||
[] { TexStorage3D(GL_TEXTURE_3D, 1, 0x8DBD /* GL_COMPRESSED_RG_RGTC2 */, 8, 8, 8); },
|
|
||||||
GL_INVALID_OPERATION},
|
|
||||||
});
|
|
||||||
|
|
||||||
// An uncompressed sized format on the same target still allocates.
|
|
||||||
DrainErrors();
|
|
||||||
TexStorage3D(GL_TEXTURE_3D, 1, GL_RGBA8, 8, 8, 8);
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(NegativeApiErrorsTest, LinkRejectsAComputeAndNonComputeMix) {
|
|
||||||
const auto attach = [](GLuint program, GLenum stage, const char* source) {
|
|
||||||
const GLuint shader = CreateShader(stage);
|
|
||||||
ShaderSource(shader, 1, &source, nullptr);
|
|
||||||
CompileShader(shader);
|
|
||||||
AttachShader(program, shader);
|
|
||||||
};
|
|
||||||
const GLuint program = CreateProgram();
|
|
||||||
attach(program, GL_COMPUTE_SHADER, R"(#version 430 core
|
|
||||||
layout(local_size_x = 1) in;
|
|
||||||
layout(std430) buffer Output { uint g_output[]; };
|
|
||||||
void main() { g_output[gl_GlobalInvocationID.x] = 0; }
|
|
||||||
)");
|
|
||||||
attach(program, GL_VERTEX_SHADER, R"(#version 430 core
|
|
||||||
layout(location = 0) in vec4 g_position;
|
|
||||||
void main() { gl_Position = g_position; }
|
|
||||||
)");
|
|
||||||
attach(program, GL_FRAGMENT_SHADER, R"(#version 430 core
|
|
||||||
layout(location = 0) out vec4 g_color;
|
|
||||||
void main() { g_color = vec4(1); }
|
|
||||||
)");
|
|
||||||
LinkProgram(program);
|
|
||||||
|
|
||||||
GLint status = GL_TRUE;
|
|
||||||
GetProgramiv(program, GL_LINK_STATUS, &status);
|
|
||||||
EXPECT_EQ(status, GL_FALSE) << "a compute shader must not link with any other stage";
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
// RC-7b: the four non-int indexed getters have to answer the same pname table glGetIntegeri_v
|
|
||||||
// does. glGetBooleani_v used to route everything through the indexed-capability path
|
|
||||||
// (GL_INVALID_ENUM for anything else) and glGetInteger64i_v straight to the driver, which
|
|
||||||
// does not have MobileGL's frontend-only values at all.
|
|
||||||
TEST_F(NegativeApiErrorsTest, IndexedGettersAgreeWithGetIntegeriv) {
|
|
||||||
DrainErrors();
|
|
||||||
const GLenum pnames[] = {GL_MAX_COMPUTE_WORK_GROUP_COUNT, GL_MAX_COMPUTE_WORK_GROUP_SIZE};
|
|
||||||
for (GLenum pname : pnames) {
|
|
||||||
for (GLuint index = 0; index < 3; ++index) {
|
|
||||||
GLint reference = -1;
|
|
||||||
GetIntegeri_v(pname, index, &reference);
|
|
||||||
ASSERT_EQ(GetError(), GL_NO_ERROR) << "glGetIntegeri_v(" << pname << ", " << index << ")";
|
|
||||||
ASSERT_GT(reference, 0) << "the reference value has to be non-trivial to compare against";
|
|
||||||
|
|
||||||
GLint64 as64 = -1;
|
|
||||||
GetInteger64i_v(pname, index, &as64);
|
|
||||||
EXPECT_EQ(as64, static_cast<GLint64>(reference)) << "glGetInteger64i_v(" << pname << ")";
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
|
|
||||||
GLfloat asFloat = -1.0f;
|
|
||||||
GetFloati_v(pname, index, &asFloat);
|
|
||||||
EXPECT_FLOAT_EQ(asFloat, static_cast<GLfloat>(reference)) << "glGetFloati_v(" << pname << ")";
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
|
|
||||||
GLdouble asDouble = -1.0;
|
|
||||||
GetDoublei_v(pname, index, &asDouble);
|
|
||||||
EXPECT_DOUBLE_EQ(asDouble, static_cast<GLdouble>(reference)) << "glGetDoublei_v(" << pname << ")";
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
|
|
||||||
GLboolean asBool = GL_FALSE;
|
|
||||||
GetBooleani_v(pname, index, &asBool);
|
|
||||||
EXPECT_EQ(asBool, GL_TRUE) << "glGetBooleani_v(" << pname << ")";
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...and the vertex-binding offset keeps its 64-bit width through glGetInteger64i_v, which is
|
|
||||||
// how KHR-GL4x.vertex_attrib_binding reads it.
|
|
||||||
TEST_F(NegativeApiErrorsTest, VertexBindingOffsetIsReadableThroughTheSixtyFourBitGetter) {
|
|
||||||
GLuint vao = 0;
|
|
||||||
GenVertexArrays(1, &vao);
|
|
||||||
BindVertexArray(vao);
|
|
||||||
const GLuint vbo = MakeBuffer(GL_ARRAY_BUFFER, 4096);
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
GLint64 offset = -1;
|
|
||||||
GetInteger64i_v(GL_VERTEX_BINDING_OFFSET, 0, &offset);
|
|
||||||
EXPECT_EQ(offset, 0);
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
|
|
||||||
BindVertexBuffer(0, vbo, 2048, 128);
|
|
||||||
GetInteger64i_v(GL_VERTEX_BINDING_OFFSET, 0, &offset);
|
|
||||||
EXPECT_EQ(offset, 2048);
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Test/State/RenderStateTest.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
|
|
||||||
//
|
|
||||||
// Indexed capability state (glEnablei/glDisablei/glIsEnabledi) exists only for GL_BLEND in this
|
|
||||||
// stack. Every other capability must come back as GL_INVALID_ENUM per GL 4.6 sec. 17.3.3 - and,
|
|
||||||
// far more importantly, must come back at all: RenderState::SetCapabilityIndexed and
|
|
||||||
// IsCapabilityEnabledIndexed used to answer a non-blend capability with THROW_UNIMPL_EXCEPTION,
|
|
||||||
// which unwinds a C++ exception through the C GL ABI and terminates the process.
|
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
|
||||||
|
|
||||||
#include "Includes.h"
|
|
||||||
#include "Init.h"
|
|
||||||
|
|
||||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
|
||||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
|
||||||
#include <MG_State/GLState/Core.h>
|
|
||||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
|
||||||
|
|
||||||
using namespace MobileGL;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
class RenderStateTest: public ::testing::Test {
|
|
||||||
protected:
|
|
||||||
// GL error flags are sticky per code and the context outlives an individual test in this
|
|
||||||
// binary, so a pending error from an earlier case would be handed to the next GetError().
|
|
||||||
static void DrainPendingGlErrors() {
|
|
||||||
for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void ExpectSingleGlError(GLenum expected) {
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), expected);
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the call recorded more than one error";
|
|
||||||
}
|
|
||||||
|
|
||||||
void SetUp() override {
|
|
||||||
MobileGL::Initialize();
|
|
||||||
DrainPendingGlErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST_F(RenderStateTest, IndexedCapabilityTogglesRejectNonBlendCapabilities) {
|
|
||||||
// GL_CLIP_DISTANCE0 is a real capability, just not an indexed one - the shape an application or
|
|
||||||
// a CTS negative test would hit.
|
|
||||||
for (const GLenum cap : {GL_CLIP_DISTANCE0, GL_DEPTH_TEST, GL_SCISSOR_TEST}) {
|
|
||||||
MG_Impl::GLImpl::Enablei(cap, 0);
|
|
||||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
|
||||||
|
|
||||||
MG_Impl::GLImpl::Disablei(cap, 0);
|
|
||||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
|
||||||
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(cap, 0), GL_FALSE);
|
|
||||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RenderStateTest, IndexedCapabilityTogglesRejectAnOutOfRangeBufferIndex) {
|
|
||||||
const GLuint outOfRange = MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS;
|
|
||||||
|
|
||||||
MG_Impl::GLImpl::Enablei(GL_BLEND, outOfRange);
|
|
||||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
|
||||||
|
|
||||||
MG_Impl::GLImpl::Disablei(GL_BLEND, outOfRange);
|
|
||||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
|
||||||
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_BLEND, outOfRange), GL_FALSE);
|
|
||||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RenderStateTest, IndexedBlendTogglesStillWork) {
|
|
||||||
// The rejection path must not have cost the one capability that is genuinely indexed.
|
|
||||||
MG_Impl::GLImpl::Enablei(GL_BLEND, 1);
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_BLEND, 1), GL_TRUE);
|
|
||||||
|
|
||||||
MG_Impl::GLImpl::Disablei(GL_BLEND, 1);
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_BLEND, 1), GL_FALSE);
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
@@ -3176,115 +3176,3 @@ TEST_F(TextureTest, WidenedRenderTargetUploadExpandsThreeChannelDataWithOpaqueAl
|
|||||||
EXPECT_EQ(PrepareChannelWidenedUpload(3, texelSize, nullptr, 0, GL_FLOAT, widened), nullptr);
|
EXPECT_EQ(PrepareChannelWidenedUpload(3, texelSize, nullptr, 0, GL_FLOAT, widened), nullptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
|
||||||
// A GL entry point may return an error, but it may never throw through the C GL ABI: unwinding a
|
|
||||||
// C++ exception across it terminates the process. These cover the sites that used to do exactly
|
|
||||||
// that (KHR-GL30.api.coverage died on the first of them on both backends).
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
struct CopyTexImage2DCall {
|
|
||||||
Bool Called = false;
|
|
||||||
GLenum Target = 0;
|
|
||||||
GLint Level = 0;
|
|
||||||
GLenum InternalFormat = 0;
|
|
||||||
GLsizei Width = 0;
|
|
||||||
GLsizei Height = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
CopyTexImage2DCall g_copyTexImage2DCall;
|
|
||||||
|
|
||||||
void RecordCopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint, GLint, GLsizei width,
|
|
||||||
GLsizei height, GLint) {
|
|
||||||
g_copyTexImage2DCall = {true, target, level, internalformat, width, height};
|
|
||||||
}
|
|
||||||
|
|
||||||
// A colour read framebuffer of the requested sized format, bound to GL_READ_FRAMEBUFFER, which
|
|
||||||
// is what glCopyTexImage2D takes its source base format from.
|
|
||||||
void BindReadFramebufferWithColorFormat(GLenum sizedInternalFormat) {
|
|
||||||
GLuint framebuffer = 0;
|
|
||||||
GLuint texture = 0;
|
|
||||||
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
|
|
||||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
|
||||||
MG_Impl::GLImpl::TextureStorage2D(texture, 1, sizedInternalFormat, 16, 16);
|
|
||||||
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0);
|
|
||||||
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint BindFreshMutableTexture2D() {
|
|
||||||
GLuint texture = 0;
|
|
||||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
|
||||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
|
||||||
return texture;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST_F(TextureTest, CopyTexImage2DAcceptsEveryComponentSubsetOfTheReadBuffer) {
|
|
||||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.CopyTexImage2D = RecordCopyTexImage2D;
|
|
||||||
|
|
||||||
BindReadFramebufferWithColorFormat(GL_RGBA8);
|
|
||||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "read framebuffer setup itself failed";
|
|
||||||
|
|
||||||
// GL 4.6 sec. 8.6: internalformat may name a SUBSET of the read buffer's components. This is
|
|
||||||
// exactly the list KHR-GL30.api.coverage walks against an rgba8888 colour buffer, and it is
|
|
||||||
// also what an ordinary GL app does with glCopyTexImage2D(GL_RGB) from an RGBA8 framebuffer.
|
|
||||||
for (const GLenum internalFormat : {GL_RED, GL_RG, GL_RGB, GL_RGBA}) {
|
|
||||||
BindFreshMutableTexture2D();
|
|
||||||
g_copyTexImage2DCall = {};
|
|
||||||
|
|
||||||
MG_Impl::GLImpl::CopyTexImage2D(GL_TEXTURE_2D, 0, internalFormat, 0, 0, 1, 1, 0);
|
|
||||||
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "internalformat " << internalFormat;
|
|
||||||
EXPECT_TRUE(g_copyTexImage2DCall.Called) << "internalformat " << internalFormat;
|
|
||||||
EXPECT_EQ(g_copyTexImage2DCall.InternalFormat, internalFormat);
|
|
||||||
EXPECT_EQ(g_copyTexImage2DCall.Width, 1);
|
|
||||||
EXPECT_EQ(g_copyTexImage2DCall.Height, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(TextureTest, CopyTexImage2DRejectsAFormatTheReadBufferCannotSupply) {
|
|
||||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.CopyTexImage2D = RecordCopyTexImage2D;
|
|
||||||
|
|
||||||
BindReadFramebufferWithColorFormat(GL_R8);
|
|
||||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "read framebuffer setup itself failed";
|
|
||||||
|
|
||||||
BindFreshMutableTexture2D();
|
|
||||||
g_copyTexImage2DCall = {};
|
|
||||||
|
|
||||||
// The subset rule still has a wrong side: GL_RGBA asks for components a GL_R8 read buffer does
|
|
||||||
// not have. That must be GL_INVALID_OPERATION and nothing else - not a throw, not silence.
|
|
||||||
MG_Impl::GLImpl::CopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 1, 1, 0);
|
|
||||||
|
|
||||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
|
||||||
EXPECT_FALSE(g_copyTexImage2DCall.Called) << "a rejected copy must not reach the backend";
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(TextureTest, CopyTexImage1DReportsUnsupportedInsteadOfTerminating) {
|
|
||||||
// 1D textures have no upload path in this stack; the entry point used to throw unconditionally.
|
|
||||||
MG_Impl::GLImpl::CopyTexImage1D(GL_TEXTURE_1D, 0, GL_RGBA, 0, 0, 1, 0);
|
|
||||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(TextureTest, GetTexLevelParameterOnBufferStorageReportsErrorInsteadOfTerminating) {
|
|
||||||
// TextureStorageType is {Mipmap, Buffer} and the level queries only answer out of a mipmap
|
|
||||||
// chain, so every glGetTexLevelParameter* on a GL_TEXTURE_BUFFER texture reached a
|
|
||||||
// THROW_UNIMPL_EXCEPTION default: label and killed the process.
|
|
||||||
GLuint texture = 0;
|
|
||||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_BUFFER, 1, &texture);
|
|
||||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_BUFFER, texture);
|
|
||||||
MG_Impl::GLImpl::TexBuffer(GL_TEXTURE_BUFFER, GL_R8, 0);
|
|
||||||
DrainPendingGlErrors();
|
|
||||||
|
|
||||||
for (const GLenum pname : {GL_TEXTURE_WIDTH, GL_TEXTURE_HEIGHT, GL_TEXTURE_DEPTH}) {
|
|
||||||
GLint intParam = 0x20202020;
|
|
||||||
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_BUFFER, 0, pname, &intParam);
|
|
||||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
|
||||||
|
|
||||||
GLfloat floatParam = 12345.0f;
|
|
||||||
MG_Impl::GLImpl::GetTexLevelParameterfv(GL_TEXTURE_BUFFER, 0, pname, &floatParam);
|
|
||||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,368 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/Util/AsyncPoolBench.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
|
||||||
|
|
||||||
|
// A head-to-head harness for the two ShaderCompilePool execution engines
|
||||||
|
// (MOBILEGL_ASYNC_POOL=asio|libfork). Not a gtest: it measures one wall-clock interval per
|
||||||
|
// process, because most of what it drives is memoized per process (the shader preprocess
|
||||||
|
// cache and the compile-adoption map both live for the life of the GL context), so a second
|
||||||
|
// timed repetition inside one process would measure the cache, not the compiler. The driver
|
||||||
|
// script re-executes the binary for every repetition instead.
|
||||||
|
//
|
||||||
|
// Two modes:
|
||||||
|
//
|
||||||
|
// corpus - the REAL frontend path. glCreateShader/glShaderSource are done untimed, then
|
||||||
|
// the clock starts and glCompileShader/glLinkProgram submit every job, and stops
|
||||||
|
// once glGetProgramiv(GL_LINK_STATUS) has joined all of them. That is exactly the
|
||||||
|
// first-submit-to-all-joined interval a shaderpack load pays.
|
||||||
|
//
|
||||||
|
// micro - N trivial JobNodes straight through ShaderCompilePool::Post, isolating the
|
||||||
|
// executor's own dispatch overhead from any workload contention.
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include "Init.h"
|
||||||
|
#include <Config.h>
|
||||||
|
|
||||||
|
#include <MG_Impl/GLImpl/Program/GL_Program.h>
|
||||||
|
#include <MG_Util/Async/JobNode.h>
|
||||||
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using namespace MobileGL::MG_Util::Async;
|
||||||
|
namespace GLImpl = MobileGL::MG_Impl::GLImpl;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
using Clock = std::chrono::steady_clock;
|
||||||
|
|
||||||
|
double MillisSince(const Clock::time_point start) {
|
||||||
|
return std::chrono::duration<double, std::milli>(Clock::now() - start).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
GLenum StageFromExtension(const std::string& ext) {
|
||||||
|
if (ext == ".vert") return GL_VERTEX_SHADER;
|
||||||
|
if (ext == ".frag") return GL_FRAGMENT_SHADER;
|
||||||
|
if (ext == ".geom") return GL_GEOMETRY_SHADER;
|
||||||
|
if (ext == ".comp") return GL_COMPUTE_SHADER;
|
||||||
|
if (ext == ".tesc") return GL_TESS_CONTROL_SHADER;
|
||||||
|
if (ext == ".tese") return GL_TESS_EVALUATION_SHADER;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ReadFile(const fs::path& path) {
|
||||||
|
std::ifstream in(path, std::ios::binary);
|
||||||
|
std::ostringstream buf;
|
||||||
|
buf << in.rdbuf();
|
||||||
|
return buf.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CorpusShader {
|
||||||
|
std::string name;
|
||||||
|
std::string source;
|
||||||
|
GLenum stage = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// One program's worth of the corpus: the trace's link group. Shaders are indices into
|
||||||
|
// the flat shader list, because a source shared by several programs must stay ONE entry
|
||||||
|
// - that sharing is what the compile-adoption map sees in the real path too.
|
||||||
|
struct CorpusProgram {
|
||||||
|
std::vector<SizeT> shaders;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Corpus {
|
||||||
|
std::vector<CorpusShader> shaders;
|
||||||
|
std::vector<CorpusProgram> programs;
|
||||||
|
SizeT totalBytes = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reads a corpus directory written by extract_corpus.py: one file per compiled shader,
|
||||||
|
// stage in the extension, plus manifest.txt naming the trace's link groups.
|
||||||
|
Corpus LoadCorpus(const fs::path& dir) {
|
||||||
|
Corpus corpus;
|
||||||
|
std::unordered_map<std::string, SizeT> byName;
|
||||||
|
|
||||||
|
const auto intern = [&](const std::string& name) -> SizeT {
|
||||||
|
if (const auto it = byName.find(name); it != byName.end()) return it->second;
|
||||||
|
const fs::path path = dir / name;
|
||||||
|
if (!fs::exists(path)) return static_cast<SizeT>(-1);
|
||||||
|
CorpusShader shader;
|
||||||
|
shader.name = name;
|
||||||
|
shader.source = ReadFile(path);
|
||||||
|
shader.stage = StageFromExtension(path.extension().string());
|
||||||
|
if (shader.stage == 0) return static_cast<SizeT>(-1);
|
||||||
|
corpus.totalBytes += shader.source.size();
|
||||||
|
corpus.shaders.push_back(Move(shader));
|
||||||
|
const SizeT index = corpus.shaders.size() - 1;
|
||||||
|
byName.emplace(name, index);
|
||||||
|
return index;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fs::path manifest = dir / "manifest.txt";
|
||||||
|
if (fs::exists(manifest)) {
|
||||||
|
std::ifstream in(manifest);
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(in, line)) {
|
||||||
|
if (line.empty() || line[0] == '#') continue;
|
||||||
|
CorpusProgram program;
|
||||||
|
std::istringstream fields(line);
|
||||||
|
std::string name;
|
||||||
|
while (fields >> name) {
|
||||||
|
const SizeT index = intern(name);
|
||||||
|
if (index != static_cast<SizeT>(-1)) program.shaders.push_back(index);
|
||||||
|
}
|
||||||
|
if (!program.shaders.empty()) corpus.programs.push_back(Move(program));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anything in the directory the manifest never linked still gets compiled, as a
|
||||||
|
// program-less group, so the corpus on disk and the corpus measured are the same set.
|
||||||
|
std::vector<fs::path> leftovers;
|
||||||
|
for (const auto& entry : fs::directory_iterator(dir)) {
|
||||||
|
if (!entry.is_regular_file()) continue;
|
||||||
|
const std::string name = entry.path().filename().string();
|
||||||
|
if (name == "manifest.txt") continue;
|
||||||
|
if (StageFromExtension(entry.path().extension().string()) == 0) continue;
|
||||||
|
if (byName.count(name) != 0) continue;
|
||||||
|
leftovers.push_back(entry.path());
|
||||||
|
}
|
||||||
|
std::sort(leftovers.begin(), leftovers.end());
|
||||||
|
for (const auto& path : leftovers) intern(path.filename().string());
|
||||||
|
|
||||||
|
return corpus;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CorpusResult {
|
||||||
|
double submitMs = 0; // first glCompileShader -> last glLinkProgram returned
|
||||||
|
double joinMs = 0; // last submit -> every program joined
|
||||||
|
double totalMs = 0; // the number that matters: first submit -> all joined
|
||||||
|
SizeT linkFailures = 0;
|
||||||
|
SizeT compileFailures = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
CorpusResult RunCorpus(const Corpus& corpus) {
|
||||||
|
// ---- Untimed: create every GL object and stage every source ----------------------
|
||||||
|
// glShaderSource is a memcpy into the shader object and glAttachShader is a pointer
|
||||||
|
// append; neither touches the pool. Keeping them outside the clock makes the measured
|
||||||
|
// interval exactly the compile+link critical path, which is what an application's
|
||||||
|
// loading screen waits on.
|
||||||
|
std::vector<GLuint> shaderNames(corpus.shaders.size(), 0);
|
||||||
|
for (SizeT i = 0; i < corpus.shaders.size(); ++i) {
|
||||||
|
const CorpusShader& shader = corpus.shaders[i];
|
||||||
|
const GLuint name = GLImpl::CreateShader(shader.stage);
|
||||||
|
const GLchar* text = shader.source.c_str();
|
||||||
|
const GLint length = static_cast<GLint>(shader.source.size());
|
||||||
|
GLImpl::ShaderSource(name, 1, &text, &length);
|
||||||
|
shaderNames[i] = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<GLuint> programNames(corpus.programs.size(), 0);
|
||||||
|
for (SizeT p = 0; p < corpus.programs.size(); ++p) {
|
||||||
|
const GLuint program = GLImpl::CreateProgram();
|
||||||
|
for (const SizeT shaderIndex : corpus.programs[p].shaders) {
|
||||||
|
GLImpl::AttachShader(program, shaderNames[shaderIndex]);
|
||||||
|
}
|
||||||
|
programNames[p] = program;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Timed ------------------------------------------------------------------------
|
||||||
|
const Clock::time_point start = Clock::now();
|
||||||
|
|
||||||
|
// Submission order follows the trace: a program's shaders, then its link. That order
|
||||||
|
// is what exercises ProgramLinkTask::SubmitAfter's dependency chaining rather than a
|
||||||
|
// flat burst of independent compiles.
|
||||||
|
std::vector<Bool> submitted(corpus.shaders.size(), false);
|
||||||
|
for (SizeT p = 0; p < corpus.programs.size(); ++p) {
|
||||||
|
for (const SizeT shaderIndex : corpus.programs[p].shaders) {
|
||||||
|
if (submitted[shaderIndex]) continue;
|
||||||
|
submitted[shaderIndex] = true;
|
||||||
|
GLImpl::CompileShader(shaderNames[shaderIndex]);
|
||||||
|
}
|
||||||
|
GLImpl::LinkProgram(programNames[p]);
|
||||||
|
}
|
||||||
|
for (SizeT i = 0; i < corpus.shaders.size(); ++i) {
|
||||||
|
if (submitted[i]) continue;
|
||||||
|
submitted[i] = true;
|
||||||
|
GLImpl::CompileShader(shaderNames[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Clock::time_point submitted_at = Clock::now();
|
||||||
|
|
||||||
|
CorpusResult result;
|
||||||
|
// GL_LINK_STATUS is a joining query (GL_COMPLETION_STATUS_KHR is the one that must
|
||||||
|
// not join), so this loop is the all-joined barrier.
|
||||||
|
for (const GLuint program : programNames) {
|
||||||
|
GLint status = 0;
|
||||||
|
GLImpl::GetProgramiv(program, GL_LINK_STATUS, &status);
|
||||||
|
if (status == GL_FALSE) ++result.linkFailures;
|
||||||
|
}
|
||||||
|
for (const GLuint shader : shaderNames) {
|
||||||
|
GLint status = 0;
|
||||||
|
GLImpl::GetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||||
|
if (status == GL_FALSE) ++result.compileFailures;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.totalMs = MillisSince(start);
|
||||||
|
result.submitMs = std::chrono::duration<double, std::milli>(submitted_at - start).count();
|
||||||
|
result.joinMs = result.totalMs - result.submitMs;
|
||||||
|
|
||||||
|
for (const GLuint program : programNames) GLImpl::DeleteProgram(program);
|
||||||
|
for (const GLuint shader : shaderNames) GLImpl::DeleteShader(shader);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Executor microbenchmark ----------------------------------------------------------
|
||||||
|
// The body is deliberately near-empty: what is being measured is Post -> engine ->
|
||||||
|
// RunOnWorker -> next dispatch, i.e. the executor's own cost per job, with no compiler
|
||||||
|
// work to hide it.
|
||||||
|
//
|
||||||
|
// The barrier is an all-jobs-ran latch, and it has to be. This bench used to stop the
|
||||||
|
// clock at StopAndDrain(), which is not a "wait for everything" - it is the teardown path,
|
||||||
|
// and its contract is to ABANDON whatever the budget has not dispatched yet (see
|
||||||
|
// ShaderCompilePool::StopAndDrain, and the JobNodeTest case that pins exactly that). With
|
||||||
|
// 100k jobs behind a budget of N, most of them were therefore cancelled rather than run,
|
||||||
|
// and the fraction that survived was decided by how fast the engine drained the queue
|
||||||
|
// relative to the posting loop - i.e. by the very quantity under test. Measured on this
|
||||||
|
// machine at 8 workers: Asio ran 75,906 of 100,000 and libfork 99,998, and both were
|
||||||
|
// scored as if they had run 100,000. The reported "libfork is 1.36x faster" was libfork
|
||||||
|
// being charged for 32% more work than Asio.
|
||||||
|
class TrivialJob final : public JobNode {
|
||||||
|
public:
|
||||||
|
TrivialJob(std::atomic<Uint64>* sink, const Uint64 total, std::mutex* mutex,
|
||||||
|
std::condition_variable* cv)
|
||||||
|
: m_sink(sink), m_total(total), m_mutex(mutex), m_cv(cv) {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void RunBody() override {
|
||||||
|
if (m_sink->fetch_add(1, std::memory_order_acq_rel) + 1 == m_total) {
|
||||||
|
// The last job wakes the timer. Under the lock, so the waiter cannot miss it
|
||||||
|
// between its predicate check and its wait.
|
||||||
|
const std::lock_guard<std::mutex> lock(*m_mutex);
|
||||||
|
m_cv->notify_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::atomic<Uint64>* m_sink;
|
||||||
|
Uint64 m_total;
|
||||||
|
std::mutex* m_mutex;
|
||||||
|
std::condition_variable* m_cv;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MicroResult {
|
||||||
|
double ms = 0;
|
||||||
|
Uint64 ran = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
MicroResult RunMicrobench(const Uint threads, const SizeT jobs) {
|
||||||
|
ShaderCompilePool pool(threads);
|
||||||
|
std::atomic<Uint64> counter{0};
|
||||||
|
std::mutex mutex;
|
||||||
|
std::condition_variable cv;
|
||||||
|
const auto total = static_cast<Uint64>(jobs);
|
||||||
|
|
||||||
|
// Nodes are allocated up front: MakeShared is not what is under test, and leaving it
|
||||||
|
// inside the loop would put an allocator on the critical path in front of the
|
||||||
|
// dispatch path this is meant to isolate.
|
||||||
|
std::vector<SharedPtr<JobNode>> nodes;
|
||||||
|
nodes.reserve(jobs);
|
||||||
|
for (SizeT i = 0; i < jobs; ++i) {
|
||||||
|
nodes.push_back(MakeShared<TrivialJob>(&counter, total, &mutex, &cv));
|
||||||
|
}
|
||||||
|
|
||||||
|
const Clock::time_point start = Clock::now();
|
||||||
|
for (auto& node : nodes) pool.Post(Move(node));
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mutex);
|
||||||
|
cv.wait(lock, [&] { return counter.load(std::memory_order_acquire) >= total; });
|
||||||
|
}
|
||||||
|
const double ms = MillisSince(start);
|
||||||
|
|
||||||
|
MicroResult result;
|
||||||
|
result.ms = ms;
|
||||||
|
result.ran = counter.load(std::memory_order_acquire);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[noreturn]] void Usage() {
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"usage: AsyncPoolBench --corpus DIR\n"
|
||||||
|
" AsyncPoolBench --micro JOBS --threads N\n"
|
||||||
|
"env: MOBILEGL_ASYNC_POOL=asio|libfork, "
|
||||||
|
"MOBILEGL_ASYNC_SHADER_COMPILE_THREADS=N\n");
|
||||||
|
std::exit(2);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
std::string corpusDir;
|
||||||
|
SizeT microJobs = 0;
|
||||||
|
Uint microThreads = 0;
|
||||||
|
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
const std::string arg = argv[i];
|
||||||
|
const auto next = [&]() -> std::string {
|
||||||
|
if (i + 1 >= argc) Usage();
|
||||||
|
return argv[++i];
|
||||||
|
};
|
||||||
|
if (arg == "--corpus") corpusDir = next();
|
||||||
|
else if (arg == "--micro") microJobs = static_cast<SizeT>(std::stoull(next()));
|
||||||
|
else if (arg == "--threads") microThreads = static_cast<Uint>(std::stoul(next()));
|
||||||
|
else Usage();
|
||||||
|
}
|
||||||
|
if (corpusDir.empty() && microJobs == 0) Usage();
|
||||||
|
|
||||||
|
Initialize();
|
||||||
|
|
||||||
|
const AsyncPoolEngine engine = DetectAsyncPoolEngine();
|
||||||
|
const char* engineName = AsyncPoolEngineName(engine);
|
||||||
|
|
||||||
|
if (microJobs != 0) {
|
||||||
|
const Uint threads = microThreads != 0 ? microThreads : DetectShaderCompileThreadCount();
|
||||||
|
const MicroResult result = RunMicrobench(threads, microJobs);
|
||||||
|
// `ran` is printed, not just checked, so that a run in which the arms did different
|
||||||
|
// amounts of work is visible in the results file rather than on a stderr the driver
|
||||||
|
// script redirects to /dev/null. ns_per_job divides by what actually ran.
|
||||||
|
std::printf("RESULT mode=micro engine=%s threads=%u jobs=%zu ran=%llu total_ms=%.3f "
|
||||||
|
"ns_per_job=%.1f\n",
|
||||||
|
engineName, threads, microJobs, static_cast<unsigned long long>(result.ran),
|
||||||
|
result.ms, result.ms * 1e6 / static_cast<double>(result.ran));
|
||||||
|
return result.ran == microJobs ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Corpus corpus = LoadCorpus(corpusDir);
|
||||||
|
if (corpus.shaders.empty()) {
|
||||||
|
std::fprintf(stderr, "AsyncPoolBench: no shaders found in %s\n", corpusDir.c_str());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AsyncShaderCompileActive()) {
|
||||||
|
std::fprintf(stderr, "AsyncPoolBench: asynchronous compilation is OFF; measuring the "
|
||||||
|
"inline path\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
const CorpusResult result = RunCorpus(corpus);
|
||||||
|
const Uint threads = ShaderCompilePool::Get().GetThreadCount();
|
||||||
|
|
||||||
|
std::printf("RESULT mode=corpus engine=%s threads=%u corpus=%s shaders=%zu programs=%zu "
|
||||||
|
"bytes=%zu total_ms=%.3f submit_ms=%.3f join_ms=%.3f link_fail=%zu "
|
||||||
|
"compile_fail=%zu\n",
|
||||||
|
engineName, threads, corpusDir.c_str(), corpus.shaders.size(),
|
||||||
|
corpus.programs.size(), corpus.totalBytes, result.totalMs, result.submitMs,
|
||||||
|
result.joinMs, result.linkFailures, result.compileFailures);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -10,11 +10,35 @@ target_include_directories(JobNodeTest PRIVATE
|
|||||||
${MGL_ROOT}/MobileGL
|
${MGL_ROOT}/MobileGL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# GTest::gtest, not GTest::gtest_main: JobNodeTest supplies its own main so that
|
||||||
|
# MOBILEGL_LOG_FILE_PATH is set before the first log write in the process. The engine
|
||||||
|
# -selection cases read the log back to assert that an unrecognized MOBILEGL_ASYNC_POOL value
|
||||||
|
# warns, and the desktop log sink is the file (MOBILEGL_LOG_ENABLE_CONSOLE is 0).
|
||||||
target_link_libraries(
|
target_link_libraries(
|
||||||
JobNodeTest PRIVATE
|
JobNodeTest PRIVATE
|
||||||
GTest::gtest_main
|
GTest::gtest
|
||||||
${LINK_LIBRARIES}
|
${LINK_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
include(GoogleTest)
|
include(GoogleTest)
|
||||||
gtest_discover_tests(JobNodeTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(JobNodeTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
|
||||||
|
# The engine comparison harness. Deliberately NOT registered with add_test: it measures wall
|
||||||
|
# time, so it has no pass/fail verdict to give CI, and it is driven by a script that varies
|
||||||
|
# MOBILEGL_ASYNC_POOL and MOBILEGL_ASYNC_SHADER_COMPILE_THREADS across a matrix. It lives
|
||||||
|
# beside JobNodeTest because it drives the same pool through the same two engines; it links
|
||||||
|
# MobileGL_s for the real glCompileShader/glLinkProgram frontend path.
|
||||||
|
add_executable(
|
||||||
|
AsyncPoolBench
|
||||||
|
AsyncPoolBench.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(AsyncPoolBench PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
AsyncPoolBench PRIVATE
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|||||||
@@ -9,8 +9,19 @@
|
|||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <process.h>
|
||||||
|
#define MGL_TEST_GETPID _getpid
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#define MGL_TEST_GETPID getpid
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "Includes.h"
|
#include "Includes.h"
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
|
|
||||||
@@ -21,6 +32,29 @@ using namespace MobileGL;
|
|||||||
using namespace MobileGL::MG_Util::Async;
|
using namespace MobileGL::MG_Util::Async;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
// Where this binary's MobileGL log lands, set by main() below. The engine-selection cases
|
||||||
|
// read it back: MobileGL's desktop log sink is the FILE, not the console
|
||||||
|
// (MOBILEGL_LOG_ENABLE_CONSOLE is 0 in Defines.h), so gtest's stdout capture would see
|
||||||
|
// nothing, and "unrecognized value warns" is a contract worth pinning rather than
|
||||||
|
// assuming - a silent fallback makes a misspelt engine name look exactly like an unset
|
||||||
|
// variable.
|
||||||
|
String g_logFilePath;
|
||||||
|
|
||||||
|
// Log.cpp flushes the file after every line, so everything written before this call is
|
||||||
|
// already visible.
|
||||||
|
String ReadLogFrom(const std::streamoff offset) {
|
||||||
|
std::ifstream file(g_logFilePath, std::ios::binary);
|
||||||
|
if (!file) return {};
|
||||||
|
file.seekg(offset);
|
||||||
|
return String((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::streamoff LogSize() {
|
||||||
|
std::error_code error;
|
||||||
|
const auto size = std::filesystem::file_size(g_logFilePath, error);
|
||||||
|
return error ? 0 : static_cast<std::streamoff>(size);
|
||||||
|
}
|
||||||
|
|
||||||
// Every test drives its own pool instance rather than ShaderCompilePool::Get(): the
|
// Every test drives its own pool instance rather than ShaderCompilePool::Get(): the
|
||||||
// process-wide pool is stopped permanently by StopAndDrain (that is the teardown
|
// process-wide pool is stopped permanently by StopAndDrain (that is the teardown
|
||||||
// contract), so a test that drained the singleton would poison every test after it.
|
// contract), so a test that drained the singleton would poison every test after it.
|
||||||
@@ -73,6 +107,21 @@ namespace {
|
|||||||
Bool m_open = false;
|
Bool m_open = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Live thread count of this process. Linux only - /proc/self/task has one entry per
|
||||||
|
// thread - and 0 where that is not available, which is how the one case that uses it
|
||||||
|
// decides to skip rather than to assert something it cannot see.
|
||||||
|
SizeT LiveThreadCount() {
|
||||||
|
#ifdef __linux__
|
||||||
|
std::error_code error;
|
||||||
|
const auto count = static_cast<SizeT>(
|
||||||
|
std::distance(std::filesystem::directory_iterator("/proc/self/task", error),
|
||||||
|
std::filesystem::directory_iterator()));
|
||||||
|
return error ? 0 : count;
|
||||||
|
#else
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
Bool WaitUntil(const std::function<Bool()>& predicate,
|
Bool WaitUntil(const std::function<Bool()>& predicate,
|
||||||
const std::chrono::milliseconds timeout = std::chrono::seconds(10)) {
|
const std::chrono::milliseconds timeout = std::chrono::seconds(10)) {
|
||||||
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||||
@@ -89,11 +138,33 @@ namespace {
|
|||||||
// ---------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------
|
||||||
|
|
||||||
TEST(ShaderCompilePoolLifecycle, ConstructingAPoolStartsNoThreadUntilSomethingIsPosted) {
|
TEST(ShaderCompilePoolLifecycle, ConstructingAPoolStartsNoThreadUntilSomethingIsPosted) {
|
||||||
|
const SizeT before = LiveThreadCount();
|
||||||
|
|
||||||
ShaderCompilePool pool(kTestThreads);
|
ShaderCompilePool pool(kTestThreads);
|
||||||
EXPECT_EQ(pool.GetThreadCount(), kTestThreads);
|
EXPECT_EQ(pool.GetThreadCount(), kTestThreads);
|
||||||
EXPECT_EQ(pool.GetMaxConcurrency(), kTestThreads);
|
EXPECT_EQ(pool.GetMaxConcurrency(), kTestThreads);
|
||||||
// Nothing observable to assert about thread creation from here; what this pins is that
|
|
||||||
// construction is side-effect free and the pool destructs cleanly without ever running.
|
if (before == 0) {
|
||||||
|
// No thread census on this platform. The rest still holds: construction is
|
||||||
|
// side-effect free and the pool destructs cleanly without ever having run.
|
||||||
|
SUCCEED();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "A build that never posts pays nothing" is a real requirement, not a stylistic one -
|
||||||
|
// asynchronous compilation can be switched off entirely, and a switched-off pool that
|
||||||
|
// still spawned its workers would cost every such process its threads and their stacks.
|
||||||
|
// Worth asserting rather than asserting-by-comment now that an engine's thread shape is
|
||||||
|
// selectable: the libfork engine starts its workers AND a dispatch thread of its own, so
|
||||||
|
// a regression here would cost more than it used to.
|
||||||
|
EXPECT_EQ(LiveThreadCount(), before) << "constructing a pool started " << (LiveThreadCount() - before)
|
||||||
|
<< " thread(s) before anything was posted";
|
||||||
|
|
||||||
|
auto job = MakeShared<TestJob>();
|
||||||
|
pool.Post(job);
|
||||||
|
job->Wait();
|
||||||
|
EXPECT_GT(LiveThreadCount(), before) << "the first Post started no thread at all, so the engine did not "
|
||||||
|
"really run the job off the calling thread";
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(ShaderCompilePoolLifecycle, StopAndDrainIsIdempotentAndSafeOnAnUnusedPool) {
|
TEST(ShaderCompilePoolLifecycle, StopAndDrainIsIdempotentAndSafeOnAnUnusedPool) {
|
||||||
@@ -217,6 +288,94 @@ TEST(JobNodeSubmit, ManyJobsAllComplete) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(JobNodeSubmit, AJobBodyMayPostAnotherJobToTheSamePool) {
|
||||||
|
// The ProgramLinkTask::SubmitAfter shape, reduced to its scheduling core: the dependent is
|
||||||
|
// posted by whichever thread drove the dependency terminal, which for a job that finished
|
||||||
|
// on a worker is that WORKER. Every engine therefore has to accept a submission from
|
||||||
|
// inside its own pool.
|
||||||
|
//
|
||||||
|
// Not a hypothetical: libfork refuses this outright at its normal entry point
|
||||||
|
// (lf::schedule throws lf::schedule_in_worker, because a libfork worker may never block),
|
||||||
|
// which is why the libfork engine owns a dispatch thread of its own. Without this case a
|
||||||
|
// naive port passes every other test in the file and turns every dependency-released link
|
||||||
|
// job into a cancelled one on the real GL path.
|
||||||
|
ShaderCompilePool pool(kTestThreads);
|
||||||
|
|
||||||
|
std::atomic<Bool> innerSawPoolThread{false};
|
||||||
|
auto inner = MakeShared<TestJob>(
|
||||||
|
[&](TestJob&) { innerSawPoolThread.store(ShaderCompilePool::IsPoolThread(), std::memory_order_release); });
|
||||||
|
|
||||||
|
std::atomic<Bool> postedFromPoolThread{false};
|
||||||
|
auto outer = MakeShared<TestJob>([&](TestJob&) {
|
||||||
|
postedFromPoolThread.store(ShaderCompilePool::IsPoolThread(), std::memory_order_release);
|
||||||
|
pool.Post(inner);
|
||||||
|
});
|
||||||
|
|
||||||
|
pool.Post(outer);
|
||||||
|
outer->Wait();
|
||||||
|
inner->Wait();
|
||||||
|
|
||||||
|
EXPECT_TRUE(postedFromPoolThread.load()) << "the outer body did not run on a pool thread, so this case "
|
||||||
|
"did not exercise posting from inside the pool";
|
||||||
|
EXPECT_TRUE(outer->IsComplete());
|
||||||
|
// The load-bearing one: the inner job RAN. A dispatch the engine refused would have
|
||||||
|
// settled it Cancelled instead, and its body would never have executed.
|
||||||
|
EXPECT_TRUE(inner->IsComplete()) << "a job posted from a pool thread was not dispatched";
|
||||||
|
EXPECT_FALSE(inner->IsCancelled());
|
||||||
|
EXPECT_EQ(inner->ran.load(), 1u);
|
||||||
|
EXPECT_TRUE(innerSawPoolThread.load());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(JobNodeSubmit, ABurstPostedFromInsideThePoolStillRunsInParallel) {
|
||||||
|
// The tail of a pack load: one compile job goes terminal and its continuations release
|
||||||
|
// several programs at once (ShaderCompileAdoptionMap lets one compile settle many), so a
|
||||||
|
// WORKER posts a burst into a pool that is otherwise idle. Every one of those posts clears
|
||||||
|
// the budget immediately, so the engine is handed `kBurst` runnable jobs from inside
|
||||||
|
// itself - and it has to spread them, not run them one behind another on the thread that
|
||||||
|
// submitted them.
|
||||||
|
//
|
||||||
|
// Asserting on peak concurrency rather than on wall time: the budget is the contract, and
|
||||||
|
// an engine that dispatches within the budget but executes serially has silently turned
|
||||||
|
// the budget into an upper bound nothing reaches.
|
||||||
|
constexpr Uint kBurst = 4; // == kTestThreads, so the budget can hold all of them at once
|
||||||
|
ShaderCompilePool pool(kTestThreads);
|
||||||
|
|
||||||
|
std::atomic<Uint> live{0};
|
||||||
|
std::atomic<Uint> peak{0};
|
||||||
|
std::atomic<Uint> finished{0};
|
||||||
|
|
||||||
|
Vector<SharedPtr<TestJob>> burst;
|
||||||
|
burst.reserve(kBurst);
|
||||||
|
for (Uint i = 0; i < kBurst; ++i) {
|
||||||
|
burst.push_back(MakeShared<TestJob>([&](TestJob&) {
|
||||||
|
const Uint now = live.fetch_add(1, std::memory_order_acq_rel) + 1;
|
||||||
|
Uint seen = peak.load(std::memory_order_acquire);
|
||||||
|
while (now > seen && !peak.compare_exchange_weak(seen, now, std::memory_order_acq_rel)) {
|
||||||
|
}
|
||||||
|
// Long enough that a serial engine cannot fake overlap, short enough to keep the
|
||||||
|
// case cheap: with any real spread every body is inside this window together.
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(120));
|
||||||
|
live.fetch_sub(1, std::memory_order_acq_rel);
|
||||||
|
finished.fetch_add(1, std::memory_order_acq_rel);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::atomic<Bool> postedFromPoolThread{false};
|
||||||
|
auto seeder = MakeShared<TestJob>([&](TestJob&) {
|
||||||
|
postedFromPoolThread.store(ShaderCompilePool::IsPoolThread(), std::memory_order_release);
|
||||||
|
for (const auto& job : burst) pool.Post(job);
|
||||||
|
});
|
||||||
|
|
||||||
|
pool.Post(seeder);
|
||||||
|
seeder->Wait();
|
||||||
|
for (const auto& job : burst) job->Wait();
|
||||||
|
|
||||||
|
ASSERT_TRUE(postedFromPoolThread.load()) << "the burst was not posted from a pool thread";
|
||||||
|
EXPECT_EQ(finished.load(), kBurst);
|
||||||
|
EXPECT_GT(peak.load(), 1u) << "a burst posted from inside the pool ran strictly one at a time; the "
|
||||||
|
"engine serialized work the budget had already cleared";
|
||||||
|
}
|
||||||
|
|
||||||
TEST(JobNodeSubmit, ConcurrencyBudgetIsNeverExceeded) {
|
TEST(JobNodeSubmit, ConcurrencyBudgetIsNeverExceeded) {
|
||||||
constexpr Uint kBudget = 2;
|
constexpr Uint kBudget = 2;
|
||||||
constexpr Uint kJobs = 64;
|
constexpr Uint kJobs = 64;
|
||||||
@@ -472,30 +631,58 @@ TEST(JobNodeException, AThrowingJobDoesNotPoisonTheWorkerForLaterJobs) {
|
|||||||
// ---------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------
|
||||||
|
|
||||||
TEST(ShaderCompilePoolDrain, StopAndDrainWithAThousandQueuedJobsLeavesNoneRunningOrPending) {
|
TEST(ShaderCompilePoolDrain, StopAndDrainWithAThousandQueuedJobsLeavesNoneRunningOrPending) {
|
||||||
constexpr Uint kJobs = 1000;
|
constexpr Uint kQueued = 1000;
|
||||||
ShaderCompilePool pool(kTestThreads);
|
ShaderCompilePool pool(kTestThreads);
|
||||||
pool.SetMaxConcurrency(1); // keep the vast majority queued behind the budget
|
pool.SetMaxConcurrency(1); // one slot, so everything behind the first job stays queued
|
||||||
|
|
||||||
Vector<SharedPtr<TestJob>> jobs;
|
// Pin that slot with a job that will not return until this test says so. Everything
|
||||||
jobs.reserve(kJobs);
|
// posted behind it is then PROVABLY still in the queue, which is what makes the counts
|
||||||
for (Uint i = 0; i < kJobs; ++i) {
|
// below exact.
|
||||||
jobs.push_back(MakeShared<TestJob>());
|
//
|
||||||
pool.Post(jobs.back());
|
// This case used to post a thousand trivial jobs and drain immediately, hoping the drain
|
||||||
|
// would beat the workers to some of them - and then assert only that "some" were
|
||||||
|
// cancelled. That hope does not survive an engine whose workers take their next job
|
||||||
|
// without a scheduler round trip: the libfork engine drained all thousand before the
|
||||||
|
// posting loop had finished, so the assertion failed about one run in fifty. The property
|
||||||
|
// being tested (a drain ABANDONS queued work rather than running it) is real and
|
||||||
|
// engine-independent; only the way it was provoked was a race.
|
||||||
|
Gate gate;
|
||||||
|
std::atomic<Bool> entered{false};
|
||||||
|
auto blocker = MakeShared<TestJob>([&](TestJob&) {
|
||||||
|
entered.store(true, std::memory_order_release);
|
||||||
|
gate.Wait();
|
||||||
|
});
|
||||||
|
pool.Post(blocker);
|
||||||
|
ASSERT_TRUE(WaitUntil([&] { return entered.load(); }));
|
||||||
|
|
||||||
|
Vector<SharedPtr<TestJob>> queued;
|
||||||
|
queued.reserve(kQueued);
|
||||||
|
for (Uint i = 0; i < kQueued; ++i) {
|
||||||
|
queued.push_back(MakeShared<TestJob>());
|
||||||
|
pool.Post(queued.back());
|
||||||
}
|
}
|
||||||
|
for (const auto& job : queued) ASSERT_FALSE(job->IsTerminal());
|
||||||
|
|
||||||
pool.StopAndDrain();
|
std::thread drain([&] { pool.StopAndDrain(); });
|
||||||
|
// StopAndDrain settles the entire queue before it waits for the running body, so the
|
||||||
|
// first cancelled node proves it is past that point - and the gate can then be released
|
||||||
|
// without racing it.
|
||||||
|
ASSERT_TRUE(WaitUntil([&] { return queued.front()->IsTerminal(); }));
|
||||||
|
gate.Open();
|
||||||
|
drain.join();
|
||||||
|
|
||||||
// Every node is terminal, so nothing can be waiting on a worker that will never come.
|
// The job that was already running still finished: an in-flight body is waited for, not
|
||||||
Uint complete = 0;
|
// interrupted.
|
||||||
Uint cancelled = 0;
|
EXPECT_TRUE(blocker->IsComplete());
|
||||||
for (const auto& job : jobs) {
|
EXPECT_EQ(blocker->ran.load(), 1u);
|
||||||
|
|
||||||
|
// And every queued node is terminal, so nothing is left waiting on a worker that will
|
||||||
|
// never come - settled as cancelled, with its body never entered.
|
||||||
|
for (const auto& job : queued) {
|
||||||
ASSERT_TRUE(job->IsTerminal());
|
ASSERT_TRUE(job->IsTerminal());
|
||||||
if (job->IsComplete()) ++complete;
|
EXPECT_TRUE(job->IsCancelled());
|
||||||
if (job->IsCancelled()) ++cancelled;
|
EXPECT_EQ(job->ran.load(), 0u);
|
||||||
EXPECT_LE(job->ran.load(), 1u);
|
|
||||||
}
|
}
|
||||||
EXPECT_EQ(complete + cancelled, kJobs);
|
|
||||||
EXPECT_GT(cancelled, 0u); // the drain really did abandon queued work rather than run it
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(ShaderCompilePoolDrain, StopAndDrainWaitsForARunningBodyToReturn) {
|
TEST(ShaderCompilePoolDrain, StopAndDrainWaitsForARunningBodyToReturn) {
|
||||||
@@ -535,3 +722,141 @@ TEST(ShaderCompilePoolDrain, JobsPostedAfterADrainStillRun) {
|
|||||||
EXPECT_TRUE(job->IsComplete());
|
EXPECT_TRUE(job->IsComplete());
|
||||||
EXPECT_EQ(job->ran.load(), 1u);
|
EXPECT_EQ(job->ran.load(), 1u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// Execution engine selection (MOBILEGL_ASYNC_POOL)
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// The engine decides only HOW a job that the concurrency budget has already cleared reaches a
|
||||||
|
// worker thread. Everything else in this file - the budget, cancel request-vs-outcome, the
|
||||||
|
// continuation machinery, the inline fallback after a stop, the drain - is engine-independent
|
||||||
|
// by construction, which is why the whole suite is expected to pass unchanged with
|
||||||
|
// MOBILEGL_ASYNC_POOL unset and with it set to libfork. These cases pin the selection itself,
|
||||||
|
// so that a run of the matrix cannot silently test asio twice.
|
||||||
|
|
||||||
|
TEST(AsyncPoolEngineSelection, EveryAcceptedSpellingParsesToItsEngine) {
|
||||||
|
EXPECT_EQ(ParseAsyncPoolEngine("asio"), AsyncPoolEngine::Asio);
|
||||||
|
EXPECT_EQ(ParseAsyncPoolEngine("libfork"), AsyncPoolEngine::Libfork);
|
||||||
|
// Case-insensitive, like the other named-value variables (MOBILEGL_*_MULTIDRAW_MODE).
|
||||||
|
EXPECT_EQ(ParseAsyncPoolEngine("Libfork"), AsyncPoolEngine::Libfork);
|
||||||
|
EXPECT_EQ(ParseAsyncPoolEngine("LIBFORK"), AsyncPoolEngine::Libfork);
|
||||||
|
EXPECT_EQ(ParseAsyncPoolEngine("ASIO"), AsyncPoolEngine::Asio);
|
||||||
|
|
||||||
|
EXPECT_STREQ(AsyncPoolEngineName(AsyncPoolEngine::Asio), "asio");
|
||||||
|
EXPECT_STREQ(AsyncPoolEngineName(AsyncPoolEngine::Libfork), "libfork");
|
||||||
|
// Round trip: whatever the name prints is a spelling the variable accepts back.
|
||||||
|
EXPECT_EQ(ParseAsyncPoolEngine(AsyncPoolEngineName(AsyncPoolEngine::Asio)), AsyncPoolEngine::Asio);
|
||||||
|
EXPECT_EQ(ParseAsyncPoolEngine(AsyncPoolEngineName(AsyncPoolEngine::Libfork)), AsyncPoolEngine::Libfork);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AsyncPoolEngineSelection, EmptyAndAutoAreTheDefaultEngineAndSaySoSilently) {
|
||||||
|
// Unset resolves through the empty string, and "auto" is the spelling the other named
|
||||||
|
// -value variables accept for "no preference". Neither is a mistake, so neither warns.
|
||||||
|
const std::streamoff before = LogSize();
|
||||||
|
EXPECT_EQ(ParseAsyncPoolEngine(""), AsyncPoolEngine::Asio);
|
||||||
|
EXPECT_EQ(ParseAsyncPoolEngine("auto"), AsyncPoolEngine::Asio);
|
||||||
|
EXPECT_EQ(ReadLogFrom(before).find("MOBILEGL_ASYNC_POOL"), String::npos)
|
||||||
|
<< "a legitimate value warned; only an unrecognized one may";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AsyncPoolEngineSelection, AnUnrecognizedEngineNameFallsBackToAsioAndWarns) {
|
||||||
|
const std::streamoff before = LogSize();
|
||||||
|
EXPECT_EQ(ParseAsyncPoolEngine("libfrok"), AsyncPoolEngine::Asio);
|
||||||
|
|
||||||
|
// The warning is the other half of the contract: a misspelt engine name that fell back
|
||||||
|
// silently would be indistinguishable from an unset variable, and a scaling measurement
|
||||||
|
// taken against the wrong engine is worse than no measurement.
|
||||||
|
//
|
||||||
|
// Guarded because MGLOG_W is a compile-time no-op unless the build's log level admits it -
|
||||||
|
// and the shipped level does not (Log.h orders the levels DEBUG=0, WARN=1, ERROR=2, INFO=3,
|
||||||
|
// FATAL=4 and gates on `ACTIVE <= LEVEL`, so the default INFO build enables only INFO and
|
||||||
|
// FATAL). Nothing is skipped: the fallback above is pinned in every build, and this half is
|
||||||
|
// checked by a build configured with
|
||||||
|
// -DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_WARN. The same guard is what makes the
|
||||||
|
// preceding "says so silently" case honest rather than vacuously true.
|
||||||
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_WARN
|
||||||
|
const String logged = ReadLogFrom(before);
|
||||||
|
EXPECT_NE(logged.find("MOBILEGL_ASYNC_POOL"), String::npos) << "no warning names the variable; log tail: " << logged;
|
||||||
|
EXPECT_NE(logged.find("libfrok"), String::npos)
|
||||||
|
<< "the warning does not quote the rejected value; log tail: " << logged;
|
||||||
|
EXPECT_NE(logged.find("asio"), String::npos)
|
||||||
|
<< "the warning does not say what it fell back to; log tail: " << logged;
|
||||||
|
#else
|
||||||
|
(void)before;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AsyncPoolEngineSelection, TheDetectedEngineIsTheOneTheEnvironmentAskedFor) {
|
||||||
|
// Read the variable directly rather than through the pool, so this really compares the
|
||||||
|
// process's answer against the environment the runner exported. This is the case that
|
||||||
|
// makes "the suite passed with MOBILEGL_ASYNC_POOL=libfork" mean something.
|
||||||
|
const char* const raw = std::getenv("MOBILEGL_ASYNC_POOL");
|
||||||
|
const AsyncPoolEngine expected = ParseAsyncPoolEngine(raw != nullptr ? String(raw) : String());
|
||||||
|
EXPECT_EQ(DetectAsyncPoolEngine(), expected);
|
||||||
|
|
||||||
|
// Stable: resolved once per process, so it cannot drift between calls.
|
||||||
|
EXPECT_EQ(DetectAsyncPoolEngine(), DetectAsyncPoolEngine());
|
||||||
|
|
||||||
|
if (DetectAsyncPoolEngine() != AsyncPoolEngine::Asio) {
|
||||||
|
// Selecting a non-default engine announces itself at INFO, which the shipped log level
|
||||||
|
// does admit - so on the libfork half of the matrix this doubles as the positive
|
||||||
|
// control for the log plumbing the preceding two cases read: it proves
|
||||||
|
// MOBILEGL_LOG_FILE_PATH took effect and that ReadLogFrom really sees MobileGL's
|
||||||
|
// output, rather than passing because the file is always empty.
|
||||||
|
const String logged = ReadLogFrom(0);
|
||||||
|
EXPECT_NE(logged.find("MOBILEGL_ASYNC_POOL"), String::npos)
|
||||||
|
<< "the selected engine was never announced, so this binary's log capture proves nothing";
|
||||||
|
EXPECT_NE(logged.find(AsyncPoolEngineName(DetectAsyncPoolEngine())), String::npos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AsyncPoolEngineSelection, EveryPoolReportsTheProcessEngineAndRunsWorkOnIt) {
|
||||||
|
ShaderCompilePool first(kTestThreads);
|
||||||
|
ShaderCompilePool second(kTestThreads);
|
||||||
|
EXPECT_EQ(first.GetEngine(), DetectAsyncPoolEngine());
|
||||||
|
EXPECT_EQ(second.GetEngine(), first.GetEngine())
|
||||||
|
<< "two pools in one process disagree about the engine; a process must never run both";
|
||||||
|
|
||||||
|
// And the engine it reports is the one that actually executed the work: the body ran off
|
||||||
|
// the calling thread, on a thread the pool owns.
|
||||||
|
const auto callingThread = std::this_thread::get_id();
|
||||||
|
std::atomic<Bool> sawPoolThread{false};
|
||||||
|
std::thread::id bodyThread{};
|
||||||
|
auto job = MakeShared<TestJob>([&](TestJob&) {
|
||||||
|
sawPoolThread.store(ShaderCompilePool::IsPoolThread(), std::memory_order_release);
|
||||||
|
bodyThread = std::this_thread::get_id();
|
||||||
|
});
|
||||||
|
first.Post(job);
|
||||||
|
job->Wait();
|
||||||
|
|
||||||
|
ASSERT_TRUE(job->IsComplete());
|
||||||
|
EXPECT_TRUE(sawPoolThread.load());
|
||||||
|
EXPECT_NE(bodyThread, callingThread);
|
||||||
|
}
|
||||||
|
|
||||||
|
// gtest_main is replaced here for one reason: the engine-selection cases above assert that an
|
||||||
|
// unrecognized MOBILEGL_ASYNC_POOL value WARNS, and MobileGL's desktop log sink is the log
|
||||||
|
// file - MOBILEGL_LOG_ENABLE_CONSOLE is 0 in Defines.h, so there is nothing on stdout to
|
||||||
|
// capture. MOBILEGL_LOG_FILE_PATH is read by Log.cpp's InitFile() at the first log write in
|
||||||
|
// the process, so it has to be set before any test body runs.
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
const std::filesystem::path logPath =
|
||||||
|
std::filesystem::temp_directory_path() /
|
||||||
|
("mobilegl-jobnodetest-" + std::to_string(static_cast<long long>(MGL_TEST_GETPID())) + ".log");
|
||||||
|
g_logFilePath = logPath.string();
|
||||||
|
std::filesystem::remove(logPath);
|
||||||
|
#ifdef _WIN32
|
||||||
|
::_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logFilePath.c_str());
|
||||||
|
#else
|
||||||
|
::setenv("MOBILEGL_LOG_FILE_PATH", g_logFilePath.c_str(), 1);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
const int result = RUN_ALL_TESTS();
|
||||||
|
|
||||||
|
// Best-effort: leaving a log file per test process in the temp directory would be litter,
|
||||||
|
// and a failed run has already printed the tail it needed into the gtest output.
|
||||||
|
std::error_code ignored;
|
||||||
|
std::filesystem::remove(logPath, ignored);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,22 +16,5 @@ target_link_libraries(
|
|||||||
${LINK_LIBRARIES}
|
${LINK_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(
|
|
||||||
VertexAttribBindingStateTest
|
|
||||||
VertexAttribBindingStateTest.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(VertexAttribBindingStateTest PRIVATE
|
|
||||||
${MGL_ROOT}/include
|
|
||||||
${MGL_ROOT}/MobileGL
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
VertexAttribBindingStateTest PRIVATE
|
|
||||||
GTest::gtest_main
|
|
||||||
${LINK_LIBRARIES}
|
|
||||||
)
|
|
||||||
|
|
||||||
include(GoogleTest)
|
include(GoogleTest)
|
||||||
gtest_discover_tests(VertexArrayTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(VertexArrayTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
gtest_discover_tests(VertexAttribBindingStateTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
|
||||||
|
|||||||
@@ -1,430 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Test/VertexArray/VertexAttribBindingStateTest.cpp
|
|
||||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
||||||
// Licensed under the GNU Lesser General Public License v3.0:
|
|
||||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
||||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// End of Source File Header
|
|
||||||
|
|
||||||
// The ARB_vertex_attrib_binding state model, replayed exactly as
|
|
||||||
// KHR-GL4x.vertex_attrib_binding.basic-state1/3/4 and .negative-* walk it
|
|
||||||
// (external/openglcts/modules/gl/gl4cVertexAttribBindingTests.cpp): after each mutation the
|
|
||||||
// ten per-attribute pnames and the four per-binding-point pnames are read back in full, which
|
|
||||||
// is what makes a single wrong field visible as itself instead of as a downstream render
|
|
||||||
// difference.
|
|
||||||
//
|
|
||||||
// Four defects are pinned here, all of them frontend-only (both backends reported them
|
|
||||||
// byte-identically):
|
|
||||||
// * VERTEX_BINDING_STRIDE defaulted to 0; the spec's initial value is 16.
|
|
||||||
// * The eager binding -> attribute resolve overwrote VERTEX_ATTRIB_ARRAY_STRIDE / _POINTER,
|
|
||||||
// which are legacy state only glVertexAttrib*Pointer may write.
|
|
||||||
// * glVertexAttribDivisor did not re-point the attribute at its own binding point, so a
|
|
||||||
// later resolve restored the old binding's divisor.
|
|
||||||
// * The binding entry points accepted the default vertex array (name 0) in a core profile.
|
|
||||||
//
|
|
||||||
// GPU-free: this is all GL object state, no backend is consulted.
|
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "Includes.h"
|
|
||||||
#include "Init.h"
|
|
||||||
#include <Config.h>
|
|
||||||
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
|
||||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
|
||||||
#include <MG_Impl/GLImpl/VertexArray/GL_VertexArray.h>
|
|
||||||
#include <MG_State/EGLState/Core.h>
|
|
||||||
#include <MG_State/GLState/Core.h>
|
|
||||||
|
|
||||||
using namespace MobileGL;
|
|
||||||
using namespace MobileGL::MG_Impl::GLImpl;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// Mirrors the CTS's VertexAttribState: the initial per-attribute state, mutated field by
|
|
||||||
// field as the sequence proceeds, and verified in full after every call.
|
|
||||||
struct AttribState {
|
|
||||||
explicit AttribState(GLuint attribIndex) : index(attribIndex), binding(attribIndex) {}
|
|
||||||
|
|
||||||
GLuint index = 0;
|
|
||||||
GLint enabled = 0;
|
|
||||||
GLint size = 4;
|
|
||||||
GLint stride = 0;
|
|
||||||
GLenum type = GL_FLOAT;
|
|
||||||
GLint normalized = 0;
|
|
||||||
GLint integer = 0;
|
|
||||||
GLint isLong = 0;
|
|
||||||
GLint divisor = 0;
|
|
||||||
GLuint pointer = 0;
|
|
||||||
GLuint bufferBinding = 0;
|
|
||||||
GLuint binding = 0;
|
|
||||||
GLint relativeOffset = 0;
|
|
||||||
|
|
||||||
void Verify(const char* where) const {
|
|
||||||
GLint p = -1;
|
|
||||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_ENABLED, &p);
|
|
||||||
EXPECT_EQ(p, enabled) << where << ": ENABLED(" << index << ")";
|
|
||||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_SIZE, &p);
|
|
||||||
EXPECT_EQ(p, size) << where << ": SIZE(" << index << ")";
|
|
||||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_STRIDE, &p);
|
|
||||||
EXPECT_EQ(p, stride) << where << ": STRIDE(" << index << ")";
|
|
||||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_TYPE, &p);
|
|
||||||
EXPECT_EQ(static_cast<GLenum>(p), type) << where << ": TYPE(" << index << ")";
|
|
||||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_NORMALIZED, &p);
|
|
||||||
EXPECT_EQ(p, normalized) << where << ": NORMALIZED(" << index << ")";
|
|
||||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_INTEGER, &p);
|
|
||||||
EXPECT_EQ(p, integer) << where << ": INTEGER(" << index << ")";
|
|
||||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_LONG, &p);
|
|
||||||
EXPECT_EQ(p, isLong) << where << ": LONG(" << index << ")";
|
|
||||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_DIVISOR, &p);
|
|
||||||
EXPECT_EQ(p, divisor) << where << ": DIVISOR(" << index << ")";
|
|
||||||
void* pp = nullptr;
|
|
||||||
GetVertexAttribPointerv(index, GL_VERTEX_ATTRIB_ARRAY_POINTER, &pp);
|
|
||||||
EXPECT_EQ(reinterpret_cast<uintptr_t>(pp), static_cast<uintptr_t>(pointer))
|
|
||||||
<< where << ": POINTER(" << index << ")";
|
|
||||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING, &p);
|
|
||||||
EXPECT_EQ(static_cast<GLuint>(p), bufferBinding) << where << ": BUFFER_BINDING(" << index << ")";
|
|
||||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_BINDING, &p);
|
|
||||||
EXPECT_EQ(static_cast<GLuint>(p), binding) << where << ": BINDING(" << index << ")";
|
|
||||||
GetVertexAttribiv(index, GL_VERTEX_ATTRIB_RELATIVE_OFFSET, &p);
|
|
||||||
EXPECT_EQ(p, relativeOffset) << where << ": RELATIVE_OFFSET(" << index << ")";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mirrors the CTS's VertexBindingState, initial stride 16 included.
|
|
||||||
struct BindingState {
|
|
||||||
explicit BindingState(GLuint bindingIndex) : index(bindingIndex) {}
|
|
||||||
|
|
||||||
GLuint index = 0;
|
|
||||||
GLuint buffer = 0;
|
|
||||||
GLint offset = 0;
|
|
||||||
GLint stride = 16;
|
|
||||||
GLint divisor = 0;
|
|
||||||
|
|
||||||
void Verify(const char* where) const {
|
|
||||||
GLint p = -1;
|
|
||||||
GetIntegeri_v(GL_VERTEX_BINDING_BUFFER, index, &p);
|
|
||||||
EXPECT_EQ(static_cast<GLuint>(p), buffer) << where << ": VERTEX_BINDING_BUFFER(" << index << ")";
|
|
||||||
// The CTS reads the offset through glGetInteger64i_v; that entry point's pname
|
|
||||||
// routing is a separate defect with its own regression (see the indexed-getter
|
|
||||||
// parity test), so the state model is pinned through the 32-bit view here.
|
|
||||||
GetIntegeri_v(GL_VERTEX_BINDING_OFFSET, index, &p);
|
|
||||||
EXPECT_EQ(p, offset) << where << ": VERTEX_BINDING_OFFSET(" << index << ")";
|
|
||||||
GetIntegeri_v(GL_VERTEX_BINDING_STRIDE, index, &p);
|
|
||||||
EXPECT_EQ(p, stride) << where << ": VERTEX_BINDING_STRIDE(" << index << ")";
|
|
||||||
GetIntegeri_v(GL_VERTEX_BINDING_DIVISOR, index, &p);
|
|
||||||
EXPECT_EQ(p, divisor) << where << ": VERTEX_BINDING_DIVISOR(" << index << ")";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Strict core rules only apply when the current EGL context explicitly asked for a core
|
|
||||||
// profile; the suite's default (no current context) is relaxed. RAII so a failed
|
|
||||||
// expectation cannot leave the context current for the rest of the binary.
|
|
||||||
struct ScopedCoreProfileContext {
|
|
||||||
ScopedCoreProfileContext() {
|
|
||||||
auto& egl = *MG_State::pEGLContext;
|
|
||||||
m_display = egl.GetDisplay(EGL_DEFAULT_DISPLAY);
|
|
||||||
EXPECT_NE(m_display, EGL_NO_DISPLAY);
|
|
||||||
EXPECT_TRUE(egl.InitializeDisplay(m_display, nullptr, nullptr));
|
|
||||||
EGLint configCount = 0;
|
|
||||||
EXPECT_TRUE(egl.ChooseConfig(m_display, nullptr, &m_config, 1, &configCount));
|
|
||||||
const EGLint surfaceAttribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE};
|
|
||||||
m_surface = egl.CreatePbufferSurface(m_display, m_config, surfaceAttribs);
|
|
||||||
EXPECT_NE(m_surface, EGL_NO_SURFACE);
|
|
||||||
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION,
|
|
||||||
3,
|
|
||||||
EGL_CONTEXT_MINOR_VERSION,
|
|
||||||
3,
|
|
||||||
EGL_CONTEXT_OPENGL_PROFILE_MASK,
|
|
||||||
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
|
|
||||||
EGL_NONE};
|
|
||||||
m_context = egl.CreateContext(m_display, m_config, EGL_NO_CONTEXT, contextAttribs);
|
|
||||||
EXPECT_NE(m_context, EGL_NO_CONTEXT);
|
|
||||||
EXPECT_TRUE(egl.MakeCurrent(m_display, m_surface, m_surface, m_context));
|
|
||||||
}
|
|
||||||
~ScopedCoreProfileContext() {
|
|
||||||
auto& egl = *MG_State::pEGLContext;
|
|
||||||
egl.MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
|
||||||
if (m_context != EGL_NO_CONTEXT) egl.DestroyContext(m_display, m_context);
|
|
||||||
if (m_surface != EGL_NO_SURFACE) egl.DestroySurface(m_display, m_surface);
|
|
||||||
}
|
|
||||||
ScopedCoreProfileContext(const ScopedCoreProfileContext&) = delete;
|
|
||||||
ScopedCoreProfileContext& operator=(const ScopedCoreProfileContext&) = delete;
|
|
||||||
|
|
||||||
private:
|
|
||||||
EGLDisplay m_display = EGL_NO_DISPLAY;
|
|
||||||
EGLConfig m_config = nullptr;
|
|
||||||
EGLSurface m_surface = EGL_NO_SURFACE;
|
|
||||||
MG_State::EGLState::EGLContext::EGLContextHandle m_context = EGL_NO_CONTEXT;
|
|
||||||
};
|
|
||||||
|
|
||||||
class VertexAttribBindingStateTest : public ::testing::Test {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
MobileGL::Initialize();
|
|
||||||
// A fresh context per case: the state model under test is cumulative, so a leftover
|
|
||||||
// VAO binding from a neighbour would silently change what "default state" means.
|
|
||||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
|
||||||
GenVertexArrays(1, &m_vao);
|
|
||||||
BindVertexArray(m_vao);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint CreateVbo(GLsizeiptr size) {
|
|
||||||
GLuint vbo = 0;
|
|
||||||
GenBuffers(1, &vbo);
|
|
||||||
BindBuffer(GL_ARRAY_BUFFER, vbo);
|
|
||||||
BufferData(GL_ARRAY_BUFFER, size, nullptr, GL_DYNAMIC_COPY);
|
|
||||||
BindBuffer(GL_ARRAY_BUFFER, 0);
|
|
||||||
return vbo;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void DrainErrors() {
|
|
||||||
for (int i = 0; i < 16 && GetError() != GL_NO_ERROR; ++i) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint m_vao = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// basic-state1's opening block: the initial per-attribute mapping and the per-binding-point
|
|
||||||
// defaults, VERTEX_BINDING_STRIDE = 16 included. That check is the FIRST thing the CTS case
|
|
||||||
// does, so a wrong default masked everything the case would have found after it.
|
|
||||||
TEST_F(VertexAttribBindingStateTest, DefaultsMatchTheSpecInitialState) {
|
|
||||||
for (GLuint i = 0; i < 16; ++i) {
|
|
||||||
AttribState(i).Verify("defaults");
|
|
||||||
BindingState(i).Verify("defaults");
|
|
||||||
}
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// basic-state3, verbatim: a full separate-format sequence, then a pointer call, then a
|
|
||||||
// binding update on top of it. The legacy STRIDE/POINTER pair must stay untouched by every
|
|
||||||
// step except the glVertexAttribPointer one, and must survive the binding update after it.
|
|
||||||
TEST_F(VertexAttribBindingStateTest, SeparateFormatSequenceKeepsLegacyStrideAndPointerAtZero) {
|
|
||||||
const GLuint vbo0 = CreateVbo(10000);
|
|
||||||
const GLuint vbo1 = CreateVbo(10000);
|
|
||||||
const GLuint vbo2 = CreateVbo(10000);
|
|
||||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
|
|
||||||
AttribState va0(0), va2(2), va15(15);
|
|
||||||
BindingState vb0(0), vb2(2), vb15(15);
|
|
||||||
|
|
||||||
VertexAttribFormat(0, 2, GL_BYTE, GL_TRUE, 16);
|
|
||||||
va0.size = 2;
|
|
||||||
va0.type = GL_BYTE;
|
|
||||||
va0.normalized = 1;
|
|
||||||
va0.relativeOffset = 16;
|
|
||||||
va0.Verify("after glVertexAttribFormat");
|
|
||||||
// The format call says nothing about a buffer, so binding point 0 keeps its defaults -
|
|
||||||
// stride 16 among them.
|
|
||||||
vb0.Verify("after glVertexAttribFormat");
|
|
||||||
|
|
||||||
VertexAttribIFormat(2, 3, GL_INT, 512);
|
|
||||||
va2.size = 3;
|
|
||||||
va2.type = GL_INT;
|
|
||||||
va2.integer = 1;
|
|
||||||
va2.relativeOffset = 512;
|
|
||||||
va2.Verify("after glVertexAttribIFormat");
|
|
||||||
vb2.Verify("after glVertexAttribIFormat");
|
|
||||||
|
|
||||||
BindVertexBuffer(0, vbo0, 2048, 128);
|
|
||||||
va0.bufferBinding = vbo0;
|
|
||||||
vb0.buffer = vbo0;
|
|
||||||
vb0.offset = 2048;
|
|
||||||
vb0.stride = 128;
|
|
||||||
va0.Verify("after glBindVertexBuffer(0)");
|
|
||||||
vb0.Verify("after glBindVertexBuffer(0)");
|
|
||||||
|
|
||||||
BindVertexBuffer(2, vbo2, 64, 256);
|
|
||||||
va2.bufferBinding = vbo2;
|
|
||||||
vb2.buffer = vbo2;
|
|
||||||
vb2.offset = 64;
|
|
||||||
vb2.stride = 256;
|
|
||||||
va2.Verify("after glBindVertexBuffer(2)");
|
|
||||||
vb2.Verify("after glBindVertexBuffer(2)");
|
|
||||||
|
|
||||||
// Attribute 2 moves onto binding 0 and takes that binding point's buffer with it.
|
|
||||||
VertexAttribBinding(2, 0);
|
|
||||||
va2.binding = 0;
|
|
||||||
va2.bufferBinding = vbo0;
|
|
||||||
va0.Verify("after glVertexAttribBinding(2,0)");
|
|
||||||
vb0.Verify("after glVertexAttribBinding(2,0)");
|
|
||||||
va2.Verify("after glVertexAttribBinding(2,0)");
|
|
||||||
vb2.Verify("after glVertexAttribBinding(2,0)");
|
|
||||||
|
|
||||||
VertexAttribBinding(0, 15);
|
|
||||||
va0.binding = 15;
|
|
||||||
va0.bufferBinding = 0;
|
|
||||||
va0.Verify("after glVertexAttribBinding(0,15)");
|
|
||||||
vb0.Verify("after glVertexAttribBinding(0,15)");
|
|
||||||
va15.Verify("after glVertexAttribBinding(0,15)");
|
|
||||||
vb15.Verify("after glVertexAttribBinding(0,15)");
|
|
||||||
|
|
||||||
BindVertexBuffer(15, vbo1, 16, 32);
|
|
||||||
va0.bufferBinding = vbo1;
|
|
||||||
va15.bufferBinding = vbo1;
|
|
||||||
vb15.buffer = vbo1;
|
|
||||||
vb15.offset = 16;
|
|
||||||
vb15.stride = 32;
|
|
||||||
va0.Verify("after glBindVertexBuffer(15)");
|
|
||||||
va15.Verify("after glBindVertexBuffer(15)");
|
|
||||||
vb15.Verify("after glBindVertexBuffer(15)");
|
|
||||||
|
|
||||||
// The one call that IS allowed to write the legacy pair - and it also re-points the
|
|
||||||
// attribute at its own binding point and rewrites that binding point.
|
|
||||||
BindBuffer(GL_ARRAY_BUFFER, vbo2);
|
|
||||||
VertexAttribPointer(0, 4, GL_UNSIGNED_BYTE, GL_FALSE, 8, reinterpret_cast<const void*>(640));
|
|
||||||
BindBuffer(GL_ARRAY_BUFFER, 0);
|
|
||||||
va0.size = 4;
|
|
||||||
va0.type = GL_UNSIGNED_BYTE;
|
|
||||||
va0.stride = 8;
|
|
||||||
va0.pointer = 640;
|
|
||||||
va0.relativeOffset = 0;
|
|
||||||
va0.normalized = 0;
|
|
||||||
va0.binding = 0;
|
|
||||||
va0.bufferBinding = vbo2;
|
|
||||||
vb0.buffer = vbo2;
|
|
||||||
vb0.offset = 640;
|
|
||||||
vb0.stride = 8;
|
|
||||||
va2.bufferBinding = vbo2;
|
|
||||||
va0.Verify("after glVertexAttribPointer");
|
|
||||||
vb0.Verify("after glVertexAttribPointer");
|
|
||||||
va2.Verify("after glVertexAttribPointer");
|
|
||||||
va15.Verify("after glVertexAttribPointer");
|
|
||||||
vb15.Verify("after glVertexAttribPointer");
|
|
||||||
|
|
||||||
// ...and a binding update on top of it leaves the legacy pair exactly where the pointer
|
|
||||||
// call left it. This is the assertion the eager resolve used to fail.
|
|
||||||
BindVertexBuffer(0, vbo1, 80, 24);
|
|
||||||
vb0.buffer = vbo1;
|
|
||||||
vb0.offset = 80;
|
|
||||||
vb0.stride = 24;
|
|
||||||
va0.bufferBinding = vbo1;
|
|
||||||
va2.bufferBinding = vbo1;
|
|
||||||
va0.Verify("after the trailing glBindVertexBuffer(0)");
|
|
||||||
vb0.Verify("after the trailing glBindVertexBuffer(0)");
|
|
||||||
va2.Verify("after the trailing glBindVertexBuffer(0)");
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// basic-state4: glVertexAttribDivisor is VertexAttribBinding(i,i) + VertexBindingDivisor(i,d),
|
|
||||||
// and glVertexBindingDivisor reaches the attribute's own DIVISOR query either way.
|
|
||||||
TEST_F(VertexAttribBindingStateTest, DivisorGoesThroughTheBindingPoint) {
|
|
||||||
for (GLuint i = 0; i < 16; ++i) {
|
|
||||||
AttribState va(i);
|
|
||||||
BindingState vb(i);
|
|
||||||
VertexAttribDivisor(i, i + 7);
|
|
||||||
va.divisor = static_cast<GLint>(i + 7);
|
|
||||||
vb.divisor = static_cast<GLint>(i + 7);
|
|
||||||
va.Verify("after glVertexAttribDivisor");
|
|
||||||
vb.Verify("after glVertexAttribDivisor");
|
|
||||||
}
|
|
||||||
for (GLuint i = 0; i < 16; ++i) {
|
|
||||||
AttribState va(i);
|
|
||||||
BindingState vb(i);
|
|
||||||
VertexBindingDivisor(i, i);
|
|
||||||
va.divisor = static_cast<GLint>(i);
|
|
||||||
vb.divisor = static_cast<GLint>(i);
|
|
||||||
va.Verify("after glVertexBindingDivisor");
|
|
||||||
vb.Verify("after glVertexBindingDivisor");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attribute 2 moves onto binding 5 and inherits binding 5's divisor; binding 2 keeps its
|
|
||||||
// own.
|
|
||||||
VertexAttribBinding(2, 5);
|
|
||||||
AttribState va5(5);
|
|
||||||
va5.divisor = 5;
|
|
||||||
BindingState vb5(5);
|
|
||||||
vb5.divisor = 5;
|
|
||||||
AttribState va2(2);
|
|
||||||
va2.divisor = 5;
|
|
||||||
va2.binding = 5;
|
|
||||||
BindingState vb2(2);
|
|
||||||
vb2.divisor = 2;
|
|
||||||
va5.Verify("after glVertexAttribBinding(2,5)");
|
|
||||||
vb5.Verify("after glVertexAttribBinding(2,5)");
|
|
||||||
va2.Verify("after glVertexAttribBinding(2,5)");
|
|
||||||
vb2.Verify("after glVertexAttribBinding(2,5)");
|
|
||||||
|
|
||||||
// ...and glVertexAttribDivisor pulls it back onto binding 2. Guarding the write on
|
|
||||||
// "binding already == index" left the attribute on binding 5 and threw the divisor away.
|
|
||||||
VertexAttribDivisor(2, 23);
|
|
||||||
va2.binding = 2;
|
|
||||||
va2.divisor = 23;
|
|
||||||
vb2.divisor = 23;
|
|
||||||
va5.Verify("after glVertexAttribDivisor(2,23)");
|
|
||||||
vb5.Verify("after glVertexAttribDivisor(2,23)");
|
|
||||||
va2.Verify("after glVertexAttribDivisor(2,23)");
|
|
||||||
vb2.Verify("after glVertexAttribDivisor(2,23)");
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The tail of every negative-* case: with the default vertex array bound, a core profile
|
|
||||||
// rejects all four binding entry points.
|
|
||||||
TEST_F(VertexAttribBindingStateTest, BindingApiRejectsTheDefaultVertexArrayInCoreProfile) {
|
|
||||||
ScopedCoreProfileContext coreContext;
|
|
||||||
ASSERT_FALSE(MG_State::IsRelaxedSemanticsActive());
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
BindVertexArray(0);
|
|
||||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
|
|
||||||
BindVertexBuffer(0, 7, 0, 12);
|
|
||||||
EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glBindVertexBuffer";
|
|
||||||
VertexAttribFormat(0, 4, GL_FLOAT, GL_FALSE, 0);
|
|
||||||
EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glVertexAttribFormat";
|
|
||||||
VertexAttribIFormat(0, 4, GL_INT, 0);
|
|
||||||
EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glVertexAttribIFormat";
|
|
||||||
VertexAttribBinding(0, 0);
|
|
||||||
EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glVertexAttribBinding";
|
|
||||||
VertexBindingDivisor(0, 1);
|
|
||||||
EXPECT_EQ(GetError(), GL_INVALID_OPERATION) << "glVertexBindingDivisor";
|
|
||||||
|
|
||||||
BindVertexArray(m_vao);
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...and the relaxed default - which is what every context that never asked for a core
|
|
||||||
// profile gets - keeps accepting them, because applications depend on it.
|
|
||||||
TEST_F(VertexAttribBindingStateTest, BindingApiStillAcceptsTheDefaultVertexArrayWhenRelaxed) {
|
|
||||||
ASSERT_TRUE(MG_State::IsRelaxedSemanticsActive());
|
|
||||||
const GLuint vbo = CreateVbo(1024);
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
BindVertexArray(0);
|
|
||||||
BindVertexBuffer(0, vbo, 0, 12);
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR) << "glBindVertexBuffer under relaxed semantics";
|
|
||||||
VertexAttribFormat(0, 4, GL_FLOAT, GL_FALSE, 0);
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR) << "glVertexAttribFormat under relaxed semantics";
|
|
||||||
VertexAttribBinding(0, 0);
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR) << "glVertexAttribBinding under relaxed semantics";
|
|
||||||
VertexBindingDivisor(0, 1);
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR) << "glVertexBindingDivisor under relaxed semantics";
|
|
||||||
|
|
||||||
BindVertexArray(m_vao);
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
// MOBILEGL_RELAXED_SEMANTICS wins even on an explicit core-profile context.
|
|
||||||
TEST_F(VertexAttribBindingStateTest, RelaxedSemanticsOverrideReopensTheDefaultVertexArray) {
|
|
||||||
ScopedCoreProfileContext coreContext;
|
|
||||||
const Bool saved = MG_Config::Features.RelaxedSemantics;
|
|
||||||
MG_Config::Features.RelaxedSemantics = true;
|
|
||||||
const GLuint vbo = CreateVbo(1024);
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
BindVertexArray(0);
|
|
||||||
BindVertexBuffer(0, vbo, 0, 12);
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
|
|
||||||
BindVertexArray(m_vao);
|
|
||||||
MG_Config::Features.RelaxedSemantics = saved;
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
@@ -12,8 +12,14 @@
|
|||||||
#include <asio/post.hpp>
|
#include <asio/post.hpp>
|
||||||
#include <asio/thread_pool.hpp>
|
#include <asio/thread_pool.hpp>
|
||||||
|
|
||||||
|
#include <libfork/core.hpp>
|
||||||
|
#include <libfork/schedule/lazy_pool.hpp>
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
|
#include <functional>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
namespace MobileGL::MG_Util::Async {
|
namespace MobileGL::MG_Util::Async {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -54,7 +60,9 @@ namespace MobileGL::MG_Util::Async {
|
|||||||
// calling eglTerminate, which is the norm for a test binary and legal
|
// calling eglTerminate, which is the norm for a test binary and legal
|
||||||
// for an application. Registered here, during main, so it runs before
|
// for an application. Registered here, during main, so it runs before
|
||||||
// the destructors of statics constructed at load time.
|
// the destructors of statics constructed at load time.
|
||||||
ShaderCompilePool::StopAndDrainProcessPoolAtExit();
|
if (ShaderCompilePool* pool = g_processPool.load(std::memory_order_acquire)) {
|
||||||
|
pool->StopAndDrain();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -124,15 +132,6 @@ namespace MobileGL::MG_Util::Async {
|
|||||||
return AsyncShaderCompileEnabled() && !IsAsyncShaderCompileSuspended();
|
return AsyncShaderCompileEnabled() && !IsAsyncShaderCompileSuspended();
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool OptimisticShaderStatusActive() {
|
|
||||||
switch (MG_Config::Features.AsyncOptimisticShaderStatus) {
|
|
||||||
case MG_Config::QuirkOverride::ForceOn: return AsyncShaderCompileActive();
|
|
||||||
case MG_Config::QuirkOverride::ForceOff: return false;
|
|
||||||
case MG_Config::QuirkOverride::Auto: break;
|
|
||||||
}
|
|
||||||
return kOptimisticShaderStatusDefault && AsyncShaderCompileActive();
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint DetectShaderCompileThreadCount() {
|
Uint DetectShaderCompileThreadCount() {
|
||||||
if (const Uint32 configured = MG_Config::Features.AsyncShaderCompileThreads; configured > 0) {
|
if (const Uint32 configured = MG_Config::Features.AsyncShaderCompileThreads; configured > 0) {
|
||||||
// An explicit request is honoured as given - it is the escape hatch for measuring
|
// An explicit request is honoured as given - it is the escape hatch for measuring
|
||||||
@@ -142,46 +141,506 @@ namespace MobileGL::MG_Util::Async {
|
|||||||
return std::clamp(DetectBigCoreCount(), 1u, kMaxAutoShaderCompileThreads);
|
return std::clamp(DetectBigCoreCount(), 1u, kMaxAutoShaderCompileThreads);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Engine selection -----------------------------------------------------------------
|
||||||
|
|
||||||
|
const char* AsyncPoolEngineName(const AsyncPoolEngine engine) {
|
||||||
|
switch (engine) {
|
||||||
|
case AsyncPoolEngine::Libfork: return "libfork";
|
||||||
|
case AsyncPoolEngine::Asio: break;
|
||||||
|
}
|
||||||
|
return "asio";
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncPoolEngine ParseAsyncPoolEngine(const String& value) {
|
||||||
|
String lowered = value;
|
||||||
|
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
|
||||||
|
[](const unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
if (lowered == "libfork") return AsyncPoolEngine::Libfork;
|
||||||
|
if (lowered == "asio" || lowered == "auto" || lowered.empty()) return AsyncPoolEngine::Asio;
|
||||||
|
// Not silent: a misspelt engine name resolving to the default would be
|
||||||
|
// indistinguishable from not having set the variable at all, and the only reason to
|
||||||
|
// set it is to know which engine ran.
|
||||||
|
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_ASYNC_POOL='%s'; expected asio|libfork, "
|
||||||
|
"using asio",
|
||||||
|
value.c_str());
|
||||||
|
return AsyncPoolEngine::Asio;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncPoolEngine DetectAsyncPoolEngine() {
|
||||||
|
// A live std::getenv rather than an MG_Config::Features mirror, and deliberately so:
|
||||||
|
// a ShaderCompilePool is constructed by binaries that never call MobileGL::Initialize()
|
||||||
|
// and therefore never run MG_ConfigLoader::Init() - MG_Test/Util/JobNodeTest builds
|
||||||
|
// pools directly, and it is the suite that exercises the engines against each other.
|
||||||
|
// Reading Features there would silently resolve to the default and the libfork half of
|
||||||
|
// the test matrix would prove nothing. See the exemption list in Config.h.
|
||||||
|
//
|
||||||
|
// Resolved once per process (a function-local static): every pool in a process gets
|
||||||
|
// the same engine, so a process can never end up running two.
|
||||||
|
static const AsyncPoolEngine engine = [] {
|
||||||
|
const char* value = std::getenv("MOBILEGL_ASYNC_POOL");
|
||||||
|
const AsyncPoolEngine resolved = ParseAsyncPoolEngine(value != nullptr ? String(value) : String());
|
||||||
|
if (resolved != AsyncPoolEngine::Asio) {
|
||||||
|
MGLOG_I("ShaderCompilePool: MOBILEGL_ASYNC_POOL selected the %s execution engine",
|
||||||
|
AsyncPoolEngineName(resolved));
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}();
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// ---- The engine boundary ----------------------------------------------------------
|
||||||
|
// Submit() has exactly asio::post's contract, and ShaderCompilePool::Impl leans on all
|
||||||
|
// four halves of it:
|
||||||
|
// * it NEVER runs `fn` on the calling thread. DispatchLocked calls it while holding
|
||||||
|
// the pool's plain, non-recursive mutex, and a job body (or a terminal
|
||||||
|
// continuation it releases) is free to call Post() again - an inline run would
|
||||||
|
// deadlock on the lock this frame already owns.
|
||||||
|
// * it is callable from ANY thread, a worker of this very pool included:
|
||||||
|
// ProgramLinkTask::OnDepSettled posts the link job from whichever thread drove the
|
||||||
|
// last compile terminal, which is a worker.
|
||||||
|
// * it may throw, and when it does it must not have consumed the caller's job node,
|
||||||
|
// so Post/DispatchLocked can settle the node instead of stranding it Pending with
|
||||||
|
// a joiner blocked forever.
|
||||||
|
// * once it has accepted `fn`, `fn` WILL run. A dropped callable is a node nothing
|
||||||
|
// ever settles, so the engines run it themselves rather than discard it.
|
||||||
|
class JobExecutor {
|
||||||
|
public:
|
||||||
|
virtual ~JobExecutor() = default;
|
||||||
|
JobExecutor() = default;
|
||||||
|
JobExecutor(const JobExecutor&) = delete;
|
||||||
|
JobExecutor& operator=(const JobExecutor&) = delete;
|
||||||
|
|
||||||
|
virtual void Submit(std::function<void()> fn) = 0;
|
||||||
|
|
||||||
|
// Returns once every callable ever handed to Submit has finished running. The
|
||||||
|
// guarantee StopAndDrain sells to library teardown: after it returns, no worker is
|
||||||
|
// still inside a job body that could touch glslang's process globals.
|
||||||
|
virtual void JoinAll() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Engine 1: Asio (the shipped default) -----------------------------------------
|
||||||
|
class AsioJobExecutor final : public JobExecutor {
|
||||||
|
public:
|
||||||
|
explicit AsioJobExecutor(const Uint threads) : m_pool(threads) {}
|
||||||
|
|
||||||
|
// asio::post only enqueues; it never runs the handler on the calling thread, which
|
||||||
|
// is what makes calling it under the pool mutex safe.
|
||||||
|
void Submit(std::function<void()> fn) override { asio::post(m_pool, Move(fn)); }
|
||||||
|
|
||||||
|
void JoinAll() override { m_pool.join(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
asio::thread_pool m_pool;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Engine 2: libfork ------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// libfork is a continuation-stealing fork-join runtime, and the shape that fits here is
|
||||||
|
// NOT fork-join: a job body is one coarse, blocking, non-forking unit (a glslang
|
||||||
|
// compile), and the concurrency budget that bounds peak RSS is Impl's, not the
|
||||||
|
// scheduler's. So libfork is used as a job executor - each dispatched job is a detached
|
||||||
|
// root task - and what it is being asked to beat is Asio's single scheduler queue with
|
||||||
|
// its per-worker work-stealing deques and sleeping workers.
|
||||||
|
//
|
||||||
|
// The one thing libfork forbids is the thing this pool does constantly: lf::schedule
|
||||||
|
// (which lf::detach is built on) THROWS lf::schedule_in_worker when the calling thread
|
||||||
|
// is a libfork worker, because workers may never block. Yet a worker submits on every
|
||||||
|
// job completion - RunOnWorker's tail refills the budget - and again whenever a
|
||||||
|
// terminal continuation posts (ProgramLinkTask::OnDepSettled). Routing those through a
|
||||||
|
// separate dispatch thread works but costs two thread wakeups per job, which measured
|
||||||
|
// 4x worse than Asio on short jobs. So instead a dispatched root is a CHAIN: when its
|
||||||
|
// body returns it takes the next queued job itself and runs it in the same coroutine
|
||||||
|
// on the same worker. The refill a worker submits is therefore absorbed by the very
|
||||||
|
// chain that submitted it - no scheduler round trip, no wakeup - and libfork is only
|
||||||
|
// entered for work that arrives from outside the pool.
|
||||||
|
//
|
||||||
|
// Absorption is bounded at one job per running chain, though, because a chain is one
|
||||||
|
// worker: past that bound the queue would be jobs the budget has already cleared,
|
||||||
|
// waiting behind each other on a single thread. See Submit.
|
||||||
|
//
|
||||||
|
// Why none of this can strand a job: the queue below is only ever added to from inside
|
||||||
|
// a running chain (tl_chainOwner == this), and a chain exits only when it finds the
|
||||||
|
// queue empty - unconditionally, whatever the bound says. Every other submitter goes
|
||||||
|
// to the dispatch thread or straight to lf::detach.
|
||||||
|
class LibforkJobExecutor;
|
||||||
|
|
||||||
|
// Which executor's chain, if any, is running on this thread. Deliberately narrower
|
||||||
|
// than ShaderCompilePool::IsPoolThread(): that flag is process-wide and latched
|
||||||
|
// forever, so a worker of a DIFFERENT pool would read as "mine" and queue a job into a
|
||||||
|
// chain that will never drain it. This says exactly "a chain of *this* executor is
|
||||||
|
// executing on this thread, and it will look at the queue again before it exits".
|
||||||
|
thread_local LibforkJobExecutor* tl_chainOwner = nullptr;
|
||||||
|
|
||||||
|
// One dispatched job, heap-owned. It reaches its coroutine as a POINTER passed BY
|
||||||
|
// VALUE: libfork forwards a root task's arguments into the coroutine frame, so a
|
||||||
|
// by-value pointer is copied into the frame, whereas anything passed by reference
|
||||||
|
// would dangle the moment lf::detach returns - and detach, unlike sync_wait, does not
|
||||||
|
// outlive the task.
|
||||||
|
struct LibforkJob {
|
||||||
|
std::function<void()> body;
|
||||||
|
LibforkJobExecutor* owner;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A scheduler adaptor for lf::detach: it places external submissions round-robin over
|
||||||
|
// lf::lazy_pool's worker contexts instead of letting the pool pick one at random.
|
||||||
|
// Both reasons are load-bearing, and the second was worth 1.3x at a budget equal to
|
||||||
|
// the worker count - the configuration MobileGL actually ships, since maxConcurrency
|
||||||
|
// is clamped to the thread count:
|
||||||
|
// * lf::lazy_pool::schedule chooses its victim with a
|
||||||
|
// std::uniform_int_distribution over a lazy_pool-member xoshiro generator -
|
||||||
|
// unsynchronized mutable state, so two concurrent submissions are a data race
|
||||||
|
// inside libfork itself. An atomic cursor is not.
|
||||||
|
// * A worker's SUBMISSION list is drained only by that worker
|
||||||
|
// (worker_context::try_pop_all is documented "for use only by the owning worker
|
||||||
|
// thread"); a thief takes from the task deque, which is a different queue. So a
|
||||||
|
// job placed on a worker that is inside a long blocking body waits for that body
|
||||||
|
// rather than being stolen - and random placement of `budget` submissions over
|
||||||
|
// `budget` workers collides by the birthday rule. Round-robin lands the GL
|
||||||
|
// thread's burst one per worker, which is exactly the intended shape.
|
||||||
|
struct RoundRobinSubmitter {
|
||||||
|
std::span<lf::worker_context*> contexts;
|
||||||
|
std::atomic<Uint64>* cursor;
|
||||||
|
|
||||||
|
void schedule(const lf::submit_handle job) const {
|
||||||
|
const Uint64 index = cursor->fetch_add(1, std::memory_order_relaxed);
|
||||||
|
contexts[static_cast<SizeT>(index % contexts.size())]->schedule(job);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void RunLibforkChain(LibforkJob* raw) noexcept;
|
||||||
|
|
||||||
|
// The root task every dispatched chain runs as. libfork async function objects are
|
||||||
|
// copyable, captureless callables returning lf::task<>, whose first parameter is the
|
||||||
|
// combinator's synthesized first argument (unused here: this task neither forks nor
|
||||||
|
// joins). The coroutine exists purely as libfork's entry protocol; the loop is in
|
||||||
|
// RunLibforkChain.
|
||||||
|
inline constexpr auto kLibforkChainTask = [](auto /*self*/, LibforkJob* job) -> lf::task<void> {
|
||||||
|
RunLibforkChain(job);
|
||||||
|
co_return;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LibforkJobExecutor final : public JobExecutor {
|
||||||
|
public:
|
||||||
|
explicit LibforkJobExecutor(const Uint threads)
|
||||||
|
: m_pool(static_cast<std::size_t>(std::max(1u, threads))), m_contexts(m_pool.contexts()),
|
||||||
|
m_fallback([this] { FallbackLoop(); }) {}
|
||||||
|
|
||||||
|
~LibforkJobExecutor() override {
|
||||||
|
JoinAll();
|
||||||
|
{
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
m_fallbackStop = true;
|
||||||
|
}
|
||||||
|
m_fallbackCv.notify_all();
|
||||||
|
if (m_fallback.joinable()) m_fallback.join();
|
||||||
|
// m_pool is destroyed last, and only here: lf::lazy_pool may not be destructed
|
||||||
|
// while any submitted task can still run or submit more. JoinAll() has
|
||||||
|
// established the first and the joined fallback thread the second. Its
|
||||||
|
// destructor then joins the worker threads, so a worker still unwinding a
|
||||||
|
// finished coroutine frame is waited for rather than pulled out from under.
|
||||||
|
}
|
||||||
|
|
||||||
|
void Submit(std::function<void()> fn) override {
|
||||||
|
if (tl_chainOwner == this) {
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
// The hot path: ONE job per running chain. A chain picks up exactly one
|
||||||
|
// queued job each time its body returns, so a queue no longer than the
|
||||||
|
// number of live chains is a queue every entry of which has a distinct
|
||||||
|
// worker waiting to take it - which is precisely the steady state this
|
||||||
|
// absorption exists for (every worker finishes a job and refills its own
|
||||||
|
// slot, all at once, with no scheduler round trip between them).
|
||||||
|
//
|
||||||
|
// Past that it is oversubscription, and absorbing it would be a
|
||||||
|
// correctness-preserving way to destroy the pool's parallelism: the
|
||||||
|
// budget would still say `maxConcurrency` jobs are in flight while one
|
||||||
|
// worker ran them one behind another. That is not hypothetical - it is
|
||||||
|
// the tail of a pack load, where one compile going terminal releases
|
||||||
|
// several programs at once (ShaderCompileAdoptionMap lets a single
|
||||||
|
// compile settle many) and the worker that drove it posts the whole
|
||||||
|
// burst into an otherwise idle pool. Measured before this branch existed:
|
||||||
|
// four such jobs took 4x one job's wall time on libfork and 1x on Asio.
|
||||||
|
//
|
||||||
|
// The overflow cannot go to lf::detach from here - a libfork worker may
|
||||||
|
// not schedule - so it goes to the dispatch thread, which detaches it to
|
||||||
|
// a worker of its own. That costs one thread wakeup; serializing costs a
|
||||||
|
// whole compile.
|
||||||
|
//
|
||||||
|
// The count is taken AFTER the push, not before: deque::push_back is
|
||||||
|
// strongly exception-safe, so an allocation failure here leaves `fn`
|
||||||
|
// intact for DispatchLocked to settle - but a count incremented in front
|
||||||
|
// of it would be a count nothing ever gives back, and JoinAll would wait
|
||||||
|
// on it forever.
|
||||||
|
const Bool takeable = m_chainQueue.size() < m_liveChains;
|
||||||
|
if (takeable) {
|
||||||
|
m_chainQueue.push_back(Move(fn));
|
||||||
|
++m_outstanding;
|
||||||
|
} else {
|
||||||
|
m_fallbackQueue.push_back(Move(fn));
|
||||||
|
++m_outstanding;
|
||||||
|
m_fallbackCv.notify_one();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Counted before anything can run it, so JoinAll cannot observe a zero
|
||||||
|
// that this job would have broken.
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
++m_outstanding;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
DetachChain(Move(fn));
|
||||||
|
} catch (const lf::schedule_in_worker&) {
|
||||||
|
// Submitted from a libfork worker that is not running one of my chains -
|
||||||
|
// a worker of another ShaderCompilePool. libfork will not take a
|
||||||
|
// submission from there at all, and the queue above is not safe for it
|
||||||
|
// (no chain of mine is running on that thread to drain it), so it goes to
|
||||||
|
// the fallback thread, which is neither. DetachChain restored `fn` before
|
||||||
|
// it threw.
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
m_fallbackQueue.push_back(Move(fn));
|
||||||
|
m_fallbackCv.notify_one();
|
||||||
|
} catch (...) {
|
||||||
|
// Out of memory. Give the count back and let the caller settle its node:
|
||||||
|
// that is Submit's contract and what DispatchLocked is written against.
|
||||||
|
Retire();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void JoinAll() override {
|
||||||
|
std::unique_lock<std::mutex> lock(m_mutex);
|
||||||
|
m_idleCv.wait(lock, [this] { return m_outstanding == 0; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// A chain announces itself before it runs its first body, so that Submit's
|
||||||
|
// absorption rule can count the workers that are going to come back and ask for
|
||||||
|
// more. Under-counting is the only direction this can be wrong in (a detached
|
||||||
|
// chain is not counted until it starts), and under-counting only sends work to
|
||||||
|
// the dispatch thread that a chain could have taken - never the reverse.
|
||||||
|
void EnterChain() noexcept {
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
++m_liveChains;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The end of one job in a chain. Returns true having loaded `body` with the next
|
||||||
|
// job to run on this same worker, false when there is nothing left - after which
|
||||||
|
// the caller must touch neither `this` nor anything owned by it, because the
|
||||||
|
// count this drops to zero may be the one JoinAll is waiting for.
|
||||||
|
//
|
||||||
|
// `body` must arrive empty: the finished job's captures (a strong reference to its
|
||||||
|
// JobNode) are released by the chain, outside this lock, so that no JobNode
|
||||||
|
// destructor ever runs inside the executor's critical section.
|
||||||
|
Bool RetireAndTakeNext(std::function<void()>& body) noexcept {
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
--m_outstanding;
|
||||||
|
if (!m_chainQueue.empty()) {
|
||||||
|
// Unconditional, and it has to stay that way: a chain that exited while
|
||||||
|
// the queue was non-empty could be the last one, and the entry would then
|
||||||
|
// be waiting on a worker that never comes. That is what makes the
|
||||||
|
// absorption bound in Submit a scheduling policy rather than a liveness
|
||||||
|
// requirement.
|
||||||
|
//
|
||||||
|
// swap, not move-assign: std::function's move assignment is not noexcept,
|
||||||
|
// and this function is.
|
||||||
|
body.swap(m_chainQueue.front());
|
||||||
|
m_chainQueue.pop_front();
|
||||||
|
return true; // the taken job's own count stays held
|
||||||
|
}
|
||||||
|
--m_liveChains;
|
||||||
|
// Notified while STILL HOLDING the lock, which is the whole reason this is not
|
||||||
|
// the usual notify-after-unlock. The wakeup this sends can be the one that
|
||||||
|
// lets JoinAll return and ~LibforkJobExecutor destroy m_idleCv - and a
|
||||||
|
// std::condition_variable may not be destroyed while another thread is inside
|
||||||
|
// notify_all() on it. Holding the lock across the notify means the waiter
|
||||||
|
// cannot re-acquire the mutex, and therefore cannot leave wait(), until this
|
||||||
|
// thread is out of both the notify and the unlock. ThreadSanitizer catches the
|
||||||
|
// other order immediately (pthread_cond_destroy vs pthread_cond_broadcast).
|
||||||
|
if (m_outstanding == 0) m_idleCv.notify_all();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Builds the root task and hands it to libfork. On any failure `fn` is restored,
|
||||||
|
// so the caller can still decide what to do with the job.
|
||||||
|
void DetachChain(std::function<void()>&& fn) {
|
||||||
|
// `new T{...}` allocates before it constructs, so a throwing operator new
|
||||||
|
// leaves `fn` untouched; the member move is std::function's noexcept one.
|
||||||
|
LibforkJob* job = new LibforkJob{Move(fn), this};
|
||||||
|
try {
|
||||||
|
lf::detach(RoundRobinSubmitter{m_contexts, &m_cursor}, kLibforkChainTask, job);
|
||||||
|
} catch (...) {
|
||||||
|
// lf::schedule upholds the strong exception guarantee, so nothing was
|
||||||
|
// scheduled and the payload is still ours.
|
||||||
|
const UniquePtr<LibforkJob> owned(job);
|
||||||
|
fn = Move(owned->body);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Retire() noexcept {
|
||||||
|
// Under the lock, for the reason RetireAndTakeNext spells out.
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
if (--m_outstanding == 0) m_idleCv.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The dispatch thread. It exists because lf::detach is illegal on a libfork worker
|
||||||
|
// and legal here, and it serves the two cases Submit cannot take itself: a
|
||||||
|
// submission from another pool's worker, and a chain's overflow past the
|
||||||
|
// one-job-per-chain bound. It sleeps otherwise, and it dispatches rather than
|
||||||
|
// executes - a body only ever runs here if libfork refuses the job outright.
|
||||||
|
void FallbackLoop() {
|
||||||
|
for (;;) {
|
||||||
|
std::function<void()> fn;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(m_mutex);
|
||||||
|
m_fallbackCv.wait(lock, [this] { return !m_fallbackQueue.empty() || m_fallbackStop; });
|
||||||
|
// Emptiness is checked before the stop flag so that a stop can never
|
||||||
|
// strand accepted work: an accepted job always runs, because the node
|
||||||
|
// behind it has a joiner that would otherwise block forever.
|
||||||
|
if (m_fallbackQueue.empty()) return;
|
||||||
|
fn.swap(m_fallbackQueue.front());
|
||||||
|
m_fallbackQueue.pop_front();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
DetachChain(Move(fn));
|
||||||
|
} catch (...) {
|
||||||
|
MGLOG_E("ShaderCompilePool: libfork refused a fallback dispatch; running the job on "
|
||||||
|
"the dispatch thread instead of dropping it");
|
||||||
|
RunHere(Move(fn));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last resort. Running the body here costs this engine its parallelism for one
|
||||||
|
// job; dropping it would cost a joiner its wakeup forever.
|
||||||
|
void RunHere(std::function<void()>&& fn) noexcept {
|
||||||
|
try {
|
||||||
|
if (fn) fn();
|
||||||
|
} catch (...) {
|
||||||
|
MGLOG_E("ShaderCompilePool: a job body escaped its own containment on the dispatch "
|
||||||
|
"thread; it has been swallowed to keep the thread alive");
|
||||||
|
}
|
||||||
|
fn = nullptr;
|
||||||
|
Retire();
|
||||||
|
}
|
||||||
|
|
||||||
|
lf::lazy_pool m_pool;
|
||||||
|
// Fixed for the pool's lifetime, so it is read once rather than per submission.
|
||||||
|
std::span<lf::worker_context*> m_contexts;
|
||||||
|
std::atomic<Uint64> m_cursor{0};
|
||||||
|
|
||||||
|
std::mutex m_mutex;
|
||||||
|
std::condition_variable m_fallbackCv;
|
||||||
|
std::condition_variable m_idleCv;
|
||||||
|
// Refills and continuations submitted from inside a chain: drained by the chains.
|
||||||
|
std::deque<std::function<void()>> m_chainQueue;
|
||||||
|
// Chains currently executing, i.e. workers that will look at m_chainQueue again
|
||||||
|
// before they exit. The bound on how much Submit may absorb into a chain.
|
||||||
|
Uint m_liveChains = 0;
|
||||||
|
// Submissions from another pool's libfork worker, and the overflow of the rule
|
||||||
|
// above: drained by m_fallback, which detaches each one to a worker.
|
||||||
|
std::deque<std::function<void()>> m_fallbackQueue;
|
||||||
|
// Everything submitted and not yet finished, whichever queue it is in and whether
|
||||||
|
// or not it has reached a worker, so JoinAll needs a single predicate.
|
||||||
|
Uint m_outstanding = 0;
|
||||||
|
Bool m_fallbackStop = false;
|
||||||
|
std::thread m_fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
void RunLibforkChain(LibforkJob* const raw) noexcept {
|
||||||
|
UniquePtr<LibforkJob> job(raw);
|
||||||
|
LibforkJobExecutor* const owner = job->owner;
|
||||||
|
std::function<void()> body;
|
||||||
|
body.swap(job->body);
|
||||||
|
job.reset();
|
||||||
|
|
||||||
|
LibforkJobExecutor* const savedOwner = tl_chainOwner;
|
||||||
|
tl_chainOwner = owner;
|
||||||
|
owner->EnterChain();
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
try {
|
||||||
|
if (body) body();
|
||||||
|
} catch (...) {
|
||||||
|
// JobNode::Run contains every body exception already; this is the backstop
|
||||||
|
// for the wrapper itself. An exception escaping here would be stashed in
|
||||||
|
// the root task's shared state, which lf::detach discards - i.e. silently
|
||||||
|
// lost - and would abandon the rest of the chain.
|
||||||
|
MGLOG_E("ShaderCompilePool: a job body escaped its own containment on a libfork worker; "
|
||||||
|
"it has been swallowed to keep the chain alive");
|
||||||
|
}
|
||||||
|
// Release the finished job's captures (its strong JobNode reference) HERE,
|
||||||
|
// outside the executor's lock: a JobNode destructor is arbitrary code.
|
||||||
|
body = nullptr;
|
||||||
|
if (!owner->RetireAndTakeNext(body)) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `owner` may already be destroyed - RetireAndTakeNext returning false can be the
|
||||||
|
// call that releases a JoinAll. Nothing below touches it.
|
||||||
|
tl_chainOwner = savedOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
UniquePtr<JobExecutor> MakeJobExecutor(const AsyncPoolEngine engine, const Uint threads) {
|
||||||
|
switch (engine) {
|
||||||
|
case AsyncPoolEngine::Libfork: return MakeUnique<LibforkJobExecutor>(threads);
|
||||||
|
case AsyncPoolEngine::Asio: break;
|
||||||
|
}
|
||||||
|
return MakeUnique<AsioJobExecutor>(threads);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
struct ShaderCompilePool::Impl {
|
struct ShaderCompilePool::Impl {
|
||||||
explicit Impl(const Uint threads) : threadCount(std::max(1u, threads)), maxConcurrency(threadCount) {}
|
explicit Impl(const Uint threads)
|
||||||
|
: threadCount(std::max(1u, threads)), engine(DetectAsyncPoolEngine()), maxConcurrency(threadCount) {}
|
||||||
|
|
||||||
const Uint threadCount;
|
const Uint threadCount;
|
||||||
|
// Latched at construction, not re-read: a pool may not change engines under its own
|
||||||
|
// workers, and GetEngine() is what the tests compare against the environment.
|
||||||
|
const AsyncPoolEngine engine;
|
||||||
|
|
||||||
std::mutex mutex;
|
std::mutex mutex;
|
||||||
// Created on the first dispatched Post, never in the constructor: asio::thread_pool
|
// Created on the first dispatched Post, never in the constructor: both engines spawn
|
||||||
// spawns its threads eagerly, and a build with async off must not pay for threads it
|
// their threads eagerly (asio::thread_pool its workers, lf::lazy_pool its workers plus
|
||||||
|
// this file's dispatch thread), and a build with async off must not pay for threads it
|
||||||
// will never use.
|
// will never use.
|
||||||
UniquePtr<asio::thread_pool> pool;
|
UniquePtr<JobExecutor> executor;
|
||||||
std::deque<SharedPtr<JobNode>> queue;
|
std::deque<SharedPtr<JobNode>> queue;
|
||||||
Uint inFlight = 0;
|
Uint inFlight = 0;
|
||||||
Uint maxConcurrency;
|
Uint maxConcurrency;
|
||||||
std::atomic<Bool> stopped{false};
|
std::atomic<Bool> stopped{false};
|
||||||
|
|
||||||
// Callers hold `mutex`. Hands as many queued nodes to Asio as the concurrency budget
|
// Callers hold `mutex`. Hands as many queued nodes to the engine as the concurrency
|
||||||
// allows. Posting under the lock is safe and is what keeps `pool` from being moved
|
// budget allows. Submitting under the lock is safe and is what keeps `executor` from
|
||||||
// out by a concurrent StopAndDrain between the decision and the dispatch: asio::post
|
// being moved out by a concurrent StopAndDrain between the decision and the dispatch:
|
||||||
// only enqueues, it never runs the handler on the calling thread, so it cannot
|
// Submit only enqueues, it never runs the callable on the calling thread, so it cannot
|
||||||
// re-enter this mutex.
|
// re-enter this mutex.
|
||||||
//
|
//
|
||||||
// A node asio::post fails to hand off is appended to `toCancel` instead of being
|
// A node the engine fails to accept is appended to `toCancel` instead of being
|
||||||
// Cancel()'d here: Cancel() runs the node's OnTerminal continuations inline (stage 4
|
// Cancel()'d here: Cancel() runs the node's OnTerminal continuations inline (stage 4
|
||||||
// added ProgramLinkTask::OnDepSettled as a real one), and a continuation is free to
|
// added ProgramLinkTask::OnDepSettled as a real one), and a continuation is free to
|
||||||
// call ShaderCompilePool::Post() again. Every caller of DispatchLocked holds `mutex`
|
// call ShaderCompilePool::Post() again. Every caller of DispatchLocked holds `mutex`
|
||||||
// (a plain, non-recursive std::mutex) - Cancel()'ing in here would let that
|
// (a plain, non-recursive std::mutex) - Cancel()'ing in here would let that
|
||||||
// re-entrant Post() deadlock on the very lock this frame already owns. The caller
|
// re-entrant Post() deadlock on the very lock this frame already owns. The caller
|
||||||
// drains `toCancel` after releasing the lock.
|
// drains `toCancel` after releasing the lock.
|
||||||
|
//
|
||||||
|
// The `stopped` check is also what keeps this loop from dereferencing a null
|
||||||
|
// `executor`: StopAndDrain sets the flag and moves the executor out in the same
|
||||||
|
// critical section, so a stopped pool never reaches the Submit below.
|
||||||
void DispatchLocked(Vector<SharedPtr<JobNode>>& toCancel) {
|
void DispatchLocked(Vector<SharedPtr<JobNode>>& toCancel) {
|
||||||
while (!queue.empty() && inFlight < maxConcurrency && !stopped.load(std::memory_order_acquire)) {
|
while (!queue.empty() && inFlight < maxConcurrency && !stopped.load(std::memory_order_acquire)) {
|
||||||
// Copy rather than move into the handler: if asio::post throws (it allocates)
|
// Copy rather than move into the callable: if Submit throws (both engines
|
||||||
// the local SharedPtr is still valid, so the node can be settled instead of
|
// allocate) the local SharedPtr is still valid, so the node can be settled
|
||||||
// being stranded Pending in a queue nothing will dispatch from again - a
|
// instead of being stranded Pending in a queue nothing will dispatch from
|
||||||
// joiner would block on it forever. Reclaiming the slot matters just as much:
|
// again - a joiner would block on it forever. Reclaiming the slot matters just
|
||||||
// a leaked `inFlight` shrinks the pool's concurrency budget permanently.
|
// as much: a leaked `inFlight` shrinks the pool's concurrency budget
|
||||||
|
// permanently.
|
||||||
SharedPtr<JobNode> node = queue.front();
|
SharedPtr<JobNode> node = queue.front();
|
||||||
queue.pop_front();
|
queue.pop_front();
|
||||||
++inFlight;
|
++inFlight;
|
||||||
try {
|
try {
|
||||||
asio::post(*pool, [this, node]() mutable { RunOnWorker(Move(node)); });
|
executor->Submit([this, node]() mutable { RunOnWorker(Move(node)); });
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
--inFlight;
|
--inFlight;
|
||||||
toCancel.push_back(Move(node));
|
toCancel.push_back(Move(node));
|
||||||
@@ -191,7 +650,7 @@ namespace MobileGL::MG_Util::Async {
|
|||||||
|
|
||||||
void RunOnWorker(SharedPtr<JobNode> node) {
|
void RunOnWorker(SharedPtr<JobNode> node) {
|
||||||
tl_isPoolThread = true;
|
tl_isPoolThread = true;
|
||||||
// A node that was already handed to Asio when StopAndDrain ran still arrives
|
// A node that was already handed to the engine when StopAndDrain ran still arrives
|
||||||
// here; cancelling it first turns the dispatch into a state transition instead of
|
// here; cancelling it first turns the dispatch into a state transition instead of
|
||||||
// a full compile, so the drain's join() returns promptly. This Cancel() runs
|
// a full compile, so the drain's join() returns promptly. This Cancel() runs
|
||||||
// before `mutex` is ever taken in this frame, so it is not subject to the
|
// before `mutex` is ever taken in this frame, so it is not subject to the
|
||||||
@@ -241,13 +700,15 @@ namespace MobileGL::MG_Util::Async {
|
|||||||
return m_impl->maxConcurrency;
|
return m_impl->maxConcurrency;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AsyncPoolEngine ShaderCompilePool::GetEngine() const { return m_impl->engine; }
|
||||||
|
|
||||||
void ShaderCompilePool::SetMaxConcurrency(const Uint n) {
|
void ShaderCompilePool::SetMaxConcurrency(const Uint n) {
|
||||||
Vector<SharedPtr<JobNode>> toCancel;
|
Vector<SharedPtr<JobNode>> toCancel;
|
||||||
{
|
{
|
||||||
const std::lock_guard<std::mutex> lock(m_impl->mutex);
|
const std::lock_guard<std::mutex> lock(m_impl->mutex);
|
||||||
m_impl->maxConcurrency = std::clamp(n, 1u, m_impl->threadCount);
|
m_impl->maxConcurrency = std::clamp(n, 1u, m_impl->threadCount);
|
||||||
// Raising the budget releases whatever the old one was holding back.
|
// Raising the budget releases whatever the old one was holding back.
|
||||||
if (m_impl->pool) m_impl->DispatchLocked(toCancel);
|
if (m_impl->executor) m_impl->DispatchLocked(toCancel);
|
||||||
}
|
}
|
||||||
// Outside the lock: see DispatchLocked's comment.
|
// Outside the lock: see DispatchLocked's comment.
|
||||||
for (const auto& n2 : toCancel) {
|
for (const auto& n2 : toCancel) {
|
||||||
@@ -259,23 +720,23 @@ namespace MobileGL::MG_Util::Async {
|
|||||||
if (!node) return;
|
if (!node) return;
|
||||||
EnsureProcessTeardownSentinel();
|
EnsureProcessTeardownSentinel();
|
||||||
|
|
||||||
// Enqueueing can throw: the thread_pool construction and asio::post both allocate,
|
// Enqueueing can throw: building the engine and submitting to it both allocate (and
|
||||||
// and under memory pressure a throw here would escape glCompileShader leaving the
|
// both spawn threads), and under memory pressure a throw here would escape
|
||||||
// node Pending with nothing left to dispatch it - the first observable read would
|
// glCompileShader leaving the node Pending with nothing left to dispatch it - the
|
||||||
// then block the GL thread forever. Settle the node instead: a cancelled node is a
|
// first observable read would then block the GL thread forever. Settle the node
|
||||||
// state every joiner already handles.
|
// instead: a cancelled node is a state every joiner already handles.
|
||||||
//
|
//
|
||||||
// `node` is still valid in the catch for every throw this try can produce. The
|
// `node` is still valid in the catch for every throw this try can produce. The engine
|
||||||
// thread_pool construction runs before the move; deque::push_back is strongly
|
// construction runs before the move; deque::push_back is strongly exception-safe and
|
||||||
// exception-safe and SharedPtr's move constructor is noexcept, so a throwing
|
// SharedPtr's move constructor is noexcept, so a throwing push_back never consumed it;
|
||||||
// push_back never consumed it; and DispatchLocked contains its own asio::post
|
// and DispatchLocked contains its own Submit failures rather than propagating them
|
||||||
// failures rather than propagating them (see above). Keep it that way.
|
// (see above). Keep it that way.
|
||||||
Bool enqueued = false;
|
Bool enqueued = false;
|
||||||
Vector<SharedPtr<JobNode>> toCancel;
|
Vector<SharedPtr<JobNode>> toCancel;
|
||||||
try {
|
try {
|
||||||
const std::lock_guard<std::mutex> lock(m_impl->mutex);
|
const std::lock_guard<std::mutex> lock(m_impl->mutex);
|
||||||
if (!m_impl->stopped.load(std::memory_order_acquire) && !InProcessTeardown()) {
|
if (!m_impl->stopped.load(std::memory_order_acquire) && !InProcessTeardown()) {
|
||||||
if (!m_impl->pool) m_impl->pool = MakeUnique<asio::thread_pool>(m_impl->threadCount);
|
if (!m_impl->executor) m_impl->executor = MakeJobExecutor(m_impl->engine, m_impl->threadCount);
|
||||||
m_impl->queue.push_back(Move(node));
|
m_impl->queue.push_back(Move(node));
|
||||||
m_impl->DispatchLocked(toCancel);
|
m_impl->DispatchLocked(toCancel);
|
||||||
enqueued = true;
|
enqueued = true;
|
||||||
@@ -311,17 +772,17 @@ namespace MobileGL::MG_Util::Async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ShaderCompilePool::StopAndDrain() {
|
void ShaderCompilePool::StopAndDrain() {
|
||||||
// asio::thread_pool::join() from a pool thread would deadlock on itself, and the
|
// Waiting for the workers from a worker would deadlock on itself (asio's join() says
|
||||||
// whole point of this call is that the GL thread waits for the workers.
|
// so outright), and the whole point of this call is that the GL thread waits.
|
||||||
MOBILEGL_ASSERT(!IsPoolThread(), "ShaderCompilePool::StopAndDrain() called from a pool thread");
|
MOBILEGL_ASSERT(!IsPoolThread(), "ShaderCompilePool::StopAndDrain() called from a pool thread");
|
||||||
|
|
||||||
std::deque<SharedPtr<JobNode>> abandoned;
|
std::deque<SharedPtr<JobNode>> abandoned;
|
||||||
UniquePtr<asio::thread_pool> pool;
|
UniquePtr<JobExecutor> executor;
|
||||||
{
|
{
|
||||||
const std::lock_guard<std::mutex> lock(m_impl->mutex);
|
const std::lock_guard<std::mutex> lock(m_impl->mutex);
|
||||||
m_impl->stopped.store(true, std::memory_order_release);
|
m_impl->stopped.store(true, std::memory_order_release);
|
||||||
abandoned.swap(m_impl->queue);
|
abandoned.swap(m_impl->queue);
|
||||||
pool = Move(m_impl->pool);
|
executor = Move(m_impl->executor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queued but never dispatched: settle them so anything chained behind them is
|
// Queued but never dispatched: settle them so anything chained behind them is
|
||||||
@@ -330,9 +791,9 @@ namespace MobileGL::MG_Util::Async {
|
|||||||
if (node) node->Cancel();
|
if (node) node->Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pool) {
|
if (executor) {
|
||||||
pool->join(); // returns once every handler already handed to Asio has finished
|
executor->JoinAll(); // returns once every job already handed to the engine is done
|
||||||
pool.reset();
|
executor.reset(); // and this stops the engine's threads
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::lock_guard<std::mutex> lock(m_impl->mutex);
|
const std::lock_guard<std::mutex> lock(m_impl->mutex);
|
||||||
@@ -343,10 +804,4 @@ namespace MobileGL::MG_Util::Async {
|
|||||||
// eglInitialize to get its worker threads back, the re-arm belongs in
|
// eglInitialize to get its worker threads back, the re-arm belongs in
|
||||||
// MobileGL::Initialize(), next to glslang::InitializeProcess().
|
// MobileGL::Initialize(), next to glslang::InitializeProcess().
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShaderCompilePool::StopAndDrainProcessPoolAtExit() {
|
|
||||||
if (ShaderCompilePool* pool = g_processPool.load(std::memory_order_acquire)) {
|
|
||||||
pool->StopAndDrain();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace MobileGL::MG_Util::Async
|
} // namespace MobileGL::MG_Util::Async
|
||||||
|
|||||||
@@ -11,11 +11,12 @@
|
|||||||
#include <MG_Util/Types.h>
|
#include <MG_Util/Types.h>
|
||||||
#include <MG_Util/Async/JobNode.h>
|
#include <MG_Util/Async/JobNode.h>
|
||||||
|
|
||||||
// This header deliberately includes NO Asio header: asio::thread_pool lives behind the pimpl
|
// This header deliberately includes NO Asio and NO libfork header: both execution engines
|
||||||
// in ShaderCompilePool.cpp. Asio stays a private implementation detail of one translation
|
// live behind the pimpl in ShaderCompilePool.cpp. They stay private implementation details of
|
||||||
// unit, so no consumer target (MG_Test, MG_IntegrationTest, MG_Benchmark - each with its own
|
// one translation unit, so no consumer target (MG_Test, MG_IntegrationTest, MG_Benchmark -
|
||||||
// target_include_directories) needs the Asio include path, and no consumer pays its compile
|
// each with its own target_include_directories) needs either include path, and no consumer
|
||||||
// time. Do not add one here.
|
// pays their compile time. libfork in particular is a C++20-coroutine header set whose
|
||||||
|
// instantiation cost nothing outside the pool has any reason to carry. Do not add one here.
|
||||||
|
|
||||||
namespace MobileGL::MG_Util::Async {
|
namespace MobileGL::MG_Util::Async {
|
||||||
// Stage 7: on by default. The gate behind the flip (2026-08-09, headless Mesa, both
|
// Stage 7: on by default. The gate behind the flip (2026-08-09, headless Mesa, both
|
||||||
@@ -59,21 +60,6 @@ namespace MobileGL::MG_Util::Async {
|
|||||||
// GL_COMPLETION_STATUS_KHR read immediately GL_TRUE.
|
// GL_COMPLETION_STATUS_KHR read immediately GL_TRUE.
|
||||||
Bool AsyncShaderCompileActive();
|
Bool AsyncShaderCompileActive();
|
||||||
|
|
||||||
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS (see Config.h): opt-in, off by default, and a
|
|
||||||
// spec violation by design - GL_COMPILE_STATUS and the shader info log answer
|
|
||||||
// optimistically while the compile job is in flight instead of joining it. Do not flip
|
|
||||||
// this default without an enumerated CTS delta: the compile-error-reporting cases WILL
|
|
||||||
// regress under it, deliberately.
|
|
||||||
inline constexpr Bool kOptimisticShaderStatusDefault = false;
|
|
||||||
|
|
||||||
// The one question the three optimistic getter sites ask. ANDed with
|
|
||||||
// AsyncShaderCompileActive() so that async-off (env kill switch) and
|
|
||||||
// glMaxShaderCompilerThreadsKHR(0) both switch the quirk off structurally: in those
|
|
||||||
// modes every compile settles before its enqueue returns, so a non-terminal node - the
|
|
||||||
// only state the quirk changes - cannot exist, and keeping the AND means there is no
|
|
||||||
// new mode interaction to reason about.
|
|
||||||
Bool OptimisticShaderStatusActive();
|
|
||||||
|
|
||||||
// min(4, big cores), where a big core is one whose cpufreq ceiling is within 15% of the
|
// min(4, big cores), where a big core is one whose cpufreq ceiling is within 15% of the
|
||||||
// machine maximum; the whole CPU count where that sysfs tree is absent. Clamped to [1, 4]
|
// machine maximum; the whole CPU count where that sysfs tree is absent. Clamped to [1, 4]
|
||||||
// because peak RSS scales as workers x largest glslang arena, and four
|
// because peak RSS scales as workers x largest glslang arena, and four
|
||||||
@@ -81,6 +67,31 @@ namespace MobileGL::MG_Util::Async {
|
|||||||
// MOBILEGL_ASYNC_SHADER_COMPILE_THREADS overrides it outright.
|
// MOBILEGL_ASYNC_SHADER_COMPILE_THREADS overrides it outright.
|
||||||
Uint DetectShaderCompileThreadCount();
|
Uint DetectShaderCompileThreadCount();
|
||||||
|
|
||||||
|
// ---- MOBILEGL_ASYNC_POOL: which engine drives the worker threads ----------------------
|
||||||
|
// The engine is ONLY the execution engine. The job queue, the concurrency budget and its
|
||||||
|
// clamping, the suspension latch, cancel request-vs-outcome, the stopped-is-synchronous
|
||||||
|
// fallback and the drain are all engine-independent - they live in ShaderCompilePool::Impl
|
||||||
|
// and are shared verbatim by both engines, which is what lets the whole async suite run
|
||||||
|
// unchanged against either one. An engine answers exactly one question: how does a job
|
||||||
|
// that the budget has already cleared reach a worker thread?
|
||||||
|
enum class AsyncPoolEngine : Uint8 {
|
||||||
|
Asio, // asio::thread_pool: one shared queue behind Asio's scheduler lock
|
||||||
|
Libfork, // lf::lazy_pool: per-worker work-stealing deques, workers sleep when idle
|
||||||
|
};
|
||||||
|
|
||||||
|
// "asio" / "libfork" - the spelling the environment variable accepts and the log prints.
|
||||||
|
const char* AsyncPoolEngineName(AsyncPoolEngine engine);
|
||||||
|
|
||||||
|
// Parses one MOBILEGL_ASYNC_POOL value. Case-insensitive; empty, "auto" and anything
|
||||||
|
// unrecognized resolve to Asio, and an unrecognized value warns (a misspelt engine name
|
||||||
|
// would otherwise be indistinguishable from the default, and the whole point of the
|
||||||
|
// variable is to know which engine ran).
|
||||||
|
AsyncPoolEngine ParseAsyncPoolEngine(const String& value);
|
||||||
|
|
||||||
|
// The process's engine, resolved from MOBILEGL_ASYNC_POOL on first call and cached. Every
|
||||||
|
// pool constructed afterwards reports the same answer, so a process never mixes engines.
|
||||||
|
AsyncPoolEngine DetectAsyncPoolEngine();
|
||||||
|
|
||||||
class ShaderCompilePool {
|
class ShaderCompilePool {
|
||||||
public:
|
public:
|
||||||
explicit ShaderCompilePool(Uint threadCount);
|
explicit ShaderCompilePool(Uint threadCount);
|
||||||
@@ -109,16 +120,14 @@ namespace MobileGL::MG_Util::Async {
|
|||||||
// but they share glslang's process globals, which teardown is about to free.
|
// but they share glslang's process globals, which teardown is about to free.
|
||||||
void StopAndDrain();
|
void StopAndDrain();
|
||||||
|
|
||||||
// StopAndDrain() on the process-wide pool if one was ever created; never creates
|
|
||||||
// one. For extra atexit sentinels owned by other subsystems (the SPIR-V validation
|
|
||||||
// switch registers one after forcing spirv-tools' lazy function-local tables into
|
|
||||||
// existence, so the drain is sequenced before those tables' destructors - a worker
|
|
||||||
// mid-Validate would otherwise touch freed memory during process exit).
|
|
||||||
static void StopAndDrainProcessPoolAtExit();
|
|
||||||
|
|
||||||
Uint GetThreadCount() const;
|
Uint GetThreadCount() const;
|
||||||
Uint GetMaxConcurrency() const;
|
Uint GetMaxConcurrency() const;
|
||||||
|
|
||||||
|
// The engine this pool was built with, latched at construction from
|
||||||
|
// DetectAsyncPoolEngine(). Reported rather than re-resolved so that a pool cannot
|
||||||
|
// change engines under its own workers.
|
||||||
|
AsyncPoolEngine GetEngine() const;
|
||||||
|
|
||||||
// Bounded concurrency doubles as the memory bound, and is how
|
// Bounded concurrency doubles as the memory bound, and is how
|
||||||
// glMaxShaderCompilerThreadsKHR(n) is honoured: a 300-program pack load cannot put
|
// glMaxShaderCompilerThreadsKHR(n) is honoured: a 300-program pack load cannot put
|
||||||
// 300 glslang arenas in flight at once. Clamped to [1, thread count].
|
// 300 glslang arenas in flight at once. Clamped to [1, thread count].
|
||||||
|
|||||||
@@ -568,26 +568,13 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
#if defined(MOBILEGL_IOS)
|
#if defined(MOBILEGL_IOS)
|
||||||
eglLib = OpenLib({"libtinygl4angle.dylib"});
|
eglLib = OpenLib({"libtinygl4angle.dylib"});
|
||||||
#else
|
#else
|
||||||
// Versioned SONAME first. The unversioned "libEGL.so" is a development
|
eglLib = OpenLib({"libEGL.so"});
|
||||||
// symlink: it ships in libegl-dev/mesa-libEGL-devel, NOT in the runtime
|
|
||||||
// package, so a machine that can run GL perfectly well may not have it -
|
|
||||||
// every stock Ubuntu/Debian runtime image, the GitHub Actions runners
|
|
||||||
// included. Asking only for the unversioned name there makes dlopen fail,
|
|
||||||
// which used to leave the whole EGL function table null and take the next
|
|
||||||
// call through a null pointer (SIGSEGV inside InitDisplayAndContext).
|
|
||||||
// Developer machines have both names, which is exactly why this only ever
|
|
||||||
// showed up in CI.
|
|
||||||
eglLib = OpenLib({"libEGL.so.1", "libEGL.so"});
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#endif // !_WIN32
|
#endif // !_WIN32
|
||||||
|
|
||||||
if (!eglLib) {
|
if (!eglLib) {
|
||||||
// MGLOG_F, not MGLOG_E: at the INFO log level every shipping and CI build
|
MGLOG_E("Failed to open EGL library");
|
||||||
// uses, MGLOG_E is compiled out (Log.h orders DEBUG < WARN < ERROR < INFO),
|
|
||||||
// so this diagnosis was invisible in precisely the builds that needed it.
|
|
||||||
MGLOG_F("Failed to open EGL library: none of libEGL.so.1 / libEGL.so could be "
|
|
||||||
"dlopened; every EGL entry point will be null");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,10 +595,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
do { \
|
do { \
|
||||||
funcs.name = (MG_External::EGL::name##_PTR)resolveEGLProc(#name); \
|
funcs.name = (MG_External::EGL::name##_PTR)resolveEGLProc(#name); \
|
||||||
if (!funcs.name) { \
|
if (!funcs.name) { \
|
||||||
/* MGLOG_F for the same reason as the open failure above: a null entry */ \
|
MGLOG_E("Failed to load EGL function: %s", #name); \
|
||||||
/* point is a crash waiting for its first caller, and MGLOG_E is compiled */ \
|
|
||||||
/* out at the INFO level every shipping and CI build uses. */ \
|
|
||||||
MGLOG_F("Failed to load EGL function: %s", #name); \
|
|
||||||
} \
|
} \
|
||||||
} while (0);
|
} while (0);
|
||||||
|
|
||||||
|
|||||||
@@ -10,20 +10,9 @@
|
|||||||
|
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <limits>
|
|
||||||
|
|
||||||
namespace MobileGL::MG_Util::BackendLoader {
|
namespace MobileGL::MG_Util::BackendLoader {
|
||||||
namespace {
|
namespace {
|
||||||
// A Vulkan limit is an unsigned 32-bit count; a GL limit is a signed Int. Drivers do report
|
|
||||||
// values with the top bit set (UINT32_MAX is the idiomatic "effectively unlimited"), and a
|
|
||||||
// plain static_cast turned those into small negatives - which every downstream std::min or
|
|
||||||
// ceiling comparison then accepted as "already small enough". Saturate instead, so a clamp
|
|
||||||
// above this can be trusted to be the only thing that lowers a limit.
|
|
||||||
Int SaturateToInt(Uint32 value) {
|
|
||||||
constexpr Uint32 kMaxInt = static_cast<Uint32>(std::numeric_limits<Int>::max());
|
|
||||||
return static_cast<Int>(std::min<Uint32>(value, kMaxInt));
|
|
||||||
}
|
|
||||||
|
|
||||||
struct VulkanDynamicFunctions {
|
struct VulkanDynamicFunctions {
|
||||||
PFN_vkGetPhysicalDeviceProperties vkGetPhysicalDeviceProperties = nullptr;
|
PFN_vkGetPhysicalDeviceProperties vkGetPhysicalDeviceProperties = nullptr;
|
||||||
PFN_vkGetPhysicalDeviceProperties2 vkGetPhysicalDeviceProperties2 = nullptr;
|
PFN_vkGetPhysicalDeviceProperties2 vkGetPhysicalDeviceProperties2 = nullptr;
|
||||||
@@ -163,47 +152,47 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.PointSizeRangeMin = p.limits.pointSizeRange[0];
|
caps.PointSizeRangeMin = p.limits.pointSizeRange[0];
|
||||||
caps.PointSizeRangeMax = p.limits.pointSizeRange[1];
|
caps.PointSizeRangeMax = p.limits.pointSizeRange[1];
|
||||||
caps.PointSizeGranularity = p.limits.pointSizeGranularity;
|
caps.PointSizeGranularity = p.limits.pointSizeGranularity;
|
||||||
caps.Max3DTextureSize = SaturateToInt(p.limits.maxImageDimension3D);
|
caps.Max3DTextureSize = static_cast<Int>(p.limits.maxImageDimension3D);
|
||||||
caps.MaxArrayTextureLayers = SaturateToInt(p.limits.maxImageArrayLayers);
|
caps.MaxArrayTextureLayers = static_cast<Int>(p.limits.maxImageArrayLayers);
|
||||||
caps.MaxCubeMapTextureSize = SaturateToInt(p.limits.maxImageDimensionCube);
|
caps.MaxCubeMapTextureSize = static_cast<Int>(p.limits.maxImageDimensionCube);
|
||||||
caps.MaxFramebufferWidth = SaturateToInt(p.limits.maxFramebufferWidth);
|
caps.MaxFramebufferWidth = static_cast<Int>(p.limits.maxFramebufferWidth);
|
||||||
caps.MaxFramebufferHeight = SaturateToInt(p.limits.maxFramebufferHeight);
|
caps.MaxFramebufferHeight = static_cast<Int>(p.limits.maxFramebufferHeight);
|
||||||
caps.MaxFramebufferLayers = SaturateToInt(p.limits.maxFramebufferLayers);
|
caps.MaxFramebufferLayers = static_cast<Int>(p.limits.maxFramebufferLayers);
|
||||||
caps.MaxRenderbufferSize = ResolveMaxRenderbufferSize(p.limits);
|
caps.MaxRenderbufferSize = ResolveMaxRenderbufferSize(p.limits);
|
||||||
caps.MaxTextureSize = SaturateToInt(p.limits.maxImageDimension2D);
|
caps.MaxTextureSize = static_cast<Int>(p.limits.maxImageDimension2D);
|
||||||
caps.MaxColorTextureSamples = MaxSampleCountFromFlags(p.limits.sampledImageColorSampleCounts);
|
caps.MaxColorTextureSamples = MaxSampleCountFromFlags(p.limits.sampledImageColorSampleCounts);
|
||||||
caps.MaxDepthTextureSamples = MaxSampleCountFromFlags(p.limits.sampledImageDepthSampleCounts);
|
caps.MaxDepthTextureSamples = MaxSampleCountFromFlags(p.limits.sampledImageDepthSampleCounts);
|
||||||
caps.MaxFramebufferSamples = ResolveConservativeFramebufferSampleLimit(p.limits);
|
caps.MaxFramebufferSamples = ResolveConservativeFramebufferSampleLimit(p.limits);
|
||||||
caps.MaxIntegerSamples = MaxSampleCountFromFlags(p.limits.sampledImageIntegerSampleCounts);
|
caps.MaxIntegerSamples = MaxSampleCountFromFlags(p.limits.sampledImageIntegerSampleCounts);
|
||||||
caps.MaxSamples = caps.MaxFramebufferSamples;
|
caps.MaxSamples = caps.MaxFramebufferSamples;
|
||||||
caps.MaxSampleMaskWords = SaturateToInt(p.limits.maxSampleMaskWords);
|
caps.MaxSampleMaskWords = static_cast<Int>(p.limits.maxSampleMaskWords);
|
||||||
caps.MaxTextureImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorSampledImages);
|
caps.MaxTextureImageUnits = static_cast<Int>(p.limits.maxPerStageDescriptorSampledImages);
|
||||||
caps.MaxVertexTextureImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorSampledImages);
|
caps.MaxVertexTextureImageUnits = static_cast<Int>(p.limits.maxPerStageDescriptorSampledImages);
|
||||||
caps.MaxComputeTextureImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorSampledImages);
|
caps.MaxComputeTextureImageUnits = static_cast<Int>(p.limits.maxPerStageDescriptorSampledImages);
|
||||||
caps.MaxCombinedTextureImageUnits = SaturateToInt(p.limits.maxDescriptorSetSampledImages);
|
caps.MaxCombinedTextureImageUnits = static_cast<Int>(p.limits.maxDescriptorSetSampledImages);
|
||||||
caps.MaxVertexAttribs = SaturateToInt(p.limits.maxVertexInputAttributes);
|
caps.MaxVertexAttribs = static_cast<Int>(p.limits.maxVertexInputAttributes);
|
||||||
caps.MaxComputeShaderStorageBlocks = SaturateToInt(p.limits.maxPerStageDescriptorStorageBuffers);
|
caps.MaxComputeShaderStorageBlocks = static_cast<Int>(p.limits.maxPerStageDescriptorStorageBuffers);
|
||||||
caps.MaxCombinedShaderStorageBlocks = SaturateToInt(p.limits.maxDescriptorSetStorageBuffers);
|
caps.MaxCombinedShaderStorageBlocks = static_cast<Int>(p.limits.maxDescriptorSetStorageBuffers);
|
||||||
caps.MaxComputeUniformBlocks = SaturateToInt(p.limits.maxPerStageDescriptorUniformBuffers);
|
caps.MaxComputeUniformBlocks = static_cast<Int>(p.limits.maxPerStageDescriptorUniformBuffers);
|
||||||
caps.MaxComputeWorkGroupInvocations = SaturateToInt(p.limits.maxComputeWorkGroupInvocations);
|
caps.MaxComputeWorkGroupInvocations = static_cast<Int>(p.limits.maxComputeWorkGroupInvocations);
|
||||||
caps.MaxShaderStorageBufferBindings = SaturateToInt(p.limits.maxDescriptorSetStorageBuffers);
|
caps.MaxShaderStorageBufferBindings = static_cast<Int>(p.limits.maxDescriptorSetStorageBuffers);
|
||||||
caps.MaxTextureBufferSize = SaturateToInt(p.limits.maxTexelBufferElements);
|
caps.MaxTextureBufferSize = static_cast<Int>(p.limits.maxTexelBufferElements);
|
||||||
caps.TextureBufferOffsetAlignment =
|
caps.TextureBufferOffsetAlignment =
|
||||||
static_cast<Int>(std::max<VkDeviceSize>(1, p.limits.minTexelBufferOffsetAlignment));
|
static_cast<Int>(std::max<VkDeviceSize>(1, p.limits.minTexelBufferOffsetAlignment));
|
||||||
caps.MaxUniformBufferBindings = SaturateToInt(p.limits.maxDescriptorSetUniformBuffers);
|
caps.MaxUniformBufferBindings = static_cast<Int>(p.limits.maxDescriptorSetUniformBuffers);
|
||||||
caps.MaxUniformBlockSize = SaturateToInt(p.limits.maxUniformBufferRange);
|
caps.MaxUniformBlockSize = static_cast<Int>(p.limits.maxUniformBufferRange);
|
||||||
caps.MaxImageUnits = SaturateToInt(p.limits.maxPerStageDescriptorStorageImages);
|
caps.MaxImageUnits = static_cast<Int>(p.limits.maxPerStageDescriptorStorageImages);
|
||||||
caps.MaxCombinedImageUniforms = SaturateToInt(p.limits.maxDescriptorSetStorageImages);
|
caps.MaxCombinedImageUniforms = static_cast<Int>(p.limits.maxDescriptorSetStorageImages);
|
||||||
caps.MaxComputeImageUniforms = SaturateToInt(p.limits.maxPerStageDescriptorStorageImages);
|
caps.MaxComputeImageUniforms = static_cast<Int>(p.limits.maxPerStageDescriptorStorageImages);
|
||||||
caps.MaxDrawBuffers = SaturateToInt(p.limits.maxFragmentOutputAttachments);
|
caps.MaxDrawBuffers = static_cast<Int>(p.limits.maxFragmentOutputAttachments);
|
||||||
caps.MaxColorAttachments = SaturateToInt(p.limits.maxColorAttachments);
|
caps.MaxColorAttachments = static_cast<Int>(p.limits.maxColorAttachments);
|
||||||
caps.MaxClipDistances = SaturateToInt(p.limits.maxClipDistances);
|
caps.MaxClipDistances = static_cast<Int>(p.limits.maxClipDistances);
|
||||||
caps.MaxViewports = SaturateToInt(p.limits.maxViewports);
|
caps.MaxViewports = static_cast<Int>(p.limits.maxViewports);
|
||||||
caps.MaxViewportWidth = SaturateToInt(p.limits.maxViewportDimensions[0]);
|
caps.MaxViewportWidth = static_cast<Int>(p.limits.maxViewportDimensions[0]);
|
||||||
caps.MaxViewportHeight = SaturateToInt(p.limits.maxViewportDimensions[1]);
|
caps.MaxViewportHeight = static_cast<Int>(p.limits.maxViewportDimensions[1]);
|
||||||
caps.ViewportBoundsRangeMin = p.limits.viewportBoundsRange[0];
|
caps.ViewportBoundsRangeMin = p.limits.viewportBoundsRange[0];
|
||||||
caps.ViewportBoundsRangeMax = p.limits.viewportBoundsRange[1];
|
caps.ViewportBoundsRangeMax = p.limits.viewportBoundsRange[1];
|
||||||
caps.ViewportSubpixelBits = SaturateToInt(p.limits.viewportSubPixelBits);
|
caps.ViewportSubpixelBits = static_cast<Int>(p.limits.viewportSubPixelBits);
|
||||||
FillFragmentInterpolationLimits(caps, p.limits);
|
FillFragmentInterpolationLimits(caps, p.limits);
|
||||||
|
|
||||||
VkPhysicalDeviceFeatures supportedFeatures{};
|
VkPhysicalDeviceFeatures supportedFeatures{};
|
||||||
@@ -280,47 +269,47 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.PointSizeRangeMin = properties.limits.pointSizeRange[0];
|
caps.PointSizeRangeMin = properties.limits.pointSizeRange[0];
|
||||||
caps.PointSizeRangeMax = properties.limits.pointSizeRange[1];
|
caps.PointSizeRangeMax = properties.limits.pointSizeRange[1];
|
||||||
caps.PointSizeGranularity = properties.limits.pointSizeGranularity;
|
caps.PointSizeGranularity = properties.limits.pointSizeGranularity;
|
||||||
caps.Max3DTextureSize = SaturateToInt(properties.limits.maxImageDimension3D);
|
caps.Max3DTextureSize = static_cast<Int>(properties.limits.maxImageDimension3D);
|
||||||
caps.MaxArrayTextureLayers = SaturateToInt(properties.limits.maxImageArrayLayers);
|
caps.MaxArrayTextureLayers = static_cast<Int>(properties.limits.maxImageArrayLayers);
|
||||||
caps.MaxCubeMapTextureSize = SaturateToInt(properties.limits.maxImageDimensionCube);
|
caps.MaxCubeMapTextureSize = static_cast<Int>(properties.limits.maxImageDimensionCube);
|
||||||
caps.MaxFramebufferWidth = SaturateToInt(properties.limits.maxFramebufferWidth);
|
caps.MaxFramebufferWidth = static_cast<Int>(properties.limits.maxFramebufferWidth);
|
||||||
caps.MaxFramebufferHeight = SaturateToInt(properties.limits.maxFramebufferHeight);
|
caps.MaxFramebufferHeight = static_cast<Int>(properties.limits.maxFramebufferHeight);
|
||||||
caps.MaxFramebufferLayers = SaturateToInt(properties.limits.maxFramebufferLayers);
|
caps.MaxFramebufferLayers = static_cast<Int>(properties.limits.maxFramebufferLayers);
|
||||||
caps.MaxRenderbufferSize = ResolveMaxRenderbufferSize(properties.limits);
|
caps.MaxRenderbufferSize = ResolveMaxRenderbufferSize(properties.limits);
|
||||||
caps.MaxTextureSize = SaturateToInt(properties.limits.maxImageDimension2D);
|
caps.MaxTextureSize = static_cast<Int>(properties.limits.maxImageDimension2D);
|
||||||
caps.MaxColorTextureSamples = MaxSampleCountFromFlags(properties.limits.sampledImageColorSampleCounts);
|
caps.MaxColorTextureSamples = MaxSampleCountFromFlags(properties.limits.sampledImageColorSampleCounts);
|
||||||
caps.MaxDepthTextureSamples = MaxSampleCountFromFlags(properties.limits.sampledImageDepthSampleCounts);
|
caps.MaxDepthTextureSamples = MaxSampleCountFromFlags(properties.limits.sampledImageDepthSampleCounts);
|
||||||
caps.MaxFramebufferSamples = ResolveConservativeFramebufferSampleLimit(properties.limits);
|
caps.MaxFramebufferSamples = ResolveConservativeFramebufferSampleLimit(properties.limits);
|
||||||
caps.MaxIntegerSamples = MaxSampleCountFromFlags(properties.limits.sampledImageIntegerSampleCounts);
|
caps.MaxIntegerSamples = MaxSampleCountFromFlags(properties.limits.sampledImageIntegerSampleCounts);
|
||||||
caps.MaxSamples = caps.MaxFramebufferSamples;
|
caps.MaxSamples = caps.MaxFramebufferSamples;
|
||||||
caps.MaxSampleMaskWords = SaturateToInt(properties.limits.maxSampleMaskWords);
|
caps.MaxSampleMaskWords = static_cast<Int>(properties.limits.maxSampleMaskWords);
|
||||||
caps.MaxTextureImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorSampledImages);
|
caps.MaxTextureImageUnits = static_cast<Int>(properties.limits.maxPerStageDescriptorSampledImages);
|
||||||
caps.MaxVertexTextureImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorSampledImages);
|
caps.MaxVertexTextureImageUnits = static_cast<Int>(properties.limits.maxPerStageDescriptorSampledImages);
|
||||||
caps.MaxComputeTextureImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorSampledImages);
|
caps.MaxComputeTextureImageUnits = static_cast<Int>(properties.limits.maxPerStageDescriptorSampledImages);
|
||||||
caps.MaxCombinedTextureImageUnits = SaturateToInt(properties.limits.maxDescriptorSetSampledImages);
|
caps.MaxCombinedTextureImageUnits = static_cast<Int>(properties.limits.maxDescriptorSetSampledImages);
|
||||||
caps.MaxVertexAttribs = SaturateToInt(properties.limits.maxVertexInputAttributes);
|
caps.MaxVertexAttribs = static_cast<Int>(properties.limits.maxVertexInputAttributes);
|
||||||
caps.MaxComputeShaderStorageBlocks = SaturateToInt(properties.limits.maxPerStageDescriptorStorageBuffers);
|
caps.MaxComputeShaderStorageBlocks = static_cast<Int>(properties.limits.maxPerStageDescriptorStorageBuffers);
|
||||||
caps.MaxCombinedShaderStorageBlocks = SaturateToInt(properties.limits.maxDescriptorSetStorageBuffers);
|
caps.MaxCombinedShaderStorageBlocks = static_cast<Int>(properties.limits.maxDescriptorSetStorageBuffers);
|
||||||
caps.MaxComputeUniformBlocks = SaturateToInt(properties.limits.maxPerStageDescriptorUniformBuffers);
|
caps.MaxComputeUniformBlocks = static_cast<Int>(properties.limits.maxPerStageDescriptorUniformBuffers);
|
||||||
caps.MaxComputeWorkGroupInvocations = SaturateToInt(properties.limits.maxComputeWorkGroupInvocations);
|
caps.MaxComputeWorkGroupInvocations = static_cast<Int>(properties.limits.maxComputeWorkGroupInvocations);
|
||||||
caps.MaxShaderStorageBufferBindings = SaturateToInt(properties.limits.maxDescriptorSetStorageBuffers);
|
caps.MaxShaderStorageBufferBindings = static_cast<Int>(properties.limits.maxDescriptorSetStorageBuffers);
|
||||||
caps.MaxTextureBufferSize = SaturateToInt(properties.limits.maxTexelBufferElements);
|
caps.MaxTextureBufferSize = static_cast<Int>(properties.limits.maxTexelBufferElements);
|
||||||
caps.TextureBufferOffsetAlignment =
|
caps.TextureBufferOffsetAlignment =
|
||||||
static_cast<Int>(std::max<VkDeviceSize>(1, properties.limits.minTexelBufferOffsetAlignment));
|
static_cast<Int>(std::max<VkDeviceSize>(1, properties.limits.minTexelBufferOffsetAlignment));
|
||||||
caps.MaxUniformBufferBindings = SaturateToInt(properties.limits.maxDescriptorSetUniformBuffers);
|
caps.MaxUniformBufferBindings = static_cast<Int>(properties.limits.maxDescriptorSetUniformBuffers);
|
||||||
caps.MaxUniformBlockSize = SaturateToInt(properties.limits.maxUniformBufferRange);
|
caps.MaxUniformBlockSize = static_cast<Int>(properties.limits.maxUniformBufferRange);
|
||||||
caps.MaxImageUnits = SaturateToInt(properties.limits.maxPerStageDescriptorStorageImages);
|
caps.MaxImageUnits = static_cast<Int>(properties.limits.maxPerStageDescriptorStorageImages);
|
||||||
caps.MaxCombinedImageUniforms = SaturateToInt(properties.limits.maxDescriptorSetStorageImages);
|
caps.MaxCombinedImageUniforms = static_cast<Int>(properties.limits.maxDescriptorSetStorageImages);
|
||||||
caps.MaxComputeImageUniforms = SaturateToInt(properties.limits.maxPerStageDescriptorStorageImages);
|
caps.MaxComputeImageUniforms = static_cast<Int>(properties.limits.maxPerStageDescriptorStorageImages);
|
||||||
caps.MaxDrawBuffers = SaturateToInt(properties.limits.maxFragmentOutputAttachments);
|
caps.MaxDrawBuffers = static_cast<Int>(properties.limits.maxFragmentOutputAttachments);
|
||||||
caps.MaxColorAttachments = SaturateToInt(properties.limits.maxColorAttachments);
|
caps.MaxColorAttachments = static_cast<Int>(properties.limits.maxColorAttachments);
|
||||||
caps.MaxClipDistances = SaturateToInt(properties.limits.maxClipDistances);
|
caps.MaxClipDistances = static_cast<Int>(properties.limits.maxClipDistances);
|
||||||
caps.MaxViewports = SaturateToInt(properties.limits.maxViewports);
|
caps.MaxViewports = static_cast<Int>(properties.limits.maxViewports);
|
||||||
caps.MaxViewportWidth = SaturateToInt(properties.limits.maxViewportDimensions[0]);
|
caps.MaxViewportWidth = static_cast<Int>(properties.limits.maxViewportDimensions[0]);
|
||||||
caps.MaxViewportHeight = SaturateToInt(properties.limits.maxViewportDimensions[1]);
|
caps.MaxViewportHeight = static_cast<Int>(properties.limits.maxViewportDimensions[1]);
|
||||||
caps.ViewportBoundsRangeMin = properties.limits.viewportBoundsRange[0];
|
caps.ViewportBoundsRangeMin = properties.limits.viewportBoundsRange[0];
|
||||||
caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1];
|
caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1];
|
||||||
caps.ViewportSubpixelBits = SaturateToInt(properties.limits.viewportSubPixelBits);
|
caps.ViewportSubpixelBits = static_cast<Int>(properties.limits.viewportSubPixelBits);
|
||||||
FillFragmentInterpolationLimits(caps, properties.limits);
|
FillFragmentInterpolationLimits(caps, properties.limits);
|
||||||
caps.SupportsWideLines = false;
|
caps.SupportsWideLines = false;
|
||||||
caps.SupportsShaderFloat64 = false;
|
caps.SupportsShaderFloat64 = false;
|
||||||
|
|||||||
@@ -151,12 +151,20 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const Uint threads = MG_Util::Async::DetectShaderCompileThreadCount();
|
const Uint threads = MG_Util::Async::DetectShaderCompileThreadCount();
|
||||||
|
// The execution engine is named here too. It changes no observable GL behaviour -
|
||||||
|
// both engines run the same job queue under the same budget - but when a scaling
|
||||||
|
// or stall report comes back from a device, "which engine was this?" is the first
|
||||||
|
// question, and a POST page is the one artefact that always accompanies it.
|
||||||
|
const char* const engineName =
|
||||||
|
MG_Util::Async::AsyncPoolEngineName(MG_Util::Async::DetectAsyncPoolEngine());
|
||||||
builder.Pass(rowName,
|
builder.Pass(rowName,
|
||||||
format("on with {} compiler thread{}; GL_KHR_parallel_shader_compile is advertised "
|
format("on with {} compiler thread{} on the {} execution engine; "
|
||||||
|
"GL_KHR_parallel_shader_compile is advertised "
|
||||||
"and GL_MAX_SHADER_COMPILER_THREADS_KHR = {} (set environment variable "
|
"and GL_MAX_SHADER_COMPILER_THREADS_KHR = {} (set environment variable "
|
||||||
"MOBILEGL_ASYNC_SHADER_COMPILE=0 to disable it, or "
|
"MOBILEGL_ASYNC_SHADER_COMPILE=0 to disable it, "
|
||||||
"MOBILEGL_ASYNC_SHADER_COMPILE_THREADS=n to change the count)",
|
"MOBILEGL_ASYNC_SHADER_COMPILE_THREADS=n to change the count, or "
|
||||||
threads, threads == 1 ? "" : "s", threads));
|
"MOBILEGL_ASYNC_POOL=asio|libfork to change the engine)",
|
||||||
|
threads, threads == 1 ? "" : "s", engineName, threads));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Appends the four "MobileGL reported ..." rows for one backend section.
|
// Appends the four "MobileGL reported ..." rows for one backend section.
|
||||||
|
|||||||
@@ -22,8 +22,6 @@
|
|||||||
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
|
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
|
||||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||||
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
|
||||||
#include "SpirvPasses/StripUniformLocationsPass.h"
|
|
||||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||||
@@ -32,13 +30,9 @@
|
|||||||
|
|
||||||
#include "ShaderSourceProcessor.h"
|
#include "ShaderSourceProcessor.h"
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
|
||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/Converters/GLToGlslang/ProgramEnumConverter.h>
|
#include <MG_Util/Converters/GLToGlslang/ProgramEnumConverter.h>
|
||||||
#include <atomic>
|
|
||||||
#include <cctype>
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <mutex>
|
|
||||||
|
|
||||||
namespace MobileGL {
|
namespace MobileGL {
|
||||||
namespace MG_Util {
|
namespace MG_Util {
|
||||||
@@ -360,215 +354,16 @@ namespace MobileGL {
|
|||||||
return allSpirv;
|
return allSpirv;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -1 unresolved, 0 off, 1 on. Resolved once from MOBILEGL_VALIDATE_SPIRV on first
|
|
||||||
// use. A live getenv rather than an MG_Config::Features field, for the same reason
|
|
||||||
// Config.h already exempts MOBILEGL_LOG_FILE_PATH: suites like SpirvPassTest never
|
|
||||||
// run MobileGL::Initialize(), and every Initialize() re-runs MG_ConfigLoader::Init,
|
|
||||||
// which would clobber a programmatic override stored in the feature table.
|
|
||||||
static std::atomic<int> g_validateSpirv{-1};
|
|
||||||
// Total validation failures observed this process. This latch - not the wrappers'
|
|
||||||
// return values - is the test-lane signal: validation must never change what a
|
|
||||||
// wrapper returns, or the validating lanes would render differently from the
|
|
||||||
// shipping configuration (fail-open call sites would silently substitute an
|
|
||||||
// earlier-stage module).
|
|
||||||
static std::atomic<Uint64> g_spirvValidationFailures{0};
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
// Test lanes (desktop/CI/WSL) validate by default; device builds do not -
|
|
||||||
// validation costs real time per module, and on device the driver is the
|
|
||||||
// final validator anyway. MOBILEGL_VALIDATE_SPIRV overrides in either
|
|
||||||
// direction, using the ConfigLoader truthy rule.
|
|
||||||
constexpr bool kValidateSpirvDefault =
|
|
||||||
#if defined(__ANDROID__)
|
|
||||||
false;
|
|
||||||
#else
|
|
||||||
true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
bool IsTruthySpirvEnvValue(const char* value) {
|
|
||||||
if (value == nullptr || value[0] == '\0') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String lowered(value);
|
|
||||||
for (auto& c : lowered) {
|
|
||||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
||||||
}
|
|
||||||
return lowered != "0" && lowered != "false";
|
|
||||||
}
|
|
||||||
|
|
||||||
// spirv-tools' validator lazily constructs function-local static tables on
|
|
||||||
// its first run, which on this codebase happens on a ShaderCompilePool
|
|
||||||
// worker. Function-local statics are destroyed in reverse construction
|
|
||||||
// order, so those tables would die BEFORE the pool's own atexit sentinel
|
|
||||||
// (registered at first pool use) gets to drain the workers - and a worker
|
|
||||||
// mid-Validate would then read freed memory during process exit. Pin the
|
|
||||||
// order instead: force the tables into existence now, then register a
|
|
||||||
// second drain handler; being registered after the tables' destructors, it
|
|
||||||
// runs before them.
|
|
||||||
void PinValidatorTablesForProcessExit() {
|
|
||||||
static std::once_flag pinnedOnce;
|
|
||||||
std::call_once(pinnedOnce, [] {
|
|
||||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
|
||||||
Vector<Uint32> warmup;
|
|
||||||
// The module is shaped to reach BOTH lazily-constructed tables in
|
|
||||||
// the vendored validate_id.cpp: a type-generating operand pins
|
|
||||||
// InstructionCanHaveTypeOperand's allow-set, and the OpExtInst use
|
|
||||||
// of the TYPELESS %glsl import is the one path into
|
|
||||||
// InstructionRequiresTypeOperand's deny-set (its call site is
|
|
||||||
// guarded on a referenced def with no result type). A straight-line
|
|
||||||
// module without it leaves the deny-set to be built later on a pool
|
|
||||||
// worker, re-creating the exit-order hazard for that one table.
|
|
||||||
if (tools.Assemble("OpCapability Shader\n"
|
|
||||||
"%glsl = OpExtInstImport \"GLSL.std.450\"\n"
|
|
||||||
"OpMemoryModel Logical GLSL450\n"
|
|
||||||
"OpEntryPoint GLCompute %main \"main\"\n"
|
|
||||||
"OpExecutionMode %main LocalSize 1 1 1\n"
|
|
||||||
"%void = OpTypeVoid\n"
|
|
||||||
"%fn = OpTypeFunction %void\n"
|
|
||||||
"%float = OpTypeFloat 32\n"
|
|
||||||
"%c = OpConstant %float 1\n"
|
|
||||||
"%main = OpFunction %void None %fn\n"
|
|
||||||
"%entry = OpLabel\n"
|
|
||||||
"%abs = OpExtInst %float %glsl FAbs %c\n"
|
|
||||||
"OpReturn\n"
|
|
||||||
"OpFunctionEnd\n",
|
|
||||||
&warmup)) {
|
|
||||||
tools.Validate(warmup);
|
|
||||||
}
|
|
||||||
std::atexit(+[] {
|
|
||||||
// Flip validation off first: a validator table this warmup does
|
|
||||||
// not know about (a future spirv-tools bump) would still be
|
|
||||||
// destroyed before this handler, and workers must stop entering
|
|
||||||
// Validate before the drain waits for them.
|
|
||||||
g_validateSpirv.store(0, std::memory_order_release);
|
|
||||||
Async::ShaderCompilePool::StopAndDrainProcessPoolAtExit();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
spvtools::MessageConsumer MakeSpirvMessageConsumer(const char* site) {
|
|
||||||
return [site](spv_message_level_t level, const char* /*source*/,
|
|
||||||
const spv_position_t& position, const char* message) {
|
|
||||||
const char* text = message ? message : "";
|
|
||||||
switch (level) {
|
|
||||||
case SPV_MSG_FATAL:
|
|
||||||
case SPV_MSG_INTERNAL_ERROR:
|
|
||||||
case SPV_MSG_ERROR:
|
|
||||||
// MGLOG_I, deliberately: at the INFO compile level of every
|
|
||||||
// CI/WSL/retrace build, MGLOG_E and MGLOG_W are compiled out
|
|
||||||
// (Log.h orders DEBUG < WARN < ERROR < INFO) and the VUID
|
|
||||||
// would never reach a log.
|
|
||||||
MGLOG_I("[spirv] %s: %s (word index %zu)", site, text, position.index);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
MGLOG_D("[spirv] %s: %s", site, text);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation is decoupled from control flow on purpose: a failure logs and
|
|
||||||
// bumps the latch, and the caller proceeds exactly as the shipping (non-
|
|
||||||
// validating) configuration would. Tests assert on the latch delta.
|
|
||||||
void ValidateOrLatch(const char* site, const Vector<Uint32>& binary) {
|
|
||||||
if (!ShaderCompiler::SpirvValidationEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
|
||||||
tools.SetMessageConsumer(MakeSpirvMessageConsumer(site));
|
|
||||||
if (!tools.Validate(binary)) {
|
|
||||||
MGLOG_I("[spirv] %s: produced a module that fails validation (failure #%llu)",
|
|
||||||
site,
|
|
||||||
static_cast<unsigned long long>(
|
|
||||||
ShaderCompiler::NoteSpirvValidationFailure()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shared tail for every Optimizer wrapper in this file. The optimizer's own
|
|
||||||
// input validator stays off even in validating lanes, for two reasons: its
|
|
||||||
// failure is indistinguishable from a transform failure (Optimizer::Run
|
|
||||||
// returns false before BuildModule), and the FIRST wrapper's input is
|
|
||||||
// glslang output that is legitimately not Vulkan-clean yet. What gets
|
|
||||||
// validated is each wrapper's OUTPUT - the only bytes a driver can ever
|
|
||||||
// receive. The message consumer is installed unconditionally: without one,
|
|
||||||
// spirv-tools drops pass diagnostics on the floor.
|
|
||||||
bool RunOptimizerChecked(const char* site, spvtools::Optimizer& optimizer,
|
|
||||||
const Vector<Uint32>& inputBinary,
|
|
||||||
Vector<uint32_t>& outputBinary) {
|
|
||||||
spvtools::OptimizerOptions options;
|
|
||||||
options.set_run_validator(false);
|
|
||||||
optimizer.SetMessageConsumer(MakeSpirvMessageConsumer(site));
|
|
||||||
if (!optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ValidateOrLatch(site, outputBinary);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
bool ShaderCompiler::SpirvValidationEnabled() {
|
|
||||||
int state = g_validateSpirv.load(std::memory_order_acquire);
|
|
||||||
if (state < 0) {
|
|
||||||
const char* env = std::getenv("MOBILEGL_VALIDATE_SPIRV");
|
|
||||||
const bool resolved = env != nullptr ? IsTruthySpirvEnvValue(env) : kValidateSpirvDefault;
|
|
||||||
int expected = -1;
|
|
||||||
g_validateSpirv.compare_exchange_strong(expected, resolved ? 1 : 0,
|
|
||||||
std::memory_order_acq_rel);
|
|
||||||
state = g_validateSpirv.load(std::memory_order_acquire);
|
|
||||||
if (state == 1) {
|
|
||||||
PinValidatorTablesForProcessExit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return state == 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderCompiler::SetSpirvValidationEnabled(bool enabled) {
|
|
||||||
g_validateSpirv.store(enabled ? 1 : 0, std::memory_order_release);
|
|
||||||
if (enabled) {
|
|
||||||
PinValidatorTablesForProcessExit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint64 ShaderCompiler::NoteSpirvValidationFailure() {
|
|
||||||
return g_spirvValidationFailures.fetch_add(1, std::memory_order_relaxed) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint64 ShaderCompiler::SpirvValidationFailureCount() {
|
|
||||||
return g_spirvValidationFailures.load(std::memory_order_relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderCompiler::SanitizeAndOptimizeBinary(const Vector<Uint32>& inputBinary,
|
bool ShaderCompiler::SanitizeAndOptimizeBinary(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary) {
|
Vector<uint32_t>& outputBinary) {
|
||||||
using namespace spvtools;
|
using namespace spvtools;
|
||||||
|
OptimizerOptions options;
|
||||||
|
options.set_run_validator(false);
|
||||||
|
|
||||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||||
|
|
||||||
// ADCE refuses to treat a Private global as deletable while the entry point
|
|
||||||
// still contains any OpFunctionCall (IsLocalVar -> IsEntryPointWithNoCalls), so
|
|
||||||
// a dead vertex input feeding a never-read Private shim used to survive the
|
|
||||||
// whole chain (the Chocapic13 shadow.vsh mc_midTexCoord/iris_MidTex case).
|
|
||||||
// Rewriting entry-point-owned Private variables to Function storage first
|
|
||||||
// satisfies ADCE without inlining: over 521 real Iris modules the rewrite
|
|
||||||
// captured 17 of the 21 extra dead interface variables exhaustive inlining
|
|
||||||
// would, while shrinking the corpus 8% - inlining grew it 20% with a 5.3x
|
|
||||||
// worst-case module and no additional GPU-side benefit.
|
|
||||||
optimizer.RegisterPass(PrivateToEntryLocalPass::CreatePrivateToEntryLocalPass());
|
|
||||||
// Keep the one-arg overload: remove_outputs must stay false, forever. Output
|
|
||||||
// variables on the entry-point interface are ADCE's only unconditional live
|
|
||||||
// roots; XFB capture resolves varyings by OpName after this chain, and the
|
|
||||||
// VS-out/FS-in interface contract on both backends depends on declared outputs
|
|
||||||
// surviving even when never stored.
|
|
||||||
optimizer.RegisterPass(CreateAggressiveDCEPass(false));
|
optimizer.RegisterPass(CreateAggressiveDCEPass(false));
|
||||||
// Complementary to ADCE, not redundant: ADCE can never delete or delist an
|
|
||||||
// Output (see above), so never-written outputs are trimmed from the
|
|
||||||
// OpEntryPoint operand list here.
|
|
||||||
optimizer.RegisterPass(CreateRemoveUnusedInterfaceVariablesPass());
|
optimizer.RegisterPass(CreateRemoveUnusedInterfaceVariablesPass());
|
||||||
// The two module-legality repairs, so the chain's output - the bytes every
|
|
||||||
// consumer downstream sees - is valid Vulkan SPIR-V. Rect lowering used to
|
|
||||||
// live only in the backends; a validating lane would flag every rectangle
|
|
||||||
// module long before the backend got the chance to fix it, and the backend
|
|
||||||
// calls remain as no-ops on the now rect-free modules.
|
|
||||||
optimizer.RegisterPass(NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass());
|
|
||||||
optimizer.RegisterPass(StripUniformLocationsPass::CreateStripUniformLocationsPass());
|
|
||||||
optimizer.RegisterPass(FlattenInterfaceStructPass::CreateFlattenInterfaceStructPass());
|
optimizer.RegisterPass(FlattenInterfaceStructPass::CreateFlattenInterfaceStructPass());
|
||||||
optimizer.RegisterPass(RenameSamplerFunctionParameterPass::CreateRenameSamplerFunctionParameterPass());
|
optimizer.RegisterPass(RenameSamplerFunctionParameterPass::CreateRenameSamplerFunctionParameterPass());
|
||||||
optimizer.RegisterPass(
|
optimizer.RegisterPass(
|
||||||
@@ -576,88 +371,104 @@ namespace MobileGL {
|
|||||||
optimizer.RegisterPass(EliminateFloatEqualsZeroPass::CreateEliminateFloatEqualsZeroPass());
|
optimizer.RegisterPass(EliminateFloatEqualsZeroPass::CreateEliminateFloatEqualsZeroPass());
|
||||||
optimizer.RegisterPass(DecomposeWorkgroupVec3Pass::CreateDecomposeWorkgroupVec3Pass());
|
optimizer.RegisterPass(DecomposeWorkgroupVec3Pass::CreateDecomposeWorkgroupVec3Pass());
|
||||||
|
|
||||||
return RunOptimizerChecked("SanitizeAndOptimizeBinary", optimizer, inputBinary,
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
outputBinary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ShaderCompiler::LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
bool ShaderCompiler::LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary) {
|
Vector<uint32_t>& outputBinary) {
|
||||||
using namespace spvtools;
|
using namespace spvtools;
|
||||||
|
OptimizerOptions options;
|
||||||
|
options.set_run_validator(false);
|
||||||
|
|
||||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||||
optimizer.RegisterPass(LowerDrawParametersPass::CreateLowerDrawParametersPass());
|
optimizer.RegisterPass(LowerDrawParametersPass::CreateLowerDrawParametersPass());
|
||||||
|
|
||||||
return RunOptimizerChecked("LowerDrawParametersForEssl", optimizer, inputBinary,
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
outputBinary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ShaderCompiler::PackDoubleVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
|
bool ShaderCompiler::PackDoubleVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary) {
|
Vector<uint32_t>& outputBinary) {
|
||||||
using namespace spvtools;
|
using namespace spvtools;
|
||||||
|
OptimizerOptions options;
|
||||||
|
options.set_run_validator(false);
|
||||||
|
|
||||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||||
optimizer.RegisterPass(PackDoubleVertexInputsPass::CreatePackDoubleVertexInputsPass());
|
optimizer.RegisterPass(PackDoubleVertexInputsPass::CreatePackDoubleVertexInputsPass());
|
||||||
|
|
||||||
return RunOptimizerChecked("PackDoubleVertexInputsForVulkan", optimizer, inputBinary,
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
outputBinary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
|
bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary) {
|
Vector<uint32_t>& outputBinary) {
|
||||||
using namespace spvtools;
|
using namespace spvtools;
|
||||||
|
OptimizerOptions options;
|
||||||
|
options.set_run_validator(false);
|
||||||
|
|
||||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||||
optimizer.RegisterPass(
|
optimizer.RegisterPass(
|
||||||
StripUboMemberRelaxedPrecisionPass::CreateStripUboMemberRelaxedPrecisionPass());
|
StripUboMemberRelaxedPrecisionPass::CreateStripUboMemberRelaxedPrecisionPass());
|
||||||
|
|
||||||
return RunOptimizerChecked("StripUboMemberRelaxedPrecisionForEssl", optimizer,
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
inputBinary, outputBinary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ShaderCompiler::StripNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
bool ShaderCompiler::StripNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary) {
|
Vector<uint32_t>& outputBinary) {
|
||||||
using namespace spvtools;
|
using namespace spvtools;
|
||||||
|
OptimizerOptions options;
|
||||||
|
options.set_run_validator(false);
|
||||||
|
|
||||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||||
optimizer.RegisterPass(StripNoPerspectivePass::CreateStripNoPerspectivePass());
|
optimizer.RegisterPass(StripNoPerspectivePass::CreateStripNoPerspectivePass());
|
||||||
|
|
||||||
return RunOptimizerChecked("StripNoPerspectiveForEssl", optimizer, inputBinary,
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
outputBinary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ShaderCompiler::EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
bool ShaderCompiler::EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary) {
|
Vector<uint32_t>& outputBinary) {
|
||||||
using namespace spvtools;
|
using namespace spvtools;
|
||||||
|
OptimizerOptions options;
|
||||||
|
options.set_run_validator(false);
|
||||||
|
|
||||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||||
optimizer.RegisterPass(EmulateNoPerspectivePass::CreateEmulateNoPerspectivePass());
|
optimizer.RegisterPass(EmulateNoPerspectivePass::CreateEmulateNoPerspectivePass());
|
||||||
|
|
||||||
return RunOptimizerChecked("EmulateNoPerspectiveForEssl", optimizer, inputBinary,
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
outputBinary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ShaderCompiler::LowerRectImages(const Vector<Uint32>& inputBinary,
|
bool ShaderCompiler::LowerRectImages(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary) {
|
Vector<uint32_t>& outputBinary) {
|
||||||
using namespace spvtools;
|
using namespace spvtools;
|
||||||
|
OptimizerOptions options;
|
||||||
|
options.set_run_validator(false);
|
||||||
|
|
||||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||||
optimizer.RegisterPass(NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass());
|
optimizer.RegisterPass(NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass());
|
||||||
|
|
||||||
return RunOptimizerChecked("LowerRectImages", optimizer, inputBinary, outputBinary);
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary) {
|
Vector<uint32_t>& outputBinary) {
|
||||||
using namespace spvtools;
|
using namespace spvtools;
|
||||||
|
OptimizerOptions options;
|
||||||
|
options.set_run_validator(false);
|
||||||
|
|
||||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||||
optimizer.RegisterPass(RebaseInstanceIndexPass::CreateRebaseInstanceIndexPass());
|
optimizer.RegisterPass(RebaseInstanceIndexPass::CreateRebaseInstanceIndexPass());
|
||||||
|
|
||||||
return RunOptimizerChecked("RebaseInstanceIndexForVulkan", optimizer, inputBinary,
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
outputBinary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
|
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary) {
|
Vector<uint32_t>& outputBinary) {
|
||||||
using namespace spvtools;
|
using namespace spvtools;
|
||||||
|
OptimizerOptions options;
|
||||||
|
options.set_run_validator(false);
|
||||||
|
|
||||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||||
optimizer.RegisterPass(DecoratePositionInvariantPass::CreateDecoratePositionInvariantPass());
|
optimizer.RegisterPass(DecoratePositionInvariantPass::CreateDecoratePositionInvariantPass());
|
||||||
|
|
||||||
return RunOptimizerChecked("DecoratePositionInvariantForVulkan", optimizer, inputBinary,
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
outputBinary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
|
bool ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
|
||||||
@@ -787,9 +598,6 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
outputBinary.insert(outputBinary.begin() + static_cast<std::ptrdiff_t>(capabilityInsertOffset),
|
outputBinary.insert(outputBinary.begin() + static_cast<std::ptrdiff_t>(capabilityInsertOffset),
|
||||||
addedCapabilities.begin(), addedCapabilities.end());
|
addedCapabilities.begin(), addedCapabilities.end());
|
||||||
// Hand-rolled word walk, so no Optimizer wrapper ever sees this rewrite;
|
|
||||||
// check the modified module explicitly in validating lanes.
|
|
||||||
ValidateOrLatch("UseUnformattedFloatStorageImagesForVulkan", outputBinary);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,27 +101,6 @@ namespace MobileGL {
|
|||||||
// it, the second eglInitialize of a process comes back up unwarmed and with
|
// it, the second eglInitialize of a process comes back up unwarmed and with
|
||||||
// no way left to warm it.
|
// no way left to warm it.
|
||||||
static void ResetPrewarmLatch();
|
static void ResetPrewarmLatch();
|
||||||
|
|
||||||
// Test-environment SPIR-V validation. When enabled, every Optimizer wrapper
|
|
||||||
// in this file validates its OUTPUT binary - the bytes a driver can actually
|
|
||||||
// receive - and a failure logs the VUID (via MGLOG_I; see the consumer for
|
|
||||||
// why not MGLOG_E) and bumps the failure latch below WITHOUT changing the
|
|
||||||
// wrapper's return value: control flow must stay identical between the
|
|
||||||
// validating and shipping configurations, or fail-open call sites would make
|
|
||||||
// the two render differently. Resolved lazily from MOBILEGL_VALIDATE_SPIRV;
|
|
||||||
// defaults on for desktop/CI/WSL builds and off for device (__ANDROID__)
|
|
||||||
// builds. The setter wins over the environment and is safe to call from test
|
|
||||||
// fixtures at any time.
|
|
||||||
static bool SpirvValidationEnabled();
|
|
||||||
static void SetSpirvValidationEnabled(bool enabled);
|
|
||||||
|
|
||||||
// The test-lane enforcement signal: total validation failures observed this
|
|
||||||
// process. Tests snapshot it, run the operation under scrutiny, and assert
|
|
||||||
// on the delta. NoteSpirvValidationFailure is for validation done outside
|
|
||||||
// this file (ProgramFactory::ValidateTransformedSpirv); it returns the new
|
|
||||||
// total.
|
|
||||||
static Uint64 SpirvValidationFailureCount();
|
|
||||||
static Uint64 NoteSpirvValidationFailure();
|
|
||||||
};
|
};
|
||||||
} // namespace ShaderTranspiler
|
} // namespace ShaderTranspiler
|
||||||
} // namespace MG_Util
|
} // namespace MG_Util
|
||||||
|
|||||||
@@ -1,250 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.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
|
|
||||||
//
|
|
||||||
// Derived from SPIRV-Tools' PrivateToLocalPass (source/opt/private_to_local_pass.cpp,
|
|
||||||
// Copyright (c) 2017 Google Inc., Apache License 2.0). The one behavioral difference is
|
|
||||||
// the entry-point restriction in FindEntryLocalFunction; see the header for why.
|
|
||||||
|
|
||||||
#include "PrivateToEntryLocalPass.h"
|
|
||||||
|
|
||||||
#include "source/opt/ir_context.h"
|
|
||||||
#include "source/opt/type_manager.h"
|
|
||||||
#include "source/spirv_constant.h"
|
|
||||||
#include "source/util/make_unique.h"
|
|
||||||
|
|
||||||
#include <cassert>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
#include <unordered_set>
|
|
||||||
|
|
||||||
namespace MobileGL {
|
|
||||||
namespace MG_Util {
|
|
||||||
namespace ShaderTranspiler {
|
|
||||||
namespace {
|
|
||||||
using spvtools::opt::BasicBlock;
|
|
||||||
using spvtools::opt::Function;
|
|
||||||
using spvtools::opt::Instruction;
|
|
||||||
using spvtools::opt::Operand;
|
|
||||||
|
|
||||||
constexpr uint32_t kVariableStorageClassInIdx = 0;
|
|
||||||
constexpr uint32_t kSpvTypePointerTypeIdInIdx = 1;
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
spvtools::opt::Pass::Status PrivateToEntryLocalPass::Process() {
|
|
||||||
// Private variables require the Shader capability; with Addresses the
|
|
||||||
// rewrite below is not guaranteed sound (variable pointers may escape).
|
|
||||||
if (context()->get_feature_mgr()->HasCapability(spv::Capability::Addresses)) {
|
|
||||||
return Status::SuccessWithoutChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::pair<Instruction*, Function*>> variablesToMove;
|
|
||||||
std::unordered_set<uint32_t> localizedVariables;
|
|
||||||
for (auto& inst : context()->types_values()) {
|
|
||||||
if (inst.opcode() != spv::Op::OpVariable) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (spv::StorageClass(inst.GetSingleWordInOperand(kVariableStorageClassInIdx)) !=
|
|
||||||
spv::StorageClass::Private) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Function* targetFunction = FindEntryLocalFunction(inst);
|
|
||||||
if (targetFunction != nullptr) {
|
|
||||||
variablesToMove.push_back({&inst, targetFunction});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool modified = !variablesToMove.empty();
|
|
||||||
for (auto& p : variablesToMove) {
|
|
||||||
if (!MoveVariable(p.first, p.second)) {
|
|
||||||
return Status::Failure;
|
|
||||||
}
|
|
||||||
localizedVariables.insert(p.first->result_id());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (get_module()->version() >= SPV_SPIRV_VERSION_WORD(1, 4)) {
|
|
||||||
// SPIR-V 1.4+ lists statically-used Private variables on OpEntryPoint;
|
|
||||||
// drop the ones that just stopped being Private. Dead code for the 1.3
|
|
||||||
// modules MobileGL emits, kept for robustness.
|
|
||||||
for (auto& entry : get_module()->entry_points()) {
|
|
||||||
std::vector<Operand> newOperands;
|
|
||||||
for (uint32_t i = 0; i < entry.NumInOperands(); ++i) {
|
|
||||||
// Execution model, function id and name are always kept.
|
|
||||||
if (i < 3 || !localizedVariables.count(entry.GetSingleWordInOperand(i))) {
|
|
||||||
newOperands.push_back(entry.GetInOperand(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (newOperands.size() != entry.NumInOperands()) {
|
|
||||||
entry.SetInOperands(std::move(newOperands));
|
|
||||||
context()->AnalyzeUses(&entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return modified ? Status::SuccessWithChange : Status::SuccessWithoutChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
Function* PrivateToEntryLocalPass::FindEntryLocalFunction(const Instruction& inst) const {
|
|
||||||
bool foundFirstUse = false;
|
|
||||||
Function* targetFunction = nullptr;
|
|
||||||
const uint32_t variableId = inst.result_id();
|
|
||||||
context()->get_def_use_mgr()->ForEachUser(
|
|
||||||
variableId, [&targetFunction, &foundFirstUse, variableId, this](Instruction* use) {
|
|
||||||
BasicBlock* currentBlock = context()->get_instr_block(use);
|
|
||||||
if (currentBlock == nullptr) {
|
|
||||||
// Module-scope users: OpName, decorations, the OpEntryPoint
|
|
||||||
// interface list. None of them pins the variable to a function,
|
|
||||||
// but a debug-info extended instruction would go stale after the
|
|
||||||
// move, so treat it as disqualifying.
|
|
||||||
if (use->opcode() == spv::Op::OpExtInst) {
|
|
||||||
foundFirstUse = true;
|
|
||||||
targetFunction = nullptr;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!IsValidUse(use, variableId)) {
|
|
||||||
foundFirstUse = true;
|
|
||||||
targetFunction = nullptr;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Function* currentFunction = currentBlock->GetParent();
|
|
||||||
if (!foundFirstUse) {
|
|
||||||
foundFirstUse = true;
|
|
||||||
targetFunction = currentFunction;
|
|
||||||
} else if (targetFunction != currentFunction) {
|
|
||||||
targetFunction = nullptr;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (targetFunction != nullptr && !IsEntryPointFunction(targetFunction)) {
|
|
||||||
// The whole point of this derivative: a helper can be called more than
|
|
||||||
// once per invocation, and Function storage would reset the variable at
|
|
||||||
// every call.
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return targetFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PrivateToEntryLocalPass::IsEntryPointFunction(Function* function) const {
|
|
||||||
for (auto& entry : get_module()->entry_points()) {
|
|
||||||
if (entry.GetSingleWordInOperand(1) == function->result_id()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PrivateToEntryLocalPass::IsValidUse(const Instruction* inst, uint32_t variableId) const {
|
|
||||||
// The cases here have to match the cases in UpdateUse: a use the rewrite
|
|
||||||
// does not know how to update disqualifies the variable.
|
|
||||||
switch (inst->opcode()) {
|
|
||||||
case spv::Op::OpLoad:
|
|
||||||
case spv::Op::OpImageTexelPointer: // treat like a load
|
|
||||||
return true;
|
|
||||||
case spv::Op::OpStore:
|
|
||||||
// Storing the variable's ADDRESS somewhere else escapes it.
|
|
||||||
return inst->GetOperand(1).AsId() != variableId;
|
|
||||||
case spv::Op::OpAccessChain:
|
|
||||||
return context()->get_def_use_mgr()->WhileEachUser(
|
|
||||||
inst, [this, inst](const Instruction* user) {
|
|
||||||
return IsValidUse(user, inst->result_id());
|
|
||||||
});
|
|
||||||
case spv::Op::OpName:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return spvOpcodeIsDecoration(inst->opcode());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PrivateToEntryLocalPass::MoveVariable(Instruction* variable, Function* function) {
|
|
||||||
// Remove from the global section and re-insert at the head of the entry
|
|
||||||
// function's first block, Function-storage variables' one legal position.
|
|
||||||
variable->RemoveFromList();
|
|
||||||
std::unique_ptr<Instruction> var(variable); // take ownership
|
|
||||||
context()->ForgetUses(variable);
|
|
||||||
|
|
||||||
variable->SetInOperand(kVariableStorageClassInIdx,
|
|
||||||
{uint32_t(spv::StorageClass::Function)});
|
|
||||||
|
|
||||||
const uint32_t newTypeId = GetNewType(variable->type_id());
|
|
||||||
if (newTypeId == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
variable->SetResultType(newTypeId);
|
|
||||||
|
|
||||||
context()->AnalyzeUses(variable);
|
|
||||||
context()->set_instr_block(variable, &*function->begin());
|
|
||||||
function->begin()->begin()->InsertBefore(std::move(var));
|
|
||||||
|
|
||||||
return UpdateUses(variable);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t PrivateToEntryLocalPass::GetNewType(uint32_t oldTypeId) {
|
|
||||||
auto* typeMgr = context()->get_type_mgr();
|
|
||||||
Instruction* oldTypeInst = get_def_use_mgr()->GetDef(oldTypeId);
|
|
||||||
const uint32_t pointeeTypeId =
|
|
||||||
oldTypeInst->GetSingleWordInOperand(kSpvTypePointerTypeIdInIdx);
|
|
||||||
const uint32_t newTypeId =
|
|
||||||
typeMgr->FindPointerToType(pointeeTypeId, spv::StorageClass::Function);
|
|
||||||
if (newTypeId != 0) {
|
|
||||||
context()->UpdateDefUse(context()->get_def_use_mgr()->GetDef(newTypeId));
|
|
||||||
}
|
|
||||||
return newTypeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PrivateToEntryLocalPass::UpdateUse(Instruction* inst, Instruction* user) {
|
|
||||||
// The cases here have to match the cases in IsValidUse.
|
|
||||||
switch (inst->opcode()) {
|
|
||||||
case spv::Op::OpLoad:
|
|
||||||
case spv::Op::OpStore:
|
|
||||||
case spv::Op::OpImageTexelPointer: // treat like a load
|
|
||||||
// Fine as-is: their type is the pointed-to type, which is unchanged.
|
|
||||||
break;
|
|
||||||
case spv::Op::OpAccessChain: {
|
|
||||||
context()->ForgetUses(inst);
|
|
||||||
const uint32_t newTypeId = GetNewType(inst->type_id());
|
|
||||||
if (newTypeId == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
inst->SetResultType(newTypeId);
|
|
||||||
context()->AnalyzeUses(inst);
|
|
||||||
if (!UpdateUses(inst)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case spv::Op::OpName:
|
|
||||||
case spv::Op::OpEntryPoint: // handled separately in Process()
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
assert(spvOpcodeIsDecoration(inst->opcode()) &&
|
|
||||||
"PrivateToEntryLocalPass: unexpected use opcode");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
(void)user;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PrivateToEntryLocalPass::UpdateUses(Instruction* inst) {
|
|
||||||
const uint32_t id = inst->result_id();
|
|
||||||
std::vector<Instruction*> uses;
|
|
||||||
context()->get_def_use_mgr()->ForEachUser(id,
|
|
||||||
[&uses](Instruction* use) { uses.push_back(use); });
|
|
||||||
for (Instruction* use : uses) {
|
|
||||||
if (!UpdateUse(use, inst)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
spvtools::Optimizer::PassToken PrivateToEntryLocalPass::CreatePrivateToEntryLocalPass() {
|
|
||||||
return spvtools::Optimizer::PassToken(
|
|
||||||
spvtools::MakeUnique<PrivateToEntryLocalPass>());
|
|
||||||
}
|
|
||||||
} // namespace ShaderTranspiler
|
|
||||||
} // namespace MG_Util
|
|
||||||
} // namespace MobileGL
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.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 "spirv-tools/optimizer.hpp"
|
|
||||||
#include "source/opt/pass.h"
|
|
||||||
|
|
||||||
namespace MobileGL {
|
|
||||||
namespace MG_Util {
|
|
||||||
namespace ShaderTranspiler {
|
|
||||||
// AggressiveDCE treats every store to a Private global as an observable side
|
|
||||||
// effect while the entry point still contains any OpFunctionCall, so a dead
|
|
||||||
// vertex-input -> Private-shim chain (Iris rewrites unused legacy attributes
|
|
||||||
// into exactly this shape) survives the whole optimizer chain. Rewriting such
|
|
||||||
// a variable to Function storage unlocks ADCE without inlining anything.
|
|
||||||
//
|
|
||||||
// Upstream's PrivateToLocalPass does that rewrite for a Private variable used
|
|
||||||
// in ANY single function - which is unsound here: a Function-storage variable
|
|
||||||
// is recreated on every call, so a Private global that carries state across
|
|
||||||
// repeated calls of one helper (a memoized init flag, LCG rand state) would
|
|
||||||
// silently lose it. This derivative applies the same rewrite restricted to
|
|
||||||
// variables whose only using function is an entry point: an entry point runs
|
|
||||||
// once per invocation, so the two lifetimes are indistinguishable there.
|
|
||||||
//
|
|
||||||
// Derived from SPIRV-Tools' PrivateToLocalPass
|
|
||||||
// (source/opt/private_to_local_pass.cpp, Copyright (c) 2017 Google Inc.,
|
|
||||||
// Apache License 2.0).
|
|
||||||
class PrivateToEntryLocalPass final : public spvtools::opt::Pass {
|
|
||||||
public:
|
|
||||||
const char* name() const override { return "mobilegl-private-to-entry-local"; }
|
|
||||||
Status Process() override;
|
|
||||||
|
|
||||||
static spvtools::Optimizer::PassToken CreatePrivateToEntryLocalPass();
|
|
||||||
|
|
||||||
private:
|
|
||||||
// The single entry-point function every block-level use of the variable
|
|
||||||
// lives in, or nullptr when the uses span functions, include an opcode the
|
|
||||||
// rewrite cannot update, or belong to a non-entry function.
|
|
||||||
spvtools::opt::Function* FindEntryLocalFunction(const spvtools::opt::Instruction& inst) const;
|
|
||||||
bool IsEntryPointFunction(spvtools::opt::Function* function) const;
|
|
||||||
bool IsValidUse(const spvtools::opt::Instruction* inst, uint32_t variableId) const;
|
|
||||||
bool MoveVariable(spvtools::opt::Instruction* variable, spvtools::opt::Function* function);
|
|
||||||
uint32_t GetNewType(uint32_t oldTypeId);
|
|
||||||
bool UpdateUse(spvtools::opt::Instruction* inst, spvtools::opt::Instruction* user);
|
|
||||||
bool UpdateUses(spvtools::opt::Instruction* inst);
|
|
||||||
};
|
|
||||||
} // namespace ShaderTranspiler
|
|
||||||
} // namespace MG_Util
|
|
||||||
} // namespace MobileGL
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.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 "StripUniformLocationsPass.h"
|
|
||||||
|
|
||||||
#include "source/opt/ir_context.h"
|
|
||||||
#include "source/util/make_unique.h"
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace MobileGL {
|
|
||||||
namespace MG_Util {
|
|
||||||
namespace ShaderTranspiler {
|
|
||||||
spvtools::opt::Pass::Status StripUniformLocationsPass::Process() {
|
|
||||||
using spvtools::opt::Instruction;
|
|
||||||
|
|
||||||
std::vector<Instruction*> toKill;
|
|
||||||
for (auto& annotation : get_module()->annotations()) {
|
|
||||||
if (annotation.opcode() != spv::Op::OpDecorate) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (annotation.GetSingleWordInOperand(1) !=
|
|
||||||
static_cast<uint32_t>(spv::Decoration::Location)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Instruction* target =
|
|
||||||
get_def_use_mgr()->GetDef(annotation.GetSingleWordInOperand(0));
|
|
||||||
if (target == nullptr || target->opcode() != spv::Op::OpVariable) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
switch (spv::StorageClass(target->GetSingleWordInOperand(0))) {
|
|
||||||
case spv::StorageClass::UniformConstant:
|
|
||||||
case spv::StorageClass::Uniform:
|
|
||||||
case spv::StorageClass::StorageBuffer:
|
|
||||||
toKill.push_back(&annotation);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Instruction* inst : toKill) {
|
|
||||||
context()->KillInst(inst);
|
|
||||||
}
|
|
||||||
return toKill.empty() ? Status::SuccessWithoutChange : Status::SuccessWithChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
spvtools::Optimizer::PassToken StripUniformLocationsPass::CreateStripUniformLocationsPass() {
|
|
||||||
return spvtools::Optimizer::PassToken(
|
|
||||||
spvtools::MakeUnique<StripUniformLocationsPass>());
|
|
||||||
}
|
|
||||||
} // namespace ShaderTranspiler
|
|
||||||
} // namespace MG_Util
|
|
||||||
} // namespace MobileGL
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.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 "spirv-tools/optimizer.hpp"
|
|
||||||
#include "source/opt/pass.h"
|
|
||||||
|
|
||||||
namespace MobileGL {
|
|
||||||
namespace MG_Util {
|
|
||||||
namespace ShaderTranspiler {
|
|
||||||
// glslang's relaxed GL path keeps `layout(location = N) uniform ...` as a
|
|
||||||
// Location decoration on the UniformConstant/Uniform variable, which Vulkan
|
|
||||||
// forbids ([VUID-StandaloneSpirv-Location-06672]). Nothing downstream reads
|
|
||||||
// it: GL-side uniform locations come from the phase-A glslang reflection,
|
|
||||||
// Vulkan binding assignment goes by (kind, name), and SPIRV-Cross's ESSL
|
|
||||||
// resolves uniforms by name. Strip it so the driver-bound module is valid.
|
|
||||||
class StripUniformLocationsPass final : public spvtools::opt::Pass {
|
|
||||||
public:
|
|
||||||
const char* name() const override { return "mobilegl-strip-uniform-locations"; }
|
|
||||||
Status Process() override;
|
|
||||||
|
|
||||||
static spvtools::Optimizer::PassToken CreateStripUniformLocationsPass();
|
|
||||||
};
|
|
||||||
} // namespace ShaderTranspiler
|
|
||||||
} // namespace MG_Util
|
|
||||||
} // namespace MobileGL
|
|
||||||
@@ -105,46 +105,8 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int TMglGlslIoResolver::resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) {
|
int TMglGlslIoResolver::resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) {
|
||||||
// NO dead-vertex-input early-out here, deliberately - the skip belongs in
|
if (!ent.live && stage == EShLangVertex && ent.symbol->getType().getQualifier().isPipeInput()) {
|
||||||
// reserverStorageSlot() and ONLY there.
|
return ent.newLocation = -1;
|
||||||
//
|
|
||||||
// Skipping RESERVATION is the GL semantic: only active inputs get generic attribute
|
|
||||||
// locations, so a dead declaration must not consume a slot an active input should
|
|
||||||
// have. Skipping RESOLUTION as well used to look like the same statement, but it is a
|
|
||||||
// different one: it leaves the variable with no layoutLocation, and glslang still
|
|
||||||
// EMITS it - a declared input is in the shader's linker objects and therefore in the
|
|
||||||
// entry point's interface. The result is an OpVariable of storage class Input with no
|
|
||||||
// Location decoration, which SPIR-V forbids
|
|
||||||
// (VUID-StandaloneSpirv-Location-04916). lavapipe tolerates it; Adreno rejects the
|
|
||||||
// whole pipeline with VK_ERROR_UNKNOWN, which is how this shipped undetected - every
|
|
||||||
// desktop gate, retrace corpus included, is blind to it.
|
|
||||||
//
|
|
||||||
// Found 2026-08-11 on an Adreno 830: the Iris weather program (mc_midTexCoord among
|
|
||||||
// seven attributes, only some of them glBindAttribLocation-bound) died at the first
|
|
||||||
// rainy-world draw, 100% reproducible, programHash 0x4a7e9a37fb49caa1.
|
|
||||||
//
|
|
||||||
// They cannot simply be handed to the base resolver either. Auto-assignment for inputs
|
|
||||||
// WITHOUT an explicit binding happens entirely in the resolve pass, in sort order, so a
|
|
||||||
// dead declaration reaching the free-slot search first would take location 0 and push
|
|
||||||
// the active input up - which is precisely the GL violation the reservation skip
|
|
||||||
// exists to prevent (ProgramTest.InactiveExplicitVertexBindingsDoNotReserveLocations
|
|
||||||
// pins it: Iris injects Position/UV0 into packs that actually read vaPosition).
|
|
||||||
//
|
|
||||||
// So dead inputs get their locations from the TOP of the attribute range downward,
|
|
||||||
// while the base resolver hands active ones out from 0 upward. Both properties hold at
|
|
||||||
// once: every emitted input carries a Location, and no active input is displaced. The
|
|
||||||
// two allocators can only meet if live + dead exceed the attribute limit, which is an
|
|
||||||
// over-subscribed program GL would reject anyway; if that happens we leave the
|
|
||||||
// variable to the base resolver rather than hand out a colliding location.
|
|
||||||
const glslang::TType& type = ent.symbol->getType();
|
|
||||||
if (!ent.live && stage == EShLangVertex && type.getQualifier().isPipeInput() &&
|
|
||||||
!type.getQualifier().hasLocation() && !type.isBuiltIn()) {
|
|
||||||
const int size = std::max(1, glslang::TIntermediate::computeTypeLocationSize(type, stage));
|
|
||||||
if (m_nextInactiveVertexInLocation - (size - 1) >= 0) {
|
|
||||||
m_nextInactiveVertexInLocation -= (size - 1);
|
|
||||||
ent.symbol->getWritableType().getQualifier().layoutLocation = m_nextInactiveVertexInLocation;
|
|
||||||
--m_nextInactiveVertexInLocation;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return TDefaultGlslIoResolver::resolveInOutLocation(stage, ent);
|
return TDefaultGlslIoResolver::resolveInOutLocation(stage, ent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,13 +51,5 @@ namespace MobileGL {
|
|||||||
std::map<glslang::TString, int> m_plainUniformLocationSizeByName;
|
std::map<glslang::TString, int> m_plainUniformLocationSizeByName;
|
||||||
std::map<glslang::TString, int> m_plainUniformLocationByName;
|
std::map<glslang::TString, int> m_plainUniformLocationByName;
|
||||||
bool m_plainUniformLocationsAssigned = false;
|
bool m_plainUniformLocationsAssigned = false;
|
||||||
// Descending allocator for INACTIVE vertex inputs (see resolveInOutLocation): they
|
|
||||||
// still have to carry a Location because glslang emits them, but they must not take a
|
|
||||||
// slot an active input would get. 15, not 31: the location survives into the ESSL
|
|
||||||
// SPIRV-Cross emits for DirectGLES, and GL/ES only guarantee GL_MAX_VERTEX_ATTRIBS
|
|
||||||
// >= 16 - a location of 31 makes the generated shader fail to compile on a real ES
|
|
||||||
// driver (caught by the super-duper-vanilla and chocapic retrace fixtures).
|
|
||||||
static constexpr int kInactiveVertexInLocationTop = 15;
|
|
||||||
int m_nextInactiveVertexInLocation = kInactiveVertexInLocationTop;
|
|
||||||
};
|
};
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
@@ -131,11 +131,6 @@ bool LoadMobileGL(const Request& request, std::string& error) {
|
|||||||
setenv("MOBILEGL_BACKEND_TYPE", request.backend.c_str(), 1);
|
setenv("MOBILEGL_BACKEND_TYPE", request.backend.c_str(), 1);
|
||||||
setenv("MOBILEGL_TRACE_LIBRARY", request.mobileGlLibrary.c_str(), 1);
|
setenv("MOBILEGL_TRACE_LIBRARY", request.mobileGlLibrary.c_str(), 1);
|
||||||
setenv("MOBILEGL_TRACE_SKIP_AUTODESTROY", "1", 1);
|
setenv("MOBILEGL_TRACE_SKIP_AUTODESTROY", "1", 1);
|
||||||
// Retrace is a test lane on every platform, including the Android AVD one where
|
|
||||||
// MobileGL's __ANDROID__ default would leave validation off. No overwrite: an outer
|
|
||||||
// MOBILEGL_VALIDATE_SPIRV=0 must keep working as the escape hatch, and retracing the
|
|
||||||
// exact shipping pipeline must stay possible.
|
|
||||||
setenv("MOBILEGL_VALIDATE_SPIRV", "1", 0);
|
|
||||||
setenv("MOBILEGL_TRACE_SURFACE", request.usePbuffer ? "pbuffer" : "window", 1);
|
setenv("MOBILEGL_TRACE_SURFACE", request.usePbuffer ? "pbuffer" : "window", 1);
|
||||||
if (request.backend == "DirectVulkan") {
|
if (request.backend == "DirectVulkan") {
|
||||||
setenv("MOBILEGL_MAGMA_R11G11B10F_FALLBACK", "1", 1);
|
setenv("MOBILEGL_MAGMA_R11G11B10F_FALLBACK", "1", 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user