mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
Compare commits
78
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24dfbb41f9 | ||
|
|
ca7878bf3a | ||
|
|
e418063b08 | ||
|
|
b43ec25bd7 | ||
|
|
525607bad6 | ||
|
|
b045024b6c | ||
|
|
14dfbeeed9 | ||
|
|
d7e79409b3 | ||
|
|
ca3b524396 | ||
|
|
071c8eb673 | ||
|
|
51a43518ac | ||
|
|
34ff95f6f5 | ||
|
|
b9d1504cc5 | ||
|
|
a223499143 | ||
|
|
5dca617f01 | ||
|
|
eb8ef893be | ||
|
|
20567fba6d | ||
|
|
e3f44e8da1 | ||
|
|
9f79a88af5 | ||
|
|
6bf32acdef | ||
|
|
23b53eacce | ||
|
|
e829e70d8b | ||
|
|
7e765e1535 | ||
|
|
bf6061811f | ||
|
|
87750c3b21 | ||
|
|
45b309db37 | ||
|
|
403c82ac4a | ||
|
|
827d46cad3 | ||
|
|
1748da0443 | ||
|
|
7efee8e3e6 | ||
|
|
08ca897a07 | ||
|
|
98f2a55214 | ||
|
|
cedc257566 | ||
|
|
821c0e0d4e | ||
|
|
bd9680ad67 | ||
|
|
6375e07030 | ||
|
|
53cac39d4e | ||
|
|
f6b1ea635b | ||
|
|
8b2711e32a | ||
|
|
9776cc8047 | ||
|
|
3b0591e0ba | ||
|
|
e62f158c22 | ||
|
|
7769156cfc | ||
|
|
0ecfdff4e7 | ||
|
|
6df5a6137f | ||
|
|
b3794f4e6a | ||
|
|
14d3901d30 | ||
|
|
d4766513e4 | ||
|
|
72dc7aa6aa | ||
|
|
9d1b280375 | ||
|
|
8acd885594 | ||
|
|
10ff5e2b18 | ||
|
|
a6e52476f3 | ||
|
|
0deff52a1b | ||
|
|
50fefca959 | ||
|
|
42ad62b54c | ||
|
|
f41403e227 | ||
|
|
5fbb17f6b9 | ||
|
|
92d8f7269b | ||
|
|
822e405c77 | ||
|
|
b8233f9c4e | ||
|
|
91475a7b6f | ||
|
|
cee17025a0 | ||
|
|
9642ae4d20 | ||
|
|
595d140036 | ||
|
|
b62d1f2078 | ||
|
|
5e82ff968a | ||
|
|
6a80a82dd3 | ||
|
|
f9182a5ca3 | ||
|
|
f37b511fca | ||
|
|
38027d21f8 | ||
|
|
dd2a62228f | ||
|
|
373aa44dd7 | ||
|
|
96646df12e | ||
|
|
f20b20e643 | ||
|
|
2587814970 | ||
|
|
43398e33e8 | ||
|
|
b7557d6615 |
@@ -209,7 +209,7 @@ jobs:
|
||||
- name: Load trace cases
|
||||
id: trace-cases
|
||||
run: |
|
||||
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk)" >> "$GITHUB_OUTPUT"
|
||||
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk-matrix)" >> "$GITHUB_OUTPUT"
|
||||
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
trace-fixtures:
|
||||
@@ -337,13 +337,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
backend:
|
||||
- name: DirectGLES
|
||||
gpu: software
|
||||
- name: DirectVulkan
|
||||
gpu: lavapipe
|
||||
case: ${{ fromJSON(needs.trace-cases.outputs.android) }}
|
||||
matrix: ${{ fromJSON(needs.trace-cases.outputs.android) }}
|
||||
steps:
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@v1.0
|
||||
|
||||
@@ -491,6 +491,7 @@ jobs:
|
||||
- benchmark
|
||||
- integration
|
||||
outputs:
|
||||
matrix: ${{ steps.trace-cases.outputs.matrix }}
|
||||
names: ${{ steps.trace-cases.outputs.names }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
@@ -498,7 +499,9 @@ jobs:
|
||||
|
||||
- name: Load trace cases
|
||||
id: trace-cases
|
||||
run: echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
|
||||
run: |
|
||||
echo "matrix=$(python3 tools/trace_replay/trace_cases.py --ci --format github-test-matrix)" >> "$GITHUB_OUTPUT"
|
||||
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
trace-fixtures:
|
||||
name: trace fixture (${{ matrix.case }})
|
||||
@@ -577,11 +580,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
backend:
|
||||
- DirectGLES
|
||||
- DirectVulkan
|
||||
case: ${{ fromJSON(needs.trace-cases.outputs.names) }}
|
||||
matrix: ${{ fromJSON(needs.trace-cases.outputs.matrix) }}
|
||||
|
||||
steps:
|
||||
- name: Set Swap Space
|
||||
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
################################################################################
|
||||
################################################################################
|
||||
# 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。
|
||||
################################################################################
|
||||
|
||||
@@ -16,6 +16,9 @@ MobileGLCodeManager
|
||||
MobileGL/MG_Test/build
|
||||
/build_*
|
||||
/cmake-build*
|
||||
/build-*/
|
||||
/local.properties
|
||||
/.jspace/
|
||||
.idea
|
||||
MobileGL/MG*/build*
|
||||
MobileGL/MG*/cmake-build*
|
||||
|
||||
+44
-1
@@ -182,6 +182,7 @@ set(ENABLE_SPVREMAPPER OFF CACHE BOOL "Enable SPVRemapper" FORCE)
|
||||
set(ENABLE_OPT ON CACHE BOOL "Enable SPIRV-Tools opt usage in glslang" FORCE)
|
||||
set(BUILD_EXTERNAL ON CACHE BOOL "Build external deps in External/" FORCE)
|
||||
set(ENABLE_GLSLANG_INSTALL OFF CACHE BOOL "Install glslang targets" FORCE)
|
||||
set(SPIRV_SKIP_EXECUTABLES ON CACHE BOOL "Skip building SPIRV-Tools executables" FORCE)
|
||||
|
||||
set(SPIRV_CROSS_C_API ON CACHE BOOL "Enable C API" FORCE)
|
||||
set(SPIRV_CROSS_ENABLE_GLSL ON CACHE BOOL "Enable GLSL backend" FORCE)
|
||||
@@ -198,7 +199,6 @@ set(SPIRV_REFLECT_ENABLE_ASSERTS OFF CACHE BOOL "Enable asserts for debugging"
|
||||
set(SPIRV_REFLECT_ENABLE_ASAN OFF CACHE BOOL "Use address sanitization" FORCE)
|
||||
set(SPIRV_REFLECT_INSTALL OFF CACHE BOOL "Whether to install" FORCE)
|
||||
|
||||
# add_subdirectory(3rdparty/DiligentCore)
|
||||
add_subdirectory(3rdparty/glslang)
|
||||
add_subdirectory(3rdparty/SPIRV-Cross)
|
||||
add_subdirectory(3rdparty/VulkanMemoryAllocator)
|
||||
@@ -210,6 +210,23 @@ set(XXHASH_BUILD_XXHSUM OFF)
|
||||
option(BUILD_SHARED_LIBS OFF)
|
||||
add_subdirectory(3rdparty/xxHash/build/cmake xxhash_build EXCLUDE_FROM_ALL)
|
||||
|
||||
# Diligent-based backend. Enabled by default on local builds; only the Vulkan
|
||||
# engine from DiligentCore is built. Added after the other 3rdparty projects so
|
||||
# DiligentCore reuses the glslang / SPIRV-Cross / SPIRV-Tools / xxHash targets
|
||||
# already defined by MobileGL instead of building its bundled copies.
|
||||
option(MOBILEGL_ENABLE_DILIGENT "Enable the Diligent/Vulkan backend" ON)
|
||||
if(MOBILEGL_ENABLE_DILIGENT)
|
||||
set(DILIGENT_NO_DIRECT3D11 ON CACHE BOOL "Disable Direct3D11 backend" FORCE)
|
||||
set(DILIGENT_NO_DIRECT3D12 ON CACHE BOOL "Disable Direct3D12 backend" FORCE)
|
||||
set(DILIGENT_NO_OPENGL ON CACHE BOOL "Disable OpenGL backend" FORCE)
|
||||
set(DILIGENT_NO_METAL ON CACHE BOOL "Disable Metal backend" FORCE)
|
||||
set(DILIGENT_NO_WEBGPU ON CACHE BOOL "Disable WebGPU backend" FORCE)
|
||||
set(DILIGENT_NO_ARCHIVER ON CACHE BOOL "Disable Archiver" FORCE)
|
||||
set(DILIGENT_BUILD_TESTS OFF CACHE BOOL "Build Diligent tests" FORCE)
|
||||
set(DILIGENT_INSTALL_CORE OFF CACHE BOOL "Install DiligentCore" FORCE)
|
||||
add_subdirectory(3rdparty/DiligentCore)
|
||||
endif()
|
||||
|
||||
set(TRACY_ENABLE ${MOBILEGL_ENABLE_TRACY} CACHE BOOL "Enable Tracy, this is an internal variable" FORCE)
|
||||
|
||||
if (TRACY_ENABLE)
|
||||
@@ -285,6 +302,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||
@@ -394,6 +412,14 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp
|
||||
)
|
||||
|
||||
if(MOBILEGL_ENABLE_DILIGENT)
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Backend/Diligent/BackendObject_Diligent.cpp
|
||||
MobileGL/MG_Backend/Diligent/DiligentVulkan.cpp
|
||||
MobileGL/MG_Backend/Diligent/Renderer/DiligentRenderer.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (APPLE AND NOT MOBILEGL_IOS)
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp
|
||||
@@ -434,6 +460,21 @@ set(MOBILEGL_LINK_LIBRARIES
|
||||
Threads::Threads
|
||||
)
|
||||
|
||||
if(MOBILEGL_ENABLE_DILIGENT)
|
||||
list(APPEND MOBILEGL_LINK_LIBRARIES
|
||||
Diligent-GraphicsEngineVk-static
|
||||
Diligent-GraphicsEngine
|
||||
Diligent-GraphicsEngineNextGenBase
|
||||
Diligent-GraphicsAccessories
|
||||
Diligent-ShaderTools
|
||||
Diligent-GraphicsTools
|
||||
Diligent-Common
|
||||
Diligent-Primitives
|
||||
Diligent-TargetPlatform
|
||||
Vulkan::Headers
|
||||
)
|
||||
endif()
|
||||
|
||||
set(MOBILEGL_COMPILE_DEF
|
||||
-DVMA_STATIC_VULKAN_FUNCTIONS=0
|
||||
-DVMA_DYNAMIC_VULKAN_FUNCTIONS=1
|
||||
@@ -499,6 +540,7 @@ target_compile_definitions(${CMAKE_PROJECT_NAME}
|
||||
${MOBILEGL_COMPILE_DEF}
|
||||
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
|
||||
$<$<BOOL:${MOBILEGL_TRACE_ANGLE_VARIANTS}>:MOBILEGL_TRACE_ANGLE_VARIANTS=1>
|
||||
$<$<BOOL:${MOBILEGL_ENABLE_DILIGENT}>:MOBILEGL_ENABLE_DILIGENT=1>
|
||||
)
|
||||
|
||||
if(UNIX AND NOT APPLE AND NOT ANDROID)
|
||||
@@ -557,6 +599,7 @@ if(NOT ANDROID)
|
||||
PUBLIC
|
||||
${MOBILEGL_COMPILE_DEF}
|
||||
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
|
||||
$<$<BOOL:${MOBILEGL_ENABLE_DILIGENT}>:MOBILEGL_ENABLE_DILIGENT=1>
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
# Handoff: Diligent/Vulkan GL3.2 Backend for MobileGL
|
||||
|
||||
Date: 2026-08-18
|
||||
Branch: `feat/diligent-vulkan-backend`
|
||||
Repo: `~/MobileGL-dev`
|
||||
Status: **Active work-in-progress. Do not mark complete yet.**
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Implement a complete OpenGL 3.2 front-end emulation on a new Diligent/Vulkan backend inside MobileGL, instead of the DirectVulkan / DirectGLES backends.
|
||||
|
||||
Target state:
|
||||
- Fully wire MobileGL front-end `MG_State` (buffers, VAO, program, texture, sampler, framebuffer, render-state) into Diligent.
|
||||
- Implement all GL 3.2 core entry points through the Diligent backend.
|
||||
- Pass local non-Android GL3.2 tests on the Turnip Adreno 750 GPU.
|
||||
|
||||
---
|
||||
|
||||
## 2. Current Branch / Commits
|
||||
|
||||
Latest 12 commits on `feat/diligent-vulkan-backend`:
|
||||
|
||||
```
|
||||
2f5abf83 test(diligent): verify indexed DrawElements path from real frontend state
|
||||
c31b7381 feat(diligent): add basic texture binding and textured state-draw test
|
||||
8945c507 feat(diligent): clear depth in GL Clear when GL_DEPTH_BUFFER_BIT set
|
||||
558d3aea feat(diligent): add offscreen depth target and depth clear
|
||||
7e2f0bc8 feat(diligent): wire stencil and color-mask state into state PSO
|
||||
f99786f6 feat(diligent): wire viewport/scissor state into state draws
|
||||
be4cc3ce feat(diligent): wire blend/depth/cull render state into state PSO
|
||||
c855e6cf feat(diligent): verify state-driven draw with real MobileGL frontend state
|
||||
02e60bfa feat(diligent): add state-driven draw path (VAO/buffer/program to Diligent)
|
||||
a9515c92 feat(diligent): add dynamic vertex buffer upload path
|
||||
2f57582a feat(diligent): wire Clear/Draw/Present into GLFunctionsTable
|
||||
beb21123 feat(diligent): add real offscreen renderer with clear and triangle draw
|
||||
```
|
||||
|
||||
Working tree is clean.
|
||||
|
||||
---
|
||||
|
||||
## 3. Key Files
|
||||
|
||||
### Backend core
|
||||
|
||||
- `MobileGL/MG_Backend/Diligent/BackendObject_Diligent.h/.cpp`
|
||||
- `BackendObject_Diligent`
|
||||
- Creates Diligent Vulkan device/context
|
||||
- Owns `DiligentRenderer`
|
||||
- Wires `GLFunctionsTable`:
|
||||
- `Clear` (color + depth)
|
||||
- `DrawArrays`
|
||||
- `DrawElements`
|
||||
- `Present`
|
||||
- `MobileGL/MG_Backend/Diligent/DiligentVulkan.h/.cpp`
|
||||
- Backend identity helper / translation unit
|
||||
- `MobileGL/MG_Backend/Diligent/Renderer/DiligentRenderer.h/.cpp`
|
||||
- Offscreen RGBA8 + D32F targets
|
||||
- Clear / ClearDepth / DrawTriangle / DrawVertices
|
||||
- `CreateTestTexture` (RGBA8 texture + SRV + sampler)
|
||||
- `DrawFromState` (main front-end emulation draw path)
|
||||
- `CreatePipelineFromState`:
|
||||
- SPIR-V → Diligent shaders via SPIRV-Reflect
|
||||
- VAO attributes → input layout
|
||||
- primitive topology from GL mode
|
||||
- blend / depth / cull / stencil / color-mask state
|
||||
- `UploadVertexDataFromState`:
|
||||
- packs enabled VAO attributes from `BufferObject` into interleaved vertex buffer
|
||||
- supports `DrawArrays`, `DrawElements`, triangle-fan and line-loop expansion
|
||||
- Static texture binding to `g_Texture` through PSO static variables + SRB
|
||||
|
||||
### Integration changes
|
||||
|
||||
- `CMakeLists.txt`
|
||||
- New option `MOBILEGL_ENABLE_DILIGENT` (default ON for local)
|
||||
- DiligentCore added **after** glslang/SPIRV-Cross/xxHash/Vulkan-Headers so it reuses existing CMake targets
|
||||
- Diligent static libraries linked into `MobileGL` / `MobileGL_s`
|
||||
- New Diligent backend sources added
|
||||
- `MobileGL/MG_Backend/BackendObject.h`
|
||||
- New `BackendType::DiligentVulkan`
|
||||
- `MobileGL/MG_Backend/Init.cpp`
|
||||
- New backend switch case
|
||||
- `MobileGL/ConfigLoader.cpp`
|
||||
- `MOBILEGL_BACKEND_TYPE=DiligentVulkan` accepted
|
||||
- `MobileGL/MG_Test/CMakeLists.txt`
|
||||
- New `MobileGL/MG_Test/Backend/Diligent` subdirectory
|
||||
- `MobileGL/MG_Test/Backend/Diligent/`
|
||||
- `CMakeLists.txt`
|
||||
- `SanityTest.cpp`
|
||||
|
||||
### Local test files
|
||||
|
||||
- `MobileGL/MG_Test/Backend/Diligent/SanityTest.cpp`
|
||||
- `CreatesDiligentDeviceAndAdvertisesGL32`
|
||||
- `ClearsAndDrawsTriangleOffscreen`
|
||||
- `DrawsFromMobileGLState`
|
||||
- `DrawsTexturedFromMobileGLState`
|
||||
- `DrawsIndexedFromMobileGLState`
|
||||
- `DrawsRealTexturedFromMobileGLState`
|
||||
- `DrawsUniformFromMobileGLState`
|
||||
- `DrawsToOffscreenFramebufferFromMobileGLState`
|
||||
- `DrawsWithScissorFromMobileGLState`
|
||||
- `DrawsWithBlendFromMobileGLState`
|
||||
- `DrawsWithDepthTestFromMobileGLState`
|
||||
- `DrawsNamedUniformBlockFromMobileGLState`
|
||||
- `DrawsWithStencilTestFromMobileGLState`
|
||||
- `DrawsToRenderbufferFramebufferFromMobileGLState`
|
||||
- `DrawsToMultipleColorAttachmentsFromMobileGLState`
|
||||
- `DrawsIndexedBaseVertexFromMobileGLState`
|
||||
|
||||
---
|
||||
|
||||
## 4. What Works Today
|
||||
|
||||
Verified locally on Turnip Adreno 750:
|
||||
|
||||
- Diligent device/context creation
|
||||
- EGL window-surface swapchain creation path through Diligent `ISwapChain` (offscreen tests still use the offscreen target)
|
||||
- GL 3.2 / GLSL 1.50 capability advertisement
|
||||
- Offscreen color + depth rendering
|
||||
- Clear color and depth
|
||||
- Real mobilegl front-end state-driven drawing:
|
||||
- Program SPIR-V → Diligent shaders
|
||||
- VAO attributes + bound GL buffer → interleaved vertex buffer
|
||||
- `DrawArrays` path
|
||||
- `DrawElements` path (index buffer)
|
||||
- Texture basics:
|
||||
- Offscreen texture creation
|
||||
- CPU → Diligent texture (`CreateTestTexture`)
|
||||
- Static sampler2D binding to `g_Texture`
|
||||
- Textured draw test passes
|
||||
- Render state:
|
||||
- Blend enable/factors/equations
|
||||
- Stencil clear + test enabled on a D24S8 default depth/stencil target
|
||||
- Depth test enable/func/write mask
|
||||
- Cull face enable/mode/front-face winding
|
||||
- Stencil test enable/masks/ops/func/ref
|
||||
- Color write mask
|
||||
- Viewport
|
||||
- Scissor rect
|
||||
- Texture/sampler full integration:
|
||||
- `ITextureObject` → Diligent `ITexture` + SRV with automatic dirty upload
|
||||
- `SamplerObject` / texture-object sampler → Diligent `ISampler`
|
||||
- Real front-end `glTexImage2D` path (not only `CreateTestTexture`) verified
|
||||
- Global UBO upload:
|
||||
- Front-end `glUniform*` shadow → Diligent uniform buffer bound as `MGL_GLOBAL_UBO`
|
||||
- User framebuffer mapping:
|
||||
- Current draw/read FBO resolves texture attachments to Diligent RTV/DSV
|
||||
- `ReadPixels` can read back from a user FBO color attachment
|
||||
- More GL entry points wired:
|
||||
- `DrawRangeElements` / `DrawRangeElementsBaseVertex`
|
||||
- `DrawElementsBaseVertex` with real baseVertex selection
|
||||
- `MultiDrawArrays` / `MultiDrawElements` / `MultiDrawElementsBaseVertex`
|
||||
- `DrawArraysInstanced` / `DrawElementsInstanced` family
|
||||
- Indirect draw CPU fallback: `DrawArraysIndirect`, `DrawElementsIndirect`, `MultiDraw*Indirect`, `*IndirectCount`
|
||||
- `ClearBufferfv` / `ClearBufferfi` / `ClearBufferiv` / `ClearBufferuiv` (incl. stencil clear)
|
||||
- `BlitFramebuffer` / `BlitNamedFramebuffer` (same-size color copy between read/draw FBOs)
|
||||
- `CopyTexImage2D` / `CopyTexSubImage2D` (whole-color copy fallback)
|
||||
- `CopyImageSubData` (whole-texture copy between two texture objects)
|
||||
- `GenerateMipmap` (Diligent GPU mip generation on state textures)
|
||||
- `GetTexImage` / `GetTextureImage` (RGBA8 readback)
|
||||
- Fence sync entries (`FenceSync` / `ClientWaitSync` / `WaitSync` / `DeleteSync` / `GetSyncStatus`) as CPU always-signaled fallback
|
||||
- Timer query entries (`BeginTimeElapsedQuery` / `EndTimeElapsedQuery` / `QueryCounterTimestamp` / `GetQueryResult64` etc.) as CPU `steady_clock` fallback
|
||||
- `ReadPixels` from default and user color attachments
|
||||
- Primitive expansion:
|
||||
- `GL_TRIANGLE_FAN` expanded to triangle list
|
||||
- `GL_LINE_LOOP` expanded to line strip
|
||||
- Local test result:
|
||||
|
||||
```
|
||||
[ PASSED ] 16 tests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. How to Build and Run Locally
|
||||
|
||||
From repo root `~/MobileGL-dev`:
|
||||
|
||||
```bash
|
||||
cmake -S . -B build-diligent -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DMOBILEGL_ENABLE_DILIGENT=ON \
|
||||
-DMOBILEGL_BUILD_TEST=ON \
|
||||
-DMOBILEGL_BUILD_BENCHMARK=OFF \
|
||||
-DFETCHCONTENT_SOURCE_DIR_GOOGLETEST="$PWD/3rdparty/DiligentCore/ThirdParty/googletest"
|
||||
|
||||
cmake --build build-diligent --target DiligentVulkanSanityTest -j 4
|
||||
|
||||
./build-diligent/MobileGL/MG_Test/Backend/Diligent/DiligentVulkanSanityTest --gtest_color=no
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `MOBILEGL_BUILD_BENCHMARK=OFF` avoids network fetch of google/benchmark in this environment.
|
||||
- `FETCHCONTENT_SOURCE_DIR_GOOGLETEST` pins googletest to DiligentCore's bundled copy, avoiding flaky network clone.
|
||||
- Max 4 cores is intentional: use `-j 4`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Environment Notes
|
||||
|
||||
- Host: Linux `aarch64`, glibc 2.43 (Fedora container on Android/Droidspaces)
|
||||
- GPU: Turnip Adreno 750, Vulkan API 1.4.354
|
||||
- GPU nodes available:
|
||||
- `/dev/dri/renderD128`
|
||||
- `/dev/kgsl-3d0`
|
||||
- Android SDK/NDK: `~/android-sdk` (aarch64 glibc)
|
||||
- NDK `27.3.13750724`
|
||||
- CMake `3.22.1`
|
||||
- JDK/Gradle for APK builds:
|
||||
- `~/android-build-tools/jdk17`
|
||||
- `~/android-build-tools/gradle/gradle-8.10.2`
|
||||
|
||||
---
|
||||
|
||||
## 7. Known Limitations / Not Yet Implemented
|
||||
|
||||
- User framebuffers now support texture color attachments, renderbuffer color readback, multiple simultaneous color targets, and depth/stencil texture or renderbuffer attachments.
|
||||
- Textures auto-sync `ITextureObject` → Diligent resources, including mip levels and sampler state; compressed textures and integer/3-channel formats that Diligent lacks are still skipped.
|
||||
- Global UBO (default-block `glUniform*`) and named application UBO blocks (through `glBindBufferBase`/`glUniformBlockBinding`) now upload and bind; SSBOs are still not fed from frontend buffer bindings.
|
||||
- Swapchain creation and resize are wired for native EGL window surfaces via `Diligent::ISwapChain`; `Present()` presents the active swap chain when present and otherwise flushes the offscreen target. Actual on-screen EGL presentation is still untested in this headless environment, and the X11 display/connection fields are not yet plumbed through `WindowHandle`. `SetSwapInterval` now forwards the requested sync interval to `ISwapChain::Present()`.
|
||||
- No transform feedback / GPU-accelerated queries / non-color readback; fence sync and timer queries use CPU fallbacks.
|
||||
- Draw range, multi-draw, instanced-draw wrappers, clear-buffer, blit, read-pixels, CopyTexImage*, CopyImageSubData, GenerateMipmap, GetTexImage/GetTextureImage and indirect draws are now wired; buffer subdata paths still remain.
|
||||
- A last-PSO cache now avoids recreating the pipeline when program/render-state/topology/VAO layout is unchanged; texture/UBO resources are still rebound dynamically per draw.
|
||||
- The `GLFunctionsTable` is only partially populated.
|
||||
|
||||
---
|
||||
|
||||
## 8. Recommended Next Steps
|
||||
|
||||
1. **Framebuffer / Renderbuffer mapping**
|
||||
- [x] Map `MG_State::GLState::FramebufferObject` attachments to Diligent `ITextureView` / `ITexture`.
|
||||
- [x] Support default framebuffer as current offscreen target.
|
||||
- [x] Support `glBindFramebuffer`, `glFramebufferTexture2D`, renderbuffer color/depth attachments and renderbuffer color readback.
|
||||
- [x] Multiple simultaneous color attachments.
|
||||
|
||||
2. **Texture / Sampler full integration**
|
||||
- [x] Translate MobileGL `ITextureObject` to Diligent `ITexture` and cache by `GetLifetimeId()`.
|
||||
- [x] Propagate texture unit bindings into the PSO SRB.
|
||||
- [x] Translate `SamplerObject` state into Diligent `SamplerDesc`.
|
||||
|
||||
3. **Uniform / UBO support**
|
||||
- [x] Create Diligent buffer for `ProgramObject::GetUBOData()` / `GetUBOSize()`.
|
||||
- [x] Bind the global UBO as a dynamic shader resource.
|
||||
- [x] Handle per-program uniform block bindings / named UBO blocks.
|
||||
|
||||
4. **PSO / resource caching**
|
||||
- [~] Cache PSOs by program + VAO config + render state + topology (single last-PSO fast path).
|
||||
- [~] Cache textures and samplers; buffers/SRBs can still be re-bound per draw.
|
||||
|
||||
5. **More GL 3.2 entry points**
|
||||
- [x] `DrawRangeElements`
|
||||
- [x] `MultiDraw*`
|
||||
- [x] `BlitFramebuffer` (same-size color copy)
|
||||
- [x] `ReadPixels` from non-default framebuffer
|
||||
- [x] `CopyTexImage*` / `CopyImageSubData` wired as whole-resource copies
|
||||
- [x] `GetTexImage` / `GetTextureImage` (RGBA8)
|
||||
- [x] Indirect draws (CPU fallback)
|
||||
|
||||
6. **Expand local test suite**
|
||||
- [x] Scissor test
|
||||
- [x] Blend test
|
||||
- [x] Texture filtering / sampler state test
|
||||
- [x] framebuffer offscreen render-to-texture test
|
||||
- [x] Depth test visual test
|
||||
- [x] Stencil test
|
||||
|
||||
---
|
||||
|
||||
## 9. Handoff Notes for Next Agent
|
||||
|
||||
- Do **not** reference `origin/Deprecated/Feat/Diligent`; that old implementation is intentionally ignored.
|
||||
- Work from this branch, keep tests green.
|
||||
- The command `./build-diligent/.../DiligentVulkanSanityTest` runs all 5 Diligent tests.
|
||||
- If a new test crashes during shader resource binding, remember Diligent texture SRVs need a sampler attached via `ITextureView::SetSampler()` before `InitializeStaticSRBResources()`.
|
||||
- When re-creating a PSO or buffer, call `Release()` (or assign `nullptr`) before the create call to avoid Diligent debug “Overwriting reference” assertions.
|
||||
+3
-9
@@ -66,14 +66,12 @@ namespace MobileGL::MG_Config {
|
||||
// - DISPLAY: X11 session variable, not MobileGL configuration.
|
||||
// - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init
|
||||
// (see MG_Util/Debug/Log.cpp).
|
||||
// - MOBILEGL_VALIDATE_SPIRV: test suites like SpirvPassTest exercise
|
||||
// ShaderCompiler without ever running MobileGL::Initialize(), and every
|
||||
// Initialize() re-runs MG_ConfigLoader::Init, which would clobber a
|
||||
// programmatic override stored here (see ShaderCompiler.cpp,
|
||||
// SpirvValidationEnabled).
|
||||
struct FeaturesTable {
|
||||
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
|
||||
Bool DisableTimerQuery = false;
|
||||
// MOBILEGL_ENABLE_SPIRV_VALIDATION: validate generated and transformed SPIR-V.
|
||||
// Disabled by default because validation is a diagnostics-only cost.
|
||||
Bool EnableSpirvValidation = false;
|
||||
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries.
|
||||
Bool UseAngle = false;
|
||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||
@@ -130,10 +128,6 @@ namespace MobileGL::MG_Config {
|
||||
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
|
||||
// version request.
|
||||
Bool RelaxedSemantics = false;
|
||||
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN: overrides the shader-source quirk that
|
||||
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
|
||||
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
|
||||
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
|
||||
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
|
||||
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
|
||||
|
||||
@@ -162,6 +162,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
inline void InitFeatures() {
|
||||
auto& features = MG_Config::Features;
|
||||
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
|
||||
features.EnableSpirvValidation = QueryEnvFlag("MOBILEGL_ENABLE_SPIRV_VALIDATION");
|
||||
features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE");
|
||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
|
||||
@@ -179,7 +180,6 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.EsprytForceDepthStencilReadbackEmulation =
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
|
||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
|
||||
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
||||
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||
@@ -202,6 +202,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
}
|
||||
ENTRY(DirectGLES)
|
||||
ENTRY(DirectVulkan)
|
||||
ENTRY(DiligentVulkan)
|
||||
ENTRY(Unknown)
|
||||
MG_Config::ActiveBackendType = BackendType::Unknown;
|
||||
#undef ENTRY
|
||||
|
||||
+13
-3
@@ -52,11 +52,15 @@
|
||||
// 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.
|
||||
//
|
||||
// Severity order, ascending: DEBUG < INFO < WARN < ERROR < FATAL. MOBILEGL_LOG_ACTIVE_LEVEL
|
||||
// names the lowest severity compiled in, so the production default INFO keeps I/W/E/F and
|
||||
// drops only D. Any edit here must be mirrored in Log.h.
|
||||
#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_INFO 1
|
||||
#define MOBILEGL_LOG_LEVEL_WARN 2
|
||||
#define MOBILEGL_LOG_LEVEL_ERROR 3
|
||||
#define MOBILEGL_LOG_LEVEL_FATAL 4
|
||||
#endif
|
||||
|
||||
@@ -91,6 +95,12 @@
|
||||
#endif
|
||||
|
||||
// =============================== Utils ================================ //
|
||||
// Asserts are live in exactly the builds where MGLOG_D is live, i.e. DEBUG builds only;
|
||||
// an INFO build (the production default) compiles them out. DEBUG is the lowest severity
|
||||
// in the ordering above, so "ACTIVE <= DEBUG" is true only for ACTIVE == DEBUG - the same
|
||||
// gate MGLOG_D uses in Log.h. That equivalence is what makes this gate survive the
|
||||
// 2026-08-13 renumbering unchanged; the contract is and stays
|
||||
// "INFO builds: asserts OFF; DEBUG builds: asserts ON".
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
#define MOBILEGL_ASSERT(condition, ...) \
|
||||
do { \
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace MobileGL {
|
||||
enum class BackendType {
|
||||
DirectGLES,
|
||||
DirectVulkan,
|
||||
DiligentVulkan,
|
||||
BackendTypeCount,
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
@@ -0,0 +1,906 @@
|
||||
// MobileGL - MobileGL/MG_Backend/Diligent/BackendObject_Diligent.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
#include "BackendObject_Diligent.h"
|
||||
#include "DiligentVulkan.h"
|
||||
#include "Renderer/DiligentRenderer.h"
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
#include <EngineFactoryVk.h>
|
||||
#include <RenderDevice.h>
|
||||
#include <DeviceContext.h>
|
||||
|
||||
#include <exception>
|
||||
#include <chrono>
|
||||
|
||||
namespace MobileGL::MG_Backend::DiligentBackend {
|
||||
namespace {
|
||||
const RendererInfo BuildInitialRendererInfo() {
|
||||
RendererInfo info;
|
||||
info.RendererName = "MobileGL (Diligent/Vulkan)";
|
||||
info.BackendName = "Diligent Vulkan";
|
||||
info.RendererGLInfo.TargetGLVersion = {3, 2, 0};
|
||||
info.RendererGLInfo.TargetGLSLVersion = {1, 50, 0};
|
||||
info.RendererGLInfo.IsCompatibilityProfile = false;
|
||||
return info;
|
||||
}
|
||||
|
||||
DiligentRenderer* GetActiveRenderer() {
|
||||
auto* backend = dynamic_cast<BackendObject_Diligent*>(pActiveBackendObject.get());
|
||||
return backend != nullptr ? backend->GetRenderer() : nullptr;
|
||||
}
|
||||
|
||||
struct DrawArraysIndirectCommand {
|
||||
Uint32 Count = 0;
|
||||
Uint32 InstanceCount = 0;
|
||||
Uint32 First = 0;
|
||||
Uint32 BaseInstance = 0;
|
||||
};
|
||||
|
||||
struct DrawElementsIndirectCommand {
|
||||
Uint32 Count = 0;
|
||||
Uint32 InstanceCount = 0;
|
||||
Uint32 FirstIndex = 0;
|
||||
Int32 BaseVertex = 0;
|
||||
Uint32 BaseInstance = 0;
|
||||
};
|
||||
|
||||
struct CpuTimerQuery {
|
||||
std::chrono::steady_clock::time_point Start;
|
||||
Uint64 TimestampNs = 0;
|
||||
Bool Available = false;
|
||||
};
|
||||
|
||||
const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) {
|
||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (drawBuffer) {
|
||||
drawBuffer->SyncPersistentMappedRange();
|
||||
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
||||
if (drawBuffer->MappedData() == nullptr || commandOffset + requiredBytes > drawBuffer->GetSize()) {
|
||||
MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
|
||||
return nullptr;
|
||||
}
|
||||
return drawBuffer->MappedData() + commandOffset;
|
||||
}
|
||||
if (indirect == nullptr) {
|
||||
MGLOG_E_ONCE("%s skipped: indirect pointer is null", label);
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<const Uint8*>(indirect);
|
||||
}
|
||||
|
||||
void Clear(GLbitfield mask) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr || MG_State::pGLContext == nullptr) {
|
||||
return;
|
||||
}
|
||||
if ((mask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||
const auto& color = MG_State::pGLContext->GetClearColor();
|
||||
renderer->Clear(color.x(), color.y(), color.z(), color.w());
|
||||
}
|
||||
if ((mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
||||
renderer->ClearDepth(MG_State::pGLContext->GetClearDepth());
|
||||
}
|
||||
if ((mask & GL_STENCIL_BUFFER_BIT) != 0) {
|
||||
renderer->ClearStencil(MG_State::pGLContext->GetClearStencil());
|
||||
}
|
||||
}
|
||||
|
||||
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer != nullptr) {
|
||||
renderer->DrawFromState(mode, first, count, 0, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer != nullptr) {
|
||||
renderer->DrawFromState(mode, 0, count, type, indices);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
||||
const void* indices) {
|
||||
// The CPU-side UploadVertexDataFromState path already honors the selected index
|
||||
// range. start/end only restrict which indices may be referenced; they do not
|
||||
// change the vertex buffer layout for this backend.
|
||||
(void)start;
|
||||
(void)end;
|
||||
DrawElements(mode, count, type, indices);
|
||||
}
|
||||
|
||||
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
||||
const void* indices, GLint basevertex) {
|
||||
(void)start;
|
||||
(void)end;
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer != nullptr) {
|
||||
renderer->DrawFromState(mode, 0, count, type, indices, basevertex);
|
||||
}
|
||||
}
|
||||
|
||||
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr) {
|
||||
return;
|
||||
}
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] > 0) {
|
||||
renderer->DrawFromState(mode, first[i], count[i], 0, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr) {
|
||||
return;
|
||||
}
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] > 0) {
|
||||
renderer->DrawFromState(mode, 0, count[i], type, indices[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLint basevertex) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer != nullptr) {
|
||||
renderer->DrawFromState(mode, 0, count, type, indices, basevertex);
|
||||
}
|
||||
}
|
||||
|
||||
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type,
|
||||
const GLvoid* const* indices, GLsizei drawcount,
|
||||
const GLint* basevertex) {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] > 0) {
|
||||
DrawElementsBaseVertex(mode, count[i], type, indices[i],
|
||||
basevertex != nullptr ? basevertex[i] : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr || MG_State::pGLContext == nullptr) {
|
||||
return;
|
||||
}
|
||||
const auto* bytes = ResolveIndirectCommandBytes(indirect, sizeof(DrawArraysIndirectCommand),
|
||||
"DrawArraysIndirect");
|
||||
if (bytes == nullptr) {
|
||||
return;
|
||||
}
|
||||
DrawArraysIndirectCommand cmd{};
|
||||
std::memcpy(&cmd, bytes, sizeof(cmd));
|
||||
if (cmd.Count == 0 || cmd.InstanceCount == 0) {
|
||||
return;
|
||||
}
|
||||
for (Uint32 i = 0; i < cmd.InstanceCount; ++i) {
|
||||
renderer->DrawFromState(mode, static_cast<GLint>(cmd.First), static_cast<GLsizei>(cmd.Count),
|
||||
0, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr || MG_State::pGLContext == nullptr) {
|
||||
return;
|
||||
}
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
if (indexSize == 0) {
|
||||
return;
|
||||
}
|
||||
const auto* bytes = ResolveIndirectCommandBytes(indirect, sizeof(DrawElementsIndirectCommand),
|
||||
"DrawElementsIndirect");
|
||||
if (bytes == nullptr) {
|
||||
return;
|
||||
}
|
||||
DrawElementsIndirectCommand cmd{};
|
||||
std::memcpy(&cmd, bytes, sizeof(cmd));
|
||||
if (cmd.Count == 0 || cmd.InstanceCount == 0) {
|
||||
return;
|
||||
}
|
||||
const void* indices = reinterpret_cast<const void*>(static_cast<SizeT>(cmd.FirstIndex) * indexSize);
|
||||
for (Uint32 i = 0; i < cmd.InstanceCount; ++i) {
|
||||
renderer->DrawFromState(mode, 0, static_cast<GLsizei>(cmd.Count), type, indices,
|
||||
static_cast<GLint>(cmd.BaseVertex));
|
||||
}
|
||||
}
|
||||
|
||||
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr || MG_State::pGLContext == nullptr || drawcount <= 0) {
|
||||
return;
|
||||
}
|
||||
const GLsizei realStride = stride == 0 ? static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand)) : stride;
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
const auto* bytes = ResolveIndirectCommandBytes(
|
||||
static_cast<const Uint8*>(indirect) + static_cast<SizeT>(i) * static_cast<SizeT>(realStride),
|
||||
sizeof(DrawArraysIndirectCommand), "MultiDrawArraysIndirect");
|
||||
if (bytes == nullptr) {
|
||||
continue;
|
||||
}
|
||||
DrawArraysIndirectCommand cmd{};
|
||||
std::memcpy(&cmd, bytes, sizeof(cmd));
|
||||
if (cmd.Count == 0 || cmd.InstanceCount == 0) {
|
||||
continue;
|
||||
}
|
||||
for (Uint32 instance = 0; instance < cmd.InstanceCount; ++instance) {
|
||||
renderer->DrawFromState(mode, static_cast<GLint>(cmd.First),
|
||||
static_cast<GLsizei>(cmd.Count), 0, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount,
|
||||
GLsizei stride) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr || MG_State::pGLContext == nullptr || drawcount <= 0) {
|
||||
return;
|
||||
}
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
if (indexSize == 0) {
|
||||
return;
|
||||
}
|
||||
const GLsizei realStride = stride == 0 ? static_cast<GLsizei>(sizeof(DrawElementsIndirectCommand)) : stride;
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
const auto* bytes = ResolveIndirectCommandBytes(
|
||||
static_cast<const Uint8*>(indirect) + static_cast<SizeT>(i) * static_cast<SizeT>(realStride),
|
||||
sizeof(DrawElementsIndirectCommand), "MultiDrawElementsIndirect");
|
||||
if (bytes == nullptr) {
|
||||
continue;
|
||||
}
|
||||
DrawElementsIndirectCommand cmd{};
|
||||
std::memcpy(&cmd, bytes, sizeof(cmd));
|
||||
if (cmd.Count == 0 || cmd.InstanceCount == 0) {
|
||||
continue;
|
||||
}
|
||||
const void* indices = reinterpret_cast<const void*>(static_cast<SizeT>(cmd.FirstIndex) * indexSize);
|
||||
for (Uint32 instance = 0; instance < cmd.InstanceCount; ++instance) {
|
||||
renderer->DrawFromState(mode, 0, static_cast<GLsizei>(cmd.Count), type, indices,
|
||||
static_cast<GLint>(cmd.BaseVertex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
|
||||
GLsizei maxdrawcount, GLsizei stride) {
|
||||
if (MG_State::pGLContext == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto paramBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
if (!paramBuffer) {
|
||||
return;
|
||||
}
|
||||
paramBuffer->SyncPersistentMappedRange();
|
||||
const Uint8* paramData = paramBuffer->MappedData();
|
||||
if (paramData == nullptr) {
|
||||
return;
|
||||
}
|
||||
Uint32 actualDrawCount = 0;
|
||||
std::memcpy(&actualDrawCount, paramData + static_cast<SizeT>(drawcount), sizeof(actualDrawCount));
|
||||
actualDrawCount = std::min<Uint32>(actualDrawCount, static_cast<Uint32>(maxdrawcount));
|
||||
MultiDrawArraysIndirect(mode, indirect, static_cast<GLsizei>(actualDrawCount), stride);
|
||||
}
|
||||
|
||||
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect,
|
||||
GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) {
|
||||
if (MG_State::pGLContext == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto paramBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
if (!paramBuffer) {
|
||||
return;
|
||||
}
|
||||
paramBuffer->SyncPersistentMappedRange();
|
||||
const Uint8* paramData = paramBuffer->MappedData();
|
||||
if (paramData == nullptr) {
|
||||
return;
|
||||
}
|
||||
Uint32 actualDrawCount = 0;
|
||||
std::memcpy(&actualDrawCount, paramData + static_cast<SizeT>(drawcount), sizeof(actualDrawCount));
|
||||
actualDrawCount = std::min<Uint32>(actualDrawCount, static_cast<Uint32>(maxdrawcount));
|
||||
MultiDrawElementsIndirect(mode, type, indirect, static_cast<GLsizei>(actualDrawCount), stride);
|
||||
}
|
||||
|
||||
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr || instancecount <= 0) {
|
||||
return;
|
||||
}
|
||||
for (GLsizei i = 0; i < instancecount; ++i) {
|
||||
renderer->DrawFromState(mode, first, count, 0, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||
GLuint baseinstance) {
|
||||
(void)baseinstance;
|
||||
DrawArraysInstanced(mode, first, count, instancecount);
|
||||
}
|
||||
|
||||
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr || instancecount <= 0) {
|
||||
return;
|
||||
}
|
||||
for (GLsizei i = 0; i < instancecount; ++i) {
|
||||
renderer->DrawFromState(mode, 0, count, type, indices);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLint basevertex) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr || instancecount <= 0) {
|
||||
return;
|
||||
}
|
||||
for (GLsizei i = 0; i < instancecount; ++i) {
|
||||
renderer->DrawFromState(mode, 0, count, type, indices, basevertex);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLuint baseinstance) {
|
||||
(void)baseinstance;
|
||||
DrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||
}
|
||||
|
||||
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type,
|
||||
const void* indices, GLsizei instancecount,
|
||||
GLint basevertex, GLuint baseinstance) {
|
||||
(void)baseinstance;
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr || instancecount <= 0) {
|
||||
return;
|
||||
}
|
||||
for (GLsizei i = 0; i < instancecount; ++i) {
|
||||
renderer->DrawFromState(mode, 0, count, type, indices, basevertex);
|
||||
}
|
||||
}
|
||||
|
||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr || value == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (buffer == GL_COLOR && drawbuffer == 0) {
|
||||
renderer->Clear(value[0], value[1], value[2], value[3]);
|
||||
} else if (buffer == GL_DEPTH && drawbuffer == 0) {
|
||||
renderer->ClearDepth(value[0]);
|
||||
}
|
||||
}
|
||||
|
||||
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) {
|
||||
if (value == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (buffer == GL_STENCIL) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer != nullptr) {
|
||||
renderer->ClearStencil(static_cast<Uint32>(value[0]));
|
||||
}
|
||||
return;
|
||||
}
|
||||
Float color[4] = {
|
||||
static_cast<Float>(value[0]) / 255.0f,
|
||||
static_cast<Float>(value[1]) / 255.0f,
|
||||
static_cast<Float>(value[2]) / 255.0f,
|
||||
static_cast<Float>(value[3]) / 255.0f,
|
||||
};
|
||||
ClearBufferfv(buffer, drawbuffer, color);
|
||||
}
|
||||
|
||||
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
||||
if (value == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (buffer == GL_STENCIL) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer != nullptr) {
|
||||
renderer->ClearStencil(value[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Float color[4] = {
|
||||
static_cast<Float>(value[0]) / 255.0f,
|
||||
static_cast<Float>(value[1]) / 255.0f,
|
||||
static_cast<Float>(value[2]) / 255.0f,
|
||||
static_cast<Float>(value[3]) / 255.0f,
|
||||
};
|
||||
ClearBufferfv(buffer, drawbuffer, color);
|
||||
}
|
||||
|
||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr || buffer != GL_DEPTH_STENCIL) {
|
||||
return;
|
||||
}
|
||||
(void)drawbuffer;
|
||||
renderer->ClearDepth(depth);
|
||||
renderer->ClearStencil(static_cast<Uint32>(stencil));
|
||||
}
|
||||
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr || pixels == nullptr) {
|
||||
return;
|
||||
}
|
||||
// The Diligent backend's offscreen targets are RGBA8; the frontend currently
|
||||
// uses this entry for the common GL_RGBA/GL_UNSIGNED_BYTE readback path.
|
||||
if (format != GL_RGBA || type != GL_UNSIGNED_BYTE) {
|
||||
return;
|
||||
}
|
||||
renderer->ReadPixels(static_cast<Uint32>(x), static_cast<Uint32>(y),
|
||||
static_cast<Uint32>(width), static_cast<Uint32>(height), pixels);
|
||||
}
|
||||
|
||||
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
||||
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
||||
GLbitfield mask, GLenum filter) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer != nullptr) {
|
||||
renderer->BlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1,
|
||||
mask, filter);
|
||||
}
|
||||
}
|
||||
|
||||
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
|
||||
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
|
||||
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
||||
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
||||
GLbitfield mask, GLenum filter) {
|
||||
(void)srcX0;
|
||||
(void)srcY0;
|
||||
(void)srcX1;
|
||||
(void)srcY1;
|
||||
(void)dstX0;
|
||||
(void)dstY0;
|
||||
(void)dstX1;
|
||||
(void)dstY1;
|
||||
(void)filter;
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer != nullptr) {
|
||||
renderer->BlitNamedFramebuffer(readFramebuffer, drawFramebuffer, mask);
|
||||
}
|
||||
}
|
||||
|
||||
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y,
|
||||
GLsizei width, GLsizei height, GLint border) {
|
||||
(void)level;
|
||||
(void)internalformat;
|
||||
(void)x;
|
||||
(void)y;
|
||||
(void)width;
|
||||
(void)height;
|
||||
(void)border;
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr || MG_State::pGLContext == nullptr || target != GL_TEXTURE_2D) {
|
||||
return;
|
||||
}
|
||||
auto& unit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||
auto texture = unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
|
||||
if (texture) {
|
||||
renderer->CopyReadFramebufferToTexture(*texture);
|
||||
}
|
||||
}
|
||||
|
||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
||||
GLsizei width, GLsizei height) {
|
||||
(void)level;
|
||||
(void)xoffset;
|
||||
(void)yoffset;
|
||||
(void)x;
|
||||
(void)y;
|
||||
(void)width;
|
||||
(void)height;
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr || MG_State::pGLContext == nullptr || target != GL_TEXTURE_2D) {
|
||||
return;
|
||||
}
|
||||
auto& unit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||
auto texture = unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
|
||||
if (texture) {
|
||||
renderer->CopyReadFramebufferToTexture(*texture);
|
||||
}
|
||||
}
|
||||
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr || MG_State::pGLContext == nullptr || target != GL_TEXTURE_2D ||
|
||||
format != GL_RGBA || type != GL_UNSIGNED_BYTE || pixels == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto& unit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||
auto texture = unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
|
||||
if (texture) {
|
||||
renderer->ReadTextureImage(*texture, static_cast<Uint32>(level), pixels);
|
||||
}
|
||||
}
|
||||
|
||||
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture,
|
||||
TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type,
|
||||
GLsizei bufSize, GLvoid* pixels) {
|
||||
(void)uploadTarget;
|
||||
(void)bufSize;
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr || !texture || format != GL_RGBA || type != GL_UNSIGNED_BYTE ||
|
||||
pixels == nullptr) {
|
||||
return;
|
||||
}
|
||||
renderer->ReadTextureImage(*texture, static_cast<Uint32>(level), pixels);
|
||||
}
|
||||
|
||||
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
(void)srcTarget;
|
||||
(void)srcLevel;
|
||||
(void)srcX;
|
||||
(void)srcY;
|
||||
(void)srcZ;
|
||||
(void)dstTarget;
|
||||
(void)dstLevel;
|
||||
(void)dstX;
|
||||
(void)dstY;
|
||||
(void)dstZ;
|
||||
(void)srcWidth;
|
||||
(void)srcHeight;
|
||||
(void)srcDepth;
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer != nullptr && srcTexture && dstTexture) {
|
||||
renderer->CopyTextureSubData(*srcTexture, *dstTexture);
|
||||
}
|
||||
}
|
||||
|
||||
void GenerateMipmap(GLenum target) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer == nullptr || MG_State::pGLContext == nullptr || target != GL_TEXTURE_2D) {
|
||||
return;
|
||||
}
|
||||
auto& unit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||
auto texture = unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
|
||||
if (texture) {
|
||||
renderer->GenerateMipmap(*texture);
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsTimerQuerySupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
BackendQueryHandle BeginTimeElapsedQuery() {
|
||||
auto* query = new CpuTimerQuery;
|
||||
query->Start = std::chrono::steady_clock::now();
|
||||
query->Available = false;
|
||||
return query;
|
||||
}
|
||||
|
||||
void EndTimeElapsedQuery(BackendQueryHandle query) {
|
||||
if (query == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto* cpuQuery = static_cast<CpuTimerQuery*>(query);
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
cpuQuery->TimestampNs = static_cast<Uint64>(
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(now - cpuQuery->Start).count());
|
||||
cpuQuery->Available = true;
|
||||
}
|
||||
|
||||
BackendQueryHandle QueryCounterTimestamp() {
|
||||
auto* query = new CpuTimerQuery;
|
||||
query->TimestampNs = static_cast<Uint64>(
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count());
|
||||
query->Available = true;
|
||||
return query;
|
||||
}
|
||||
|
||||
Bool IsQueryResultAvailable(BackendQueryHandle query) {
|
||||
return query != nullptr && static_cast<CpuTimerQuery*>(query)->Available;
|
||||
}
|
||||
|
||||
Bool GetQueryResult64(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds) {
|
||||
if (query == nullptr || outNanoseconds == nullptr) {
|
||||
return false;
|
||||
}
|
||||
auto* cpuQuery = static_cast<CpuTimerQuery*>(query);
|
||||
if (!cpuQuery->Available && !wait) {
|
||||
return false;
|
||||
}
|
||||
*outNanoseconds = cpuQuery->TimestampNs;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DeleteBackendQuery(BackendQueryHandle query) {
|
||||
delete static_cast<CpuTimerQuery*>(query);
|
||||
}
|
||||
|
||||
BackendSyncHandle FenceSync() {
|
||||
// CPU fallback fence: always signaled is a valid implementation for a
|
||||
// backend without native sync primitives. The handle still round-trips
|
||||
// through ClientWaitSync/DeleteSync so frontend state stays balanced.
|
||||
return new int(0);
|
||||
}
|
||||
|
||||
GLenum ClientWaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout) {
|
||||
(void)sync;
|
||||
(void)flags;
|
||||
(void)timeout;
|
||||
return GL_ALREADY_SIGNALED;
|
||||
}
|
||||
|
||||
void WaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout) {
|
||||
(void)sync;
|
||||
(void)flags;
|
||||
(void)timeout;
|
||||
}
|
||||
|
||||
void DeleteSync(BackendSyncHandle sync) {
|
||||
delete static_cast<int*>(sync);
|
||||
}
|
||||
|
||||
Bool GetSyncStatus(BackendSyncHandle sync) {
|
||||
(void)sync;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SetSwapInterval(Int interval) {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer != nullptr) {
|
||||
renderer->SetSwapInterval(interval > 0 ? static_cast<Uint32>(interval) : 0);
|
||||
}
|
||||
}
|
||||
|
||||
void Present() {
|
||||
auto* renderer = GetActiveRenderer();
|
||||
if (renderer != nullptr) {
|
||||
renderer->Present();
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
BackendObject_Diligent::BackendObject_Diligent()
|
||||
: m_rendererInfo(BuildInitialRendererInfo()) {}
|
||||
|
||||
BackendObject_Diligent::~BackendObject_Diligent() {
|
||||
m_pRenderer.reset();
|
||||
m_pContext.Release();
|
||||
m_pDevice.Release();
|
||||
m_pFactoryVk = nullptr;
|
||||
}
|
||||
|
||||
Bool BackendObject_Diligent::CreateDiligentDevice() {
|
||||
if (m_pDevice && m_pContext) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
if (m_pFactoryVk == nullptr) {
|
||||
m_pFactoryVk = ::Diligent::GetEngineFactoryVk();
|
||||
if (m_pFactoryVk == nullptr) {
|
||||
MGLOG_E("Diligent: failed to load Vulkan engine factory");
|
||||
return false;
|
||||
}
|
||||
m_pFactoryVk->SetBreakOnError(false);
|
||||
}
|
||||
|
||||
::Diligent::Uint32 numAdapters = 0;
|
||||
m_pFactoryVk->EnumerateAdapters(::Diligent::Version{}, numAdapters, nullptr);
|
||||
if (numAdapters == 0) {
|
||||
MGLOG_W("Diligent: no Vulkan adapters available; skipping device creation");
|
||||
return false;
|
||||
}
|
||||
|
||||
::Diligent::EngineVkCreateInfo engineCI;
|
||||
::Diligent::ImmediateContextCreateInfo ctxCI;
|
||||
ctxCI.Name = "MobileGL Diligent Main Context";
|
||||
ctxCI.QueueId = 0;
|
||||
ctxCI.Priority = ::Diligent::QUEUE_PRIORITY_MEDIUM;
|
||||
engineCI.NumImmediateContexts = 1;
|
||||
engineCI.pImmediateContextInfo = &ctxCI;
|
||||
|
||||
::Diligent::IRenderDevice* pDevice = nullptr;
|
||||
::Diligent::IDeviceContext* pContext = nullptr;
|
||||
m_pFactoryVk->CreateDeviceAndContextsVk(engineCI, &pDevice, &pContext);
|
||||
if (pDevice == nullptr || pContext == nullptr) {
|
||||
MGLOG_E("Diligent: failed to create Vulkan device/context");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_pDevice.Attach(pDevice);
|
||||
m_pContext.Attach(pContext);
|
||||
MGLOG_I("Diligent: Vulkan device created");
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
MGLOG_W("Diligent: Vulkan device creation failed: %s", e.what());
|
||||
return false;
|
||||
} catch (...) {
|
||||
MGLOG_W("Diligent: Vulkan device creation failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void BackendObject_Diligent::Initialize() {
|
||||
if (m_initialized) {
|
||||
return;
|
||||
}
|
||||
if (!CreateDiligentDevice()) {
|
||||
MGLOG_W("Diligent: backend initialization failed");
|
||||
return;
|
||||
}
|
||||
|
||||
m_pRenderer = std::make_unique<DiligentRenderer>(m_pDevice, m_pContext);
|
||||
if (!m_pRenderer->Initialize(256, 256)) {
|
||||
MGLOG_W("Diligent: renderer initialization failed");
|
||||
m_pRenderer.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
m_functions.GL.Clear = Clear;
|
||||
m_functions.GL.DrawArrays = DrawArrays;
|
||||
m_functions.GL.DrawElements = DrawElements;
|
||||
m_functions.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
|
||||
m_functions.GL.DrawRangeElements = DrawRangeElements;
|
||||
m_functions.GL.DrawRangeElementsBaseVertex = DrawRangeElementsBaseVertex;
|
||||
m_functions.GL.MultiDrawArrays = MultiDrawArrays;
|
||||
m_functions.GL.MultiDrawElements = MultiDrawElements;
|
||||
m_functions.GL.MultiDrawElementsBaseVertex = MultiDrawElementsBaseVertex;
|
||||
m_functions.GL.DrawArraysInstanced = DrawArraysInstanced;
|
||||
m_functions.GL.DrawArraysInstancedBaseInstance = DrawArraysInstancedBaseInstance;
|
||||
m_functions.GL.DrawElementsInstanced = DrawElementsInstanced;
|
||||
m_functions.GL.DrawElementsInstancedBaseVertex = DrawElementsInstancedBaseVertex;
|
||||
m_functions.GL.DrawElementsInstancedBaseInstance = DrawElementsInstancedBaseInstance;
|
||||
m_functions.GL.DrawElementsInstancedBaseVertexBaseInstance = DrawElementsInstancedBaseVertexBaseInstance;
|
||||
m_functions.GL.DrawArraysIndirect = DrawArraysIndirect;
|
||||
m_functions.GL.DrawElementsIndirect = DrawElementsIndirect;
|
||||
m_functions.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect;
|
||||
m_functions.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
|
||||
m_functions.GL.MultiDrawArraysIndirectCount = MultiDrawArraysIndirectCount;
|
||||
m_functions.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount;
|
||||
m_functions.GL.ClearBufferfv = ClearBufferfv;
|
||||
m_functions.GL.ClearBufferfi = ClearBufferfi;
|
||||
m_functions.GL.ClearBufferiv = ClearBufferiv;
|
||||
m_functions.GL.ClearBufferuiv = ClearBufferuiv;
|
||||
m_functions.GL.BlitFramebuffer = BlitFramebuffer;
|
||||
m_functions.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
|
||||
m_functions.GL.CopyTexImage2D = CopyTexImage2D;
|
||||
m_functions.GL.CopyTexSubImage2D = CopyTexSubImage2D;
|
||||
m_functions.GL.CopyImageSubData = CopyImageSubData;
|
||||
m_functions.GL.GenerateMipmap = GenerateMipmap;
|
||||
m_functions.GL.GetTexImage = GetTexImage;
|
||||
m_functions.GL.GetTextureImage = GetTextureImage;
|
||||
m_functions.GL.ReadPixels = ReadPixels;
|
||||
m_functions.GL.FenceSync = FenceSync;
|
||||
m_functions.GL.ClientWaitSync = ClientWaitSync;
|
||||
m_functions.GL.WaitSync = WaitSync;
|
||||
m_functions.GL.DeleteSync = DeleteSync;
|
||||
m_functions.GL.GetSyncStatus = GetSyncStatus;
|
||||
m_functions.GL.IsTimerQuerySupported = IsTimerQuerySupported;
|
||||
m_functions.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
|
||||
m_functions.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
|
||||
m_functions.GL.QueryCounterTimestamp = QueryCounterTimestamp;
|
||||
m_functions.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
||||
m_functions.GL.GetQueryResult64 = GetQueryResult64;
|
||||
m_functions.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||
m_functions.Present = Present;
|
||||
m_functions.SetSwapInterval = SetSwapInterval;
|
||||
|
||||
m_initialized = true;
|
||||
}
|
||||
|
||||
DiligentRenderer* BackendObject_Diligent::GetRenderer() {
|
||||
return m_pRenderer.get();
|
||||
}
|
||||
|
||||
Bool BackendObject_Diligent::InitCapabilities() {
|
||||
// Skeleton: no format probing yet. The backend advertises GL 3.2 core
|
||||
// capability, and the capability tables will be filled as resource
|
||||
// creation paths are ported.
|
||||
m_backendCapabilitiesInitialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool BackendObject_Diligent::InitWindowSurface() {
|
||||
if (!m_windowHandle.Handle) {
|
||||
MGLOG_E("BackendObject_Diligent::InitWindowSurface failed: native window handle is null");
|
||||
return false;
|
||||
}
|
||||
if (m_pRenderer == nullptr || m_pFactoryVk == nullptr) {
|
||||
MGLOG_E("BackendObject_Diligent::InitWindowSurface failed: renderer/factory is not ready");
|
||||
return false;
|
||||
}
|
||||
return m_pRenderer->CreateSwapChain(m_pFactoryVk, m_windowHandle,
|
||||
m_windowHandle.Width, m_windowHandle.Height);
|
||||
}
|
||||
|
||||
Bool BackendObject_Diligent::InitPbufferSurface(EGLint width, EGLint height) {
|
||||
// The Diligent backend keeps its offscreen target for pbuffer EGL surfaces.
|
||||
// A future enhancement can resize/recreate the offscreen target to match the
|
||||
// pbuffer dimensions.
|
||||
(void)width;
|
||||
(void)height;
|
||||
return m_pRenderer != nullptr;
|
||||
}
|
||||
|
||||
void BackendObject_Diligent::ReleaseEGLResources() {
|
||||
if (m_pRenderer != nullptr) {
|
||||
m_pRenderer->ReleaseSwapChain();
|
||||
}
|
||||
BackendObject::ReleaseEGLResources();
|
||||
}
|
||||
|
||||
void BackendObject_Diligent::OnEGLSurfaceReleased(EGLSurface surface) {
|
||||
(void)surface;
|
||||
if (m_pRenderer != nullptr) {
|
||||
m_pRenderer->ReleaseSwapChain();
|
||||
}
|
||||
}
|
||||
|
||||
Bool BackendObject_Diligent::CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (!m_initialized) {
|
||||
MGLOG_E("BackendObject_Diligent::CreateEGLWindowSurface failed: backend not initialized");
|
||||
return false;
|
||||
}
|
||||
if (!handle.Handle || (handle.Backend != WindowBackend::Android && handle.Backend != WindowBackend::X11 &&
|
||||
handle.Backend != WindowBackend::MetalLayer && handle.Backend != WindowBackend::Win32)) {
|
||||
MGLOG_E("BackendObject_Diligent::CreateEGLWindowSurface failed: unsupported native window backend");
|
||||
return false;
|
||||
}
|
||||
return RegisterEGLWindowSurface(surface, handle);
|
||||
}
|
||||
|
||||
Bool BackendObject_Diligent::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (!m_initialized) {
|
||||
MGLOG_E("BackendObject_Diligent::CreateEGLPbufferSurface failed: backend not initialized");
|
||||
return false;
|
||||
}
|
||||
return RegisterEGLPbufferSurface(surface, width, height);
|
||||
}
|
||||
|
||||
Bool BackendObject_Diligent::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (!BackendObject::ResizeEGLWindowSurface(surface, width, height)) {
|
||||
return false;
|
||||
}
|
||||
if (m_eglSurface == surface && m_pRenderer != nullptr) {
|
||||
return m_pRenderer->ResizeSwapChain(width, height);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const RendererInfo& BackendObject_Diligent::GetRendererInfo() const {
|
||||
return m_rendererInfo;
|
||||
}
|
||||
|
||||
String BackendObject_Diligent::GetBackendAPIVersionString() const {
|
||||
return "Diligent Vulkan 0.1 (GL 3.2 skeleton)";
|
||||
}
|
||||
|
||||
const GlobalBackendFunctionsTable& BackendObject_Diligent::GetBackendFunctions() const {
|
||||
return m_functions;
|
||||
}
|
||||
|
||||
const DynamicBackendParameters& BackendObject_Diligent::GetDynamicParameters() const {
|
||||
return m_dynamicParameters;
|
||||
}
|
||||
|
||||
BackendType BackendObject_Diligent::GetBackendType() const {
|
||||
return BackendType::DiligentVulkan;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DiligentBackend
|
||||
@@ -0,0 +1,72 @@
|
||||
// MobileGL - MobileGL/MG_Backend/Diligent/BackendObject_Diligent.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Includes.h>
|
||||
#include "../BackendObject.h"
|
||||
|
||||
// X11 (pulled in by Includes.h through Vulkan-Headers) defines True/False as
|
||||
// macros, which collide with Diligent's Bool constants in BasicTypes.h.
|
||||
#if defined(True)
|
||||
#undef True
|
||||
#endif
|
||||
#if defined(False)
|
||||
#undef False
|
||||
#endif
|
||||
|
||||
#include <RefCntAutoPtr.hpp>
|
||||
|
||||
namespace Diligent {
|
||||
struct IEngineFactoryVk;
|
||||
struct IRenderDevice;
|
||||
struct IDeviceContext;
|
||||
}
|
||||
|
||||
namespace MobileGL::MG_Backend::DiligentBackend {
|
||||
class DiligentRenderer;
|
||||
|
||||
// New Diligent/Vulkan backend, implemented from scratch on top of
|
||||
// DiligentCore. The backend object owns the Diligent device/context and
|
||||
// currently advertises OpenGL 3.2 core capability; the GL function table
|
||||
// is intentionally empty until drawing/resource paths are ported.
|
||||
class BackendObject_Diligent : public BackendObject {
|
||||
public:
|
||||
BackendObject_Diligent();
|
||||
~BackendObject_Diligent() override;
|
||||
|
||||
void Initialize() override;
|
||||
Bool InitCapabilities() override;
|
||||
Bool InitWindowSurface() override;
|
||||
Bool InitPbufferSurface(EGLint width, EGLint height) override;
|
||||
Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) override;
|
||||
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
|
||||
Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) override;
|
||||
void OnEGLSurfaceReleased(EGLSurface surface) override;
|
||||
|
||||
const RendererInfo& GetRendererInfo() const override;
|
||||
String GetBackendAPIVersionString() const override;
|
||||
const GlobalBackendFunctionsTable& GetBackendFunctions() const override;
|
||||
const DynamicBackendParameters& GetDynamicParameters() const override;
|
||||
BackendType GetBackendType() const override;
|
||||
void ReleaseEGLResources() override;
|
||||
|
||||
DiligentRenderer* GetRenderer();
|
||||
|
||||
private:
|
||||
Bool CreateDiligentDevice();
|
||||
|
||||
RendererInfo m_rendererInfo;
|
||||
DynamicBackendParameters m_dynamicParameters;
|
||||
GlobalBackendFunctionsTable m_functions{};
|
||||
::Diligent::IEngineFactoryVk* m_pFactoryVk = nullptr;
|
||||
::Diligent::RefCntAutoPtr<::Diligent::IRenderDevice> m_pDevice;
|
||||
::Diligent::RefCntAutoPtr<::Diligent::IDeviceContext> m_pContext;
|
||||
std::unique_ptr<DiligentRenderer> m_pRenderer;
|
||||
Bool m_initialized = false;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DiligentBackend
|
||||
@@ -0,0 +1,8 @@
|
||||
// MobileGL - MobileGL/MG_Backend/Diligent/DiligentVulkan.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
#include "DiligentVulkan.h"
|
||||
@@ -0,0 +1,17 @@
|
||||
// MobileGL - MobileGL/MG_Backend/Diligent/DiligentVulkan.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DiligentBackend {
|
||||
// Backend identity string used by the backend object and local smoke tests.
|
||||
inline String GetDiligentVulkanBackendName() {
|
||||
return "DiligentVulkan";
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DiligentBackend
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,153 @@
|
||||
// MobileGL - MobileGL/MG_Backend/Diligent/Renderer/DiligentRenderer.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
// X11 (pulled in by Includes.h through Vulkan-Headers) defines True/False as
|
||||
// macros, which collide with Diligent's Bool constants in BasicTypes.h.
|
||||
#if defined(True)
|
||||
#undef True
|
||||
#endif
|
||||
#if defined(False)
|
||||
#undef False
|
||||
#endif
|
||||
|
||||
#include <RefCntAutoPtr.hpp>
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
struct WindowHandle;
|
||||
}
|
||||
|
||||
namespace Diligent {
|
||||
struct IRenderDevice;
|
||||
struct IDeviceContext;
|
||||
struct ITexture;
|
||||
struct ITextureView;
|
||||
struct IPipelineState;
|
||||
struct IBuffer;
|
||||
struct ISampler;
|
||||
struct IShaderResourceBinding;
|
||||
struct ISwapChain;
|
||||
struct IEngineFactoryVk;
|
||||
}
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
class ITextureObject;
|
||||
class SamplerObject;
|
||||
class ProgramObject;
|
||||
class RenderbufferObject;
|
||||
class FramebufferObject;
|
||||
}
|
||||
|
||||
namespace MobileGL::MG_Backend::DiligentBackend {
|
||||
// Minimal real Diligent renderer used to prove the GL 3.2 basic path:
|
||||
// clear an offscreen color target, draw a hardcoded triangle, and read
|
||||
// pixels back. This is the first concrete rendering layer on top of the
|
||||
// Diligent device; it will be expanded into the full MobileGL backend.
|
||||
class DiligentRenderer {
|
||||
public:
|
||||
DiligentRenderer(::Diligent::IRenderDevice* device, ::Diligent::IDeviceContext* context);
|
||||
~DiligentRenderer();
|
||||
|
||||
Bool Initialize(Uint32 width, Uint32 height);
|
||||
void Clear(Float r, Float g, Float b, Float a);
|
||||
void ClearDepth(Float depth);
|
||||
void ClearStencil(Uint32 stencil);
|
||||
void DrawTriangle();
|
||||
void DrawVertices(const Float* vertices, Uint32 vertexCount);
|
||||
// Creates a real Diligent swap chain for a native EGL window surface.
|
||||
Bool CreateSwapChain(::Diligent::IEngineFactoryVk* factory, const WindowHandle& handle,
|
||||
Uint32 width, Uint32 height);
|
||||
Bool ResizeSwapChain(Uint32 width, Uint32 height);
|
||||
void SetSwapInterval(Uint32 interval);
|
||||
// Creates a simple 2D RGBA8 texture from CPU data and makes it available
|
||||
// to state PSOs under the shader variable name "g_Texture".
|
||||
Bool CreateTestTexture(const void* data, Uint32 width, Uint32 height);
|
||||
// Draws using the live MG_State GL context: current program, VAO and
|
||||
// bound buffers. This is the front-end emulation entry point.
|
||||
void DrawFromState(GLenum mode, GLint first, GLsizei count, GLenum type, const void* indices,
|
||||
GLint baseVertex = 0);
|
||||
void ReadPixels(Uint32 x, Uint32 y, Uint32 width, Uint32 height, void* pixels);
|
||||
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
||||
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
||||
GLbitfield mask, GLenum filter);
|
||||
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFbo,
|
||||
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFbo,
|
||||
GLbitfield mask);
|
||||
void CopyReadFramebufferToTexture(MG_State::GLState::ITextureObject& dst);
|
||||
void CopyTextureSubData(MG_State::GLState::ITextureObject& src, MG_State::GLState::ITextureObject& dst);
|
||||
void GenerateMipmap(MG_State::GLState::ITextureObject& texture);
|
||||
Bool ReadTextureImage(MG_State::GLState::ITextureObject& texture, Uint32 level, void* pixels);
|
||||
void ReleaseSwapChain();
|
||||
void Present();
|
||||
|
||||
::Diligent::IRenderDevice* GetDevice() const { return m_pDevice; }
|
||||
::Diligent::IDeviceContext* GetContext() const { return m_pContext; }
|
||||
|
||||
private:
|
||||
struct TextureResource {
|
||||
::Diligent::RefCntAutoPtr<::Diligent::ITexture> Texture;
|
||||
::Diligent::RefCntAutoPtr<::Diligent::ITextureView> SRV;
|
||||
::Diligent::RefCntAutoPtr<::Diligent::ITextureView> RTV;
|
||||
::Diligent::RefCntAutoPtr<::Diligent::ITextureView> DSV;
|
||||
Uint64 ContentVersion = 0;
|
||||
Uint16 ParamsVersion = 0;
|
||||
Bool IsDepth = false;
|
||||
};
|
||||
|
||||
struct SamplerResource {
|
||||
::Diligent::RefCntAutoPtr<::Diligent::ISampler> Sampler;
|
||||
Uint16 Version = 0;
|
||||
};
|
||||
|
||||
Bool CreateOffscreenTargets();
|
||||
Bool CreatePipeline();
|
||||
Bool CreateVertexBuffer();
|
||||
Bool CreatePipelineFromState(GLenum mode);
|
||||
Bool UploadVertexDataFromState(GLenum mode, GLint first, GLsizei count, GLenum type, const void* indices,
|
||||
GLint baseVertex = 0);
|
||||
::Diligent::ITextureView* SyncTexture(MG_State::GLState::ITextureObject& texture);
|
||||
::Diligent::ITextureView* SyncTextureForAttachment(MG_State::GLState::ITextureObject& texture, Bool depth);
|
||||
::Diligent::ITextureView* SyncRenderbuffer(MG_State::GLState::RenderbufferObject& renderbuffer);
|
||||
::Diligent::ISampler* SyncSampler(const MG_State::GLState::SamplerObject& sampler);
|
||||
Bool BindShaderResourcesFromState(const MG_State::GLState::ProgramObject& program);
|
||||
Bool UploadUBOFromState(const MG_State::GLState::ProgramObject& program);
|
||||
Bool ResolveCurrentRenderTargets(Vector<::Diligent::ITextureView*>& rtvs,
|
||||
::Diligent::ITextureView*& dsv);
|
||||
|
||||
::Diligent::IRenderDevice* m_pDevice = nullptr;
|
||||
::Diligent::IDeviceContext* m_pContext = nullptr;
|
||||
::Diligent::RefCntAutoPtr<::Diligent::ITexture> m_pColorTarget;
|
||||
::Diligent::RefCntAutoPtr<::Diligent::ITextureView> m_pColorRTV;
|
||||
::Diligent::RefCntAutoPtr<::Diligent::ITexture> m_pDepthTarget;
|
||||
::Diligent::RefCntAutoPtr<::Diligent::ITextureView> m_pDepthDSV;
|
||||
::Diligent::RefCntAutoPtr<::Diligent::ISwapChain> m_pSwapChain;
|
||||
::Diligent::RefCntAutoPtr<::Diligent::ITexture> m_pTestTexture;
|
||||
::Diligent::RefCntAutoPtr<::Diligent::ITextureView> m_pTestSRV;
|
||||
::Diligent::RefCntAutoPtr<::Diligent::ISampler> m_pTestSampler;
|
||||
::Diligent::RefCntAutoPtr<::Diligent::IShaderResourceBinding> m_pStateSRB;
|
||||
::Diligent::RefCntAutoPtr<::Diligent::IPipelineState> m_pPSO;
|
||||
::Diligent::RefCntAutoPtr<::Diligent::IBuffer> m_pVertexBuffer;
|
||||
::Diligent::RefCntAutoPtr<::Diligent::IBuffer> m_pUBO;
|
||||
Uint32 m_uboSize = 0;
|
||||
Uint32 m_uboContentVersion = 0;
|
||||
Uint64 m_uboProgramLifetimeId = 0;
|
||||
UnorderedMap<Uint64, TextureResource> m_textureCache;
|
||||
UnorderedMap<Uint64, SamplerResource> m_samplerCache;
|
||||
UnorderedMap<Uint32, TextureResource> m_renderbufferCache;
|
||||
UnorderedMap<Uint64, ::Diligent::RefCntAutoPtr<::Diligent::IBuffer>> m_namedUboCache;
|
||||
Uint32 m_width = 256;
|
||||
Uint32 m_height = 256;
|
||||
Uint32 m_swapInterval = 0;
|
||||
Uint32 m_lastDrawVertexCount = 0;
|
||||
Uint64 m_lastPSOKey = 0;
|
||||
Bool m_hasCachedPSO = false;
|
||||
Bool m_initialized = false;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DiligentBackend
|
||||
@@ -712,9 +712,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
{
|
||||
.TargetGLVersion = {4, 0, 0}, // GL target version
|
||||
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
||||
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
|
||||
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false),
|
||||
// Baseline advertisement (no runtime capabilities yet); reconciled once
|
||||
// the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||
.IsCompatibilityProfile = false // Is Compatibility Profile
|
||||
},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
|
||||
@@ -734,9 +734,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// thread can only observe the extension string after the
|
||||
// advertisement for its context has settled; rebuilding the whole
|
||||
// list keeps the re-run after a context recreation idempotent.
|
||||
void UpdateAdvertisedCapabilityExtensions(Bool anisotropicFilteringSupported) {
|
||||
MutableRendererInfo().RendererGLInfo.Extensions =
|
||||
BuildAdvertisedExtensions(AreTimerQueriesSupported(), anisotropicFilteringSupported);
|
||||
void UpdateAdvertisedCapabilityExtensions(const MG_External::GLESCapabilities& capabilities) {
|
||||
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
|
||||
capabilities.SupportsDrawIndirect,
|
||||
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -779,11 +781,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return false;
|
||||
}
|
||||
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
|
||||
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query and
|
||||
// GL_EXT_texture_filter_anisotropic, reconcile the advertisement (see the comment on
|
||||
// UpdateAdvertisedCapabilityExtensions for why it cannot happen when the extension
|
||||
// list is first built).
|
||||
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities.SupportsTextureFilterAnisotropy);
|
||||
// Now that g_GLESCapabilities knows the host extensions, entry points, and ES version,
|
||||
// reconcile every runtime-gated advertisement (see the comment on
|
||||
// UpdateAdvertisedCapabilityExtensions for why this cannot happen when the list is first
|
||||
// built).
|
||||
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities);
|
||||
UpdateDynamicBackendParameters();
|
||||
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
|
||||
PrintFormatCapabilities(GetFormatCapabilities());
|
||||
@@ -924,11 +926,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return MutableRendererInfo();
|
||||
}
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) {
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||
Vector<GLExtension> extensions = {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
|
||||
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
|
||||
E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
|
||||
E_GL_ARB_texture_storage_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
|
||||
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_ARB_shader_draw_parameters,
|
||||
@@ -955,6 +959,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
E_GL_ARB_get_program_binary};
|
||||
// Minecraft 26.3 checks this prerequisite before it even considers
|
||||
// GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader
|
||||
// folds the version and pointer checks into SupportsDrawIndirect.
|
||||
if (drawIndirectSupported) {
|
||||
extensions.push_back(E_GL_ARB_draw_indirect);
|
||||
}
|
||||
// ARB_base_instance also defines the last word of an indirect command. Direct calls are
|
||||
// emulated on every Espryt device, but without host GL_EXT_base_instance a native indirect
|
||||
// draw cannot shift divisor attributes by a GPU-authored non-zero value, so do not promise
|
||||
// that incomplete case.
|
||||
if (drawIndirectSupported && nonZeroIndirectBaseInstanceSupported) {
|
||||
extensions.push_back(E_GL_ARB_base_instance);
|
||||
}
|
||||
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the host ES
|
||||
// driver's: the compiler threads are MobileGL's, and glCompileShader/glLinkProgram
|
||||
// are serviced entirely inside the frontend. Whether the device driver advertises
|
||||
|
||||
@@ -67,9 +67,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const RendererInfo& GetRendererIdentity();
|
||||
|
||||
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
|
||||
// for a device whose timer queries / anisotropic filtering are (or are not) usable.
|
||||
// for a device whose timer queries / anisotropic filtering / native indirect draws /
|
||||
// non-zero indirect baseInstance semantics are (or are not) usable.
|
||||
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported);
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported);
|
||||
|
||||
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
|
||||
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
||||
|
||||
@@ -261,14 +261,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
drawBuffer->SyncPersistentMappedRange();
|
||||
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
||||
if (commandOffset + requiredBytes > drawBuffer->GetSize()) {
|
||||
MGLOG_E("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
|
||||
MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
|
||||
return nullptr;
|
||||
}
|
||||
return drawBuffer->MappedData() + commandOffset;
|
||||
}
|
||||
|
||||
if (!indirect) {
|
||||
MGLOG_E("%s skipped: indirect pointer is null", label);
|
||||
MGLOG_E_ONCE("%s skipped: indirect pointer is null", label);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -341,7 +341,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
auto* backendResource = EnsureBufferResource(obj);
|
||||
if (!backendResource || backendResource->id == 0) {
|
||||
MGLOG_E("No backend buffer found for %s binding point %zu.",
|
||||
MGLOG_E_ONCE("No backend buffer found for %s binding point %zu.",
|
||||
MG_Util::ConvertGLEnumToString(glTarget).c_str(), i);
|
||||
continue;
|
||||
}
|
||||
@@ -385,7 +385,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
auto* backendResource = EnsureBufferResource(bufferObject);
|
||||
if (!backendResource || backendResource->id == 0) {
|
||||
MGLOG_E("No backend buffer found for %s.", MG_Util::ConvertGLEnumToString(glTarget).c_str());
|
||||
MGLOG_E_ONCE("No backend buffer found for %s.", MG_Util::ConvertGLEnumToString(glTarget).c_str());
|
||||
return;
|
||||
}
|
||||
BindBufferId(glTarget, backendResource->id);
|
||||
@@ -410,7 +410,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// PBO is not needed since it should be handled in frontend
|
||||
|
||||
if (!currentVAOObject) {
|
||||
MGLOG_E("No VAO is currently bound, cannot sync necessary buffers.");
|
||||
MGLOG_E_ONCE("No VAO is currently bound, cannot sync necessary buffers.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -643,7 +643,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static_cast<GLintptr>(target.start),
|
||||
static_cast<GLsizeiptr>(size), GL_MAP_READ_BIT);
|
||||
if (mapped == nullptr) {
|
||||
MGLOG_E("EndTransformFeedback: failed to map backend buffer %u for capture readback",
|
||||
MGLOG_E_ONCE("EndTransformFeedback: failed to map backend buffer %u for capture readback",
|
||||
target.backendId);
|
||||
continue;
|
||||
}
|
||||
@@ -709,7 +709,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static_cast<GLsizeiptr>(packedStride * vertices),
|
||||
GL_MAP_READ_BIT);
|
||||
if (packed == nullptr) {
|
||||
MGLOG_E("EndTransformFeedback: failed to map the scatter capture buffer");
|
||||
MGLOG_E_ONCE("EndTransformFeedback: failed to map the scatter capture buffer");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -935,7 +935,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_backendVertexArrayObjects.CollectGarbageIfNeeded();
|
||||
|
||||
if (!currentVAOObject || !vaoTwin) {
|
||||
MGLOG_E("No VAO is currently bound, cannot sync current VAO.");
|
||||
MGLOG_E_ONCE("No VAO is currently bound, cannot sync current VAO.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -999,7 +999,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glVertexAttribI4uiv(location, currentValue.uintValue.data());
|
||||
break;
|
||||
case MG_State::GLState::VertexAttribBaseType::Unsupported:
|
||||
MGLOG_E("SyncCurrentVertexAttributeValues: program=%u location=%u has no enabled array and its "
|
||||
MGLOG_E_ONCE("SyncCurrentVertexAttributeValues: program=%u location=%u has no enabled array and its "
|
||||
"shader input type 0x%x is not supported as a current generic vertex attribute",
|
||||
program->GetExternalIndex(), location, program->GetAttribType(location));
|
||||
break;
|
||||
@@ -1469,7 +1469,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
if (!currentFBO) {
|
||||
MGLOG_E("No FBO is currently bound, cannot sync current FBO.");
|
||||
MGLOG_E_ONCE("No FBO is currently bound, cannot sync current FBO.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1600,7 +1600,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
!g_hasSyncedRenderState || std::memcmp(currentBytes + kBlendSpanEnd, syncedBytes + kBlendSpanEnd,
|
||||
sizeof(RenderStateParameters) - kBlendSpanEnd) != 0;
|
||||
|
||||
IntVec4 backendViewport = parameters.Viewport;
|
||||
IntVec4 backendViewport = MG_State::pGLContext->GetViewport();
|
||||
if (backendViewport.z() <= 0 || backendViewport.w() <= 0) {
|
||||
Int surfaceWidth = 0;
|
||||
Int surfaceHeight = 0;
|
||||
@@ -1614,7 +1614,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_syncedBackendViewport = backendViewport;
|
||||
}
|
||||
|
||||
// All 12 capability bools live after LogicOp in the struct, i.e. in the tail span.
|
||||
// Every capability bool (and the scissor-test mask below) lives after LogicOp in the
|
||||
// struct, i.e. in the tail span.
|
||||
if (tailSpanDirty) {
|
||||
#define SYNC_CAPABILITY(cap_mg, cap_gl) \
|
||||
if (forceFullPush || parameters.cap_mg##Enabled != g_syncedRenderStateParameters.cap_mg##Enabled) { \
|
||||
@@ -1633,11 +1634,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
SYNC_CAPABILITY(SampleMask, GL_SAMPLE_MASK);
|
||||
SYNC_CAPABILITY(PolygonOffsetFill, GL_POLYGON_OFFSET_FILL);
|
||||
SYNC_CAPABILITY(RasterizerDiscard, GL_RASTERIZER_DISCARD);
|
||||
SYNC_CAPABILITY(ScissorTest, GL_SCISSOR_TEST);
|
||||
SYNC_CAPABILITY(StencilTest, GL_STENCIL_TEST);
|
||||
SYNC_CAPABILITY(CullFace, GL_CULL_FACE);
|
||||
|
||||
#undef SYNC_CAPABILITY
|
||||
|
||||
// GL_SCISSOR_TEST is per-viewport enable state (ARB_viewport_array), so it is a
|
||||
// 16-bit mask and not a "<Name>Enabled" bool the macro above could key off. ES
|
||||
// has exactly one scissor rectangle and one scissor enable, so only bit 0 - the
|
||||
// index every ES draw rasterizes against - can be forwarded; a program that
|
||||
// enables the test for viewport 3 alone gets viewport 0's answer here. That is
|
||||
// the same limitation as the unemulated gl_ViewportIndex on this backend and is
|
||||
// why the multi-viewport half of KHR-GL43.viewport_array stays red on Espryt.
|
||||
{
|
||||
const Bool scissorTest = (parameters.ScissorTestEnabledMask & 1u) != 0;
|
||||
const Bool syncedScissorTest =
|
||||
(g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0;
|
||||
if (forceFullPush || scissorTest != syncedScissorTest) {
|
||||
scissorTest ? g_GLESFuncs.glEnable(GL_SCISSOR_TEST) : g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tailSpanDirty && g_GLESCapabilities.SupportsClipDistance) {
|
||||
@@ -1864,8 +1880,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (forceFullPush || parameters.DepthMask != g_syncedRenderStateParameters.DepthMask) {
|
||||
g_GLESFuncs.glDepthMask(parameters.DepthMask ? GL_TRUE : GL_FALSE);
|
||||
}
|
||||
if (forceFullPush || parameters.DepthRange != g_syncedRenderStateParameters.DepthRange) {
|
||||
g_GLESFuncs.glDepthRangef(parameters.DepthRange.x(), parameters.DepthRange.y());
|
||||
if (forceFullPush || parameters.DepthRanges[0] != g_syncedRenderStateParameters.DepthRanges[0]) {
|
||||
g_GLESFuncs.glDepthRangef(parameters.DepthRanges[0].x(), parameters.DepthRanges[0].y());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2003,7 +2019,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// everything drawn with GL_SCISSOR_TEST enabled before the app's first glScissor
|
||||
// is clipped away - Minecraft 26.2 keeps only its unscissored sky and hand and
|
||||
// loses the terrain and the whole GUI.
|
||||
IntVec4 backendScissorBox = parameters.ScissorBox;
|
||||
IntVec4 backendScissorBox = parameters.ScissorBoxes[0];
|
||||
if (backendScissorBox.z() <= 0 || backendScissorBox.w() <= 0) {
|
||||
Int surfaceWidth = 0;
|
||||
Int surfaceHeight = 0;
|
||||
@@ -2195,7 +2211,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
twin->GetUnormFallbackClampOutputMask() != g_unormFallbackClampOutputMask ||
|
||||
twin->GetFragColorBroadcastCount() != g_fragColorBroadcastCount ||
|
||||
twin->GetShaderStorageBlockBindingSignature() !=
|
||||
ComputeShaderStorageBlockBindingSignature(*currentProgram)) {
|
||||
ComputeShaderStorageBlockBindingSignature(*currentProgram) ||
|
||||
// A fourth of the same shape, and the reason glBindImageTexture itself does
|
||||
// nothing: GLSL ES demands a format layout qualifier on an image where desktop
|
||||
// GLSL lets a writeonly declaration omit one, so a format-less declaration is
|
||||
// compiled against the format the application BOUND, and a rebind to a
|
||||
// different one makes what was built wrong. Asked of the twin because only it
|
||||
// knows which units its own images address - and answered by an empty-vector
|
||||
// test for every program that declares its formats, which is nearly all of them.
|
||||
!twin->ImageUnitFormatsStillMatch()) {
|
||||
twin->SyncToBackend(currentProgram);
|
||||
}
|
||||
g_currentDrawFrontendProgram = currentProgram.get();
|
||||
@@ -2232,7 +2256,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (twin) {
|
||||
twin->Bind(target);
|
||||
} else {
|
||||
MGLOG_E("No backend FBO found (maybe not synced) for current %s FBO, cannot bind FBO.",
|
||||
MGLOG_E_ONCE("No backend FBO found (maybe not synced) for current %s FBO, cannot bind FBO.",
|
||||
(target == FramebufferTarget::Read ? "READ" : "DRAW"));
|
||||
}
|
||||
} else {
|
||||
@@ -2836,7 +2860,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
(GLintptr)range.start, (GLintptr)(range.end - range.start));
|
||||
}
|
||||
} else {
|
||||
MGLOG_E("No backend buffer found for UBO binding, cannot bind UBO.");
|
||||
MGLOG_E_ONCE("No backend buffer found for UBO binding, cannot bind UBO.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2968,7 +2992,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
} else {
|
||||
g_GLESFuncs.glUseProgram(0);
|
||||
PrgramImpl::g_lastUsedBackendProgramId = 0;
|
||||
MGLOG_E("No backend program found (maybe not synced) for current program, cannot use program.");
|
||||
MGLOG_E_ONCE("No backend program found (maybe not synced) for current program, cannot use program.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3037,10 +3061,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
static Bool SupportsNativeIndirectDraws() {
|
||||
const auto& version = g_GLESCapabilities.GLESVersion;
|
||||
const Bool esVersionOk = version.Major > 3 || (version.Major == 3 && version.Minor >= 1);
|
||||
return esVersionOk && g_GLESFuncs.glDrawElementsIndirect != nullptr &&
|
||||
g_GLESFuncs.glDrawArraysIndirect != nullptr;
|
||||
return g_GLESCapabilities.SupportsDrawIndirect;
|
||||
}
|
||||
|
||||
// Runs an (indexed) indirect multi-draw. When a GL_DRAW_INDIRECT_BUFFER is bound the draws
|
||||
@@ -3205,13 +3226,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
GLuint GetBackendProgramId(GLuint program) {
|
||||
if (!MG_State::pGLContext->ValidateProgramName(program)) {
|
||||
MGLOG_E("Invalid frontend program object: %u", program);
|
||||
MGLOG_E_ONCE("Invalid frontend program object: %u", program);
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto& programObject = MG_State::pGLContext->GetProgramObject(program);
|
||||
if (!programObject) {
|
||||
MGLOG_E("Program object %u is null.", program);
|
||||
MGLOG_E_ONCE("Program object %u is null.", program);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -3478,7 +3499,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
stride = sizeof(DrawElementsIndirectCommand);
|
||||
}
|
||||
if (stride < static_cast<GLsizei>(sizeof(DrawElementsIndirectCommand))) {
|
||||
MGLOG_E("MultiDrawElementsIndirect skipped: stride %d is smaller than command size %zu",
|
||||
MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: stride %d is smaller than command size %zu",
|
||||
stride, sizeof(DrawElementsIndirectCommand));
|
||||
return;
|
||||
}
|
||||
@@ -3488,7 +3509,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
if (indexSize == 0) {
|
||||
MGLOG_E("MultiDrawElementsIndirect skipped: unsupported index type 0x%x", type);
|
||||
MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: unsupported index type 0x%x", type);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3518,7 +3539,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
stride = sizeof(DrawElementsIndirectCommand);
|
||||
}
|
||||
if (stride < static_cast<GLsizei>(sizeof(DrawElementsIndirectCommand))) {
|
||||
MGLOG_E("MultiDrawElementsIndirectCount skipped: stride %d is smaller than command size %zu",
|
||||
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: stride %d is smaller than command size %zu",
|
||||
stride, sizeof(DrawElementsIndirectCommand));
|
||||
return;
|
||||
}
|
||||
@@ -3528,18 +3549,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
if (indexSize == 0) {
|
||||
MGLOG_E("MultiDrawElementsIndirectCount skipped: unsupported index type 0x%x", type);
|
||||
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: unsupported index type 0x%x", type);
|
||||
return;
|
||||
}
|
||||
|
||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
if (!drawBuffer) {
|
||||
MGLOG_E("MultiDrawElementsIndirectCount skipped: no GL_DRAW_INDIRECT_BUFFER is bound");
|
||||
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: no GL_DRAW_INDIRECT_BUFFER is bound");
|
||||
return;
|
||||
}
|
||||
if (!parameterBuffer) {
|
||||
MGLOG_E("MultiDrawElementsIndirectCount skipped: no GL_PARAMETER_BUFFER is bound");
|
||||
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: no GL_PARAMETER_BUFFER is bound");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3550,11 +3571,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const SizeT commandBytes = commandOffset + static_cast<SizeT>(stride) * static_cast<SizeT>(maxdrawcount - 1) +
|
||||
sizeof(DrawElementsIndirectCommand);
|
||||
if (commandBytes > drawBuffer->GetSize()) {
|
||||
MGLOG_E("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
|
||||
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
|
||||
return;
|
||||
}
|
||||
if (drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
|
||||
MGLOG_E("MultiDrawElementsIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
||||
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3562,7 +3583,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// have - MappedData() is null there and the reads below would be a null dereference,
|
||||
// not a wrong picture. The DirectVulkan twin declines the same way.
|
||||
if (parameterBuffer->MappedData() == nullptr || drawBuffer->MappedData() == nullptr) {
|
||||
MGLOG_E("MultiDrawElementsIndirectCount skipped: CPU fallback cannot read the parameter or "
|
||||
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: CPU fallback cannot read the parameter or "
|
||||
"draw-indirect buffer");
|
||||
return;
|
||||
}
|
||||
@@ -3586,7 +3607,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
stride = sizeof(DrawArraysIndirectCommand);
|
||||
}
|
||||
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
|
||||
MGLOG_E("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu",
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu",
|
||||
stride, sizeof(DrawArraysIndirectCommand));
|
||||
return;
|
||||
}
|
||||
@@ -3626,7 +3647,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
stride = sizeof(DrawArraysIndirectCommand);
|
||||
}
|
||||
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
|
||||
stride, sizeof(DrawArraysIndirectCommand));
|
||||
return;
|
||||
}
|
||||
@@ -3637,11 +3658,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
if (!drawBuffer) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: no GL_DRAW_INDIRECT_BUFFER is bound");
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: no GL_DRAW_INDIRECT_BUFFER is bound");
|
||||
return;
|
||||
}
|
||||
if (!parameterBuffer) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: no GL_PARAMETER_BUFFER is bound");
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: no GL_PARAMETER_BUFFER is bound");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3652,17 +3673,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const SizeT commandBytes = commandOffset + static_cast<SizeT>(stride) * static_cast<SizeT>(maxdrawcount - 1) +
|
||||
sizeof(DrawArraysIndirectCommand);
|
||||
if (commandBytes > drawBuffer->GetSize()) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
|
||||
return;
|
||||
}
|
||||
if (drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
||||
return;
|
||||
}
|
||||
|
||||
// See the indexed twin: no CPU shadow means no count to read, not a wrong one.
|
||||
if (parameterBuffer->MappedData() == nullptr || drawBuffer->MappedData() == nullptr) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read the parameter or "
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read the parameter or "
|
||||
"draw-indirect buffer");
|
||||
return;
|
||||
}
|
||||
@@ -3755,7 +3776,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
if (indexSize == 0) {
|
||||
MGLOG_E("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
|
||||
MGLOG_E_ONCE("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3952,7 +3973,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(previousDraw));
|
||||
FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||
if (!resolved) {
|
||||
MGLOG_E("BlitFramebuffer: multisample resolve fallback failed");
|
||||
MGLOG_E_ONCE("BlitFramebuffer: multisample resolve fallback failed");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
@@ -4251,7 +4272,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
s_stencilProgram = BuildProgram(kStencilFragmentSource);
|
||||
if (s_depthProgram == 0 || s_stencilProgram == 0) {
|
||||
s_programsFailed = true;
|
||||
MGLOG_E("BlitFramebuffer: could not build the multisample replicate programs");
|
||||
MGLOG_E_ONCE("BlitFramebuffer: could not build the multisample replicate programs");
|
||||
return false;
|
||||
}
|
||||
s_depthUvTransform = g_GLESFuncs.glGetUniformLocation(s_depthProgram, "uUvTransform");
|
||||
@@ -4455,7 +4476,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(previousRead));
|
||||
FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||
if (!ok) {
|
||||
MGLOG_E("BlitFramebuffer: could not stage the source for the multisample replicate");
|
||||
MGLOG_E_ONCE("BlitFramebuffer: could not stage the source for the multisample replicate");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -4524,7 +4545,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
// Everything the pass disturbed goes back through emulationState's destructor.
|
||||
if (!replicated) {
|
||||
MGLOG_E("BlitFramebuffer: multisample replicate fallback failed");
|
||||
MGLOG_E_ONCE("BlitFramebuffer: multisample replicate fallback failed");
|
||||
}
|
||||
return replicated;
|
||||
}
|
||||
@@ -4559,7 +4580,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// outright, desktop GL replicates the source sample into every destination one.
|
||||
if (ReplicateBlitIntoMultisampleDraw(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask)) {
|
||||
if ((mask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||
MGLOG_E("BlitFramebuffer: colour replicate into a multisample draw framebuffer is not emulated");
|
||||
MGLOG_E_ONCE("BlitFramebuffer: colour replicate into a multisample draw framebuffer is not emulated");
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -4660,7 +4681,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
if (!TextureImpl::IsSupportedTextureTarget(textureTarget)) {
|
||||
MGLOG_E(" Texture target %s is not supported, skipping.",
|
||||
MGLOG_E_ONCE(" Texture target %s is not supported, skipping.",
|
||||
MG_Util::ConvertTextureTargetToString(textureTarget).c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -4669,7 +4690,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
{
|
||||
const auto& textureObject = bindingSlot.GetBoundObject();
|
||||
if (!textureObject) {
|
||||
MGLOG_W("%s: Texture target %s does not have texture bound.", __func__,
|
||||
MGLOG_D("%s: Texture target %s does not have texture bound.", __func__,
|
||||
MG_Util::ConvertTextureTargetToString(textureTarget).c_str());
|
||||
}
|
||||
|
||||
@@ -4754,7 +4775,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// restores the app state on exit, tracked via the render-state shadow.
|
||||
class ScopedScissorDisable {
|
||||
public:
|
||||
ScopedScissorDisable() : m_wasEnabled(RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabled) {
|
||||
ScopedScissorDisable()
|
||||
: m_wasEnabled((RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0) {
|
||||
if (m_wasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
||||
}
|
||||
~ScopedScissorDisable() {
|
||||
@@ -4918,7 +4940,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return true;
|
||||
}
|
||||
|
||||
MGLOG_E("%s failed: %s. target=%s, format=%s", operation,
|
||||
MGLOG_E_ONCE("%s failed: %s. target=%s, format=%s", operation,
|
||||
MG_Util::ConvertGLEnumToString(err).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(target).c_str(),
|
||||
MG_Util::ConvertTextureInternalFormatToString(format).c_str());
|
||||
@@ -5254,7 +5276,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
.GetBoundObject();
|
||||
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
|
||||
if (!backendTextureSlot || !*backendTextureSlot) {
|
||||
MGLOG_E("CopyTexSubImage2D: No backend texture found for texture %u.",
|
||||
MGLOG_E_ONCE("CopyTexSubImage2D: No backend texture found for texture %u.",
|
||||
textureObject ? textureObject->GetExternalIndex() : 0);
|
||||
return;
|
||||
}
|
||||
@@ -5305,7 +5327,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static_cast<Uint>(currentTex), target, level, isStencilFormat);
|
||||
|
||||
if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
MGLOG_E("ES glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE");
|
||||
MGLOG_E_ONCE("ES glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5349,7 +5371,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
.GetBoundObject();
|
||||
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
|
||||
if (!backendTextureSlot || !*backendTextureSlot) {
|
||||
MGLOG_E("CopyTexSubImage2D: No backend texture found for texture %u.",
|
||||
MGLOG_E_ONCE("CopyTexSubImage2D: No backend texture found for texture %u.",
|
||||
textureObject ? textureObject->GetExternalIndex() : 0);
|
||||
return;
|
||||
}
|
||||
@@ -5387,7 +5409,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
||||
});
|
||||
if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
MGLOG_E("ES glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE");
|
||||
MGLOG_E_ONCE("ES glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5555,6 +5577,56 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glMemoryBarrierByRegion(barriers);
|
||||
}
|
||||
|
||||
// One endpoint of a glCopyImageSubData, expressed the way the ES driver stores it.
|
||||
//
|
||||
// The frontend hands this backend the target the APPLICATION named, and three of the
|
||||
// targets core GL has do not exist in ES at all. They are not missing here either - the
|
||||
// texture managers already store a 1D texture as a height-1 2D one, a 1D array as a
|
||||
// height-1 2D array and a rectangle texture as a plain 2D one (MapToBackendTextureTarget) -
|
||||
// but glCopyImageSubData was the one path that never asked for that translation and passed
|
||||
// 0x84F5 / 0x0DE0 / 0x8C18 straight through. ES rejects the enum, the copy does not happen,
|
||||
// and with the error only asserted on (asserts are compiled out of an INFO build) the
|
||||
// destination silently keeps whatever it held.
|
||||
//
|
||||
// The 1D-array case is not just a rename: GL addresses its layers with y/height while the
|
||||
// ES 2D array that backs it addresses them with z/depth, so the two axes swap with the
|
||||
// target.
|
||||
struct GLESCopyImageEndpoint {
|
||||
GLenum target = GL_TEXTURE_2D;
|
||||
GLint x = 0;
|
||||
GLint y = 0;
|
||||
GLint z = 0;
|
||||
};
|
||||
|
||||
static GLESCopyImageEndpoint MakeGLESCopyImageEndpoint(GLenum appTarget, GLint x, GLint y, GLint z) {
|
||||
const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget);
|
||||
GLESCopyImageEndpoint endpoint{};
|
||||
endpoint.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget);
|
||||
if (stateTarget == TextureTarget::Texture1DArray) {
|
||||
endpoint.x = x;
|
||||
endpoint.y = 0;
|
||||
endpoint.z = y;
|
||||
return endpoint;
|
||||
}
|
||||
endpoint.x = x;
|
||||
endpoint.y = y;
|
||||
endpoint.z = z;
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
// The region extent swaps the same two axes for a 1D array, and does so for whichever side
|
||||
// of the copy is one - GL forbids a copy whose two endpoints disagree about how many layers
|
||||
// move, so at most one of the two can be a 1D array only in the degenerate single-layer
|
||||
// case, where the swap is the identity anyway.
|
||||
static void ApplyGLESCopyImageExtent(GLenum appSrcTarget, GLenum appDstTarget, GLsizei& height, GLsizei& depth) {
|
||||
const TextureTarget srcStateTarget = MG_Util::ConvertGLEnumToTextureTarget(appSrcTarget);
|
||||
const TextureTarget dstStateTarget = MG_Util::ConvertGLEnumToTextureTarget(appDstTarget);
|
||||
if (srcStateTarget != TextureTarget::Texture1DArray && dstStateTarget != TextureTarget::Texture1DArray) {
|
||||
return;
|
||||
}
|
||||
std::swap(height, depth);
|
||||
}
|
||||
|
||||
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
@@ -5573,6 +5645,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
TextureImpl::SyncTextureObjectToBackend(srcTexture);
|
||||
const SharedPtr<TextureImpl::BackendTextureObject> dstBackendTexture =
|
||||
TextureImpl::SyncTextureObjectToBackend(dstTexture);
|
||||
// The DirectVulkan half of this entry point died exactly here, on a texture whose sync
|
||||
// produced nothing - and it died in a release build, where the MOBILEGL_ASSERT that was
|
||||
// supposed to catch it expands to nothing. The four GetBackendTextureId() calls below
|
||||
// are the same dereference. The frontend validator is what keeps this unreachable and
|
||||
// what reports the error the application is owed; declining is only how a future gap up
|
||||
// there stops being a crash. See the level guard in VulkanRenderer::CopyImageSubData.
|
||||
if (!srcBackendTexture || !dstBackendTexture) {
|
||||
MGLOG_E_ONCE("%s: source or destination texture failed to sync; declining the copy", __func__);
|
||||
return;
|
||||
}
|
||||
|
||||
const GLESCopyImageEndpoint src = MakeGLESCopyImageEndpoint(srcTarget, srcX, srcY, srcZ);
|
||||
const GLESCopyImageEndpoint dst = MakeGLESCopyImageEndpoint(dstTarget, dstX, dstY, dstZ);
|
||||
GLsizei copyHeight = srcHeight;
|
||||
GLsizei copyDepth = srcDepth;
|
||||
ApplyGLESCopyImageExtent(srcTarget, dstTarget, copyHeight, copyDepth);
|
||||
|
||||
const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcTexture->GetFormat());
|
||||
const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstTexture->GetFormat());
|
||||
@@ -5581,12 +5669,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (srcIsDepth || dstIsDepth || srcStencil || dstStencil) {
|
||||
MOBILEGL_ASSERT(srcIsDepth && dstIsDepth && !srcStencil && !dstStencil,
|
||||
"DirectGLES CopyImageSubData only supports depth-only image copies.");
|
||||
MOBILEGL_ASSERT(srcTarget == GL_TEXTURE_2D && dstTarget == GL_TEXTURE_2D,
|
||||
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
|
||||
"DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D.");
|
||||
MOBILEGL_ASSERT(srcZ == 0 && dstZ == 0 && srcDepth == 1,
|
||||
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
|
||||
"DirectGLES depth CopyImageSubData only supports single-layer copies.");
|
||||
BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, srcX, srcY, srcWidth, srcHeight,
|
||||
dstBackendTexture->GetBackendTextureId(), dstLevel, dstX, dstY, srcWidth, srcHeight);
|
||||
BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
|
||||
dstBackendTexture->GetBackendTextureId(), dstLevel, dst.x, dst.y, srcWidth, copyHeight);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5597,29 +5685,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// with the always-live helper so a stale flag cannot misroute a
|
||||
// succeeded native copy into the 2D-only fallback.
|
||||
ClearGLErrors();
|
||||
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), srcTarget, srcLevel, srcX, srcY, srcZ,
|
||||
dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY, dstZ,
|
||||
srcWidth, srcHeight, srcDepth);
|
||||
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z,
|
||||
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z,
|
||||
srcWidth, copyHeight, copyDepth);
|
||||
const GLenum copyImageError = g_GLESFuncs.glGetError();
|
||||
if (copyImageError == GL_NO_ERROR) {
|
||||
return;
|
||||
}
|
||||
MOBILEGL_ASSERT(IsColorOnlyFormat(srcTexture->GetFormat()) && IsColorOnlyFormat(dstTexture->GetFormat()),
|
||||
"DirectGLES CopyImageSubData only supports color-only or depth-only copies.");
|
||||
MOBILEGL_ASSERT(srcTarget == GL_TEXTURE_2D && dstTarget == GL_TEXTURE_2D,
|
||||
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
|
||||
"DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D.");
|
||||
MOBILEGL_ASSERT(srcZ == 0 && dstZ == 0 && srcDepth == 1,
|
||||
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
|
||||
"DirectGLES color CopyImageSubData only supports single-layer copies.");
|
||||
CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, srcX, srcY, srcWidth, srcHeight,
|
||||
dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY);
|
||||
CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
|
||||
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y);
|
||||
return;
|
||||
}
|
||||
|
||||
ClearGLErrors();
|
||||
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), srcTarget, srcLevel, srcX, srcY, srcZ,
|
||||
dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY, dstZ,
|
||||
srcWidth, srcHeight, srcDepth);
|
||||
AssertNoGLError("glCopyImageSubData");
|
||||
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z,
|
||||
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z,
|
||||
srcWidth, copyHeight, copyDepth);
|
||||
// Every error condition glCopyImageSubData has was already ruled out by the frontend
|
||||
// validator, so a driver error here is an internal invariant violation, not something
|
||||
// the application can provoke. Say so where an INFO build can still see it, then trap
|
||||
// in the builds that trap - the previous bare assert left a release build with a
|
||||
// destination that silently kept its old contents.
|
||||
const GLenum copyImageError = g_GLESFuncs.glGetError();
|
||||
if (copyImageError != GL_NO_ERROR) {
|
||||
MGLOG_E_ONCE("glCopyImageSubData failed: %s. src target=%s (app %s), dst target=%s (app %s)",
|
||||
MG_Util::ConvertGLEnumToString(copyImageError).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(src.target).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(srcTarget).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(dst.target).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(dstTarget).c_str());
|
||||
MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request.");
|
||||
}
|
||||
}
|
||||
|
||||
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
|
||||
@@ -5994,7 +6096,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
const SizeT pboOffset = reinterpret_cast<SizeT>(pixels);
|
||||
if (pixelPackBufferObject && pboOffset + packedSize > pixelPackBufferObject->GetSize()) {
|
||||
MGLOG_E("ReadPixels: %s readback PBO is too small", what);
|
||||
MGLOG_E_ONCE("ReadPixels: %s readback PBO is too small", what);
|
||||
return false;
|
||||
}
|
||||
Vector<Uint8> rowBuf(rowBytes);
|
||||
@@ -6148,7 +6250,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
s_stencilProgram = ReplicateBlitImpl::BuildProgram(kStencilFetchFragmentSource);
|
||||
if (s_depthProgram == 0 || s_stencilProgram == 0) {
|
||||
s_programsFailed = true;
|
||||
MGLOG_E("ReadPixels: could not build the depth/stencil readback programs");
|
||||
MGLOG_E_ONCE("ReadPixels: could not build the depth/stencil readback programs");
|
||||
return false;
|
||||
}
|
||||
s_depthUvTransform = g_GLESFuncs.glGetUniformLocation(s_depthProgram, "uUvTransform");
|
||||
@@ -6406,7 +6508,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ClearGLErrors();
|
||||
g_GLESFuncs.glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||
MGLOG_E("ReadPixels: the %s conversion pass failed", stencilAspect ? "stencil" : "depth");
|
||||
MGLOG_E_ONCE("ReadPixels: the %s conversion pass failed", stencilAspect ? "stencil" : "depth");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -6422,7 +6524,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glReadPixels(0, 0, width, height, GL_RGBA_INTEGER, GL_UNSIGNED_INT, outWords.data());
|
||||
const GLenum readError = g_GLESFuncs.glGetError();
|
||||
if (readError != GL_NO_ERROR) {
|
||||
MGLOG_E("ReadPixels: could not read the %s conversion target back: %s",
|
||||
MGLOG_E_ONCE("ReadPixels: could not read the %s conversion target back: %s",
|
||||
stencilAspect ? "stencil" : "depth", MG_Util::ConvertGLEnumToString(readError).c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -6442,20 +6544,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, slot.framebuffer);
|
||||
if (!StageAspect(slot, candidates, stencilAspect, isDefault, x, y, width, height)) {
|
||||
MGLOG_E("ReadPixels: no ES-compatible scratch format for the %s source",
|
||||
MGLOG_E_ONCE("ReadPixels: no ES-compatible scratch format for the %s source",
|
||||
stencilAspect ? "stencil" : "depth");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EnsureColorTexture(width, height)) {
|
||||
MGLOG_E("ReadPixels: could not allocate the depth/stencil conversion target");
|
||||
MGLOG_E_ONCE("ReadPixels: could not allocate the depth/stencil conversion target");
|
||||
return false;
|
||||
}
|
||||
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, s_colorFramebuffer);
|
||||
g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||
s_colorTexture, 0);
|
||||
if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
MGLOG_E("ReadPixels: the depth/stencil conversion target is not renderable");
|
||||
MGLOG_E_ONCE("ReadPixels: the depth/stencil conversion target is not renderable");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -6560,7 +6662,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (DepthStencilSamplingReadImpl::Read(x, y, width, height, &outDepth, /*outStencil=*/nullptr)) {
|
||||
return true;
|
||||
}
|
||||
MGLOG_E("ReadPixels: no depth readback path is available: native reads failed with %s and the "
|
||||
MGLOG_E_ONCE("ReadPixels: no depth readback path is available: native reads failed with %s and the "
|
||||
"sampling emulation could not service the source",
|
||||
MG_Util::ConvertGLEnumToString(floatError).c_str());
|
||||
return false;
|
||||
@@ -6664,7 +6766,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (DepthStencilSamplingReadImpl::Read(x, y, width, height, /*outDepth=*/nullptr, &outStencil)) {
|
||||
return true;
|
||||
}
|
||||
MGLOG_E("ReadPixels: no stencil readback path is available: native reads failed with %s and the "
|
||||
MGLOG_E_ONCE("ReadPixels: no stencil readback path is available: native reads failed with %s and the "
|
||||
"sampling emulation could not service the source",
|
||||
MG_Util::ConvertGLEnumToString(packedError).c_str());
|
||||
return false;
|
||||
@@ -6969,7 +7071,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const Bool integerAttachment =
|
||||
attachmentComponentType == GL_INT || attachmentComponentType == GL_UNSIGNED_INT;
|
||||
if (mapping.isInteger != integerAttachment) {
|
||||
MGLOG_E("Readback conversion: integer-ness of format %s does not match the read buffer, skipping",
|
||||
MGLOG_E_ONCE("Readback conversion: integer-ness of format %s does not match the read buffer, skipping",
|
||||
MG_Util::ConvertGLEnumToString(format).c_str());
|
||||
return true;
|
||||
}
|
||||
@@ -7045,7 +7147,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
if (wideType == GL_NONE) {
|
||||
MGLOG_E("Readback conversion: ES accepted no wide read type for format %s type %s, skipping readback",
|
||||
MGLOG_E_ONCE("Readback conversion: ES accepted no wide read type for format %s type %s, skipping readback",
|
||||
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
|
||||
return true;
|
||||
}
|
||||
@@ -7159,6 +7261,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return false;
|
||||
}
|
||||
|
||||
// glGetTexImage returns the stored texels, and for a packed internal format read with the
|
||||
// matching client type the shadow word already IS the client word. Decoding it to float and
|
||||
// re-encoding would canonicalize an RGB9_E5 shared exponent (0xf8fc0000 -> 0xe7e00000: the
|
||||
// same value, different bits), so those pairs copy the words straight through.
|
||||
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
|
||||
textureMipmapObject->GetFormat(), MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(type))) {
|
||||
if (!ReadbackImpl::StorePackedWordsToClient(static_cast<const Uint8*>(shadow), width, sliceHeight,
|
||||
sliceCount, type, pixels, applyPackImageParams)) {
|
||||
return false;
|
||||
}
|
||||
MGLOG_D("GetTexImage: copied %s/%s verbatim from the CPU shadow copy",
|
||||
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
Vector<Uint8> wide;
|
||||
Bool isInteger = false;
|
||||
Bool isSigned = false;
|
||||
@@ -7228,7 +7346,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const Bool convertible = GetReadbackChannelMapping(format, conversionMapping) &&
|
||||
GetReadbackDstPixelSize(conversionMapping, type) != 0;
|
||||
if (!useNativeReadback && !convertible) {
|
||||
MGLOG_E("ReadPixels: format %s with type %s is not implemented yet, skipping readback",
|
||||
MGLOG_E_ONCE("ReadPixels: format %s with type %s is not implemented yet, skipping readback",
|
||||
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
|
||||
return;
|
||||
}
|
||||
@@ -7249,7 +7367,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("ReadPixels: GL_READ_FRAMEBUFFER status = %s", MG_Util::ConvertGLEnumToString(fbStatus).c_str());
|
||||
|
||||
if (fbStatus != GL_FRAMEBUFFER_COMPLETE) {
|
||||
MGLOG_E("ReadPixels: bound READ FBO is not complete");
|
||||
MGLOG_E_ONCE("ReadPixels: bound READ FBO is not complete");
|
||||
return;
|
||||
}
|
||||
// ES only guarantees GL_RGBA/GL_UNSIGNED_BYTE and GL_RGBA_INTEGER/GL_(UNSIGNED_)INT for the
|
||||
@@ -7274,7 +7392,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("ReadPixels: finished via client-format conversion");
|
||||
return;
|
||||
}
|
||||
MGLOG_E("ReadPixels: format %s with type %s is not implemented yet, skipping readback",
|
||||
MGLOG_E_ONCE("ReadPixels: format %s with type %s is not implemented yet, skipping readback",
|
||||
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
|
||||
return;
|
||||
}
|
||||
@@ -7307,7 +7425,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
auto* backendResource = BufferImpl::EnsureBufferResource(pixelPackBufferObject);
|
||||
MGLOG_D("ReadPixels: Using PBO %u", pixelPackBufferObject->GetExternalIndex());
|
||||
if (!backendResource || backendResource->id == 0) {
|
||||
MGLOG_E("ReadPixels: No backend buffer found for PBO %u.",
|
||||
MGLOG_E_ONCE("ReadPixels: No backend buffer found for PBO %u.",
|
||||
pixelPackBufferObject ? pixelPackBufferObject->GetExternalIndex() : 0);
|
||||
return;
|
||||
}
|
||||
@@ -7339,7 +7457,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("ReadPixels: finished via client-format conversion after native failure");
|
||||
return;
|
||||
}
|
||||
MGLOG_E("ReadPixels: native read of %s/%s failed (%s) and no conversion path covers it, "
|
||||
MGLOG_E_ONCE("ReadPixels: native read of %s/%s failed (%s) and no conversion path covers it, "
|
||||
"skipping readback",
|
||||
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(nativeReadError).c_str());
|
||||
@@ -7359,7 +7477,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("ReadPixels: Unmapping PBO");
|
||||
g_GLESFuncs.glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
||||
} else {
|
||||
MGLOG_E("ReadPixels: glMapBufferRange returned nullptr");
|
||||
MGLOG_E_ONCE("ReadPixels: glMapBufferRange returned nullptr");
|
||||
}
|
||||
}
|
||||
MGLOG_D("ReadPixels: finished");
|
||||
@@ -7395,7 +7513,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const Bool convertible = GetReadbackChannelMapping(format, conversionMapping) &&
|
||||
GetReadbackDstPixelSize(conversionMapping, type) != 0;
|
||||
if (!useNativeReadback && !convertible) {
|
||||
MGLOG_E("GetTexImage: format %s with type %s is not implemented yet, skipping readback",
|
||||
MGLOG_E_ONCE("GetTexImage: format %s with type %s is not implemented yet, skipping readback",
|
||||
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
|
||||
return;
|
||||
}
|
||||
@@ -7423,7 +7541,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
|
||||
|
||||
if (!backendTextureSlot || !*backendTextureSlot) {
|
||||
MGLOG_E("GetTexImage: No backend texture found for texture %u.",
|
||||
MGLOG_E_ONCE("GetTexImage: No backend texture found for texture %u.",
|
||||
textureObject ? textureObject->GetExternalIndex() : 0);
|
||||
return;
|
||||
}
|
||||
@@ -7491,7 +7609,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("GetTexImage: texture storage type = %d", (int)storageType);
|
||||
|
||||
if (storageType == TextureStorageType::Buffer) {
|
||||
MGLOG_E("GetTexImage: Texture storage type Buffer is not supported.");
|
||||
MGLOG_E_ONCE("GetTexImage: Texture storage type Buffer is not supported.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7503,7 +7621,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// levelRange.y() is GL_TEXTURE_MAX_LEVEL, an inclusive level index — a single-level
|
||||
// texture has range [0, 0] and level 0 must be readable.
|
||||
if (static_cast<Uint>(level) < levelRange.x() || static_cast<Uint>(level) > levelRange.y()) {
|
||||
MGLOG_E("GetTexImage: Requested level %d is out of range (base level %u, max level %u), skipping readback",
|
||||
MGLOG_E_ONCE("GetTexImage: Requested level %d is out of range (base level %u, max level %u), skipping readback",
|
||||
level, levelRange.x(), levelRange.y());
|
||||
return;
|
||||
}
|
||||
@@ -7600,15 +7718,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
if (!tempFBOComplete) {
|
||||
MGLOG_E("GetTexImage: READ FBO incomplete and no shadow copy available, skipping readback");
|
||||
MGLOG_E_ONCE("GetTexImage: READ FBO incomplete and no shadow copy available, skipping readback");
|
||||
return;
|
||||
}
|
||||
MGLOG_E("GetTexImage: format %s with type %s is not implemented yet, skipping readback",
|
||||
MGLOG_E_ONCE("GetTexImage: format %s with type %s is not implemented yet, skipping readback",
|
||||
MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str());
|
||||
return;
|
||||
}
|
||||
if (!tempFBOComplete) {
|
||||
MGLOG_E("GetTexImage: bound READ FBO is not complete");
|
||||
MGLOG_E_ONCE("GetTexImage: bound READ FBO is not complete");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7636,7 +7754,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
auto* backendResource = BufferImpl::EnsureBufferResource(pixelPackBufferObject);
|
||||
MGLOG_D("GetTexImage: Using PBO %u", pixelPackBufferObject->GetExternalIndex());
|
||||
if (!backendResource || backendResource->id == 0) {
|
||||
MGLOG_E("GetTexImage: No backend buffer found for PBO %u.",
|
||||
MGLOG_E_ONCE("GetTexImage: No backend buffer found for PBO %u.",
|
||||
pixelPackBufferObject ? pixelPackBufferObject->GetExternalIndex() : 0);
|
||||
return;
|
||||
}
|
||||
@@ -7672,7 +7790,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("ReadPixels: Unmapping PBO");
|
||||
g_GLESFuncs.glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
||||
} else {
|
||||
MGLOG_E("ReadPixels: glMapBufferRange returned nullptr");
|
||||
MGLOG_E_ONCE("ReadPixels: glMapBufferRange returned nullptr");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8059,7 +8177,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (g_requestedSwapInterval < 0) return;
|
||||
if (!g_EGLFuncs.eglSwapInterval || g_Display == EGL_NO_DISPLAY || g_Surface == EGL_NO_SURFACE) return;
|
||||
const EGLBoolean ok = g_EGLFuncs.eglSwapInterval(g_Display, g_requestedSwapInterval);
|
||||
MGLOG_I("DirectGLES: applied native swap interval %d (%s)", g_requestedSwapInterval,
|
||||
MGLOG_D("DirectGLES: applied native swap interval %d (%s)", g_requestedSwapInterval,
|
||||
ok ? "ok" : "failed");
|
||||
}
|
||||
} // namespace
|
||||
@@ -8170,12 +8288,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool MakeCurrent() {
|
||||
if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY || g_Surface == EGL_NO_SURFACE ||
|
||||
g_Context == EGL_NO_CONTEXT) {
|
||||
MGLOG_E("DirectGLES::MakeCurrent failed: EGL display/surface/context is not initialized");
|
||||
MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: EGL display/surface/context is not initialized");
|
||||
return false;
|
||||
}
|
||||
if (!g_EGLFuncs.eglMakeCurrent(g_Display, g_Surface, g_Surface, g_Context)) {
|
||||
const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS;
|
||||
MGLOG_E("DirectGLES::MakeCurrent failed: native eglMakeCurrent returned error 0x%04x", error);
|
||||
MGLOG_E_ONCE("DirectGLES::MakeCurrent failed: native eglMakeCurrent returned error 0x%04x", error);
|
||||
return false;
|
||||
}
|
||||
InvalidateEglVerifiedStamp();
|
||||
@@ -8208,7 +8326,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
if (!g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) {
|
||||
const EGLint error = g_EGLFuncs.eglGetError ? g_EGLFuncs.eglGetError() : EGL_SUCCESS;
|
||||
MGLOG_E("DirectGLES::ReleaseCurrent failed: native eglMakeCurrent returned error 0x%04x", error);
|
||||
MGLOG_E_ONCE("DirectGLES::ReleaseCurrent failed: native eglMakeCurrent returned error 0x%04x", error);
|
||||
return false;
|
||||
}
|
||||
// Clearing the global owner works from ANY thread (a release request can
|
||||
|
||||
@@ -598,7 +598,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void* ptr = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size),
|
||||
GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit);
|
||||
if (!ptr) {
|
||||
MGLOG_E("Ops_AcquirePersistentMap: glMapBufferRange(persistent) failed for buffer %u",
|
||||
MGLOG_E_ONCE("Ops_AcquirePersistentMap: glMapBufferRange(persistent) failed for buffer %u",
|
||||
resource->id);
|
||||
resource->persistentMapped = false;
|
||||
resource->persistentPtr = nullptr;
|
||||
@@ -716,7 +716,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
resource->syncedChangeSerial = bufferObject.GetChangeSerial();
|
||||
return;
|
||||
}
|
||||
MGLOG_E("Failed to map buffer with ID: %u for flush, falling back to glBufferSubData",
|
||||
MGLOG_E_ONCE("Failed to map buffer with ID: %u for flush, falling back to glBufferSubData",
|
||||
resource->id);
|
||||
}
|
||||
UploadRangeNow(*resource, bufferObject, range.start, range.end);
|
||||
@@ -729,8 +729,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void Ops_ReadbackFromGpu(BufferObject& bufferObject) {
|
||||
auto* resource = ResourceOf(bufferObject);
|
||||
if (!resource || resource->id == 0 || !resource->storageInitialized) return;
|
||||
if (resource->persistentMapped) return; // shadow already IS the GPU storage
|
||||
if (!CanTouchGLNow() || resource->contextGeneration != g_bufferContextGeneration) return;
|
||||
if (resource->persistentMapped) {
|
||||
// Host writes to a persistent map must not race shader writes already queued
|
||||
// on this context. There is no backend copy to read back in this case.
|
||||
if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish();
|
||||
return;
|
||||
}
|
||||
if (!g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) return;
|
||||
const SizeT size = std::min<SizeT>(bufferObject.GetSize(), resource->storageSize);
|
||||
if (size == 0) return;
|
||||
@@ -739,7 +744,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size),
|
||||
GL_MAP_READ_BIT);
|
||||
if (mapped == nullptr) {
|
||||
MGLOG_E("Ops_ReadbackFromGpu: glMapBufferRange(read) failed for buffer %u", resource->id);
|
||||
MGLOG_E_ONCE("Ops_ReadbackFromGpu: glMapBufferRange(read) failed for buffer %u", resource->id);
|
||||
return;
|
||||
}
|
||||
bufferObject.WritebackFromBackend({mapped, size}, 0);
|
||||
@@ -993,8 +998,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
} else {
|
||||
g_GLESFuncs.glGenBuffers(1, &resource->id);
|
||||
if (resource->id == 0) {
|
||||
MGLOG_E("Failed to generate buffer object.");
|
||||
MGLOG_E("ES glGetError(): %s",
|
||||
MGLOG_E_ONCE("Failed to generate buffer object.");
|
||||
MGLOG_E_ONCE("ES glGetError(): %s",
|
||||
MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||
return resource;
|
||||
}
|
||||
@@ -1224,7 +1229,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
if (id == 0) {
|
||||
MGLOG_E("Global-UBO ring: persistent storage creation failed (%zu bytes); "
|
||||
MGLOG_E_ONCE("Global-UBO ring: persistent storage creation failed (%zu bytes); "
|
||||
"falling back to glBufferSubData uploads.",
|
||||
newSize);
|
||||
g_uboRing.creationFailed = true;
|
||||
@@ -1470,8 +1475,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_clientAttributeBufferIds.fill(0);
|
||||
g_GLESFuncs.glGenVertexArrays(1, &m_backendVAOId);
|
||||
if (m_backendVAOId == 0) {
|
||||
MGLOG_E("Failed to generate vertex array object.");
|
||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||
MGLOG_E_ONCE("Failed to generate vertex array object.");
|
||||
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||
} else {
|
||||
MGLOG_D("Generated vertex array object with ID: %u.", m_backendVAOId);
|
||||
}
|
||||
@@ -1529,13 +1534,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
inline Bool BindAttributeBuffer(const MG_State::GLState::VertexAttribute& attrib) {
|
||||
const auto& bufferObject = attrib.Buffer;
|
||||
if (!bufferObject) {
|
||||
MGLOG_W("Attribute has no bound buffer, skipping.");
|
||||
MGLOG_W_ONCE("Attribute has no bound buffer, skipping.");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* backendResource = BufferImpl::EnsureBufferResource(bufferObject);
|
||||
if (!backendResource || backendResource->id == 0) {
|
||||
MGLOG_E("No backend buffer found for attribute's buffer, cannot bind attribute.");
|
||||
MGLOG_E_ONCE("No backend buffer found for attribute's buffer, cannot bind attribute.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1586,12 +1591,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
inline Bool SyncZeroStrideAttribute(Uint attribIndex, const MG_State::GLState::VertexAttribute& attrib) {
|
||||
const auto& bufferObject = attrib.Buffer;
|
||||
if (!bufferObject) {
|
||||
MGLOG_W("Zero-stride attribute %u has no bound buffer, skipping.", attribIndex);
|
||||
MGLOG_W_ONCE("Zero-stride attribute %u has no bound buffer, skipping.", attribIndex);
|
||||
return false;
|
||||
}
|
||||
auto* backendResource = BufferImpl::EnsureBufferResource(bufferObject);
|
||||
if (!backendResource || backendResource->id == 0) {
|
||||
MGLOG_E("No backend buffer for zero-stride attribute %u, cannot bind it.", attribIndex);
|
||||
MGLOG_E_ONCE("No backend buffer for zero-stride attribute %u, cannot bind it.", attribIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1621,7 +1626,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (!stateVAOObject) {
|
||||
MGLOG_E("State VAO object is null, cannot sync to backend.");
|
||||
MGLOG_E_ONCE("State VAO object is null, cannot sync to backend.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1694,7 +1699,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// because the array stayed enabled with no pointer the failed call could set.
|
||||
// The type test therefore covers the storage, not the spelling.
|
||||
if (attrib.IsLong || attrib.Type == DataType::Float64) {
|
||||
MGLOG_I("DirectGLES: vertex attribute %u is a 64-bit (GL_DOUBLE) array, which this "
|
||||
MGLOG_W_ONCE("DirectGLES: vertex attribute %u is a 64-bit (GL_DOUBLE) array, which this "
|
||||
"backend cannot feed - disabling the array",
|
||||
attribIndex);
|
||||
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
|
||||
@@ -1761,7 +1766,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
if (formatMayBeRefused && g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||
MGLOG_I("DirectGLES: the driver refused the vertex format of attribute %u "
|
||||
MGLOG_W_ONCE("DirectGLES: the driver refused the vertex format of attribute %u "
|
||||
"(size=%d bgra=%d type=%s) - disabling the array so the draw cannot "
|
||||
"fetch through a pointer the driver never accepted",
|
||||
attribIndex, attrib.Size, attrib.IsBgra ? 1 : 0,
|
||||
@@ -1784,7 +1789,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, backendResource->id);
|
||||
indexBufferSynced = true;
|
||||
} else {
|
||||
MGLOG_W("No backend buffer found for index buffer binding, cannot bind index buffer.");
|
||||
MGLOG_W_ONCE("No backend buffer found for index buffer binding, cannot bind index buffer.");
|
||||
}
|
||||
} else {
|
||||
g_GLESFuncs.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
@@ -1842,7 +1847,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (bufferId == 0) {
|
||||
g_GLESFuncs.glGenBuffers(1, &bufferId);
|
||||
if (bufferId == 0) {
|
||||
MGLOG_E("Failed to create client-side vertex attribute upload buffer.");
|
||||
MGLOG_E_ONCE("Failed to create client-side vertex attribute upload buffer.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -1877,8 +1882,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
||||
m_contextGeneration = g_backendContextGeneration;
|
||||
if (m_backendTextureId == 0) {
|
||||
MGLOG_E("Failed to generate texture object.");
|
||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||
MGLOG_E_ONCE("Failed to generate texture object.");
|
||||
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||
} else {
|
||||
MGLOG_D("Generated texture object with ID: %u.", m_backendTextureId);
|
||||
}
|
||||
@@ -1956,8 +1961,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
||||
m_contextGeneration = g_backendContextGeneration;
|
||||
if (m_backendTextureId == 0) {
|
||||
MGLOG_E("Failed to regenerate texture object.");
|
||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||
MGLOG_E_ONCE("Failed to regenerate texture object.");
|
||||
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||
} else {
|
||||
MGLOG_D("Regenerated texture object with ID: %u.", m_backendTextureId);
|
||||
}
|
||||
@@ -2391,7 +2396,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void BackendTextureObject::SyncMipmapsToBackend(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
||||
if (!stateTextureObject) {
|
||||
MGLOG_E("State texture object is null, cannot sync to backend.");
|
||||
MGLOG_E_ONCE("State texture object is null, cannot sync to backend.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2424,7 +2429,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D(" Texture target for syncing is %s",
|
||||
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
|
||||
if (!IsSupportedTextureTarget(targetInternal)) {
|
||||
MGLOG_E(" Texture target %s is not supported, skipping.",
|
||||
MGLOG_E_ONCE(" Texture target %s is not supported, skipping.",
|
||||
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
|
||||
return;
|
||||
}
|
||||
@@ -2576,7 +2581,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static_cast<GLsizei>(uploadSize.z()), 0, glFormat, glType, uploadData);
|
||||
break;
|
||||
default:
|
||||
MGLOG_E("Unhandled texture target %s",
|
||||
MGLOG_E_ONCE("Unhandled texture target %s",
|
||||
MG_Util::ConvertTextureTargetToString(stateTextureObject->GetTarget()).c_str());
|
||||
break;
|
||||
}
|
||||
@@ -2654,7 +2659,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static_cast<GLsizei>(storageSize.z()));
|
||||
break;
|
||||
default:
|
||||
MGLOG_E("Unhandled immutable texture target %s",
|
||||
MGLOG_E_ONCE("Unhandled immutable texture target %s",
|
||||
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
|
||||
break;
|
||||
}
|
||||
@@ -2776,7 +2781,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
MGLOG_E("Unhandled texture target %s",
|
||||
MGLOG_E_ONCE("Unhandled texture target %s",
|
||||
MG_Util::ConvertTextureTargetToString(textureTarget).c_str());
|
||||
}
|
||||
}
|
||||
@@ -2828,7 +2833,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
auto byteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
|
||||
if (byteSize == 0) {
|
||||
MGLOG_W("Mipmap level %d has no data, skipping update.", level);
|
||||
MGLOG_D("Mipmap level %d has no data, skipping update.", level);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2979,7 +2984,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
MGLOG_E("Unhandled texture target %s",
|
||||
MGLOG_E_ONCE("Unhandled texture target %s",
|
||||
MG_Util::ConvertTextureTargetToString(stateTextureObject->GetTarget()).c_str());
|
||||
break;
|
||||
}
|
||||
@@ -3007,7 +3012,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Need to sync texture buffer if not synced yet
|
||||
auto* backendBufferResource = BufferImpl::EnsureBufferResource(buffer);
|
||||
if (!backendBufferResource || backendBufferResource->id == 0) {
|
||||
MGLOG_E("Failed to sync backing buffer for texture buffer with ID: %u",
|
||||
MGLOG_E_ONCE("Failed to sync backing buffer for texture buffer with ID: %u",
|
||||
stateTextureObject->GetExternalIndex());
|
||||
return;
|
||||
}
|
||||
@@ -3026,15 +3031,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// below that without EXT/OES_texture_buffer. Calling it was an unconditional
|
||||
// null dereference. There is no conformant way to refuse the call (it is valid
|
||||
// in the context MobileGL claims), so the texture is left unbacked and the
|
||||
// reason is stated once per respecify at a level that survives the shipped
|
||||
// INFO build - MGLOG_E is compiled out there, which is exactly how this class
|
||||
// of defect stays invisible.
|
||||
// reason is stated once per object, latched by the flag below. It was parked
|
||||
// at MGLOG_I while the level ordering compiled MGLOG_W out of INFO builds;
|
||||
// W is the correct level and now survives there.
|
||||
if (!AreBufferTexturesSupported()) {
|
||||
if (m_bufferTextureUnsupportedReported) {
|
||||
break;
|
||||
}
|
||||
m_bufferTextureUnsupportedReported = true;
|
||||
MGLOG_I("Texture buffer %u cannot be backed: this ES driver has no buffer "
|
||||
MGLOG_W("Texture buffer %u cannot be backed: this ES driver has no buffer "
|
||||
"textures (%s). Every draw sampling it will read zero and every "
|
||||
"shader declaring a samplerBuffer will fail to compile. MobileGL "
|
||||
"still advertises GL_MAX_TEXTURE_BUFFER_SIZE = %d because an "
|
||||
@@ -3062,7 +3067,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
} else if (!CallTexBufferRange(GL_TEXTURE_BUFFER, glInternalFormat, backendId,
|
||||
static_cast<GLintptr>(rangeOffset),
|
||||
static_cast<GLsizeiptr>(rangeSize))) {
|
||||
MGLOG_I("Texture buffer %u names a sub-range but the driver has no "
|
||||
MGLOG_W_ONCE("Texture buffer %u names a sub-range but the driver has no "
|
||||
"glTexBufferRange; binding the whole buffer instead",
|
||||
stateTextureObject->GetExternalIndex());
|
||||
CallTexBuffer(GL_TEXTURE_BUFFER, glInternalFormat, backendId);
|
||||
@@ -3080,7 +3085,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// TextureStorageType is {Mipmap, Buffer}, both handled above, so this is a
|
||||
// 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; "
|
||||
MGLOG_E_ONCE("DirectGLES texture sync: no upload path for storage type %d on texture %u; "
|
||||
"skipping this sync",
|
||||
static_cast<int>(stateTextureObject->GetStorageType()),
|
||||
stateTextureObject->GetExternalIndex());
|
||||
@@ -3115,7 +3120,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
|
||||
if (!stateTextureObject) {
|
||||
MGLOG_E("State texture object is null, cannot sync to backend.");
|
||||
MGLOG_E_ONCE("State texture object is null, cannot sync to backend.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3136,7 +3141,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D(" Texture target for syncing is %s",
|
||||
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
|
||||
if (!IsSupportedTextureTarget(targetInternal)) {
|
||||
MGLOG_E(" Texture target %s is not supported, skipping.",
|
||||
MGLOG_E_ONCE(" Texture target %s is not supported, skipping.",
|
||||
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
|
||||
return;
|
||||
}
|
||||
@@ -3225,7 +3230,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
|
||||
if (!stateTextureObject) {
|
||||
MGLOG_E("State texture object is null, cannot sync to backend.");
|
||||
MGLOG_E_ONCE("State texture object is null, cannot sync to backend.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3245,7 +3250,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D(" Texture target for syncing is %s",
|
||||
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
|
||||
if (!IsSupportedTextureTarget(targetInternal)) {
|
||||
MGLOG_E(" Texture target %s is not supported, skipping.",
|
||||
MGLOG_E_ONCE(" Texture target %s is not supported, skipping.",
|
||||
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
|
||||
return;
|
||||
}
|
||||
@@ -3393,8 +3398,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glGenFramebuffers(1, &m_backendFBOId);
|
||||
m_contextGeneration = g_backendContextGeneration;
|
||||
if (m_backendFBOId == 0) {
|
||||
MGLOG_E("Failed to generate framebuffer object.");
|
||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||
MGLOG_E_ONCE("Failed to generate framebuffer object.");
|
||||
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||
} else {
|
||||
MGLOG_D("Generated framebuffer object with ID: %u.", m_backendFBOId);
|
||||
}
|
||||
@@ -3530,7 +3535,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
backendTextureObject = newTextureSlot;
|
||||
}
|
||||
if (!backendTextureObject) {
|
||||
MGLOG_E("%s: No backend texture found for FBO attachment, cannot bind texture.", __func__);
|
||||
MGLOG_E_ONCE("%s: No backend texture found for FBO attachment, cannot bind texture.", __func__);
|
||||
return false;
|
||||
}
|
||||
backendTextureObject->SyncMipmapsToBackend(textureObject);
|
||||
@@ -3899,7 +3904,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (!stateFBOObject) {
|
||||
MGLOG_E("State FBO object is null, cannot sync to backend.");
|
||||
MGLOG_E_ONCE("State FBO object is null, cannot sync to backend.");
|
||||
return;
|
||||
}
|
||||
MGLOG_D("Syncing FBO with backend ID %u to backend for state ID %u, as %s FBO", m_backendFBOId,
|
||||
@@ -4418,8 +4423,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
m_backendProgramId = g_GLESFuncs.glCreateProgram();
|
||||
if (m_backendProgramId == 0) {
|
||||
MGLOG_E("Failed to create program object in backend.");
|
||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||
MGLOG_E_ONCE("Failed to create program object in backend.");
|
||||
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||
|
||||
} else {
|
||||
MGLOG_D("Created backend program object with ID: %u", m_backendProgramId);
|
||||
@@ -4497,13 +4502,168 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return signature;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The GL internal format bound to an image unit right now. GL_NONE for a unit
|
||||
// outside the frontend's array, which cannot be addressed at all.
|
||||
Uint BoundImageUnitFormat(Int unit) {
|
||||
if (unit < 0 || unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) return 0;
|
||||
return static_cast<Uint>(MG_State::pGLContext->GetImageTextureBinding(unit).Format);
|
||||
}
|
||||
|
||||
// Combines one (unit, format) pair into a running digest. Commutative, so the order
|
||||
// the uniforms are walked in cannot change the answer, and mixed rather than summed
|
||||
// so a unit and a format cannot trade places between two pairs and cancel out.
|
||||
Uint64 MixImageUnitFormat(Uint64 signature, Int unit, Uint format) {
|
||||
Uint64 entry = static_cast<Uint64>(static_cast<Uint32>(unit)) + 0x9e3779b97f4a7c15ull;
|
||||
entry ^= static_cast<Uint64>(format) + 0xbf58476d1ce4e5b9ull + (entry << 6) + (entry >> 2);
|
||||
return signature + entry;
|
||||
}
|
||||
|
||||
// Reflection names an array uniform after its first element ("g_image[0]") at every
|
||||
// location it spans; SPIR-V names the variable once, without the subscript. This is
|
||||
// the name both sides agree on.
|
||||
String ImageUniformBaseName(const String& reflectionName) {
|
||||
if (reflectionName.size() >= 3 && reflectionName.compare(reflectionName.size() - 3, 3, "[0]") == 0) {
|
||||
return reflectionName.substr(0, reflectionName.size() - 3);
|
||||
}
|
||||
return reflectionName;
|
||||
}
|
||||
|
||||
// Whether a glslang layout format is one GLSL ES has in core; the rest reach ES only
|
||||
// through GL_NV_image_formats. Asked of DECLARED formats, which this backend passes
|
||||
// through untouched - the emitted ESSL still has to be legal for the driver.
|
||||
Bool IsCoreEsslLayoutFormat(glslang::TLayoutFormat format) {
|
||||
switch (format) {
|
||||
case glslang::ElfRgba32f:
|
||||
case glslang::ElfRgba16f:
|
||||
case glslang::ElfR32f:
|
||||
case glslang::ElfRgba8:
|
||||
case glslang::ElfRgba8Snorm:
|
||||
case glslang::ElfRgba32i:
|
||||
case glslang::ElfRgba16i:
|
||||
case glslang::ElfRgba8i:
|
||||
case glslang::ElfR32i:
|
||||
case glslang::ElfRgba32ui:
|
||||
case glslang::ElfRgba16ui:
|
||||
case glslang::ElfRgba8ui:
|
||||
case glslang::ElfR32ui:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// What the format bake needs from the frontend, collected in one walk of the uniform
|
||||
// reflection: which image uniforms declared NO format (the only ones a bake may touch -
|
||||
// a declared format is authoritative and stays), what the units they address currently
|
||||
// hold, and whether any format in play - declared or baked - is outside the ES core set.
|
||||
ImageFormatBakeInputs CollectImageFormatBakeInputs(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject) {
|
||||
ImageFormatBakeInputs inputs;
|
||||
const Uint maxUniformLoc = stateProgramObject.GetMaxUniformLocation();
|
||||
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
|
||||
const auto& name = stateProgramObject.GetUniformName(loc);
|
||||
if (name.empty()) continue;
|
||||
if (!IsImageUniformType(stateProgramObject.GetUniformType(loc))) continue;
|
||||
const glslang::TType* type = stateProgramObject.GetUniformTType(loc);
|
||||
if (type == nullptr) continue;
|
||||
if (type->getQualifier().hasFormat()) {
|
||||
// Declared, and therefore left exactly as written - but a non-core spelling
|
||||
// still needs the extension directive to survive the ES compiler.
|
||||
if (!IsCoreEsslLayoutFormat(type->getQualifier().getFormat())) {
|
||||
inputs.needsExtendedImageFormats = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const Int unit = stateProgramObject.GetUniformSamplerOrImageUnitIndex(loc);
|
||||
if (unit < 0 || unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) continue;
|
||||
const Uint boundFormat = BoundImageUnitFormat(unit);
|
||||
|
||||
// Every format-less uniform contributes to the rebuild key, including one whose
|
||||
// unit holds nothing yet: an image bound for the first time AFTER the link has
|
||||
// to move the key, or the program built against "nothing bound" would never be
|
||||
// rebuilt against the real format.
|
||||
inputs.units.push_back(unit);
|
||||
inputs.signature = MixImageUnitFormat(inputs.signature, unit, boundFormat);
|
||||
|
||||
if (boundFormat == 0) continue;
|
||||
if (!MG_Util::ShaderTranspiler::ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(boundFormat)) {
|
||||
// Outside the GLSL ES core set, so the emitted ESSL only compiles with
|
||||
// GL_NV_image_formats. Without the extension there is no legal spelling at
|
||||
// all, and baking one would trade a "no format qualifier" compile error for
|
||||
// an "unsupported format" one - so the image is left format-less. Its unit
|
||||
// stays in the key, so a rebind to a core format still rebuilds and works.
|
||||
if (!g_GLESCapabilities.SupportsExtendedImageFormats) {
|
||||
MGLOG_D("Image uniform '%s' has no declared format and its unit %d holds 0x%x, which GLSL ES "
|
||||
"core cannot spell and this driver has no GL_NV_image_formats for.",
|
||||
name.c_str(), unit, boundFormat);
|
||||
continue;
|
||||
}
|
||||
inputs.needsExtendedImageFormats = true;
|
||||
}
|
||||
const String baseName = ImageUniformBaseName(name);
|
||||
const auto existing = inputs.glFormatByUniformName.find(baseName);
|
||||
if (existing == inputs.glFormatByUniformName.end()) {
|
||||
inputs.glFormatByUniformName.emplace(baseName, boundFormat);
|
||||
} else if (existing->second != boundFormat) {
|
||||
// An ARRAY whose elements were pointed at units holding different formats.
|
||||
// One declaration carries one qualifier, so there is no spelling for it, and
|
||||
// the uniform is left format-less rather than given a format that is wrong
|
||||
// for all but one element. Marked in place with GL_NONE and swept below -
|
||||
// never by erasing here, because the entry is reached again by the array's
|
||||
// remaining elements and a flat hash map must not be mutated structurally
|
||||
// while an iterator into it is live.
|
||||
existing->second = 0;
|
||||
}
|
||||
}
|
||||
for (const auto& entry : inputs.glFormatByUniformName) {
|
||||
if (entry.second == 0) inputs.conflictedNames.push_back(entry.first);
|
||||
}
|
||||
for (const auto& conflicted : inputs.conflictedNames) {
|
||||
inputs.glFormatByUniformName.erase(conflicted);
|
||||
}
|
||||
// Split off the ones SPIRV-Cross will not print. They cannot go through the module -
|
||||
// it throws for them when targeting ESSL, and the stage is lost - so they are spelled
|
||||
// into the emitted text instead. Collected first, erased after, because a flat hash
|
||||
// map must not be restructured while it is being walked.
|
||||
Vector<String> textCompleted;
|
||||
for (const auto& entry : inputs.glFormatByUniformName) {
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(entry.second)) {
|
||||
continue;
|
||||
}
|
||||
String spelling = MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(entry.second);
|
||||
if (spelling.empty()) continue; // no image-format spelling at all; nothing to write
|
||||
inputs.esslFormatQualifierByUniformName.emplace(entry.first, Move(spelling));
|
||||
textCompleted.push_back(entry.first);
|
||||
}
|
||||
for (const auto& name : textCompleted) {
|
||||
inputs.glFormatByUniformName.erase(name);
|
||||
}
|
||||
return inputs;
|
||||
}
|
||||
|
||||
Uint64 BackendProgramObjectImpl::ComputeImageUnitFormatSignature() const {
|
||||
if (m_formatlessImageUnits.empty()) return 0; // all but a handful of programs
|
||||
Uint64 signature = 0;
|
||||
for (const Int unit : m_formatlessImageUnits) {
|
||||
signature = MixImageUnitFormat(signature, unit, BoundImageUnitFormat(unit));
|
||||
}
|
||||
return signature;
|
||||
}
|
||||
|
||||
Bool BackendProgramObjectImpl::ImageUnitFormatsStillMatch() const {
|
||||
if (m_formatlessImageUnits.empty()) return m_imageUnitFormatSignature == 0;
|
||||
return ComputeImageUnitFormatSignature() == m_imageUnitFormatSignature;
|
||||
}
|
||||
|
||||
void BackendProgramObjectImpl::SyncToBackend(
|
||||
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (!stateProgramObject) {
|
||||
MGLOG_E("State program object is null, skipping backend sync.");
|
||||
MGLOG_E_ONCE("State program object is null, skipping backend sync.");
|
||||
return;
|
||||
}
|
||||
// Recorded before either early return below, so Use() can always name the GL
|
||||
@@ -4516,7 +4676,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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 "
|
||||
MGLOG_E_ONCE("Program object is not linked or has no generated SPIR-V, skipping backend sync. State "
|
||||
"program ID: %u",
|
||||
stateProgramObject->GetExternalIndex());
|
||||
return;
|
||||
@@ -4537,6 +4697,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// this build current - the draw path compares the signature and rebuilds on a change.
|
||||
const auto& storageBlockBindingOverrides = stateProgramObject->GetShaderStorageBlockBindingOverrides();
|
||||
m_shaderStorageBlockBindingSignature = ComputeShaderStorageBlockBindingSignature(*stateProgramObject);
|
||||
// The same shape again for image FORMATS: what a format-less image declaration
|
||||
// compiles to depends on live glBindImageTexture state, so the pairs it was built
|
||||
// against are recorded here and compared per draw (ImageUnitFormatsStillMatch).
|
||||
// Taken BEFORE the transpile loop so both the bake and the key see one snapshot.
|
||||
const ImageFormatBakeInputs imageFormatBake = CollectImageFormatBakeInputs(*stateProgramObject);
|
||||
m_formatlessImageUnits = imageFormatBake.units;
|
||||
m_imageUnitFormatSignature = imageFormatBake.signature;
|
||||
for (const auto& conflicted : imageFormatBake.conflictedNames) {
|
||||
MGLOG_D("Image uniform '%s' of program %u declares no format and its elements address units with "
|
||||
"different bound formats; left format-less.",
|
||||
conflicted.c_str(), stateProgramObject->GetExternalIndex());
|
||||
}
|
||||
|
||||
// Detach all existing shaders
|
||||
GLint attachedCount = 0;
|
||||
@@ -4574,6 +4746,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("%s:", src.empty() ? "" : src.c_str());
|
||||
}
|
||||
auto& shaderSpirvs = stateProgramObject->GetGeneratedSpirv();
|
||||
const Bool enableSpirvValidation = stateProgramObject->GetSpirvValidationEnabled();
|
||||
|
||||
// Blocks a transform-feedback capture request names a member of ("StageData" of
|
||||
// "StageData.attrib[0]"). The Adreno ES driver accepts such a request, links, and
|
||||
@@ -4596,7 +4769,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GLuint backendShaderId = g_GLESFuncs.glCreateShader(glShaderType);
|
||||
|
||||
if (backendShaderId == 0) {
|
||||
MGLOG_E("Failed to create backend shader for attachment.");
|
||||
MGLOG_E_ONCE("Failed to create backend shader for attachment.");
|
||||
continue;
|
||||
}
|
||||
String source;
|
||||
@@ -4606,12 +4779,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// ES 3.2 or EXT/OES_texture_buffer on the host. Without it SPIRV-Cross emits
|
||||
// `#extension GL_EXT_texture_buffer : require` and the driver rejects both that
|
||||
// and the isamplerBuffer keyword - the program never links and every draw using it
|
||||
// becomes a silent no-op. Say so here, naming the stage, instead of leaving a
|
||||
// driver info log the shipped INFO build compiles out (MGLOG_E is inactive there).
|
||||
// becomes a silent no-op. Say so here, naming the stage. Deliberately unlatched:
|
||||
// this is bounded by program count, and which stage failed is the whole point.
|
||||
// Gated on the capability so the module walk never runs on a healthy driver.
|
||||
if (!AreBufferTexturesSupported() &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::ModuleDeclaresBufferTextureSampler(spirvCode)) {
|
||||
MGLOG_I("Program %u stage %s samples a buffer texture, which this ES driver "
|
||||
MGLOG_E("Program %u stage %s samples a buffer texture, which this ES driver "
|
||||
"cannot provide (%s). The shader will not compile and the program will "
|
||||
"not link; every draw using it is a no-op.",
|
||||
m_backendProgramId,
|
||||
@@ -4627,7 +4800,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Vector<unsigned int> loweredSpirv;
|
||||
const Vector<unsigned int>* effectiveSpirv = &spirvCode;
|
||||
if (glShaderType == GL_VERTEX_SHADER &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::LowerDrawParametersForEssl(spirvCode, loweredSpirv) &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::LowerDrawParametersForEssl(spirvCode, loweredSpirv, enableSpirvValidation) &&
|
||||
!loweredSpirv.empty()) {
|
||||
effectiveSpirv = &loweredSpirv;
|
||||
}
|
||||
@@ -4637,7 +4810,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Vector<unsigned int> splitArrayInputSpirv;
|
||||
if (glShaderType == GL_VERTEX_SHADER &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::SplitArrayVertexInputsForEssl(
|
||||
*effectiveSpirv, splitArrayInputSpirv) &&
|
||||
*effectiveSpirv, splitArrayInputSpirv, enableSpirvValidation) &&
|
||||
!splitArrayInputSpirv.empty() && splitArrayInputSpirv != *effectiveSpirv) {
|
||||
// Only when the pass ACTUALLY split something. The optimizer hands back a
|
||||
// re-serialised copy either way, and adopting that copy for every vertex
|
||||
@@ -4659,7 +4832,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!xfbCaptureBlockNames.empty() &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(
|
||||
*effectiveSpirv, xfbCaptureBlockNames, stageFlattenedXfbBlockNames,
|
||||
flattenedXfbSpirv) &&
|
||||
flattenedXfbSpirv, enableSpirvValidation) &&
|
||||
!flattenedXfbSpirv.empty() && !stageFlattenedXfbBlockNames.empty()) {
|
||||
effectiveSpirv = &flattenedXfbSpirv;
|
||||
flattenedXfbBlockNames.insert(stageFlattenedXfbBlockNames.begin(),
|
||||
@@ -4675,7 +4848,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// declare the member highp; nothing else about emission changes.
|
||||
Vector<unsigned int> uboPrecisionSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(
|
||||
*effectiveSpirv, uboPrecisionSpirv) &&
|
||||
*effectiveSpirv, uboPrecisionSpirv, enableSpirvValidation) &&
|
||||
!uboPrecisionSpirv.empty()) {
|
||||
effectiveSpirv = &uboPrecisionSpirv;
|
||||
}
|
||||
@@ -4690,7 +4863,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Vector<unsigned int> noperspectiveSpirv;
|
||||
if (!g_GLESCapabilities.SupportsNoperspectiveInterpolation &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::EmulateNoPerspectiveForEssl(
|
||||
*effectiveSpirv, noperspectiveSpirv) &&
|
||||
*effectiveSpirv, noperspectiveSpirv, enableSpirvValidation) &&
|
||||
!noperspectiveSpirv.empty()) {
|
||||
effectiveSpirv = &noperspectiveSpirv;
|
||||
}
|
||||
@@ -4700,7 +4873,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// divides the coordinate of every normalized-coordinate lookup by the texture
|
||||
// size, which is the whole of the difference between the two.
|
||||
Vector<unsigned int> rectLoweredSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(*effectiveSpirv, rectLoweredSpirv) &&
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(*effectiveSpirv, rectLoweredSpirv, enableSpirvValidation) &&
|
||||
!rectLoweredSpirv.empty()) {
|
||||
effectiveSpirv = &rectLoweredSpirv;
|
||||
}
|
||||
@@ -4714,11 +4887,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// coordinate to (u, 0, layer) - before SPIRV-Cross can apply its own.
|
||||
Vector<unsigned int> arrayImageSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::Lower1DArrayImagesForEssl(*effectiveSpirv,
|
||||
arrayImageSpirv) &&
|
||||
arrayImageSpirv, enableSpirvValidation) &&
|
||||
!arrayImageSpirv.empty()) {
|
||||
effectiveSpirv = &arrayImageSpirv;
|
||||
}
|
||||
|
||||
// GLSL ES has no format-less image: `writeonly uniform uimage2D` is legal desktop
|
||||
// GLSL 4.2 and an Adreno ES compile error ("all images have to define layout
|
||||
// format"), which loses the whole program. Give each such image the format the
|
||||
// application bound to its unit - the one GL's format-class rules make correct -
|
||||
// so SPIRV-Cross prints a qualifier. AFTER the 1D-array lowering above, which
|
||||
// also rewrites image types, so this one is looking at the final shapes.
|
||||
//
|
||||
// Gated on the module actually declaring one: the map is empty for every program
|
||||
// whose images all declare formats, and the cheap probe keeps a program that has
|
||||
// an unbound format-less image from paying an optimizer round trip per stage.
|
||||
Vector<unsigned int> imageFormatSpirv;
|
||||
if (!imageFormatBake.glFormatByUniformName.empty() &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::DeclaresFormatlessStorageImage(*effectiveSpirv) &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::BakeImageFormatsForEssl(
|
||||
*effectiveSpirv, imageFormatBake.glFormatByUniformName, imageFormatSpirv,
|
||||
enableSpirvValidation) &&
|
||||
!imageFormatSpirv.empty()) {
|
||||
effectiveSpirv = &imageFormatSpirv;
|
||||
}
|
||||
|
||||
// GLSL ES demands a constant integral expression to index a fragment output
|
||||
// array; SPIR-V does not, so a shader that writes coeff[i] from a loop
|
||||
// reaches SPIRV-Cross intact and comes out as ESSL a strict driver rejects
|
||||
@@ -4730,7 +4923,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Vector<unsigned int> outputIndexSpirv;
|
||||
if (glShaderType == GL_FRAGMENT_SHADER &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(
|
||||
*effectiveSpirv, outputIndexSpirv) &&
|
||||
*effectiveSpirv, outputIndexSpirv, enableSpirvValidation) &&
|
||||
!outputIndexSpirv.empty()) {
|
||||
effectiveSpirv = &outputIndexSpirv;
|
||||
}
|
||||
@@ -4761,14 +4954,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
spvcSession.Compile(&result);
|
||||
|
||||
if (!result) {
|
||||
// MGLOG_I, for the same reason as the compile- and link-failure diagnostics
|
||||
// below: every CI, retrace and release build compiles at
|
||||
// MOBILEGL_LOG_LEVEL_INFO, where MGLOG_E expands to nothing. A stage that
|
||||
// MGLOG_E, unlatched, like the compile- and link-failure diagnostics below:
|
||||
// one line per failing stage is bounded by program count and naming the
|
||||
// stage is the entire diagnostic value. A stage that
|
||||
// never reaches the driver leaves the program short of that stage, so the
|
||||
// link fails with an EMPTY driver info log - the least debuggable failure
|
||||
// MobileGL can produce, and what hid the whole
|
||||
// KHR-GL43.vertex_attrib_binding family behind "the draw captured zeros".
|
||||
MGLOG_I("Shader transpilation to ESSL failed. State program ID: %u, stage: %s, "
|
||||
MGLOG_E("Shader transpilation to ESSL failed. State program ID: %u, stage: %s, "
|
||||
"SPIRV-Cross error: %s",
|
||||
stateProgramObject->GetExternalIndex(),
|
||||
MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
|
||||
@@ -4786,8 +4979,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// because a header concern reads better before the body ones.
|
||||
source = RetargetTextureBufferExtension(std::move(source),
|
||||
g_GLESCapabilities.TextureBufferSupport);
|
||||
// The other header-level rewrite, and next to that one for the same reason. The
|
||||
// formats it covers are both the ones the bake above put into the module and the
|
||||
// ones the application declared itself - either can be outside the thirteen GLSL
|
||||
// ES has in core, and neither reaches the driver without this directive.
|
||||
source = RequestExtendedImageFormats(std::move(source),
|
||||
imageFormatBake.needsExtendedImageFormats &&
|
||||
g_GLESCapabilities.SupportsExtendedImageFormats);
|
||||
|
||||
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
|
||||
// The completion half of the format bake, for the formats SPIRV-Cross throws on
|
||||
// rather than prints (r8ui and the rest of its desktop-only set). Empty for every
|
||||
// program whose format-less images bound a format the module could carry, which
|
||||
// is the normal case - those were baked into the SPIR-V above and this pass finds
|
||||
// their declarations already qualified. AFTER the rebind, so the layout qualifier
|
||||
// it edits is the one that already exists; BEFORE the split and the binding
|
||||
// strip, so both halves of a split image inherit the format.
|
||||
source = BakeImageFormatQualifiers(std::move(source),
|
||||
imageFormatBake.esslFormatQualifierByUniformName);
|
||||
// Wedged between those two on purpose:
|
||||
// * AFTER RebindImageUniformsToFrontendUnits, so the binding it copies onto
|
||||
// both halves of a split image is already the frontend texture unit (and so
|
||||
@@ -4839,14 +5048,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Vector<GLchar> log(static_cast<SizeT>(logLength) + 1, '\0');
|
||||
g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data());
|
||||
log.back() = '\0';
|
||||
// MGLOG_I, deliberately. Every CI, retrace and release build compiles at
|
||||
// MOBILEGL_LOG_LEVEL_INFO, where MGLOG_E and MGLOG_W expand to nothing
|
||||
// (Log.h orders DEBUG < WARN < ERROR < INFO), so this diagnostic used to
|
||||
// exist only in debug builds: the Android retrace artifact carried 294
|
||||
// INFO lines and zero ERROR lines while two generated shaders were being
|
||||
// rejected outright, and the lane could not say why it was rendering an
|
||||
// empty translucent layer. A shader the driver refuses is never noise.
|
||||
MGLOG_I("Shader compilation failed. State program ID: %u, stage: %s, backend shader ID: "
|
||||
// MGLOG_E, unlatched. This was parked at MGLOG_I while the level ordering
|
||||
// compiled E and W out of every INFO build: the Android retrace artifact
|
||||
// carried 294 INFO lines and zero ERROR lines while two generated shaders
|
||||
// were being rejected outright, and the lane could not say why it was
|
||||
// rendering an empty translucent layer. A shader the driver refuses is
|
||||
// never noise, and one line per refused shader is bounded by program count.
|
||||
MGLOG_E("Shader compilation failed. State program ID: %u, stage: %s, backend shader ID: "
|
||||
"%u, driver log: %s",
|
||||
stateProgramObject->GetExternalIndex(),
|
||||
MG_Util::ConvertGLEnumToString(glShaderType).c_str(), backendShaderId,
|
||||
@@ -4930,7 +5138,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// MGLOG_I for the same reason as the compile failure above: a program that
|
||||
// links nothing no-ops every draw that uses it, and that has to be readable
|
||||
// in an INFO-level artifact.
|
||||
MGLOG_I("Program linking failed. State program ID: %u, backend program ID: %u, driver log: %s",
|
||||
MGLOG_E("Program linking failed. State program ID: %u, backend program ID: %u, driver log: %s",
|
||||
stateProgramObject->GetExternalIndex(), m_backendProgramId, log.data());
|
||||
} else {
|
||||
MGLOG_D("Program linked successfully. ID: %u", m_backendProgramId);
|
||||
@@ -5022,7 +5230,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_globalUboBackendBlockSize = static_cast<Int>(blockDataSize);
|
||||
}
|
||||
} else {
|
||||
MGLOG_W("Program %u has frontend global UBO storage, but backend has no %s block.",
|
||||
MGLOG_W_ONCE("Program %u has frontend global UBO storage, but backend has no %s block.",
|
||||
stateProgramObject->GetExternalIndex(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME);
|
||||
}
|
||||
}
|
||||
@@ -5098,13 +5306,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
if (!m_backendProgramUsable) {
|
||||
// MGLOG_I, not MGLOG_W: at MOBILEGL_LOG_LEVEL_INFO - the level the shipped
|
||||
// fordebug builds compile at - only I and F survive, and this is precisely the
|
||||
// line those builds need. Every draw made with this program renders nothing and
|
||||
// raises no GL error, so without it the only symptom is a framebuffer that kept
|
||||
// its clear colour. The early return above keeps it to at most one line per
|
||||
// program state change, not one per draw.
|
||||
MGLOG_I("Backend program for GL program %u is unusable (a shader failed to transpile, "
|
||||
// Every draw made with this program renders nothing and raises no GL error, so
|
||||
// without this line the only symptom is a framebuffer that kept its clear
|
||||
// colour. Latched: the early return above only dedupes CONSECUTIVE binds, so an
|
||||
// app alternating a healthy and a broken program would otherwise log every
|
||||
// single draw. Parked at MGLOG_I until the level ordering was fixed.
|
||||
MGLOG_E_ONCE("Backend program for GL program %u is unusable (a shader failed to transpile, "
|
||||
"compile or link); binding program 0 - draws with it will render nothing",
|
||||
m_frontendProgramId);
|
||||
}
|
||||
@@ -5152,8 +5359,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glGenSamplers(1, &m_backendSamplerId);
|
||||
m_contextGeneration = g_backendContextGeneration;
|
||||
if (m_backendSamplerId == 0) {
|
||||
MGLOG_E("Failed to generate sampler object.");
|
||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||
MGLOG_E_ONCE("Failed to generate sampler object.");
|
||||
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||
} else {
|
||||
MGLOG_D("Generated sampler object with ID: %u.", m_backendSamplerId);
|
||||
}
|
||||
@@ -5185,7 +5392,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (!stateSamplerObject) {
|
||||
MGLOG_E("State sampler object is null, cannot sync to backend.");
|
||||
MGLOG_E_ONCE("State sampler object is null, cannot sync to backend.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5296,8 +5503,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glGenRenderbuffers(1, &m_backendRBOId);
|
||||
m_contextGeneration = g_backendContextGeneration;
|
||||
if (m_backendRBOId == 0) {
|
||||
MGLOG_E("Failed to generate renderbuffer object.");
|
||||
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||
MGLOG_E_ONCE("Failed to generate renderbuffer object.");
|
||||
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5329,7 +5536,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (!stateRBOObject) {
|
||||
MGLOG_E("State RBO object is null, cannot sync to backend.");
|
||||
MGLOG_E_ONCE("State RBO object is null, cannot sync to backend.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1122,6 +1122,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// stale as one built before a relink - while the sampler half, which really is
|
||||
// re-issued per draw, needs nothing of the sort.
|
||||
Uint32 GetSyncedImageUnitVersion() const { return m_syncedImageUnitVersion; }
|
||||
// Whether the (unit, bound format) pairs this program's FORMAT-LESS image uniforms
|
||||
// resolve to are still the ones its ESSL was generated against.
|
||||
//
|
||||
// A fourth condition of the same family as the three above, and the only one that
|
||||
// reads live state rather than a program-side counter, because that is where the
|
||||
// dependency actually is. GLSL ES requires a format layout qualifier on every image
|
||||
// where desktop GLSL lets a writeonly declaration omit one, and the only correct
|
||||
// qualifier is whatever glBindImageTexture named - so a declaration with no format
|
||||
// is compiled against the BINDING, and a rebind to a different format makes the
|
||||
// built program wrong. Keyed on the units the program's own images address (cached
|
||||
// at sync, since a unit can only move by glUniform1i, which bumps the image-unit
|
||||
// version above and forces a re-sync anyway), so the cost on a program with no
|
||||
// format-less image - which is all but a handful - is one empty-vector test.
|
||||
//
|
||||
// Deliberately NOT reached from glBindImageTexture: that entry point must never
|
||||
// trigger a build (same constraint as glShaderStorageBlockBinding). It moves the
|
||||
// state and this comparison notices at the next Prepare, which is also what makes
|
||||
// an image first bound AFTER link work.
|
||||
Bool ImageUnitFormatsStillMatch() const;
|
||||
// The value ImageUnitFormatsStillMatch() compares against, recomputed from live
|
||||
// image-unit state. 0 when the program has no format-less image uniform.
|
||||
Uint64 ComputeImageUnitFormatSignature() const;
|
||||
|
||||
private:
|
||||
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||
@@ -1155,6 +1177,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
|
||||
Uint32 m_syncedLinkVersion = ~0u;
|
||||
Uint32 m_syncedImageUnitVersion = ~0u;
|
||||
// Image units addressed by the program's FORMAT-LESS image uniforms, and the digest
|
||||
// of the (unit, format) pairs the generated ESSL baked. Empty/0 for every program
|
||||
// that declares a format on all of its images, which is the overwhelming majority -
|
||||
// and what keeps the per-draw comparison free for them.
|
||||
Vector<Int> m_formatlessImageUnits;
|
||||
Uint64 m_imageUnitFormatSignature = 0;
|
||||
SamplerPassMemo m_samplerPassMemo;
|
||||
};
|
||||
|
||||
@@ -1198,6 +1226,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// already has costs nothing. 0 when nothing was ever rebound.
|
||||
Uint64 ComputeShaderStorageBlockBindingSignature(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||
|
||||
// Everything the image-format bake needs from one walk of a program's uniform
|
||||
// reflection. GLSL ES requires a format layout qualifier on every image uniform;
|
||||
// desktop GLSL lets a writeonly (or readonly) declaration omit one, and the only
|
||||
// format that is CORRECT to substitute is whatever glBindImageTexture named for the
|
||||
// unit that uniform addresses - so the transpile bakes it in and the build is keyed
|
||||
// on it.
|
||||
struct ImageFormatBakeInputs {
|
||||
// Uniform name (SPIR-V spelling, i.e. an array named once, unsubscripted) to the GL
|
||||
// internal format to bake. Holds only uniforms that DECLARED no format; a declared
|
||||
// one is authoritative and is never overridden.
|
||||
UnorderedMap<String, Uint> glFormatByUniformName;
|
||||
// The same uniforms whose format SPIRV-Cross REFUSES to print for ESSL (it throws on
|
||||
// its desktop-only set, which loses the stage), paired with the ESSL spelling to
|
||||
// write into the emitted declaration instead. Disjoint from the map above by
|
||||
// construction: a format is baked into the module or completed in the text, never
|
||||
// both. r8ui - the stencil half of the packed_depth_stencil case - lands here.
|
||||
UnorderedMap<String, String> esslFormatQualifierByUniformName;
|
||||
// Units those uniforms address, kept so the draw path can re-read their formats
|
||||
// without walking the reflection again.
|
||||
Vector<Int> units;
|
||||
// Digest of the (unit, format) pairs above. 0 when the program has no format-less
|
||||
// image uniform, which is all but a handful.
|
||||
Uint64 signature = 0;
|
||||
// Array uniforms whose elements resolved to units holding DIFFERENT formats: one
|
||||
// declaration carries one qualifier, so there is nothing correct to bake and they
|
||||
// are dropped from the map above. Kept for diagnostics.
|
||||
Vector<String> conflictedNames;
|
||||
// Some format in play - declared or baked - is outside the GLSL ES core image
|
||||
// format set, so the emitted ESSL needs the GL_NV_image_formats directive.
|
||||
Bool needsExtendedImageFormats = false;
|
||||
};
|
||||
ImageFormatBakeInputs CollectImageFormatBakeInputs(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace SamplerImpl {
|
||||
|
||||
@@ -252,7 +252,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
g_resolvedTier =
|
||||
ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode,
|
||||
&g_tierResolution);
|
||||
MGLOG_I("DirectGLES multi-draw: %s", g_tierResolution.c_str());
|
||||
MGLOG_D("DirectGLES multi-draw: %s", g_tierResolution.c_str());
|
||||
}
|
||||
|
||||
// Which tiers have already announced themselves, one bit per GLESMultiDrawMode.
|
||||
@@ -267,7 +267,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
const Uint32 bit = 1u << static_cast<Uint32>(tier);
|
||||
if (g_announcedTiers & bit) return;
|
||||
g_announcedTiers |= bit;
|
||||
MGLOG_I("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
|
||||
MGLOG_D("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
|
||||
}
|
||||
|
||||
// The tier this particular batch can actually take. A tier is demoted here when
|
||||
@@ -490,7 +490,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i],
|
||||
subDrawCount, indexSize);
|
||||
if (!source) {
|
||||
MGLOG_E("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
|
||||
"buffer; skipping the batch",
|
||||
i);
|
||||
return false;
|
||||
@@ -596,7 +596,7 @@ void main() {
|
||||
|
||||
const GLuint shader = g_GLESFuncs.glCreateShader(GL_COMPUTE_SHADER);
|
||||
if (shader == 0) {
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
|
||||
return false;
|
||||
}
|
||||
const char* source = kFlattenComputeSource;
|
||||
@@ -607,14 +607,14 @@ void main() {
|
||||
if (status != GL_TRUE) {
|
||||
char log[1024] = {};
|
||||
g_GLESFuncs.glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
|
||||
g_GLESFuncs.glDeleteShader(shader);
|
||||
return false;
|
||||
}
|
||||
|
||||
const GLuint program = g_GLESFuncs.glCreateProgram();
|
||||
if (program == 0) {
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateProgram failed");
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateProgram failed");
|
||||
g_GLESFuncs.glDeleteShader(shader);
|
||||
return false;
|
||||
}
|
||||
@@ -625,7 +625,7 @@ void main() {
|
||||
if (status != GL_TRUE) {
|
||||
char log[1024] = {};
|
||||
g_GLESFuncs.glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
|
||||
g_GLESFuncs.glDeleteProgram(program);
|
||||
return false;
|
||||
}
|
||||
@@ -635,7 +635,7 @@ void main() {
|
||||
g_uDrawCount = g_GLESFuncs.glGetUniformLocation(program, "uDrawCount");
|
||||
g_uTotalIndices = g_GLESFuncs.glGetUniformLocation(program, "uTotalIndices");
|
||||
g_computeProgramFailed = false;
|
||||
MGLOG_I("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
|
||||
MGLOG_D("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -920,7 +920,7 @@ void main() {
|
||||
feedBaseVertex);
|
||||
}
|
||||
if (!drawn) {
|
||||
MGLOG_E("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
|
||||
"the batch was dropped",
|
||||
drawcount, mode, type);
|
||||
}
|
||||
|
||||
@@ -535,6 +535,92 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
String RequestExtendedImageFormats(String glslCode, Bool needed) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// GLSL ES core has thirteen image formats; GL has forty. SPIRV-Cross prints whatever
|
||||
// format the OpTypeImage carries and asks for no extension for it, so an r8ui or
|
||||
// rg16f image - declared as such, or baked from the bound one - reaches the driver as
|
||||
// a format its core language does not know. GL_NV_image_formats is the only thing
|
||||
// that adds them, and it has to be requested by name.
|
||||
//
|
||||
// The caller decides `needed`: it knows which formats are in play (from the uniform
|
||||
// reflection and the image-unit bindings) and whether the driver advertises the
|
||||
// extension at all - `#extension` on an unadvertised name is itself a hard error, so
|
||||
// this must never be emitted speculatively.
|
||||
static constexpr const char* kDirective = "#extension GL_NV_image_formats : require\n";
|
||||
static constexpr const char* kExtName = "GL_NV_image_formats";
|
||||
if (!needed || glslCode.find(kExtName) != String::npos) {
|
||||
return glslCode;
|
||||
}
|
||||
// After the #version line, which must stay first. Everything else about the header is
|
||||
// order-insensitive, and ForceSupporterOutput's scan for the LAST #extension
|
||||
// directive still finds whichever one that is.
|
||||
const SizeT versionPos = glslCode.find("#version");
|
||||
if (versionPos == String::npos) {
|
||||
return kDirective + glslCode;
|
||||
}
|
||||
const SizeT lineEnd = glslCode.find('\n', versionPos);
|
||||
if (lineEnd == String::npos) {
|
||||
return glslCode + "\n" + kDirective;
|
||||
}
|
||||
glslCode.insert(lineEnd + 1, kDirective);
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
String BakeImageFormatQualifiers(String glslCode,
|
||||
const UnorderedMap<String, String>& esslFormatByUniformName) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (esslFormatByUniformName.empty() || glslCode.find("image") == String::npos) {
|
||||
return glslCode;
|
||||
}
|
||||
// Same declaration shape RebindImageUniformsToFrontendUnits matches, and for the same
|
||||
// reason: one line, one image uniform, the name in group 3.
|
||||
static const std::regex imageDeclRegex(
|
||||
R"((layout\s*\(([^)]*)\)\s*)?uniform\s+(?:(?:readonly|writeonly|coherent|volatile|restrict|highp|mediump|lowp)\s+)*[iu]?image[A-Za-z0-9]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*(\[[^\]]*\])?\s*;)");
|
||||
// Every image format spelling GLSL has, so a declaration that already carries one is
|
||||
// recognised whatever it says - the caller's map is consulted only for declarations
|
||||
// with NO format, never to override a written one.
|
||||
static const std::regex existingFormatRegex(
|
||||
R"(\b(rgba32f|rgba16f|rg32f|rg16f|r11f_g11f_b10f|r32f|r16f|rgba16|rgb10_a2|rg16|rg8|r16|r8|rgba16_snorm|rgba8_snorm|rg16_snorm|rg8_snorm|r16_snorm|r8_snorm|rgba32i|rgba16i|rgba8i|rg32i|rg16i|rg8i|r32i|r16i|r8i|rgba32ui|rgba16ui|rgba8ui|rgb10_a2ui|rg32ui|rg16ui|rg8ui|r32ui|r16ui|r8ui)\b)");
|
||||
|
||||
String result;
|
||||
result.reserve(glslCode.size());
|
||||
SizeT lineStart = 0;
|
||||
while (lineStart <= glslCode.size()) {
|
||||
const SizeT lineEnd = glslCode.find('\n', lineStart);
|
||||
const Bool lastLine = lineEnd == String::npos;
|
||||
String line = glslCode.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart);
|
||||
|
||||
std::smatch match;
|
||||
if (std::regex_search(line, match, imageDeclRegex)) {
|
||||
const String name = match[3].str();
|
||||
const auto formatIt = esslFormatByUniformName.find(name);
|
||||
const String layoutContents = match[2].matched ? match[2].str() : String();
|
||||
if (formatIt != esslFormatByUniformName.end() && !formatIt->second.empty() &&
|
||||
!std::regex_search(layoutContents, existingFormatRegex)) {
|
||||
if (match[1].matched) {
|
||||
const SizeT layoutOpen = line.find('(', match.position(1));
|
||||
line.insert(layoutOpen + 1, formatIt->second + ", ");
|
||||
} else {
|
||||
line.insert(match.position(0), "layout(" + formatIt->second + ") ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result += line;
|
||||
if (lastLine) {
|
||||
break;
|
||||
}
|
||||
result += '\n';
|
||||
lineStart = lineEnd + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
String RemoveLayoutBinding(const String& glslCode) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
@@ -1090,7 +1176,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
for (GLenum err = g_GLESFuncs.glGetError(); err != GL_NO_ERROR; err = g_GLESFuncs.glGetError()) {
|
||||
MGLOG_E("-> GLES Error: %s", MG_Util::ConvertGLEnumToString(err).c_str());
|
||||
MGLOG_D("-> GLES Error: %s", MG_Util::ConvertGLEnumToString(err).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1510,88 +1596,71 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return (rowBytes + align - 1) / align * align;
|
||||
}
|
||||
|
||||
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
|
||||
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
|
||||
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
|
||||
// 4 components x GetReadbackComponentSize(wideType) bytes each.
|
||||
// Walks the client-side destination the PACK parameters describe and hands each row to
|
||||
// `fillRow(slice, row, dstRow)`, which writes width * dstPixelBytes bytes of finished client
|
||||
// texels. Shared by the converting and the raw-word stores so both address the destination -
|
||||
// and feed the bound pixel-pack buffer - identically.
|
||||
// applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage
|
||||
// of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4).
|
||||
// Per the GL addressing rules, slice k row j lands at
|
||||
// SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes
|
||||
// + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride.
|
||||
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
||||
void* pixels, Bool applyPackImageParams) {
|
||||
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
|
||||
if (dstPixelBytes == 0) {
|
||||
return false;
|
||||
}
|
||||
PackedReadbackLayout packedLayout{};
|
||||
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
|
||||
const SizeT dstComponentSize = GetReadbackComponentSize(type);
|
||||
template <typename FillRow>
|
||||
static Bool StoreClientRows(SizeT dstPixelBytes, SizeT swapGroupSize, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, void* pixels, Bool applyPackImageParams, FillRow&& fillRow) {
|
||||
const auto& pixelPackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
|
||||
const auto& pixelPackBufferObject =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
|
||||
// rows are written so skip regions of the destination stay untouched.
|
||||
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
||||
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
||||
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
|
||||
const SizeT imageRows =
|
||||
applyPackImageParams && packParams.ImageHeight > 0
|
||||
? static_cast<SizeT>(packParams.ImageHeight)
|
||||
: static_cast<SizeT>(sliceHeight);
|
||||
const SizeT dstImageStride = imageRows * dstRowStride;
|
||||
const SizeT skipImages =
|
||||
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
|
||||
const SizeT dstSkipOffset = skipImages * dstImageStride +
|
||||
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
|
||||
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
|
||||
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
|
||||
|
||||
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
|
||||
// rows are written so skip regions of the destination stay untouched.
|
||||
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
||||
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
||||
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
|
||||
const SizeT imageRows =
|
||||
applyPackImageParams && packParams.ImageHeight > 0
|
||||
? static_cast<SizeT>(packParams.ImageHeight)
|
||||
: static_cast<SizeT>(sliceHeight);
|
||||
const SizeT dstImageStride = imageRows * dstRowStride;
|
||||
const SizeT skipImages =
|
||||
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
|
||||
const SizeT dstSkipOffset = skipImages * dstImageStride +
|
||||
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
|
||||
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
|
||||
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
|
||||
|
||||
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
|
||||
if (pixelPackBufferObject) {
|
||||
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
|
||||
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
|
||||
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
|
||||
if (requiredSize > pixelPackBufferObject->GetSize()) {
|
||||
MGLOG_E("Readback conversion: pixel pack buffer is too small");
|
||||
return true;
|
||||
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
|
||||
if (pixelPackBufferObject) {
|
||||
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
|
||||
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
|
||||
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
|
||||
if (requiredSize > pixelPackBufferObject->GetSize()) {
|
||||
MGLOG_E_ONCE("Readback conversion: pixel pack buffer is too small");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SizeT srcComponentSize = GetReadbackComponentSize(wideType);
|
||||
const SizeT srcPixelBytes = 4 * srcComponentSize;
|
||||
Vector<Uint8> convertedRow(dstRowBytes);
|
||||
Vector<Uint8> convertedRow(dstRowBytes);
|
||||
|
||||
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
|
||||
for (GLsizei row = 0; row < sliceHeight; ++row) {
|
||||
const SizeT flatRow = static_cast<SizeT>(slice) * static_cast<SizeT>(sliceHeight) +
|
||||
static_cast<SizeT>(row);
|
||||
const Uint8* srcRow = wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
|
||||
ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast<SizeT>(width), wideType,
|
||||
mapping, type);
|
||||
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
|
||||
for (GLsizei row = 0; row < sliceHeight; ++row) {
|
||||
fillRow(slice, row, convertedRow.data());
|
||||
|
||||
if (packParams.SwapBytes) {
|
||||
const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize;
|
||||
if (groupSize > 1) {
|
||||
for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) {
|
||||
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize);
|
||||
if (packParams.SwapBytes && swapGroupSize > 1) {
|
||||
for (SizeT offset = 0; offset + swapGroupSize <= dstRowBytes; offset += swapGroupSize) {
|
||||
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + swapGroupSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
|
||||
static_cast<SizeT>(row) * dstRowStride;
|
||||
if (pixelPackBufferObject) {
|
||||
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
|
||||
pboBaseOffset + dstOffset);
|
||||
} else {
|
||||
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
|
||||
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
|
||||
static_cast<SizeT>(row) * dstRowStride;
|
||||
if (pixelPackBufferObject) {
|
||||
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
|
||||
pboBaseOffset + dstOffset);
|
||||
} else {
|
||||
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pixelPackBufferObject) {
|
||||
// WritebackFromBackend bumps change serials with no backend op; re-open
|
||||
// the buffer draw-clean memos (once for the whole row loop).
|
||||
@@ -1599,5 +1668,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
|
||||
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
|
||||
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
|
||||
// 4 components x GetReadbackComponentSize(wideType) bytes each.
|
||||
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
||||
void* pixels, Bool applyPackImageParams) {
|
||||
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
|
||||
if (dstPixelBytes == 0) {
|
||||
return false;
|
||||
}
|
||||
PackedReadbackLayout packedLayout{};
|
||||
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
|
||||
const SizeT swapGroupSize = isPackedType ? packedLayout.byteSize : GetReadbackComponentSize(type);
|
||||
const SizeT srcPixelBytes = 4 * GetReadbackComponentSize(wideType);
|
||||
|
||||
return StoreClientRows(dstPixelBytes, swapGroupSize, width, sliceHeight, sliceCount, pixels,
|
||||
applyPackImageParams,
|
||||
[&](GLsizei slice, GLsizei row, Uint8* dstRow) {
|
||||
const SizeT flatRow = static_cast<SizeT>(slice) *
|
||||
static_cast<SizeT>(sliceHeight) +
|
||||
static_cast<SizeT>(row);
|
||||
const Uint8* srcRow =
|
||||
wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
|
||||
ConvertWideReadbackRow(srcRow, dstRow, static_cast<SizeT>(width), wideType,
|
||||
mapping, type);
|
||||
});
|
||||
}
|
||||
|
||||
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
|
||||
GLenum type, void* pixels, Bool applyPackImageParams) {
|
||||
PackedReadbackLayout packedLayout{};
|
||||
if (!GetPackedReadbackLayout(type, packedLayout) || packedLayout.byteSize != 4) {
|
||||
return false;
|
||||
}
|
||||
const SizeT srcRowBytes = static_cast<SizeT>(width) * 4;
|
||||
|
||||
return StoreClientRows(4, packedLayout.byteSize, width, sliceHeight, sliceCount, pixels,
|
||||
applyPackImageParams,
|
||||
[&](GLsizei slice, GLsizei row, Uint8* dstRow) {
|
||||
const SizeT flatRow = static_cast<SizeT>(slice) *
|
||||
static_cast<SizeT>(sliceHeight) +
|
||||
static_cast<SizeT>(row);
|
||||
Memcpy(dstRow, srcWords + flatRow * srcRowBytes, srcRowBytes);
|
||||
});
|
||||
}
|
||||
} // namespace ReadbackImpl
|
||||
} // namespace MobileGL::MG_Backend::DirectGLES
|
||||
|
||||
@@ -115,6 +115,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
||||
void* pixels, Bool applyPackImageParams);
|
||||
|
||||
// Stores packed 32-bit source words verbatim, with the same destination addressing, PACK
|
||||
// parameters and pixel-pack-buffer handling as StoreWideRowsToClient. For the sources whose
|
||||
// storage word already IS the client word (MG_Util::IsRawPackedPixelTransfer): routing those
|
||||
// through the wide float intermediate re-encodes them, and the RGB9_E5 encoder canonicalizes
|
||||
// the shared exponent, so glGetTexImage would answer with different bits than were stored.
|
||||
// `srcWords` holds sliceHeight * sliceCount tightly stacked rows of `width` 32-bit words.
|
||||
// False when `type` is not a 4-byte packed type.
|
||||
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
|
||||
GLenum type, void* pixels, Bool applyPackImageParams);
|
||||
} // namespace ReadbackImpl
|
||||
|
||||
namespace PrgramImpl {
|
||||
@@ -137,6 +147,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// ES 3.2 needs no directive at all and an EXT driver already has the right one.
|
||||
String RetargetTextureBufferExtension(String glslCode,
|
||||
MG_External::GLESCapabilities::TextureBufferTier tier);
|
||||
// Adds `#extension GL_NV_image_formats : require` when the shader carries an image
|
||||
// format qualifier GLSL ES has no core spelling for. SPIRV-Cross prints the format and
|
||||
// asks for nothing, so the request has to be made here. `needed` is the caller's answer,
|
||||
// because only it knows which formats are in play AND whether the driver advertises the
|
||||
// extension - requesting an unadvertised extension is itself a compile error, so this is
|
||||
// never emitted speculatively. A no-op when not needed or already present.
|
||||
String RequestExtendedImageFormats(String glslCode, Bool needed);
|
||||
// Writes a format layout qualifier into the image declarations named in
|
||||
// `esslFormatByUniformName` that still have none. The completion half of the image-format
|
||||
// bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
|
||||
// format in, but SPIRV-Cross throws rather than printing the formats it calls
|
||||
// desktop-only when it targets ESSL - r8ui among them, which is what the stencil half of
|
||||
// KHR-GL4x.packed_depth_stencil.stencil_texturing binds - and a throw loses the whole
|
||||
// stage. So those formats stay out of the module and are spelled here instead, on the
|
||||
// emitted text, where nothing can refuse them.
|
||||
//
|
||||
// Declarations that already carry a format are left exactly as they are, whoever wrote
|
||||
// it. Must run before RemoveLayoutBinding, which is where an image's layout qualifier
|
||||
// stops being safe to edit by hand.
|
||||
String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName);
|
||||
String RemoveLayoutBinding(const String& glslCode);
|
||||
// Prefix of the writeonly half a read+write image uniform is split into (see
|
||||
// SplitReadWriteImageUniforms); the suffix is the image's own name.
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "MG_Backend/BackendObject.h"
|
||||
#include "DirectVulkan.h"
|
||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include "MG_State/GLState/TextureState/TextureState.h"
|
||||
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
|
||||
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
|
||||
@@ -383,6 +384,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MG_State::pGLContext) {
|
||||
MG_State::pGLContext->InvalidateCompileEnv();
|
||||
}
|
||||
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
|
||||
MutableFormatCapabilities());
|
||||
PrintFormatCapabilities(GetFormatCapabilities());
|
||||
@@ -497,20 +501,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.ExtraVendor = Nullopt,
|
||||
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
|
||||
.TargetGLSLVersion = {4, 6, 0},
|
||||
// Baseline advertisement (no shader subgroup, no timer queries); a
|
||||
// live backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false),
|
||||
// Baseline advertisement (no runtime-gated capabilities); a live
|
||||
// backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||
.IsCompatibilityProfile = false},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
||||
return rendererInfo;
|
||||
}
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported) {
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||
Vector<GLExtension> extensions = {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_multi_draw_indirect,
|
||||
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_draw_indirect,
|
||||
E_GL_ARB_multi_draw_indirect,
|
||||
E_GL_ARB_indirect_parameters, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
||||
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, E_GL_ARB_texture_multisample,
|
||||
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_shader_draw_parameters,
|
||||
@@ -530,6 +536,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
E_GL_ARB_get_program_binary};
|
||||
// Vulkan's drawIndirectFirstInstance feature is optional. Direct base-instance calls work
|
||||
// without it, but ARB_base_instance also promises non-zero firstInstance in GPU indirect
|
||||
// commands; the renderer supplies true only when that word is legal and gl_InstanceID can
|
||||
// be rebased to OpenGL's zero-based semantics.
|
||||
if (nonZeroIndirectBaseInstanceSupported) {
|
||||
extensions.push_back(E_GL_ARB_base_instance);
|
||||
}
|
||||
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||
}
|
||||
@@ -678,6 +691,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_vulkanCaps = capabilities;
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MG_State::pGLContext) {
|
||||
MG_State::pGLContext->InvalidateCompileEnv();
|
||||
}
|
||||
MutableFormatCapabilities().Clear();
|
||||
}
|
||||
|
||||
@@ -690,7 +706,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// the whole list keeps re-runs idempotent.
|
||||
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported());
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
||||
|
||||
@@ -62,8 +62,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// POST screen shows.
|
||||
|
||||
// Static identity of the Magma renderer (renderer/backend names, target GL/GLSL
|
||||
// versions, ExtraVendor) with the baseline extension advertisement (no shader
|
||||
// subgroup, no timer queries). A live backend copies this in its constructor and
|
||||
// versions, ExtraVendor) with the baseline extension advertisement (no runtime-gated
|
||||
// capabilities). A live backend copies this in its constructor and
|
||||
// reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities
|
||||
// exist; callers that need the advertised list for a known capability set must
|
||||
// use BuildAdvertisedExtensions instead.
|
||||
@@ -74,7 +74,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
|
||||
// the detected device support (passing an already-gated value is harmless).
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported);
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported);
|
||||
|
||||
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
||||
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
||||
|
||||
@@ -269,14 +269,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
drawBuffer->SyncPersistentMappedRange();
|
||||
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
||||
if (drawBuffer->MappedData() == nullptr || commandOffset + requiredBytes > drawBuffer->GetSize()) {
|
||||
MGLOG_E("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
|
||||
MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
|
||||
return nullptr;
|
||||
}
|
||||
return drawBuffer->MappedData() + commandOffset;
|
||||
}
|
||||
|
||||
if (!indirect) {
|
||||
MGLOG_E("%s skipped: indirect pointer is null", label);
|
||||
MGLOG_E_ONCE("%s skipped: indirect pointer is null", label);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -398,7 +398,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
stride = sizeof(DrawArraysIndirectCommand);
|
||||
}
|
||||
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
|
||||
MGLOG_E("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu",
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu",
|
||||
stride, sizeof(DrawArraysIndirectCommand));
|
||||
return;
|
||||
}
|
||||
@@ -446,20 +446,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
stride = sizeof(DrawArraysIndirectCommand);
|
||||
}
|
||||
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
|
||||
stride, sizeof(DrawArraysIndirectCommand));
|
||||
return;
|
||||
}
|
||||
|
||||
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||
if (!parameterBuffer || drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
|
||||
return;
|
||||
}
|
||||
|
||||
parameterBuffer->SyncPersistentMappedRange();
|
||||
if (parameterBuffer->MappedData() == nullptr) {
|
||||
MGLOG_E("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read parameter buffer");
|
||||
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read parameter buffer");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -513,7 +513,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
if (indexSize == 0) {
|
||||
MGLOG_E("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
|
||||
MGLOG_E_ONCE("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1009,7 +1009,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// shift - the hardware divide was the hottest instruction of this loop.
|
||||
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||
if (indexSize == 0) {
|
||||
MGLOG_E("MultiDrawElements skipped: unsupported index type 0x%x", type);
|
||||
MGLOG_E_ONCE("MultiDrawElements skipped: unsupported index type 0x%x", type);
|
||||
return;
|
||||
}
|
||||
const Uint32 indexSizeShift = static_cast<Uint32>(std::countr_zero(indexSize));
|
||||
|
||||
@@ -205,7 +205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// commands away. The device is gone on that path anyway - stay silent-safe
|
||||
// rather than trade a lost device for a barrier into a closed buffer.
|
||||
if (frame.hasCommandBufferRecorded) {
|
||||
MGLOG_E("TransitionToPresent: command buffer already closed; skipping the present barrier");
|
||||
MGLOG_E_ONCE("TransitionToPresent: command buffer already closed; skipping the present barrier");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -206,6 +206,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
||||
@@ -259,7 +260,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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 "
|
||||
// Unlatched, like the CreatePipeline report it accompanies: a pipeline MobileGL
|
||||
// assembled and the driver refused is a broken invariant, not an expected failure,
|
||||
// so it stays loud for as long as it is reachable. Raised from MGLOG_I once the
|
||||
// Log.h ordering fix made MGLOG_E live in INFO builds.
|
||||
MGLOG_E("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));
|
||||
@@ -402,8 +407,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
tessellation.patchControlPoints = payload.patchControlPoints;
|
||||
|
||||
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||
vpci.viewportCount = 1;
|
||||
vpci.scissorCount = 1;
|
||||
// Both counts move together: GL has one scissor rectangle per viewport, and Vulkan
|
||||
// requires viewportCount == scissorCount whenever both are dynamic
|
||||
// (VUID-VkPipelineViewportStateCreateInfo-scissorCount-04136). The caller has already
|
||||
// clamped this to the device's multiViewport capability.
|
||||
vpci.viewportCount = std::max<Uint32>(payload.viewportCount, 1u);
|
||||
vpci.scissorCount = vpci.viewportCount;
|
||||
|
||||
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||
raster.polygonMode = payload.polygonMode;
|
||||
@@ -471,9 +480,56 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
blend.attachmentCount = payload.colorAttachmentCount;
|
||||
blend.pAttachments = colorAttachments.empty() ? nullptr : colorAttachments.data();
|
||||
|
||||
// A GL program may have a tessellation EVALUATION stage and no CONTROL stage: GL 4.6 core
|
||||
// 11.2.2 gives it a fixed-function pass-through instead. Vulkan has no such stage, and
|
||||
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 requires both tessellation stages or
|
||||
// neither - so the renderer synthesizes the pass-through GL describes and hands it in
|
||||
// here (see ProgramFactory::GetOrCreatePassthroughTessControlStage).
|
||||
//
|
||||
// The refusal below is what keeps the half-tessellated shape away from the driver when
|
||||
// there is no synthesized stage to add - because Mali does not reject it, it dereferences
|
||||
// null INSIDE vkCreateGraphicsPipelines and takes the process down (SIGSEGV, fault addr
|
||||
// 0x34, on Mali-G715/r54p2 and Mali-G925/r49p1 alike; Adreno and lavapipe merely render
|
||||
// wrong). Returning VK_NULL_HANDLE routes this through the same path a driver rejection
|
||||
// takes: the draw is skipped, nothing is memoised, and the process survives.
|
||||
const Vector<VkPipelineShaderStageCreateInfo>* effectiveStages = payload.stages;
|
||||
Vector<VkPipelineShaderStageCreateInfo> stagesWithPassthrough;
|
||||
if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE) {
|
||||
stagesWithPassthrough = *payload.stages;
|
||||
stagesWithPassthrough.push_back(payload.passthroughTessControlStage);
|
||||
effectiveStages = &stagesWithPassthrough;
|
||||
}
|
||||
{
|
||||
VkShaderStageFlags stagesPresent = 0;
|
||||
for (const auto& stageInfo : *effectiveStages) {
|
||||
stagesPresent |= stageInfo.stage;
|
||||
}
|
||||
const Bool hasTessControl = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT) != 0;
|
||||
const Bool hasTessEval = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT) != 0;
|
||||
if (hasTessControl != hasTessEval) {
|
||||
// Latched, and the latch is the point: a failed creation is deliberately never
|
||||
// memoised (see GetOrCreatePipeline), so a program in this state re-enters here
|
||||
// once per draw, every frame - and a refusal diagnostic that repeats per draw is
|
||||
// noise, not a diagnostic. One line names the program; the draws it explains are
|
||||
// all the same draw.
|
||||
static Bool s_warnedHalfTessellatedPipeline = false;
|
||||
if (!s_warnedHalfTessellatedPipeline) {
|
||||
s_warnedHalfTessellatedPipeline = true;
|
||||
MGLOG_E_ONCE("PipelineFactory::CreatePipeline: refusing a pipeline with %s tessellation stage and "
|
||||
"no %s stage (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). programHash=0x%llx "
|
||||
"patchControlPoints=%u. Its draws are skipped; logged once.",
|
||||
hasTessEval ? "an evaluation" : "a control",
|
||||
hasTessEval ? "control" : "evaluation",
|
||||
static_cast<unsigned long long>(payload.programHash),
|
||||
payload.patchControlPoints);
|
||||
}
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
}
|
||||
|
||||
VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
||||
gpi.stageCount = static_cast<Uint32>(payload.stages->size());
|
||||
gpi.pStages = payload.stages->data();
|
||||
gpi.stageCount = static_cast<Uint32>(effectiveStages->size());
|
||||
gpi.pStages = effectiveStages->data();
|
||||
gpi.pVertexInputState = payload.vertexInputState;
|
||||
gpi.pInputAssemblyState = &ia;
|
||||
gpi.pTessellationState =
|
||||
@@ -490,6 +546,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
const VkResult result = vkCreateGraphicsPipelines(m_device, m_pipelineCache, 1, &gpi, nullptr, &pipeline);
|
||||
// Loud, at MGLOG_F, and deliberately NOT latched. vkCreateGraphicsPipelines refusing a
|
||||
// pipeline MobileGL assembled is a should-never-happen state, and the driver's own
|
||||
// answer is VK_ERROR_UNKNOWN - no information at all - so this dump is the entire
|
||||
// diagnosis. It is not an expected failure mode, so the one-shot rule that quiets W/E
|
||||
// does not apply: while this is reachable it should keep saying so on every draw.
|
||||
// GetOrCreatePipeline deliberately does not cache the failure, which is what makes that
|
||||
// repetition happen; if the repetition ever needs to stop, fix the pipeline, not the log.
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_F("PipelineFactory::CreatePipeline failed: result=%s (%d) programHash=0x%llx vertexInputHash=0x%llx stageCount=%u topology=%s(%d) colorAttachmentCount=%u samples=%s(%d) subpass=%u",
|
||||
VkResultToString(result),
|
||||
@@ -522,8 +585,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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.
|
||||
// investigation) is to name the modules. MGLOG_I, not _D: this is part of a
|
||||
// should-never-happen report and must survive in the INFO-level builds that CTS
|
||||
// actually runs against, alongside the MGLOG_F lines above.
|
||||
if (payload.stageSpirvDigests) {
|
||||
for (SizeT i = 0; i < payload.stageSpirvDigests->size(); ++i) {
|
||||
const auto& digest = (*payload.stageSpirvDigests)[i];
|
||||
|
||||
@@ -42,6 +42,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool primitiveRestartEnable = false;
|
||||
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
||||
Uint32 patchControlPoints = 3;
|
||||
// How many of ARB_viewport_array's viewports this pipeline rasterizes into. 1 for
|
||||
// every program that never assigns gl_ViewportIndex, which is all of them outside the
|
||||
// conformance suite - the wide shape costs a longer vkCmdSetViewport/Scissor per state
|
||||
// change and can cost hardware fast paths, so it is opt-in per program. Baked into the
|
||||
// pipeline (viewportCount is not dynamic without VK_EXT_extended_dynamic_state) and
|
||||
// therefore hashed; the DYNAMIC viewport/scissor arrays the draw pushes must have
|
||||
// exactly this many elements (VUID-vkCmdDraw-viewportCount-03417/-03418).
|
||||
Uint32 viewportCount = 1;
|
||||
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
|
||||
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
||||
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
||||
@@ -71,6 +79,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool fragmentReplacesDepth = false;
|
||||
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
|
||||
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
|
||||
// The tessellation control stage this renderer synthesized for a program that has
|
||||
// an evaluation stage and none of its own (GL 4.6 core 11.2.2 gives such a program a
|
||||
// fixed-function pass-through; Vulkan has no such thing and
|
||||
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 forbids the half-tessellated
|
||||
// pipeline outright). Appended to `stages` at creation. A null module means the
|
||||
// renderer could not build one, and CreatePipeline refuses the pipeline - the same
|
||||
// refusal it applies when `stages` itself is half-tessellated.
|
||||
//
|
||||
// NOT hashed: it is a pure function of the program and of patchControlPoints, both
|
||||
// of which ComputeHash already mixes in.
|
||||
VkPipelineShaderStageCreateInfo passthroughTessControlStage{};
|
||||
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
|
||||
// Diagnostic only; may be null. Read solely from the pipeline-creation failure path.
|
||||
const Vector<ShaderStageSpirvDigest>* stageSpirvDigests = nullptr;
|
||||
|
||||
@@ -376,12 +376,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
spv_diagnostic diagnostic = nullptr;
|
||||
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.
|
||||
// MGLOG_E, unlatched: reaching here already requires the validation switch to
|
||||
// be armed, which bounds the volume, and each VUID names a different defect.
|
||||
// (Parked at MGLOG_I until the Log.h level ordering was fixed, when E was
|
||||
// compiled out of every INFO build.) The latch is what a test harness asserts on.
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
|
||||
MGLOG_I(
|
||||
MGLOG_E(
|
||||
"ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d index=%zu msg=%s",
|
||||
static_cast<Int>(shaderStage),
|
||||
programExternalIndex,
|
||||
@@ -1266,7 +1266,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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 "
|
||||
MGLOG_D("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;
|
||||
@@ -1855,16 +1855,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// unification and the set->0 normalisation this function exists to do. A
|
||||
// program with an image array plus any second descriptor got aliased
|
||||
// bindings out of that, and a DEBUG build trapped on the same program.
|
||||
// Which is also why the message below is MGLOG_I: MGLOG_E is compiled out
|
||||
// of an INFO build, so a refusal that only said MGLOG_E said nothing at all
|
||||
// in the builds that ship.
|
||||
// The refusal below is MGLOG_E and per-program-compile, so it reports every
|
||||
// program it declines. It spent time at MGLOG_I because the old level
|
||||
// ordering compiled E out of the builds that ship.
|
||||
const Bool arraySupportedForKind =
|
||||
kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic ||
|
||||
kind == ProgramFactory::DescriptorBindingKind::StorageBuffer ||
|
||||
kind == ProgramFactory::DescriptorBindingKind::StorageImage ||
|
||||
kind == ProgramFactory::DescriptorBindingKind::CombinedImageSampler;
|
||||
if (binding->count != 1 && !arraySupportedForKind) {
|
||||
MGLOG_I("ProgramFactory: descriptor arrays are unsupported for this descriptor "
|
||||
MGLOG_E("ProgramFactory: descriptor arrays are unsupported for this descriptor "
|
||||
"kind (name='%s' count=%u type=%d)",
|
||||
binding->name ? binding->name : "<null>", binding->count,
|
||||
static_cast<Int>(binding->descriptor_type));
|
||||
@@ -1997,6 +1997,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return ReflectedDeclaresInputBuiltin(reflectModule, SpvBuiltInBaseVertex);
|
||||
}
|
||||
|
||||
// gl_ViewportIndex on the last pre-rasterization stage. glslang emits it natively for Vulkan
|
||||
// (BuiltIn ViewportIndex plus OpCapability MultiViewport), and nothing in the SpirvPasses
|
||||
// chain touches it, so a plain reflection of the declared output builtins is the whole test.
|
||||
Bool ProgramFactory::ReflectedWritesViewportIndexBuiltin(const SpvReflectShaderModule& reflectModule) {
|
||||
return ReflectedDeclaresOutputBuiltin(reflectModule, SpvBuiltInViewportIndex);
|
||||
}
|
||||
|
||||
Bool ProgramFactory::ReflectedDeclaresOutputBuiltin(const SpvReflectShaderModule& reflectModule,
|
||||
SpvBuiltIn builtin) {
|
||||
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
||||
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
|
||||
for (Uint32 variableIndex = 0; variableIndex < entryPoint.output_variable_count; ++variableIndex) {
|
||||
const SpvReflectInterfaceVariable* variable = entryPoint.output_variables[variableIndex];
|
||||
if (variable != nullptr &&
|
||||
(variable->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0 &&
|
||||
variable->built_in == builtin) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool ProgramFactory::ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule,
|
||||
SpvBuiltIn builtin) {
|
||||
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
||||
@@ -2339,6 +2362,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
// Which pre-rasterization stage assigns gl_ViewportIndex is not fixed: GL 4.1 allows only the
|
||||
// geometry stage, ARB_shader_viewport_layer_array/GL 4.6 also the vertex and tessellation
|
||||
// evaluation stages. Rather than guess which one is last, every non-fragment, non-compute
|
||||
// module is asked - one writer anywhere means this program's draws need a multi-viewport
|
||||
// pipeline, and a false positive costs only a wider viewportCount.
|
||||
void ProgramFactory::ReflectViewportIndexUsage(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const {
|
||||
entry.writesViewportIndexBuiltin = false;
|
||||
|
||||
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||
if (!shaders[moduleIndex]) continue;
|
||||
const ShaderStage stage = shaders[moduleIndex]->GetShaderStage();
|
||||
if (stage == ShaderStage::Fragment || stage == ShaderStage::Compute) continue;
|
||||
|
||||
const auto& module = spirv[moduleIndex];
|
||||
if (module.empty()) continue;
|
||||
|
||||
SpvReflectShaderModule reflectModule{};
|
||||
const SpvReflectResult createResult =
|
||||
spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule);
|
||||
if (createResult != SPV_REFLECT_RESULT_SUCCESS) {
|
||||
// Fail toward the wide pipeline. Missing a real gl_ViewportIndex writer would
|
||||
// silently collapse every viewport onto 0 (the exact bug this reflection exists
|
||||
// to fix); over-declaring costs one extra viewport slot on a program that never
|
||||
// uses it.
|
||||
MGLOG_E_ONCE("ProgramFactory::ReflectViewportIndexUsage: reflection failed (result=%d); assuming the "
|
||||
"program writes gl_ViewportIndex",
|
||||
static_cast<Int>(createResult));
|
||||
entry.writesViewportIndexBuiltin = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ReflectedWritesViewportIndexBuiltin(reflectModule)) {
|
||||
entry.writesViewportIndexBuiltin = true;
|
||||
}
|
||||
spvReflectDestroyShaderModule(&reflectModule);
|
||||
}
|
||||
}
|
||||
|
||||
void ProgramFactory::ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const {
|
||||
@@ -2468,7 +2531,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// inert; a device whose binding cap is smaller than a shader's array is not a
|
||||
// configuration MobileGL can serve at all. Needs a >maxBindings-element array to
|
||||
// reach (256 on desktop, ~16 on mobile).
|
||||
MGLOG_I("ProgramFactory::ReflectLayout: %s array '%s' at binding %u has %u elements, past the %u "
|
||||
MGLOG_D("ProgramFactory::ReflectLayout: %s array '%s' at binding %u has %u elements, past the %u "
|
||||
"this device can describe - declining the program",
|
||||
kindLabel, uniformName.c_str(), binding, count, maxBindings);
|
||||
outDeclined = true;
|
||||
@@ -2476,7 +2539,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
if (baseLocation < 0 ||
|
||||
!program.UniformLocationsAliasSameUniform(baseLocation, baseLocation + static_cast<Int>(count - 1u))) {
|
||||
MGLOG_I("ProgramFactory::ReflectLayout: %s array '%s' at binding %u spans %u descriptors but the "
|
||||
MGLOG_D("ProgramFactory::ReflectLayout: %s array '%s' at binding %u spans %u descriptors but the "
|
||||
"reflection reserved fewer uniform locations for it (base=%d) - a multi-dimensional array "
|
||||
"is the usual cause, and MobileGL declines it rather than resolve elements onto a "
|
||||
"neighbouring uniform",
|
||||
@@ -2713,7 +2776,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// a Uint16 on the way, where 65536 would silently become 0.
|
||||
const Uint32 storageArrayCount = std::max<Uint32>(1u, sampler->count);
|
||||
if (storageArrayCount > m_maxBindings) {
|
||||
MGLOG_I("ProgramFactory::ReflectLayout: storage block array '%s' at binding %u has %u "
|
||||
MGLOG_D("ProgramFactory::ReflectLayout: storage block array '%s' at binding %u has %u "
|
||||
"elements, past the %u this device can describe - declining the program",
|
||||
uniformName.c_str(), binding, storageArrayCount, m_maxBindings);
|
||||
entry.declinedDescriptors = true;
|
||||
@@ -2736,7 +2799,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// so at a level that survives a release build, because dropping the binding
|
||||
// leaves the shader reading a descriptor the layout never declared.
|
||||
if (sampler->count > 1) {
|
||||
MGLOG_I("ProgramFactory::ReflectLayout: declining '%s' at binding %u - a %u-element "
|
||||
MGLOG_E("ProgramFactory::ReflectLayout: declining '%s' at binding %u - a %u-element "
|
||||
"descriptor array with no frontend uniform location (a multi-dimensional array "
|
||||
"of samplers or images is the known cause)",
|
||||
uniformName.c_str(), binding, sampler->count);
|
||||
@@ -2910,8 +2973,73 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
bindings.push_back(layoutBinding);
|
||||
}
|
||||
|
||||
// UPDATE_AFTER_BIND is strictly an optional per-layout acceleration. The GL
|
||||
// descriptor model still resolves every sampler uniform element independently
|
||||
// (including its texture-unit sampler-object override); selecting this path
|
||||
// changes neither that resolution nor the set versioning in UniformManager.
|
||||
// A conservative count keeps a layout on ordinary descriptors whenever any
|
||||
// relevant update-after-bind limit is not large enough, rather than asking a
|
||||
// driver to reject it during vkCreateDescriptorSetLayout.
|
||||
Uint32 updateAfterBindSamplers = 0;
|
||||
Uint32 updateAfterBindUniformBuffers = 0;
|
||||
Uint32 updateAfterBindStorageBuffers = 0;
|
||||
Uint32 updateAfterBindSampledImages = 0;
|
||||
Uint32 updateAfterBindStorageImages = 0;
|
||||
for (Uint32 binding = 0; binding < m_maxBindings; ++binding) {
|
||||
const Uint32 count = entry.bindingDescriptorCounts[binding];
|
||||
switch (entry.bindingKinds[binding]) {
|
||||
case DescriptorBindingKind::UniformBufferDynamic:
|
||||
updateAfterBindUniformBuffers += count;
|
||||
break;
|
||||
case DescriptorBindingKind::CombinedImageSampler:
|
||||
updateAfterBindSamplers += count;
|
||||
updateAfterBindSampledImages += count;
|
||||
break;
|
||||
case DescriptorBindingKind::UniformTexelBuffer:
|
||||
updateAfterBindSampledImages += count;
|
||||
break;
|
||||
case DescriptorBindingKind::StorageBuffer:
|
||||
case DescriptorBindingKind::StorageTexelBuffer:
|
||||
updateAfterBindStorageBuffers += count;
|
||||
break;
|
||||
case DescriptorBindingKind::StorageImage:
|
||||
updateAfterBindStorageImages += count;
|
||||
break;
|
||||
case DescriptorBindingKind::None:
|
||||
break;
|
||||
}
|
||||
}
|
||||
const Uint32 updateAfterBindResources = updateAfterBindUniformBuffers + updateAfterBindStorageBuffers +
|
||||
updateAfterBindSampledImages + updateAfterBindStorageImages;
|
||||
const auto& uab = m_updateAfterBindLimits;
|
||||
entry.usesUpdateAfterBind =
|
||||
uab.enabled && updateAfterBindSamplers <= uab.maxPerStageSamplers &&
|
||||
updateAfterBindUniformBuffers <= uab.maxPerStageUniformBuffers &&
|
||||
updateAfterBindStorageBuffers <= uab.maxPerStageStorageBuffers &&
|
||||
updateAfterBindSampledImages <= uab.maxPerStageSampledImages &&
|
||||
updateAfterBindStorageImages <= uab.maxPerStageStorageImages &&
|
||||
updateAfterBindResources <= uab.maxPerStageResources &&
|
||||
updateAfterBindSamplers <= uab.maxSetSamplers &&
|
||||
updateAfterBindUniformBuffers <= uab.maxSetUniformBuffers &&
|
||||
updateAfterBindUniformBuffers <= uab.maxSetUniformBuffersDynamic &&
|
||||
updateAfterBindStorageBuffers <= uab.maxSetStorageBuffers &&
|
||||
updateAfterBindStorageBuffers <= uab.maxSetStorageBuffersDynamic &&
|
||||
updateAfterBindSampledImages <= uab.maxSetSampledImages &&
|
||||
updateAfterBindStorageImages <= uab.maxSetStorageImages;
|
||||
|
||||
Vector<VkDescriptorBindingFlags> bindingFlags;
|
||||
VkDescriptorSetLayoutBindingFlagsCreateInfo bindingFlagsInfo{};
|
||||
if (entry.usesUpdateAfterBind) {
|
||||
bindingFlags.assign(bindings.size(), VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT);
|
||||
bindingFlagsInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO;
|
||||
bindingFlagsInfo.bindingCount = static_cast<Uint32>(bindingFlags.size());
|
||||
bindingFlagsInfo.pBindingFlags = bindingFlags.data();
|
||||
}
|
||||
|
||||
VkDescriptorSetLayoutCreateInfo setLayoutInfo{};
|
||||
setLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
|
||||
setLayoutInfo.flags = entry.usesUpdateAfterBind ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0;
|
||||
setLayoutInfo.pNext = entry.usesUpdateAfterBind ? &bindingFlagsInfo : nullptr;
|
||||
setLayoutInfo.bindingCount = static_cast<Uint32>(bindings.size());
|
||||
setLayoutInfo.pBindings = bindings.data();
|
||||
VK_VERIFY(vkCreateDescriptorSetLayout(m_device, &setLayoutInfo, nullptr, &entry.descriptorSetLayout),
|
||||
@@ -2991,6 +3119,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto& shaders = program.GetAttachedShaders();
|
||||
auto& spirv = program.GetGeneratedSpirv();
|
||||
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
|
||||
const Bool enableSpirvValidation = program.GetSpirvValidationEnabled();
|
||||
if (enableSpirvValidation) {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
}
|
||||
|
||||
const ShaderStage fixupStage = PickClipFixupStage(shaders);
|
||||
|
||||
@@ -3036,7 +3168,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// stored as - which addresses [0,1] where the application addressed texels.
|
||||
{
|
||||
Vector<Uint> rectLoweredSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(moduleSpirvs[i], rectLoweredSpirv) &&
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(moduleSpirvs[i], rectLoweredSpirv, enableSpirvValidation) &&
|
||||
!rectLoweredSpirv.empty()) {
|
||||
moduleSpirvs[i] = Move(rectLoweredSpirv);
|
||||
}
|
||||
@@ -3049,7 +3181,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
{
|
||||
Vector<Uint> invariantSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::DecoratePositionInvariantForVulkan(
|
||||
moduleSpirvs[i], invariantSpirv)) {
|
||||
moduleSpirvs[i], invariantSpirv, enableSpirvValidation)) {
|
||||
moduleSpirvs[i] = std::move(invariantSpirv);
|
||||
} else {
|
||||
// The pass round-trips through SPIRV-Tools IR, so an unparseable module
|
||||
@@ -3073,7 +3205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_shaderDrawParametersEnabled) {
|
||||
Vector<Uint> rebasedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i],
|
||||
rebasedSpirv)) {
|
||||
rebasedSpirv, enableSpirvValidation)) {
|
||||
moduleSpirvs[i] = std::move(rebasedSpirv);
|
||||
} else {
|
||||
MGLOG_E("ProgramFactory: failed to rebase gl_InstanceID for program %u; "
|
||||
@@ -3091,7 +3223,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
(flags & CompileOptionBit::ZeroBaseVertex)) {
|
||||
Vector<Uint> zeroedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroBaseVertexForVulkan(moduleSpirvs[i],
|
||||
zeroedSpirv)) {
|
||||
zeroedSpirv, enableSpirvValidation)) {
|
||||
moduleSpirvs[i] = std::move(zeroedSpirv);
|
||||
} else {
|
||||
// Failing open keeps the native builtin, which is the pre-fix behavior:
|
||||
@@ -3114,7 +3246,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) {
|
||||
Vector<Uint> packedSpirv;
|
||||
const Bool packOk = MG_Util::ShaderTranspiler::ShaderCompiler::PackDoubleVertexInputsForVulkan(
|
||||
moduleSpirvs[i], packedSpirv);
|
||||
moduleSpirvs[i], packedSpirv, enableSpirvValidation);
|
||||
MOBILEGL_ASSERT(packOk,
|
||||
"ProgramFactory: 64-bit vertex input packing failed for program %u; the "
|
||||
"vertex-input format and the shader input type now disagree",
|
||||
@@ -3138,7 +3270,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (m_unformattedFloatStorageImagesEnabled) {
|
||||
Vector<Uint> unformattedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
|
||||
moduleSpirvs[i], unformattedSpirv)) {
|
||||
moduleSpirvs[i], unformattedSpirv, enableSpirvValidation)) {
|
||||
moduleSpirvs[i] = std::move(unformattedSpirv);
|
||||
} else {
|
||||
MGLOG_E("ProgramFactory: failed to make float storage images unformatted for program %u",
|
||||
@@ -3159,7 +3291,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
#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()) {
|
||||
if (enableSpirvValidation) {
|
||||
ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex());
|
||||
}
|
||||
#endif
|
||||
@@ -3189,7 +3321,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ValidateRasterizationStageInterface(shaders, moduleSpirvs, entry, program.GetExternalIndex());
|
||||
#endif
|
||||
ReflectVertexInputs(shaders, moduleSpirvs, entry);
|
||||
ReflectViewportIndexUsage(shaders, moduleSpirvs, entry);
|
||||
ReflectFragmentOutputs(shaders, moduleSpirvs, entry);
|
||||
ReflectPassthroughTessControlNeed(shaders, moduleSpirvs, entry);
|
||||
ReflectLayout(program, moduleSpirvs, entry);
|
||||
// A failed remap means the modules kept glslang's per-stage auto-mapped binding numbers -
|
||||
// no cross-stage unification, no set->0 normalisation - so the bindings this layout
|
||||
@@ -3200,7 +3334,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// "the layout and the shader disagree", so route it through that. Set AFTER ReflectLayout,
|
||||
// which clears the flag.
|
||||
if (!remapOk) {
|
||||
MGLOG_I("ProgramFactory::GetOrCreateProgram: declining program %u - its descriptor bindings could not "
|
||||
MGLOG_E("ProgramFactory::GetOrCreateProgram: declining program %u - its descriptor bindings could not "
|
||||
"be remapped, so the layout does not describe what the shader reads",
|
||||
program.GetExternalIndex());
|
||||
entry.declinedDescriptors = true;
|
||||
@@ -3234,17 +3368,249 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkDescriptorSetLayout descriptorSetLayout = it->second.descriptorSetLayout;
|
||||
MGLOG_D("ProgramFactory::OnFrameBoundary: evicting idle program entry hash=0x%llx",
|
||||
static_cast<unsigned long long>(hash));
|
||||
// erase runs ~VkProgramObject (modules/layouts destroyed); notify after
|
||||
// so an observer never observes a half-destroyed entry through a lookup.
|
||||
// Observers only need the handle values to purge their keyed caches.
|
||||
++m_cacheStructureEpoch; // erase moves/kills entries: memoised pointers die
|
||||
it = m_cache.erase(it);
|
||||
// The observer destroys dependent pipelines and frees descriptor sets while
|
||||
// this entry still owns its layout. Vulkan requires every descriptor set to be
|
||||
// freed before its VkDescriptorSetLayout is destroyed.
|
||||
if (m_evictionObserver != nullptr) {
|
||||
m_evictionObserver->OnProgramEvicted(hash, descriptorSetLayout);
|
||||
}
|
||||
++m_cacheStructureEpoch; // erase moves/kills entries: memoised pointers die
|
||||
it = m_cache.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProgramFactory::~ProgramFactory() {
|
||||
for (auto& entry : m_passthroughTessControlStages) {
|
||||
if (entry.second.module != VK_NULL_HANDLE) {
|
||||
vkDestroyShaderModule(m_device, entry.second.module, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String ProgramFactory::BuildPassthroughTessControlSource(Uint32 patchVertices) {
|
||||
// The stage GL 4.6 core 11.2.2 describes when a program has an evaluation shader and no
|
||||
// control shader: "the input patch is passed through unmodified", the output patch has
|
||||
// as many vertices as the input one (PATCH_VERTICES), and the levels come from the
|
||||
// PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
|
||||
//
|
||||
// Those two levels default to 1.0 and are baked here as literals because
|
||||
// glPatchParameterfv - their only setter - is not implemented in this frontend (it is a
|
||||
// stub in MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means
|
||||
// making the levels a parameter of this source AND of the cache key in
|
||||
// GetOrCreatePassthroughTessControlStage; the two must move together, so they are named
|
||||
// together here.
|
||||
//
|
||||
// gl_out carries gl_Position and nothing else on purpose. The evaluation stage that
|
||||
// reads it was linked against the VERTEX stage directly, so its input gl_PerVertex holds
|
||||
// exactly the built-ins that stage used, and its user-defined inputs (if any) come
|
||||
// straight off the vertex stage's outputs - which a control stage sitting in between
|
||||
// would leave unwritten. ReflectPassthroughTessControlNeed refuses those programs rather
|
||||
// than let this write a partial interface.
|
||||
//
|
||||
// All four outer levels and both inner levels are written unconditionally: writing a
|
||||
// level the evaluation stage's domain does not use is legal and ignored, and it saves
|
||||
// this from having to know the domain.
|
||||
String source = "#version 450 core\n";
|
||||
source += "layout(vertices = " + std::to_string(patchVertices) + ") out;\n";
|
||||
// gl_in and gl_out are redeclared to the exact gl_PerVertex the FRONTEND's linked programs
|
||||
// carry - gl_Position, gl_PointSize, gl_ClipDistance[1], in that order - because Vulkan
|
||||
// matches built-in interface blocks by their whole shape, and the two obvious spellings
|
||||
// are both wrong:
|
||||
// * narrowing the block to gl_Position alone makes the evaluation stage read a patch of
|
||||
// zeroes (degenerate triangles, nothing rasterized), and
|
||||
// * taking glslang's DEFAULT block for a standalone control stage yields FOUR members -
|
||||
// it appends gl_CullDistance - where a linked vertex+evaluation program has three.
|
||||
// PassthroughTessControlTest.MatchesTheFrontendPerVertexBlock is the latch: it links a
|
||||
// vertex+evaluation program through this same compiler and fails if the two shapes ever
|
||||
// stop agreeing, rather than letting the mismatch show up as a black frame.
|
||||
//
|
||||
// Only gl_Position is written. gl_PointSize is declared but left alone deliberately:
|
||||
// writing it from a tessellation stage requires the shaderTessellationAndGeometryPointSize
|
||||
// feature, which this renderer does not enable, so a program whose evaluation stage reads
|
||||
// gl_in[].gl_PointSize gets an undefined point size instead of the vertex stage's - a gap
|
||||
// this trades for not making every tessellated pipeline depend on an optional feature.
|
||||
source += "in gl_PerVertex {\n"
|
||||
" vec4 gl_Position;\n"
|
||||
" float gl_PointSize;\n"
|
||||
" float gl_ClipDistance[1];\n"
|
||||
"} gl_in[gl_MaxPatchVertices];\n";
|
||||
source += "out gl_PerVertex {\n"
|
||||
" vec4 gl_Position;\n"
|
||||
" float gl_PointSize;\n"
|
||||
" float gl_ClipDistance[1];\n"
|
||||
"} gl_out[];\n";
|
||||
source += "void main() {\n";
|
||||
source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n";
|
||||
source += " gl_TessLevelOuter[0] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[1] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[2] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[3] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[0] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[1] = 1.0;\n";
|
||||
source += "}\n";
|
||||
return source;
|
||||
}
|
||||
|
||||
VkPipelineShaderStageCreateInfo ProgramFactory::GetOrCreatePassthroughTessControlStage(Uint32 patchVertices) {
|
||||
// A cached VK_NULL_HANDLE is a remembered failure, not a miss: returning it keeps a
|
||||
// generator that cannot compile from re-running glslang on every draw.
|
||||
const auto cached = m_passthroughTessControlStages.find(patchVertices);
|
||||
if (cached != m_passthroughTessControlStages.end()) {
|
||||
return cached->second;
|
||||
}
|
||||
|
||||
VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
|
||||
stage.stage = VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT;
|
||||
stage.module = VK_NULL_HANDLE;
|
||||
stage.pName = "main";
|
||||
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
const String source = BuildPassthroughTessControlSource(patchVertices);
|
||||
// Same compile configuration as every other stage of every other program: this runs on
|
||||
// the GL thread (the draw path), so the live compile env is the right one, and flags=0
|
||||
// is the Vulkan-targeting form (CompileForOpenGL is what the GLES backend adds).
|
||||
const SharedPtr<const CompileEnv>& env = GetCurrentCompileEnv();
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_TESS_CONTROL_SHADER,
|
||||
.sourceStr = source,
|
||||
.flags = 0,
|
||||
.env = env.get()};
|
||||
auto compiled = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
if (!compiled) {
|
||||
MGLOG_E("ProgramFactory: could not compile the pass-through tessellation control stage for "
|
||||
"patchVertices=%u; a program with an evaluation stage and no control stage cannot draw. %s",
|
||||
patchVertices, compiled.error().log.c_str());
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
ProgramAttrib programAttrib{};
|
||||
programAttrib.shaders.push_back(compiled.value());
|
||||
auto linked = ShaderCompiler::LinkProgram(programAttrib);
|
||||
if (!linked) {
|
||||
MGLOG_E("ProgramFactory: could not link the pass-through tessellation control stage for "
|
||||
"patchVertices=%u. %s", patchVertices, linked.error().log.c_str());
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_TESS_CONTROL_SHADER}, .program = *linked.value()};
|
||||
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
if (!binary || binary.value().empty() || binary.value().front().empty()) {
|
||||
MGLOG_E("ProgramFactory: could not generate SPIR-V for the pass-through tessellation control stage "
|
||||
"for patchVertices=%u", patchVertices);
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
const Vector<Uint>& spirv = binary.value().front();
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
|
||||
#else
|
||||
if (m_enableSpirvValidation) {
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
smci.codeSize = spirv.size() * sizeof(Uint);
|
||||
smci.pCode = spirv.data();
|
||||
VkShaderModule module = VK_NULL_HANDLE;
|
||||
const VkResult result = vkCreateShaderModule(m_device, &smci, nullptr, &module);
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E("ProgramFactory: vkCreateShaderModule failed (%d) for the pass-through tessellation control "
|
||||
"stage for patchVertices=%u", static_cast<Int>(result), patchVertices);
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
stage.module = module;
|
||||
MGLOG_D("ProgramFactory: built the pass-through tessellation control stage for patchVertices=%u "
|
||||
"(GL 4.6 11.2.2; Vulkan has no fixed-function equivalent)", patchVertices);
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
void ProgramFactory::ReflectPassthroughTessControlNeed(
|
||||
const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const {
|
||||
entry.needsPassthroughTessControl = false;
|
||||
entry.passthroughTessControlEmulatable = false;
|
||||
|
||||
Bool hasTessEval = false;
|
||||
Bool hasTessControl = false;
|
||||
SizeT tessEvalModuleIndex = 0;
|
||||
for (SizeT i = 0; i < shaders.size(); ++i) {
|
||||
if (!shaders[i]) continue;
|
||||
const auto stage = shaders[i]->GetShaderStage();
|
||||
if (stage == ShaderStage::TessControl) hasTessControl = true;
|
||||
if (stage == ShaderStage::TessEval) {
|
||||
hasTessEval = true;
|
||||
tessEvalModuleIndex = i;
|
||||
}
|
||||
}
|
||||
if (!hasTessEval || hasTessControl) return;
|
||||
|
||||
entry.needsPassthroughTessControl = true;
|
||||
|
||||
if (tessEvalModuleIndex >= spirv.size() || spirv[tessEvalModuleIndex].empty()) return;
|
||||
const auto& module = spirv[tessEvalModuleIndex];
|
||||
|
||||
SpvReflectShaderModule reflectModule{};
|
||||
const SpvReflectResult createResult =
|
||||
spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule);
|
||||
if (createResult != SPV_REFLECT_RESULT_SUCCESS) {
|
||||
MGLOG_E("ProgramFactory::ReflectPassthroughTessControlNeed: reflection failed (result=%d); the "
|
||||
"evaluation stage's inputs are unknown, so the pass-through is not offered",
|
||||
static_cast<Int>(createResult));
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t inputCount = 0;
|
||||
SpvReflectResult reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, nullptr);
|
||||
Vector<SpvReflectInterfaceVariable*> inputs(inputCount);
|
||||
if (reflectResult == SPV_REFLECT_RESULT_SUCCESS && inputCount > 0) {
|
||||
reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, inputs.data());
|
||||
}
|
||||
if (reflectResult != SPV_REFLECT_RESULT_SUCCESS) {
|
||||
spvReflectDestroyShaderModule(&reflectModule);
|
||||
return;
|
||||
}
|
||||
|
||||
// The question is only ever "does this stage read anything a control stage would have to
|
||||
// forward", and the answer is: does it have a LOCATION. A located input is a user-defined
|
||||
// varying (or a per-patch input), which the vertex stage writes today and would stop
|
||||
// reaching once a control stage sits in between - the pass-through carries gl_Position and
|
||||
// nothing else, so such a program is declined instead of being handed undefined values.
|
||||
// Everything without a location is a built-in: gl_in, gl_TessCoord, gl_PatchVerticesIn,
|
||||
// gl_PrimitiveID, gl_TessLevel*, all either forwarded or generated for the evaluation
|
||||
// stage by the tessellator itself.
|
||||
//
|
||||
// This deliberately does NOT judge on SpvReflectInterfaceVariable::built_in. gl_in is an
|
||||
// array of interface blocks, and for those SPIRV-Reflect reports built_in == -1 on the
|
||||
// block AND leaves every member's built_in at 0 - which is SpvBuiltInPosition, so a
|
||||
// member walk reads "Position, Position, Position" for a {Position, PointSize,
|
||||
// ClipDistance} block and would accept anything on the strength of parse garbage. The
|
||||
// location, by contrast, is decorated on the OpVariable and is what SPIRV-Reflect reads
|
||||
// straight through.
|
||||
constexpr Uint32 kNoLocation = 0xFFFFFFFFu;
|
||||
Bool emulatable = true;
|
||||
for (auto* input : inputs) {
|
||||
if (input == nullptr) continue;
|
||||
if (input->location == kNoLocation) continue;
|
||||
MGLOG_E("ProgramFactory: a tessellation evaluation stage with no control stage reads the "
|
||||
"user-defined input '%s' at location=%u; a synthesized control stage cannot forward it, so "
|
||||
"this program's draws are declined rather than fed an undefined varying",
|
||||
input->name != nullptr ? input->name : "<null>", input->location);
|
||||
emulatable = false;
|
||||
break;
|
||||
}
|
||||
|
||||
spvReflectDestroyShaderModule(&reflectModule);
|
||||
entry.passthroughTessControlEmulatable = emulatable;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -76,6 +76,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||
using HashType = Uint64;
|
||||
|
||||
struct UpdateAfterBindLimits {
|
||||
Bool enabled = false;
|
||||
Uint32 maxPerStageSamplers = 0;
|
||||
Uint32 maxPerStageUniformBuffers = 0;
|
||||
Uint32 maxPerStageStorageBuffers = 0;
|
||||
Uint32 maxPerStageSampledImages = 0;
|
||||
Uint32 maxPerStageStorageImages = 0;
|
||||
Uint32 maxPerStageResources = 0;
|
||||
Uint32 maxSetSamplers = 0;
|
||||
Uint32 maxSetUniformBuffers = 0;
|
||||
Uint32 maxSetUniformBuffersDynamic = 0;
|
||||
Uint32 maxSetStorageBuffers = 0;
|
||||
Uint32 maxSetStorageBuffersDynamic = 0;
|
||||
Uint32 maxSetSampledImages = 0;
|
||||
Uint32 maxSetStorageImages = 0;
|
||||
};
|
||||
|
||||
struct VkProgramObject {
|
||||
static constexpr Uint32 kMaxVertexInputLocations = 32;
|
||||
|
||||
@@ -88,6 +105,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// Layout data (previously in separate VkProgramLayout)
|
||||
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
|
||||
// True only when this layout passed every descriptor-indexing feature and
|
||||
// update-after-bind limit gate at reflection time. It controls both the
|
||||
// layout/binding flags and the pool class used by UniformManager.
|
||||
Bool usesUpdateAfterBind = false;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
Vector<DescriptorBindingKind> bindingKinds;
|
||||
// The bindings this program actually declares, ascending. bindingKinds is sized to the
|
||||
@@ -151,6 +172,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// PROGRAM rather than of the variant: the zeroed variant leaves the variable
|
||||
// declared, so both variants answer the same and the draw path can ask either.
|
||||
Bool readsBaseVertexBuiltin = false;
|
||||
// Some pre-rasterization stage assigns gl_ViewportIndex. Its pipeline declares
|
||||
// viewportCount = the renderer's rasterizable viewport count instead of 1, and its
|
||||
// draws push the whole viewport/scissor array; every other program keeps the
|
||||
// single-viewport fast path untouched. Part of the program's identity (folded into
|
||||
// the pipeline hash through programHash), so no memo can serve the wrong shape.
|
||||
Bool writesViewportIndexBuiltin = false;
|
||||
// This program has a tessellation EVALUATION stage and no tessellation CONTROL
|
||||
// stage. GL allows that (4.6 core 11.2.2: with no control shader the input patch
|
||||
// is passed through unmodified, the output patch size is PATCH_VERTICES, and the
|
||||
// levels come from the PATCH_DEFAULT_*_LEVEL state); Vulkan does not - either both
|
||||
// tessellation stages are present or neither
|
||||
// (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). So the draw path has to supply
|
||||
// the pass-through stage GL describes; see GetOrCreatePassthroughTessControlStage.
|
||||
Bool needsPassthroughTessControl = false;
|
||||
// ...and the pass-through this renderer can synthesize carries gl_Position and
|
||||
// nothing else, so it is only correct when the evaluation stage's inputs are
|
||||
// built-ins. A user-defined varying would arrive at the evaluation stage
|
||||
// UNWRITTEN once a control stage sits between it and the vertex stage, which is
|
||||
// silently wrong pixels rather than a crash - so those programs are declined
|
||||
// instead (PipelineFactory::CreatePipeline refuses the pipeline and the draw is
|
||||
// skipped). See ReflectPassthroughTessControlNeed.
|
||||
Bool passthroughTessControlEmulatable = false;
|
||||
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
||||
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
|
||||
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
||||
@@ -174,6 +217,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// a pipeline failure would be reported against the wrong SPIR-V.
|
||||
stageSpirvDigests = std::move(other.stageSpirvDigests);
|
||||
descriptorSetLayout = other.descriptorSetLayout;
|
||||
usesUpdateAfterBind = other.usesUpdateAfterBind;
|
||||
pipelineLayout = other.pipelineLayout;
|
||||
bindingKinds = std::move(other.bindingKinds);
|
||||
activeBindings = std::move(other.activeBindings);
|
||||
@@ -202,9 +246,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
other.usesUpdateAfterBind = false;
|
||||
other.pipelineLayout = VK_NULL_HANDLE;
|
||||
other.hasStorageImages = false;
|
||||
other.declinedDescriptors = false;
|
||||
@@ -216,6 +264,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.readsBaseVertexBuiltin = false;
|
||||
other.writesViewportIndexBuiltin = false;
|
||||
other.needsPassthroughTessControl = false;
|
||||
other.passthroughTessControlEmulatable = false;
|
||||
other.lastUsedFrame = 0;
|
||||
}
|
||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||
@@ -228,6 +279,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
modules = std::move(other.modules);
|
||||
stageSpirvDigests = std::move(other.stageSpirvDigests); // travels with `modules` - see the move ctor
|
||||
descriptorSetLayout = other.descriptorSetLayout;
|
||||
usesUpdateAfterBind = other.usesUpdateAfterBind;
|
||||
pipelineLayout = other.pipelineLayout;
|
||||
bindingKinds = std::move(other.bindingKinds);
|
||||
activeBindings = std::move(other.activeBindings);
|
||||
@@ -256,9 +308,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
other.usesUpdateAfterBind = false;
|
||||
other.pipelineLayout = VK_NULL_HANDLE;
|
||||
other.hasStorageImages = false;
|
||||
other.declinedDescriptors = false;
|
||||
@@ -270,6 +326,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.readsBaseVertexBuiltin = false;
|
||||
other.writesViewportIndexBuiltin = false;
|
||||
other.needsPassthroughTessControl = false;
|
||||
other.passthroughTessControlEmulatable = false;
|
||||
other.lastUsedFrame = 0;
|
||||
return *this;
|
||||
}
|
||||
@@ -313,15 +372,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
|
||||
};
|
||||
|
||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
|
||||
Bool shaderDrawParametersEnabled = false,
|
||||
Bool unformattedFloatStorageImagesEnabled = false)
|
||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings,
|
||||
Bool shaderDrawParametersEnabled,
|
||||
Bool unformattedFloatStorageImagesEnabled,
|
||||
Bool enableSpirvValidation,
|
||||
UpdateAfterBindLimits updateAfterBindLimits)
|
||||
: m_device(device), m_maxBindings(maxBindings), m_config(config),
|
||||
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
|
||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
|
||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled),
|
||||
m_enableSpirvValidation(enableSpirvValidation),
|
||||
m_updateAfterBindLimits(updateAfterBindLimits) {
|
||||
VkProgramObject::s_device = device;
|
||||
}
|
||||
~ProgramFactory() = default;
|
||||
// Destroys the pass-through tessellation control modules. Runs while the device is
|
||||
// still alive for the same reason ~VkProgramObject's does: this factory outlives
|
||||
// nothing that owns the device.
|
||||
~ProgramFactory();
|
||||
ProgramFactory(const ProgramFactory&) = delete;
|
||||
|
||||
HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const;
|
||||
@@ -373,6 +439,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Shared by the two above: does any entry point list an input variable decorated with
|
||||
// this builtin?
|
||||
static Bool ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
|
||||
// True when an entry point writes the ViewportIndex builtin (gl_ViewportIndex), i.e. when
|
||||
// the program can route primitives to a viewport other than 0 and its pipeline therefore
|
||||
// has to declare more than one. Asks about OUTPUT variables because that is the direction
|
||||
// a pre-rasterization stage declares it in.
|
||||
static Bool ReflectedWritesViewportIndexBuiltin(const SpvReflectShaderModule& reflectModule);
|
||||
static Bool ReflectedDeclaresOutputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
|
||||
|
||||
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes for a
|
||||
// program that has an evaluation stage and no control stage, for an input patch of
|
||||
// `patchVertices` control points. Returned BY VALUE (a stage description is a POD, and
|
||||
// the cache below is a rehashing map, so a pointer into it would not survive the next
|
||||
// distinct patch size). `.module == VK_NULL_HANDLE` means the stage could not be built:
|
||||
// the caller then has no control stage to inject, and CreatePipeline refuses the
|
||||
// pipeline rather than handing the driver a half-tessellated one.
|
||||
//
|
||||
// Keyed on the patch size because GL takes the output patch size from PATCH_VERTICES,
|
||||
// which is draw state, not link state - the CTS case that motivated this links at the
|
||||
// default 3 and draws at 4. The pipeline cache already re-keys on patchControlPoints,
|
||||
// so the module a pipeline was built with is part of that pipeline's identity.
|
||||
// Compiling is bounded by the number of distinct patch sizes a program draws with
|
||||
// (MAX_PATCH_VERTICES = 32 in the worst case, one or two in practice) and only ever
|
||||
// happens for the rare program that has no control stage at all.
|
||||
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices);
|
||||
|
||||
// Source of the module above. Exposed for tests: the generated GLSL is the whole
|
||||
// contract with the evaluation stage, so it is worth pinning independently of a device.
|
||||
static String BuildPassthroughTessControlSource(Uint32 patchVertices);
|
||||
|
||||
private:
|
||||
struct ProgramLookupCache {
|
||||
@@ -386,11 +479,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectViewportIndexUsage(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
// Fills needsPassthroughTessControl / passthroughTessControlEmulatable off the linked
|
||||
// modules. Const and reflection-only: it decides nothing about the pipeline, it only
|
||||
// records what the evaluation stage's input interface is made of.
|
||||
void ReflectPassthroughTessControlNeed(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
Uint32 m_maxBindings = 0;
|
||||
@@ -402,6 +504,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// True only when the logical device enabled both
|
||||
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||
// Startup snapshot used only by internally synthesized shader modules, which do not
|
||||
// originate from a ProgramLinkTask.
|
||||
Bool m_enableSpirvValidation = false;
|
||||
// Device feature and limit gate resolved before vkCreateDevice. Keeping it in
|
||||
// the factory lets each reflected layout choose ordinary descriptors when its
|
||||
// own counts would exceed the update-after-bind budget.
|
||||
UpdateAfterBindLimits m_updateAfterBindLimits{};
|
||||
// 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;
|
||||
@@ -411,6 +520,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
|
||||
Uint64 m_cacheStructureEpoch = 1;
|
||||
IEvictionObserver* m_evictionObserver = nullptr;
|
||||
// Pass-through tessellation control stages by input patch size. Never evicted: at most
|
||||
// MAX_PATCH_VERTICES entries exist for the lifetime of the device, and every pipeline
|
||||
// ever built from one keeps referencing its module. A failed build is cached as
|
||||
// VK_NULL_HANDLE so a broken generator costs one compile, not one per draw.
|
||||
UnorderedMap<Uint32, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -157,7 +157,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
MGLOG_I("Got %d surface formats:", swapchainCapabilities.surfaceFormats.size());
|
||||
for (const auto& sf : swapchainCapabilities.surfaceFormats) {
|
||||
MGLOG_I(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
|
||||
MGLOG_D(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
|
||||
}
|
||||
|
||||
const auto pickedSurfaceFormat = ChooseSwapchainSurfaceFormat(swapchainCapabilities.surfaceFormats);
|
||||
@@ -166,7 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
MGLOG_I("Got %d present modes:", swapchainCapabilities.presentModes.size());
|
||||
for (const auto& pm : swapchainCapabilities.presentModes) {
|
||||
MGLOG_I(" %s", string_VkPresentModeKHR(pm));
|
||||
MGLOG_D(" %s", string_VkPresentModeKHR(pm));
|
||||
}
|
||||
|
||||
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes);
|
||||
|
||||
@@ -156,13 +156,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
frame.descriptorPools.clear();
|
||||
|
||||
VkDescriptorPool initialPool = VK_NULL_HANDLE;
|
||||
if (!CreateDescriptorPool(m_setsPerFrame, initialPool)) {
|
||||
MGLOG_E("UniformDescriptorBinder::Initialize failed: cannot create frame descriptor pool %u",
|
||||
if (!CreateDescriptorPool(m_setsPerFrame, false, initialPool)) {
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::Initialize failed: cannot create frame descriptor pool %u",
|
||||
frameIndex);
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
frame.descriptorPools.push_back({initialPool, m_setsPerFrame, 0});
|
||||
frame.descriptorPools.push_back({initialPool, m_setsPerFrame, 0, false});
|
||||
MGLOG_D("UniformDescriptorBinder: frame %u descriptor pool created (maxSets=%u)", frameIndex,
|
||||
m_setsPerFrame);
|
||||
}
|
||||
@@ -305,7 +305,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// texture/sampler resolution, completeness probe, sync, layout handling, sampler
|
||||
// and view lookups - would recompute the identical descriptor.
|
||||
if (trustUnchangedHint && descriptorMemoUsable && binding < m_samplerResolveMemo.size() &&
|
||||
m_samplerResolveMemo[binding].infoValid) {
|
||||
m_samplerResolveMemo[binding].infoValid &&
|
||||
m_samplerResolveMemo[binding].infoProgramLifetimeId == program.GetLifetimeId()) {
|
||||
outImageInfo = m_samplerResolveMemo[binding].info;
|
||||
return true;
|
||||
}
|
||||
@@ -345,13 +346,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
fallbackHolder = GetFallbackTexture(preferredTarget);
|
||||
texture = fallbackHolder.get();
|
||||
if (texture == nullptr) {
|
||||
MGLOG_E("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
|
||||
MGLOG_E_ONCE("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
|
||||
"location=%d unit=%d target=%d",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
|
||||
static_cast<Int>(preferredTarget));
|
||||
return false;
|
||||
}
|
||||
MGLOG_W(
|
||||
MGLOG_W_ONCE(
|
||||
"ResolveSamplerDescriptor: using fallback texture for unbound sampler binding=%u ('%s') location=%d unit=%d target=%d",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
|
||||
static_cast<Int>(preferredTarget));
|
||||
@@ -360,7 +361,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const MG_State::GLState::SamplerObject* samplerToUse =
|
||||
samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get();
|
||||
if (samplerToUse == nullptr) {
|
||||
MGLOG_E(
|
||||
MGLOG_E_ONCE(
|
||||
"ResolveSamplerDescriptor: sampler binding %u ('%s') has no sampler object (textureId=%d location=%d unit=%d)",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(), location,
|
||||
unit);
|
||||
@@ -368,7 +369,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
VkTextureManager::TextureResource* resource = m_textureManager->SyncTextureAndGetDescriptor(*texture);
|
||||
if (resource == nullptr) {
|
||||
MGLOG_E(
|
||||
MGLOG_E_ONCE(
|
||||
"ResolveSamplerDescriptor: sampler binding %u ('%s') failed to create/sync texture resource (textureId=%d target=%d location=%d unit=%d)",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
||||
static_cast<Int>(texture->GetTarget()), location, unit);
|
||||
@@ -380,7 +381,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Int attachmentLevel = 0;
|
||||
if (drawFbo &&
|
||||
FindFramebufferAttachmentForTexture(*drawFbo, *texture, attachmentType, attachmentLevel)) {
|
||||
MGLOG_W("ResolveSamplerDescriptor: framebuffer feedback loop detected: textureId=%d is bound "
|
||||
MGLOG_W_ONCE("ResolveSamplerDescriptor: framebuffer feedback loop detected: textureId=%d is bound "
|
||||
"for sampling at binding=%u, but is also attached to drawFbo=%u as %s (level=%d, "
|
||||
"trackedLayout=%d)",
|
||||
texture->GetExternalIndex(), binding, drawFbo->GetExternalIndex(),
|
||||
@@ -390,7 +391,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const Bool readyForSampling = m_textureManager->TransitionTextureForSampling(commandBuffer, *texture);
|
||||
if (!readyForSampling) {
|
||||
MGLOG_E("ResolveSamplerDescriptor: failed to transition textureId=%d for sampler binding=%u",
|
||||
MGLOG_E_ONCE("ResolveSamplerDescriptor: failed to transition textureId=%d for sampler binding=%u",
|
||||
texture->GetExternalIndex(), binding);
|
||||
return false;
|
||||
}
|
||||
@@ -432,7 +433,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
if (sampledViewFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
|
||||
MGLOG_E_ONCE("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
|
||||
"textureId=%d imageFormat=%d numericDomain=%d",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
||||
static_cast<Int>(resource->format), static_cast<Int>(numericDomain));
|
||||
@@ -445,7 +446,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
? resource->sampledView
|
||||
: m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
|
||||
if (sampledImageView == VK_NULL_HANDLE) {
|
||||
MGLOG_E("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
|
||||
MGLOG_E_ONCE("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
|
||||
"textureId=%d imageFormat=%d viewFormat=%d numericDomain=%d",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
||||
static_cast<Int>(resource->format), static_cast<Int>(sampledViewFormat),
|
||||
@@ -504,6 +505,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (binding < m_samplerResolveMemo.size()) {
|
||||
if (descriptorMemoUsable) {
|
||||
m_samplerResolveMemo[binding].info = outImageInfo;
|
||||
m_samplerResolveMemo[binding].infoProgramLifetimeId = program.GetLifetimeId();
|
||||
m_samplerResolveMemo[binding].infoValid = true;
|
||||
} else {
|
||||
// An arrayed binding publishes nothing here, and clears what a previous program
|
||||
@@ -671,14 +673,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
SharedPtr<MG_State::GLState::ITextureObject> texture;
|
||||
if (!ResolveSamplerTexture(program, programObj, binding, texture) || texture == nullptr) {
|
||||
MGLOG_E("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') is unbound", binding,
|
||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') is unbound", binding,
|
||||
programObj.samplerNameByBinding[binding].c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (texture->GetStorageType() != TextureStorageType::Buffer ||
|
||||
texture->GetTarget() != TextureTarget::TextureBuffer) {
|
||||
MGLOG_E(
|
||||
MGLOG_E_ONCE(
|
||||
"ResolveTexelBufferDescriptor: binding %u ('%s') expected texture buffer, got textureId=%u target=%d storage=%d",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
|
||||
static_cast<Int>(texture->GetTarget()), static_cast<Int>(texture->GetStorageType()));
|
||||
@@ -688,14 +690,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
|
||||
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
|
||||
if (bufferObject == nullptr) {
|
||||
MGLOG_E("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') has no GL buffer bound",
|
||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') has no GL buffer bound",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) || !slice.IsValid()) {
|
||||
MGLOG_E("ResolveTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
|
||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
|
||||
bufferObject->GetExternalIndex(), texture->GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
@@ -703,7 +705,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const auto internalFormat = textureBuffer->GetFormat();
|
||||
const VkFormat vkFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||
if (vkFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E("ResolveTexelBufferDescriptor: unsupported texture buffer internal format %d",
|
||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: unsupported texture buffer internal format %d",
|
||||
static_cast<Int>(internalFormat));
|
||||
return false;
|
||||
}
|
||||
@@ -719,7 +721,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
viewRange = (viewRange / texelSize) * texelSize;
|
||||
}
|
||||
if (viewRange == 0) {
|
||||
MGLOG_E("ResolveTexelBufferDescriptor: texture buffer %u has empty view range", texture->GetExternalIndex());
|
||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer %u has empty view range", texture->GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -733,7 +735,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkBufferView bufferView = VK_NULL_HANDLE;
|
||||
const VkResult result = vkCreateBufferView(m_device, &viewInfo, nullptr, &bufferView);
|
||||
if (result != VK_SUCCESS || bufferView == VK_NULL_HANDLE) {
|
||||
MGLOG_E("ResolveTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
|
||||
MGLOG_E_ONCE("ResolveTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
|
||||
result, static_cast<Int>(vkFormat), static_cast<SizeT>(viewRange));
|
||||
return false;
|
||||
}
|
||||
@@ -769,13 +771,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const Int location = programObj.samplerUniformLocationByBinding[binding];
|
||||
if (location < 0) {
|
||||
MGLOG_E("ResolveStorageTexelBufferDescriptor: binding %u ('%s') has no uniform location", binding,
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: binding %u ('%s') has no uniform location", binding,
|
||||
programObj.samplerNameByBinding[binding].c_str());
|
||||
return false;
|
||||
}
|
||||
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
||||
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
||||
MGLOG_E("ResolveStorageTexelBufferDescriptor: image unit %d out of range for binding %u", imageUnit,
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: image unit %d out of range for binding %u", imageUnit,
|
||||
binding);
|
||||
return false;
|
||||
}
|
||||
@@ -783,13 +785,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||
const auto& texture = imageBinding.Texture;
|
||||
if (texture == nullptr) {
|
||||
MGLOG_E("ResolveStorageTexelBufferDescriptor: image unit %d is unbound for binding %u", imageUnit,
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: image unit %d is unbound for binding %u", imageUnit,
|
||||
binding);
|
||||
return false;
|
||||
}
|
||||
if (texture->GetStorageType() != TextureStorageType::Buffer ||
|
||||
texture->GetTarget() != TextureTarget::TextureBuffer) {
|
||||
MGLOG_E("ResolveStorageTexelBufferDescriptor: binding %u ('%s') expected a texture buffer on image "
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: binding %u ('%s') expected a texture buffer on image "
|
||||
"unit %d, got textureId=%u target=%d storage=%d",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), imageUnit,
|
||||
texture->GetExternalIndex(), static_cast<Int>(texture->GetTarget()),
|
||||
@@ -800,7 +802,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto* textureBuffer = static_cast<MG_State::GLState::TextureObjectBuffer*>(texture.get());
|
||||
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
|
||||
if (bufferObject == nullptr) {
|
||||
MGLOG_E("ResolveStorageTexelBufferDescriptor: texture buffer on image unit %d has no GL buffer bound",
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: texture buffer on image unit %d has no GL buffer bound",
|
||||
imageUnit);
|
||||
return false;
|
||||
}
|
||||
@@ -819,7 +821,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) ||
|
||||
!slice.IsValid()) {
|
||||
MGLOG_E("ResolveStorageTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u",
|
||||
bufferObject->GetExternalIndex(), texture->GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
@@ -842,7 +844,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
vkFormat = resourceFormat;
|
||||
}
|
||||
if (vkFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E("ResolveStorageTexelBufferDescriptor: unsupported image buffer format (internal=%d bind=0x%x)",
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: unsupported image buffer format (internal=%d bind=0x%x)",
|
||||
static_cast<Int>(internalFormat), imageBinding.Format);
|
||||
return false;
|
||||
}
|
||||
@@ -862,7 +864,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
viewRange = (viewRange / texelSize) * texelSize;
|
||||
}
|
||||
if (viewRange == 0) {
|
||||
MGLOG_E("ResolveStorageTexelBufferDescriptor: texture buffer %u has empty view range",
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: texture buffer %u has empty view range",
|
||||
texture->GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
@@ -877,7 +879,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkBufferView bufferView = VK_NULL_HANDLE;
|
||||
const VkResult result = vkCreateBufferView(m_device, &viewInfo, nullptr, &bufferView);
|
||||
if (result != VK_SUCCESS || bufferView == VK_NULL_HANDLE) {
|
||||
MGLOG_E("ResolveStorageTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
|
||||
MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu",
|
||||
result, static_cast<Int>(vkFormat), static_cast<SizeT>(viewRange));
|
||||
return false;
|
||||
}
|
||||
@@ -914,7 +916,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto& bindingPoint = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, frontendBinding);
|
||||
const auto& bufferObject = bindingPoint.GetBoundObject();
|
||||
if (bufferObject == nullptr) {
|
||||
MGLOG_E("ResolveStorageBufferDescriptor: no SSBO bound at frontend binding %u for block '%s'",
|
||||
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: no SSBO bound at frontend binding %u for block '%s'",
|
||||
frontendBinding, programObj.storageBlockNameByBinding[binding].c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -929,7 +931,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager->AcquireResidentSlice(BufferKind::ShaderStorage, bufferObject, slice) || !slice.IsValid()) {
|
||||
MGLOG_E("ResolveStorageBufferDescriptor: failed to sync GL buffer %u for block '%s'",
|
||||
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: failed to sync GL buffer %u for block '%s'",
|
||||
bufferObject->GetExternalIndex(), programObj.storageBlockNameByBinding[binding].c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -943,7 +945,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
rangeEnd = bufferSize;
|
||||
}
|
||||
if (rangeEnd <= rangeStart) {
|
||||
MGLOG_E("ResolveStorageBufferDescriptor: empty SSBO range for block '%s'",
|
||||
MGLOG_E_ONCE("ResolveStorageBufferDescriptor: empty SSBO range for block '%s'",
|
||||
programObj.storageBlockNameByBinding[binding].c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -967,7 +969,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const Int baseLocation = programObj.samplerUniformLocationByBinding[binding];
|
||||
if (baseLocation < 0) {
|
||||
MGLOG_E("ResolveStorageImageDescriptor: storage image binding %u has no uniform location", binding);
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: storage image binding %u has no uniform location", binding);
|
||||
return false;
|
||||
}
|
||||
// Per ELEMENT, and this is where an image array differs from a storage-block array: GL
|
||||
@@ -979,26 +981,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// uniform.
|
||||
const Int location = baseLocation + static_cast<Int>(element);
|
||||
if (!program.UniformLocationsAliasSameUniform(baseLocation, location)) {
|
||||
MGLOG_E("ResolveStorageImageDescriptor: binding %u element %u is past the end of its image array",
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: binding %u element %u is past the end of its image array",
|
||||
binding, element);
|
||||
return false;
|
||||
}
|
||||
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
||||
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
||||
MGLOG_E("ResolveStorageImageDescriptor: image unit %d out of range for binding %u",
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: image unit %d out of range for binding %u",
|
||||
imageUnit, binding);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
||||
if (imageBinding.Texture == nullptr) {
|
||||
MGLOG_E("ResolveStorageImageDescriptor: image unit %d is unbound for binding %u", imageUnit, binding);
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: image unit %d is unbound for binding %u", imageUnit, binding);
|
||||
return false;
|
||||
}
|
||||
|
||||
const Bool ready = m_textureManager->TransitionTextureForStorageImage(commandBuffer, *imageBinding.Texture);
|
||||
if (!ready) {
|
||||
MGLOG_E("ResolveStorageImageDescriptor: failed to transition textureId=%d for image unit %d",
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: failed to transition textureId=%d for image unit %d",
|
||||
imageBinding.Texture->GetExternalIndex(), imageUnit);
|
||||
return false;
|
||||
}
|
||||
@@ -1018,7 +1020,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkFormat viewFormat = ResolveStorageImageViewFormat(
|
||||
reflectedFormat, imageBinding.Format, resource->format, useBindingFormat);
|
||||
if (viewFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x "
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x "
|
||||
"for binding=%u imageUnit=%d textureId=%d bindingPolicy=%s",
|
||||
imageBinding.Format, binding, imageUnit, imageBinding.Texture->GetExternalIndex(),
|
||||
useBindingFormat ? "true" : "false");
|
||||
@@ -1027,7 +1029,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkImageView view = m_textureManager->GetOrCreateStorageImageView(
|
||||
*imageBinding.Texture, mipLevel, viewFormat, imageBinding.Layered != GL_FALSE, imageBinding.Layer);
|
||||
if (view == VK_NULL_HANDLE) {
|
||||
MGLOG_E("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u "
|
||||
MGLOG_E_ONCE("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u "
|
||||
"bindingFormat=0x%x imageFormat=%d reflectedFormat=%d selectedFormat=%d bindingPolicy=%s",
|
||||
imageBinding.Texture->GetExternalIndex(), mipLevel, imageBinding.Format,
|
||||
static_cast<Int>(resource->format), static_cast<Int>(reflectedFormat),
|
||||
@@ -1048,7 +1050,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Report that there is no fallback and let the caller decline the draw - aborting the
|
||||
// process over an unbound sampler is never the right answer.
|
||||
if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) {
|
||||
MGLOG_E("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
|
||||
MGLOG_E_ONCE("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
|
||||
static_cast<Int>(target));
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1224,13 +1226,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
}
|
||||
if (binding >= programObj.samplerUniformLocationByBinding.size()) {
|
||||
MGLOG_E("CollectStorageImageTextures: binding %u has no uniform-location mapping", binding);
|
||||
MGLOG_E_ONCE("CollectStorageImageTextures: binding %u has no uniform-location mapping", binding);
|
||||
return false;
|
||||
}
|
||||
|
||||
const Int baseLocation = programObj.samplerUniformLocationByBinding[binding];
|
||||
if (baseLocation < 0) {
|
||||
MGLOG_E("CollectStorageImageTextures: binding %u has no image uniform location", binding);
|
||||
MGLOG_E_ONCE("CollectStorageImageTextures: binding %u has no image uniform location", binding);
|
||||
return false;
|
||||
}
|
||||
// Per ELEMENT, for the same reason the sampled walk above is: an image ARRAY is one
|
||||
@@ -1242,20 +1244,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
||||
const Int location = ResolveDescriptorElementLocation(program, baseLocation, element);
|
||||
if (location < 0) {
|
||||
MGLOG_E("CollectStorageImageTextures: binding %u element %u is past the end of its image array",
|
||||
MGLOG_E_ONCE("CollectStorageImageTextures: binding %u element %u is past the end of its image array",
|
||||
binding, element);
|
||||
return false;
|
||||
}
|
||||
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
||||
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
||||
MGLOG_E("CollectStorageImageTextures: image unit %d is invalid for binding %u element %u",
|
||||
MGLOG_E_ONCE("CollectStorageImageTextures: image unit %d is invalid for binding %u element %u",
|
||||
imageUnit, binding, element);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
|
||||
if (texture == nullptr) {
|
||||
MGLOG_E("CollectStorageImageTextures: image unit %d is unbound for binding %u element %u",
|
||||
MGLOG_E_ONCE("CollectStorageImageTextures: image unit %d is unbound for binding %u element %u",
|
||||
imageUnit, binding, element);
|
||||
return false;
|
||||
}
|
||||
@@ -1388,7 +1390,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool UniformManager::CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const {
|
||||
Bool UniformManager::CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const {
|
||||
outPool = VK_NULL_HANDLE;
|
||||
if (m_device == VK_NULL_HANDLE || maxSets == 0 || m_maxBindings == 0) {
|
||||
return false;
|
||||
@@ -1406,7 +1408,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Uint64 descriptorCount64 =
|
||||
static_cast<Uint64>(maxSets) * static_cast<Uint64>(std::min(m_maxBindings, kEstimatedBindingsPerSet));
|
||||
if (descriptorCount64 > static_cast<Uint64>(std::numeric_limits<Uint32>::max())) {
|
||||
MGLOG_E("UniformDescriptorBinder::CreateDescriptorPool failed: descriptorCount overflow");
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::CreateDescriptorPool failed: descriptorCount overflow");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1431,38 +1433,43 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// (OnDescriptorSetLayoutDestroyed) so program churn recycles pool capacity.
|
||||
// The cost is on set allocation only, which happens when a layout's per-frame
|
||||
// cache grows - never on the per-draw reuse path.
|
||||
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
|
||||
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT |
|
||||
(updateAfterBind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0);
|
||||
poolInfo.maxSets = maxSets;
|
||||
poolInfo.poolSizeCount = static_cast<Uint32>(std::size(poolSizes));
|
||||
poolInfo.pPoolSizes = poolSizes;
|
||||
|
||||
const VkResult result = vkCreateDescriptorPool(m_device, &poolInfo, nullptr, &outPool);
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E("UniformDescriptorBinder::CreateDescriptorPool failed: vkCreateDescriptorPool returned %d",
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::CreateDescriptorPool failed: vkCreateDescriptorPool returned %d",
|
||||
result);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool UniformManager::GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex) {
|
||||
Bool UniformManager::GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind) {
|
||||
if (frame.descriptorPools.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& currentBucket = frame.descriptorPools[frame.activeDescriptorPoolIndex];
|
||||
const Uint32 currentMaxSets = std::max<Uint32>(1, currentBucket.maxSets);
|
||||
const auto matchingBucket = std::find_if(
|
||||
frame.descriptorPools.begin(), frame.descriptorPools.end(),
|
||||
[updateAfterBind](const DescriptorPoolBucket& candidate) { return candidate.updateAfterBind == updateAfterBind; });
|
||||
const Uint32 currentMaxSets = matchingBucket != frame.descriptorPools.end()
|
||||
? std::max<Uint32>(1, matchingBucket->maxSets)
|
||||
: m_setsPerFrame;
|
||||
const Uint32 grownMaxSets = currentMaxSets <= (std::numeric_limits<Uint32>::max() / 2) ? (currentMaxSets * 2)
|
||||
: currentMaxSets;
|
||||
|
||||
VkDescriptorPool grownPool = VK_NULL_HANDLE;
|
||||
if (!CreateDescriptorPool(grownMaxSets, grownPool)) {
|
||||
MGLOG_E("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u -> %u sets)",
|
||||
if (!CreateDescriptorPool(grownMaxSets, updateAfterBind, grownPool)) {
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u -> %u sets)",
|
||||
currentMaxSets, grownMaxSets);
|
||||
return false;
|
||||
}
|
||||
|
||||
frame.descriptorPools.push_back({grownPool, grownMaxSets, 0});
|
||||
frame.descriptorPools.push_back({grownPool, grownMaxSets, 0, updateAfterBind});
|
||||
frame.activeDescriptorPoolIndex = static_cast<Uint32>(frame.descriptorPools.size() - 1);
|
||||
MGLOG_D(
|
||||
"UniformDescriptorBinder: frame %u descriptor pool exhausted, grew pool (%u -> %u sets), poolCount=%zu",
|
||||
@@ -1472,14 +1479,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkResult UniformManager::AllocateDescriptorSetsFromActivePool(Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet) {
|
||||
auto& frame = m_frames[frameIndex];
|
||||
if (frame.activeDescriptorPoolIndex >= frame.descriptorPools.size()) {
|
||||
frame.activeDescriptorPoolIndex = 0;
|
||||
}
|
||||
if (frame.descriptorPools[frame.activeDescriptorPoolIndex].allocatedSets >=
|
||||
frame.descriptorPools[frame.activeDescriptorPoolIndex].maxSets) {
|
||||
const Bool updateAfterBind = programObj.usesUpdateAfterBind;
|
||||
if (frame.activeDescriptorPoolIndex >= frame.descriptorPools.size() ||
|
||||
frame.descriptorPools[frame.activeDescriptorPoolIndex].updateAfterBind != updateAfterBind ||
|
||||
frame.descriptorPools[frame.activeDescriptorPoolIndex].allocatedSets >=
|
||||
frame.descriptorPools[frame.activeDescriptorPoolIndex].maxSets) {
|
||||
const auto availableBucket = std::find_if(
|
||||
frame.descriptorPools.begin(), frame.descriptorPools.end(),
|
||||
[](const DescriptorPoolBucket& candidate) { return candidate.allocatedSets < candidate.maxSets; });
|
||||
[updateAfterBind](const DescriptorPoolBucket& candidate) {
|
||||
return candidate.updateAfterBind == updateAfterBind && candidate.allocatedSets < candidate.maxSets;
|
||||
});
|
||||
if (availableBucket == frame.descriptorPools.end()) {
|
||||
outDescriptorSet = VK_NULL_HANDLE;
|
||||
return VK_ERROR_OUT_OF_POOL_MEMORY;
|
||||
@@ -1515,8 +1524,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
} else {
|
||||
VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
|
||||
if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) {
|
||||
if (!GrowFrameDescriptorPool(frame, frameIndex)) {
|
||||
MGLOG_E("UniformDescriptorBinder::AcquireDescriptorSet failed: descriptor pool growth failed");
|
||||
if (!GrowFrameDescriptorPool(frame, frameIndex, programObj.usesUpdateAfterBind)) {
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::AcquireDescriptorSet failed: descriptor pool growth failed");
|
||||
return allocResult;
|
||||
}
|
||||
allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
|
||||
@@ -1647,7 +1656,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
auto& frame = m_frames[frameIndex];
|
||||
if (frame.descriptorPools.empty()) {
|
||||
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: frame descriptor pools are invalid");
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::BindProgramUniformBuffers failed: frame descriptor pools are invalid");
|
||||
return false;
|
||||
}
|
||||
if (frame.activeDescriptorPoolIndex >= frame.descriptorPools.size()) {
|
||||
@@ -1784,7 +1793,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkBufferView bufferView = VK_NULL_HANDLE;
|
||||
if (!ResolveTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) ||
|
||||
bufferView == VK_NULL_HANDLE) {
|
||||
MGLOG_E(
|
||||
MGLOG_E_ONCE(
|
||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: texture buffer binding %u has no valid descriptor",
|
||||
binding);
|
||||
return false;
|
||||
@@ -1803,7 +1812,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkBufferView bufferView = VK_NULL_HANDLE;
|
||||
if (!ResolveStorageTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) ||
|
||||
bufferView == VK_NULL_HANDLE) {
|
||||
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: image buffer binding %u "
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::BindProgramUniformBuffers failed: image buffer binding %u "
|
||||
"has no valid descriptor",
|
||||
binding);
|
||||
return false;
|
||||
@@ -1823,7 +1832,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
||||
VkDescriptorBufferInfo bufferInfo{};
|
||||
if (!ResolveStorageBufferDescriptor(program, programObj, binding, element, bufferInfo)) {
|
||||
MGLOG_E(
|
||||
MGLOG_E_ONCE(
|
||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: storage buffer binding %u "
|
||||
"element %u has no valid descriptor",
|
||||
binding, element);
|
||||
@@ -1850,7 +1859,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkDescriptorImageInfo imageInfo{};
|
||||
if (!ResolveStorageImageDescriptor(commandBuffer, program, programObj, binding, element,
|
||||
imageInfo)) {
|
||||
MGLOG_E(
|
||||
MGLOG_E_ONCE(
|
||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: storage image binding %u "
|
||||
"element %u has no valid descriptor",
|
||||
binding, element);
|
||||
@@ -1892,14 +1901,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
imageInfo, samplerDescriptorsUnchangedHint);
|
||||
}
|
||||
if (!hasImage) {
|
||||
MGLOG_E(
|
||||
MGLOG_E_ONCE(
|
||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
|
||||
"has no valid texture descriptor",
|
||||
binding, element);
|
||||
return false;
|
||||
}
|
||||
if (imageInfo.sampler == VK_NULL_HANDLE || imageInfo.imageView == VK_NULL_HANDLE) {
|
||||
MGLOG_E(
|
||||
MGLOG_E_ONCE(
|
||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
|
||||
"has null sampler or imageView",
|
||||
binding, element);
|
||||
@@ -1973,7 +1982,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
} else {
|
||||
VkResult allocResult = AcquireDescriptorSet(frameIndex, programObj, descriptorSet);
|
||||
if (allocResult != VK_SUCCESS || descriptorSet == VK_NULL_HANDLE) {
|
||||
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: descriptor set acquire returned %d",
|
||||
MGLOG_E_ONCE("UniformDescriptorBinder::BindProgramUniformBuffers failed: descriptor set acquire returned %d",
|
||||
allocResult);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkDescriptorPool handle = VK_NULL_HANDLE;
|
||||
Uint32 maxSets = 0;
|
||||
Uint32 allocatedSets = 0;
|
||||
Bool updateAfterBind = false;
|
||||
};
|
||||
|
||||
// A cached descriptor set together with the pool it was allocated from, so a
|
||||
@@ -223,8 +224,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
|
||||
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
|
||||
const Vector<Uint32>& dynamicOffsets);
|
||||
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
|
||||
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
|
||||
Bool CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const;
|
||||
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind);
|
||||
VkResult AllocateDescriptorSetsFromActivePool(
|
||||
Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet);
|
||||
VkResult AcquireDescriptorSet(Uint32 frameIndex,
|
||||
@@ -341,8 +342,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// lifetime id, so a freed-and-reallocated sampler or texture at the same heap address
|
||||
// always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a
|
||||
// stale guess can only miss and fall through to the hash, never resolve wrong. Still
|
||||
// reset each frame alongside the descriptor-set cache. Indexed by binding.
|
||||
// reset each frame alongside the descriptor-set cache. Indexed by binding, but the
|
||||
// whole-descriptor entry is additionally keyed by program lifetime: Vulkan binding
|
||||
// numbers are layout-local and unrelated programs routinely reuse binding 0/1.
|
||||
struct SamplerResolveMemo {
|
||||
Uint64 infoProgramLifetimeId = 0;
|
||||
Uint64 samplerLifetimeId = 0;
|
||||
Uint64 textureLifetimeId = 0;
|
||||
VkSampler sampler = VK_NULL_HANDLE;
|
||||
|
||||
@@ -110,7 +110,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkFormat sourceVkFormat =
|
||||
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
|
||||
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
||||
MGLOG_E_ONCE("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
||||
"enabled but cannot be mapped to a VkFormat",
|
||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||
unsupportedAttribMask |= (1u << location);
|
||||
@@ -125,7 +125,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
|
||||
vkFormat = fallbackFormat;
|
||||
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
|
||||
MGLOG_W("Vertex attribute location=%u format=%d lacks "
|
||||
MGLOG_W_ONCE("Vertex attribute location=%u format=%d lacks "
|
||||
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
|
||||
"(type=%s size=%d normalized=%s integer=%s)",
|
||||
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
|
||||
@@ -135,7 +135,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (conversion == VertexStreamConversion::None) {
|
||||
MGLOG_E("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
|
||||
MGLOG_E_ONCE("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
|
||||
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists",
|
||||
location, static_cast<Int>(sourceVkFormat),
|
||||
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||
@@ -146,7 +146,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
|
||||
if (attribByteSize == 0) {
|
||||
MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
|
||||
MGLOG_E_ONCE("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
|
||||
"enabled but cannot be sized",
|
||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str());
|
||||
unsupportedAttribMask |= (1u << location);
|
||||
@@ -175,7 +175,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
|
||||
// attribute into a tightly packed transient stream without changing its format.
|
||||
conversion = VertexStreamConversion::Repack;
|
||||
MGLOG_W("Vertex attribute location=%u uses Vulkan-incompatible alignment "
|
||||
MGLOG_W_ONCE("Vertex attribute location=%u uses Vulkan-incompatible alignment "
|
||||
"(offset=%zu stride=%u required=%zu); using a tightly packed stream",
|
||||
location, attr.Offset, sourceStride, requiredAlignment);
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.requiredFlags = requiredFlags,
|
||||
});
|
||||
if (!created || resource.buffer.Map() == nullptr) {
|
||||
MGLOG_E("VkBufferManager::CreateResidentStorage failed (size=%llu)",
|
||||
MGLOG_E_ONCE("VkBufferManager::CreateResidentStorage failed (size=%llu)",
|
||||
static_cast<unsigned long long>(size));
|
||||
resource.buffer.Destroy();
|
||||
resource.storageSize = 0;
|
||||
@@ -324,7 +324,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
if (!resource.buffer.Upload(bufferObject.MappedData(), size, 0)) {
|
||||
MGLOG_E("VkBufferManager::SwapStorageAndUploadAll: upload failed");
|
||||
MGLOG_E_ONCE("VkBufferManager::SwapStorageAndUploadAll: upload failed");
|
||||
resource.pendingFullUpload = true;
|
||||
return false;
|
||||
}
|
||||
@@ -409,7 +409,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) {
|
||||
MGLOG_E("VkBufferManager::OnRespecify: in-place upload failed");
|
||||
MGLOG_E_ONCE("VkBufferManager::OnRespecify: in-place upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
}
|
||||
}
|
||||
@@ -434,7 +434,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!IsResourceBusy(*resource)) {
|
||||
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
|
||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
|
||||
MGLOG_E("VkBufferManager::OnSubData: host upload failed");
|
||||
MGLOG_E_ONCE("VkBufferManager::OnSubData: host upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
}
|
||||
return;
|
||||
@@ -471,7 +471,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if ((appAccess & BufferMappingAccessBit::Unsynchronized) || !IsResourceBusy(*resource)) {
|
||||
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
|
||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
|
||||
MGLOG_E("VkBufferManager::OnFlushMappedRange: host upload failed");
|
||||
MGLOG_E_ONCE("VkBufferManager::OnFlushMappedRange: host upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
}
|
||||
return;
|
||||
@@ -563,7 +563,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
||||
if (size == 0) {
|
||||
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
|
||||
MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -585,7 +585,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
if (!resource->buffer.Upload(bufferObject->MappedData(), size, 0)) {
|
||||
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: initial upload failed");
|
||||
MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: initial upload failed");
|
||||
resource->buffer.Destroy();
|
||||
resource->storageSize = 0;
|
||||
resource->usageFlags = 0;
|
||||
@@ -620,7 +620,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
||||
if (size == 0) {
|
||||
MGLOG_E("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
|
||||
MGLOG_E_ONCE("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkResult result =
|
||||
vmaCreateBuffer(m_allocator, &bufferInfo, &allocationInfo, &m_buffer, &m_allocation, nullptr);
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
|
||||
MGLOG_E_ONCE("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
|
||||
m_allocator = nullptr;
|
||||
m_buffer = VK_NULL_HANDLE;
|
||||
m_allocation = nullptr;
|
||||
@@ -108,7 +108,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkResult mapResult = vmaMapMemory(m_allocator, m_allocation, &m_mappedData);
|
||||
if (mapResult != VK_SUCCESS || m_mappedData == nullptr) {
|
||||
MGLOG_E("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
|
||||
MGLOG_E_ONCE("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
|
||||
m_mappedData = nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
@@ -138,14 +138,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool wasMapped = IsMapped();
|
||||
void* mapped = wasMapped ? m_mappedData : Map();
|
||||
if (mapped == nullptr) {
|
||||
MGLOG_E("VkBufferObject::Upload failed: unable to map buffer");
|
||||
MGLOG_E_ONCE("VkBufferObject::Upload failed: unable to map buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
|
||||
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
|
||||
if (flushResult != VK_SUCCESS) {
|
||||
MGLOG_E("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
|
||||
MGLOG_E_ONCE("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
|
||||
if (!wasMapped) {
|
||||
Unmap();
|
||||
}
|
||||
@@ -170,7 +170,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
|
||||
MGLOG_E_ONCE("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -123,7 +123,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (!attachment.IsComplete()) {
|
||||
MGLOG_W("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u has an incomplete texture attachment; using VK_ATTACHMENT_UNUSED",
|
||||
MGLOG_W_ONCE("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u has an incomplete texture attachment; using VK_ATTACHMENT_UNUSED",
|
||||
drawBufferIndex,
|
||||
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
||||
fbo.GetExternalIndex());
|
||||
@@ -132,7 +132,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
auto* texture = attachment.GetTexture().get();
|
||||
if (texture == nullptr) {
|
||||
MGLOG_W("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u resolved to a null texture; using VK_ATTACHMENT_UNUSED",
|
||||
MGLOG_W_ONCE("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u resolved to a null texture; using VK_ATTACHMENT_UNUSED",
|
||||
drawBufferIndex,
|
||||
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
||||
fbo.GetExternalIndex());
|
||||
@@ -311,7 +311,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
if (!TryResolveSampleCountFlagBits(renderbuffer->GetSamples(), sampleCount)) {
|
||||
MGLOG_E("GetOrCreateRenderbufferResource: unsupported renderbuffer sample count %d for renderbuffer %u",
|
||||
MGLOG_E_ONCE("GetOrCreateRenderbufferResource: unsupported renderbuffer sample count %d for renderbuffer %u",
|
||||
renderbuffer->GetSamples(),
|
||||
renderbuffer->GetExternalIndex());
|
||||
return nullptr;
|
||||
@@ -457,7 +457,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, imageInfo.flags,
|
||||
&imageFormatProperties);
|
||||
if (imageFormatResult != VK_SUCCESS || (imageFormatProperties.sampleCounts & sampleCount) == 0) {
|
||||
MGLOG_E("GetOrCreateRenderbufferResource: unsupported renderbuffer format=%d samples=%d for renderbuffer %u",
|
||||
MGLOG_E_ONCE("GetOrCreateRenderbufferResource: unsupported renderbuffer format=%d samples=%d for renderbuffer %u",
|
||||
static_cast<Int>(format),
|
||||
static_cast<Int>(sampleCount),
|
||||
renderbuffer->GetExternalIndex());
|
||||
@@ -929,7 +929,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const auto& renderbuffer = rbAtt.GetRenderbuffer();
|
||||
auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer);
|
||||
if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
|
||||
MGLOG_E("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
|
||||
"renderbuffer %u; using VK_ATTACHMENT_UNUSED",
|
||||
i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex());
|
||||
continue;
|
||||
@@ -1105,7 +1105,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex());
|
||||
|
||||
if (!hasClear && trackedColorLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
MGLOG_W("GetOrCreateRenderPass: color attachment textureId=%d starts with undefined layout and no clear; "
|
||||
MGLOG_W_ONCE("GetOrCreateRenderPass: color attachment textureId=%d starts with undefined layout and no clear; "
|
||||
"using LOAD_OP_DONT_CARE",
|
||||
texture->GetExternalIndex());
|
||||
desc.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
@@ -1161,7 +1161,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
|
||||
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
|
||||
if (hasDistinctDepthAndStencilAttachments) {
|
||||
MGLOG_E("GetOrCreateRenderPass: separate depth/stencil attachments are not supported yet; using the depth attachment and ignoring the standalone stencil attachment for framebuffer %u",
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: separate depth/stencil attachments are not supported yet; using the depth attachment and ignoring the standalone stencil attachment for framebuffer %u",
|
||||
fbo.GetExternalIndex());
|
||||
}
|
||||
if (selectedDepthStencilAttachment != nullptr) {
|
||||
@@ -1223,7 +1223,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
depthAttachmentDescription.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
depthAttachmentDescription.initialLayout = loadInfo.initialLayout;
|
||||
if (trackedDepthLayout == VK_IMAGE_LAYOUT_UNDEFINED && (!clearDepth || !clearStencil)) {
|
||||
MGLOG_W("GetOrCreateRenderPass: depth/stencil attachment id=%d starts with undefined layout "
|
||||
MGLOG_W_ONCE("GetOrCreateRenderPass: depth/stencil attachment id=%d starts with undefined layout "
|
||||
"and partial/no clear; using DONT_CARE for uncleared aspects",
|
||||
depthAttachmentId);
|
||||
}
|
||||
|
||||
@@ -300,8 +300,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, newResource.image, newResource.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
0, VK_ACCESS_TRANSFER_WRITE_BIT, newResource.aspect, 0, newResource.mipLevels,
|
||||
newResource.arrayLayers);
|
||||
0, VK_ACCESS_TRANSFER_WRITE_BIT, newResource.aspect, 0, newResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare destination image");
|
||||
|
||||
VkImageLayout srcTrackedLayout = oldResource.layout;
|
||||
@@ -311,8 +310,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, oldResource.image, srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels,
|
||||
oldResource.arrayLayers);
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels);
|
||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare source image");
|
||||
|
||||
Vector<VkImageCopy> copyRegions;
|
||||
@@ -344,8 +342,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, newResource.image, newResource.layout, oldResource.layout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels,
|
||||
newResource.arrayLayers);
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to restore destination layout");
|
||||
|
||||
VK_VERIFY(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer(texture preserve)");
|
||||
@@ -975,13 +972,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return resource->sampledView;
|
||||
}
|
||||
if (!AreSampledImageViewFormatsCompatible(resource->format, format)) {
|
||||
MGLOG_E("%s: incompatible sampled image view format=%d for textureId=%d imageFormat=%d",
|
||||
MGLOG_E_ONCE("%s: incompatible sampled image view format=%d for textureId=%d imageFormat=%d",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||
static_cast<Int>(resource->format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||
MGLOG_E("%s: textureId=%d needs mutable image format=%d for sampled view format=%d",
|
||||
MGLOG_E_ONCE("%s: textureId=%d needs mutable image format=%d for sampled view format=%d",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
@@ -1001,7 +998,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||
if ((formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0) {
|
||||
MGLOG_E("%s: sampled image view format=%d lacks VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT "
|
||||
MGLOG_E_ONCE("%s: sampled image view format=%d lacks VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT "
|
||||
"for textureId=%d (available=0x%x)",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||
static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
||||
@@ -1015,7 +1012,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource->sampledBaseMipLevel, resource->sampledLevelCount, 0, resource->arrayLayers,
|
||||
&sampledComponents, VK_IMAGE_USAGE_SAMPLED_BIT);
|
||||
if (view == VK_NULL_HANDLE) {
|
||||
MGLOG_E("%s: failed to create sampled image view textureId=%d imageFormat=%d viewFormat=%d",
|
||||
MGLOG_E_ONCE("%s: failed to create sampled image view textureId=%d imageFormat=%d viewFormat=%d",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
@@ -1043,14 +1040,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
format = resource->format;
|
||||
}
|
||||
if (!AreStorageImageViewFormatsCompatible(resource->format, format)) {
|
||||
MGLOG_E("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d",
|
||||
MGLOG_E_ONCE("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
|
||||
static_cast<Int>(resource->format));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
if (format != resource->format &&
|
||||
(resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
||||
MGLOG_E("%s: textureId=%d needs mutable image format=%d for storage view format=%d",
|
||||
MGLOG_E_ONCE("%s: textureId=%d needs mutable image format=%d for storage view format=%d",
|
||||
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
@@ -1070,7 +1067,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
break;
|
||||
case VK_IMAGE_VIEW_TYPE_3D:
|
||||
MGLOG_E("%s: non-layered 3D storage views are unsupported for textureId=%d",
|
||||
MGLOG_E_ONCE("%s: non-layered 3D storage views are unsupported for textureId=%d",
|
||||
__func__, texture.GetExternalIndex());
|
||||
return VK_NULL_HANDLE;
|
||||
default:
|
||||
@@ -1079,7 +1076,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
if (viewType != resource->viewType) {
|
||||
if (layer < 0 || static_cast<Uint32>(layer) >= resource->arrayLayers) {
|
||||
MGLOG_E("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u",
|
||||
MGLOG_E_ONCE("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u",
|
||||
__func__, layer, texture.GetExternalIndex(), resource->arrayLayers);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
@@ -1114,7 +1111,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||
if ((formatProperties.optimalTilingFeatures & requiredFormatFeatures) != requiredFormatFeatures) {
|
||||
MGLOG_E("%s: storage image view format=%d lacks required features=0x%x for textureId=%d "
|
||||
MGLOG_E_ONCE("%s: storage image view format=%d lacks required features=0x%x for textureId=%d "
|
||||
"(available=0x%x)",
|
||||
__func__, static_cast<Int>(format), static_cast<Uint32>(requiredFormatFeatures),
|
||||
texture.GetExternalIndex(), static_cast<Uint32>(formatProperties.optimalTilingFeatures));
|
||||
@@ -1125,7 +1122,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
mipLevel, 1, baseArrayLayer, layerCount, nullptr,
|
||||
VK_IMAGE_USAGE_STORAGE_BIT);
|
||||
if (view == VK_NULL_HANDLE) {
|
||||
MGLOG_E("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d",
|
||||
MGLOG_E_ONCE("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d",
|
||||
__func__, texture.GetExternalIndex(), mipLevel, static_cast<Int>(resource->format),
|
||||
static_cast<Int>(format));
|
||||
return VK_NULL_HANDLE;
|
||||
@@ -1191,7 +1188,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool lowerTransitioned = TransitionImageLayout(
|
||||
commandBuffer, resource.image, lowerMipLayout, newLayout,
|
||||
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
||||
resource.aspect, 0, writtenMipLevel, resource.arrayLayers);
|
||||
resource.aspect, 0, writtenMipLevel);
|
||||
MOBILEGL_ASSERT(lowerTransitioned,
|
||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition lower mip levels for textureId=%d",
|
||||
texture->GetExternalIndex());
|
||||
@@ -1203,8 +1200,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool upperTransitioned = TransitionImageLayout(
|
||||
commandBuffer, resource.image, upperMipLayout, newLayout,
|
||||
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
||||
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel,
|
||||
resource.arrayLayers);
|
||||
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel);
|
||||
MOBILEGL_ASSERT(upperTransitioned,
|
||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition upper mip levels for textureId=%d",
|
||||
texture->GetExternalIndex());
|
||||
@@ -1223,7 +1219,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
if (resource->layout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
MGLOG_W("TransitionTextureForSampling: textureId=%d is still in VK_IMAGE_LAYOUT_UNDEFINED before sampling",
|
||||
MGLOG_W_ONCE("TransitionTextureForSampling: textureId=%d is still in VK_IMAGE_LAYOUT_UNDEFINED before sampling",
|
||||
texture.GetExternalIndex());
|
||||
}
|
||||
|
||||
@@ -1257,8 +1253,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
|
||||
s_sampledReadStages, srcAccessMask,
|
||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
|
||||
resource->arrayLayers);
|
||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||
StampResourceRecordingUse(*resource);
|
||||
@@ -1288,7 +1283,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_IMAGE_LAYOUT_GENERAL, srcStageMask,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, srcAccessMask,
|
||||
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
||||
resource->aspect, 0, resource->mipLevels, resource->arrayLayers);
|
||||
resource->aspect, 0, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
|
||||
texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||
@@ -1355,8 +1350,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageLayout& trackedLayout, VkImageLayout newLayout,
|
||||
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
|
||||
VkAccessFlags srcAccessMask, VkAccessFlags dstAccessMask,
|
||||
VkImageAspectFlags aspectMask, Uint32 baseMipLevel, Uint32 levelCount,
|
||||
Uint32 layerCount) {
|
||||
VkImageAspectFlags aspectMask, Uint32 baseMipLevel,
|
||||
Uint32 levelCount) {
|
||||
MOBILEGL_ASSERT(image != VK_NULL_HANDLE, "TransitionImageLayout: m_image == VK_NULL_HANDLE");
|
||||
MOBILEGL_ASSERT(!((dstAccessMask & VK_ACCESS_TRANSFER_READ_BIT) != 0 &&
|
||||
(dstStageMask & VK_PIPELINE_STAGE_TRANSFER_BIT) == 0),
|
||||
@@ -1381,7 +1376,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
barrier.subresourceRange.baseMipLevel = baseMipLevel;
|
||||
barrier.subresourceRange.levelCount = levelCount;
|
||||
barrier.subresourceRange.baseArrayLayer = 0;
|
||||
barrier.subresourceRange.layerCount = layerCount;
|
||||
// Every layer, always - see the declaration for why layout tracking leaves no other
|
||||
// correct answer. VK_REMAINING_ARRAY_LAYERS rather than the image's own `arrayLayers`
|
||||
// because those are not the same number for a 3D image: MobileGL creates 3D images
|
||||
// 2D_ARRAY_COMPATIBLE and their arrayLayers is 1, which today Vulkan reads as "all depth
|
||||
// slices" but will read as "depth slice 0" once VK_KHR_maintenance9 is enabled. The
|
||||
// validation layer warns about that literal 1 by name.
|
||||
barrier.subresourceRange.layerCount = VK_REMAINING_ARRAY_LAYERS;
|
||||
vkCmdPipelineBarrier(commandBuffer, srcStageMask, dstStageMask, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
|
||||
trackedLayout = newLayout;
|
||||
@@ -1574,7 +1575,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// targets this manager has no Vulkan image shape for yet (cube map arrays above all).
|
||||
// Declining the sync leaves the texture unbacked - wrong, but recoverable - where an
|
||||
// assertion would take the whole process down instead.
|
||||
MGLOG_W("SyncTextureResource: unsupported uploadTarget=%s textureTarget=%s textureId=%d size=(%d,%d,%d) "
|
||||
MGLOG_W_ONCE("SyncTextureResource: unsupported uploadTarget=%s textureTarget=%s textureId=%d size=(%d,%d,%d) "
|
||||
"mipLevels=%u vkViewType=%d",
|
||||
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
||||
MG_Util::ConvertTextureTargetToString(texture.GetTarget()).c_str(), texture.GetExternalIndex(),
|
||||
@@ -1803,7 +1804,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Losing reinterpreted views only degrades the formatless-image feature for
|
||||
// this texture; failing creation would lose the texture entirely, so retry
|
||||
// as a plain immutable-format image.
|
||||
MGLOG_W("%s: mutable image format=%d is unsupported for textureId=%d; creating "
|
||||
MGLOG_W_ONCE("%s: mutable image format=%d is unsupported for textureId=%d; creating "
|
||||
"without VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT (format reinterpretation "
|
||||
"will be unavailable for it)",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
||||
@@ -1821,7 +1822,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Losing 2D-array compatibility only costs per-slice framebuffer attachment for this
|
||||
// format; failing creation would lose the texture entirely. Remembered so later syncs
|
||||
// neither reprobe nor flag-mismatch against this image and recreate it.
|
||||
MGLOG_W("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
|
||||
MGLOG_W_ONCE("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
|
||||
"textureId=%d; creating without it (per-slice framebuffer attachment will be "
|
||||
"unavailable for it)",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
||||
@@ -1853,7 +1854,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkResult createImageResult =
|
||||
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr);
|
||||
if (createImageResult != VK_SUCCESS) {
|
||||
MGLOG_F("SyncTextureResource: vmaCreateImage failed (%d) textureId=%d extent=%ux%u depth=%u layers=%u "
|
||||
// E_ONCE, not F: the comment above says it - this is a soft failure the caller
|
||||
// recovers from, and it re-fires on every sync of every texture the driver refuses.
|
||||
MGLOG_E_ONCE("SyncTextureResource: vmaCreateImage failed (%d) textureId=%d extent=%ux%u depth=%u layers=%u "
|
||||
"mips=%u samples=%d format=%d",
|
||||
createImageResult, texture.GetExternalIndex(), imageInfo.extent.width, imageInfo.extent.height,
|
||||
imageInfo.extent.depth, imageInfo.arrayLayers, imageInfo.mipLevels,
|
||||
@@ -2426,7 +2429,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool srcIsD24S8 = outResource.format == VK_FORMAT_D24_UNORM_S8_UINT;
|
||||
const Bool srcIsD32FS8 = outResource.format == VK_FORMAT_D32_SFLOAT_S8_UINT;
|
||||
if (!srcIsD24S8 && !srcIsD32FS8) {
|
||||
MGLOG_E("UploadDirtyMipLevels: unsupported combined depth-stencil format %d for textureId=%d",
|
||||
MGLOG_E_ONCE("UploadDirtyMipLevels: unsupported combined depth-stencil format %d for textureId=%d",
|
||||
static_cast<Int>(outResource.format), mipmapTexture.GetExternalIndex());
|
||||
for (const auto& item : uploadItems) {
|
||||
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
|
||||
@@ -2603,7 +2606,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
uploadSrcAccessMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
||||
aspectMask, 0, outResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionImageLayout to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL failed");
|
||||
|
||||
// Array textures keep their GL "depth" in VkImage array layers, so the
|
||||
@@ -2707,7 +2710,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
s_sampledReadStages,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
VK_ACCESS_SHADER_READ_BIT,
|
||||
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
||||
aspectMask, 0, outResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionImageLayout to sampled read-only layout failed");
|
||||
outResource.layout = finalLayout;
|
||||
|
||||
|
||||
@@ -388,12 +388,24 @@ public:
|
||||
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||
|
||||
// Moves `image` to `newLayout` and writes the new layout back through `trackedLayout`.
|
||||
//
|
||||
// The barrier covers EVERY array layer of the image, and there is deliberately no layer
|
||||
// parameter to say otherwise: layout here is tracked per IMAGE (one `TextureResource::layout`,
|
||||
// or one caller-owned variable), so a barrier narrower than the image would leave the layers it
|
||||
// skipped in the old layout while the tracker claims they moved. Every transfer against a
|
||||
// framebuffer attachment above layer 0 - glReadPixels, glBlitFramebuffer, glCopyTexSubImage,
|
||||
// glCopyImageSubData - then ran its copy on a layer no barrier had transitioned.
|
||||
//
|
||||
// The mip range IS a parameter, because mip levels really are transitioned piecewise (see
|
||||
// UpdateTrackedImageLayoutAfterAttachmentWrite and the mipmap generation loops): those callers
|
||||
// move the complement of the level they wrote so the whole image converges on one layout again.
|
||||
// Nothing does, or can, do that per layer.
|
||||
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
|
||||
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
|
||||
VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask,
|
||||
VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask,
|
||||
Uint32 baseMipLevel = 0, Uint32 levelCount = 1,
|
||||
Uint32 layerCount = 1);
|
||||
Uint32 baseMipLevel = 0, Uint32 levelCount = 1);
|
||||
|
||||
SizeT CollectGarbage();
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(initInfo.device != VK_NULL_HANDLE, "VkTimerQueryManager::Initialize requires valid VkDevice");
|
||||
MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkTimerQueryManager::Initialize requires non-zero frame count");
|
||||
if (initInfo.timestampValidBits == 0 || initInfo.timestampPeriodNs <= 0.0f || initInfo.slotsPerPool == 0) {
|
||||
MGLOG_W("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
|
||||
MGLOG_W_ONCE("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
|
||||
initInfo.timestampValidBits, initInfo.timestampPeriodNs, initInfo.slotsPerPool);
|
||||
return false;
|
||||
}
|
||||
@@ -35,7 +35,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
for (auto& poolState : m_pools) {
|
||||
const VkResult result = vkCreateQueryPool(m_device, &poolInfo, nullptr, &poolState.pool);
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
|
||||
MGLOG_E_ONCE("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
@@ -90,7 +90,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto& poolState = m_pools[frameIndex];
|
||||
if (poolState.cursor >= m_slotsPerPool) {
|
||||
if (!poolState.exhaustionWarned) {
|
||||
MGLOG_W("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
|
||||
MGLOG_W_ONCE("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
|
||||
"this frame fall back to the frontend path",
|
||||
frameIndex, m_slotsPerPool);
|
||||
poolState.exhaustionWarned = true;
|
||||
@@ -120,7 +120,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_device, m_pools[record.poolIndex].pool, record.slot, 1, sizeof(resultWithAvailability),
|
||||
resultWithAvailability, sizeof(Uint64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
|
||||
if (result != VK_SUCCESS && result != VK_NOT_READY) {
|
||||
MGLOG_E("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
|
||||
MGLOG_E_ONCE("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
|
||||
return false;
|
||||
}
|
||||
if (resultWithAvailability[1] == 0) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -229,6 +229,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
|
||||
VkImageLayout dstRestoreLayout, Bool stencilAspect);
|
||||
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
|
||||
// Map a GL bottom-left-origin rectangle into the display-oriented swapchain image.
|
||||
// Quarter-turn surface transforms swap the copy extent's axes.
|
||||
static Bool MapDefaultFramebufferReadbackRect(GLint x, GLint y, GLsizei width, GLsizei height,
|
||||
VkExtent2D imageExtent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||
VkOffset2D* imageOffset, VkExtent2D* imageCopyExtent);
|
||||
// Reorder a tightly packed block copied with MapDefaultFramebufferReadbackRect back into
|
||||
// GL row order. The input block has swapped dimensions for 90/270 degree transforms.
|
||||
static Bool RemapDefaultFramebufferReadback(const Uint8* rawPixels, Uint32 logicalWidth,
|
||||
Uint32 logicalHeight,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||
SizeT texelSize, Uint8* outPixels);
|
||||
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
||||
GLsizei width, GLsizei height, GLenum destinationFormat,
|
||||
GLenum destinationType, SizeT destinationRowStride,
|
||||
@@ -298,6 +310,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
|
||||
// honored rather than accepted-and-ignored.
|
||||
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
|
||||
// ARB_base_instance extends indirect command records with a non-zero firstInstance and
|
||||
// requires gl_InstanceID to remain zero-based. Vulkan needs both features to honor that
|
||||
// complete contract: one legalizes the command word, the other enables the shader rebase.
|
||||
Bool IsNonZeroIndirectBaseInstanceSupported() const {
|
||||
return m_drawIndirectFirstInstanceFeatureEnabled && m_shaderDrawParametersFeatureEnabled;
|
||||
}
|
||||
// Ensures the frame command buffer is recording (same lazy pattern as
|
||||
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
|
||||
// frame's pool. Null when unsupported or the pool is exhausted.
|
||||
@@ -537,6 +555,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool m_shaderDrawParametersExtensionEnabled = false;
|
||||
Bool m_shaderDrawParametersFeatureEnabled = false;
|
||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||
// Set only after descriptor-indexing feature AND property queries prove that
|
||||
// update-after-bind is legal for every descriptor category this renderer emits.
|
||||
ProgramFactory::UpdateAfterBindLimits m_updateAfterBindLimits{};
|
||||
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
|
||||
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
|
||||
// drive a runtime fallback when the device lacks them.
|
||||
@@ -547,6 +568,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
|
||||
Bool m_dualSrcBlendFeatureEnabled = false;
|
||||
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
|
||||
// multiViewport gates rasterizing into more than one of ARB_viewport_array's 16 viewports
|
||||
// (gl_ViewportIndex). m_maxRasterizableViewports is min(MAX_VIEWPORTS, device limit), or 1
|
||||
// when the feature is off, and is the viewportCount a gl_ViewportIndex-writing pipeline
|
||||
// declares - it is NOT what GL_MAX_VIEWPORTS reports, which is the frontend state width.
|
||||
Bool m_multiViewportFeatureEnabled = false;
|
||||
Uint32 m_maxRasterizableViewports = 1;
|
||||
// Union of shader stages sampled-read barriers may name; built at device creation
|
||||
// because geometry/tessellation stage bits are invalid in a barrier when their
|
||||
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
|
||||
@@ -830,6 +857,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// re-resolve just the pipeline against the active pass; a change that
|
||||
// flips it must fall back to the full path's pass selection.
|
||||
Bool drawUsesDepthStencil = false;
|
||||
// The snapshotting draw's pipeline viewportCount. A pure function of the PROGRAM
|
||||
// (writesViewportIndexBuiltin) and of a device feature fixed at renderer init, both
|
||||
// of which the programLifetimeId/programVersion guards above already pin - carried
|
||||
// here so the fast path does not re-fetch the program object to re-derive it.
|
||||
Uint32 viewportCount = 1;
|
||||
IntVec2 renderPassExtent = {0, 0};
|
||||
// colorAttachmentCount of the snapshotting draw's render pass: the
|
||||
// pipeline-state hash input, so the fast path can refresh that hash and
|
||||
@@ -1120,7 +1152,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
|
||||
// bias, line width, stencil), gated behind one render-state-parameters-version
|
||||
// compare per command buffer - see the gate fields in DynamicStateShadow.
|
||||
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo);
|
||||
// viewportCount is the bound pipeline's declared viewport count: 1 for every program that
|
||||
// does not write gl_ViewportIndex (the memoized fast path), otherwise the renderer's
|
||||
// rasterizable viewport count, which takes the unmemoized array path.
|
||||
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo,
|
||||
Uint32 viewportCount = 1);
|
||||
void ApplyMultiViewportDynamicState(VkCommandBuffer commandBuffer, Uint32 viewportCount, const IntVec2& extent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo);
|
||||
VkRect2D ComputeGLScissorRect(Uint32 index, const IntVec2& extent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo) const;
|
||||
// How many viewports a draw with this program rasterizes into: 1 unless the program
|
||||
// assigns gl_ViewportIndex AND the device enabled multiViewport. Both the pipeline's
|
||||
// baked viewportCount and the dynamic arrays come from this one answer, so they cannot
|
||||
// disagree.
|
||||
Uint32 ResolveDrawViewportCount(Bool programWritesViewportIndex) const {
|
||||
return programWritesViewportIndex && m_multiViewportFeatureEnabled ? m_maxRasterizableViewports : 1u;
|
||||
}
|
||||
|
||||
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
|
||||
@@ -74,6 +74,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The context line (__VA_ARGS__ = its own format string + args) must be a SEPARATE log
|
||||
// call: appending its format to the base format while its arguments precede the base
|
||||
// arguments makes every conversion read the wrong slot (a %s pulling an int crashes).
|
||||
//
|
||||
// MGLOG_F and deliberately NOT latched. VK_VERIFY is the invariant-check macro: a Vulkan call
|
||||
// MobileGL believes it has already made legal came back non-success, which is a
|
||||
// should-never-happen state, not an expected failure mode a user hits. Those fast-fail loudly
|
||||
// and keep saying so - the log-quietness rules that latch W/E cover expected failures (driver
|
||||
// capability gaps, app misuse), not broken internal invariants. MOBILEGL_ASSERT below traps in
|
||||
// a DEBUG build; MGLOG_F is what makes the same condition visible in an INFO test run, where
|
||||
// the assert is compiled out by contract.
|
||||
//
|
||||
// A soft, recoverable failure must therefore NOT be routed through VK_VERIFY. Check the
|
||||
// VkResult directly and report it with MGLOG_E_ONCE - see VkTextureManager::SyncTextureResource,
|
||||
// where a driver legitimately refuses an image the format pre-check accepted.
|
||||
#define VK_VERIFY(expr, ...) \
|
||||
do { \
|
||||
VkResult _vk_verify_result = (expr); \
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
#include <Config.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
#include <MG_Util/Converters/MGToStr/GLExtensionConverter.h>
|
||||
#if defined(MOBILEGL_ENABLE_DILIGENT)
|
||||
#include <MG_Backend/Diligent/BackendObject_Diligent.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
void LogBackendInfo() {
|
||||
@@ -55,6 +58,11 @@ namespace MobileGL::MG_Backend {
|
||||
case BackendType::DirectVulkan:
|
||||
pActiveBackendObject = MakeUnique<DirectVulkan::BackendObject_DirectVulkan>();
|
||||
break;
|
||||
#if defined(MOBILEGL_ENABLE_DILIGENT)
|
||||
case BackendType::DiligentVulkan:
|
||||
pActiveBackendObject = MakeUnique<DiligentBackend::BackendObject_Diligent>();
|
||||
break;
|
||||
#endif
|
||||
case BackendType::Unknown:
|
||||
default:
|
||||
MGLOG_W("Unknown backend type, defaulting to unknown backend");
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
EGLStateContext* GetState() {
|
||||
if (!MG_State::pEGLContext) {
|
||||
MGLOG_E("pEGLContext is null. MG_State may not be initialized.");
|
||||
MGLOG_E_ONCE("pEGLContext is null. MG_State may not be initialized.");
|
||||
}
|
||||
return MG_State::pEGLContext.get();
|
||||
}
|
||||
@@ -146,7 +146,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject) {
|
||||
MGLOG_E("activeBackendObject not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
state->DestroySurface(dpy, surface);
|
||||
return EGL_NO_SURFACE;
|
||||
}
|
||||
@@ -172,11 +172,11 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject) {
|
||||
MGLOG_E("activeBackendObject not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
return EGL_FALSE;
|
||||
}
|
||||
if (!backendObject->SwapEGLBuffers(dpy, draw)) {
|
||||
MGLOG_E("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
|
||||
MGLOG_E_ONCE("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
|
||||
state->SetError(EGL_BAD_SURFACE);
|
||||
return EGL_FALSE;
|
||||
}
|
||||
@@ -211,7 +211,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject) {
|
||||
MGLOG_E("activeBackendObject not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
return EGL_FALSE;
|
||||
}
|
||||
if (!backendObject->InitializeEGLDisplay(dpy, major, minor)) {
|
||||
@@ -265,7 +265,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
if (releaseCurrentRequest) {
|
||||
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
|
||||
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
|
||||
MGLOG_E("eglMakeCurrent release failed in backend thread=%s", threadId.c_str());
|
||||
MGLOG_E_ONCE("eglMakeCurrent release failed in backend thread=%s", threadId.c_str());
|
||||
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
||||
state->SetError(EGL_BAD_ACCESS);
|
||||
return EGL_FALSE;
|
||||
@@ -277,12 +277,12 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject) {
|
||||
MGLOG_E("activeBackendObject not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
||||
return EGL_FALSE;
|
||||
}
|
||||
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
|
||||
MGLOG_E("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(),
|
||||
MGLOG_E_ONCE("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(),
|
||||
dpy, draw, read, ctx);
|
||||
state->SetError(EGL_BAD_ACCESS);
|
||||
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
|
||||
@@ -703,7 +703,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject) {
|
||||
MGLOG_E("activeBackendObject not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
state->DestroySurface(dpy, surface);
|
||||
return EGL_NO_SURFACE;
|
||||
}
|
||||
@@ -726,7 +726,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
}
|
||||
auto* backendObject = GetBackendObject(state);
|
||||
if (!backendObject) {
|
||||
MGLOG_E("activeBackendObject not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject not initialized!");
|
||||
return EGL_FALSE;
|
||||
}
|
||||
width = std::max<EGLint>(width, 1);
|
||||
@@ -764,7 +764,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
MGLOG_D("eglGetProcAddress(%s)", name);
|
||||
void* proc = MG_Impl::GetProcAddress(name);
|
||||
if (!proc) {
|
||||
MGLOG_W("Failed to get function: %s", name);
|
||||
MGLOG_D("Failed to get function: %s", name);
|
||||
return nullptr;
|
||||
}
|
||||
return (__eglMustCastToProperFunctionPointerType)proc;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
|
||||
#include <MG_Util/Texture/PixelStoreProcessor.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
@@ -31,6 +32,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
NamedBufferData,
|
||||
NamedBufferSubData,
|
||||
CopyNamedBufferSubData,
|
||||
ClearBufferData,
|
||||
ClearBufferSubData,
|
||||
ClearNamedBufferData,
|
||||
ClearNamedBufferSubData,
|
||||
MapBufferRange,
|
||||
@@ -65,6 +68,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return "NamedBufferSubData";
|
||||
case BufferOp::CopyNamedBufferSubData:
|
||||
return "CopyNamedBufferSubData";
|
||||
case BufferOp::ClearBufferData:
|
||||
return "ClearBufferData";
|
||||
case BufferOp::ClearBufferSubData:
|
||||
return "ClearBufferSubData";
|
||||
case BufferOp::ClearNamedBufferData:
|
||||
return "ClearNamedBufferData";
|
||||
case BufferOp::ClearNamedBufferSubData:
|
||||
@@ -143,16 +150,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The pattern is replicated verbatim, which is only the whole story while the client
|
||||
// layout already matches the internal format - the case every entry point in practice
|
||||
// uses, and the only one the conversion machinery here can express. Say so rather than
|
||||
// quietly writing a differently-sized pattern.
|
||||
const SizeT sourceSize = MG_Util::GetInputBytesPerPixel(inputFormat, pixelType);
|
||||
if (sourceSize != elementSize) {
|
||||
MGLOG_W("%s: clear pattern is %zu bytes but internalformat 0x%X stores %zu; "
|
||||
"converting between them is not implemented",
|
||||
GetBufferOpName(op), sourceSize, internalformat, elementSize);
|
||||
}
|
||||
return elementSize;
|
||||
}
|
||||
|
||||
@@ -194,27 +191,59 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClearNamedBufferRange_State(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size,
|
||||
GLenum format, GLenum type, const void* data, BufferOp op) {
|
||||
Bool BuildClearPattern(GLenum internalformat, GLenum format, GLenum type, const void* data,
|
||||
SizeT patternSize, BufferOp op, Vector<Uint8>& pattern) {
|
||||
const TextureInternalFormat internal = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||
const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
|
||||
const TexturePixelDataType inputType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
|
||||
|
||||
Vector<Uint8> zeroInput;
|
||||
const void* inputPixel = data;
|
||||
if (inputPixel == nullptr) {
|
||||
const SizeT inputSize = MG_Util::GetInputBytesPerPixel(inputFormat, inputType);
|
||||
if (inputSize == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
|
||||
"format and type do not describe a source pixel."));
|
||||
return false;
|
||||
}
|
||||
zeroInput.resize(inputSize);
|
||||
inputPixel = zeroInput.data();
|
||||
}
|
||||
|
||||
if (!MG_Util::PixelStoreProcessor::ConvertOnePixelToInternal(
|
||||
internal, inputFormat, inputType, inputPixel, pattern)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", GetBufferOpName(op),
|
||||
std::format("Cannot convert one ({}, {}) pixel into internalformat 0x{:X}.",
|
||||
MG_Util::ConvertGLEnumToString(format), MG_Util::ConvertGLEnumToString(type),
|
||||
internalformat)));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data == nullptr) {
|
||||
// GL defines a null clear value as all zero bits in the destination store, while
|
||||
// retaining the format/type validation above.
|
||||
pattern.assign(patternSize, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClearBufferRange_State(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
|
||||
GLenum internalformat, GLintptr offset, GLsizeiptr size,
|
||||
GLenum format, GLenum type, const void* data, BufferOp op) {
|
||||
const SizeT patternSize = GetClearPatternSize(internalformat, format, type, op);
|
||||
if (patternSize == 0) return;
|
||||
|
||||
auto bufferObject = GetNamedBufferObject(buffer, op);
|
||||
if (!bufferObject) return;
|
||||
if (!ValidateBufferClearRange(bufferObject, offset, size, patternSize, op)) return;
|
||||
if (size == 0) return;
|
||||
|
||||
Vector<Uint8> clearData(static_cast<SizeT>(size));
|
||||
if (data) {
|
||||
const auto* pattern = static_cast<const Uint8*>(data);
|
||||
for (SizeT at = 0; at < clearData.size(); at += patternSize) {
|
||||
Memcpy(clearData.data() + at, pattern, patternSize);
|
||||
}
|
||||
} else {
|
||||
Memset(clearData.data(), 0, clearData.size());
|
||||
}
|
||||
|
||||
bufferObject->UploadSubData({clearData.data(), clearData.size()}, static_cast<SizeT>(offset));
|
||||
Vector<Uint8> pattern;
|
||||
if (!BuildClearPattern(internalformat, format, type, data, patternSize, op, pattern)) return;
|
||||
bufferObject->FillSubData({pattern.data(), pattern.size()}, static_cast<SizeT>(offset),
|
||||
static_cast<SizeT>(size));
|
||||
}
|
||||
|
||||
auto& GetBufferBindingSlot(BufferTarget target) {
|
||||
@@ -1197,17 +1226,34 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
static_cast<SizeT>(writeOffset), static_cast<SizeT>(size));
|
||||
}
|
||||
|
||||
void ClearBufferData_State(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
||||
auto bufferObject = GetBoundBufferObject(target, BufferOp::ClearBufferData);
|
||||
if (!bufferObject) return;
|
||||
ClearBufferRange_State(bufferObject, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
|
||||
type, data, BufferOp::ClearBufferData);
|
||||
}
|
||||
|
||||
void ClearBufferSubData_State(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size,
|
||||
GLenum format, GLenum type, const void* data) {
|
||||
auto bufferObject = GetBoundBufferObject(target, BufferOp::ClearBufferSubData);
|
||||
if (!bufferObject) return;
|
||||
ClearBufferRange_State(bufferObject, internalformat, offset, size, format, type, data,
|
||||
BufferOp::ClearBufferSubData);
|
||||
}
|
||||
|
||||
void ClearNamedBufferData_State(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
||||
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferData);
|
||||
if (!bufferObject) return;
|
||||
ClearNamedBufferRange_State(buffer, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
|
||||
type, data, BufferOp::ClearNamedBufferData);
|
||||
ClearBufferRange_State(bufferObject, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
|
||||
type, data, BufferOp::ClearNamedBufferData);
|
||||
}
|
||||
|
||||
void ClearNamedBufferSubData_State(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size,
|
||||
GLenum format, GLenum type, const void* data) {
|
||||
ClearNamedBufferRange_State(buffer, internalformat, offset, size, format, type, data,
|
||||
BufferOp::ClearNamedBufferSubData);
|
||||
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferSubData);
|
||||
if (!bufferObject) return;
|
||||
ClearBufferRange_State(bufferObject, internalformat, offset, size, format, type, data,
|
||||
BufferOp::ClearNamedBufferSubData);
|
||||
}
|
||||
|
||||
void* MapNamedBuffer_State(GLuint buffer, GLenum access) {
|
||||
@@ -1662,6 +1708,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
CopyNamedBufferSubData_State(readBuffer, writeBuffer, readOffset, writeOffset, size);
|
||||
}
|
||||
|
||||
void ClearBufferData(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
||||
ClearBufferData_State(target, internalformat, format, type, data);
|
||||
}
|
||||
|
||||
void ClearBufferSubData(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
|
||||
GLenum type, const void* data) {
|
||||
ClearBufferSubData_State(target, internalformat, offset, size, format, type, data);
|
||||
}
|
||||
|
||||
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
|
||||
ClearNamedBufferData_State(buffer, internalformat, format, type, data);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void NamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, const void* data);
|
||||
void CopyNamedBufferSubData(GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset,
|
||||
GLsizeiptr size);
|
||||
void ClearBufferData(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data);
|
||||
void ClearBufferSubData(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
|
||||
GLenum type, const void* data);
|
||||
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data);
|
||||
void ClearNamedBufferSubData(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
|
||||
GLenum type, const void* data);
|
||||
|
||||
@@ -25,12 +25,12 @@
|
||||
#define DECLARE_GL_FUNCTION_STUB_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
|
||||
|
||||
#define DECLARE_GL_FUNCTION_STUB_END(type, name, ...) \
|
||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__); \
|
||||
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__); \
|
||||
return (type)1; \
|
||||
}
|
||||
|
||||
#define DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(type, name, ...) \
|
||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__); \
|
||||
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__); \
|
||||
}
|
||||
|
||||
#define DECLARE_GL_FUNCTION_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
|
||||
@@ -969,14 +969,14 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL3dv, GLuint index, const GLdoub
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL4dv, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL4dv, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLPointer, index, size, type, stride, pointer)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexAttribLdv, GLuint index, GLenum pname, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexAttribLdv, index, pname, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportIndexedf, GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedf, index, x, y, w, h)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedfv, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexed, GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexed, index, left, bottom, width, height)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexedv, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeIndexed, index, n, f)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedf, GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportIndexedf, index, x, y, w, h)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportIndexedfv, index, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexed, GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorIndexed, index, left, bottom, width, height)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorIndexedv, index, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DepthRangeArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DepthRangeIndexed, index, n, f)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetFloati_v, GLenum target, GLuint index, GLfloat* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetFloati_v, target, index, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdouble* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetDoublei_v, target, index, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance)
|
||||
@@ -985,8 +985,8 @@ DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLen
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInternalformati64v, GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInternalformati64v, target, internalformat, pname, count, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexImage, GLuint texture, GLint level) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexImage, texture, level)
|
||||
@@ -2585,7 +2585,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedbackNV, GLenum target, GLui
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacksNV, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacksNV, n, ids)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacksNV, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacksNV, n, ids)
|
||||
MOBILEGL_GL_API GLboolean glIsTransformFeedbackNV(GLuint id) {
|
||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
|
||||
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__);
|
||||
return GL_FALSE;
|
||||
}
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedbackNV, )
|
||||
@@ -3181,5 +3181,5 @@ MOBILEGL_GL_API void glVertexAttribDivisorARB(GLuint index, GLuint divisor) {
|
||||
}
|
||||
|
||||
MOBILEGL_GL_API void glWindowRectanglesEXT(GLenum mode, GLsizei count, const GLint* box) {
|
||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
|
||||
MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__);
|
||||
}
|
||||
|
||||
@@ -547,7 +547,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
|
||||
auto blitNamedFramebuffer = MG_Backend::gBackendFunctionsTable.GL.BlitNamedFramebuffer;
|
||||
if (!blitNamedFramebuffer) {
|
||||
MGLOG_E("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
|
||||
MGLOG_E_ONCE("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
|
||||
return;
|
||||
}
|
||||
blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
|
||||
@@ -558,7 +558,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
||||
auto clearNamedFramebufferfv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfv;
|
||||
if (!clearNamedFramebufferfv) {
|
||||
MGLOG_E("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
|
||||
@@ -568,7 +568,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||
auto clearNamedFramebufferfi = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfi;
|
||||
if (!clearNamedFramebufferfi) {
|
||||
MGLOG_E("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
|
||||
@@ -578,7 +578,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum buffer, GLint drawbuffer, const GLint* value) {
|
||||
auto clearNamedFramebufferiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferiv;
|
||||
if (!clearNamedFramebufferiv) {
|
||||
MGLOG_E("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
|
||||
@@ -588,7 +588,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
||||
auto clearNamedFramebufferuiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferuiv;
|
||||
if (!clearNamedFramebufferuiv) {
|
||||
MGLOG_E("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
MGLOG_E_ONCE("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
|
||||
return;
|
||||
}
|
||||
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/RenderStateEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/ErrorCodeConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
@@ -27,6 +28,11 @@
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Declared rather than #included from GL_RenderState.h on purpose: that header also declares
|
||||
// a free function named BlendEquation, which would hide the ::MobileGL::BlendEquation enum
|
||||
// this file's blend-state queries name unqualified.
|
||||
GLboolean IsEnabledi(GLenum target, GLuint index);
|
||||
|
||||
namespace {
|
||||
enum class IndexedBufferQueryKind {
|
||||
Binding,
|
||||
@@ -339,26 +345,70 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return sampler ? static_cast<GLint>(sampler->GetExternalIndex()) : 0;
|
||||
}
|
||||
|
||||
// The ARB_viewport_array indexed rectangles. MobileGL keeps exactly one viewport, one
|
||||
// scissor box and one depth range, so every in-range index answers with that single
|
||||
// value - but it has to come from the frontend state the non-indexed getters read.
|
||||
// The generic path at the bottom of GetIntegeri_v is a raw backend passthrough that
|
||||
// has no case for these, so routing them through it returned zeros.
|
||||
// The ARB_viewport_array indexed rectangles. Each of these is genuinely per-viewport
|
||||
// frontend state (RenderStateParameters::Viewports / ScissorBoxes / DepthRanges), so the
|
||||
// indexed getters must read the indexed storage - the generic path at the bottom of
|
||||
// GetIntegeri_v is a raw backend passthrough that has no case for them and returned
|
||||
// zeros, and routing them to the NON-indexed getter (what this used to do) answered every
|
||||
// index with viewport 0's value, which is what
|
||||
// KHR-GL43.viewport_array.{viewport,scissor,depth_range}_api caught.
|
||||
Bool IsIndexedViewportQuery(GLenum target) {
|
||||
return target == GL_VIEWPORT || target == GL_SCISSOR_BOX || target == GL_DEPTH_RANGE;
|
||||
}
|
||||
|
||||
// ARB_viewport_array: `index` selects a viewport and MAX_VIEWPORTS bounds it.
|
||||
// Component count of an indexed viewport-array query, so every width of getter writes the
|
||||
// caller's whole buffer instead of just element 0 (GL 4.6 core 22.1).
|
||||
GLsizei IndexedViewportQueryComponents(GLenum target) {
|
||||
return target == GL_DEPTH_RANGE ? 2 : 4;
|
||||
}
|
||||
|
||||
// ARB_viewport_array: `index` selects a viewport and MAX_VIEWPORTS bounds it. The bound is
|
||||
// the frontend's own state width, which is also exactly what GL_MAX_VIEWPORTS reports -
|
||||
// taking it from the backend caps instead would let a device limit of 1 (a Vulkan device
|
||||
// without the multiViewport feature) make index 1 illegal even though the state exists.
|
||||
Bool ValidateViewportQueryIndex(GLuint index, const char* caller) {
|
||||
GLint maxViewports = 0;
|
||||
GetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
if (index < static_cast<GLuint>(std::max(maxViewports, 1))) return true;
|
||||
if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Viewport index is out of range."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// The indexed viewport/scissor/depth-range state as floats, which is the widest lossless
|
||||
// shape MobileGL stores (the viewport really is float state; the scissor box is integral
|
||||
// and well inside float's exact range, and every depth range is in [0, 1]). Every indexed
|
||||
// getter width funnels through this so they can never disagree with each other.
|
||||
void ReadIndexedViewportStateFloat(GLenum target, GLuint index, GLfloat* out) {
|
||||
switch (target) {
|
||||
case GL_VIEWPORT: {
|
||||
const FloatVec4& viewport = MG_State::pGLContext->GetViewportIndexed(index);
|
||||
out[0] = viewport.x();
|
||||
out[1] = viewport.y();
|
||||
out[2] = viewport.z();
|
||||
out[3] = viewport.w();
|
||||
return;
|
||||
}
|
||||
case GL_SCISSOR_BOX: {
|
||||
const IntVec4& box = MG_State::pGLContext->GetScissorBoxIndexed(index);
|
||||
out[0] = static_cast<GLfloat>(box.x());
|
||||
out[1] = static_cast<GLfloat>(box.y());
|
||||
out[2] = static_cast<GLfloat>(box.z());
|
||||
out[3] = static_cast<GLfloat>(box.w());
|
||||
return;
|
||||
}
|
||||
case GL_DEPTH_RANGE: {
|
||||
const FloatVec2& range = MG_State::pGLContext->GetDepthRangeIndexed(index);
|
||||
out[0] = range.x();
|
||||
out[1] = range.y();
|
||||
return;
|
||||
}
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "ReadIndexedViewportStateFloat: unexpected target 0x%x",
|
||||
static_cast<Uint32>(target));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void CopyIntsToBooleans(const GLint* src, SizeT count, GLboolean* dst) {
|
||||
for (SizeT i = 0; i < count; ++i) {
|
||||
dst[i] = src[i] ? GL_TRUE : GL_FALSE;
|
||||
@@ -383,7 +433,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
MGLOG_D("glGetString, name: %s", MG_Util::ConvertGLEnumToString(name).c_str());
|
||||
if (!activeBackendObject) {
|
||||
MGLOG_E("activeBackendObject is not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||
return (GLubyte*)"Unknown";
|
||||
}
|
||||
|
||||
@@ -442,7 +492,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||
if (!activeBackendObject) {
|
||||
MGLOG_E("activeBackendObject is not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||
return (GLubyte*)"Unknown";
|
||||
}
|
||||
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
||||
@@ -629,6 +679,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[1] = dynamicParameters.ViewportBoundsRangeMax;
|
||||
return;
|
||||
}
|
||||
// Viewport 0's rectangle, verbatim. Falling through to the integer width below would
|
||||
// round the fractional rectangle a glViewportIndexedf(0, ...) is allowed to set, and
|
||||
// glGetFloatv(GL_VIEWPORT) is a lossless query of float state.
|
||||
case GL_VIEWPORT: {
|
||||
const FloatVec4& viewport = MG_State::pGLContext->GetViewportIndexed(0);
|
||||
params[0] = viewport.x();
|
||||
params[1] = viewport.y();
|
||||
params[2] = viewport.z();
|
||||
params[3] = viewport.w();
|
||||
return;
|
||||
}
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
||||
@@ -792,15 +853,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
// GL 4.6 core 22.1: an indexed query answers EVERY indexed state, and GL_SCISSOR_TEST is
|
||||
// indexed by viewport just like GL_BLEND is by draw buffer. Without this the integer
|
||||
// width fell through to the backend passthrough and answered GL_INVALID_ENUM, which is
|
||||
// the sticky error KHR-GL43.viewport_array.queries trips over at its next error check.
|
||||
if (MG_Util::ConvertGLEnumToCapabilityInput(target) != CapabilityInput::Unknown) {
|
||||
*data = IsEnabledi(target, index);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (target) {
|
||||
// ARB_viewport_array queries the indexed rectangles through glGetIntegeri_v as well
|
||||
// (gl4cMultiBindTests and the viewport_array group both do). The frontend keeps one
|
||||
// viewport and one scissor box, so every in-range index reports that one.
|
||||
// (gl4cMultiBindTests and the viewport_array group both do).
|
||||
case GL_VIEWPORT:
|
||||
case GL_SCISSOR_BOX:
|
||||
case GL_DEPTH_RANGE: {
|
||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||
GetIntegerv(target, data);
|
||||
GLfloat values[4] = {};
|
||||
ReadIndexedViewportStateFloat(target, index, values);
|
||||
const GLsizei components = IndexedViewportQueryComponents(target);
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
// Round, not truncate: glGetIntegerv on floating-point state rounds to nearest
|
||||
// (GL 4.6 core 22.2), so a 255.875-wide viewport reads back as 256 and not 255.
|
||||
data[i] = static_cast<GLint>(std::lround(values[i]));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// The vertex buffer binding points of the vertex array object that is bound. Indexed by
|
||||
// binding point, not by attribute (GL 4.6 core 10.3.1).
|
||||
case GL_VERTEX_BINDING_BUFFER:
|
||||
@@ -927,7 +1005,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (IsIndexedViewportQuery(target)) {
|
||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||
GetFloatv(target, data);
|
||||
// Verbatim, NOT via the integer width: the viewport is float state and
|
||||
// KHR-GL43.viewport_array.viewport_api compares the read-back with ==, so a
|
||||
// glViewportIndexedf(i, 0.125f, ...) has to come back as 0.125f exactly.
|
||||
ReadIndexedViewportStateFloat(target, index, data);
|
||||
return;
|
||||
}
|
||||
GLint ints[4] = {};
|
||||
@@ -944,7 +1025,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (IsIndexedViewportQuery(target)) {
|
||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||
GetDoublev(target, data);
|
||||
GLfloat values[4] = {};
|
||||
ReadIndexedViewportStateFloat(target, index, values);
|
||||
const GLsizei components = IndexedViewportQueryComponents(target);
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
data[i] = static_cast<GLdouble>(values[i]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
GLint ints[4] = {};
|
||||
@@ -1020,7 +1106,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// 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]);
|
||||
// The viewport-array rectangles are the only multi-component indexed state here; every
|
||||
// other pname is scalar, so widening element 0 alone would silently truncate them.
|
||||
const GLsizei components = IsIndexedViewportQuery(target) ? IndexedViewportQueryComponents(target) : 1;
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
data[i] = static_cast<GLint64>(values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void GetInteger64v(GLenum pname, GLint64* params) {
|
||||
@@ -1954,7 +2045,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||
if (!activeBackendObject) {
|
||||
MGLOG_E("activeBackendObject is not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||
return;
|
||||
}
|
||||
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
||||
@@ -2192,7 +2283,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[1] = dynamicParameters.MaxViewportHeight;
|
||||
break;
|
||||
case GL_MAX_VIEWPORTS:
|
||||
*params = dynamicParameters.MaxViewports;
|
||||
// The frontend's own state width, not the backend's device limit. GL 4.3 core
|
||||
// requires MAX_VIEWPORTS >= 16 and every indexed viewport entry point validates
|
||||
// against RenderStateParameters::MAX_VIEWPORTS, so reporting anything else would
|
||||
// either advertise viewports the state cannot hold or reject indices it can. A
|
||||
// Vulkan device without the multiViewport feature reports maxViewports == 1, which
|
||||
// limits what can be RASTERIZED to more than one rectangle (see the multiViewport
|
||||
// gate in VulkanRenderer), not what the GL state can hold; caps.MaxViewports keeps
|
||||
// carrying that device number for exactly that decision.
|
||||
*params = static_cast<GLint>(RenderStateParameters::MAX_VIEWPORTS);
|
||||
break;
|
||||
case GL_MINOR_VERSION:
|
||||
*params = rendererInfo.RendererGLInfo.TargetGLVersion.Minor;
|
||||
@@ -2248,7 +2347,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxTextureMaxAnisotropy));
|
||||
break;
|
||||
default:
|
||||
MGLOG_E("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname);
|
||||
MGLOG_D("glGetIntegerv: Invalid enum %s (0x%X)", MG_Util::ConvertGLEnumToString(pname).c_str(), pname);
|
||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetIntegerv",
|
||||
std::format("Invalid enum: 0x{:X}", pname)));
|
||||
|
||||
@@ -908,7 +908,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||
offset + span > programObject->GetUBOSize()) {
|
||||
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
program, location);
|
||||
return;
|
||||
}
|
||||
@@ -962,7 +962,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||
offset + span > programObject->GetUBOSize()) {
|
||||
MGLOG_E("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
program, location);
|
||||
return;
|
||||
}
|
||||
@@ -1062,7 +1062,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!initialized) {
|
||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||
if (!activeBackendObject) {
|
||||
MGLOG_E("activeBackendObject is not initialized!");
|
||||
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||
return;
|
||||
}
|
||||
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
||||
@@ -1152,7 +1152,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
SizeT writeSize = ItemCount * sizeof(T);
|
||||
if (size < writeSize) {
|
||||
// Metadata bug: degrade to a clamped copy instead of killing the process.
|
||||
MGLOG_E("%s: uniform size mismatch at program %u location %u: expected at least %zu bytes, got %zu "
|
||||
MGLOG_E_ONCE("%s: uniform size mismatch at program %u location %u: expected at least %zu bytes, got %zu "
|
||||
"bytes; clamping",
|
||||
__func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size);
|
||||
writeSize = size;
|
||||
@@ -1173,7 +1173,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
offset + byteOffsetInsideUniform + writeSize > uboSize) {
|
||||
// Should not happen: linking gives every settable uniform backing
|
||||
// storage. Log and drop the write instead of faulting.
|
||||
MGLOG_E("%s: uniform at program %u location %u has no backing storage (ubo=%p offset=%u size=%zu "
|
||||
MGLOG_E_ONCE("%s: uniform at program %u location %u has no backing storage (ubo=%p offset=%u size=%zu "
|
||||
"uboSize=%zu); dropping write",
|
||||
__func__, programObject.GetExternalIndex(), location, static_cast<void*>(pUBO), offset,
|
||||
writeSize, uboSize);
|
||||
@@ -1807,7 +1807,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
MGLOG_E("%s: unknown pname = %p %s", __func__, pname, MG_Util::ConvertGLEnumToString(pname).c_str());
|
||||
MGLOG_D("%s: unknown pname = %p %s", __func__, pname, MG_Util::ConvertGLEnumToString(pname).c_str());
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
|
||||
@@ -20,28 +20,118 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return std::clamp(static_cast<Float>(value), 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
static Bool ValidateIndexedBlendCapability(GLenum target, GLuint index, const char* functionName) {
|
||||
if (target != GL_BLEND) {
|
||||
// GL 4.6 core 17.3.2 and 22.1 give exactly two indexed capabilities: GL_BLEND, indexed by
|
||||
// draw buffer, and GL_SCISSOR_TEST, indexed by viewport. They have DIFFERENT bounds
|
||||
// (MAX_DRAW_BUFFERS vs MAX_VIEWPORTS), so the limit is picked per target rather than shared.
|
||||
static Bool ValidateIndexedCapability(GLenum target, GLuint index, const char* functionName) {
|
||||
GLuint limit = 0;
|
||||
const char* indexName = nullptr;
|
||||
switch (target) {
|
||||
case GL_BLEND:
|
||||
limit = MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS;
|
||||
indexName = "Buffer";
|
||||
break;
|
||||
case GL_SCISSOR_TEST:
|
||||
limit = RenderStateParameters::MAX_VIEWPORTS;
|
||||
indexName = "Viewport";
|
||||
break;
|
||||
default:
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Only GL_BLEND is supported for indexed capability state."));
|
||||
"Only GL_BLEND and GL_SCISSOR_TEST are supported for indexed "
|
||||
"capability state."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
|
||||
if (index >= limit) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
"Buffer index " + std::to_string(index) + " is out of range. Max supported is " +
|
||||
std::to_string(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS - 1) + "."));
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
String(indexName) + " index " + std::to_string(index) +
|
||||
" is out of range. Max supported is " + std::to_string(limit - 1) +
|
||||
"."));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ------------------ ARB_viewport_array parameter validation ------------------
|
||||
// All three families share the same two shapes, so they share the two checkers. GL 4.6 core
|
||||
// 13.6.1/17.3.2: an out-of-range index is GL_INVALID_VALUE, and so is a negative width or
|
||||
// height. `first + count == MAX_VIEWPORTS` is LEGAL - only strictly greater is an error,
|
||||
// which KHR-GL43.viewport_array.api_errors checks explicitly in both directions.
|
||||
static Bool ValidateViewportIndex(GLuint index, const char* functionName) {
|
||||
if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
|
||||
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Viewport index " + std::to_string(index) +
|
||||
" is out of range. Max supported is " +
|
||||
std::to_string(RenderStateParameters::MAX_VIEWPORTS - 1) + "."));
|
||||
return false;
|
||||
}
|
||||
|
||||
static Bool ValidateViewportRange(GLuint first, GLsizei count, const char* functionName) {
|
||||
if (count < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "count must not be negative."));
|
||||
return false;
|
||||
}
|
||||
// Widened before adding: first is a GLuint and count a GLsizei, so `first + count` in
|
||||
// 32 bits can wrap past MAX_VIEWPORTS and let an out-of-range range through.
|
||||
const Uint64 last = static_cast<Uint64>(first) + static_cast<Uint64>(count);
|
||||
if (last > RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"first (" + std::to_string(first) + ") + count (" +
|
||||
std::to_string(count) + ") exceeds GL_MAX_VIEWPORTS (" +
|
||||
std::to_string(RenderStateParameters::MAX_VIEWPORTS) + ")."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static Bool ValidateNonNegativeExtent(T width, T height, const char* functionName) {
|
||||
if (width >= T(0) && height >= T(0)) return true;
|
||||
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "Width and height must be non-negative."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// The array forms are all-or-nothing: one bad element rejects the whole call with a SINGLE
|
||||
// GL_INVALID_VALUE and leaves every rectangle untouched. api_errors relies on both halves -
|
||||
// it passes a full 16-element array with exactly one negative extent and then asserts the
|
||||
// error queue holds exactly one entry.
|
||||
template <typename T>
|
||||
static Bool ValidateArrayExtents(GLsizei count, const T* v, const char* functionName) {
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
if (v[i * 4 + 2] >= T(0) && v[i * 4 + 3] >= T(0)) continue;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Width and height must be non-negative (element " + std::to_string(i) +
|
||||
")."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool ValidateNonNullArray(const void* v, const char* functionName) {
|
||||
if (v != nullptr) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "value pointer cannot be null."));
|
||||
return false;
|
||||
}
|
||||
|
||||
static Bool TryConvertBlendEquation(GLenum mode, const char* functionName,
|
||||
::MobileGL::BlendEquation& outEquation) {
|
||||
outEquation = MG_Util::ConvertGLEnumToBlendEquation(mode);
|
||||
@@ -93,16 +183,70 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Viewport_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||
if (width < 0 || height < 0) {
|
||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Viewport_State",
|
||||
"Width abd height must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateNonNegativeExtent(width, height, "Viewport_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetViewport(IntVec4(x, y, width, height));
|
||||
}
|
||||
|
||||
// ------------------ ARB_viewport_array setters ------------------
|
||||
void ViewportArrayv_State(GLuint first, GLsizei count, const GLfloat* v) {
|
||||
if (!ValidateViewportRange(first, count, "ViewportArrayv_State")) return;
|
||||
if (count == 0) return;
|
||||
if (!ValidateNonNullArray(v, "ViewportArrayv_State")) return;
|
||||
if (!ValidateArrayExtents(count, v, "ViewportArrayv_State")) return;
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
MG_State::pGLContext->SetViewportIndexed(first + static_cast<GLuint>(i),
|
||||
FloatVec4(v[i * 4 + 0], v[i * 4 + 1], v[i * 4 + 2], v[i * 4 + 3]));
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportIndexedf_State(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) {
|
||||
if (!ValidateViewportIndex(index, "ViewportIndexedf_State")) return;
|
||||
if (!ValidateNonNegativeExtent(w, h, "ViewportIndexedf_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetViewportIndexed(index, FloatVec4(x, y, w, h));
|
||||
}
|
||||
|
||||
void ScissorArrayv_State(GLuint first, GLsizei count, const GLint* v) {
|
||||
if (!ValidateViewportRange(first, count, "ScissorArrayv_State")) return;
|
||||
if (count == 0) return;
|
||||
if (!ValidateNonNullArray(v, "ScissorArrayv_State")) return;
|
||||
if (!ValidateArrayExtents(count, v, "ScissorArrayv_State")) return;
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
MG_State::pGLContext->SetScissorBoxIndexed(first + static_cast<GLuint>(i),
|
||||
IntVec4(v[i * 4 + 0], v[i * 4 + 1], v[i * 4 + 2], v[i * 4 + 3]));
|
||||
}
|
||||
}
|
||||
|
||||
void ScissorIndexed_State(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) {
|
||||
if (!ValidateViewportIndex(index, "ScissorIndexed_State")) return;
|
||||
if (!ValidateNonNegativeExtent(width, height, "ScissorIndexed_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetScissorBoxIndexed(index, IntVec4(left, bottom, width, height));
|
||||
}
|
||||
|
||||
void DepthRangeArrayv_State(GLuint first, GLsizei count, const GLdouble* v) {
|
||||
if (!ValidateViewportRange(first, count, "DepthRangeArrayv_State")) return;
|
||||
if (count == 0) return;
|
||||
if (!ValidateNonNullArray(v, "DepthRangeArrayv_State")) return;
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
MG_State::pGLContext->SetDepthRangeIndexed(
|
||||
first + static_cast<GLuint>(i),
|
||||
FloatVec2(ClampUnitFloat(static_cast<GLfloat>(v[i * 2 + 0])),
|
||||
ClampUnitFloat(static_cast<GLfloat>(v[i * 2 + 1]))));
|
||||
}
|
||||
}
|
||||
|
||||
void DepthRangeIndexed_State(GLuint index, GLdouble n, GLdouble f) {
|
||||
if (!ValidateViewportIndex(index, "DepthRangeIndexed_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetDepthRangeIndexed(
|
||||
index, FloatVec2(ClampUnitFloat(static_cast<GLfloat>(n)), ClampUnitFloat(static_cast<GLfloat>(f))));
|
||||
}
|
||||
|
||||
void StencilOpSeparate_State(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
|
||||
Bool applyFront = false;
|
||||
Bool applyBack = false;
|
||||
@@ -175,12 +319,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Scissor_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||
if (width < 0 || height < 0) {
|
||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Scissor_State",
|
||||
"Width abd height must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateNonNegativeExtent(width, height, "Scissor_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetScissorBox(IntVec4(x, y, width, height));
|
||||
}
|
||||
@@ -336,7 +475,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
GLboolean IsEnabledi_State(GLenum target, GLuint index) {
|
||||
if (!ValidateIndexedBlendCapability(target, index, "IsEnabledi_State")) {
|
||||
if (!ValidateIndexedCapability(target, index, "IsEnabledi_State")) {
|
||||
return GL_FALSE;
|
||||
}
|
||||
|
||||
@@ -392,7 +531,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
GLint values[4] = {};
|
||||
GetIntegeri_v(target, index, values);
|
||||
*data = values[0] != 0 ? GL_TRUE : GL_FALSE;
|
||||
// The ARB_viewport_array rectangles are the only multi-component indexed state that
|
||||
// reaches here; writing element 0 alone would leave the caller's other three untouched.
|
||||
const GLsizei components = target == GL_VIEWPORT || target == GL_SCISSOR_BOX
|
||||
? 4
|
||||
: (target == GL_DEPTH_RANGE ? 2 : 1);
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
data[i] = values[i] != 0 ? GL_TRUE : GL_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
GLboolean IsEnabled_State(GLenum cap) {
|
||||
@@ -725,7 +871,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Disablei_State(GLenum target, GLuint index) {
|
||||
if (!ValidateIndexedBlendCapability(target, index, "Disablei_State")) {
|
||||
if (!ValidateIndexedCapability(target, index, "Disablei_State")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -743,7 +889,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Enablei_State(GLenum target, GLuint index) {
|
||||
if (!ValidateIndexedBlendCapability(target, index, "Enablei_State")) {
|
||||
if (!ValidateIndexedCapability(target, index, "Enablei_State")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -797,6 +943,44 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Viewport_State(x, y, width, height);
|
||||
}
|
||||
|
||||
void ViewportArrayv(GLuint first, GLsizei count, const GLfloat* v) {
|
||||
ViewportArrayv_State(first, count, v);
|
||||
}
|
||||
|
||||
void ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) {
|
||||
ViewportIndexedf_State(index, x, y, w, h);
|
||||
}
|
||||
|
||||
void ViewportIndexedfv(GLuint index, const GLfloat* v) {
|
||||
// The index is validated before the pointer is touched: glViewportIndexedfv(MAX, nullptr)
|
||||
// must be one GL_INVALID_VALUE, not a null dereference.
|
||||
if (!ValidateViewportIndex(index, "ViewportIndexedfv")) return;
|
||||
if (!ValidateNonNullArray(v, "ViewportIndexedfv")) return;
|
||||
ViewportIndexedf_State(index, v[0], v[1], v[2], v[3]);
|
||||
}
|
||||
|
||||
void ScissorArrayv(GLuint first, GLsizei count, const GLint* v) {
|
||||
ScissorArrayv_State(first, count, v);
|
||||
}
|
||||
|
||||
void ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) {
|
||||
ScissorIndexed_State(index, left, bottom, width, height);
|
||||
}
|
||||
|
||||
void ScissorIndexedv(GLuint index, const GLint* v) {
|
||||
if (!ValidateViewportIndex(index, "ScissorIndexedv")) return;
|
||||
if (!ValidateNonNullArray(v, "ScissorIndexedv")) return;
|
||||
ScissorIndexed_State(index, v[0], v[1], v[2], v[3]);
|
||||
}
|
||||
|
||||
void DepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v) {
|
||||
DepthRangeArrayv_State(first, count, v);
|
||||
}
|
||||
|
||||
void DepthRangeIndexed(GLuint index, GLdouble n, GLdouble f) {
|
||||
DepthRangeIndexed_State(index, n, f);
|
||||
}
|
||||
|
||||
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
|
||||
StencilOpSeparate_State(face, sfail, dpfail, dppass);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void Enablei(GLenum target, GLuint index);
|
||||
void BlendFunc(GLenum sfactor, GLenum dfactor);
|
||||
void Viewport(GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
// ARB_viewport_array (core since GL 4.1). Every one of these addresses the same 16-element
|
||||
// indexed state the classic glViewport/glScissor/glDepthRange trio broadcasts to.
|
||||
void ViewportArrayv(GLuint first, GLsizei count, const GLfloat* v);
|
||||
void ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h);
|
||||
void ViewportIndexedfv(GLuint index, const GLfloat* v);
|
||||
void ScissorArrayv(GLuint first, GLsizei count, const GLint* v);
|
||||
void ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height);
|
||||
void ScissorIndexedv(GLuint index, const GLint* v);
|
||||
void DepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v);
|
||||
void DepthRangeIndexed(GLuint index, GLdouble n, GLdouble f);
|
||||
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass);
|
||||
void StencilOp(GLenum fail, GLenum zfail, GLenum zpass);
|
||||
void StencilMaskSeparate(GLenum face, GLuint mask);
|
||||
|
||||
@@ -621,7 +621,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// 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 "
|
||||
MGLOG_W_ONCE("%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(
|
||||
@@ -870,7 +870,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_Util::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
|
||||
if (readBytesPerTexel != bytesPerTexel) {
|
||||
MGLOG_I("%s: cannot copy into a %zu-byte texel from a %zu-byte readback layout", caller,
|
||||
MGLOG_W_ONCE("%s: cannot copy into a %zu-byte texel from a %zu-byte readback layout", caller,
|
||||
bytesPerTexel, readBytesPerTexel);
|
||||
return false;
|
||||
}
|
||||
@@ -1490,7 +1490,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
|
||||
yoffset + height > static_cast<GLsizei>(texelSize.y()) ||
|
||||
zoffset + depth > static_cast<GLsizei>(texelSize.z())) {
|
||||
MGLOG_E("TexSubImage3D_State: Specified region exceeds texture level dimensions");
|
||||
MGLOG_E_ONCE("TexSubImage3D_State: Specified region exceeds texture level dimensions");
|
||||
free(processedPixels);
|
||||
return;
|
||||
}
|
||||
@@ -1599,7 +1599,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
{width, height, 1}, false, inputSize);
|
||||
|
||||
if (!processedPixels || inputSize == 0) {
|
||||
MGLOG_E("TexSubImage2D_State: Failed to process pixel data for TexSubImage2D, width: %d, height: %d", width,
|
||||
MGLOG_E_ONCE("TexSubImage2D_State: Failed to process pixel data for TexSubImage2D, width: %d, height: %d", width,
|
||||
height);
|
||||
if (processedPixels) free(processedPixels);
|
||||
return;
|
||||
@@ -1613,7 +1613,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
|
||||
yoffset + height > static_cast<GLsizei>(texelSize.y())) {
|
||||
MGLOG_E("TexSubImage2D_State: Specified region exceeds texture dimensions");
|
||||
MGLOG_E_ONCE("TexSubImage2D_State: Specified region exceeds texture dimensions");
|
||||
free(processedPixels);
|
||||
return;
|
||||
}
|
||||
@@ -2164,7 +2164,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (processedPixels && imageSize > 0) {
|
||||
if (imageSize != internalBytes) {
|
||||
MGLOG_W("%s: Processed pixel data size (%zu) does not match expected size (%zu). "
|
||||
MGLOG_W_ONCE("%s: Processed pixel data size (%zu) does not match expected size (%zu). "
|
||||
"This may indicate an alignment or processing issue.",
|
||||
__func__, imageSize, internalBytes);
|
||||
}
|
||||
@@ -2310,7 +2310,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (processedPixels && imageSize > 0) {
|
||||
if (imageSize != internalBytes) {
|
||||
MGLOG_W("TexImage2D_State: Processed pixel data size (%zu) does not match expected size (%zu). "
|
||||
MGLOG_W_ONCE("TexImage2D_State: Processed pixel data size (%zu) does not match expected size (%zu). "
|
||||
"This may indicate an alignment or processing issue.",
|
||||
imageSize, internalBytes);
|
||||
}
|
||||
@@ -3382,12 +3382,84 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The eleven targets GL 4.6 core 18.3.2 accepts. GL_TEXTURE_BUFFER, the six cube FACE
|
||||
// enums and every PROXY enum all convert to a TextureTarget this frontend recognises,
|
||||
// so ValidateTextureTarget lets them through; here they are INVALID_ENUM.
|
||||
Bool ValidateCopyImageTarget(GLenum target, const char* endpointName) {
|
||||
switch (target) {
|
||||
case GL_RENDERBUFFER:
|
||||
case GL_TEXTURE_1D:
|
||||
case GL_TEXTURE_1D_ARRAY:
|
||||
case GL_TEXTURE_2D:
|
||||
case GL_TEXTURE_2D_ARRAY:
|
||||
case GL_TEXTURE_2D_MULTISAMPLE:
|
||||
case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
|
||||
case GL_TEXTURE_3D:
|
||||
case GL_TEXTURE_CUBE_MAP:
|
||||
case GL_TEXTURE_CUBE_MAP_ARRAY:
|
||||
case GL_TEXTURE_RECTANGLE:
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
|
||||
std::format("{} is not a target glCopyImageSubData accepts as the {}.",
|
||||
MG_Util::ConvertGLEnumToString(target), endpointName)));
|
||||
return false;
|
||||
}
|
||||
|
||||
IntVec3 GetCopyImageLevelSize(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureUploadTarget uploadTarget, GLint level) {
|
||||
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
|
||||
if (!mipmapTexture) return textureObject->GetBaseSize();
|
||||
return mipmapTexture->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
|
||||
}
|
||||
|
||||
// glCopyImageSubData names an object that must already exist, and GL 4.6 core 18.3.2
|
||||
// spells the failure INVALID_VALUE - "if either name does not correspond to a valid
|
||||
// object". The shared ValidateTextureObject says INVALID_OPERATION, which is right for
|
||||
// the ~30 entry points that reach it through a BOUND object (where the name was never
|
||||
// in question and the fault is the binding), so this is a local rule rather than a
|
||||
// change to the helper.
|
||||
Bool ValidateCopyImageObjectExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
const char* endpointName) {
|
||||
if (textureObject) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
|
||||
std::format("The {} name does not correspond to an existing image object.", endpointName)));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Same split for the target/object disagreement: GL 4.6 core 18.3.2 makes a target that
|
||||
// does not match the object INVALID_ENUM, where the shared uniformity helper records
|
||||
// INVALID_OPERATION for the upload paths that share it.
|
||||
Bool ValidateCopyImageTargetMatchesObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureTarget target, const char* endpointName) {
|
||||
if (!textureObject || textureObject->GetTarget() == target) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
|
||||
std::format("The {} target {} does not match the target the object was created with ({}).",
|
||||
endpointName, MG_Util::ConvertTextureTargetToString(target),
|
||||
MG_Util::ConvertTextureTargetToString(textureObject->GetTarget()))));
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ValidateCopyImageSubData_State(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
GLenum dstTarget, GLint dstLevel,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
if (!TextureImpl::ValidateTextureObject(srcTexture) || !TextureImpl::ValidateTextureObject(dstTexture)) {
|
||||
if (!ValidateCopyImageObjectExists(srcTexture, "source") ||
|
||||
!ValidateCopyImageObjectExists(dstTexture, "destination")) {
|
||||
return false;
|
||||
}
|
||||
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
|
||||
@@ -3396,14 +3468,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
!TextureImpl::ValidateTextureTarget(dstTextureTarget)) {
|
||||
return false;
|
||||
}
|
||||
if (!TextureImpl::ValidateTextureTargetUniformity(srcTexture, srcTextureTarget) ||
|
||||
!TextureImpl::ValidateTextureTargetUniformity(dstTexture, dstTextureTarget)) {
|
||||
// GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but
|
||||
// 18.3.2 does not accept them here - only the eleven whole-image targets do.
|
||||
if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) {
|
||||
return false;
|
||||
}
|
||||
if (!ValidateCopyImageTargetMatchesObject(srcTexture, srcTextureTarget, "source") ||
|
||||
!ValidateCopyImageTargetMatchesObject(dstTexture, dstTextureTarget, "destination")) {
|
||||
return false;
|
||||
}
|
||||
if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) ||
|
||||
!TextureImpl::ValidateTextureLevelNumber(dstLevel)) {
|
||||
return false;
|
||||
}
|
||||
// ValidateTextureLevelNumber only bounds the index by GL_MAX_TEXTURE_SIZE; it cannot
|
||||
// see that this particular texture stops at level 0. Both backends turn <level> into an
|
||||
// image subresource with no further checking (DirectVulkan builds a VkImageCopy from it,
|
||||
// DirectGLES forwards it to the ES copy), so a level the texture never had reached the
|
||||
// driver as an out-of-range mip index - on Adreno that is a SIGSEGV inside
|
||||
// vkCmdCopyImage, which is what KHR-GL43.copy_image.non_existent_mipmap used to do to
|
||||
// the whole glcts process. The answer the spec asks for is GL_INVALID_VALUE.
|
||||
if (!TextureImpl::ValidateTextureLevelExists(srcTexture, srcLevel, __func__) ||
|
||||
!TextureImpl::ValidateTextureLevelExists(dstTexture, dstLevel, __func__)) {
|
||||
return false;
|
||||
}
|
||||
if (srcWidth < 0 || srcHeight < 0 || srcDepth < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
@@ -3414,7 +3502,44 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (srcWidth == 0 || srcHeight == 0 || srcDepth == 0) {
|
||||
return false;
|
||||
}
|
||||
if (!TextureImpl::ValidateBaseInternalFormatMatch(srcTexture->GetFormat(), dstTexture->GetFormat())) {
|
||||
// A multisample image can only be copied to one with the same sample count, and a
|
||||
// single-sample image reports zero - so this one comparison is also what rejects
|
||||
// copying between a multisample target and a non-multisample one.
|
||||
if (srcTexture->GetSamples() != dstTexture->GetSamples()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::format("The two images have different sample counts ({} vs. {}).",
|
||||
srcTexture->GetSamples(), dstTexture->GetSamples())));
|
||||
return false;
|
||||
}
|
||||
// 18.3.2: both images must be complete. An incomplete one has no defined texels to copy
|
||||
// and no defined storage to copy into.
|
||||
if (!srcTexture->IsComplete() || !dstTexture->IsComplete()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::format("A copied image is incomplete (source complete: {}, destination complete: {}).",
|
||||
srcTexture->IsComplete(), dstTexture->IsComplete())));
|
||||
return false;
|
||||
}
|
||||
const auto srcUploadTarget = GetPrimaryUploadTarget(srcTexture);
|
||||
const auto dstUploadTarget = GetPrimaryUploadTarget(dstTexture);
|
||||
const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock(
|
||||
srcTexture->GetFormat(), GetCompressedLevelFormat(srcTexture, srcUploadTarget, srcLevel));
|
||||
const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock(
|
||||
dstTexture->GetFormat(), GetCompressedLevelFormat(dstTexture, dstUploadTarget, dstLevel));
|
||||
if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) {
|
||||
return false;
|
||||
}
|
||||
const IntVec3 srcLevelSize = GetCopyImageLevelSize(srcTexture, srcUploadTarget, srcLevel);
|
||||
const IntVec3 dstLevelSize = GetCopyImageLevelSize(dstTexture, dstUploadTarget, dstLevel);
|
||||
if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight,
|
||||
srcLevelSize.x(), srcLevelSize.y(), "source") ||
|
||||
!TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight,
|
||||
dstLevelSize.x(), dstLevelSize.y(), "destination")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -3643,10 +3768,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// an application has every right to expect from it. Before this existed the call answered
|
||||
// GL_INVALID_ENUM, which was wrong but at least visible; a silent success that leaves the
|
||||
// sampled texels untouched is the kind of thing that costs a day to find from the other
|
||||
// end. MGLOG_I, not _W: warnings are compiled out at the level everything ships at.
|
||||
// end. MGLOG_W is the right level and now survives at INFO; it sat at MGLOG_I only
|
||||
// while the Log.h ordering compiled warnings out of the builds that ship.
|
||||
static std::atomic<Bool> announcedNoCodec{false};
|
||||
if (!announcedNoCodec.exchange(true)) {
|
||||
MGLOG_I("%s: the compressed blocks are stored verbatim and returned by "
|
||||
MGLOG_W("%s: the compressed blocks are stored verbatim and returned by "
|
||||
"glGetCompressedTexImage, but there is no BC/ETC decoder here, so they do not "
|
||||
"reach the texels this level SAMPLES as. Upload through glTexSubImage2D for "
|
||||
"that.",
|
||||
@@ -5588,10 +5714,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void CopyImageSubData(GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
auto srcTexture = GetTextureObjectByName(srcName, __func__);
|
||||
auto dstTexture = GetTextureObjectByName(dstName, __func__);
|
||||
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, dstTexture, dstTarget, dstLevel,
|
||||
srcWidth, srcHeight, srcDepth)) {
|
||||
// A missing name is INVALID_VALUE here, where GetTextureObjectByName's own diagnostic is
|
||||
// INVALID_OPERATION - so resolve through the plain lookup, which answers a null
|
||||
// SharedPtr, and let the validator record the error this entry point owes.
|
||||
const SharedPtr<MG_State::GLState::ITextureObject> srcTexture =
|
||||
MG_State::pGLContext->GetTextureObject(srcName);
|
||||
const SharedPtr<MG_State::GLState::ITextureObject> dstTexture =
|
||||
MG_State::pGLContext->GetTextureObject(dstName);
|
||||
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, srcX, srcY, dstTexture, dstTarget,
|
||||
dstLevel, dstX, dstY, srcWidth, srcHeight, srcDepth)) {
|
||||
return;
|
||||
}
|
||||
CopyImageSubData_Backend(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
|
||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
Bool ValidateTextureTarget(TextureTarget target) {
|
||||
@@ -353,6 +354,63 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateTextureLevelExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int level,
|
||||
const char* caller) {
|
||||
// A null object is somebody else's error to report - ValidateTextureObject runs
|
||||
// first at every call site and has already recorded it.
|
||||
if (!textureObject) return false;
|
||||
|
||||
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
|
||||
if (mipmapTexture == nullptr) {
|
||||
// The only non-mipmap storage class is a buffer texture, and GL_TEXTURE_BUFFER is
|
||||
// not a target glCopyImageSubData accepts at all (it is in the CTS's invalid-target
|
||||
// set). Declining here is not the error code the spec asks for - that would be
|
||||
// INVALID_ENUM from a target check this validator is not - but it does keep a
|
||||
// texture with no image levels whatsoever from reaching a backend that would
|
||||
// dereference a backend texture it never created.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Texture has no mipmap levels to address."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// What this number is, exactly, because two other things are almost it and neither is
|
||||
// safe to assume: it is the number of level SLOTS the shadow has allocated - holes
|
||||
// included, since MipmapStorage::AllocateLevel grows to level+1 and never fills the gap.
|
||||
// For a cube map MipmapUploadTargetArray reports face +X's chain rather than the union.
|
||||
//
|
||||
// The guarantee that matters is one-sided: this count is always >= the level count the
|
||||
// backends derive (VkTextureManager::GetUploadMipLevelCount stops at the first level
|
||||
// with a non-positive extent, so it can only be shorter). That is the safe direction -
|
||||
// no copy to a level the texture genuinely has is ever rejected here. It is NOT an
|
||||
// exact match, so the backends keep their own range guard for the band in between: a
|
||||
// chain with a hole (level 0 and 2 defined, 1 not) is accepted by this predicate and
|
||||
// declined by the backend, which is a silent no-op rather than a copy. That band is a
|
||||
// backend storage limitation, not a validation one - rejecting it here with
|
||||
// INVALID_VALUE would be refusing a copy the spec permits.
|
||||
const Uint levelCount = mipmapTexture->GetMipmapLevelCount();
|
||||
|
||||
if (levelCount == 0) {
|
||||
// No image has ever been defined on this texture, so the fault is the texture,
|
||||
// not the number: GL 4.6 core 18.3.2 asks for INVALID_OPERATION when an object a
|
||||
// copy names is an incomplete texture.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Texture has no image defined at any level."));
|
||||
return false;
|
||||
}
|
||||
if (level < 0 || static_cast<Uint>(level) >= levelCount) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Texture level does not exist in this texture."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
|
||||
if (!textureObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -458,26 +516,86 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2) {
|
||||
const auto unsizedFormat1 = MG_Util::ConvertInternalFormatToUnsized(format1);
|
||||
const auto unsizedFormat2 = MG_Util::ConvertInternalFormatToUnsized(format2);
|
||||
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.
|
||||
CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat) {
|
||||
CopyImageTexelBlock block{};
|
||||
if (compressedFormat != GL_NONE) {
|
||||
const auto info = MG_Util::GetCompressedFormatInfo(compressedFormat);
|
||||
if (info.blockByteSize != 0) {
|
||||
block.byteSize = info.blockByteSize;
|
||||
block.blockWidth = info.blockWidth;
|
||||
block.blockHeight = info.blockHeight;
|
||||
block.compressed = true;
|
||||
return block;
|
||||
}
|
||||
}
|
||||
// The size MobileGL actually stores a texel of this format in, which for every format GL
|
||||
// gives a required size is that required size. The handful of legacy formats GL leaves
|
||||
// implementation-defined (R3_G3_B2, RGB4/5/10/12, RGBA2/12) have no view class in table
|
||||
// 8.22 to be compared against anyway, and this is the size that decides whether a raw
|
||||
// copy between them would in fact preserve the bytes.
|
||||
block.byteSize = MG_Util::GetSizedInternalFormatSizeInBytes(format);
|
||||
return block;
|
||||
}
|
||||
|
||||
Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock,
|
||||
const CopyImageTexelBlock& dstBlock) {
|
||||
if (srcBlock.byteSize == 0 || dstBlock.byteSize == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||
"A copied image has no storage whose texel size is known."));
|
||||
return false;
|
||||
}
|
||||
if (srcBlock.byteSize != dstBlock.byteSize) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateBaseInternalFormatMatch",
|
||||
std::format("The base internal format of the two formats do not match ({} vs. {})",
|
||||
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat1),
|
||||
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat2))));
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||
std::format("The two images' texel blocks are different sizes ({} vs. {} bytes), so the "
|
||||
"formats are not copy-compatible.",
|
||||
srcBlock.byteSize, dstBlock.byteSize)));
|
||||
return false;
|
||||
}
|
||||
// Two compressed images additionally have to agree on the SHAPE of the block, not only
|
||||
// its size: an 8-byte 4x4 block and a hypothetical 8-byte 8x8 one hold different texel
|
||||
// counts, and GL 4.6 core 18.3.2 requires both dimensions to match.
|
||||
if (srcBlock.compressed && dstBlock.compressed &&
|
||||
(srcBlock.blockWidth != dstBlock.blockWidth || srcBlock.blockHeight != dstBlock.blockHeight)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||
std::format("The two compressed images have different block dimensions ({}x{} vs. {}x{}).",
|
||||
srcBlock.blockWidth, srcBlock.blockHeight, dstBlock.blockWidth,
|
||||
dstBlock.blockHeight)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height,
|
||||
Int imageWidth, Int imageHeight, const char* endpointName) {
|
||||
if (!block.compressed) return true;
|
||||
const Int blockWidth = static_cast<Int>(block.blockWidth);
|
||||
const Int blockHeight = static_cast<Int>(block.blockHeight);
|
||||
if (blockWidth <= 1 && blockHeight <= 1) return true;
|
||||
// The origin is unconditional; the extent gets the "or it reaches the edge of the image"
|
||||
// exemption GL 4.6 core 18.3.2 grants, which is what lets a 16x16 BPTC image be copied
|
||||
// whole even when the last block is partial.
|
||||
const Bool originAligned = (x % blockWidth == 0) && (y % blockHeight == 0);
|
||||
const Bool widthOk = (width % blockWidth == 0) || (x + width == imageWidth);
|
||||
const Bool heightOk = (height % blockHeight == 0) || (y + height == imageHeight);
|
||||
if (originAligned && widthOk && heightOk) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageBlockAlignment",
|
||||
std::format("The {} region [{}, {}] + [{} x {}] is not aligned to the {}x{} compressed block "
|
||||
"grid of a {} x {} image.",
|
||||
endpointName, x, y, width, height, blockWidth, blockHeight, imageWidth, imageHeight)));
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat) {
|
||||
const auto unsizedDest = MG_Util::ConvertInternalFormatToUnsized(destFormat);
|
||||
const auto unsizedSrc = MG_Util::ConvertInternalFormatToUnsized(srcFormat);
|
||||
|
||||
@@ -30,6 +30,16 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
TextureInternalFormat internalFormat,
|
||||
TexturePixelDataType type);
|
||||
Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level);
|
||||
// "Is <level> a level this texture actually has?", which ValidateTextureLevelNumber above
|
||||
// does NOT answer - that one only bounds the index by GL_MAX_TEXTURE_SIZE and knows nothing
|
||||
// about the object. Entry points that resolve a level straight into a backend image
|
||||
// subresource need this one: a level the texture never had is GL_INVALID_VALUE (GL 4.6 core
|
||||
// 18.3.2), and passing it through instead reaches the driver as an out-of-range subresource.
|
||||
// Note the error split is per-entry-point, so this is not universally reusable:
|
||||
// glClearTexImage owes INVALID_OPERATION for the same out-of-range level and spells its own
|
||||
// copy of this predicate in GL_Texture.cpp (GetClearTextureObject).
|
||||
Bool ValidateTextureLevelExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int level,
|
||||
const char* caller);
|
||||
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject);
|
||||
// Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry
|
||||
// points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION
|
||||
@@ -40,8 +50,32 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
TextureTarget target);
|
||||
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);
|
||||
// Exact base-format equality - what glCopyImageSubData's format compatibility needs.
|
||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2);
|
||||
// The texel block of one glCopyImageSubData endpoint, resolved to the two things the
|
||||
// compatibility rule actually asks about. `compressed` is not redundant with a block bigger
|
||||
// than 1x1: it is what distinguishes "compressed, and so the region is measured in texels of
|
||||
// a blocked image" from "uncompressed, and so it is measured in texels".
|
||||
struct CopyImageTexelBlock {
|
||||
SizeT byteSize = 0;
|
||||
Uint blockWidth = 1;
|
||||
Uint blockHeight = 1;
|
||||
Bool compressed = false;
|
||||
};
|
||||
// `compressedFormat` is the GLenum a glCompressedTexImage* upload recorded for the level, or
|
||||
// GL_NONE. It has to be asked for separately because MobileGL stores every compressed format
|
||||
// in uncompressed storage (ConvertGLEnumToTextureInternalFormat), so the TextureInternalFormat
|
||||
// alone can no longer tell a BPTC image from the RGBA8 backing it.
|
||||
CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat);
|
||||
// GL 4.6 core 18.3.2: the two images must be COMPATIBLE, and compatible means their texel
|
||||
// blocks are the same SIZE - not that they share a base internal format. RGBA32UI into
|
||||
// RGBA32F is legal (both 128-bit) while RGBA8 into RGBA32F is not, and a compressed image
|
||||
// pairs with an uncompressed one whose texel is as big as the compressed block.
|
||||
Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock,
|
||||
const CopyImageTexelBlock& dstBlock);
|
||||
// GL 4.6 core 18.3.2: for a compressed image the region's origin must sit on a block
|
||||
// boundary and its size must be a whole number of blocks - unless the edge it runs to is
|
||||
// the edge of the image.
|
||||
Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height,
|
||||
Int imageWidth, Int imageHeight, const char* endpointName);
|
||||
// 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);
|
||||
|
||||
@@ -527,7 +527,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (!MG_Backend::pActiveBackendObject ||
|
||||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
||||
MGLOG_I("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
|
||||
MGLOG_W_ONCE("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
|
||||
"backend has no double-precision vertex attribute support - see the "
|
||||
"\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs",
|
||||
attribindex);
|
||||
|
||||
@@ -166,32 +166,32 @@ MOBILEGL_GLX_API int glXSwapIntervalSGI(int interval) {
|
||||
|
||||
// Legacy entry points some loaders probe for; harmless no-op stubs.
|
||||
MOBILEGL_GLX_API void glXCopyContext(Display*, void*, void*, unsigned long) {
|
||||
MGLOG_W("glx: glXCopyContext is not supported");
|
||||
MGLOG_W_ONCE("glx: glXCopyContext is not supported");
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API unsigned long glXCreateGLXPixmap(Display*, void*, unsigned long) {
|
||||
MGLOG_W("glx: glXCreateGLXPixmap is not supported");
|
||||
MGLOG_W_ONCE("glx: glXCreateGLXPixmap is not supported");
|
||||
return 0;
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXDestroyGLXPixmap(Display*, unsigned long) {}
|
||||
|
||||
MOBILEGL_GLX_API unsigned long glXCreatePixmap(Display*, void*, unsigned long, const int*) {
|
||||
MGLOG_W("glx: glXCreatePixmap is not supported");
|
||||
MGLOG_W_ONCE("glx: glXCreatePixmap is not supported");
|
||||
return 0;
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXDestroyPixmap(Display*, unsigned long) {}
|
||||
|
||||
MOBILEGL_GLX_API unsigned long glXCreatePbuffer(Display*, void*, const int*) {
|
||||
MGLOG_W("glx: glXCreatePbuffer is not supported");
|
||||
MGLOG_W_ONCE("glx: glXCreatePbuffer is not supported");
|
||||
return 0;
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXDestroyPbuffer(Display*, unsigned long) {}
|
||||
|
||||
MOBILEGL_GLX_API void glXUseXFont(unsigned long, int, int, int) {
|
||||
MGLOG_W("glx: glXUseXFont is not supported");
|
||||
MGLOG_W_ONCE("glx: glXUseXFont is not supported");
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXSelectEvent(Display*, unsigned long, unsigned long) {}
|
||||
|
||||
@@ -149,7 +149,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
fns->Sync = reinterpret_cast<decltype(fns->Sync)>(dlsym(fns->Library, "XSync"));
|
||||
}
|
||||
if (!fns->Valid()) {
|
||||
MGLOG_E("glx: failed to load libX11 entry points");
|
||||
MGLOG_E_ONCE("glx: failed to load libX11 entry points");
|
||||
}
|
||||
return fns;
|
||||
}();
|
||||
@@ -175,14 +175,14 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
|
||||
struct ContextObject {
|
||||
Display* XDisplay = nullptr;
|
||||
EGLDisplay Display = EGL_NO_DISPLAY;
|
||||
EGLDisplay Dpy = EGL_NO_DISPLAY;
|
||||
EGLConfig Config = nullptr;
|
||||
EGLContext Context = EGL_NO_CONTEXT;
|
||||
const FBConfigInfo* FBConfig = nullptr;
|
||||
};
|
||||
|
||||
struct DrawableSurface {
|
||||
EGLDisplay Display = EGL_NO_DISPLAY;
|
||||
EGLDisplay Dpy = EGL_NO_DISPLAY;
|
||||
EGLSurface Surface = EGL_NO_SURFACE;
|
||||
Uint32 Width = 0;
|
||||
Uint32 Height = 0;
|
||||
@@ -294,7 +294,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
if (width == surface.Width && height == surface.Height) {
|
||||
return;
|
||||
}
|
||||
if (EGLImpl::ResizePlatformWindowSurface(surface.Display, surface.Surface,
|
||||
if (EGLImpl::ResizePlatformWindowSurface(surface.Dpy, surface.Surface,
|
||||
static_cast<EGLint>(width),
|
||||
static_cast<EGLint>(height))) {
|
||||
surface.Width = width;
|
||||
@@ -314,7 +314,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
Uint32 width = 0;
|
||||
Uint32 height = 0;
|
||||
if (!QueryDrawableSize(dpy, drawable, width, height)) {
|
||||
MGLOG_E("glx: XGetGeometry failed for drawable 0x%lx", drawable);
|
||||
MGLOG_E_ONCE("glx: XGetGeometry failed for drawable 0x%lx", drawable);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -324,15 +324,15 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
EGL_NONE,
|
||||
};
|
||||
EGLSurface surface = EGLImpl::CreatePlatformWindowSurface(
|
||||
context.Display, context.Config, reinterpret_cast<void*>(drawable), attribs);
|
||||
context.Dpy, context.Config, reinterpret_cast<void*>(drawable), attribs);
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
MGLOG_E("glx: failed to create window surface for drawable 0x%lx (%ux%u)", drawable,
|
||||
MGLOG_E_ONCE("glx: failed to create window surface for drawable 0x%lx (%ux%u)", drawable,
|
||||
width, height);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
DrawableSurface record;
|
||||
record.Display = context.Display;
|
||||
record.Dpy = context.Dpy;
|
||||
record.Surface = surface;
|
||||
record.Width = width;
|
||||
record.Height = height;
|
||||
@@ -347,7 +347,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
EGLDisplay display = EnsureDisplay();
|
||||
if (display == EGL_NO_DISPLAY) {
|
||||
MGLOG_E("glx: no EGL display");
|
||||
MGLOG_E_ONCE("glx: no EGL display");
|
||||
return nullptr;
|
||||
}
|
||||
EGLImpl::BindAPI(EGL_OPENGL_API);
|
||||
@@ -376,19 +376,19 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
EGLint configCount = 0;
|
||||
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) ||
|
||||
configCount <= 0) {
|
||||
MGLOG_E("glx: eglChooseConfig failed");
|
||||
MGLOG_E_ONCE("glx: eglChooseConfig failed");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
|
||||
if (eglContext == EGL_NO_CONTEXT) {
|
||||
MGLOG_E("glx: eglCreateContext failed");
|
||||
MGLOG_E_ONCE("glx: eglCreateContext failed");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ContextObject object;
|
||||
object.XDisplay = dpy;
|
||||
object.Display = display;
|
||||
object.Dpy = display;
|
||||
object.Config = config;
|
||||
object.Context = eglContext;
|
||||
object.FBConfig = fbconfig;
|
||||
@@ -895,7 +895,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
return;
|
||||
}
|
||||
if (object->Context != EGL_NO_CONTEXT) {
|
||||
EGLImpl::DestroyContext(object->Display, object->Context);
|
||||
EGLImpl::DestroyContext(object->Dpy, object->Context);
|
||||
}
|
||||
Contexts().erase(context);
|
||||
}
|
||||
@@ -929,9 +929,9 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface,
|
||||
if (!EGLImpl::MakeCurrent(object->Dpy, surface->Surface, surface->Surface,
|
||||
object->Context)) {
|
||||
MGLOG_E("glx: eglMakeCurrent failed (drawable=0x%lx, ctx=%p)", drawable, context);
|
||||
MGLOG_E_ONCE("glx: eglMakeCurrent failed (drawable=0x%lx, ctx=%p)", drawable, context);
|
||||
return 0;
|
||||
}
|
||||
t_current = {dpy, drawable, drawable, context};
|
||||
@@ -943,7 +943,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
if (context && draw != read) {
|
||||
// MobileGL's backends reject split draw/read surfaces; bind the draw
|
||||
// drawable for both, which is what every real caller here needs.
|
||||
MGLOG_W("glx: glXMakeContextCurrent draw 0x%lx != read 0x%lx, using draw for both", draw,
|
||||
MGLOG_W_ONCE("glx: glXMakeContextCurrent draw 0x%lx != read 0x%lx, using draw for both", draw,
|
||||
read);
|
||||
}
|
||||
const int result = MakeCurrent(dpy, draw, context);
|
||||
@@ -958,11 +958,11 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
auto& surfaces = DrawableSurfaces();
|
||||
auto it = surfaces.find(drawable);
|
||||
if (it == surfaces.end()) {
|
||||
MGLOG_W("glx: glXSwapBuffers with no surface for drawable 0x%lx", drawable);
|
||||
MGLOG_W_ONCE("glx: glXSwapBuffers with no surface for drawable 0x%lx", drawable);
|
||||
return;
|
||||
}
|
||||
SyncSurfaceSize(dpy, drawable, it->second);
|
||||
EGLImpl::SwapBuffers(it->second.Display, it->second.Surface);
|
||||
EGLImpl::SwapBuffers(it->second.Dpy, it->second.Surface);
|
||||
}
|
||||
|
||||
GLXDrawableHandle CreateWindow(Display*, GLXFBConfigHandle config, GLXDrawableHandle window,
|
||||
@@ -988,7 +988,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
||||
if (it == surfaces.end()) {
|
||||
return;
|
||||
}
|
||||
EGLImpl::DestroySurface(it->second.Display, it->second.Surface);
|
||||
EGLImpl::DestroySurface(it->second.Dpy, it->second.Surface);
|
||||
surfaces.erase(it);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace MG_Impl::GLXImpl {
|
||||
#endif
|
||||
void* proc = MobileGL::MG_Impl::GetProcAddress(name);
|
||||
if (!proc) {
|
||||
MGLOG_W("Failed to get function: %s", (const char*)name);
|
||||
MGLOG_D("Failed to get function: %s", (const char*)name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
@@ -1403,7 +1403,7 @@ namespace MobileGL::MG_Impl {
|
||||
GETPROC(glFramebufferTextureMultiviewOVR, name);
|
||||
// GETPROC(glNamedFramebufferTextureMultiviewOVR, name);
|
||||
|
||||
MGLOG_W("GetProcAddress(%s) = nullptr!", name);
|
||||
MGLOG_D("GetProcAddress(%s) = nullptr!", name);
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl
|
||||
|
||||
@@ -269,7 +269,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
}
|
||||
id metalLayerClass = reinterpret_cast<id>(objc_getClass("CAMetalLayer"));
|
||||
if (!metalLayerClass) {
|
||||
MGLOG_E("NSOpenGLImpl: CAMetalLayer class not found");
|
||||
MGLOG_E_ONCE("NSOpenGLImpl: CAMetalLayer class not found");
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
static_cast<GLint>(geometry.DrawableSize.width),
|
||||
static_cast<GLint>(geometry.DrawableSize.height));
|
||||
if (error != kCGLNoError) {
|
||||
MGLOG_E("NSOpenGLImpl: failed to attach drawable: %s", CGLImpl::ErrorString(error));
|
||||
MGLOG_E_ONCE("NSOpenGLImpl: failed to attach drawable: %s", CGLImpl::ErrorString(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +325,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
}
|
||||
const auto error = CGLImpl::SetCurrentContext(context);
|
||||
if (error != kCGLNoError) {
|
||||
MGLOG_E("NSOpenGLImpl: makeCurrentContext failed: %s", CGLImpl::ErrorString(error));
|
||||
MGLOG_E_ONCE("NSOpenGLImpl: makeCurrentContext failed: %s", CGLImpl::ErrorString(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
}
|
||||
const auto error = CGLImpl::FlushDrawable(context);
|
||||
if (error != kCGLNoError) {
|
||||
MGLOG_E("NSOpenGLImpl: flushBuffer failed: %s", CGLImpl::ErrorString(error));
|
||||
MGLOG_E_ONCE("NSOpenGLImpl: flushBuffer failed: %s", CGLImpl::ErrorString(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,7 +377,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
static_cast<GLint>(geometry.DrawableSize.width),
|
||||
static_cast<GLint>(geometry.DrawableSize.height));
|
||||
if (error != kCGLNoError) {
|
||||
MGLOG_E("NSOpenGLImpl: update failed to attach drawable: %s", CGLImpl::ErrorString(error));
|
||||
MGLOG_E_ONCE("NSOpenGLImpl: update failed to attach drawable: %s", CGLImpl::ErrorString(error));
|
||||
return;
|
||||
}
|
||||
CGLImpl::UpdateContext(context);
|
||||
@@ -421,7 +421,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
SEL selector = sel_registerName(selectorName);
|
||||
Method method = class_getInstanceMethod(cls, selector);
|
||||
if (!method) {
|
||||
MGLOG_W("NSOpenGLImpl: missing instance method %s", selectorName);
|
||||
MGLOG_W_ONCE("NSOpenGLImpl: missing instance method %s", selectorName);
|
||||
return;
|
||||
}
|
||||
if (original) {
|
||||
@@ -434,7 +434,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
SEL selector = sel_registerName(selectorName);
|
||||
Method method = class_getClassMethod(cls, selector);
|
||||
if (!method) {
|
||||
MGLOG_W("NSOpenGLImpl: missing class method %s", selectorName);
|
||||
MGLOG_W_ONCE("NSOpenGLImpl: missing class method %s", selectorName);
|
||||
return;
|
||||
}
|
||||
method_setImplementation(method, replacement);
|
||||
@@ -444,7 +444,7 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
||||
Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat");
|
||||
Class contextClass = objc_getClass("NSOpenGLContext");
|
||||
if (!pixelFormatClass || !contextClass) {
|
||||
MGLOG_W("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
|
||||
MGLOG_W_ONCE("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ extern "C" HGLRC WINAPI wglCreateLayerContext(HDC hdc, int iLayerPlane) {
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglCopyContext(HGLRC, HGLRC, UINT) {
|
||||
MGLOG_W("wglCopyContext is not supported");
|
||||
MGLOG_W_ONCE("wglCopyContext is not supported");
|
||||
SetLastError(ERROR_NOT_SUPPORTED);
|
||||
return FALSE;
|
||||
}
|
||||
@@ -132,24 +132,24 @@ extern "C" DWORD WINAPI wglSwapMultipleBuffers(UINT n, CONST WGLSWAP* ps) {
|
||||
// ---- Font rendering (legacy immediate-mode feature; not supported) ----
|
||||
|
||||
extern "C" BOOL WINAPI wglUseFontBitmapsA(HDC, DWORD, DWORD, DWORD) {
|
||||
MGLOG_W("wglUseFontBitmapsA is not supported");
|
||||
MGLOG_W_ONCE("wglUseFontBitmapsA is not supported");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglUseFontBitmapsW(HDC, DWORD, DWORD, DWORD) {
|
||||
MGLOG_W("wglUseFontBitmapsW is not supported");
|
||||
MGLOG_W_ONCE("wglUseFontBitmapsW is not supported");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglUseFontOutlinesA(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
|
||||
LPGLYPHMETRICSFLOAT) {
|
||||
MGLOG_W("wglUseFontOutlinesA is not supported");
|
||||
MGLOG_W_ONCE("wglUseFontOutlinesA is not supported");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
extern "C" BOOL WINAPI wglUseFontOutlinesW(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
|
||||
LPGLYPHMETRICSFLOAT) {
|
||||
MGLOG_W("wglUseFontOutlinesW is not supported");
|
||||
MGLOG_W_ONCE("wglUseFontOutlinesW is not supported");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
||||
Uint32 width = 0;
|
||||
Uint32 height = 0;
|
||||
if (!QueryClientSize(hwnd, width, height)) {
|
||||
MGLOG_E("wgl: GetClientRect failed for HWND %p", hwnd);
|
||||
MGLOG_E_ONCE("wgl: GetClientRect failed for HWND %p", hwnd);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
||||
EGLSurface surface =
|
||||
EGLImpl::CreatePlatformWindowSurface(context.Display, context.Config, hwnd, attribs);
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
MGLOG_E("wgl: failed to create window surface for HWND %p (%ux%u)", hwnd, width, height);
|
||||
MGLOG_E_ONCE("wgl: failed to create window surface for HWND %p (%ux%u)", hwnd, width, height);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||
EGLDisplay display = EnsureDisplay();
|
||||
if (display == EGL_NO_DISPLAY) {
|
||||
MGLOG_E("wgl: no EGL display");
|
||||
MGLOG_E_ONCE("wgl: no EGL display");
|
||||
return nullptr;
|
||||
}
|
||||
EGLImpl::BindAPI(EGL_OPENGL_API);
|
||||
@@ -275,13 +275,13 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
||||
EGLConfig config = nullptr;
|
||||
EGLint configCount = 0;
|
||||
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) || configCount <= 0) {
|
||||
MGLOG_E("wgl: eglChooseConfig failed");
|
||||
MGLOG_E_ONCE("wgl: eglChooseConfig failed");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
|
||||
if (eglContext == EGL_NO_CONTEXT) {
|
||||
MGLOG_E("wgl: eglCreateContext failed");
|
||||
MGLOG_E_ONCE("wgl: eglCreateContext failed");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -612,7 +612,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
||||
auto& surfaces = WindowSurfaces();
|
||||
auto it = surfaces.find(hwnd);
|
||||
if (it == surfaces.end()) {
|
||||
MGLOG_W("wglSwapBuffers: no surface for HWND %p", hwnd);
|
||||
MGLOG_W_ONCE("wglSwapBuffers: no surface for HWND %p", hwnd);
|
||||
return FALSE;
|
||||
}
|
||||
SyncSurfaceSize(hwnd, it->second);
|
||||
@@ -685,7 +685,7 @@ namespace MobileGL::MG_Impl::WGLImpl {
|
||||
}
|
||||
|
||||
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface, object->Context)) {
|
||||
MGLOG_E("wglMakeCurrent: eglMakeCurrent failed (hdc=%p, hglrc=%p)", hdc, hglrc);
|
||||
MGLOG_E_ONCE("wglMakeCurrent: eglMakeCurrent failed (hdc=%p, hglrc=%p)", hdc, hglrc);
|
||||
return FALSE;
|
||||
}
|
||||
t_current = {hdc, hglrc};
|
||||
|
||||
@@ -63,6 +63,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/DepthStencilReadbackMatrixScenario.cpp
|
||||
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
|
||||
Scenarios/ClipDistanceScenario.cpp
|
||||
Scenarios/ViewportArrayScenario.cpp
|
||||
Scenarios/SsboArrayLengthScenario.cpp
|
||||
Scenarios/DoublePrecisionScenario.cpp
|
||||
Scenarios/UniformInitializerScenario.cpp
|
||||
@@ -70,6 +71,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/ProgramPipelineScenario.cpp
|
||||
Scenarios/ImageLoadStoreSsoScenario.cpp
|
||||
Scenarios/ImageTargetKindScenario.cpp
|
||||
Scenarios/ImageFormatQualifierScenario.cpp
|
||||
Scenarios/SsboDeclarationFormScenario.cpp
|
||||
Scenarios/Glsl420DeclarationScenario.cpp
|
||||
Scenarios/FragmentOutputArrayIndexScenario.cpp
|
||||
@@ -77,6 +79,9 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/VertexAttribBindingScenario.cpp
|
||||
Scenarios/XfbCaptureBufferReuseScenario.cpp
|
||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||
Scenarios/CopyImageLayeredScenario.cpp
|
||||
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
|
||||
@@ -199,5 +199,61 @@ namespace MGITest {
|
||||
"derived component limits are computed in";
|
||||
}
|
||||
|
||||
// ARB_viewport_array's own limits. They are advertised from three different places -
|
||||
// GL_MAX_VIEWPORTS from the frontend's indexed state width, the bounds range and the
|
||||
// subpixel bits from the backend caps table - and each backend fills that table from a
|
||||
// different source, so all three are checked on both lanes.
|
||||
//
|
||||
// GL_VIEWPORT_BOUNDS_RANGE is the one that shipped wrong: GLES has no such query, the
|
||||
// DirectGLES loader's glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE) therefore raised
|
||||
// GL_INVALID_ENUM and left the probe's zero-initialized array in place, and MobileGL
|
||||
// advertised [0, 0] - a range that admits no viewport origin at all, and the check that
|
||||
// kept KHR-GL43.viewport_array.queries red on Espryt after the indexed-state work.
|
||||
TEST_F(AdvertisedLimitsScenario, ViewportArrayLimitsMeetTheirGL43Floors) {
|
||||
GLint maxViewports = -1;
|
||||
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_GE(maxViewports, 16) << "GL 4.3 core table 23.53 sets the MAX_VIEWPORTS minimum at 16";
|
||||
EXPECT_LE(maxViewports, 256) << "one viewport rectangle of indexed state is allocated per advertised "
|
||||
"viewport, and the CTS sizes its arrays off this number";
|
||||
|
||||
GLfloat boundsRange[2] = {1.0f, -1.0f};
|
||||
glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE, boundsRange);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_LE(boundsRange[0], -32768.0f)
|
||||
<< "GL 4.6 core table 23.60 sets the VIEWPORT_BOUNDS_RANGE minimum at [-32768, 32767]; got ["
|
||||
<< boundsRange[0] << ", " << boundsRange[1] << "]";
|
||||
EXPECT_GE(boundsRange[1], 32767.0f)
|
||||
<< "GL 4.6 core table 23.60 sets the VIEWPORT_BOUNDS_RANGE minimum at [-32768, 32767]; got ["
|
||||
<< boundsRange[0] << ", " << boundsRange[1] << "]";
|
||||
|
||||
// KNOWN INFIDELITY, pinned here rather than hidden. MobileGL reports the driver's own
|
||||
// VIEWPORT_SUBPIXEL_BITS (4 on llvmpipe, i.e. 1/16-pixel viewport precision), but the
|
||||
// float viewport rectangle glViewportIndexedf stores is snapped to integers on its
|
||||
// way to both backends (ComputeGLViewport, DirectGLES SyncRenderState). The STATE
|
||||
// round trip is exact - which is all KHR-GL43.viewport_array.viewport_api checks, and
|
||||
// all this cluster set out to fix - so the gap is in rasterization only: a fractional
|
||||
// viewport origin rasterizes as if it had been rounded. Nothing in the suite or in
|
||||
// Minecraft sets one. Only the spec floor is asserted; tightening this to EQ(0) would
|
||||
// mean advertising no subpixel precision at all, which is a separate decision about a
|
||||
// limit MobileGL currently passes through from the driver.
|
||||
GLint subpixelBits = -1;
|
||||
glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &subpixelBits);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_GE(subpixelBits, 0) << "GL 4.6 core table 23.60: VIEWPORT_SUBPIXEL_BITS has a minimum of 0, and "
|
||||
"a negative value is what a sign-flipped uint32 looks like";
|
||||
|
||||
GLint viewportDims[2] = {-1, -1};
|
||||
glGetIntegerv(GL_MAX_VIEWPORT_DIMS, viewportDims);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
GLint maxRenderbufferSize = -1;
|
||||
glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maxRenderbufferSize);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
// GL 4.6 core 13.6.1: MAX_VIEWPORT_DIMS must be at least as large as the largest
|
||||
// renderable surface, or a full-size framebuffer could not be fully viewported.
|
||||
EXPECT_GE(viewportDims[0], maxRenderbufferSize);
|
||||
EXPECT_GE(viewportDims[1], maxRenderbufferSize);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImageLayeredScenario.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 - glCopyImageSubData MOVES EVERY SLICE IT WAS ASKED FOR, NOT JUST SLICE 0.
|
||||
//
|
||||
// KHR-GL43.copy_image.functional_* copies a whole 12-layer region in one call whenever both
|
||||
// endpoints are layered, i.e. for the four target pairs 2d_array->2d_array, 2d_array->3d,
|
||||
// 3d->2d_array and 3d->3d. DirectVulkan built its VkImageCopy with baseArrayLayer 0, layerCount 1
|
||||
// and srcOffset.z 0 no matter what the call asked for, so slice 0 landed correctly and slices 1..N
|
||||
// were never written - 64 conformance cases (16 compatible format pairs x those 4 pairs) failing
|
||||
// with "first mismatch at [x, y, 1]", the first texel of the first slice the copy skipped.
|
||||
//
|
||||
// The reason one hardcode covered both shapes wrongly is that GL states a layered copy ONE way -
|
||||
// srcZ/dstZ and srcDepth - while Vulkan states it two ways and picks by image type:
|
||||
//
|
||||
// GL_TEXTURE_3D -> VK_IMAGE_TYPE_3D: slices are z, so srcOffset.z/dstOffset.z select them
|
||||
// and extent.depth counts them; the layer range must stay (0, 1).
|
||||
// GL_TEXTURE_2D_ARRAY -> VK_IMAGE_TYPE_2D: slices are array layers, so baseArrayLayer selects
|
||||
// them and layerCount counts them; offset.z stays 0.
|
||||
//
|
||||
// A mixed pair is legal (maintenance1, core in Vulkan 1.1) but only when the counts correspond:
|
||||
// the 3D side's extent.depth has to equal the array side's layerCount. So the four pairs below are
|
||||
// four DIFFERENT VkImageCopy shapes, not one shape with different arguments, which is why one
|
||||
// scenario per pair is the coverage that matters here.
|
||||
//
|
||||
// Every case also asserts the slices OUTSIDE the copied range still hold their fill. A backend
|
||||
// that "fixed" the miss by copying the whole image regardless of srcZ/srcDepth would pass a
|
||||
// slices-landed check and fail this one.
|
||||
//
|
||||
// The verification path is an FBO attachment per slice plus glReadPixels, not glGetTexImage: it is
|
||||
// the readback both backends share, and glFramebufferTextureLayer names an array layer and a 3D
|
||||
// slice through the same call, so the two texture kinds are read back identically.
|
||||
//
|
||||
// DirectGLES is the control - it forwards to the driver's own glCopyImageSubData - so a failure on
|
||||
// both backends means the scenario is wrong, and a failure on DirectVulkan alone means Magma is.
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kWidth = 4;
|
||||
constexpr int kHeight = 4;
|
||||
// Six is enough for a copy that starts and ends away from both edges of both endpoints
|
||||
// while still leaving untouched slices on either side to assert against.
|
||||
constexpr int kSlices = 6;
|
||||
|
||||
struct Rgba8 {
|
||||
GLubyte r = 0, g = 0, b = 0, a = 0;
|
||||
|
||||
bool operator==(const Rgba8& other) const {
|
||||
return r == other.r && g == other.g && b == other.b && a == other.a;
|
||||
}
|
||||
};
|
||||
|
||||
std::string Describe(const Rgba8& color) {
|
||||
return "(" + std::to_string(color.r) + ", " + std::to_string(color.g) + ", " + std::to_string(color.b) +
|
||||
", " + std::to_string(color.a) + ")";
|
||||
}
|
||||
|
||||
// Per-slice constants, uniform within a slice. A uniform fill is deliberate: the defect is
|
||||
// in which SLICE the copy addresses, and a value that also varied within the slice would
|
||||
// make the assertions depend on the framebuffer row order as well.
|
||||
Rgba8 SourceColor(int slice) {
|
||||
return {static_cast<GLubyte>(10 + slice * 20), static_cast<GLubyte>(40 + slice * 10),
|
||||
static_cast<GLubyte>(200 - slice * 15), 255};
|
||||
}
|
||||
|
||||
Rgba8 DestinationFill(int slice) {
|
||||
return {static_cast<GLubyte>(3 + slice), static_cast<GLubyte>(250 - slice * 7),
|
||||
static_cast<GLubyte>(120 + slice * 5), 255};
|
||||
}
|
||||
|
||||
class CopyImageLayeredScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
if (!CopyImageSubDataUsable()) {
|
||||
GTEST_SKIP() << "glCopyImageSubData is unavailable on backend " << Gl().BackendName();
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
for (const GLuint texture : m_textures) {
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
m_textures.clear();
|
||||
if (m_fbo != 0) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &m_fbo);
|
||||
m_fbo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// A trivial 1x1x1 array-to-array copy: it exercises the entry point without depending
|
||||
// on any of the behaviour under test, so a driver (or a backend function table) that
|
||||
// simply does not have the call skips instead of failing every case below.
|
||||
bool CopyImageSubDataUsable() {
|
||||
GLuint probe[2] = {0, 0};
|
||||
glGenTextures(2, probe);
|
||||
for (const GLuint texture : probe) {
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 1, 1, 1);
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
glCopyImageSubData(probe[0], GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, probe[1], GL_TEXTURE_2D_ARRAY, 0, 0, 0,
|
||||
0, 1, 1, 1);
|
||||
const bool usable = glGetError() == GL_NO_ERROR;
|
||||
glDeleteTextures(2, probe);
|
||||
return usable;
|
||||
}
|
||||
|
||||
// `target` is GL_TEXTURE_2D_ARRAY or GL_TEXTURE_3D; both take glTexStorage3D and
|
||||
// glTexSubImage3D with the slice on the same axis, which is the whole reason GL can
|
||||
// copy between them. `levels` > 1 puts a real mip chain behind the level the copy
|
||||
// names, so the level's own extent - a 3D level's depth included - has to be resolved
|
||||
// rather than assumed to be the image's.
|
||||
GLuint MakeTexture(GLenum target, int levels, Rgba8 (*colorForSlice)(int)) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(target, texture);
|
||||
glTexStorage3D(target, levels, GL_RGBA8, kWidth << (levels - 1), kHeight << (levels - 1),
|
||||
target == GL_TEXTURE_3D ? (kSlices << (levels - 1)) : kSlices);
|
||||
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
|
||||
// Fill every level, so nothing below can pass by reading a level that was never
|
||||
// written and happened to hold the expected bytes.
|
||||
for (int level = 0; level < levels; ++level) {
|
||||
const int levelWidth = kWidth << (levels - 1 - level);
|
||||
const int levelHeight = kHeight << (levels - 1 - level);
|
||||
const int levelSlices =
|
||||
target == GL_TEXTURE_3D ? (kSlices << (levels - 1 - level)) : kSlices;
|
||||
for (int slice = 0; slice < levelSlices; ++slice) {
|
||||
const Rgba8 color = colorForSlice(slice % kSlices);
|
||||
std::vector<Rgba8> texels(static_cast<size_t>(levelWidth) * levelHeight, color);
|
||||
glTexSubImage3D(target, level, 0, 0, slice, levelWidth, levelHeight, 1, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, texels.data());
|
||||
}
|
||||
}
|
||||
glBindTexture(target, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// One slice of one level, through an FBO attachment. glFramebufferTextureLayer takes an
|
||||
// array layer and a 3D slice through the same argument, so both targets read back the
|
||||
// same way.
|
||||
Rgba8 ReadSlice(GLuint texture, int level, int slice, int width, int height) {
|
||||
if (m_fbo == 0) {
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, level, slice);
|
||||
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "slice " << slice << " of level " << level << " is not attachable";
|
||||
std::vector<Rgba8> pixels(static_cast<size_t>(width) * height, Rgba8{});
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
// The fill is uniform within a slice, so any disagreement between texels is itself
|
||||
// a failure - reported here rather than silently reduced to pixels[0].
|
||||
for (size_t i = 1; i < pixels.size(); ++i) {
|
||||
EXPECT_TRUE(pixels[i] == pixels[0])
|
||||
<< "slice " << slice << " of level " << level << " is not uniform: texel 0 is "
|
||||
<< Describe(pixels[0]) << ", texel " << i << " is " << Describe(pixels[i]);
|
||||
}
|
||||
return pixels[0];
|
||||
}
|
||||
|
||||
// The assertion every case ends with: slices inside [dstZ, dstZ + depth) hold the
|
||||
// source slice they were fed, and every slice outside it still holds its own fill.
|
||||
void ExpectCopied(GLuint destination, int level, int width, int height, int sliceCount, int srcZ,
|
||||
int dstZ, int depth, const char* what) {
|
||||
for (int slice = 0; slice < sliceCount; ++slice) {
|
||||
const bool inRange = slice >= dstZ && slice < dstZ + depth;
|
||||
const Rgba8 expected =
|
||||
inRange ? SourceColor(srcZ + (slice - dstZ)) : DestinationFill(slice);
|
||||
const Rgba8 actual = ReadSlice(destination, level, slice, width, height);
|
||||
EXPECT_TRUE(actual == expected)
|
||||
<< what << ": destination slice " << slice << (inRange ? " (copied)" : " (untouched)")
|
||||
<< " is " << Describe(actual) << ", expected " << Describe(expected);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_textures;
|
||||
GLuint m_fbo = 0;
|
||||
};
|
||||
|
||||
// 2d_array -> 2d_array. Both endpoints put the slices on the layer axis, so BOTH layer
|
||||
// counts carry the depth and extent.depth must stay 1.
|
||||
TEST_F(CopyImageLayeredScenario, ArrayToArrayCopiesEverySlice) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0,
|
||||
kWidth, kHeight, kSlices);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, 0, 0, kSlices, "array->array, all slices");
|
||||
}
|
||||
|
||||
// The same pair with the layer ranges offset differently on the two sides: the shape that
|
||||
// separates "copies more than slice 0" from "copies the RIGHT slices". A backend that read
|
||||
// the source range but wrote from layer 0 (or vice versa) passes the case above.
|
||||
TEST_F(CopyImageLayeredScenario, ArrayToArrayHonoursDifferentLayerOffsets) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 3;
|
||||
constexpr int kDstZ = 1;
|
||||
constexpr int kDepth = 2;
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0,
|
||||
kDstZ, kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth,
|
||||
"array->array, offset layer ranges");
|
||||
}
|
||||
|
||||
// 3d -> 3d. Neither endpoint has array layers at all: the depth travels on extent.depth and
|
||||
// the offsets on srcOffset.z/dstOffset.z, with both layer counts pinned to 1.
|
||||
TEST_F(CopyImageLayeredScenario, VolumeToVolumeHonoursNonZeroZ) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_3D, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 1;
|
||||
constexpr int kDstZ = 3;
|
||||
constexpr int kDepth = 3;
|
||||
glCopyImageSubData(source, GL_TEXTURE_3D, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, 0, 0, 0, kDstZ,
|
||||
kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "3d->3d, non-zero z");
|
||||
}
|
||||
|
||||
// The same pair one mip level down. A 3D level's DEPTH halves with its width and height, so
|
||||
// this is the only case where the slice count the copy may name is not the image's own -
|
||||
// the bound a layered endpoint is checked against has to come from the level.
|
||||
TEST_F(CopyImageLayeredScenario, VolumeToVolumeAtNonZeroMipLevel) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_3D, 2, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 2, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kLevel = 1;
|
||||
constexpr int kSrcZ = 2;
|
||||
constexpr int kDstZ = 0;
|
||||
constexpr int kDepth = 4;
|
||||
glCopyImageSubData(source, GL_TEXTURE_3D, kLevel, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, kLevel, 0, 0,
|
||||
kDstZ, kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, kLevel, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth,
|
||||
"3d->3d at mip level 1");
|
||||
}
|
||||
|
||||
// 2d_array -> 3d. The mixed shape: the source counts its slices as layers, the destination
|
||||
// as depth, and Vulkan requires extent.depth to equal the source's layerCount.
|
||||
TEST_F(CopyImageLayeredScenario, ArrayToVolumeCopiesEverySlice) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 2;
|
||||
constexpr int kDstZ = 1;
|
||||
constexpr int kDepth = 4;
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, 0, 0, 0, kDstZ,
|
||||
kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "2d_array->3d");
|
||||
}
|
||||
|
||||
// 3d -> 2d_array, the mirror image: the depth now has to reach the DESTINATION's layerCount
|
||||
// while the source states it as extent.depth from a z offset.
|
||||
TEST_F(CopyImageLayeredScenario, VolumeToArrayCopiesEverySlice) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_3D, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 1;
|
||||
constexpr int kDstZ = 2;
|
||||
constexpr int kDepth = 4;
|
||||
glCopyImageSubData(source, GL_TEXTURE_3D, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kDstZ,
|
||||
kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "3d->2d_array");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,209 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImageLevelRangeScenario.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
|
||||
//
|
||||
// KHR-GL43.copy_image.non_existent_mipmap, and what it cost.
|
||||
//
|
||||
// The CTS case is a pure negative test: two 16x16 textures that have level 0 and
|
||||
// nothing else, and a glCopyImageSubData naming level 1. The answer is
|
||||
// GL_INVALID_VALUE (GL 4.6 core 18.3.2 / ARB_copy_image: "srcLevel/dstLevel is not
|
||||
// a valid level"). MobileGL's frontend only checked the level against
|
||||
// GL_MAX_TEXTURE_SIZE, so level 1 sailed through into the backends, DirectVulkan
|
||||
// resolved it into a VkImageCopy subresource on a VkImage that was created with
|
||||
// exactly one mip level, and the Adreno driver dereferenced the level it was
|
||||
// promised - SIGSEGV inside vkCmdCopyImage, taking the whole glcts process down
|
||||
// mid-run. A negative case must never do that.
|
||||
//
|
||||
// So the level-1-on-a-one-level-texture rejection is the regression proper, and the
|
||||
// rest of this file is what keeps the fix honest. A validator that answered
|
||||
// GL_INVALID_VALUE to every level would satisfy the regression tests alone, so the
|
||||
// scenarios below pin the BOUNDARY rather than the symptom:
|
||||
//
|
||||
// * a texture that really does have two levels must accept a copy at level 1,
|
||||
// * the same texture must still reject level 2,
|
||||
// * and a plain level-0 copy must move pixels, which is checked by reading the
|
||||
// destination back rather than by trusting glGetError.
|
||||
//
|
||||
// Both backends are covered because the fix is in the shared frontend: DirectGLES
|
||||
// forwards to the ES glCopyImageSubData (whose own error lands in the ES context,
|
||||
// not in MobileGL's, so it never reached the application either) and DirectVulkan
|
||||
// records the copy itself.
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr GLsizei kSize = 16;
|
||||
|
||||
struct Rgba8 {
|
||||
GLubyte r, g, b, a;
|
||||
bool operator==(const Rgba8& other) const {
|
||||
return r == other.r && g == other.g && b == other.b && a == other.a;
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<Rgba8> SolidImage(GLsizei width, GLsizei height, Rgba8 color) {
|
||||
return std::vector<Rgba8>(static_cast<std::size_t>(width) * static_cast<std::size_t>(height), color);
|
||||
}
|
||||
|
||||
class CopyImageLevelRangeScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
DeleteTextures();
|
||||
if (m_fbo != 0) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &m_fbo);
|
||||
m_fbo = 0;
|
||||
}
|
||||
DrainErrors();
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
void DeleteTextures() {
|
||||
if (m_src != 0) glDeleteTextures(1, &m_src);
|
||||
if (m_dst != 0) glDeleteTextures(1, &m_dst);
|
||||
m_src = 0;
|
||||
m_dst = 0;
|
||||
}
|
||||
|
||||
// One 16x16 RGBA8 texture with `levelCount` levels defined through
|
||||
// glTexImage2D - the same way the CTS case builds its textures, and
|
||||
// deliberately NOT glTexStorage2D: an immutable allocation would define the
|
||||
// whole chain up front and could not express "level 1 does not exist".
|
||||
GLuint MakeTexture(int levelCount, Rgba8 baseColor) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
for (int level = 0; level < levelCount; ++level) {
|
||||
const GLsizei extent = kSize >> level;
|
||||
const std::vector<Rgba8> pixels = SolidImage(extent, extent, baseColor);
|
||||
glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, extent, extent, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
pixels.data());
|
||||
}
|
||||
// What Utils::makeTextureComplete does in the CTS case: the texture is
|
||||
// complete for the levels it actually has, not for a chain it does not.
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, levelCount - 1);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
void MakePair(int levelCount) {
|
||||
DeleteTextures();
|
||||
m_src = MakeTexture(levelCount, Rgba8{11, 22, 33, 255});
|
||||
m_dst = MakeTexture(levelCount, Rgba8{200, 100, 50, 255});
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "texture setup with " << levelCount << " level(s)";
|
||||
}
|
||||
|
||||
// The call under test, at whatever levels the caller wants, over a 1x1
|
||||
// region so the region check can never be what rejects it.
|
||||
GLenum CopyAt(GLint srcLevel, GLint dstLevel, GLsizei extent = 1) {
|
||||
DrainErrors();
|
||||
glCopyImageSubData(m_src, GL_TEXTURE_2D, srcLevel, 0, 0, 0, m_dst, GL_TEXTURE_2D, dstLevel, 0, 0, 0,
|
||||
extent, extent, 1);
|
||||
const GLenum error = glGetError();
|
||||
// A second pending error would mean the entry point queued more than one,
|
||||
// and the extra would be handed out at an unrelated call site later.
|
||||
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "the copy recorded more than one error";
|
||||
return error;
|
||||
}
|
||||
|
||||
Rgba8 ReadBackDestinationLevel0() {
|
||||
if (m_fbo == 0) glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_dst, 0);
|
||||
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
ADD_FAILURE() << "readback framebuffer incomplete: " << status;
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
return Rgba8{0, 0, 0, 0};
|
||||
}
|
||||
Rgba8 texel{0, 0, 0, 0};
|
||||
glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &texel);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
return texel;
|
||||
}
|
||||
|
||||
GLuint m_src = 0;
|
||||
GLuint m_dst = 0;
|
||||
GLuint m_fbo = 0;
|
||||
};
|
||||
|
||||
// The regression. Level 1 of a texture that has only level 0 is not a level, and
|
||||
// saying so is the whole job: before the fix this reached DirectVulkan, which
|
||||
// handed mipLevel=1 to vkCmdCopyImage on a one-level VkImage and died inside the
|
||||
// Adreno driver.
|
||||
TEST_F(CopyImageLevelRangeScenario, LevelOneOfASingleLevelTextureIsRejected) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakePair(1);
|
||||
|
||||
EXPECT_EQ(CopyAt(1, 0), static_cast<GLenum>(GL_INVALID_VALUE)) << "source level 1";
|
||||
EXPECT_EQ(CopyAt(0, 1), static_cast<GLenum>(GL_INVALID_VALUE)) << "destination level 1";
|
||||
EXPECT_EQ(CopyAt(1, 1), static_cast<GLenum>(GL_INVALID_VALUE)) << "both levels 1";
|
||||
}
|
||||
|
||||
// The negative control that makes the test above falsifiable: the same level
|
||||
// index, on textures that genuinely have it, must be accepted. A validator that
|
||||
// rejected every non-zero level would pass the regression test and fail here.
|
||||
TEST_F(CopyImageLevelRangeScenario, LevelOneOfATwoLevelTextureIsAccepted) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakePair(2);
|
||||
|
||||
EXPECT_EQ(CopyAt(1, 1), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
// And the boundary from the other side: two levels means 0 and 1, not 2.
|
||||
TEST_F(CopyImageLevelRangeScenario, LevelTwoOfATwoLevelTextureIsRejected) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakePair(2);
|
||||
|
||||
EXPECT_EQ(CopyAt(2, 0), static_cast<GLenum>(GL_INVALID_VALUE)) << "source level 2";
|
||||
EXPECT_EQ(CopyAt(0, 2), static_cast<GLenum>(GL_INVALID_VALUE)) << "destination level 2";
|
||||
}
|
||||
|
||||
// Errors alone cannot tell an accepted copy from a silently dropped one, so the
|
||||
// ordinary case is checked by reading the destination back: the copy has to move
|
||||
// the source's texel, not merely decline to complain.
|
||||
TEST_F(CopyImageLevelRangeScenario, AValidLevelZeroCopyStillMovesPixels) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakePair(1);
|
||||
|
||||
ASSERT_EQ(ReadBackDestinationLevel0(), (Rgba8{200, 100, 50, 255})) << "destination before the copy";
|
||||
EXPECT_EQ(CopyAt(0, 0, kSize), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_EQ(ReadBackDestinationLevel0(), (Rgba8{11, 22, 33, 255})) << "destination after the copy";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,314 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageFormatQualifierScenario.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 - AN IMAGE UNIFORM THAT DECLARES NO FORMAT.
|
||||
//
|
||||
// Desktop GLSL 4.2 lets a writeonly image declaration omit its format layout qualifier:
|
||||
//
|
||||
// writeonly uniform uimage2D uni_image; // legal desktop GLSL
|
||||
//
|
||||
// GLSL ES has no such relaxation; every image uniform must carry one, and Adreno says so as "all
|
||||
// images have to define layout format", which fails the whole program. That is what took the
|
||||
// compute half of KHR-GL4x.packed_depth_stencil.stencil_texturing.
|
||||
//
|
||||
// The only qualifier that is CORRECT to substitute is whatever glBindImageTexture named for the
|
||||
// unit that uniform addresses - GL requires the qualifier, the bind format and the texture's
|
||||
// internal format to belong to one format class - so the format is not knowable when the shader
|
||||
// is compiled, only when it is drawn with. Espryt therefore BAKES it into the program it
|
||||
// generates and keys that program on the (unit, format) pairs it baked
|
||||
// (BackendProgramObjectImpl::ImageUnitFormatsStillMatch, MG_Backend/DirectGLES).
|
||||
//
|
||||
// Three separate things follow from "the program is built against live binding state", and each
|
||||
// one is a case below:
|
||||
//
|
||||
// 1. the format reaches the shader at all, so the store lands where the texture is (Writes);
|
||||
// 2. binding a DIFFERENT format to the same unit rebuilds the program, rather than reusing one
|
||||
// compiled against the old format (RebindToADifferentFormatRebuilds);
|
||||
// 3. an image bound for the FIRST time after the link works, i.e. the program built against
|
||||
// "nothing bound yet" is not the one the dispatch runs (FirstBindAfterLinkRebuilds).
|
||||
//
|
||||
// Magma needs none of this - Vulkan takes an Unknown-format storage image given
|
||||
// shaderStorageImageWriteWithoutFormat, and the view format is resolved from the same bind state
|
||||
// at descriptor time - so every case here runs on both backends and must agree, which is what
|
||||
// makes the ES-only machinery falsifiable rather than merely exercised.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kExtent = 4;
|
||||
// The image unit is deliberately NOT 0 and the uniform declares no binding, so the unit
|
||||
// has to travel through glUniform1i and be baked into the ESSL alongside the format -
|
||||
// the two bakes share a rebuild key and a bug in either shows up as the wrong texel.
|
||||
constexpr GLint kImageUnit = 1;
|
||||
|
||||
// KHR-GL4x.packed_depth_stencil.stencil_texturing's own image declaration, verbatim.
|
||||
const char* kStoreSource = R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
writeonly uniform uimage2D uni_image;
|
||||
|
||||
void main()
|
||||
{
|
||||
imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(gl_GlobalInvocationID.x + 100u, 0u, 0u, 0u));
|
||||
}
|
||||
)";
|
||||
|
||||
class ImageFormatQualifierScenario : public ScenarioTest {
|
||||
protected:
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (GLuint p : m_programs) glDeleteProgram(p);
|
||||
for (GLuint t : m_textures) glDeleteTextures(1, &t);
|
||||
m_programs.clear();
|
||||
m_textures.clear();
|
||||
GLint maxImageUnits = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
for (GLint unit = 0; unit < maxImageUnits; ++unit) {
|
||||
glBindImageTexture(static_cast<GLuint>(unit), 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
|
||||
}
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
bool ImagesAreUsable() const {
|
||||
GLint maxImageUnits = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
GLint maxComputeImageUniforms = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return maxImageUnits > kImageUnit && maxComputeImageUniforms >= 1;
|
||||
}
|
||||
|
||||
GLuint MakeComputeProgram(const std::string& source) {
|
||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||
const char* text = source.c_str();
|
||||
glShaderSource(shader, 1, &text, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute shader did not compile: " << log;
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
m_programs.push_back(program);
|
||||
glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
glDeleteShader(shader);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
char log[4096] = {};
|
||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
ADD_FAILURE() << "the compute program did not link: " << log;
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint MakeTexture(GLenum internalFormat) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kExtent, kExtent);
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << "allocating storage errored with " << GLErrorName(error);
|
||||
return 0;
|
||||
}
|
||||
// Seeded to a value no dispatch writes, so "the store never happened" and "the
|
||||
// store wrote the right thing" cannot be confused.
|
||||
const std::vector<GLuint> zeros(static_cast<std::size_t>(kExtent) * kExtent * 4u, 0u);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent,
|
||||
internalFormat == GL_RGBA32UI ? GL_RGBA_INTEGER : GL_RED_INTEGER, GL_UNSIGNED_INT,
|
||||
zeros.data());
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
// Texel (x, 0) of the texture's red channel, read back through the GL frontend rather
|
||||
// than through a second image uniform: a defect in the format bake would be shared by
|
||||
// a reader declared the same way and could cancel itself out.
|
||||
GLuint ReadRedTexel(GLuint texture, GLenum internalFormat, int x) {
|
||||
const bool rgba = internalFormat == GL_RGBA32UI;
|
||||
std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * (rgba ? 4u : 1u),
|
||||
0xFFFFFFFFu);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glGetTexImage(GL_TEXTURE_2D, 0, rgba ? GL_RGBA_INTEGER : GL_RED_INTEGER, GL_UNSIGNED_INT,
|
||||
texels.data());
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << "reading the image back errored with " << GLErrorName(error);
|
||||
return 0xFFFFFFFFu;
|
||||
}
|
||||
return texels[static_cast<std::size_t>(x) * (rgba ? 4u : 1u)];
|
||||
}
|
||||
|
||||
void DispatchStore(GLuint program, GLuint texture, GLenum internalFormat) {
|
||||
glBindImageTexture(static_cast<GLuint>(kImageUnit), texture, 0, GL_FALSE, 0, GL_WRITE_ONLY,
|
||||
internalFormat);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "glBindImageTexture errored";
|
||||
glUseProgram(program);
|
||||
const GLint location = glGetUniformLocation(program, "uni_image");
|
||||
ASSERT_GE(location, 0) << "the image uniform was not reflected";
|
||||
glUniform1i(location, kImageUnit);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "assigning the image unit errored";
|
||||
glDispatchCompute(kExtent, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_programs;
|
||||
std::vector<GLuint> m_textures;
|
||||
};
|
||||
|
||||
// The defect itself. Without the bake the ES driver refuses the program outright and the
|
||||
// texture keeps its seed - which is also exactly what a silently no-op dispatch looks
|
||||
// like, and why the seed is a value no store writes.
|
||||
TEST_F(ImageFormatQualifierScenario, AFormatlessWriteonlyImageWrites) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
const GLuint program = MakeComputeProgram(kStoreSource);
|
||||
const GLuint texture = MakeTexture(GL_R32UI);
|
||||
if (program == 0 || texture == 0) return;
|
||||
|
||||
DispatchStore(program, texture, GL_R32UI);
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||
<< "texel " << x << " of a format-less writeonly image did not take the store";
|
||||
}
|
||||
}
|
||||
|
||||
// The rebuild key. The SAME program is dispatched twice with a different format bound to
|
||||
// its unit; a build keyed only on the link (or only on the image UNIT) would reuse the
|
||||
// r32ui program for the rgba32ui texture, and the second half would come back seeded.
|
||||
//
|
||||
// What the SOFTWARE lanes cannot falsify: with the key disabled this case still passes on
|
||||
// Mesa, because the reused r32ui declaration writes the red channel of an RGBA32UI image
|
||||
// anyway - a format-class mismatch GL leaves undefined and that driver happens to absorb.
|
||||
// FirstBindAfterLinkRebuilds below is the case that fails there, because the reused
|
||||
// program was built with no format at all and never compiled. Both are kept: this one is
|
||||
// the shape a strict driver is entitled to reject, and it is the shape the device runs.
|
||||
TEST_F(ImageFormatQualifierScenario, RebindToADifferentFormatRebuilds) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
const GLuint program = MakeComputeProgram(kStoreSource);
|
||||
const GLuint first = MakeTexture(GL_R32UI);
|
||||
const GLuint second = MakeTexture(GL_RGBA32UI);
|
||||
if (program == 0 || first == 0 || second == 0) return;
|
||||
|
||||
DispatchStore(program, first, GL_R32UI);
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
ASSERT_EQ(ReadRedTexel(first, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||
<< "the first format must work before the rebind can be blamed for anything";
|
||||
}
|
||||
|
||||
DispatchStore(program, second, GL_RGBA32UI);
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
EXPECT_EQ(ReadRedTexel(second, GL_RGBA32UI, x), static_cast<GLuint>(x) + 100u)
|
||||
<< "texel " << x << ": the program was not rebuilt for the newly bound format";
|
||||
}
|
||||
|
||||
// ...and back, so the rebuild is not a one-way door: returning to a format the
|
||||
// program was once built against must build for it again, not resurrect a cache row.
|
||||
const GLuint third = MakeTexture(GL_R32UI);
|
||||
if (third == 0) return;
|
||||
DispatchStore(program, third, GL_R32UI);
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
EXPECT_EQ(ReadRedTexel(third, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||
<< "texel " << x << ": going back to the first format did not rebuild";
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing is bound to the unit when the program links, so whatever the first build sees
|
||||
// is not the format the dispatch needs. glBindImageTexture must not itself trigger a
|
||||
// build - it is an entry point, and building there is the constraint
|
||||
// glShaderStorageBlockBinding is held to as well - so the rebuild has to happen at the
|
||||
// next dispatch preparation instead. This case fails either way round: no rebuild, or a
|
||||
// build attempted from the entry point before the state settles.
|
||||
TEST_F(ImageFormatQualifierScenario, FirstBindAfterLinkRebuilds) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
const GLuint program = MakeComputeProgram(kStoreSource);
|
||||
if (program == 0) return;
|
||||
|
||||
// Use it once with NOTHING bound to the unit, which is what makes the backend build
|
||||
// against an empty binding. The dispatch writes nowhere and must not error.
|
||||
glUseProgram(program);
|
||||
const GLint location = glGetUniformLocation(program, "uni_image");
|
||||
ASSERT_GE(location, 0);
|
||||
glUniform1i(location, kImageUnit);
|
||||
glDispatchCompute(kExtent, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "dispatching with an unbound image unit must not error";
|
||||
glUseProgram(0);
|
||||
|
||||
const GLuint texture = MakeTexture(GL_R32UI);
|
||||
if (texture == 0) return;
|
||||
DispatchStore(program, texture, GL_R32UI);
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||
<< "texel " << x << ": the first bind after the link did not reach the shader";
|
||||
}
|
||||
}
|
||||
|
||||
// A DECLARED format is authoritative and the bake must never touch it - including when
|
||||
// the texture behind the unit has a different (but class-compatible) internal format,
|
||||
// which GL explicitly allows. If the bake ever overrode a declaration, this is the case
|
||||
// that would go wrong while every other one stayed green.
|
||||
TEST_F(ImageFormatQualifierScenario, ADeclaredFormatStillWins) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
const GLuint program = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (r32ui) writeonly uniform uimage2D uni_image;
|
||||
|
||||
void main()
|
||||
{
|
||||
imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(gl_GlobalInvocationID.x + 100u, 0u, 0u, 0u));
|
||||
}
|
||||
)");
|
||||
const GLuint texture = MakeTexture(GL_R32UI);
|
||||
if (program == 0 || texture == 0) return;
|
||||
|
||||
DispatchStore(program, texture, GL_R32UI);
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
||||
<< "texel " << x << ": a declared format stopped working";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,378 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/LayeredAttachmentBarrierScenario.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 TRANSFER OFF A NON-ZERO ATTACHMENT LAYER READS THE LAYER THE BARRIER MOVED.
|
||||
//
|
||||
// Every transfer DirectVulkan performs against a framebuffer attachment is three commands: a
|
||||
// barrier that puts the image in TRANSFER_SRC/DST, the copy or blit itself, and a barrier that
|
||||
// puts it back. The copy names the attachment's layer - glFramebufferTextureLayer(.., layer) ends
|
||||
// up in `srcSubresource.baseArrayLayer` - but TransitionImageLayout used to emit `layerCount = 1`
|
||||
// from `baseArrayLayer 0`, so for every attachment on a layer above zero the barrier moved layer 0
|
||||
// and the copy read layer N. The layer the transfer touched was never transitioned: it sat in
|
||||
// COLOR_ATTACHMENT_OPTIMAL (or DEPTH_STENCIL_ATTACHMENT_OPTIMAL) while being read as TRANSFER_SRC.
|
||||
//
|
||||
// That is undefined behaviour, not a guaranteed wrong pixel: a layout is a compression/tiling
|
||||
// promise, so a driver that stores both layouts identically returns the right bytes anyway. The
|
||||
// software lanes (lavapipe) are exactly such a driver, which is why this scenario is paired with a
|
||||
// validation-layer run - the layer names the mismatch outright
|
||||
// (VUID-vkCmdCopyImageToBuffer-srcImageLayout-00189, "srcImageLayout ... doesn't match the actual
|
||||
// current layout") where the pixels here cannot. On a tiler that really does re-tile per layout,
|
||||
// these are the reads that come back as garbage.
|
||||
//
|
||||
// The four cases below are the four transfer paths that take an attachment layer from GL:
|
||||
//
|
||||
// glReadPixels (colour) -> VulkanRenderer::ReadPixels
|
||||
// glBlitFramebuffer (colour) -> VulkanRenderer::BlitNamedFramebuffer
|
||||
// glReadPixels (GL_DEPTH_COMPONENT) -> VulkanRenderer::ReadDepthStencilImageToClient
|
||||
// glBlitFramebuffer (GL_DEPTH_BUFFER_BIT) -> VulkanRenderer::BlitNamedFramebuffer, depth leg
|
||||
//
|
||||
// Each one renders or clears INTO the non-zero layer first, so the image is genuinely sitting in
|
||||
// its attachment layout when the transfer starts - a scenario that only uploaded texels would
|
||||
// leave it in a transfer layout already and the mismatched barrier would be a no-op.
|
||||
//
|
||||
// Every case also asserts the layers it did not name still hold their own fill, so a backend that
|
||||
// "fixed" the miss by transferring the whole image passes neither half.
|
||||
//
|
||||
// DirectGLES is the control: it hands the same calls to the driver, so a failure on both backends
|
||||
// means the scenario is wrong and a failure on DirectVulkan alone means Magma is.
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kWidth = 8;
|
||||
constexpr int kHeight = 8;
|
||||
// Four layers with the subject at index 2: layers on both sides of it stay untouched, so
|
||||
// "moved the whole image" and "moved layer 0" are both distinguishable from correct.
|
||||
constexpr int kLayers = 4;
|
||||
constexpr int kSubjectLayer = 2;
|
||||
|
||||
// A value no correct read can produce, so "the backend wrote nothing" fails loudly.
|
||||
constexpr float kDepthPoison = 0.2f;
|
||||
|
||||
std::string Describe(const Rgba8& color) {
|
||||
return "(" + std::to_string(color.r) + ", " + std::to_string(color.g) + ", " + std::to_string(color.b) +
|
||||
", " + std::to_string(color.a) + ")";
|
||||
}
|
||||
|
||||
// Per-layer fill, uniform within a layer: the defect is about WHICH layer is addressed, and
|
||||
// a value that also varied inside the layer would make the assertions depend on row order.
|
||||
Rgba8 LayerFill(int layer) {
|
||||
return {static_cast<GLubyte>(17 + layer * 30), static_cast<GLubyte>(200 - layer * 25),
|
||||
static_cast<GLubyte>(60 + layer * 40), 255};
|
||||
}
|
||||
|
||||
// What the draw paints - matches kFS below, and is deliberately none of the LayerFill
|
||||
// values so "the draw never landed" cannot read as a pass.
|
||||
constexpr Rgba8 kPaintedColor{26, 51, 204, 255};
|
||||
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(0.1, 0.2, 0.8, 1.0); }
|
||||
)";
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
class LayeredAttachmentBarrierScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
for (const GLuint fbo : m_fbos) {
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
}
|
||||
m_fbos.clear();
|
||||
for (const GLuint texture : m_textures) {
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
m_textures.clear();
|
||||
if (m_program != 0) {
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(m_program);
|
||||
m_program = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// An RGBA8 2D array with a different uniform colour per layer.
|
||||
GLuint MakeColorArray() {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kWidth, kHeight, kLayers);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
for (int layer = 0; layer < kLayers; ++layer) {
|
||||
const std::vector<Rgba8> texels(static_cast<std::size_t>(kWidth) * kHeight, LayerFill(layer));
|
||||
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, kWidth, kHeight, 1, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, texels.data());
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// A depth 2D array. No initial upload: depth arrays are filled by clearing through an
|
||||
// attachment, which is also the state the transfer paths have to cope with.
|
||||
GLuint MakeDepthArray() {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH_COMPONENT24, kWidth, kHeight, kLayers);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// One FBO naming `layer` of the given arrays. Depth is optional (0 = colour only).
|
||||
GLuint MakeLayerFbo(GLuint colorArray, GLuint depthArray, int layer) {
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
m_fbos.push_back(fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, colorArray, 0, layer);
|
||||
if (depthArray != 0) {
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthArray, 0, layer);
|
||||
}
|
||||
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "layer " << layer << " is not attachable";
|
||||
return fbo;
|
||||
}
|
||||
|
||||
// glReadPixels of one whole layer, through an FBO that names it.
|
||||
Rgba8 ReadLayer(GLuint colorArray, int layer) {
|
||||
const GLuint fbo = MakeLayerFbo(colorArray, 0, layer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
std::vector<Rgba8> pixels(static_cast<std::size_t>(kWidth) * kHeight, Rgba8{});
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
// The fill is uniform within a layer, so any disagreement between texels is itself
|
||||
// a failure - reported here rather than silently reduced to pixels[0].
|
||||
for (std::size_t i = 1; i < pixels.size(); ++i) {
|
||||
EXPECT_TRUE(pixels[i] == pixels[0])
|
||||
<< "layer " << layer << " is not uniform: texel 0 is " << Describe(pixels[0]) << ", texel "
|
||||
<< i << " is " << Describe(pixels[i]);
|
||||
}
|
||||
return pixels[0];
|
||||
}
|
||||
|
||||
// Every layer but `changed` still holds its own fill.
|
||||
void ExpectOtherLayersUntouched(GLuint colorArray, int changed, const char* what) {
|
||||
for (int layer = 0; layer < kLayers; ++layer) {
|
||||
if (layer == changed) continue;
|
||||
const Rgba8 actual = ReadLayer(colorArray, layer);
|
||||
EXPECT_TRUE(actual == LayerFill(layer))
|
||||
<< what << ": layer " << layer << " should still hold its fill but is " << Describe(actual)
|
||||
<< ", expected " << Describe(LayerFill(layer));
|
||||
}
|
||||
}
|
||||
|
||||
float ReadDepthAt(int x, int y) const {
|
||||
float depth = kDepthPoison;
|
||||
glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);
|
||||
return depth;
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_textures;
|
||||
std::vector<GLuint> m_fbos;
|
||||
unsigned int m_program = 0;
|
||||
};
|
||||
|
||||
// glReadPixels straight off a layer that was just rendered to. The image is in
|
||||
// COLOR_ATTACHMENT_OPTIMAL when the readback barrier runs, so the barrier and the copy
|
||||
// disagreeing about the layer is a live layout mismatch, not a bookkeeping detail.
|
||||
TEST_F(LayeredAttachmentBarrierScenario, ReadPixelsOffRenderedNonZeroLayer) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint colorArray = MakeColorArray();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||
|
||||
const GLuint fbo = MakeLayerFbo(colorArray, 0, kSubjectLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
DrawFullViewportQuad(m_program);
|
||||
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
std::vector<Rgba8> pixels(static_cast<std::size_t>(kWidth) * kHeight, Rgba8{});
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
for (std::size_t i = 0; i < pixels.size(); ++i) {
|
||||
ASSERT_NEAR(pixels[i].r, kPaintedColor.r, 2)
|
||||
<< "texel " << i << " of the rendered layer is " << Describe(pixels[i]);
|
||||
ASSERT_NEAR(pixels[i].g, kPaintedColor.g, 2) << "texel " << i;
|
||||
ASSERT_NEAR(pixels[i].b, kPaintedColor.b, 2) << "texel " << i;
|
||||
}
|
||||
|
||||
ExpectOtherLayersUntouched(colorArray, kSubjectLayer, "readback off a rendered layer");
|
||||
}
|
||||
|
||||
// glBlitFramebuffer between two non-zero layers of two different arrays. Both endpoints are
|
||||
// above layer 0, so the source and destination barriers are each wrong on their own side.
|
||||
TEST_F(LayeredAttachmentBarrierScenario, BlitBetweenNonZeroColorLayers) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint sourceArray = MakeColorArray();
|
||||
const GLuint destinationArray = MakeColorArray();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||
|
||||
constexpr int kSourceLayer = 3;
|
||||
constexpr int kDestinationLayer = 1;
|
||||
|
||||
const GLuint sourceFbo = MakeLayerFbo(sourceArray, 0, kSourceLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, sourceFbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
DrawFullViewportQuad(m_program);
|
||||
|
||||
const GLuint destinationFbo = MakeLayerFbo(destinationArray, 0, kDestinationLayer);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFbo);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, destinationFbo);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glBlitFramebuffer(0, 0, kWidth, kHeight, 0, 0, kWidth, kHeight, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const Rgba8 blitted = ReadLayer(destinationArray, kDestinationLayer);
|
||||
EXPECT_NEAR(blitted.r, kPaintedColor.r, 2) << "blit destination layer is " << Describe(blitted);
|
||||
EXPECT_NEAR(blitted.g, kPaintedColor.g, 2);
|
||||
EXPECT_NEAR(blitted.b, kPaintedColor.b, 2);
|
||||
|
||||
ExpectOtherLayersUntouched(destinationArray, kDestinationLayer, "colour blit destination");
|
||||
// The source layer was rendered, not blitted into, so it is checked separately.
|
||||
const Rgba8 source = ReadLayer(sourceArray, kSourceLayer);
|
||||
EXPECT_NEAR(source.r, kPaintedColor.r, 2) << "blit source layer is " << Describe(source);
|
||||
ExpectOtherLayersUntouched(sourceArray, kSourceLayer, "colour blit source");
|
||||
}
|
||||
|
||||
// The depth aspect of the same readback path: the depth image sits in
|
||||
// DEPTH_STENCIL_ATTACHMENT_OPTIMAL after the clear, and the copy names the attached layer.
|
||||
TEST_F(LayeredAttachmentBarrierScenario, ReadDepthOffClearedNonZeroLayer) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint colorArray = MakeColorArray();
|
||||
const GLuint depthArray = MakeDepthArray();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||
|
||||
const GLuint fbo = MakeLayerFbo(colorArray, depthArray, kSubjectLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(0.375);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
const float centre = ReadDepthAt(kWidth / 2, kHeight / 2);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(centre, 0.375f, 1.0f / 4096.0f)
|
||||
<< "glReadPixels(GL_DEPTH_COMPONENT) off layer " << kSubjectLayer << " returned " << centre
|
||||
<< (std::fabs(centre - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");
|
||||
}
|
||||
|
||||
// The depth leg of the blit path, both endpoints above layer 0. Verified by reading the
|
||||
// destination's depth back, which is the same readback the case above pins - so a failure
|
||||
// here with that one passing is the blit, not the readback.
|
||||
TEST_F(LayeredAttachmentBarrierScenario, BlitDepthBetweenNonZeroLayers) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint sourceColor = MakeColorArray();
|
||||
const GLuint sourceDepth = MakeDepthArray();
|
||||
const GLuint destinationColor = MakeColorArray();
|
||||
const GLuint destinationDepth = MakeDepthArray();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||
|
||||
constexpr int kSourceLayer = 3;
|
||||
constexpr int kDestinationLayer = 1;
|
||||
|
||||
const GLuint sourceFbo = MakeLayerFbo(sourceColor, sourceDepth, kSourceLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, sourceFbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(0.625);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
// A destination pre-cleared to something the blit must overwrite, so "the blit did
|
||||
// nothing" and "the blit landed" are different answers.
|
||||
const GLuint destinationFbo = MakeLayerFbo(destinationColor, destinationDepth, kDestinationLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destinationFbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(0.125);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, destinationFbo);
|
||||
glBlitFramebuffer(0, 0, kWidth, kHeight, 0, 0, kWidth, kHeight, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destinationFbo);
|
||||
const float blitted = ReadDepthAt(kWidth / 2, kHeight / 2);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(blitted, 0.625f, 1.0f / 4096.0f)
|
||||
<< "depth blitted onto layer " << kDestinationLayer << " reads back as " << blitted
|
||||
<< (std::fabs(blitted - 0.125f) < 1e-3f ? " - the destination kept its own clear" : "");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,524 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ViewportArrayScenario.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_ViewportIndex ACTUALLY ROUTES, AND THE PER-INDEX STATE IT SELECTS IS REAL.
|
||||
//
|
||||
// The state half of ARB_viewport_array is asserted in MG_Test/State/RenderStateTest.cpp, which
|
||||
// is a pure set/get exercise and would pass just as green against a backend that stores all 16
|
||||
// rectangles and rasterizes only the first. This file is the other half: every case here routes
|
||||
// primitives to a viewport OTHER than 0 and then looks at where the pixels landed.
|
||||
//
|
||||
// Three claims, one per case:
|
||||
// 1. gl_ViewportIndex selects the viewport RECTANGLE - a 4x4 grid of 32x32 viewports, one
|
||||
// geometry-shader invocation per cell, and every cell must hold its own index.
|
||||
// 2. gl_ViewportIndex selects the DEPTH RANGE - 16 one-pixel-wide viewports whose ranges are
|
||||
// (i/16, 1 - i/16), a quad at each end of clip space, and gl_FragCoord.z read back.
|
||||
// This is the claim that fails loudest against a single-viewport backend, because the
|
||||
// geometry is still in the right place while every depth comes back as viewport 0's.
|
||||
// 3. The per-index SCISSOR TEST ENABLE is honoured. Vulkan has no per-viewport scissor-test
|
||||
// toggle, so a disabled index has to be given the whole framebuffer as its rectangle; the
|
||||
// case draws the same primitive into the same index twice, once with the test off and once
|
||||
// with it on, and requires the two results to differ in the documented direction.
|
||||
//
|
||||
// Case 1 runs a second time against the DEFAULT framebuffer. MobileGL Y-flips (and pre-transform
|
||||
// rotates) the default framebuffer's rectangles and does not touch an FBO's, so a port that
|
||||
// applies the flip to viewport 0 and forgets the other fifteen renders a correct-looking FBO and
|
||||
// an upside-down window - the classic multi-viewport bug, and invisible to every FBO-only case.
|
||||
//
|
||||
// HONEST LIMIT OF THIS FILE. DirectGLES SKIPS every case: GLES has one viewport, one scissor
|
||||
// rectangle and no gl_ViewportIndex, so routing to index > 0 is an emulation feature that has
|
||||
// not been built (the Espryt half of KHR-GL43.viewport_array's rendering group is deliberately
|
||||
// still red). The skip is explicit rather than silent so a future emulation lands here as a
|
||||
// failing test and not as a test that was quietly never running. DirectVulkan additionally
|
||||
// skips when the device lacks the multiViewport feature - Vulkan then forbids a pipeline from
|
||||
// declaring more than one viewport at all, which is a device limit and not a MobileGL bug;
|
||||
// lavapipe (every CI lane) and both Mali/Adreno devices support it, so the cases do run where
|
||||
// it matters.
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kViewportCount = 16;
|
||||
constexpr int kGridSide = 4; // 4x4 grid of viewports
|
||||
constexpr int kCellSize = 32; // ... each 32x32
|
||||
constexpr int kSurfaceSide = kGridSide * kCellSize;
|
||||
constexpr GLint kUnwritten = -1;
|
||||
|
||||
// A geometry shader is the only stage GL 4.1 lets write gl_ViewportIndex, and
|
||||
// `invocations` runs it once per viewport off a single input point - the same shape
|
||||
// KHR-GL43.viewport_array.draw_to_single_layer_with_multiple_viewports uses.
|
||||
const char* const kVertexSource = R"(#version 410 core
|
||||
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
const char* const kGridGeometrySource = R"(#version 410 core
|
||||
layout(points, invocations = 16) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
flat out int gsIndex;
|
||||
void main() {
|
||||
gsIndex = gl_InvocationID;
|
||||
gl_ViewportIndex = gl_InvocationID;
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// One invocation, viewport chosen by a uniform: lets a case draw the SAME primitive into
|
||||
// the SAME index twice under two different scissor-enable states.
|
||||
const char* const kSingleGeometrySource = R"(#version 410 core
|
||||
layout(points, invocations = 1) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
uniform int uViewport;
|
||||
flat out int gsIndex;
|
||||
void main() {
|
||||
gsIndex = uViewport;
|
||||
gl_ViewportIndex = uViewport;
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kIntFragmentSource = R"(#version 410 core
|
||||
flat in int gsIndex;
|
||||
layout(location = 0) out int fragColor;
|
||||
void main() { fragColor = gsIndex; }
|
||||
)";
|
||||
|
||||
// Two quads, one at each end of clip space, so the fragment stage can report the depth
|
||||
// the viewport's range mapped them to. gl_FragCoord.z IS the post-range window depth, so
|
||||
// it reads back the per-viewport minDepth/maxDepth directly.
|
||||
const char* const kDepthGeometrySource = R"(#version 410 core
|
||||
layout(points, invocations = 16) in;
|
||||
layout(triangle_strip, max_vertices = 8) out;
|
||||
void main() {
|
||||
gl_ViewportIndex = gl_InvocationID;
|
||||
gl_Position = vec4(-1.0, -1.0, -1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, -1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 0.0, -1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 0.0, -1.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
gl_Position = vec4(-1.0, 0.0, 1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 0.0, 1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 1.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kDepthFragmentSource = R"(#version 410 core
|
||||
layout(location = 0) out float fragColor;
|
||||
void main() { fragColor = gl_FragCoord.z; }
|
||||
)";
|
||||
|
||||
class ViewportArrayScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
if (Gl().BackendName() == "DirectGLES") {
|
||||
GTEST_SKIP() << "gl_ViewportIndex routing is not emulated on DirectGLES: GLES has one viewport "
|
||||
"and one scissor rectangle, so every index rasterizes as index 0. The indexed "
|
||||
"STATE is still asserted (MG_Test RenderStateTest); this is the deferred "
|
||||
"rendering half of KHR-GL43.viewport_array.";
|
||||
}
|
||||
|
||||
GLint maxViewports = 0;
|
||||
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
ASSERT_GE(maxViewports, kViewportCount) << "GL 4.3 core requires GL_MAX_VIEWPORTS >= 16";
|
||||
|
||||
m_program = BuildProgram(kGridGeometrySource, kIntFragmentSource);
|
||||
ASSERT_NE(m_program, 0u) << "grid program failed to build: " << m_buildLog;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
ResetViewportArrayState();
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "setup left a GL error behind";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ResetViewportArrayState();
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
// Every case starts from the same slate: this fixture shares its context with every
|
||||
// other scenario in the process, and a leftover per-index scissor enable is exactly
|
||||
// the kind of state that would make a later case pass or fail for the wrong reason.
|
||||
static void ResetViewportArrayState() {
|
||||
for (int i = 0; i < kViewportCount; ++i) {
|
||||
glDisablei(GL_SCISSOR_TEST, static_cast<GLuint>(i));
|
||||
}
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glViewport(0, 0, kSurfaceSide, kSurfaceSide);
|
||||
glScissor(0, 0, kSurfaceSide, kSurfaceSide);
|
||||
glDepthRange(0.0, 1.0);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
}
|
||||
|
||||
// The 4x4 grid: viewport y*4+x covers the cell whose lower-left corner is
|
||||
// (x*cellW, y*cellH), in GL's bottom-left-origin window coordinates. Parameterized on
|
||||
// the cell size because the default framebuffer this scenario also renders into is
|
||||
// deliberately non-square (HeadlessGL is 128x96, so a transposing bug cannot hide).
|
||||
static void SetupGridViewports(int cellW, int cellH) {
|
||||
std::vector<GLfloat> data(static_cast<size_t>(kViewportCount) * 4);
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
const size_t base = static_cast<size_t>(y * kGridSide + x) * 4;
|
||||
data[base + 0] = static_cast<GLfloat>(x * cellW);
|
||||
data[base + 1] = static_cast<GLfloat>(y * cellH);
|
||||
data[base + 2] = static_cast<GLfloat>(cellW);
|
||||
data[base + 3] = static_cast<GLfloat>(cellH);
|
||||
}
|
||||
}
|
||||
glViewportArrayv(0, kViewportCount, data.data());
|
||||
}
|
||||
|
||||
GLuint BuildProgram(const char* geometrySource, const char* fragmentSource) {
|
||||
const GLuint vs = CompileStage(GL_VERTEX_SHADER, kVertexSource);
|
||||
if (vs == 0) return 0;
|
||||
const GLuint gs = CompileStage(GL_GEOMETRY_SHADER, geometrySource);
|
||||
if (gs == 0) {
|
||||
glDeleteShader(vs);
|
||||
return 0;
|
||||
}
|
||||
const GLuint fs = CompileStage(GL_FRAGMENT_SHADER, fragmentSource);
|
||||
if (fs == 0) {
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(gs);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, gs);
|
||||
glAttachShader(program, fs);
|
||||
glLinkProgram(program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(gs);
|
||||
glDeleteShader(fs);
|
||||
if (!linked) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
||||
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
m_buildLog = log.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint CompileStage(GLenum stage, const char* source) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled) return shader;
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
||||
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
m_buildLog = log.data();
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// An R32I colour target, pre-filled with kUnwritten so "nothing was drawn here" is
|
||||
// distinguishable from "index 0 was drawn here".
|
||||
struct IntTarget {
|
||||
GLuint fbo = 0;
|
||||
GLuint texture = 0;
|
||||
};
|
||||
|
||||
// The "nothing drawn here" value is UPLOADED, not cleared: the CTS fills its R32I
|
||||
// targets the same way (fillTexture), and an upload cannot be confused with a clear
|
||||
// that a backend defers, reorders or drops - which is exactly the ambiguity a case
|
||||
// asserting "this cell must be untouched" cannot afford.
|
||||
static void FillIntTarget(const IntTarget& target, int width, int height) {
|
||||
const std::vector<GLint> unwritten(static_cast<size_t>(width) * height, kUnwritten);
|
||||
glBindTexture(GL_TEXTURE_2D, target.texture);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RED_INTEGER, GL_INT, unwritten.data());
|
||||
}
|
||||
|
||||
static IntTarget MakeIntTarget(int width, int height) {
|
||||
IntTarget target;
|
||||
glGenTextures(1, &target.texture);
|
||||
glBindTexture(GL_TEXTURE_2D, target.texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32I, width, height, 0, GL_RED_INTEGER, GL_INT, nullptr);
|
||||
glGenFramebuffers(1, &target.fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
|
||||
FillIntTarget(target, width, height);
|
||||
return target;
|
||||
}
|
||||
|
||||
static void DestroyIntTarget(IntTarget& target) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (target.fbo != 0) glDeleteFramebuffers(1, &target.fbo);
|
||||
if (target.texture != 0) glDeleteTextures(1, &target.texture);
|
||||
}
|
||||
|
||||
static std::vector<GLint> ReadInts(int width, int height) {
|
||||
std::vector<GLint> pixels(static_cast<size_t>(width) * height, 0);
|
||||
glReadPixels(0, 0, width, height, GL_RED_INTEGER, GL_INT, pixels.data());
|
||||
return pixels;
|
||||
}
|
||||
|
||||
// The centre of grid cell (x, y), in the bottom-left-origin coordinates glReadPixels
|
||||
// returns. Sampling the centre rather than a corner keeps the assertion about WHICH
|
||||
// viewport was selected rather than about edge rounding.
|
||||
static GLint CellCentre(const std::vector<GLint>& pixels, int stride, int x, int y) {
|
||||
const int px = x * kCellSize + kCellSize / 2;
|
||||
const int py = y * kCellSize + kCellSize / 2;
|
||||
return pixels[static_cast<size_t>(py) * stride + px];
|
||||
}
|
||||
|
||||
std::string m_buildLog;
|
||||
GLuint m_program = 0;
|
||||
GLuint m_vao = 0;
|
||||
};
|
||||
|
||||
// --- 1. the viewport rectangle -------------------------------------------------------
|
||||
|
||||
TEST_F(ViewportArrayScenario, EachViewportIndexRasterizesIntoItsOwnRectangle) {
|
||||
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
||||
SetupGridViewports(kCellSize, kCellSize);
|
||||
glUseProgram(m_program);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
const GLint expected = y * kGridSide + x;
|
||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), expected)
|
||||
<< "cell (" << x << ", " << y << ") should hold viewport index " << expected
|
||||
<< "; a single-viewport backend paints the whole image with 15 (the last invocation)";
|
||||
}
|
||||
}
|
||||
DestroyIntTarget(target);
|
||||
}
|
||||
|
||||
// The same claim against the DEFAULT framebuffer, where MobileGL applies its Y-flip and
|
||||
// pre-transform rotation. Index 0 alone getting the mapping is the classic bug.
|
||||
TEST_F(ViewportArrayScenario, TheDefaultFramebufferAppliesTheSameFlipToEveryViewport) {
|
||||
const int surfaceW = Gl().Width();
|
||||
const int surfaceH = Gl().Height();
|
||||
ASSERT_GE(surfaceW, kGridSide);
|
||||
ASSERT_GE(surfaceH, kGridSide);
|
||||
const int cellW = surfaceW / kGridSide;
|
||||
const int cellH = surfaceH / kGridSide;
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
// Paint a value no viewport index can produce, so an unwritten cell is obvious.
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
// The default framebuffer is 8-bit RGBA, so the index travels as a colour: cell i is
|
||||
// painted with red = i * 16, which is exact in 8 bits for i in [0, 16).
|
||||
const char* const kColorFragmentSource = R"(#version 410 core
|
||||
flat in int gsIndex;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
const GLuint colorProgram = BuildProgram(kGridGeometrySource, kColorFragmentSource);
|
||||
ASSERT_NE(colorProgram, 0u) << "colour program failed to build: " << m_buildLog;
|
||||
|
||||
SetupGridViewports(cellW, cellH);
|
||||
glUseProgram(colorProgram);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
std::vector<unsigned char> pixels(static_cast<size_t>(surfaceW) * surfaceH * 4, 0);
|
||||
glReadPixels(0, 0, surfaceW, surfaceH, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
const int px = x * cellW + cellW / 2;
|
||||
const int py = y * cellH + cellH / 2;
|
||||
const int red = pixels[(static_cast<size_t>(py) * surfaceW + px) * 4];
|
||||
const int expected = (y * kGridSide + x) * 16;
|
||||
// One LSB of slack for an 8-bit round trip; the values are 16 apart, so this
|
||||
// cannot confuse two neighbouring indices.
|
||||
EXPECT_LE(std::abs(red - expected), 1)
|
||||
<< "default-framebuffer cell (" << x << ", " << y << ") holds red=" << red << ", expected "
|
||||
<< expected << ". A vertically mirrored grid means the Y-flip was applied to viewport 0 "
|
||||
<< "only";
|
||||
}
|
||||
}
|
||||
glDeleteProgram(colorProgram);
|
||||
}
|
||||
|
||||
// --- 2. the depth range --------------------------------------------------------------
|
||||
|
||||
TEST_F(ViewportArrayScenario, EachViewportIndexUsesItsOwnDepthRange) {
|
||||
// 16 columns one pixel wide and two rows tall: row 0 gets the near-plane quad, row 1
|
||||
// the far-plane one, so both ends of viewport i's range land in the same column.
|
||||
constexpr int kWidth = kViewportCount;
|
||||
constexpr int kHeight = 2;
|
||||
|
||||
GLuint texture = 0;
|
||||
GLuint fbo = 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_R32F, kWidth, kHeight, 0, GL_RED, GL_FLOAT, nullptr);
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||
const GLfloat clearValue[4] = {-1.0f, 0.0f, 0.0f, 0.0f};
|
||||
glClearBufferfv(GL_COLOR, 0, clearValue);
|
||||
|
||||
std::vector<GLfloat> viewports(static_cast<size_t>(kViewportCount) * 4);
|
||||
std::vector<GLdouble> ranges(static_cast<size_t>(kViewportCount) * 2);
|
||||
for (int i = 0; i < kViewportCount; ++i) {
|
||||
viewports[static_cast<size_t>(i) * 4 + 0] = static_cast<GLfloat>(i);
|
||||
viewports[static_cast<size_t>(i) * 4 + 1] = 0.0f;
|
||||
viewports[static_cast<size_t>(i) * 4 + 2] = 1.0f;
|
||||
viewports[static_cast<size_t>(i) * 4 + 3] = 2.0f;
|
||||
ranges[static_cast<size_t>(i) * 2 + 0] = static_cast<GLdouble>(i) / 16.0;
|
||||
ranges[static_cast<size_t>(i) * 2 + 1] = 1.0 - static_cast<GLdouble>(i) / 16.0;
|
||||
}
|
||||
glViewportArrayv(0, kViewportCount, viewports.data());
|
||||
glDepthRangeArrayv(0, kViewportCount, ranges.data());
|
||||
|
||||
const GLuint depthProgram = BuildProgram(kDepthGeometrySource, kDepthFragmentSource);
|
||||
ASSERT_NE(depthProgram, 0u) << "depth program failed to build: " << m_buildLog;
|
||||
glUseProgram(depthProgram);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
std::vector<GLfloat> pixels(static_cast<size_t>(kWidth) * kHeight, 0.0f);
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_RED, GL_FLOAT, pixels.data());
|
||||
for (int i = 0; i < kViewportCount; ++i) {
|
||||
const float near = static_cast<float>(i) / 16.0f;
|
||||
const float far = 1.0f - static_cast<float>(i) / 16.0f;
|
||||
// The tolerance covers depth-buffer-free rasterization of gl_FragCoord.z on a
|
||||
// software rasterizer; the per-index values are 1/16 apart, so it cannot let a
|
||||
// neighbouring viewport's range through, and viewport 0's range (0, 1) differs
|
||||
// from every other index by at least 1/16.
|
||||
EXPECT_NEAR(pixels[i], near, 1.0e-3f)
|
||||
<< "viewport " << i << " near-plane depth; got viewport 0's range if this is 0";
|
||||
EXPECT_NEAR(pixels[static_cast<size_t>(kWidth) + i], far, 1.0e-3f)
|
||||
<< "viewport " << i << " far-plane depth; got viewport 0's range if this is 1";
|
||||
}
|
||||
|
||||
glDeleteProgram(depthProgram);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
|
||||
// --- 3. the per-index scissor-test enable --------------------------------------------
|
||||
|
||||
TEST_F(ViewportArrayScenario, AnIndexedScissorEnableClipsOnlyThatIndex) {
|
||||
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
||||
|
||||
// One full-size viewport per index so the scissor rectangle is the ONLY thing that
|
||||
// can shrink the quad - the same separation KHR-GL43.viewport_array.scissor uses.
|
||||
glViewport(0, 0, kSurfaceSide, kSurfaceSide);
|
||||
std::vector<GLint> boxes(static_cast<size_t>(kViewportCount) * 4);
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
const size_t base = static_cast<size_t>(y * kGridSide + x) * 4;
|
||||
boxes[base + 0] = x * kCellSize;
|
||||
boxes[base + 1] = y * kCellSize;
|
||||
boxes[base + 2] = kCellSize;
|
||||
boxes[base + 3] = kCellSize;
|
||||
}
|
||||
}
|
||||
glScissorArrayv(0, kViewportCount, boxes.data());
|
||||
|
||||
const GLuint singleProgram = BuildProgram(kSingleGeometrySource, kIntFragmentSource);
|
||||
ASSERT_NE(singleProgram, 0u) << "single-viewport program failed to build: " << m_buildLog;
|
||||
glUseProgram(singleProgram);
|
||||
glBindVertexArray(m_vao);
|
||||
const GLint uViewport = glGetUniformLocation(singleProgram, "uViewport");
|
||||
ASSERT_NE(uViewport, -1);
|
||||
|
||||
constexpr GLint kProbeIndex = 6; // grid cell (2, 1)
|
||||
constexpr int kProbeX = kProbeIndex % kGridSide;
|
||||
constexpr int kProbeY = kProbeIndex / kGridSide;
|
||||
|
||||
// (a) scissor test ENABLED for this index: the quad is clipped to its 32x32 box.
|
||||
glUniform1i(uViewport, kProbeIndex);
|
||||
glEnablei(GL_SCISSOR_TEST, kProbeIndex);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
{
|
||||
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
|
||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, kProbeX, kProbeY), kProbeIndex)
|
||||
<< "the scissored index must still paint inside its own box";
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
if (x == kProbeX && y == kProbeY) continue;
|
||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), kUnwritten)
|
||||
<< "cell (" << x << ", " << y << ") is outside scissor rectangle " << kProbeIndex
|
||||
<< " and must be untouched";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (b) scissor test DISABLED for the same index, everything else identical: with no
|
||||
// per-viewport toggle in Vulkan this is the case that needs the disabled index to be
|
||||
// given the full framebuffer rectangle, and it is exactly where "leave the last
|
||||
// rectangle bound" would show up as a still-clipped quad.
|
||||
FillIntTarget(target, kSurfaceSide, kSurfaceSide);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
|
||||
glDisablei(GL_SCISSOR_TEST, kProbeIndex);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
{
|
||||
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), kProbeIndex)
|
||||
<< "with the scissor test off for index " << kProbeIndex
|
||||
<< ", its full-viewport quad must cover cell (" << x << ", " << y << ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glDeleteProgram(singleProgram);
|
||||
DestroyIntTarget(target);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -250,6 +250,34 @@ namespace MobileGL::MG_State::GLState {
|
||||
NotifyContentWrite(atOffset, data.size);
|
||||
}
|
||||
|
||||
void BufferObject::FillSubData(DataPtr pattern, SizeT atOffset, SizeT size) {
|
||||
MOBILEGL_ASSERT(pattern.data != nullptr && pattern.size > 0,
|
||||
"FillSubData requires a non-empty pattern.");
|
||||
MOBILEGL_ASSERT(size % pattern.size == 0,
|
||||
"FillSubData size (%zu) must be a multiple of pattern size (%zu).", size, pattern.size);
|
||||
MOBILEGL_ASSERT(atOffset <= m_size && size <= m_size - atOffset,
|
||||
"FillSubData out of bounds: atOffset (%zu) + size (%zu) > m_size (%zu)", atOffset, size,
|
||||
m_size);
|
||||
MOBILEGL_ASSERT(!m_isMapped || (m_mappingAccess & BufferMappingAccessBit::Persistent),
|
||||
"Cannot fill data while buffer is non-persistently mapped.");
|
||||
if (size == 0) return;
|
||||
|
||||
// A clear is ordered after all earlier GPU writes. Partial clears additionally need the
|
||||
// retained shadow bytes; whole-store clears need the same synchronization before writing
|
||||
// an adopted persistent mapping that the GPU may still be accessing.
|
||||
SyncGpuWrites();
|
||||
|
||||
Uint8* dst = m_resource.Bytes() + atOffset;
|
||||
if (pattern.size == 1) {
|
||||
Memset(dst, *static_cast<const Uint8*>(pattern.data), size);
|
||||
} else {
|
||||
for (SizeT at = 0; at < size; at += pattern.size) {
|
||||
Memcpy(dst + at, pattern.data, pattern.size);
|
||||
}
|
||||
}
|
||||
NotifyContentWrite(atOffset, size);
|
||||
}
|
||||
|
||||
void BufferObject::DownloadSubData(void* dst, SizeT atOffset, SizeT size) const {
|
||||
MOBILEGL_ASSERT(atOffset + size <= m_size,
|
||||
"DownloadSubData out of bounds: atOffset (%zu) + size (%zu) > m_size (%zu)", atOffset, size,
|
||||
|
||||
@@ -132,6 +132,9 @@ namespace MobileGL {
|
||||
|
||||
void UploadData(DataPtr data, SizeT atOffset);
|
||||
void UploadSubData(DataPtr data, SizeT atOffset);
|
||||
// Repeats one already-converted element through [atOffset, atOffset + size) and
|
||||
// publishes the range as one content mutation.
|
||||
void FillSubData(DataPtr pattern, SizeT atOffset, SizeT size);
|
||||
// Reads `size` bytes from the CPU shadow at `atOffset` into `dst` (glGetBufferSubData).
|
||||
// The shadow reflects CPU writes (BufferData/SubData/maps) and backend write-backs, but not
|
||||
// arbitrary GPU-side writes.
|
||||
|
||||
@@ -39,6 +39,11 @@ namespace MobileGL::MG_State {
|
||||
return m_compileEnv;
|
||||
}
|
||||
|
||||
void GLContext::InvalidateCompileEnv() {
|
||||
m_compileEnv.reset();
|
||||
m_compileEnvBackend = nullptr;
|
||||
}
|
||||
|
||||
// Error
|
||||
void GLContext::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
|
||||
// Invariant I1, mechanically enforced: the GL error state is GL-thread-owned.
|
||||
@@ -196,7 +201,7 @@ namespace MobileGL::MG_State {
|
||||
// (which expands to nothing outside debug builds).
|
||||
void GLContext::SetCurrentVertexAttributeFloat(Uint index, const Array<Float, 4>& value) {
|
||||
if (index >= m_currentVertexAttributes.size()) {
|
||||
MGLOG_E("SetCurrentVertexAttributeFloat: index %u is out of range", index);
|
||||
MGLOG_E_ONCE("SetCurrentVertexAttributeFloat: index %u is out of range", index);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -210,7 +215,7 @@ namespace MobileGL::MG_State {
|
||||
|
||||
void GLContext::SetCurrentVertexAttributeInt(Uint index, const Array<Int32, 4>& value) {
|
||||
if (index >= m_currentVertexAttributes.size()) {
|
||||
MGLOG_E("SetCurrentVertexAttributeInt: index %u is out of range", index);
|
||||
MGLOG_E_ONCE("SetCurrentVertexAttributeInt: index %u is out of range", index);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -224,7 +229,7 @@ namespace MobileGL::MG_State {
|
||||
|
||||
void GLContext::SetCurrentVertexAttributeUint(Uint index, const Array<Uint32, 4>& value) {
|
||||
if (index >= m_currentVertexAttributes.size()) {
|
||||
MGLOG_E("SetCurrentVertexAttributeUint: index %u is out of range", index);
|
||||
MGLOG_E_ONCE("SetCurrentVertexAttributeUint: index %u is out of range", index);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -239,7 +244,7 @@ namespace MobileGL::MG_State {
|
||||
const CurrentVertexAttributeValue& GLContext::GetCurrentVertexAttribute(Uint index) const {
|
||||
static const CurrentVertexAttributeValue defaultValue{};
|
||||
if (index >= m_currentVertexAttributes.size()) {
|
||||
MGLOG_E("GetCurrentVertexAttribute: index %u is out of range", index);
|
||||
MGLOG_E_ONCE("GetCurrentVertexAttribute: index %u is out of range", index);
|
||||
return defaultValue;
|
||||
}
|
||||
return m_currentVertexAttributes[index];
|
||||
@@ -712,10 +717,18 @@ namespace MobileGL::MG_State {
|
||||
m_renderState.SetViewport(viewport);
|
||||
}
|
||||
|
||||
const IntVec4& GLContext::GetViewport() const {
|
||||
IntVec4 GLContext::GetViewport() const {
|
||||
return m_renderState.GetViewport();
|
||||
}
|
||||
|
||||
void GLContext::SetViewportIndexed(Uint index, FloatVec4 viewport) {
|
||||
m_renderState.SetViewportIndexed(index, viewport);
|
||||
}
|
||||
|
||||
const FloatVec4& GLContext::GetViewportIndexed(Uint index) const {
|
||||
return m_renderState.GetViewportIndexed(index);
|
||||
}
|
||||
|
||||
void GLContext::SetLineWidth(Float width) {
|
||||
m_renderState.SetLineWidth(width);
|
||||
}
|
||||
@@ -953,6 +966,14 @@ namespace MobileGL::MG_State {
|
||||
return m_renderState.GetDepthRange();
|
||||
}
|
||||
|
||||
void GLContext::SetDepthRangeIndexed(Uint index, FloatVec2 range) {
|
||||
m_renderState.SetDepthRangeIndexed(index, range);
|
||||
}
|
||||
|
||||
const FloatVec2& GLContext::GetDepthRangeIndexed(Uint index) const {
|
||||
return m_renderState.GetDepthRangeIndexed(index);
|
||||
}
|
||||
|
||||
void GLContext::SetSampleCoverage(Float value, Bool invert) {
|
||||
m_renderState.SetSampleCoverage(value, invert);
|
||||
}
|
||||
@@ -1017,6 +1038,14 @@ namespace MobileGL::MG_State {
|
||||
return m_renderState.GetScissorBox();
|
||||
}
|
||||
|
||||
void GLContext::SetScissorBoxIndexed(Uint index, IntVec4 box) {
|
||||
m_renderState.SetScissorBoxIndexed(index, box);
|
||||
}
|
||||
|
||||
const IntVec4& GLContext::GetScissorBoxIndexed(Uint index) const {
|
||||
return m_renderState.GetScissorBoxIndexed(index);
|
||||
}
|
||||
|
||||
// Framebuffer
|
||||
void GLContext::GenFramebufferNames(Uint number, Vector<Uint>& framebuffers) {
|
||||
m_framebufferState.GenerateNames(number, framebuffers);
|
||||
|
||||
@@ -198,8 +198,10 @@ namespace MobileGL {
|
||||
// Only the pipeline-relevant subset - see RenderState::m_pipelineStateVersion.
|
||||
Uint GetPipelineStateVersion() const;
|
||||
const RenderStateParameters& GetRenderStateParameters() const;
|
||||
void SetViewport(IntVec4 viewport); // x, y, width, height
|
||||
const IntVec4& GetViewport() const; // x, y, width, height
|
||||
void SetViewport(IntVec4 viewport); // x, y, width, height; writes ALL viewports
|
||||
IntVec4 GetViewport() const; // x, y, width, height; viewport 0, rounded
|
||||
void SetViewportIndexed(Uint index, FloatVec4 viewport);
|
||||
const FloatVec4& GetViewportIndexed(Uint index) const;
|
||||
void SetLineWidth(Float width);
|
||||
Float GetLineWidth() const;
|
||||
void SetPointSize(Float size);
|
||||
@@ -260,8 +262,10 @@ namespace MobileGL {
|
||||
Uint32 GetClearStencil() const;
|
||||
void SetBlendColor(FloatVec4 color);
|
||||
const FloatVec4& GetBlendColor() const;
|
||||
void SetDepthRange(FloatVec2 range);
|
||||
void SetDepthRange(FloatVec2 range); // writes ALL viewports' depth ranges
|
||||
const FloatVec2& GetDepthRange() const;
|
||||
void SetDepthRangeIndexed(Uint index, FloatVec2 range);
|
||||
const FloatVec2& GetDepthRangeIndexed(Uint index) const;
|
||||
void SetSampleCoverage(Float value, Bool invert);
|
||||
Float GetSampleCoverageValue() const;
|
||||
Bool GetSampleCoverageInvert() const;
|
||||
@@ -276,8 +280,10 @@ namespace MobileGL {
|
||||
FrontFaceMode GetFrontFaceMode() const;
|
||||
void SetProvokingVertexMode(ProvokingVertexMode mode);
|
||||
ProvokingVertexMode GetProvokingVertexMode() const;
|
||||
void SetScissorBox(IntVec4 box); // x, y, width, height
|
||||
const IntVec4& GetScissorBox() const; // x, y, width, height
|
||||
void SetScissorBox(IntVec4 box); // x, y, width, height; writes ALL rectangles
|
||||
const IntVec4& GetScissorBox() const; // x, y, width, height; rectangle 0
|
||||
void SetScissorBoxIndexed(Uint index, IntVec4 box);
|
||||
const IntVec4& GetScissorBoxIndexed(Uint index) const;
|
||||
|
||||
// Transform feedback. The fields below are the state of the transform
|
||||
// feedback object currently bound to GL_TRANSFORM_FEEDBACK; see the object
|
||||
@@ -407,9 +413,12 @@ namespace MobileGL {
|
||||
// cannot be captured in MG_State::Init() - that runs BEFORE MG_Backend::Init(),
|
||||
// so there is no backend to query yet. Re-captured whenever the active backend
|
||||
// object changes, which also rolls the fingerprint and therefore invalidates
|
||||
// every P0b preprocess memo keyed against the old one.
|
||||
// every P0b preprocess memo keyed against the old one. A backend whose dynamic
|
||||
// capabilities become available without changing object identity must call
|
||||
// InvalidateCompileEnv() after publishing them.
|
||||
// GL thread only.
|
||||
const SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv>& GetCompileEnv();
|
||||
void InvalidateCompileEnv();
|
||||
|
||||
private:
|
||||
// State Components
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
void ErrorState::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
|
||||
if (code == ErrorCode::NoError) {
|
||||
MGLOG_E("Recording Non-OpenGL error:\n%s", info->toString().c_str());
|
||||
MGLOG_D("Recording Non-OpenGL error:\n%s", info->toString().c_str());
|
||||
m_nonGLErrors.push_back(MakeUnique<Error>(code, Move(info)));
|
||||
} else {
|
||||
MGLOG_E("Recording OpenGL error (%s):\n%s",
|
||||
MGLOG_D("Recording OpenGL error (%s):\n%s",
|
||||
MG_Util::ConvertGLEnumToString(MG_Util::ConvertErrorCodeToGLEnum(code)).c_str(),
|
||||
info->toString().c_str());
|
||||
// GL error semantics are sticky flags, not a queue (GL 3.3 core §2.5): with multiple
|
||||
|
||||
@@ -60,6 +60,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
Uint externalIndex = 0; // logs only
|
||||
Vector<LinkShaderInput> shaders; // already stage-sorted
|
||||
SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> env;
|
||||
// Startup configuration copied with the task, never read from worker code.
|
||||
Bool enableSpirvValidation = false;
|
||||
// The four "takes effect at the next link" request maps. Snapshotted rather than
|
||||
// referenced, which is precisely what makes glBindAttribLocation and friends
|
||||
// legal to call over a pending link without cancelling it: the pending link keeps
|
||||
|
||||
@@ -255,7 +255,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
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 "
|
||||
MGLOG_E_ONCE("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;
|
||||
@@ -436,7 +436,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
defaultFS->Compile(); // TODO: use a global default FS object.
|
||||
auto status = defaultFS->GetCompileStatus();
|
||||
if (!status) {
|
||||
MGLOG_E("ProgramObject %u: Failed to compile default fragment shader. InfoLog:\n%s", m_externalIndex,
|
||||
MGLOG_E_ONCE("ProgramObject %u: Failed to compile default fragment shader. InfoLog:\n%s", m_externalIndex,
|
||||
defaultFS->GetInfoLog().c_str());
|
||||
return;
|
||||
}
|
||||
@@ -494,6 +494,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
auto task = MakeShared<ProgramLinkTask>();
|
||||
task->in.externalIndex = m_externalIndex;
|
||||
task->in.env = MG_Util::ShaderTranspiler::GetCurrentCompileEnv();
|
||||
task->in.enableSpirvValidation = MG_Config::Features.EnableSpirvValidation;
|
||||
task->in.explicitAttribLocations = m_explicitAttribLocations;
|
||||
task->in.explicitFragDataLocation = m_explicitFragDataLocation;
|
||||
task->in.explicitFragDataIndex = m_explicitFragDataIndex;
|
||||
|
||||
@@ -819,6 +819,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
// 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; }
|
||||
// Copied from the link task that generated this program's SPIR-V. Backends use it for
|
||||
// their final transforms, which must honor the same diagnostic setting as phase B.
|
||||
Bool GetSpirvValidationEnabled() const { return Spirv().enableSpirvValidation; }
|
||||
|
||||
// The linked glslang reflection itself, for the ONE consumer that needs resource
|
||||
// lists no typed getter above exposes: the GL program-interface query layer
|
||||
@@ -985,6 +988,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
// cannot be lifted out of glslang's reflection instead.
|
||||
struct SpirvArtifacts {
|
||||
Vector<Vector<unsigned>> generatedSpirv;
|
||||
Bool enableSpirvValidation = false;
|
||||
// Byte offset of each uniform location inside globalUboScratch, or
|
||||
// kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass.
|
||||
Vector<Uint> uniformOffsets;
|
||||
|
||||
@@ -102,7 +102,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
MGLOG_D("ProgramObject %u: Starting SPIR-V generation", externalIndex);
|
||||
GenerateSpirv(handoff, externalIndex);
|
||||
const Bool deferOutputValidationForDirectVulkan =
|
||||
m_phaseA->in.env != nullptr && m_phaseA->in.env->backend == BackendType::DirectVulkan;
|
||||
const Bool enableSpirvValidation = m_phaseA->in.enableSpirvValidation;
|
||||
artifacts.enableSpirvValidation = enableSpirvValidation;
|
||||
GenerateSpirv(handoff, externalIndex, deferOutputValidationForDirectVulkan, enableSpirvValidation);
|
||||
// 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
|
||||
@@ -137,7 +141,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
artifacts.generatedSpirv.size());
|
||||
}
|
||||
|
||||
void ProgramSpirvTask::GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, const Uint externalIndex) {
|
||||
void ProgramSpirvTask::GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, const Uint externalIndex,
|
||||
const Bool deferOutputValidationForDirectVulkan,
|
||||
const Bool enableSpirvValidation) {
|
||||
/* As we passed first stage compilation/linking,
|
||||
* we'll assume all the operations here should
|
||||
* pass. We may be able to employ some optimizations
|
||||
@@ -169,7 +175,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
Bool allOptimized = true;
|
||||
{
|
||||
for (auto& spv : artifacts.generatedSpirv) {
|
||||
auto success = ShaderCompiler::SanitizeAndOptimizeBinary(spv, spv);
|
||||
auto success = ShaderCompiler::SanitizeAndOptimizeBinary(
|
||||
spv, spv, !deferOutputValidationForDirectVulkan, enableSpirvValidation);
|
||||
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
|
||||
|
||||
@@ -65,7 +65,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
private:
|
||||
void RunBody() override;
|
||||
|
||||
void GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
|
||||
void GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex,
|
||||
Bool deferOutputValidationForDirectVulkan, Bool enableSpirvValidation);
|
||||
void BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
|
||||
|
||||
// Worker-side MGLOG replacement, replayed by the join on the GL thread. Same reason as
|
||||
|
||||
@@ -25,6 +25,12 @@ namespace MobileGL {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Every viewport's scissor-test bit set, i.e. what glEnable(GL_SCISSOR_TEST) writes.
|
||||
constexpr Uint32 kAllViewportsMask =
|
||||
RenderStateParameters::MAX_VIEWPORTS >= 32
|
||||
? ~0u
|
||||
: (1u << RenderStateParameters::MAX_VIEWPORTS) - 1u;
|
||||
} // namespace
|
||||
|
||||
RenderState::RenderState() {
|
||||
@@ -32,6 +38,15 @@ namespace MobileGL {
|
||||
for (auto& mask : m_parameters.ColorMasks) {
|
||||
mask = BoolVec4(true, true, true, true);
|
||||
}
|
||||
// Every viewport's depth range starts at (0, 1) - GL 4.6 core table 23.4. The
|
||||
// viewport and scissor rectangles legitimately start all-zero here: their spec
|
||||
// initial value is the size of the window the context is first made current to,
|
||||
// which the frontend does not know yet, so an all-zero rectangle means "never
|
||||
// written" and the backends resolve it against the live surface (see
|
||||
// DirectGLES' SyncRenderState and VulkanRenderer's ApplyGLViewportState).
|
||||
for (auto& range : m_parameters.DepthRanges) {
|
||||
range = FloatVec2(0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
Uint RenderState::GetVersion() const {
|
||||
@@ -47,15 +62,47 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
// -------------------- Rasterization --------------------
|
||||
// ARB_viewport_array, "Additions to Chapter 2": Viewport(x, y, w, h) is equivalent to
|
||||
// ViewportIndexedf(i, x, y, w, h) for every i in [0, MAX_VIEWPORTS) - it is not a
|
||||
// synonym for "viewport 0".
|
||||
void RenderState::SetViewport(IntVec4 viewport) {
|
||||
if (m_parameters.Viewport == viewport) return;
|
||||
const FloatVec4 asFloat(static_cast<Float>(viewport.x()), static_cast<Float>(viewport.y()),
|
||||
static_cast<Float>(viewport.z()), static_cast<Float>(viewport.w()));
|
||||
Bool stateChanged = false;
|
||||
for (auto& stored : m_parameters.Viewports) {
|
||||
if (stored == asFloat) continue;
|
||||
stored = asFloat;
|
||||
stateChanged = true;
|
||||
}
|
||||
if (stateChanged) ++m_version;
|
||||
}
|
||||
|
||||
m_parameters.Viewport = viewport;
|
||||
IntVec4 RenderState::GetViewport() const {
|
||||
const FloatVec4& viewport = m_parameters.Viewports[0];
|
||||
// Round rather than truncate: glGetIntegerv on floating-point state rounds to
|
||||
// nearest (GL 4.6 core 22.2), and truncating a 63.5-wide viewport to 63 would
|
||||
// also hand the backends a rectangle one pixel short of what was asked for.
|
||||
return IntVec4(static_cast<Int>(std::lround(viewport.x())), static_cast<Int>(std::lround(viewport.y())),
|
||||
static_cast<Int>(std::lround(viewport.z())), static_cast<Int>(std::lround(viewport.w())));
|
||||
}
|
||||
|
||||
void RenderState::SetViewportIndexed(Uint index, FloatVec4 viewport) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Viewport index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
if (m_parameters.Viewports[index] == viewport) return;
|
||||
|
||||
m_parameters.Viewports[index] = viewport;
|
||||
++m_version;
|
||||
}
|
||||
|
||||
const IntVec4& RenderState::GetViewport() const {
|
||||
return m_parameters.Viewport;
|
||||
const FloatVec4& RenderState::GetViewportIndexed(Uint index) const {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Viewport index out of range: %u", index);
|
||||
return m_parameters.Viewports[0];
|
||||
}
|
||||
return m_parameters.Viewports[index];
|
||||
}
|
||||
|
||||
void RenderState::SetLineWidth(Float width) {
|
||||
@@ -223,7 +270,6 @@ namespace MobileGL {
|
||||
SET_CAPABILITY(SampleAlphaToOne, enabled);
|
||||
SET_CAPABILITY(SampleCoverage, enabled);
|
||||
SET_CAPABILITY(SampleMask, enabled);
|
||||
SET_CAPABILITY(ScissorTest, enabled);
|
||||
SET_CAPABILITY(StencilTest, enabled);
|
||||
SET_CAPABILITY(ProgramPointSize, enabled);
|
||||
case CapabilityInput::Blend: {
|
||||
@@ -236,6 +282,17 @@ namespace MobileGL {
|
||||
if (stateChanged) BumpVersions();
|
||||
break;
|
||||
}
|
||||
// GL 4.6 core 17.3.2: the non-indexed Enable/Disable(SCISSOR_TEST) enables or
|
||||
// disables the test for ALL viewports, exactly like glViewport writes all
|
||||
// viewports. Anything narrower fails KHR-GL43.viewport_array.scissor_test_state_api,
|
||||
// whose "enable all" phase reads every index back through glIsEnabledi.
|
||||
case CapabilityInput::ScissorTest: {
|
||||
const Uint32 updated = enabled ? kAllViewportsMask : 0u;
|
||||
if (m_parameters.ScissorTestEnabledMask == updated) break;
|
||||
m_parameters.ScissorTestEnabledMask = updated;
|
||||
BumpVersions();
|
||||
break;
|
||||
}
|
||||
case CapabilityInput::ClipDistance0:
|
||||
case CapabilityInput::ClipDistance1:
|
||||
case CapabilityInput::ClipDistance2:
|
||||
@@ -287,11 +344,14 @@ namespace MobileGL {
|
||||
RETURN_CAPABILITY(SampleAlphaToOne);
|
||||
RETURN_CAPABILITY(SampleCoverage);
|
||||
RETURN_CAPABILITY(SampleMask);
|
||||
RETURN_CAPABILITY(ScissorTest);
|
||||
RETURN_CAPABILITY(StencilTest);
|
||||
RETURN_CAPABILITY(ProgramPointSize);
|
||||
case CapabilityInput::Blend:
|
||||
return m_parameters.BlendStates[0].Enabled;
|
||||
// The non-indexed query of an indexed capability answers for index 0
|
||||
// (GL 4.6 core 22.1), which is also the only bit either backend consumes today.
|
||||
case CapabilityInput::ScissorTest:
|
||||
return (m_parameters.ScissorTestEnabledMask & 1u) != 0;
|
||||
case CapabilityInput::ClipDistance0:
|
||||
case CapabilityInput::ClipDistance1:
|
||||
case CapabilityInput::ClipDistance2:
|
||||
@@ -307,13 +367,29 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
void RenderState::SetCapabilityIndexed(CapabilityInput cap, Uint index, Bool enabled) {
|
||||
// Only for BlendState currently. The GL entry points (glEnablei/glDisablei) already
|
||||
// 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.
|
||||
// GL_BLEND (indexed by draw buffer) and GL_SCISSOR_TEST (indexed by viewport) are
|
||||
// the only indexed capabilities in GL 4.6 core. The GL entry points
|
||||
// (glEnablei/glDisablei) already reject every other target with GL_INVALID_ENUM
|
||||
// and every out-of-range index with GL_INVALID_VALUE before reaching here, so the
|
||||
// guards below are backstops - but they must stay backstops:
|
||||
// THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and
|
||||
// terminates the process.
|
||||
if (cap == CapabilityInput::ScissorTest) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Scissor test capability index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
const Uint32 bit = 1u << index;
|
||||
const Uint32 updated = enabled ? (m_parameters.ScissorTestEnabledMask | bit)
|
||||
: (m_parameters.ScissorTestEnabledMask & ~bit);
|
||||
if (updated == m_parameters.ScissorTestEnabledMask) return;
|
||||
m_parameters.ScissorTestEnabledMask = updated;
|
||||
BumpVersions();
|
||||
return;
|
||||
}
|
||||
if (cap != CapabilityInput::Blend) {
|
||||
MGLOG_I("RenderState::SetCapabilityIndexed: indexed capability state exists only for "
|
||||
"GL_BLEND (cap=%d, index=%u); ignoring",
|
||||
"GL_BLEND and GL_SCISSOR_TEST (cap=%d, index=%u); ignoring",
|
||||
static_cast<int>(cap), index);
|
||||
return;
|
||||
}
|
||||
@@ -328,9 +404,17 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
Bool RenderState::IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const {
|
||||
// Only for BlendState currently - same backstop reasoning as SetCapabilityIndexed:
|
||||
// glIsEnabledi has already answered GL_INVALID_ENUM/GL_FALSE for anything else, and a
|
||||
// query must never be able to terminate the process.
|
||||
// GL_BLEND and GL_SCISSOR_TEST only - same backstop reasoning as
|
||||
// SetCapabilityIndexed: glIsEnabledi has already answered
|
||||
// GL_INVALID_ENUM/GL_INVALID_VALUE for anything else, and a query must never be
|
||||
// able to terminate the process.
|
||||
if (cap == CapabilityInput::ScissorTest) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Scissor test capability index out of range: %u", index);
|
||||
return false;
|
||||
}
|
||||
return (m_parameters.ScissorTestEnabledMask & (1u << index)) != 0;
|
||||
}
|
||||
if (cap != CapabilityInput::Blend) {
|
||||
MGLOG_I("RenderState::IsCapabilityEnabledIndexed: indexed capability state exists only "
|
||||
"for GL_BLEND (cap=%d, index=%u); reporting disabled",
|
||||
@@ -591,15 +675,39 @@ namespace MobileGL {
|
||||
return m_parameters.BlendColor;
|
||||
}
|
||||
|
||||
// Like Viewport: ARB_viewport_array makes DepthRange(n, f) the same as
|
||||
// DepthRangeIndexed(i, n, f) for every i.
|
||||
void RenderState::SetDepthRange(FloatVec2 range) {
|
||||
if (m_parameters.DepthRange == range) return;
|
||||
|
||||
m_parameters.DepthRange = range;
|
||||
++m_version;
|
||||
Bool stateChanged = false;
|
||||
for (auto& stored : m_parameters.DepthRanges) {
|
||||
if (stored == range) continue;
|
||||
stored = range;
|
||||
stateChanged = true;
|
||||
}
|
||||
if (stateChanged) ++m_version;
|
||||
}
|
||||
|
||||
const FloatVec2& RenderState::GetDepthRange() const {
|
||||
return m_parameters.DepthRange;
|
||||
return m_parameters.DepthRanges[0];
|
||||
}
|
||||
|
||||
void RenderState::SetDepthRangeIndexed(Uint index, FloatVec2 range) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Depth range index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
if (m_parameters.DepthRanges[index] == range) return;
|
||||
|
||||
m_parameters.DepthRanges[index] = range;
|
||||
++m_version;
|
||||
}
|
||||
|
||||
const FloatVec2& RenderState::GetDepthRangeIndexed(Uint index) const {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Depth range index out of range: %u", index);
|
||||
return m_parameters.DepthRanges[0];
|
||||
}
|
||||
return m_parameters.DepthRanges[index];
|
||||
}
|
||||
|
||||
void RenderState::SetSampleCoverage(Float value, Bool invert) {
|
||||
@@ -726,15 +834,39 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
// --------------------- Scissor ---------------------
|
||||
// Like Viewport: ARB_viewport_array makes Scissor(x, y, w, h) the same as
|
||||
// ScissorIndexed(i, x, y, w, h) for every i.
|
||||
void RenderState::SetScissorBox(IntVec4 box) {
|
||||
if (m_parameters.ScissorBox == box) return;
|
||||
|
||||
m_parameters.ScissorBox = box;
|
||||
++m_version;
|
||||
Bool stateChanged = false;
|
||||
for (auto& stored : m_parameters.ScissorBoxes) {
|
||||
if (stored == box) continue;
|
||||
stored = box;
|
||||
stateChanged = true;
|
||||
}
|
||||
if (stateChanged) ++m_version;
|
||||
}
|
||||
|
||||
const IntVec4& RenderState::GetScissorBox() const {
|
||||
return m_parameters.ScissorBox;
|
||||
return m_parameters.ScissorBoxes[0];
|
||||
}
|
||||
|
||||
void RenderState::SetScissorBoxIndexed(Uint index, IntVec4 box) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Scissor box index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
if (m_parameters.ScissorBoxes[index] == box) return;
|
||||
|
||||
m_parameters.ScissorBoxes[index] = box;
|
||||
++m_version;
|
||||
}
|
||||
|
||||
const IntVec4& RenderState::GetScissorBoxIndexed(Uint index) const {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Scissor box index out of range: %u", index);
|
||||
return m_parameters.ScissorBoxes[0];
|
||||
}
|
||||
return m_parameters.ScissorBoxes[index];
|
||||
}
|
||||
} // namespace GLState
|
||||
} // namespace MG_State
|
||||
|
||||
@@ -220,8 +220,22 @@ namespace MobileGL {
|
||||
};
|
||||
|
||||
struct RenderStateParameters {
|
||||
// ARB_viewport_array / GL 4.6 core 13.6.1: the viewport, the scissor rectangle, the depth
|
||||
// range and the scissor-test enable are all arrays indexed by gl_ViewportIndex, and the
|
||||
// spec floor for MAX_VIEWPORTS is 16. MobileGL advertises exactly 16 on both backends, so
|
||||
// this is also what GL_MAX_VIEWPORTS reports (see the backend loaders' caps.MaxViewports).
|
||||
static constexpr Uint MAX_VIEWPORTS = 16;
|
||||
|
||||
// Rasterization
|
||||
IntVec4 Viewport = IntVec4(0, 0, 0, 0); // x, y, width, height
|
||||
// The viewport rectangle is FLOAT state as of GL 4.1 - ViewportIndexedf writes fractional
|
||||
// values and GetFloati_v(GL_VIEWPORT) must hand them back bit-exact
|
||||
// (KHR-GL43.viewport_array.viewport_api compares with ==, no tolerance). glViewport's
|
||||
// integers are simply one way to write it. Index 0 is what a program that never assigns
|
||||
// gl_ViewportIndex rasterizes against, and what the classic glViewport /
|
||||
// glGetIntegerv(GL_VIEWPORT) pair addresses. Both backends rasterize the rectangle
|
||||
// rounded back to integers; the STATE stays exact, which is the half the conformance
|
||||
// suite checks (see the KNOWN INFIDELITY note in AdvertisedLimitsScenario.cpp).
|
||||
Array<FloatVec4, MAX_VIEWPORTS> Viewports{}; // x, y, width, height
|
||||
Float LineWidth = 1.0f;
|
||||
Float PointSize = 1.0f;
|
||||
// GL_PATCH_VERTICES: how many vertices one tessellation patch consumes.
|
||||
@@ -247,7 +261,13 @@ namespace MobileGL {
|
||||
Float ClearDepth = 1.0f;
|
||||
Uint32 ClearStencil = 0;
|
||||
FloatVec4 BlendColor = FloatVec4(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
FloatVec2 DepthRange = FloatVec2(0.0f, 1.0f);
|
||||
// Per-viewport depth range (glDepthRangeIndexed / glDepthRangeArrayv). Every entry is
|
||||
// initialized to (0, 1) in RenderState's constructor - a default member initializer would
|
||||
// not survive the Array<> aggregate. Kept float rather than double: DepthRangeArrayv takes
|
||||
// GLdouble, but the value reaches the hardware as VkViewport::minDepth/maxDepth (float) on
|
||||
// Magma and glDepthRangef on Espryt, so a double store would only widen the readback and
|
||||
// then lose it again at the same place.
|
||||
Array<FloatVec2, MAX_VIEWPORTS> DepthRanges{};
|
||||
Float SampleCoverageValue = 1.0f;
|
||||
Bool SampleCoverageInvert = false;
|
||||
Uint32 SampleMaskValue = 0xffffffffu;
|
||||
@@ -299,10 +319,15 @@ namespace MobileGL {
|
||||
Bool SampleAlphaToOneEnabled = false;
|
||||
Bool SampleCoverageEnabled = false;
|
||||
Bool SampleMaskEnabled = false;
|
||||
Bool ScissorTestEnabled = false;
|
||||
Bool StencilTestEnabled = false;
|
||||
Bool ProgramPointSizeEnabled = false;
|
||||
IntVec4 ScissorBox = IntVec4(0, 0, 0, 0); // x, y, width, height
|
||||
// glEnable(GL_SCISSOR_TEST) enables the test for EVERY viewport, glEnablei for one
|
||||
// (GL 4.6 core 17.3.2), so this is 16 bits and not a bool. Bit 0 is what the classic
|
||||
// glIsEnabled(GL_SCISSOR_TEST) reports and what both backends currently consume. Unlike
|
||||
// ClipDistanceEnabledMask below it DOES bump the pipeline version, because DirectGLES
|
||||
// turns it into a real glEnable/glDisable.
|
||||
Uint32 ScissorTestEnabledMask = 0;
|
||||
Array<IntVec4, MAX_VIEWPORTS> ScissorBoxes{}; // x, y, width, height
|
||||
// glEnable(GL_CLIP_DISTANCE0 + i) for i in [0, 8), one bit each. A bitmask rather than
|
||||
// eight bools because every consumer wants the set, not an individual flag, and because
|
||||
// the SYNC_CAPABILITY/SET_CAPABILITY macros key off a "<Name>Enabled" field name that
|
||||
@@ -323,8 +348,14 @@ namespace MobileGL {
|
||||
const RenderStateParameters& GetAllParameters() const;
|
||||
|
||||
// Rasterization
|
||||
// ARB_viewport_array defines glViewport as ViewportIndexedf on EVERY index, so the
|
||||
// classic setter broadcasts; GetViewport answers for index 0 (rounded to the
|
||||
// integers glGetIntegerv(GL_VIEWPORT) and both backends want) and is BY VALUE for
|
||||
// that reason. The indexed pair is the verbatim float state.
|
||||
void SetViewport(IntVec4 viewport); // x, y, width, height
|
||||
const IntVec4& GetViewport() const; // x, y, width, height
|
||||
IntVec4 GetViewport() const; // x, y, width, height, viewport 0, rounded
|
||||
void SetViewportIndexed(Uint index, FloatVec4 viewport);
|
||||
const FloatVec4& GetViewportIndexed(Uint index) const;
|
||||
void SetLineWidth(Float width);
|
||||
Float GetLineWidth() const;
|
||||
void SetPointSize(Float size);
|
||||
@@ -400,8 +431,12 @@ namespace MobileGL {
|
||||
Uint32 GetClearStencil() const;
|
||||
void SetBlendColor(FloatVec4 color);
|
||||
const FloatVec4& GetBlendColor() const;
|
||||
// glDepthRange(f) writes every viewport's range (ARB_viewport_array); the indexed
|
||||
// pair is glDepthRangeIndexed / glDepthRangeArrayv. GetDepthRange answers index 0.
|
||||
void SetDepthRange(FloatVec2 range);
|
||||
const FloatVec2& GetDepthRange() const;
|
||||
void SetDepthRangeIndexed(Uint index, FloatVec2 range);
|
||||
const FloatVec2& GetDepthRangeIndexed(Uint index) const;
|
||||
void SetSampleCoverage(Float value, Bool invert);
|
||||
Float GetSampleCoverageValue() const;
|
||||
Bool GetSampleCoverageInvert() const;
|
||||
@@ -421,9 +456,12 @@ namespace MobileGL {
|
||||
void SetProvokingVertexMode(ProvokingVertexMode mode);
|
||||
ProvokingVertexMode GetProvokingVertexMode() const;
|
||||
|
||||
// Scissor
|
||||
// Scissor. glScissor writes every rectangle (ARB_viewport_array); GetScissorBox
|
||||
// answers for index 0.
|
||||
void SetScissorBox(IntVec4 box); // x, y, width, height
|
||||
const IntVec4& GetScissorBox() const; // x, y, width, height
|
||||
void SetScissorBoxIndexed(Uint index, IntVec4 box);
|
||||
const IntVec4& GetScissorBoxIndexed(Uint index) const;
|
||||
|
||||
private:
|
||||
// Bump both: any state change invalidates the draw snapshot, and this one also
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
message(STATUS "Generating build files for MobileGL Diligent Backend Test...")
|
||||
|
||||
add_executable(
|
||||
DiligentVulkanSanityTest
|
||||
SanityTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(DiligentVulkanSanityTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
DiligentVulkanSanityTest PRIVATE
|
||||
GTest::gtest_main
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(DiligentVulkanSanityTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS integration)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,9 +16,11 @@
|
||||
#include <MG_Backend/DirectGLES/Utils.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BakeImageFormatQualifiers;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ForceFlatIntegerVaryings;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestExtendedImageFormats;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::SplitReadWriteImageUniforms;
|
||||
|
||||
namespace {
|
||||
@@ -435,3 +437,116 @@ void main() { tes_gs_coord = tcs_tes_coord[0]; }
|
||||
EXPECT_TRUE(Contains(out, "layout(location = 1) out vec2 tes_gs_coord;")) << out;
|
||||
EXPECT_EQ(CountOf(out, "flat"), 0u) << out;
|
||||
}
|
||||
|
||||
// --- image format qualifier completion ---------------------------------------------------------
|
||||
//
|
||||
// GLSL ES requires a format layout qualifier on every image; desktop GLSL lets a writeonly
|
||||
// declaration omit one. The format is normally written into the SPIR-V before SPIRV-Cross runs
|
||||
// (BakeImageFormatsPass), but SPIRV-Cross THROWS rather than printing the formats it calls
|
||||
// desktop-only for ESSL - r8ui among them - so those are completed here, on the emitted text.
|
||||
|
||||
// The KHR-GL4x.packed_depth_stencil.stencil_texturing stencil half: `writeonly uniform uimage2D`
|
||||
// with GL_R8UI bound to its unit.
|
||||
TEST(BakeImageFormatQualifiersTest, AFormatlessDeclarationGetsTheBoundFormat) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(binding = 1) uniform writeonly highp uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(15u)); }
|
||||
)";
|
||||
const String out = BakeImageFormatQualifiers(source, {{"uni_image", "r8ui"}});
|
||||
EXPECT_TRUE(Contains(out, "layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;")) << out;
|
||||
}
|
||||
|
||||
// A declaration with NO layout at all still has to end up with one, or the driver rejects it for
|
||||
// exactly the reason this pass exists.
|
||||
TEST(BakeImageFormatQualifiersTest, ADeclarationWithNoLayoutGetsOne) {
|
||||
const String source = R"(#version 320 es
|
||||
uniform writeonly highp uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
||||
)";
|
||||
const String out = BakeImageFormatQualifiers(source, {{"uni_image", "r16i"}});
|
||||
EXPECT_TRUE(Contains(out, "layout(r16i) uniform writeonly highp uimage2D uni_image;")) << out;
|
||||
}
|
||||
|
||||
// A DECLARED format is authoritative and must survive, whatever the map says - the frontend never
|
||||
// puts a declared image in the map, and the pass must not depend on that being true.
|
||||
TEST(BakeImageFormatQualifiersTest, ADeclaredFormatIsNeverOverwritten) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(binding = 1, rgba8ui) uniform writeonly highp uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
||||
)";
|
||||
const String out = BakeImageFormatQualifiers(source, {{"uni_image", "r8ui"}});
|
||||
EXPECT_EQ(out, source) << out;
|
||||
}
|
||||
|
||||
// Only the named uniform. A second image in the same shader - format-less because the pass
|
||||
// declined it, or because its unit holds nothing - must be left exactly as it is.
|
||||
TEST(BakeImageFormatQualifiersTest, OnlyTheNamedUniformIsTouched) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(binding = 0) uniform writeonly highp uimage2D named;
|
||||
layout(binding = 1) uniform writeonly highp uimage2D other;
|
||||
void main() { imageStore(named, ivec2(0), uvec4(1u)); imageStore(other, ivec2(0), uvec4(2u)); }
|
||||
)";
|
||||
const String out = BakeImageFormatQualifiers(source, {{"named", "r8ui"}});
|
||||
EXPECT_TRUE(Contains(out, "layout(r8ui, binding = 0) uniform writeonly highp uimage2D named;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 1) uniform writeonly highp uimage2D other;")) << out;
|
||||
}
|
||||
|
||||
// The format the pass writes has to survive the two passes that run after it, or nothing was
|
||||
// gained: the read+write split copies declarations, and the binding strip edits layout qualifiers.
|
||||
TEST(BakeImageFormatQualifiersTest, TheWrittenFormatSurvivesTheLaterImagePasses) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(binding = 3) uniform writeonly highp uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
||||
)";
|
||||
String out = BakeImageFormatQualifiers(source, {{"uni_image", "r8ui"}});
|
||||
out = SplitReadWriteImageUniforms(out);
|
||||
out = RemoveLayoutBinding(out);
|
||||
EXPECT_TRUE(Contains(out, "r8ui")) << out;
|
||||
EXPECT_TRUE(Contains(out, "binding = 3")) << out;
|
||||
}
|
||||
|
||||
TEST(BakeImageFormatQualifiersTest, AnEmptyMapOrAnImagelessShaderIsANoOp) {
|
||||
const String withImage = R"(#version 320 es
|
||||
layout(binding = 1) uniform writeonly highp uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
||||
)";
|
||||
EXPECT_EQ(BakeImageFormatQualifiers(withImage, {}), withImage);
|
||||
|
||||
const String withoutImage = R"(#version 320 es
|
||||
layout(location = 0) out highp vec4 mg_FragColor;
|
||||
void main() { mg_FragColor = vec4(1.0); }
|
||||
)";
|
||||
EXPECT_EQ(BakeImageFormatQualifiers(withoutImage, {{"uni_image", "r8ui"}}), withoutImage);
|
||||
}
|
||||
|
||||
// --- GL_NV_image_formats directive --------------------------------------------------------------
|
||||
|
||||
TEST(RequestExtendedImageFormatsTest, TheDirectiveGoesRightAfterTheVersionLine) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
||||
)";
|
||||
const String out = RequestExtendedImageFormats(source, true);
|
||||
EXPECT_TRUE(Contains(out, "#version 320 es\n#extension GL_NV_image_formats : require\n")) << out;
|
||||
}
|
||||
|
||||
// Never speculatively: `#extension` naming an extension the driver does not advertise is itself a
|
||||
// compile error, so the caller's "not needed" answer has to be honoured exactly.
|
||||
TEST(RequestExtendedImageFormatsTest, NotNeededMeansNotEmitted) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
||||
)";
|
||||
EXPECT_EQ(RequestExtendedImageFormats(source, false), source);
|
||||
}
|
||||
|
||||
TEST(RequestExtendedImageFormatsTest, AnAlreadyPresentDirectiveIsNotDuplicated) {
|
||||
const String source = R"(#version 320 es
|
||||
#extension GL_NV_image_formats : require
|
||||
layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
||||
)";
|
||||
const String out = RequestExtendedImageFormats(source, true);
|
||||
EXPECT_EQ(out, source);
|
||||
EXPECT_EQ(CountOf(out, "GL_NV_image_formats"), 1u) << out;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,28 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <iostream>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
|
||||
|
||||
TEST(DirectVulkanSanity, ProgramMovePreservesViewportIndexUsage) {
|
||||
using VkProgramObject = MobileGL::MG_Backend::DirectVulkan::ProgramFactory::VkProgramObject;
|
||||
|
||||
VkProgramObject moveConstructedSource;
|
||||
moveConstructedSource.writesViewportIndexBuiltin = true;
|
||||
VkProgramObject moveConstructed(std::move(moveConstructedSource));
|
||||
EXPECT_TRUE(moveConstructed.writesViewportIndexBuiltin);
|
||||
EXPECT_FALSE(moveConstructedSource.writesViewportIndexBuiltin);
|
||||
|
||||
VkProgramObject moveAssignedSource;
|
||||
moveAssignedSource.writesViewportIndexBuiltin = true;
|
||||
VkProgramObject moveAssigned;
|
||||
moveAssigned = std::move(moveAssignedSource);
|
||||
EXPECT_TRUE(moveAssigned.writesViewportIndexBuiltin);
|
||||
EXPECT_FALSE(moveAssignedSource.writesViewportIndexBuiltin);
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, ExtensionEnumeration) {
|
||||
uint32_t extensionCount = 0;
|
||||
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
|
||||
|
||||
@@ -725,22 +725,57 @@ TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSu
|
||||
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
|
||||
};
|
||||
|
||||
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false);
|
||||
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false);
|
||||
EXPECT_FALSE(contains(without, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_FALSE(contains(without, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
|
||||
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true);
|
||||
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true, false, false);
|
||||
EXPECT_TRUE(contains(with, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_TRUE(contains(with, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
|
||||
// Same rule on the Vulkan backend, where the gate is the samplerAnisotropy device feature.
|
||||
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false);
|
||||
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false);
|
||||
EXPECT_FALSE(contains(vkWithout, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true);
|
||||
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true, false);
|
||||
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
}
|
||||
|
||||
// Minecraft 26.3 checks ARB_draw_indirect before it considers the already-advertised
|
||||
// ARB_multi_draw_indirect, then separately requires ARB_base_instance before enabling its terrain
|
||||
// indirect path. Pin both strings and, just as importantly, the non-zero firstInstance gate.
|
||||
TEST(IndirectDrawAdvertisement, MatchesEachBackendsUsableCommandSemantics) {
|
||||
const auto contains = [](const MobileGL::Vector<MobileGL::GLExtension>& extensions,
|
||||
MobileGL::GLExtension wanted) {
|
||||
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
|
||||
};
|
||||
|
||||
const auto esWithoutIndirect =
|
||||
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false);
|
||||
EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
const auto esWithoutBaseInstance =
|
||||
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, false);
|
||||
EXPECT_TRUE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_FALSE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
const auto esWithBoth =
|
||||
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, true);
|
||||
EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
const auto vkWithoutBaseInstance =
|
||||
MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false);
|
||||
EXPECT_TRUE(contains(vkWithoutBaseInstance, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_FALSE(contains(vkWithoutBaseInstance, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
const auto vkWithBoth =
|
||||
MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, true);
|
||||
EXPECT_TRUE(contains(vkWithBoth, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_TRUE(contains(vkWithBoth, MobileGL::E_GL_ARB_base_instance));
|
||||
}
|
||||
|
||||
TEST(TextureAnisotropyCapabilities, MaxAnisotropyIsQueriedOnlyWhenTheExtensionIsPresent) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
@@ -836,3 +871,63 @@ TEST(MultiDrawCapabilities, ExtensionWithoutResolvedPointerIsNotSupport) {
|
||||
EXPECT_FALSE(caps.SupportsMultiDrawIndirect);
|
||||
EXPECT_FALSE(caps.SupportsMultiDrawElementsBaseVertex);
|
||||
}
|
||||
|
||||
TEST(DrawIndirectCapabilities, RequiresEs31AndBothCoreEntryPoints) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
auto funcs = MakeFakeGLESFunctions();
|
||||
funcs.glDrawElementsIndirect = [](GLenum, GLenum, const void*) {};
|
||||
|
||||
MobileGL::MG_External::GLESCapabilities supportedCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(supportedCaps, funcs));
|
||||
EXPECT_TRUE(supportedCaps.SupportsDrawIndirect);
|
||||
|
||||
// The same pointers on an ES 3.0 context are not core entry points and cannot back the
|
||||
// desktop extension contract.
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.glesMinorVersion = 0;
|
||||
MobileGL::MG_External::GLESCapabilities es30Caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es30Caps, funcs));
|
||||
EXPECT_FALSE(es30Caps.SupportsDrawIndirect);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
const auto missingElements = MakeFakeGLESFunctions();
|
||||
MobileGL::MG_External::GLESCapabilities missingEntryPointCaps;
|
||||
ASSERT_TRUE(
|
||||
MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(missingEntryPointCaps, missingElements));
|
||||
EXPECT_FALSE(missingEntryPointCaps.SupportsDrawIndirect);
|
||||
}
|
||||
|
||||
TEST(BaseInstanceCapabilities, RequiresTheExtensionAndAllThreeEntryPoints) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
auto funcs = MakeFakeGLESFunctions();
|
||||
funcs.glDrawArraysInstancedBaseInstanceEXT = [](GLenum, GLint, GLsizei, GLsizei, GLuint) {};
|
||||
funcs.glDrawElementsInstancedBaseInstanceEXT =
|
||||
[](GLenum, GLsizei, GLenum, const void*, GLsizei, GLuint) {};
|
||||
funcs.glDrawElementsInstancedBaseVertexBaseInstanceEXT =
|
||||
[](GLenum, GLsizei, GLenum, const void*, GLsizei, GLint, GLuint) {};
|
||||
|
||||
// Resolved stubs alone must never make the capability true.
|
||||
MobileGL::MG_External::GLESCapabilities pointersOnlyCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(pointersOnlyCaps, funcs));
|
||||
EXPECT_FALSE(pointersOnlyCaps.SupportsBaseInstance);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.extensions.emplace_back("GL_EXT_base_instance");
|
||||
MobileGL::MG_External::GLESCapabilities supportedCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(supportedCaps, funcs));
|
||||
EXPECT_TRUE(supportedCaps.SupportsBaseInstance);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.extensions.emplace_back("GL_EXT_base_instance");
|
||||
funcs.glDrawElementsInstancedBaseInstanceEXT = nullptr;
|
||||
MobileGL::MG_External::GLESCapabilities missingEntryPointCaps;
|
||||
ASSERT_TRUE(
|
||||
MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(missingEntryPointCaps, funcs));
|
||||
EXPECT_FALSE(missingEntryPointCaps.SupportsBaseInstance);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
||||
#include <MG_Impl/GetProcAddress.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -599,6 +600,117 @@ TEST_F(BufferTest, ClearNamedBufferSubDataRepeatsPattern) {
|
||||
EXPECT_EQ(actual, (Vector<Uint32>{0, pattern, pattern, pattern, 0}));
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
TEST_F(BufferTest, ClearBufferSubDataInitializesIrisStaticSsboRange) {
|
||||
GLuint buffer = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
|
||||
Vector<Uint8> initial(32, 0x7F);
|
||||
MobileGL::MG_Impl::GLImpl::BufferData(
|
||||
GL_SHADER_STORAGE_BUFFER, initial.size(), initial.data(), GL_STATIC_DRAW);
|
||||
const GLbyte zero = 0;
|
||||
const auto clear = reinterpret_cast<PFNGLCLEARBUFFERSUBDATAPROC>(
|
||||
MobileGL::MG_Impl::GetProcAddress("glClearBufferSubData"));
|
||||
ASSERT_NE(clear, nullptr);
|
||||
clear(GL_SHADER_STORAGE_BUFFER, GL_R8, 4, 24, GL_RED, GL_BYTE, &zero);
|
||||
|
||||
Vector<Uint8> actual(initial.size());
|
||||
auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), actual.size());
|
||||
EXPECT_EQ(actual, (Vector<Uint8>{0x7F, 0x7F, 0x7F, 0x7F,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0x7F, 0x7F, 0x7F, 0x7F}));
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, ClearBufferSubDataInitializesCompleteIrisStaticSsbo) {
|
||||
constexpr SizeT irisStaticSsboSize = 5'000'192;
|
||||
GLuint buffer = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||
|
||||
Vector<Uint8> initial(irisStaticSsboSize, 0x7F);
|
||||
MobileGL::MG_Impl::GLImpl::BufferData(
|
||||
GL_SHADER_STORAGE_BUFFER, initial.size(), initial.data(), GL_STATIC_DRAW);
|
||||
const GLbyte zero = 0;
|
||||
MobileGL::MG_Impl::GLImpl::ClearBufferSubData(
|
||||
GL_SHADER_STORAGE_BUFFER, GL_R8, 0, irisStaticSsboSize, GL_RED, GL_BYTE, &zero);
|
||||
|
||||
Vector<Uint8> actual(irisStaticSsboSize);
|
||||
auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), actual.size());
|
||||
EXPECT_EQ(actual, Vector<Uint8>(irisStaticSsboSize, 0));
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, ClearBufferDataConvertsOneClientPixelBeforeRepeatingIt) {
|
||||
GLuint buffer = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
|
||||
Vector<Uint32> initial(4, 0u);
|
||||
MobileGL::MG_Impl::GLImpl::BufferData(GL_ARRAY_BUFFER, initial.size() * sizeof(Uint32), initial.data(),
|
||||
GL_STATIC_DRAW);
|
||||
const Uint8 value = 0xAB;
|
||||
MobileGL::MG_Impl::GLImpl::ClearBufferData(
|
||||
GL_ARRAY_BUFFER, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_BYTE, &value);
|
||||
|
||||
Vector<Uint32> actual(initial.size());
|
||||
auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), actual.size() * sizeof(Uint32));
|
||||
EXPECT_EQ(actual, Vector<Uint32>(initial.size(), value));
|
||||
EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, ClearBufferSubDataRejectsUnboundTarget) {
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
const GLbyte zero = 0;
|
||||
MobileGL::MG_Impl::GLImpl::ClearBufferSubData(
|
||||
GL_SHADER_STORAGE_BUFFER, GL_R8, 0, 1, GL_RED, GL_BYTE, &zero);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, ClearBufferDataRejectsInvalidPixelFormatTypePairs) {
|
||||
GLuint buffer = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer);
|
||||
|
||||
const Vector<Uint8> initial{0x7F, 0x7F};
|
||||
MobileGL::MG_Impl::GLImpl::BufferData(GL_ARRAY_BUFFER, initial.size(), initial.data(), GL_STATIC_DRAW);
|
||||
const Uint16 packed = 0;
|
||||
MobileGL::MG_Impl::GLImpl::ClearBufferData(
|
||||
GL_ARRAY_BUFFER, GL_R16, GL_RED, GL_UNSIGNED_SHORT_5_6_5, &packed);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MobileGL::MG_Impl::GLImpl::ClearBufferData(
|
||||
GL_ARRAY_BUFFER, GL_R16, GL_RED, GL_UNSIGNED_SHORT_5_6_5, nullptr);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
Vector<Uint8> actual(initial.size());
|
||||
auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer);
|
||||
ASSERT_NE(bufferObject, nullptr);
|
||||
Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), actual.size());
|
||||
EXPECT_EQ(actual, initial);
|
||||
|
||||
MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
|
||||
// GL 4.6 core 6.5: glBufferSubData fails only when the written range OVERLAPS the mapped range.
|
||||
|
||||
@@ -84,3 +84,6 @@ add_subdirectory(Backend/DirectGLES)
|
||||
if (ENABLE_INTEGRATION_TESTS)
|
||||
add_subdirectory(Backend/DirectVulkan)
|
||||
endif()
|
||||
if (MOBILEGL_ENABLE_DILIGENT)
|
||||
add_subdirectory(Backend/Diligent)
|
||||
endif()
|
||||
|
||||
@@ -3,6 +3,8 @@ cmake_minimum_required(VERSION 3.14)
|
||||
add_executable(
|
||||
PipelineQuirkTest
|
||||
PipelineQuirkTest.cpp
|
||||
PassthroughTessControlTest.cpp
|
||||
ViewportIndexReflectionTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(PipelineQuirkTest PRIVATE
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
// MobileGL - MobileGL/MG_Test/Pipeline/PassthroughTessControlTest.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 <gtest/gtest.h>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
|
||||
namespace {
|
||||
// A test-side SPIR-V walker, deliberately independent of the production reflection: the
|
||||
// generator's contract with the evaluation stage is "declare this many output vertices and
|
||||
// write these built-ins", and that has to be readable off the module itself.
|
||||
constexpr Uint32 kSpirvHeaderWordCount = 5;
|
||||
constexpr Uint32 kOpExecutionMode = 16;
|
||||
constexpr Uint32 kOpDecorate = 71;
|
||||
constexpr Uint32 kOpMemberDecorate = 72;
|
||||
constexpr Uint32 kExecutionModeOutputVertices = 26;
|
||||
constexpr Uint32 kDecorationBuiltIn = 11;
|
||||
|
||||
// SpvBuiltIn values used below.
|
||||
constexpr Uint32 kBuiltInPosition = 0;
|
||||
constexpr Uint32 kBuiltInInvocationId = 8;
|
||||
constexpr Uint32 kBuiltInTessLevelOuter = 11;
|
||||
constexpr Uint32 kBuiltInTessLevelInner = 12;
|
||||
|
||||
template <typename Visitor>
|
||||
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
|
||||
for (SizeT i = kSpirvHeaderWordCount; i < spirv.size();) {
|
||||
const Uint32 wordCount = spirv[i] >> 16;
|
||||
const Uint32 opcode = spirv[i] & 0xFFFFu;
|
||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
||||
visit(opcode, &spirv[i], wordCount);
|
||||
i += wordCount;
|
||||
}
|
||||
}
|
||||
|
||||
// -1 when the module declares no OutputVertices mode at all, which is itself a failure the
|
||||
// tests want to see named rather than silently compared against a wrong number.
|
||||
Int DeclaredOutputVertices(const Vector<Uint32>& spirv) {
|
||||
Int declared = -1;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == kOpExecutionMode && wordCount >= 4 && words[2] == kExecutionModeOutputVertices) {
|
||||
declared = static_cast<Int>(words[3]);
|
||||
}
|
||||
});
|
||||
return declared;
|
||||
}
|
||||
|
||||
// The built-in members of every block in the module, keyed by the struct's result id, in
|
||||
// member order. A gl_PerVertex is exactly such a struct, and its member list IS the shape the
|
||||
// neighbouring stage has to agree with.
|
||||
constexpr Uint32 kOpTypeStruct = 30;
|
||||
|
||||
std::map<Uint32, Vector<Uint32>> BuiltInBlockShapes(const Vector<Uint32>& spirv) {
|
||||
std::map<Uint32, Vector<Uint32>> shapes;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == kOpMemberDecorate && wordCount >= 5 && words[3] == kDecorationBuiltIn) {
|
||||
shapes[words[1]].push_back(words[4]);
|
||||
}
|
||||
});
|
||||
return shapes;
|
||||
}
|
||||
|
||||
// Member count of a struct type, so a shape comparison can also catch a block that grew a
|
||||
// NON-built-in member (which the decoration walk above would not see).
|
||||
Uint32 StructMemberCount(const Vector<Uint32>& spirv, Uint32 structId) {
|
||||
Uint32 count = 0;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == kOpTypeStruct && wordCount >= 2 && words[1] == structId) {
|
||||
count = wordCount - 2;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
std::set<Uint32> DeclaredBuiltIns(const Vector<Uint32>& spirv) {
|
||||
std::set<Uint32> builtIns;
|
||||
ForEachInstruction(spirv, [&](Uint32 opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode == kOpDecorate && wordCount >= 4 && words[2] == kDecorationBuiltIn) {
|
||||
builtIns.insert(words[3]);
|
||||
}
|
||||
if (opcode == kOpMemberDecorate && wordCount >= 5 && words[3] == kDecorationBuiltIn) {
|
||||
builtIns.insert(words[4]);
|
||||
}
|
||||
});
|
||||
return builtIns;
|
||||
}
|
||||
|
||||
Vector<Uint32> CompileGeneratedSource(Uint32 patchVertices) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
const String source = ProgramFactory::BuildPassthroughTessControlSource(patchVertices);
|
||||
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_TESS_CONTROL_SHADER, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log) << "\n" << source;
|
||||
if (!shaderResult) return {};
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||
if (!programResult) return {};
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_TESS_CONTROL_SHADER},
|
||||
.program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class PassthroughTessControlTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override { MobileGL::Initialize(); }
|
||||
};
|
||||
|
||||
// The whole reason this stage is generated per patch size rather than once: GL takes the output
|
||||
// patch size from PATCH_VERTICES, which is draw state. A program that links at the default 3 and
|
||||
// draws at 4 - which is exactly what
|
||||
// KHR-GL43.shader_storage_buffer_object.advanced-write-tessellation does - must get a stage built
|
||||
// for 4, or its evaluation stage reads gl_in[3] out of a three-element array.
|
||||
TEST_F(PassthroughTessControlTest, DeclaresTheRequestedPatchSize) {
|
||||
for (const Uint32 patchVertices : {1u, 2u, 3u, 4u, 16u, 32u}) {
|
||||
const Vector<Uint32> spirv = CompileGeneratedSource(patchVertices);
|
||||
ASSERT_FALSE(spirv.empty()) << "patchVertices=" << patchVertices;
|
||||
EXPECT_EQ(DeclaredOutputVertices(spirv), static_cast<Int>(patchVertices))
|
||||
<< "patchVertices=" << patchVertices;
|
||||
}
|
||||
}
|
||||
|
||||
// gl_Position in, gl_Position out, and both tessellation level arrays written: the four facts the
|
||||
// evaluation stage downstream of this depends on. Position appearing at all is what makes the
|
||||
// pass-through a pass-through; the levels are what GL's PATCH_DEFAULT_*_LEVEL state supplies when
|
||||
// there is no control shader, and without them the tessellator produces nothing.
|
||||
TEST_F(PassthroughTessControlTest, ForwardsPositionAndWritesBothLevelArrays) {
|
||||
const Vector<Uint32> spirv = CompileGeneratedSource(4);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
const std::set<Uint32> builtIns = DeclaredBuiltIns(spirv);
|
||||
EXPECT_TRUE(builtIns.contains(kBuiltInPosition));
|
||||
EXPECT_TRUE(builtIns.contains(kBuiltInInvocationId));
|
||||
EXPECT_TRUE(builtIns.contains(kBuiltInTessLevelOuter));
|
||||
EXPECT_TRUE(builtIns.contains(kBuiltInTessLevelInner));
|
||||
}
|
||||
|
||||
// The generated source carries nothing but gl_Position across the interface. If that ever grows a
|
||||
// user-defined varying, ReflectPassthroughTessControlNeed's "built-ins only" refusal stops being
|
||||
// the right gate and both have to move together.
|
||||
TEST_F(PassthroughTessControlTest, InterfaceIsBuiltInsOnly) {
|
||||
const String source = ProgramFactory::BuildPassthroughTessControlSource(4);
|
||||
EXPECT_EQ(source.find("layout(location"), String::npos) << source;
|
||||
EXPECT_NE(source.find("layout(vertices = 4) out;"), String::npos) << source;
|
||||
}
|
||||
|
||||
// THE load-bearing test. Vulkan matches built-in interface blocks by their whole shape, and this
|
||||
// stage is compiled ON ITS OWN - it never goes through the glslang link that gives a real program
|
||||
// its gl_PerVertex. So the shape it declares has to equal the shape a linked vertex+evaluation
|
||||
// program carries, and nothing at runtime says otherwise: a mismatch renders a black frame, no
|
||||
// error, no validation message. That is exactly how the first cut of this shipped-and-failed
|
||||
// (gl_Position only, three members short), and how the second did (glslang's default block for a
|
||||
// standalone control stage, which appends gl_CullDistance where a linked program has no such
|
||||
// member). This links the shader pair the motivating CTS case uses and compares the two shapes
|
||||
// directly.
|
||||
TEST_F(PassthroughTessControlTest, MatchesTheFrontendPerVertexBlock) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// Deliberately the shape of KHR-GL43.shader_storage_buffer_object.advanced-write-tessellation:
|
||||
// a vertex stage feeding an evaluation stage with no control stage in between.
|
||||
static const char* kVs = R"(#version 430 core
|
||||
layout(location = 0) in vec4 g_in_position;
|
||||
void main() { gl_Position = g_in_position; }
|
||||
)";
|
||||
static const char* kTes = R"(#version 430 core
|
||||
layout(quads) in;
|
||||
void main() {
|
||||
vec4 p0 = mix(gl_in[0].gl_Position, gl_in[1].gl_Position, gl_TessCoord.x);
|
||||
vec4 p1 = mix(gl_in[3].gl_Position, gl_in[2].gl_Position, gl_TessCoord.x);
|
||||
gl_Position = mix(p0, p1, gl_TessCoord.y);
|
||||
}
|
||||
)";
|
||||
static const char* kFs = R"(#version 430 core
|
||||
layout(location = 0) out vec4 g_fs_out;
|
||||
void main() { g_fs_out = vec4(0, 1, 0, 1); }
|
||||
)";
|
||||
|
||||
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_EVALUATION_SHADER, GL_FRAGMENT_SHADER};
|
||||
const Vector<const char*> sources{kVs, kTes, kFs};
|
||||
Vector<SharedPtr<glslang::TShader>> shaders;
|
||||
for (SizeT i = 0; i < types.size(); ++i) {
|
||||
ShaderAttrib attrib{.shaderType = types[i], .sourceStr = sources[i]};
|
||||
auto compiled = ShaderCompiler::CompileShader(attrib);
|
||||
ASSERT_TRUE(compiled) << compiled.error().log;
|
||||
shaders.push_back(compiled.value());
|
||||
}
|
||||
ProgramAttrib programAttrib{.shaders = shaders};
|
||||
auto linked = ShaderCompiler::LinkProgram(programAttrib);
|
||||
ASSERT_TRUE(linked) << linked.error().log;
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = types, .program = *linked.value()};
|
||||
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
ASSERT_TRUE(binary);
|
||||
ASSERT_EQ(binary->size(), types.size());
|
||||
|
||||
// The evaluation stage's gl_in is the block the pass-through has to feed. It is the only
|
||||
// built-in block that stage declares as an input, so the module holds exactly one such shape
|
||||
// besides its own gl_PerVertex output - and both are the same shape, which is the point.
|
||||
const auto tesShapes = BuiltInBlockShapes((*binary)[1]);
|
||||
ASSERT_FALSE(tesShapes.empty());
|
||||
const Vector<Uint32> frontendShape = tesShapes.begin()->second;
|
||||
const Uint32 frontendMembers = StructMemberCount((*binary)[1], tesShapes.begin()->first);
|
||||
for (const auto& [structId, shape] : tesShapes) {
|
||||
EXPECT_EQ(shape, frontendShape) << "the evaluation stage's own built-in blocks disagree";
|
||||
EXPECT_EQ(StructMemberCount((*binary)[1], structId), frontendMembers);
|
||||
}
|
||||
|
||||
const Vector<Uint32> passthrough = CompileGeneratedSource(4);
|
||||
ASSERT_FALSE(passthrough.empty());
|
||||
const auto passthroughShapes = BuiltInBlockShapes(passthrough);
|
||||
ASSERT_FALSE(passthroughShapes.empty());
|
||||
|
||||
Uint32 perVertexBlocksChecked = 0;
|
||||
for (const auto& [structId, shape] : passthroughShapes) {
|
||||
// gl_TessLevelOuter/Inner are decorated on plain variables, not on a block, so every
|
||||
// struct that reaches here is a gl_PerVertex - gl_in's and gl_out's.
|
||||
EXPECT_EQ(shape, frontendShape)
|
||||
<< "the pass-through control stage's gl_PerVertex no longer matches the one the "
|
||||
"frontend gives a linked vertex+evaluation program";
|
||||
EXPECT_EQ(StructMemberCount(passthrough, structId), frontendMembers)
|
||||
<< "the pass-through control stage's gl_PerVertex has a different member count";
|
||||
++perVertexBlocksChecked;
|
||||
}
|
||||
EXPECT_EQ(perVertexBlocksChecked, 2u) << "expected both gl_in and gl_out to be gl_PerVertex blocks";
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// MobileGL - MobileGL/MG_Test/Pipeline/ViewportIndexReflectionTest.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
|
||||
//
|
||||
// ProgramFactory::ReflectedWritesViewportIndexBuiltin is the switch that decides whether a
|
||||
// DirectVulkan pipeline declares one viewport or all sixteen. Getting it wrong is silent in both
|
||||
// directions and neither direction is caught by a state test:
|
||||
//
|
||||
// - a false NEGATIVE collapses every gl_ViewportIndex onto viewport 0, which is precisely the
|
||||
// bug the multi-viewport work exists to fix and which a set/get round trip cannot see;
|
||||
// - a false POSITIVE widens viewportCount for an ordinary Minecraft shader, costing a longer
|
||||
// vkCmdSetViewport per state change and, on a tiler, possibly a hardware fast path.
|
||||
//
|
||||
// So this compiles REAL GLSL through the same glslang path the renderer uses and reflects the
|
||||
// SPIR-V that comes out, rather than asserting against hand-assembled words: what has to hold is
|
||||
// that the detector agrees with what glslang actually emits for a shader that writes the builtin,
|
||||
// including the stage-by-stage question of WHERE it may be written (GL 4.1 allows the geometry
|
||||
// stage; ARB_shader_viewport_layer_array adds vertex and tessellation evaluation).
|
||||
//
|
||||
// The end-to-end claim - that a detected writer really does route pixels to its own viewport -
|
||||
// lives in MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
|
||||
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <spirv_reflect.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
|
||||
namespace {
|
||||
|
||||
Vector<Uint32> CompileToSpirv(GLenum stage, const String& source) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||
if (!shaderResult) return {};
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||
if (!programResult) return {};
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
// Owns the reflection module so a failing EXPECT cannot leak it.
|
||||
class ReflectModule {
|
||||
public:
|
||||
explicit ReflectModule(const Vector<Uint32>& spirv) {
|
||||
if (spirv.empty()) return;
|
||||
m_created = spvReflectCreateShaderModule(spirv.size() * sizeof(Uint32), spirv.data(), &m_module) ==
|
||||
SPV_REFLECT_RESULT_SUCCESS;
|
||||
}
|
||||
~ReflectModule() {
|
||||
if (m_created) spvReflectDestroyShaderModule(&m_module);
|
||||
}
|
||||
ReflectModule(const ReflectModule&) = delete;
|
||||
ReflectModule& operator=(const ReflectModule&) = delete;
|
||||
|
||||
Bool Created() const { return m_created; }
|
||||
const SpvReflectShaderModule& Get() const { return m_module; }
|
||||
|
||||
private:
|
||||
SpvReflectShaderModule m_module{};
|
||||
Bool m_created = false;
|
||||
};
|
||||
|
||||
class ViewportIndexReflectionTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override { MobileGL::Initialize(); }
|
||||
};
|
||||
|
||||
const char* const kGeometryWritesViewportIndex = R"(#version 410 core
|
||||
layout(points, invocations = 16) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
void main() {
|
||||
gl_ViewportIndex = gl_InvocationID;
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// Same stage, same shape, writing gl_Layer INSTEAD. Layered rendering and viewport routing
|
||||
// are different features and the detector must not confuse them: a Minecraft-style cubemap
|
||||
// pass writes gl_Layer and must keep the one-viewport pipeline.
|
||||
const char* const kGeometryWritesLayerOnly = R"(#version 410 core
|
||||
layout(points, invocations = 6) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
void main() {
|
||||
gl_Layer = gl_InvocationID;
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kPlainGeometry = R"(#version 410 core
|
||||
layout(points, invocations = 1) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
void main() {
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kPlainVertex = R"(#version 410 core
|
||||
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
const char* const kPlainFragment = R"(#version 410 core
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
)";
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, TrueForAGeometryShaderThatAssignsViewportIndex) {
|
||||
const ReflectModule module(CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesViewportIndex));
|
||||
ASSERT_TRUE(module.Created());
|
||||
EXPECT_TRUE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(module.Get()))
|
||||
<< "a shader that assigns gl_ViewportIndex must get a multi-viewport pipeline; missing it is what "
|
||||
"collapses every index onto viewport 0";
|
||||
}
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, FalseForAGeometryShaderThatOnlyAssignsLayer) {
|
||||
const ReflectModule module(CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesLayerOnly));
|
||||
ASSERT_TRUE(module.Created());
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(module.Get()))
|
||||
<< "gl_Layer is layered rendering, not viewport routing; widening viewportCount for it costs the "
|
||||
"single-viewport fast path for nothing";
|
||||
}
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, FalseForAPlainGeometryShader) {
|
||||
const ReflectModule module(CompileToSpirv(GL_GEOMETRY_SHADER, kPlainGeometry));
|
||||
ASSERT_TRUE(module.Created());
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(module.Get()));
|
||||
}
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, FalseForTheOrdinaryVertexAndFragmentStages) {
|
||||
// The shape every real application ships: neither stage may widen the pipeline.
|
||||
const ReflectModule vertexModule(CompileToSpirv(GL_VERTEX_SHADER, kPlainVertex));
|
||||
ASSERT_TRUE(vertexModule.Created());
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(vertexModule.Get()));
|
||||
|
||||
const ReflectModule fragmentModule(CompileToSpirv(GL_FRAGMENT_SHADER, kPlainFragment));
|
||||
ASSERT_TRUE(fragmentModule.Created());
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(fragmentModule.Get()));
|
||||
}
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, FalseForAnEmptyModuleWithoutDereferencing) {
|
||||
// A default-constructed module has no entry points. The scan runs on every link, so it
|
||||
// must survive a reflection that never got built rather than walk a null array.
|
||||
SpvReflectShaderModule emptyModule{};
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(emptyModule));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -516,17 +516,17 @@ TEST_F(ParallelShaderCompileTest, MaxShaderCompilerThreadsIgnoresTheCurrentBudge
|
||||
TEST_F(ParallelShaderCompileTest, BothBackendsAdvertiseTheExtensionIffAsyncIsEnabled) {
|
||||
{
|
||||
const AsyncModeScope async(true);
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false),
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile));
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false),
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile));
|
||||
}
|
||||
{
|
||||
const AsyncModeScope async(false);
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false),
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile))
|
||||
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false),
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile))
|
||||
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
|
||||
}
|
||||
|
||||
@@ -2692,11 +2692,13 @@ out vec4 fragColor;
|
||||
float fma
|
||||
(float a, float b, float c) { return a * b + c; }
|
||||
float sinh(float x, float y) { return x * y; }
|
||||
float length_squared(vec3 value) { return dot(value, value); }
|
||||
float round(float x) { return floor(x + 0.5); }
|
||||
float min3(float a, float b, float c) { return min(min(a, b), c); }
|
||||
|
||||
void main() {
|
||||
fragColor = vec4(fma(0.1, 0.2, 0.3), sinh(0.4, 2.0), round(1.25), min3(0.1, 0.2, 0.3));
|
||||
fragColor = vec4(fma(0.1, 0.2, 0.3), sinh(0.4, 2.0), round(1.25),
|
||||
min3(0.1, 0.2, 0.3) + length_squared(vec3(0.1, 0.2, 0.3)));
|
||||
}
|
||||
)";
|
||||
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
|
||||
@@ -2707,6 +2709,7 @@ void main() {
|
||||
if (essl.find("fragColor") == String::npos) continue; // fragment module only
|
||||
EXPECT_NE(essl.find("mg_fma("), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("mg_sinh("), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("mg_length_squared("), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("mg_round("), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("mg_min3("), String::npos) << essl;
|
||||
EXPECT_EQ(essl.find("float fma("), String::npos) << essl;
|
||||
|
||||
@@ -52,20 +52,24 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
|
||||
OpEntryPoint Fragment %main "main" %outColor
|
||||
OpExecutionMode %main OriginUpperLeft
|
||||
OpName %globalSampler "sampler"
|
||||
OpName %globalNew "new"
|
||||
OpName %paramSampler "sampler"
|
||||
OpName %paramNew "new"
|
||||
OpName %main "main"
|
||||
OpDecorate %outColor Location 0
|
||||
%void = OpTypeVoid
|
||||
%float = OpTypeFloat 32
|
||||
%v4float = OpTypeVector %float 4
|
||||
%mainFn = OpTypeFunction %void
|
||||
%paramFn = OpTypeFunction %void %float
|
||||
%paramFn = OpTypeFunction %void %float %float
|
||||
%outV4Ptr = OpTypePointer Output %v4float
|
||||
%privatePtr = OpTypePointer Private %float
|
||||
%outColor = OpVariable %outV4Ptr Output
|
||||
%globalSampler = OpVariable %privatePtr Private
|
||||
%globalNew = OpVariable %privatePtr Private
|
||||
%helper = OpFunction %void None %paramFn
|
||||
%paramSampler = OpFunctionParameter %float
|
||||
%paramNew = OpFunctionParameter %float
|
||||
%helperBody = OpLabel
|
||||
OpReturn
|
||||
OpFunctionEnd
|
||||
@@ -91,6 +95,7 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
|
||||
ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText));
|
||||
|
||||
EXPECT_NE(outputText.find("\"MGL_COMPAT_sampler\""), String::npos);
|
||||
EXPECT_NE(outputText.find("\"MGL_COMPAT_new\""), String::npos);
|
||||
|
||||
SizeT exactSamplerNameCount = 0;
|
||||
SizeT searchOffset = 0;
|
||||
@@ -99,6 +104,14 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
|
||||
searchOffset += std::strlen("\"sampler\"");
|
||||
}
|
||||
EXPECT_EQ(exactSamplerNameCount, 1u);
|
||||
|
||||
SizeT exactNewNameCount = 0;
|
||||
searchOffset = 0;
|
||||
while ((searchOffset = outputText.find("\"new\"", searchOffset)) != String::npos) {
|
||||
++exactNewNameCount;
|
||||
searchOffset += std::strlen("\"new\"");
|
||||
}
|
||||
EXPECT_EQ(exactNewNameCount, 1u);
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, UnformattedFloatStorageImagesKeepIntegerAtomicImagesTyped) {
|
||||
@@ -2060,153 +2073,6 @@ void main() {
|
||||
EXPECT_NE(source.find("layout(std140) uniform Blk"), String::npos);
|
||||
}
|
||||
|
||||
namespace {
|
||||
String MakeLinearSubgroupPrefixScanShader() {
|
||||
return R"(#version 460 core
|
||||
#extension GL_KHR_shader_subgroup_arithmetic : enable
|
||||
layout(local_size_x = 1024) in;
|
||||
shared float prefixSumCache[64];
|
||||
|
||||
layout(std430, binding = 0) writeonly buffer OutputBuffer {
|
||||
float outputValues[];
|
||||
};
|
||||
|
||||
void main() {
|
||||
float importance = 1.0f;
|
||||
float prefixSum = subgroupInclusiveAdd(importance);
|
||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) prefixSumCache[gl_SubgroupID] = prefixSum;
|
||||
barrier();
|
||||
uint loopLength = uint(findMSB(gl_NumSubgroups));
|
||||
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
|
||||
for (uint i = 0; i < loopLength; i++) {
|
||||
if ((gl_SubgroupID & (1u << i)) > 0u) {
|
||||
prefixSum += prefixSumCache[(gl_SubgroupID >> i << i) - 1u];
|
||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) prefixSumCache[gl_SubgroupID] = prefixSum;
|
||||
}
|
||||
barrier();
|
||||
}
|
||||
if (gl_LocalInvocationID.x == uint(1024 - 1)) prefixSumCache[0] = prefixSum;
|
||||
barrier();
|
||||
float sum = prefixSumCache[0];
|
||||
float warp = (prefixSum - importance) / sum - float(gl_LocalInvocationID.x + 1u) / float(1024);
|
||||
outputValues[gl_GlobalInvocationID.x] = warp;
|
||||
}
|
||||
)";
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanUsesSharedMemoryAndProducesValidSpirv) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String source = MakeLinearSubgroupPrefixScanShader();
|
||||
ASSERT_TRUE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
|
||||
|
||||
EXPECT_NE(source.find("shared float prefixSumCache[1024]"), String::npos) << source;
|
||||
EXPECT_NE(source.find("mglVirtualSubgroupInvocation"), String::npos) << source;
|
||||
EXPECT_NE(source.find("for (uint mglPrefixLane"), String::npos) << source;
|
||||
EXPECT_EQ(source.find("subgroupInclusiveAdd"), String::npos) << source;
|
||||
EXPECT_EQ(source.find("gl_Subgroup"), String::npos) << source;
|
||||
|
||||
const String onceRewritten = source;
|
||||
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
|
||||
EXPECT_EQ(source, onceRewritten);
|
||||
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
ASSERT_TRUE(shaderResult) << shaderResult.error().log << "\nsource:\n" << source;
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
ASSERT_TRUE(programResult) << programResult.error().log;
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
ASSERT_TRUE(binaryResult) << binaryResult.error().log;
|
||||
ASSERT_EQ(binaryResult->size(), 1u);
|
||||
|
||||
String validationDiagnostics;
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
tools.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&, const char* message) {
|
||||
validationDiagnostics += message;
|
||||
validationDiagnostics += '\n';
|
||||
});
|
||||
EXPECT_TRUE(tools.Validate(binaryResult->front())) << validationDiagnostics;
|
||||
|
||||
String spirvText;
|
||||
ASSERT_TRUE(tools.Disassemble(binaryResult->front(), &spirvText));
|
||||
EXPECT_EQ(spirvText.find("OpGroupNonUniform"), String::npos) << spirvText;
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanRejectsOtherStagesAndSubgroupWidths) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const String original = MakeLinearSubgroupPrefixScanShader();
|
||||
for (const auto& [stage, subgroupSize] :
|
||||
{std::pair{ShaderStage::Compute, Uint32{32}}, std::pair{ShaderStage::Fragment, Uint32{64}},
|
||||
std::pair{ShaderStage::Compute, Uint32{96}}}) {
|
||||
String source = original;
|
||||
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(stage, subgroupSize, source));
|
||||
EXPECT_EQ(source, original);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanRejectsPartialOrUnsafeTemplateMatches) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const auto expectUnchanged = [](String source) {
|
||||
const String original = source;
|
||||
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
|
||||
EXPECT_EQ(source, original);
|
||||
};
|
||||
|
||||
String wrongLocalSize = MakeLinearSubgroupPrefixScanShader();
|
||||
wrongLocalSize.replace(wrongLocalSize.find("local_size_x = 1024"), std::strlen("local_size_x = 1024"),
|
||||
"local_size_x = 512");
|
||||
expectUnchanged(std::move(wrongLocalSize));
|
||||
|
||||
String cacheHasAnotherUse = MakeLinearSubgroupPrefixScanShader();
|
||||
cacheHasAnotherUse.insert(cacheHasAnotherUse.find("float importance"), "prefixSumCache[0] = 0.0f;\n ");
|
||||
expectUnchanged(std::move(cacheHasAnotherUse));
|
||||
|
||||
String extraSubgroupBuiltin = MakeLinearSubgroupPrefixScanShader();
|
||||
extraSubgroupBuiltin.insert(extraSubgroupBuiltin.find("float importance"),
|
||||
"uvec4 extraMask = gl_SubgroupEqMask;\n ");
|
||||
expectUnchanged(std::move(extraSubgroupBuiltin));
|
||||
|
||||
String alteredBarrier = MakeLinearSubgroupPrefixScanShader();
|
||||
alteredBarrier.replace(alteredBarrier.find("barrier();"), std::strlen("barrier();"), "memoryBarrierShared();");
|
||||
expectUnchanged(std::move(alteredBarrier));
|
||||
|
||||
String nestedScan = MakeLinearSubgroupPrefixScanShader();
|
||||
nestedScan.insert(nestedScan.find("float prefixSum ="), "if (importance > 0.0f) {\n ");
|
||||
const SizeT consumerEnd = nestedScan.find(';', nestedScan.find("float warp ="));
|
||||
ASSERT_NE(consumerEnd, String::npos);
|
||||
nestedScan.insert(consumerEnd + 1, "\n }");
|
||||
expectUnchanged(std::move(nestedScan));
|
||||
|
||||
// ARB/NV spellings of lane-width-sensitive builtins must block the rewrite exactly
|
||||
// like their KHR counterparts.
|
||||
String arbSubgroupBuiltin = MakeLinearSubgroupPrefixScanShader();
|
||||
arbSubgroupBuiltin.insert(arbSubgroupBuiltin.find("float importance"),
|
||||
"uint arbLane = gl_SubGroupInvocationARB;\n ");
|
||||
expectUnchanged(std::move(arbSubgroupBuiltin));
|
||||
|
||||
String arbBallotCall = MakeLinearSubgroupPrefixScanShader();
|
||||
arbBallotCall.insert(arbBallotCall.find("float importance"),
|
||||
"uint64_t arbMask = ballotARB(true);\n ");
|
||||
expectUnchanged(std::move(arbBallotCall));
|
||||
|
||||
String nvWarpBuiltin = MakeLinearSubgroupPrefixScanShader();
|
||||
nvWarpBuiltin.insert(nvWarpBuiltin.find("float importance"),
|
||||
"uint warpSize = gl_WarpSizeNV;\n ");
|
||||
expectUnchanged(std::move(nvWarpBuiltin));
|
||||
|
||||
String nvShuffleCall = MakeLinearSubgroupPrefixScanShader();
|
||||
nvShuffleCall.insert(nvShuffleCall.find("float importance"),
|
||||
"float other = shuffleNV(1.0f, 0u, 32u);\n ");
|
||||
expectUnchanged(std::move(nvShuffleCall));
|
||||
}
|
||||
|
||||
// The LEXICAL half must fire at the source level (before the parse) for the
|
||||
// preempt-list names - the end-to-end ESSL tests cannot tell which half did the
|
||||
// rename, and for these names the parse would fail without the source rewrite.
|
||||
@@ -2435,9 +2301,6 @@ TEST_F(ProgramUtilTest, CompileEnvFingerprintTracksEveryInput) {
|
||||
otherExtensions.advertisedExtensions.push_back(MobileGL::E_GL_ARB_gpu_shader_int64);
|
||||
EXPECT_NE(ComputeCompileEnvFingerprint(otherExtensions), baseline);
|
||||
|
||||
CompileEnv otherQuirk = base;
|
||||
otherQuirk.subgroupPrefixScanQuirk = MobileGL::MG_Config::QuirkOverride::ForceOn;
|
||||
EXPECT_NE(ComputeCompileEnvFingerprint(otherQuirk), baseline);
|
||||
}
|
||||
|
||||
// The no-backend fallback must stay exactly what the pipeline used to do inline:
|
||||
@@ -2836,16 +2699,6 @@ vec4 helperTint() { return vec4(1.0); }
|
||||
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) {
|
||||
@@ -2867,7 +2720,7 @@ TEST_F(ProgramUtilTest, DeadPrivateChainVertexInputIsEliminatedFromOptimizedBina
|
||||
<< "entry-point-with-calls shape it exists for";
|
||||
|
||||
Vector<Uint32> optimized;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized, true, true));
|
||||
|
||||
const SpirvVariableCensus after = TakeVariableCensus(optimized);
|
||||
EXPECT_EQ(after.inputCount, 1u)
|
||||
@@ -2905,7 +2758,7 @@ void main() {
|
||||
ASSERT_GE(before.outputCount, 3u);
|
||||
|
||||
Vector<Uint32> optimized;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized));
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized, true, true));
|
||||
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";
|
||||
@@ -2964,17 +2817,15 @@ void main() {
|
||||
// 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_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized, true, true));
|
||||
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_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized, true, false));
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore);
|
||||
}
|
||||
}
|
||||
@@ -3044,10 +2895,9 @@ void main() {
|
||||
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));
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized, true, true));
|
||||
EXPECT_EQ(CountRectImageTypes(optimized), 0u);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "a rectangle module must leave the chain valid, not latched as a failure";
|
||||
@@ -3072,10 +2922,9 @@ void main() {
|
||||
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));
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, optimized, true, true));
|
||||
EXPECT_FALSE(AnyLocationOnUniformStorage(optimized));
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the stripped module must validate clean";
|
||||
@@ -3172,11 +3021,10 @@ void main() {
|
||||
<< "the fixture must reproduce the defect before the fix is asked to remove it:\n"
|
||||
<< DisassembleSpirv(raw);
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> legalized;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized));
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized, true));
|
||||
ASSERT_FALSE(legalized.empty());
|
||||
|
||||
const String disassembly = DisassembleSpirv(legalized);
|
||||
@@ -3213,11 +3061,10 @@ void main() {
|
||||
ASSERT_TRUE(LegalizeFragmentOutputIndexPass::BinaryHasDynamicOutputIndexing(raw))
|
||||
<< DisassembleSpirv(raw);
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> legalized;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized));
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized, true));
|
||||
ASSERT_FALSE(legalized.empty());
|
||||
|
||||
const String disassembly = DisassembleSpirv(legalized);
|
||||
@@ -3257,11 +3104,10 @@ void main() {
|
||||
ASSERT_FALSE(raw.empty());
|
||||
ASSERT_TRUE(LegalizeFragmentOutputIndexPass::BinaryHasDynamicOutputIndexing(raw));
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> legalized;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized));
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized, true));
|
||||
ASSERT_FALSE(legalized.empty());
|
||||
|
||||
const String disassembly = DisassembleSpirv(legalized);
|
||||
@@ -3300,7 +3146,7 @@ void main() {
|
||||
ASSERT_FALSE(LegalizeFragmentOutputIndexPass::BinaryHasDynamicOutputIndexing(raw));
|
||||
|
||||
Vector<Uint32> legalized;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized));
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(raw, legalized, true));
|
||||
EXPECT_EQ(legalized, raw) << "the module must not be rewritten - not even re-serialized - when "
|
||||
"nothing indexes a fragment output dynamically";
|
||||
}
|
||||
@@ -3550,11 +3396,10 @@ TEST_F(ProgramUtilTest, Lower1DArrayImagesRewritesTheTypeAndWidensTheCoordinate)
|
||||
ASSERT_EQ(Count1DArrayStorageImageTypes(spirv), 1u)
|
||||
<< "the shared chain must leave the 1D-array image for this pass to handle";
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
|
||||
ASSERT_FALSE(lowered.empty());
|
||||
|
||||
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 0u)
|
||||
@@ -3602,11 +3447,10 @@ void main() { ssb.sum = imageLoad(i0, ivec2(2, 3)).r + imageLoad(i1, ivec3(1, 1,
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
|
||||
ASSERT_EQ(Count1DArrayStorageImageTypes(spirv), 1u);
|
||||
|
||||
SpirvValidationScope validationOn(true);
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
|
||||
ASSERT_FALSE(lowered.empty());
|
||||
|
||||
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 0u) << DisassembleSpirv(lowered);
|
||||
@@ -3636,7 +3480,7 @@ void main() { ssb.sum = imageLoad(i0, 2).r; }
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
|
||||
EXPECT_EQ(lowered, spirv) << "a non-arrayed 1D storage image must pass through byte for byte";
|
||||
|
||||
const String essl = DecompileToEssl(lowered);
|
||||
@@ -3660,7 +3504,7 @@ void main() { fragColor = texture(uTex, vUv); }
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
|
||||
EXPECT_EQ(lowered, spirv) << "a sampled 1D-array image must pass through byte for byte";
|
||||
}
|
||||
|
||||
@@ -3684,8 +3528,315 @@ void main() { ssb.sum = uint(imageSize(i0).x) + imageLoad(i0, ivec2(0, 0)).r; }
|
||||
<< "the fixture must contain the shape the pass declines";
|
||||
|
||||
Vector<Uint32> lowered;
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered));
|
||||
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
|
||||
EXPECT_EQ(lowered, spirv) << "a declined module must be handed back untouched, not partly rewritten";
|
||||
EXPECT_EQ(Count1DArrayStorageImageTypes(lowered), 1u)
|
||||
<< "declining means the 1D-array type is still there for the driver to reject";
|
||||
}
|
||||
|
||||
// --- image format qualifier bake (BakeImageFormatsPass) ---------------------------------------
|
||||
//
|
||||
// Desktop GLSL 4.2 lets a writeonly image declaration omit its format layout qualifier; GLSL ES
|
||||
// requires one of every image, and Adreno says so as "all images have to define layout format",
|
||||
// losing the whole program. The only correct qualifier to substitute is the format the
|
||||
// application passed to glBindImageTexture for that unit, so the transpile bakes it in.
|
||||
|
||||
namespace {
|
||||
Uint CountSpirvOpcode(const String& disassembly, const String& opcode) {
|
||||
Uint count = 0;
|
||||
SizeT offset = 0;
|
||||
const String needle = opcode + " ";
|
||||
while ((offset = disassembly.find(needle, offset)) != String::npos) {
|
||||
count += 1;
|
||||
offset += needle.size();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
constexpr Uint kGlR32ui = 0x8236;
|
||||
constexpr Uint kGlRgba32ui = 0x8D70;
|
||||
constexpr Uint kGlR8ui = 0x8232;
|
||||
constexpr Uint kGlR32f = 0x822E;
|
||||
} // namespace
|
||||
|
||||
// The KHR-GL4x.packed_depth_stencil.stencil_texturing compute shader, reduced: one format-less
|
||||
// writeonly image, and a bind of a concrete format to the unit it addresses. (The DEPTH half of
|
||||
// that case binds GL_R32F; the stencil half's GL_R8UI is one SPIRV-Cross will not print and takes
|
||||
// the text route instead - see the test below.)
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsGivesAFormatlessImageTheFormatBoundToItsUnit) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
|
||||
layout (local_size_x = 1) in;
|
||||
writeonly uniform uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(15u, 0u, 0u, 0u)); }
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv))
|
||||
<< "the fixture must reproduce the defect before the fix is asked to remove it:\n"
|
||||
<< DisassembleSpirv(spirv);
|
||||
// Precondition: SPIRV-Cross prints no format for it, which is the ESSL the driver refuses.
|
||||
EXPECT_EQ(DecompileToEssl(spirv).find("r32ui"), String::npos);
|
||||
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked, true));
|
||||
ASSERT_FALSE(baked.empty());
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked)) << DisassembleSpirv(baked);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the baked module must stay validator-clean:\n"
|
||||
<< DisassembleSpirv(baked);
|
||||
|
||||
const String essl = DecompileToEssl(baked);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_NE(essl.find("r32ui"), String::npos)
|
||||
<< "the bound format must reach the declaration as a layout qualifier:\n" << essl;
|
||||
EXPECT_NE(essl.find("writeonly"), String::npos)
|
||||
<< "the access qualifier the declaration already had must survive:\n" << essl;
|
||||
}
|
||||
|
||||
// SPIRV-Cross THROWS rather than printing the formats it calls desktop-only when it targets ESSL
|
||||
// (Compiler::is_desktop_only_format), and a throw loses the whole stage - so baking one of those
|
||||
// into the module would trade a missing qualifier for a missing shader. They are left format-less
|
||||
// here and completed on the emitted text instead (PrgramImpl::BakeImageFormatQualifiers). r8ui,
|
||||
// which the stencil half of the packed_depth_stencil case binds, is one of them.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsLeavesTheFormatsSpirvCrossRefusesToPrint) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR8ui))
|
||||
<< "if SPIRV-Cross ever learns to print r8ui for ES, the text completion can go";
|
||||
ASSERT_TRUE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR32ui));
|
||||
EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(kGlR8ui), "r8ui");
|
||||
EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(0x8051 /*GL_RGB8*/), "");
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
|
||||
layout (local_size_x = 1) in;
|
||||
writeonly uniform uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(15u)); }
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR8ui}}, baked));
|
||||
EXPECT_EQ(baked, spirv) << "a format SPIRV-Cross cannot print must leave the module untouched";
|
||||
// ...and the stage still transpiles, which is the whole point of declining.
|
||||
EXPECT_FALSE(DecompileToEssl(baked).empty());
|
||||
}
|
||||
|
||||
// A DECLARED format is authoritative: GL requires the qualifier, the bind format and the
|
||||
// texture's internal format to be in the same class, but the qualifier is what the shader is
|
||||
// specified to read the memory as, and a bake that overrode it would change what the shader does.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsNeverOverridesADeclaredFormat) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
|
||||
layout (local_size_x = 1) in;
|
||||
layout (binding = 0, rgba32ui) writeonly uniform uimage2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
|
||||
|
||||
Vector<Uint32> baked;
|
||||
// Even asked to, with a format of the right component class.
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked, true));
|
||||
EXPECT_EQ(baked, spirv) << "a module with nothing format-less must pass through byte for byte";
|
||||
EXPECT_NE(DecompileToEssl(baked).find("rgba32ui"), String::npos);
|
||||
}
|
||||
|
||||
// Review finding. Every use has to be one the retype can carry end to end, and the decision has
|
||||
// to be made BEFORE anything is mutated - a half-retyped module is not something a later decline
|
||||
// could undo. An image handed to a FUNCTION is the shape that reaches SPIRV-Cross intact (nothing
|
||||
// in the ESSL chain inlines), and its OpFunctionCall is a use this pass does not follow.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsDeclinesAnImagePassedToAFunction) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
|
||||
layout (local_size_x = 1) in;
|
||||
writeonly uniform uimage2D uni_image;
|
||||
void writeIt(writeonly uimage2D img) { imageStore(img, ivec2(0), uvec4(1u)); }
|
||||
void main() { writeIt(uni_image); }
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
|
||||
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked, true));
|
||||
EXPECT_EQ(baked, spirv) << "a shape the retype cannot follow must leave the module untouched, "
|
||||
"not partly rewritten:\n"
|
||||
<< DisassembleSpirv(baked);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore);
|
||||
}
|
||||
|
||||
// spirv-val requires the Image Format's component class to agree with the OpTypeImage's Sampled
|
||||
// Type. Binding a uint format to a float image is an application error GL leaves undefined;
|
||||
// baking it would turn that into an INVALID module, which is strictly worse than the compile
|
||||
// error the shader already has, so the image is left format-less.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsDeclinesAFormatOfTheWrongComponentClass) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
|
||||
layout (local_size_x = 1) in;
|
||||
writeonly uniform image2D uni_image;
|
||||
void main() { imageStore(uni_image, ivec2(0), vec4(1.0)); }
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32ui}}, baked, true));
|
||||
EXPECT_EQ(baked, spirv) << "a declined module must be handed back untouched, not partly rewritten";
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore);
|
||||
|
||||
// ...and the same image with a float bind format is baked, so the decline above is about the
|
||||
// class and not about the pass refusing float images.
|
||||
Vector<Uint32> bakedFloat;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlR32f}}, bakedFloat, true));
|
||||
EXPECT_NE(DecompileToEssl(bakedFloat).find("r32f"), String::npos) << DisassembleSpirv(bakedFloat);
|
||||
}
|
||||
|
||||
// Two format-less images of the same type share ONE OpTypeImage. Giving them different formats
|
||||
// therefore cannot be an in-place edit of that type - each needs its own declaration, and the
|
||||
// variable, the loads and (for arrays) the access chains all have to follow.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsSplitsATypeTwoImagesShareWhenTheirFormatsDiffer) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
|
||||
layout (local_size_x = 1) in;
|
||||
writeonly uniform uimage2D imgA;
|
||||
writeonly uniform uimage2D imgB;
|
||||
void main() {
|
||||
imageStore(imgA, ivec2(0), uvec4(1u));
|
||||
imageStore(imgB, ivec2(0), uvec4(2u));
|
||||
}
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_EQ(CountSpirvOpcode(DisassembleSpirv(spirv), "OpTypeImage"), 1u)
|
||||
<< "the fixture must have the two images sharing one type:\n" << DisassembleSpirv(spirv);
|
||||
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(
|
||||
spirv, {{"imgA", kGlR32ui}, {"imgB", kGlRgba32ui}}, baked, true));
|
||||
ASSERT_FALSE(baked.empty());
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "splitting the shared type must not leave a dangling or duplicate declaration:\n"
|
||||
<< DisassembleSpirv(baked);
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked));
|
||||
|
||||
const String essl = DecompileToEssl(baked);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_NE(essl.find("r32ui"), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("rgba32ui"), String::npos) << essl;
|
||||
}
|
||||
|
||||
// The mirror of the split: when the module ALREADY declares the type the bake wants, the two must
|
||||
// be JOINED, not duplicated. SPIR-V forbids two identical non-aggregate type declarations, and
|
||||
// that is exactly the defect an earlier image pass shipped and a reviewer caught.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsJoinsATypeTheModuleAlreadyDeclares) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
|
||||
layout (local_size_x = 1) in;
|
||||
writeonly uniform uimage2D formatless;
|
||||
layout (binding = 1, r32ui) writeonly uniform uimage2D declared;
|
||||
void main() {
|
||||
imageStore(formatless, ivec2(0), uvec4(1u));
|
||||
imageStore(declared, ivec2(0), uvec4(2u));
|
||||
}
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_EQ(CountSpirvOpcode(DisassembleSpirv(spirv), "OpTypeImage"), 2u)
|
||||
<< "the fixture needs one Unknown-format and one r32ui image type:\n" << DisassembleSpirv(spirv);
|
||||
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"formatless", kGlR32ui}}, baked, true));
|
||||
ASSERT_FALSE(baked.empty());
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the baked image collided with the module's own r32ui image and left a duplicate type:\n"
|
||||
<< DisassembleSpirv(baked);
|
||||
EXPECT_EQ(CountSpirvOpcode(DisassembleSpirv(baked), "OpTypeImage"), 1u)
|
||||
<< "the two identical image types must be the same declaration:\n" << DisassembleSpirv(baked);
|
||||
}
|
||||
|
||||
// An ARRAY of format-less images: the variable's type is a pointer to an array, every use goes
|
||||
// through an OpAccessChain, and all three levels have to be rebuilt for the load to still type-check.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsRetypesAnArrayOfFormatlessImagesThroughItsAccessChains) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
|
||||
layout (local_size_x = 1) in;
|
||||
writeonly uniform uimage2D imgs[2];
|
||||
void main() {
|
||||
for (int i = 0; i < 2; ++i) imageStore(imgs[i], ivec2(0), uvec4(uint(i)));
|
||||
}
|
||||
)",
|
||||
GL_COMPUTE_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv));
|
||||
|
||||
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"imgs", kGlR32ui}}, baked, true));
|
||||
ASSERT_FALSE(baked.empty());
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(baked)) << DisassembleSpirv(baked);
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
|
||||
<< "the array and pointer types above the image must have been rebuilt too:\n"
|
||||
<< DisassembleSpirv(baked);
|
||||
EXPECT_NE(DecompileToEssl(baked).find("r32ui"), String::npos);
|
||||
}
|
||||
|
||||
// A SAMPLED image's format operand is Unknown in every GLSL dialect and has no qualifier to bake;
|
||||
// only storage images (Sampled == 2) are in scope.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsLeavesSampledImagesAlone) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 430 core
|
||||
uniform usampler2D uni_sampler;
|
||||
out uvec4 fragColor;
|
||||
in vec2 vUv;
|
||||
void main() { fragColor = texture(uni_sampler, vUv); }
|
||||
)",
|
||||
GL_FRAGMENT_SHADER);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresFormatlessStorageImage(spirv))
|
||||
<< "a sampled image must not read as a format-less STORAGE image:\n" << DisassembleSpirv(spirv);
|
||||
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_sampler", kGlR32ui}}, baked, true));
|
||||
EXPECT_EQ(baked, spirv) << "a sampled image must pass through byte for byte";
|
||||
}
|
||||
|
||||
// The core/extended split the emitted ESSL depends on: GLSL ES has thirteen image formats, and a
|
||||
// bind format outside them only compiles with GL_NV_image_formats - which the backend must not
|
||||
// request on a driver that does not advertise it.
|
||||
TEST_F(ProgramUtilTest, EsslCoreImageFormatSetIsTheThirteenTheSpecLists) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlR32ui));
|
||||
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlRgba32ui));
|
||||
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlR32f));
|
||||
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8058 /*GL_RGBA8*/));
|
||||
// The stencil half of KHR-GL4x.packed_depth_stencil.stencil_texturing binds this one, and it
|
||||
// is NOT core - the whole reason the directive machinery exists.
|
||||
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(kGlR8ui));
|
||||
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x822D /*GL_R16F*/));
|
||||
// Not an image format at all.
|
||||
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8051 /*GL_RGB8*/));
|
||||
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0 /*GL_NONE*/));
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h>
|
||||
#include <MG_Util/Math/HalfFloat.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
#include <MG_Util/Types.h>
|
||||
@@ -710,6 +711,37 @@ TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) {
|
||||
EXPECT_TRUE(backend.GetDynamicParameters().SubgroupQuadOperationsInAllStages);
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, CapabilityRefreshInvalidatesTheCachedCompileEnvironment) {
|
||||
using namespace MobileGL;
|
||||
|
||||
auto previousContext = Move(MG_State::pGLContext);
|
||||
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
|
||||
auto backend = MakeUnique<MG_Backend::DirectVulkan::BackendObject_DirectVulkan>();
|
||||
auto* backendPtr = backend.get();
|
||||
MG_Backend::pActiveBackendObject = Move(backend);
|
||||
|
||||
const auto before = MG_State::pGLContext->GetCompileEnv();
|
||||
EXPECT_EQ(before->params.SubgroupSize, 0u);
|
||||
|
||||
MG_External::VulkanCapabilities caps;
|
||||
caps.SupportsShaderSubgroup = true;
|
||||
caps.SubgroupSize = 8;
|
||||
caps.SubgroupSupportedStages = VK_SHADER_STAGE_COMPUTE_BIT;
|
||||
caps.SubgroupSupportedOperations = VK_SUBGROUP_FEATURE_BASIC_BIT | VK_SUBGROUP_FEATURE_ARITHMETIC_BIT;
|
||||
backendPtr->ApplyVulkanCapabilitiesForTesting(caps);
|
||||
|
||||
const auto after = MG_State::pGLContext->GetCompileEnv();
|
||||
EXPECT_NE(after.get(), before.get());
|
||||
EXPECT_NE(after->fingerprint, before->fingerprint);
|
||||
EXPECT_EQ(after->backend, BackendType::DirectVulkan);
|
||||
EXPECT_EQ(after->params.SubgroupSize, 8u);
|
||||
|
||||
MG_Backend::pActiveBackendObject = Move(previousBackend);
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, KeepsOptionalGpuShaderInt64BranchForVoxyQuadDecode) {
|
||||
using namespace MobileGL;
|
||||
|
||||
@@ -936,6 +968,45 @@ TEST(DirectVulkanSanity, ReadbackUsesTheSourceFormatTexelSize) {
|
||||
EXPECT_EQ(VulkanRenderer::GetReadbackTexelSize(VK_FORMAT_R32G32B32A32_SFLOAT), 16u);
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, DefaultFramebufferQuarterTurnReadbackMapsRectAndPixels) {
|
||||
using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer;
|
||||
using MobileGL::Uint8;
|
||||
|
||||
VkOffset2D offset{};
|
||||
VkExtent2D copyExtent{};
|
||||
ASSERT_TRUE(VulkanRenderer::MapDefaultFramebufferReadbackRect(
|
||||
1, 0, 2, 1, VkExtent2D{2, 3}, VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR,
|
||||
&offset, ©Extent));
|
||||
EXPECT_EQ(offset.x, 0);
|
||||
EXPECT_EQ(offset.y, 1);
|
||||
EXPECT_EQ(copyExtent.width, 1u);
|
||||
EXPECT_EQ(copyExtent.height, 2u);
|
||||
|
||||
ASSERT_TRUE(VulkanRenderer::MapDefaultFramebufferReadbackRect(
|
||||
1, 0, 2, 1, VkExtent2D{2, 3}, VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR,
|
||||
&offset, ©Extent));
|
||||
EXPECT_EQ(offset.x, 1);
|
||||
EXPECT_EQ(offset.y, 0);
|
||||
EXPECT_EQ(copyExtent.width, 1u);
|
||||
EXPECT_EQ(copyExtent.height, 2u);
|
||||
|
||||
// Logical GL rows, bottom to top, are abc / def. The display-oriented swapchain blocks are
|
||||
// transposed in opposite directions for 90 and 270 degrees.
|
||||
const Uint8 raw90[] = {'a', 'd', 'b', 'e', 'c', 'f'};
|
||||
const Uint8 raw270[] = {'f', 'c', 'e', 'b', 'd', 'a'};
|
||||
const Uint8 expected[] = {'a', 'b', 'c', 'd', 'e', 'f'};
|
||||
Uint8 result[sizeof(expected)]{};
|
||||
|
||||
ASSERT_TRUE(VulkanRenderer::RemapDefaultFramebufferReadback(
|
||||
raw90, 3, 2, VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR, 1, result));
|
||||
EXPECT_TRUE(std::equal(std::begin(expected), std::end(expected), std::begin(result)));
|
||||
|
||||
std::fill(std::begin(result), std::end(result), 0);
|
||||
ASSERT_TRUE(VulkanRenderer::RemapDefaultFramebufferReadback(
|
||||
raw270, 3, 2, VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR, 1, result));
|
||||
EXPECT_TRUE(std::equal(std::begin(expected), std::end(expected), std::begin(result)));
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, ReadbackConvertsRgba8AndRgba16fPixels) {
|
||||
using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer;
|
||||
using MobileGL::MG_Util::EncodeFloatToHalfBits;
|
||||
|
||||
@@ -154,7 +154,6 @@ class DemoteFloat64Test : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
MobileGL::Initialize();
|
||||
ShaderCompiler::SetSpirvValidationEnabled(true);
|
||||
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
|
||||
}
|
||||
|
||||
@@ -175,7 +174,7 @@ TEST_F(DemoteFloat64Test, DemotesEveryWidthAndDropsTheCapability) {
|
||||
ASSERT_TRUE(DeclaresFloat64Capability(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
|
||||
|
||||
EXPECT_EQ(CountFloatTypesOfWidth(output, 64), 0u) << Disassemble(output);
|
||||
// And exactly one 32-bit float type survives: the merge has to happen, or spirv-val rejects
|
||||
@@ -210,7 +209,7 @@ void main() {
|
||||
<< Disassemble(input);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
|
||||
|
||||
// std140 for the demoted members: float at 4, vec2 at 8, vec3 at 16 (aligned like a vec4),
|
||||
// vec4 at 32, mat4 at 48 with a 16-byte column stride, the array at 112 with the std140
|
||||
@@ -241,7 +240,7 @@ void main() {
|
||||
EXPECT_EQ(CollectOffsetsOf(input, "Ssbo"), (Vector<Uint32>{0, 32, 64})) << Disassemble(input);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
|
||||
|
||||
// std430, so the array packs at its element size rather than being rounded to 16: float at 0,
|
||||
// vec4 at 16, float[4] at 32 with a 4-byte stride. A storage block must NOT come out std140,
|
||||
@@ -273,7 +272,7 @@ void main() {
|
||||
ASSERT_FALSE(before.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
|
||||
|
||||
// Only the block that actually narrowed is re-laid-out. Touching the other one would be
|
||||
// churn at best, and a disagreement with glslang's own layout at worst.
|
||||
@@ -287,7 +286,7 @@ TEST_F(DemoteFloat64Test, FoldsTheConversionsThatBecameIdentities) {
|
||||
ASSERT_GT(CountFConverts(input), 0u) << "the fixture no longer converts between the two widths";
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
|
||||
|
||||
// SPIR-V requires the two component widths of an OpFConvert to differ, so every one of them
|
||||
// has to be gone: both sides are 32 bits now.
|
||||
@@ -307,7 +306,7 @@ void main() {
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
|
||||
|
||||
// A 64-bit literal is two words wide and a 32-bit one is a single word, so a constant left
|
||||
// unconverted is not merely imprecise - it is an unparseable instruction. Disassembling both
|
||||
@@ -326,7 +325,7 @@ void main() { gl_Position = inPos; }
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
|
||||
// The pass reports SuccessWithoutChange here, and SPIRV-Tools asserts (in assert-enabled
|
||||
// builds) that such a run round-trips byte-identically.
|
||||
EXPECT_EQ(output, input);
|
||||
@@ -351,7 +350,7 @@ void main() {
|
||||
ASSERT_EQ(CountFloatTypesOfWidth(input, 64), 1u) << Disassemble(input);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(input, output, true));
|
||||
EXPECT_EQ(output, input) << Disassemble(output);
|
||||
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresFloat64(output));
|
||||
}
|
||||
@@ -362,7 +361,7 @@ TEST_F(DemoteFloat64Test, ModuleDeclaresFloat64AnswersBothWays) {
|
||||
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresFloat64(wide));
|
||||
|
||||
Vector<Uint32> demoted;
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(wide, demoted));
|
||||
ASSERT_TRUE(ShaderCompiler::DemoteFloat64ToFloat32(wide, demoted, true));
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64(demoted));
|
||||
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64({}));
|
||||
@@ -375,7 +374,7 @@ TEST_F(DemoteFloat64Test, TheSharedChainDemotesToo) {
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true));
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64(output)) << Disassemble(output);
|
||||
}
|
||||
|
||||
@@ -459,7 +458,7 @@ TEST_P(DemoteFloat64EsslTest, TheDemotedModuleCanBeEmittedAsEssl) {
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output));
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true));
|
||||
|
||||
SpvcSession session(output, SessionUsageBit::Transpile);
|
||||
spvc_compiler_options options;
|
||||
@@ -482,7 +481,7 @@ TEST_P(DemoteFloat64EsslTest, TheDemotedModuleCanBeEmittedAsEssl) {
|
||||
TEST_F(DemoteFloat64Test, RejectsGarbageInput) {
|
||||
const Vector<Uint32> notSpirv{0xdeadbeefu, 0u, 0u, 0u, 0u};
|
||||
Vector<Uint32> output;
|
||||
EXPECT_FALSE(ShaderCompiler::DemoteFloat64ToFloat32(notSpirv, output));
|
||||
EXPECT_FALSE(ShaderCompiler::DemoteFloat64ToFloat32(notSpirv, output, true));
|
||||
}
|
||||
|
||||
// EliminateFloatEqualsZeroPass turns a comparison against 0.0 into an epsilon test, a
|
||||
@@ -502,7 +501,7 @@ namespace {
|
||||
EXPECT_FALSE(input.empty());
|
||||
if (input.empty()) return false;
|
||||
Vector<Uint32> output;
|
||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output));
|
||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true));
|
||||
return Disassemble(output).find("FAbs") != String::npos;
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +98,6 @@ class FlattenXfbInterfaceBlocksTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
MobileGL::Initialize();
|
||||
ShaderCompiler::SetSpirvValidationEnabled(true);
|
||||
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
|
||||
}
|
||||
|
||||
@@ -116,7 +115,7 @@ TEST_F(FlattenXfbInterfaceBlocksTest, FlattensACapturedBlockIntoOneVariablePerMe
|
||||
|
||||
std::set<String> flattened;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output));
|
||||
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_EQ(flattened, (std::set<String>{"StageData"}));
|
||||
|
||||
@@ -145,7 +144,7 @@ TEST_F(FlattenXfbInterfaceBlocksTest, TheEmittedDeclarationIsAPlainArrayNotABloc
|
||||
|
||||
std::set<String> flattened;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output));
|
||||
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output, true));
|
||||
|
||||
const String after = Transpile(output);
|
||||
EXPECT_NE(after.find("StageData_attrib[16]"), String::npos) << after;
|
||||
@@ -162,7 +161,7 @@ TEST_F(FlattenXfbInterfaceBlocksTest, GivesEachMemberItsOwnConsecutiveLocations)
|
||||
|
||||
std::set<String> flattened;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output));
|
||||
ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
|
||||
const String dis = Disassemble(output);
|
||||
@@ -184,7 +183,7 @@ TEST_F(FlattenXfbInterfaceBlocksTest, LeavesABlockNoCaptureNamesAlone) {
|
||||
std::set<String> flattened;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(
|
||||
ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"SomeOtherBlock"}, flattened, output));
|
||||
ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"SomeOtherBlock"}, flattened, output, true));
|
||||
EXPECT_TRUE(flattened.empty());
|
||||
|
||||
const String after = Transpile(output);
|
||||
@@ -200,7 +199,7 @@ TEST_F(FlattenXfbInterfaceBlocksTest, DeclinesAnEmptyRequestWithoutRewriting) {
|
||||
|
||||
std::set<String> flattened;
|
||||
Vector<Uint32> output;
|
||||
EXPECT_FALSE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {}, flattened, output));
|
||||
EXPECT_FALSE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {}, flattened, output, true));
|
||||
EXPECT_TRUE(flattened.empty());
|
||||
EXPECT_TRUE(output.empty());
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user