Compare commits

..
Author SHA1 Message Date
BZLZHH 24dfbb41f9 [Fix] (Diligent): fix y inversion and front-face winding in viewport mapping
- Translate OpenGL bottom-left viewport/scissor rects to Diligent top-left origin
- Invert FrontCounterClockwise to compensate for the Y conversion
- Add DrawsTopHalfTriangleFromMobileGLState y-orientation test
2026-08-23 12:25:41 +08:00
BZLZHH ca7878bf3a [Feat] (AndroidPlugin): expose DiligentVulkan backend in plugin selector 2026-08-23 11:11:20 +08:00
BZLZHH e418063b08 [Feat] (Diligent, EGL): forward eglSwapInterval to swap chain Present
- Renderer stores the requested swap interval and passes it to ISwapChain::Present
- SetSwapInterval now updates the active renderer instead of being a no-op
2026-08-23 11:01:40 +08:00
BZLZHH b43ec25bd7 [Feat] (Diligent, EGL): validate and defer EGL surface activation
- CreateEGLWindowSurface/CreateEGLPbufferSurface register surfaces only; activation happens on eglMakeCurrent
- Reject unsupported native window backends
2026-08-23 10:57:09 +08:00
BZLZHH 525607bad6 [Feat] (Diligent, EGL): release swap chain on EGL surface release 2026-08-23 10:55:40 +08:00
BZLZHH b045024b6c [Feat] (Diligent, EGL): route EGL window resize to Diligent swap chain
- Add renderer->ResizeSwapChain and override BackendObject_Diligent::ResizeEGLWindowSurface
- Update handoff; 16 Diligent tests pass
2026-08-23 10:53:19 +08:00
BZLZHH 14dfbeeed9 [Feat] (Diligent, EGL): wire EGL window swapchain creation via Diligent ISwapChain
- Add CreateSwapChain to renderer using IEngineFactoryVk::CreateSwapChainVk
- InitWindowSurface creates the swapchain for native window surfaces
- Present now presents the active swapchain; ReleaseEGLResources releases it
- InitPbufferSurface keeps offscreen target for pbuffer EGL surfaces
- Update handoff; 16 Diligent tests pass
2026-08-23 10:46:06 +08:00
BZLZHH d7e79409b3 [Feat] (Diligent): wire SetSwapInterval no-op
- GlobalBackendFunctionsTable.SetSwapInterval is now present; offscreen renderer ignores it
- Update handoff; 16 Diligent tests pass
2026-08-23 10:16:56 +08:00
BZLZHH ca3b524396 [Feat] (Diligent): wire CPU timer query fallback
- Add steady_clock based BeginTimeElapsedQuery/EndTimeElapsedQuery/QueryCounterTimestamp
- Wire IsTimerQuerySupported/IsQueryResultAvailable/GetQueryResult64/DeleteBackendQuery
- Update handoff; 16 Diligent tests pass
2026-08-23 10:15:19 +08:00
BZLZHH 071c8eb673 [Feat] (Diligent): wire fence sync CPU fallback
- Provide always-signaled FenceSync/ClientWaitSync/WaitSync/DeleteSync/GetSyncStatus
- Update handoff; 16 Diligent tests pass
2026-08-23 10:12:58 +08:00
BZLZHH 51a43518ac [Feat] (Diligent, MG_Impl): wire BlitNamedFramebuffer color copy
- Explicit read/draw FBO color attachments resolve to Diligent textures/renderbuffers
- CopyTexture between them for same-size color blits
- Update handoff; 16 Diligent tests pass
2026-08-23 10:11:17 +08:00
BZLZHH 34ff95f6f5 [Feat] (Diligent, MG_Impl): wire GenerateMipmap via Diligent GPU mip generation
- Create state textures with MISC_TEXTURE_FLAG_GENERATE_MIPS
- GenerateMipmap resolves active GL_TEXTURE_2D and calls IDeviceContext::GenerateMips
- Update handoff; 16 Diligent tests pass
2026-08-23 10:08:54 +08:00
BZLZHH b9d1504cc5 [Feat] (Diligent, MG_Impl): wire CopyImageSubData whole-texture copy
- CopyImageSubData syncs both texture objects and issues a Diligent CopyTexture
- Update handoff; 16 Diligent tests pass
2026-08-23 10:05:50 +08:00
BZLZHH a223499143 [Feat] (Diligent, MG_Impl): wire ClearBufferfi and stencil clears through ClearBufferiv/uiv
- ClearBufferfi clears depth+stencil on the current draw framebuffer
- ClearBufferiv/uiv now support GL_STENCIL via ClearStencil
- Update handoff; 16 Diligent tests pass
2026-08-23 10:04:14 +08:00
BZLZHH 5dca617f01 [Feat] (Diligent, MG_Impl): honor DrawElementsBaseVertex baseVertex in CPU vertex packing
- Add baseVertex parameter through DrawFromState/UploadVertexDataFromState
- Apply baseVertex when resolving indexed vertex indices
- Pass baseVertex through DrawElementsBaseVertex, instanced, and indirect indexed draws
- Add DrawsIndexedBaseVertexFromMobileGLState; 16 Diligent tests pass
2026-08-23 10:02:44 +08:00
BZLZHH eb8ef893be [Feat] (Diligent): support multiple simultaneous color attachments
- PSO RTV count/formats now derive from the bound draw FBO color attachments
- Include RT layout in the last-PSO cache key
- Add DrawsToMultipleColorAttachmentsFromMobileGLState; 15 Diligent tests pass
2026-08-23 09:56:58 +08:00
BZLZHH 20567fba6d [Feat] (Diligent, MG_State): cache renderbuffer resources and support color readback
- SyncRenderbuffer now reuses the cached Diligent texture so Clear/Draw/ReadPixels target the same resource
- ReadPixels resolves renderbuffer color attachments from the read FBO
- Add DrawsToRenderbufferFramebufferFromMobileGLState; 14 Diligent tests pass
2026-08-23 09:54:08 +08:00
BZLZHH e3f44e8da1 [Feat] (Diligent, MG_Impl): add indirect draw CPU fallbacks
- Wire DrawArraysIndirect/DrawElementsIndirect
- Wire MultiDraw*Indirect and *IndirectCount using client memory or GL_DRAW_INDIRECT_BUFFER/GL_PARAMETER_BUFFER CPU reads
- Update handoff; 13 Diligent tests pass
2026-08-23 09:45:34 +08:00
BZLZHH 9f79a88af5 [Feat] (Diligent, MG_Impl): wire GetTexImage/GetTextureImage RGBA8 readback
- Copy texture to staging and map rows for GL_RGBA/GL_UNSIGNED_BYTE
- Wire both GLFunctionsTable entries; 13 Diligent tests pass
2026-08-23 09:41:17 +08:00
BZLZHH 6bf32acdef [Feat] (Diligent, MG_Impl): wire CopyTexImage2D/CopyTexSubImage2D readback copy
- Copy current read-FBO color attachment into the bound GL_TEXTURE_2D
- Uses whole-color CopyTexture fallback for now; 13 Diligent tests pass
2026-08-23 09:39:40 +08:00
BZLZHH 23b53eacce [Feat, Test] (Diligent, MG_Test): add stencil clear and stencil state test
- Use D24S8 for the default offscreen depth/stencil target
- Wire GL_STENCIL_BUFFER_BIT Clear through renderer->ClearStencil
- Add DrawsWithStencilTestFromMobileGLState; 13 Diligent tests pass
2026-08-23 09:37:49 +08:00
BZLZHH e829e70d8b [Feat] (Diligent, MG_State): bind named application uniform blocks from frontend buffers
- Resolve named UBOs through SPIRV-Reflect type names when block names are empty
- Read the bound GL buffer range at the frontend uniform-block binding point and upload it as a Diligent uniform buffer
- Add DrawsNamedUniformBlockFromMobileGLState test; 12 Diligent tests pass
2026-08-23 09:35:32 +08:00
BZLZHH 7e765e1535 [Test] (Diligent, MG_Test): add depth test and ensure default framebuffer isolation
- Verify nearer depth draw occludes farther draw
- Bind default framebuffer at test start so previous FBO state cannot leak
- Update handoff to 11 passing Diligent tests
2026-08-23 09:23:06 +08:00
BZLZHH bf6061811f [Test] (Diligent, MG_Test): add scissor and blend state tests
- Verify scissor clipping leaves outside pixels untouched
- Verify alpha blend combines source/destination colors
- Update handoff to 10 passing Diligent tests
2026-08-23 09:17:41 +08:00
BZLZHH 87750c3b21 [Feat] (Diligent): add instanced/clear-buffer/blit GL entry points
- Wire DrawElementsBaseVertex, instanced draw family, MultiDrawElementsBaseVertex
- Wire ClearBufferfv/iv/uiv to the Diligent clear path
- Add same-size color BlitFramebuffer between current read/draw framebuffers
- Update handoff with newly implemented GL 3.2 entry points
2026-08-23 09:15:21 +08:00
BZLZHH 45b309db37 [Feat] (Diligent, MG_Impl): wire GL ReadPixels entry to offscreen/user-FBO readback 2026-08-23 09:11:56 +08:00
BZLZHH 403c82ac4a [Feat, Docs] (Diligent): cache last PSO and document framebuffer/UBO progress
- Reuse the last state PSO when program/render-state/topology/VAO layout is unchanged
- Update handoff with completed texture/sampler, UBO, framebuffer, and multi-draw work
2026-08-23 09:10:53 +08:00
BZLZHH 827d46cad3 [Feat] (Diligent, MG_State): wire textures, samplers, global UBO, and user framebuffers
- Auto-sync ITextureObject to Diligent textures with dirty-level uploads
- Translate SamplerObject/unit sampler state into Diligent samplers
- Bind the synthesized MGL_GLOBAL_UBO for default-block glUniform data
- Resolve bound draw/read framebuffers to Diligent RTV/DSV for draws/clears/readback
- Wire DrawRangeElements, DrawRangeElementsBaseVertex, MultiDrawArrays, MultiDrawElements
- Add real-texture, uniform, and user-framebuffer tests; 8 Diligent tests pass
2026-08-23 09:09:03 +08:00
BZLZHH 1748da0443 [Docs] (Diligent): add Diligent GL3.2 backend handoff document 2026-08-23 08:36:57 +08:00
BZLZHH 7efee8e3e6 [Test] (Diligent, MG_Test): verify indexed DrawElements path from real frontend state
Add DrawsIndexedFromMobileGLState: creates a GL program, VBO, EBO and VAO
through the frontend, then draws via DrawFromState(GL_TRIANGLES, ...,
GL_UNSIGNED_INT, nullptr) and verifies the offscreen center is red.

All 5 Diligent local tests pass.
2026-08-18 14:22:20 +08:00
BZLZHH 08ca897a07 [Feat, Test] (Diligent, MG_Test): add basic texture binding and textured state-draw test
- Add CreateTestTexture(): creates an RGBA8 texture, SRV and default sampler,
  and attaches the sampler to the SRV.
- State PSOs now bind a static pixel-shader variable 'g_Texture' to the test
  texture and commit shader resources before drawing.
- Add DrawsTexturedFromMobileGLState test using a real GL program with
  sampler2D and interleaved position+UV attributes.
- All 4 Diligent local tests pass on Turnip Adreno 750.
2026-08-18 14:20:02 +08:00
BZLZHH 98f2a55214 [Feat] (Diligent, MG_Impl): clear depth in GL Clear when GL_DEPTH_BUFFER_BIT set 2026-08-18 13:38:44 +08:00
BZLZHH cedc257566 [Feat] (Diligent): add offscreen depth target and depth clear
The renderer now creates a D32_FLOAT depth target and binds it as DSV for all
render passes, so depth-test state wired earlier can actually work. Add
ClearDepth for depth clears. All Diligent local tests still pass.
2026-08-18 13:34:37 +08:00
BZLZHH 821c0e0d4e [Feat] (Diligent): wire stencil and color-mask state into state PSO
CreatePipelineFromState now applies GL_STENCIL_TEST state (front/back funcs,
ops, read/write masks) and GL color write mask. DrawFromState sets the stencil
reference before drawing. All Diligent local tests still pass.
2026-08-18 13:31:13 +08:00
BZLZHH bd9680ad67 [Feat] (Diligent): wire viewport/scissor state into state draws
DrawFromState now uses MG_State viewport (with full-target fallback when the
viewport is uninitialized) and applies the scissor test rect when enabled.
The state-driven test sets an explicit glViewport and passes again.
2026-08-18 13:26:20 +08:00
BZLZHH 6375e07030 [Feat] (Diligent): wire blend/depth/cull render state into state PSO
CreatePipelineFromState now reads MG_State render state:
- GL_BLEND enable, blend factors/equations
- GL_DEPTH_TEST enable, depth func, depth write mask
- GL_CULL_FACE enable, cull face mode, front-face winding

All Diligent local tests still pass.
2026-08-18 13:16:55 +08:00
BZLZHH 53cac39d4e [Test] (Diligent, MG_Test): verify state-driven draw with real MobileGL frontend state
Add DrawsFromMobileGLState test that creates a GL 3.2 program, buffer and VAO
through the real frontend, then draws through DiligentRenderer::DrawFromState
and verifies the offscreen pixels.

Also release PSO/vertex buffer before recreation to avoid Diligent debug
assertions about overwriting references.

All 3 Diligent local tests pass on Turnip Adreno 750.
2026-08-18 13:13:10 +08:00
BZLZHH f6b1ea635b [Feat] (Diligent, MG_State): add state-driven draw path (VAO/buffer/program to Diligent)
DiligentRenderer now has DrawFromState() that:
- reads the current MobileGL program SPIR-V and creates Diligent shaders
- reads the current VAO enabled attributes and packs bound buffer data into
  an interleaved vertex buffer
- creates a PSO with the matching input layout and primitive topology
- supports DrawArrays, DrawElements, triangle-fan and line-loop expansion

This is the first real front-end state wiring; it compiles and is used by
GLFunctionsTable DrawArrays/DrawElements, but is not yet covered by a
runtime state-driven test.
2026-08-18 13:08:23 +08:00
BZLZHH 8b2711e32a [Feat] (Diligent): add dynamic vertex buffer upload path
DiligentRenderer can now upload arbitrary vec2 vertex data into a dynamic
vertex buffer and draw it with the existing triangle PSO. The local sanity
test uses this path instead of the hardcoded triangle, verifying buffer
basics on Turnip Adreno 750.
2026-08-18 13:01:29 +08:00
BZLZHH 9776cc8047 [Feat] (Diligent, MG_Backend): wire Clear/Draw/Present into GLFunctionsTable
BackendObject_Diligent now exposes real function-table entries backed by the
DiligentRenderer: Clear reads the current GL clear color from MG_State, and
DrawArrays/DrawElements currently render the built-in triangle as a
placeholder until buffer/VAO/program state is connected. Present flushes the
immediate context.

Local Diligent tests still pass on Turnip Adreno 750.
2026-08-18 12:57:06 +08:00
BZLZHH 3b0591e0ba [Feat] (Diligent): add real offscreen renderer with clear and triangle draw
- Add DiligentRenderer: creates an offscreen RGBA8 render target, compiles a
  GLSL vertex/pixel shader through Diligent's glslang path, creates a triangle
  vertex buffer and pipeline, and supports clear/draw/readback.
- BackendObject_Diligent now owns a DiligentRenderer after device creation.
- Extend local sanity test to clear green, draw a red triangle, and verify
  center is red and corner stays green.
- All Diligent local tests pass on Turnip Adreno 750.
2026-08-18 12:54:10 +08:00
BZLZHH e62f158c22 [Feat] (Diligent, CMake): enable DiligentCore and add initial Diligent/Vulkan backend skeleton
- Add MOBILEGL_ENABLE_DILIGENT option; build DiligentCore Vulkan-only after
  MobileGL's existing 3rdparty targets so shared glslang/SPIRV-Cross/xxHash
  targets are reused instead of duplicated.
- Add BackendType::DiligentVulkan, config parsing, and backend-object switch.
- Add DiligentBackend::BackendObject_Diligent skeleton: creates a Diligent
  Vulkan device/context when an adapter is available, advertises GL 3.2 core,
  and returns an empty GL function table for now.
- Add local DiligentVulkanSanityTest that compiles/runs on the host (skips
  device creation gracefully when no Vulkan adapter is present).
- Fix GLXImpl EGLDisplay member shadowing the X11 Display typedef, exposed by
  GCC 16 + Diligent header include order.
2026-08-18 12:42:17 +08:00
55 changed files with 4830 additions and 3742 deletions
-3
View File
@@ -435,9 +435,6 @@ jobs:
if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then
extra_retrace_args+=(--coherent-as-flush) extra_retrace_args+=(--coherent-as-flush)
fi fi
if [ "${{ matrix.case.num_subgroups_quirk || false }}" = "true" ]; then
extra_retrace_args+=(--num-subgroups-quirk)
fi
run_retrace() { run_retrace() {
timeout "$(( ${{ matrix.case.timeout_seconds }} + 300 ))" sh android-plugin/trace-replay-ci.sh \ timeout "$(( ${{ matrix.case.timeout_seconds }} + 300 ))" sh android-plugin/trace-replay-ci.sh \
+4 -1
View File
@@ -1,4 +1,4 @@
################################################################################ ################################################################################
# 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。 # 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。
################################################################################ ################################################################################
@@ -16,6 +16,9 @@ MobileGLCodeManager
MobileGL/MG_Test/build MobileGL/MG_Test/build
/build_* /build_*
/cmake-build* /cmake-build*
/build-*/
/local.properties
/.jspace/
.idea .idea
MobileGL/MG*/build* MobileGL/MG*/build*
MobileGL/MG*/cmake-build* MobileGL/MG*/cmake-build*
+42 -3
View File
@@ -199,7 +199,6 @@ set(SPIRV_REFLECT_ENABLE_ASSERTS OFF CACHE BOOL "Enable asserts for debugging"
set(SPIRV_REFLECT_ENABLE_ASAN OFF CACHE BOOL "Use address sanitization" FORCE) set(SPIRV_REFLECT_ENABLE_ASAN OFF CACHE BOOL "Use address sanitization" FORCE)
set(SPIRV_REFLECT_INSTALL OFF CACHE BOOL "Whether to install" FORCE) set(SPIRV_REFLECT_INSTALL OFF CACHE BOOL "Whether to install" FORCE)
# add_subdirectory(3rdparty/DiligentCore)
add_subdirectory(3rdparty/glslang) add_subdirectory(3rdparty/glslang)
add_subdirectory(3rdparty/SPIRV-Cross) add_subdirectory(3rdparty/SPIRV-Cross)
add_subdirectory(3rdparty/VulkanMemoryAllocator) add_subdirectory(3rdparty/VulkanMemoryAllocator)
@@ -211,6 +210,23 @@ set(XXHASH_BUILD_XXHSUM OFF)
option(BUILD_SHARED_LIBS OFF) option(BUILD_SHARED_LIBS OFF)
add_subdirectory(3rdparty/xxHash/build/cmake xxhash_build EXCLUDE_FROM_ALL) add_subdirectory(3rdparty/xxHash/build/cmake xxhash_build EXCLUDE_FROM_ALL)
# Diligent-based backend. Enabled by default on local builds; only the Vulkan
# engine from DiligentCore is built. Added after the other 3rdparty projects so
# DiligentCore reuses the glslang / SPIRV-Cross / SPIRV-Tools / xxHash targets
# already defined by MobileGL instead of building its bundled copies.
option(MOBILEGL_ENABLE_DILIGENT "Enable the Diligent/Vulkan backend" ON)
if(MOBILEGL_ENABLE_DILIGENT)
set(DILIGENT_NO_DIRECT3D11 ON CACHE BOOL "Disable Direct3D11 backend" FORCE)
set(DILIGENT_NO_DIRECT3D12 ON CACHE BOOL "Disable Direct3D12 backend" FORCE)
set(DILIGENT_NO_OPENGL ON CACHE BOOL "Disable OpenGL backend" FORCE)
set(DILIGENT_NO_METAL ON CACHE BOOL "Disable Metal backend" FORCE)
set(DILIGENT_NO_WEBGPU ON CACHE BOOL "Disable WebGPU backend" FORCE)
set(DILIGENT_NO_ARCHIVER ON CACHE BOOL "Disable Archiver" FORCE)
set(DILIGENT_BUILD_TESTS OFF CACHE BOOL "Build Diligent tests" FORCE)
set(DILIGENT_INSTALL_CORE OFF CACHE BOOL "Install DiligentCore" FORCE)
add_subdirectory(3rdparty/DiligentCore)
endif()
set(TRACY_ENABLE ${MOBILEGL_ENABLE_TRACY} CACHE BOOL "Enable Tracy, this is an internal variable" FORCE) set(TRACY_ENABLE ${MOBILEGL_ENABLE_TRACY} CACHE BOOL "Enable Tracy, this is an internal variable" FORCE)
if (TRACY_ENABLE) if (TRACY_ENABLE)
@@ -284,7 +300,6 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
@@ -299,7 +314,6 @@ set(SOURCE_FILES
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
MobileGL/MG_Util/SelfTest/DriverPost.cpp MobileGL/MG_Util/SelfTest/DriverPost.cpp
MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.cpp
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
@@ -398,6 +412,14 @@ set(SOURCE_FILES
MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp
) )
if(MOBILEGL_ENABLE_DILIGENT)
list(APPEND SOURCE_FILES
MobileGL/MG_Backend/Diligent/BackendObject_Diligent.cpp
MobileGL/MG_Backend/Diligent/DiligentVulkan.cpp
MobileGL/MG_Backend/Diligent/Renderer/DiligentRenderer.cpp
)
endif()
if (APPLE AND NOT MOBILEGL_IOS) if (APPLE AND NOT MOBILEGL_IOS)
list(APPEND SOURCE_FILES list(APPEND SOURCE_FILES
MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp
@@ -438,6 +460,21 @@ set(MOBILEGL_LINK_LIBRARIES
Threads::Threads Threads::Threads
) )
if(MOBILEGL_ENABLE_DILIGENT)
list(APPEND MOBILEGL_LINK_LIBRARIES
Diligent-GraphicsEngineVk-static
Diligent-GraphicsEngine
Diligent-GraphicsEngineNextGenBase
Diligent-GraphicsAccessories
Diligent-ShaderTools
Diligent-GraphicsTools
Diligent-Common
Diligent-Primitives
Diligent-TargetPlatform
Vulkan::Headers
)
endif()
set(MOBILEGL_COMPILE_DEF set(MOBILEGL_COMPILE_DEF
-DVMA_STATIC_VULKAN_FUNCTIONS=0 -DVMA_STATIC_VULKAN_FUNCTIONS=0
-DVMA_DYNAMIC_VULKAN_FUNCTIONS=1 -DVMA_DYNAMIC_VULKAN_FUNCTIONS=1
@@ -503,6 +540,7 @@ target_compile_definitions(${CMAKE_PROJECT_NAME}
${MOBILEGL_COMPILE_DEF} ${MOBILEGL_COMPILE_DEF}
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL} MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
$<$<BOOL:${MOBILEGL_TRACE_ANGLE_VARIANTS}>:MOBILEGL_TRACE_ANGLE_VARIANTS=1> $<$<BOOL:${MOBILEGL_TRACE_ANGLE_VARIANTS}>:MOBILEGL_TRACE_ANGLE_VARIANTS=1>
$<$<BOOL:${MOBILEGL_ENABLE_DILIGENT}>:MOBILEGL_ENABLE_DILIGENT=1>
) )
if(UNIX AND NOT APPLE AND NOT ANDROID) if(UNIX AND NOT APPLE AND NOT ANDROID)
@@ -561,6 +599,7 @@ if(NOT ANDROID)
PUBLIC PUBLIC
${MOBILEGL_COMPILE_DEF} ${MOBILEGL_COMPILE_DEF}
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL} MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
$<$<BOOL:${MOBILEGL_ENABLE_DILIGENT}>:MOBILEGL_ENABLE_DILIGENT=1>
) )
endif() endif()
+278
View File
@@ -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.
-5
View File
@@ -80,11 +80,6 @@ namespace MobileGL::MG_Config {
#endif #endif
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support. // MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support.
Bool DisableSubgroup = false; Bool DisableSubgroup = false;
// MOBILEGL_NUM_SUBGROUPS_QUIRK: derive compute gl_NumSubgroups from the local
// workgroup dimensions and gl_SubgroupSize instead of reading Vulkan's
// NumSubgroups builtin. Off by default; enable only for drivers whose builtin
// disagrees with the SubgroupId topology emitted by the same dispatch.
Bool NumSubgroupsQuirk = false;
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension // MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any // string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension // module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
+1 -1
View File
@@ -168,7 +168,6 @@ namespace MobileGL::MG_ConfigLoader {
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, ""); QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
#endif #endif
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP"); features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
features.NumSubgroupsQuirk = QueryEnvFlag("MOBILEGL_NUM_SUBGROUPS_QUIRK");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64"); features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK"); features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64); features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
@@ -203,6 +202,7 @@ namespace MobileGL::MG_ConfigLoader {
} }
ENTRY(DirectGLES) ENTRY(DirectGLES)
ENTRY(DirectVulkan) ENTRY(DirectVulkan)
ENTRY(DiligentVulkan)
ENTRY(Unknown) ENTRY(Unknown)
MG_Config::ActiveBackendType = BackendType::Unknown; MG_Config::ActiveBackendType = BackendType::Unknown;
#undef ENTRY #undef ENTRY
+1
View File
@@ -19,6 +19,7 @@ namespace MobileGL {
enum class BackendType { enum class BackendType {
DirectGLES, DirectGLES,
DirectVulkan, DirectVulkan,
DiligentVulkan,
BackendTypeCount, BackendTypeCount,
Unknown = -1 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
@@ -8,7 +8,6 @@
#include "ProgramFactory.h" #include "ProgramFactory.h"
#include "Config.h"
#include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h" #include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h"
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h" #include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
#include "MG_Util/ShaderTranspiler/SpvcSession.h" #include "MG_Util/ShaderTranspiler/SpvcSession.h"
@@ -3164,23 +3163,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
// NumSubgroups is defined by the local workgroup dimensions and SubgroupSize. Derive
// it in SPIR-V instead of trusting a driver builtin that can disagree with the
// SubgroupId topology produced by the same compute dispatch (Adreno reports 1 while
// emitting IDs 0..7 for a 512-invocation, 64-wide workgroup).
if (MG_Config::Features.NumSubgroupsQuirk && shaders[i] &&
shaders[i]->GetShaderStage() == ShaderStage::Compute) {
Vector<Uint> derivedNumSubgroupsSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::DeriveNumSubgroupsForVulkan(
moduleSpirvs[i], derivedNumSubgroupsSpirv, enableSpirvValidation)) {
moduleSpirvs[i] = std::move(derivedNumSubgroupsSpirv);
} else {
MGLOG_E("ProgramFactory: failed to derive gl_NumSubgroups for program %u; "
"compute shaders may observe a driver-inconsistent subgroup count",
program.GetExternalIndex());
}
}
// Vulkan's SPIR-V environment has no rectangle image dimension, so a // Vulkan's SPIR-V environment has no rectangle image dimension, so a
// GL_TEXTURE_RECTANGLE lookup has to become the 2D one the texture is really // GL_TEXTURE_RECTANGLE lookup has to become the 2D one the texture is really
// stored as - which addresses [0,1] where the application addressed texels. // stored as - which addresses [0,1] where the application addressed texels.
@@ -542,14 +542,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
outImageInfo = { outImageInfo = {
.sampler = m_samplerManager->GetOrCreateSampler(*samplerBindingOverride.sampler, .sampler = m_samplerManager->GetOrCreateSampler(*samplerBindingOverride.sampler,
*samplerBindingOverride.texture, *samplerBindingOverride.texture),
samplerBindingOverride.forceNearestFiltering,
resource->sampledLevelCount),
.imageView = samplerBindingOverride.imageView != VK_NULL_HANDLE ? .imageView = samplerBindingOverride.imageView != VK_NULL_HANDLE ?
samplerBindingOverride.imageView : samplerBindingOverride.imageView :
(resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView), (resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView),
.imageLayout = samplerBindingOverride.imageLayout != VK_IMAGE_LAYOUT_UNDEFINED ? .imageLayout = resource->layout,
samplerBindingOverride.imageLayout : resource->layout,
}; };
return outImageInfo.sampler != VK_NULL_HANDLE; return outImageInfo.sampler != VK_NULL_HANDLE;
} }
@@ -1272,87 +1269,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true; return true;
} }
Bool UniformManager::SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
GLint imageLevel, GLenum imageAccess) {
return imageAccess != GL_READ_ONLY && imageLevel >= samplerBaseLevel && imageLevel <= samplerMaxLevel;
}
Bool UniformManager::CollectSamplerImageFeedback(
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<SamplerImageFeedbackBinding>& outBindings) const {
outBindings.clear();
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr,
"CollectSamplerImageFeedback: GL context is null");
if (programObj.declinedDescriptors) return true;
for (const Uint32 samplerBinding : programObj.activeBindings) {
if (samplerBinding >= m_maxBindings ||
programObj.bindingKinds[samplerBinding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
continue;
}
const Uint32 samplerCount = BindingDescriptorCount(programObj, samplerBinding);
for (Uint32 samplerElement = 0; samplerElement < samplerCount; ++samplerElement) {
MG_State::GLState::ITextureObject* sampledTexture = nullptr;
const MG_State::GLState::SamplerObject* sampledSampler = nullptr;
if (!ResolveSampledBinding(program, programObj, samplerBinding, samplerElement,
sampledTexture, sampledSampler) ||
sampledTexture == nullptr || sampledSampler == nullptr ||
MG_State::GLState::SamplesAsIncompleteTexture(sampledTexture, sampledSampler)) {
// ResolveSamplerDescriptor uses a fallback in these cases, which cannot
// alias the image-unit binding of the original texture.
continue;
}
// Multisample source images intentionally omit TRANSFER_SRC usage. Keep their existing
// direct binding instead of turning otherwise valid sampler2DMS/image2DMS dispatches
// into failed dispatches; a correct snapshot for them needs a same-sample-count path.
const TextureTarget sampledTarget = sampledTexture->GetTarget();
if (sampledTarget == TextureTarget::Texture2DMultisample ||
sampledTarget == TextureTarget::Texture2DMultisampleArray) {
continue;
}
const auto& levelRange = sampledTexture->GetLevelRange();
Bool aliasesWritableImage = false;
for (const Uint32 imageBinding : programObj.activeBindings) {
if (imageBinding >= m_maxBindings ||
programObj.bindingKinds[imageBinding] != ProgramFactory::DescriptorBindingKind::StorageImage) {
continue;
}
if (imageBinding >= programObj.samplerUniformLocationByBinding.size()) return false;
const Int baseLocation = programObj.samplerUniformLocationByBinding[imageBinding];
if (baseLocation < 0) return false;
const Uint32 imageCount = BindingDescriptorCount(programObj, imageBinding);
for (Uint32 imageElement = 0; imageElement < imageCount; ++imageElement) {
const Int location = ResolveDescriptorElementLocation(program, baseLocation, imageElement);
if (location < 0) return false;
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
return false;
}
const auto& image = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
// A sampler view exposes all layers of its target; equal texture plus an
// overlapping mip therefore aliases the writable image subresource.
if (image.Texture.get() == sampledTexture &&
SamplerOverlapsWritableImageSubresource(levelRange.x(), levelRange.y(),
image.Level, image.Access)) {
aliasesWritableImage = true;
break;
}
}
if (aliasesWritableImage) break;
}
if (aliasesWritableImage) {
outBindings.push_back({.samplerBinding = samplerBinding,
.samplerElement = samplerElement,
.texture = sampledTexture,
.sampler = sampledSampler,
.numericDomain = programObj.samplerNumericDomainByBinding[samplerBinding]});
}
}
}
return true;
}
Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program, Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 arrayElement, UboBindResult& out) const { Uint32 arrayElement, UboBindResult& out) const {
@@ -1726,8 +1642,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 frameIndex, Uint32 frameIndex,
VkPipelineBindPoint bindPoint, VkPipelineBindPoint bindPoint,
const SamplerBindingOverride* samplerBindingOverride, const SamplerBindingOverride* samplerBindingOverride,
Bool samplerDescriptorsUnchangedHint, Bool samplerDescriptorsUnchangedHint) {
const Vector<SamplerBindingOverride>* samplerBindingOverrides) {
// This program has a descriptor MobileGL could not resolve (see // This program has a descriptor MobileGL could not resolve (see
// VkProgramObject::declinedDescriptors). Refusing here is the whole of the decline: the // VkProgramObject::declinedDescriptors). Refusing here is the whole of the decline: the
// binding is still declared in the layout, so the pipeline is consistent with the shader // binding is still declared in the layout, so the pipeline is consistent with the shader
@@ -1754,8 +1669,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// sampler binding, and an unchanged (buffer, range) for the single // sampler binding, and an unchanged (buffer, range) for the single
// dynamic UBO covers the rest - except the dynamic offset, which rebinding // dynamic UBO covers the rest - except the dynamic offset, which rebinding
// the SAME set delivers without any descriptor write. // the SAME set delivers without any descriptor write.
const Bool cacheable = samplerBindingOverride == nullptr && const Bool cacheable = (samplerBindingOverride == nullptr);
(samplerBindingOverrides == nullptr || samplerBindingOverrides->empty());
if (cacheable && samplerDescriptorsUnchangedHint && m_fastRebindMemo.valid && if (cacheable && samplerDescriptorsUnchangedHint && m_fastRebindMemo.valid &&
m_fastRebindMemo.frameIndex == frameIndex && m_fastRebindMemo.frameIndex == frameIndex &&
m_fastRebindMemo.programLifetimeId == program.GetLifetimeId() && m_fastRebindMemo.programLifetimeId == program.GetLifetimeId() &&
@@ -1979,23 +1893,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const SizeT firstImageInfoIndex = imageInfos.size(); const SizeT firstImageInfoIndex = imageInfos.size();
for (Uint32 element = 0; element < descriptorCount; ++element) { for (Uint32 element = 0; element < descriptorCount; ++element) {
VkDescriptorImageInfo imageInfo{}; VkDescriptorImageInfo imageInfo{};
const SamplerBindingOverride* overrideForElement = Bool hasImage = false;
overrideThisBinding && element == 0 ? samplerBindingOverride : nullptr; if (overrideThisBinding && element == 0) {
if (overrideForElement == nullptr && samplerBindingOverrides != nullptr) { hasImage = ResolveSamplerDescriptorOverride(*samplerBindingOverride, imageInfo);
const auto overrideIt = std::find_if( } else {
samplerBindingOverrides->begin(), samplerBindingOverrides->end(), hasImage = ResolveSamplerDescriptor(commandBuffer, program, programObj, binding, element,
[binding, element](const SamplerBindingOverride& candidate) { imageInfo, samplerDescriptorsUnchangedHint);
return candidate.binding == binding && candidate.element == element;
});
if (overrideIt != samplerBindingOverrides->end()) {
overrideForElement = &*overrideIt;
} }
}
const Bool hasImage = overrideForElement != nullptr
? ResolveSamplerDescriptorOverride(*overrideForElement, imageInfo)
: ResolveSamplerDescriptor(commandBuffer, program, programObj, binding,
element, imageInfo,
samplerDescriptorsUnchangedHint);
if (!hasImage) { if (!hasImage) {
MGLOG_E_ONCE( MGLOG_E_ONCE(
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u " "UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
@@ -26,20 +26,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
public: public:
struct SamplerBindingOverride { struct SamplerBindingOverride {
Uint32 binding = 0; Uint32 binding = 0;
Uint32 element = 0;
MG_State::GLState::ITextureObject* texture = nullptr; MG_State::GLState::ITextureObject* texture = nullptr;
const MG_State::GLState::SamplerObject* sampler = nullptr; const MG_State::GLState::SamplerObject* sampler = nullptr;
VkImageView imageView = VK_NULL_HANDLE; VkImageView imageView = VK_NULL_HANDLE;
VkImageLayout imageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
Bool forceNearestFiltering = false;
};
struct SamplerImageFeedbackBinding {
Uint32 samplerBinding = 0;
Uint32 samplerElement = 0;
MG_State::GLState::ITextureObject* texture = nullptr;
const MG_State::GLState::SamplerObject* sampler = nullptr;
SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown;
}; };
Bool Initialize(VkDevice device, VkBufferManager* bufferManager, Bool Initialize(VkDevice device, VkBufferManager* bufferManager,
@@ -90,12 +79,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program, Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures) const; Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
Bool CollectSamplerImageFeedback(
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<SamplerImageFeedbackBinding>& outBindings) const;
static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
GLint imageLevel, GLenum imageAccess);
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that // samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
// every input of every combined-image-sampler resolution is unchanged since the // every input of every combined-image-sampler resolution is unchanged since the
// previous draw's resolve - same (texture, sampler) per binding, texture params // previous draw's resolve - same (texture, sampler) per binding, texture params
@@ -108,8 +91,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 frameIndex, Uint32 frameIndex,
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
const SamplerBindingOverride* samplerBindingOverride = nullptr, const SamplerBindingOverride* samplerBindingOverride = nullptr,
Bool samplerDescriptorsUnchangedHint = false, Bool samplerDescriptorsUnchangedHint = false);
const Vector<SamplerBindingOverride>* samplerBindingOverrides = nullptr);
// Pure format-policy helper kept public for host regression tests. Formatted storage // Pure format-policy helper kept public for host regression tests. Formatted storage
// images use their shader qualifier; transformed float images use glBindImageTexture's // images use their shader qualifier; transformed float images use glBindImageTexture's
@@ -1291,158 +1291,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return ok; return ok;
} }
Bool VkTextureManager::SnapshotTextureForSampling(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture,
SamplerNumericDomain numericDomain,
VkPipelineStageFlags consumerShaderStageMask,
SampledTextureSnapshot& outSnapshot) {
outSnapshot = {};
TextureResource* source = SyncTextureAndGetDescriptor(texture);
if (source == nullptr || source->image == VK_NULL_HANDLE || source->sampleCount != VK_SAMPLE_COUNT_1_BIT ||
source->sampledLevelCount == 0) {
return false;
}
const VkFormat sampledFormat = ResolveSampledImageViewFormat(source->format, numericDomain);
if (sampledFormat == VK_FORMAT_UNDEFINED ||
!AreSampledImageViewFormatsCompatible(source->format, sampledFormat)) {
MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d cannot create sampled view format=%d from image format=%d",
texture.GetExternalIndex(), static_cast<Int>(sampledFormat), static_cast<Int>(source->format));
return false;
}
if (sampledFormat != source->format &&
(source->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d needs unavailable mutable image format=%d for sampled view=%d",
texture.GetExternalIndex(), static_cast<Int>(source->format), static_cast<Int>(sampledFormat));
return false;
}
VkImageType imageType = VK_IMAGE_TYPE_2D;
switch (source->viewType) {
case VK_IMAGE_VIEW_TYPE_1D:
case VK_IMAGE_VIEW_TYPE_1D_ARRAY:
imageType = VK_IMAGE_TYPE_1D;
break;
case VK_IMAGE_VIEW_TYPE_3D:
imageType = VK_IMAGE_TYPE_3D;
break;
default:
break;
}
TextureResource snapshot{};
VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.flags = source->imageCreateFlags;
imageInfo.imageType = imageType;
imageInfo.extent = {source->extent.width, source->extent.height, source->depth};
imageInfo.mipLevels = source->mipLevels;
imageInfo.arrayLayers = source->arrayLayers;
imageInfo.format = source->format;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
// Keep the temporary's view-format list just as narrow as the source's sampler use. This
// has no storage-image usage, so unlike an app image binding the exact list is knowable.
Vector<VkFormat> viewFormats;
VkImageFormatListCreateInfo formatListInfo{};
if (m_imageFormatListSupported && (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
viewFormats.push_back(source->format);
if (sampledFormat != source->format) {
viewFormats.push_back(sampledFormat);
}
formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
formatListInfo.viewFormatCount = static_cast<Uint32>(viewFormats.size());
formatListInfo.pViewFormats = viewFormats.data();
imageInfo.pNext = &formatListInfo;
}
VmaAllocationCreateInfo allocationInfo{};
allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
const VkResult createResult =
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &snapshot.image, &snapshot.allocation, nullptr);
if (createResult != VK_SUCCESS) {
MGLOG_E_ONCE("SnapshotTextureForSampling: vmaCreateImage failed result=%d textureId=%d", createResult,
texture.GetExternalIndex());
return false;
}
snapshot.extent = source->extent;
snapshot.depth = source->depth;
snapshot.arrayLayers = source->arrayLayers;
snapshot.mipLevels = source->mipLevels;
snapshot.sampledBaseMipLevel = source->sampledBaseMipLevel;
snapshot.sampledLevelCount = source->sampledLevelCount;
snapshot.format = source->format;
snapshot.aspect = source->aspect;
snapshot.viewType = source->viewType;
snapshot.sampleCount = VK_SAMPLE_COUNT_1_BIT;
snapshot.imageCreateFlags = imageInfo.flags;
snapshot.usageFlags = imageInfo.usage;
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
const VkImageAspectFlags sampledAspect =
ResolveSampledImageViewAspectMask(snapshot.aspect, texture.GetDepthStencilTextureMode());
snapshot.sampledView = CreateImageView(snapshot.image, sampledFormat, sampledAspect, snapshot.viewType,
snapshot.sampledBaseMipLevel, snapshot.sampledLevelCount, 0,
snapshot.arrayLayers, &sampledComponents);
if (snapshot.sampledView == VK_NULL_HANDLE) {
MGLOG_E_ONCE("SnapshotTextureForSampling: failed to create sampled view textureId=%d", texture.GetExternalIndex());
return false;
}
VkPipelineStageFlags sourceStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags sourceAccessMask = 0;
const VkImageLayout sourceLayout = source->layout;
GetImageTransitionSourceState(sourceLayout, sourceStageMask, sourceAccessMask);
if (!TransitionImageLayout(commandBuffer, source->image, source->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
sourceStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, sourceAccessMask,
VK_ACCESS_TRANSFER_READ_BIT, source->aspect, 0, source->mipLevels) ||
!TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
VK_ACCESS_TRANSFER_WRITE_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel,
snapshot.sampledLevelCount)) {
return false;
}
Vector<VkImageCopy> copyRegions;
copyRegions.reserve(snapshot.sampledLevelCount);
for (Uint32 level = snapshot.sampledBaseMipLevel;
level < snapshot.sampledBaseMipLevel + snapshot.sampledLevelCount; ++level) {
VkImageCopy copy{};
copy.srcSubresource = {source->aspect, level, 0, source->arrayLayers};
copy.dstSubresource = {snapshot.aspect, level, 0, snapshot.arrayLayers};
copy.extent = {std::max(source->extent.width >> level, 1u),
std::max(source->extent.height >> level, 1u),
std::max(source->depth >> level, 1u)};
copyRegions.push_back(copy);
}
vkCmdCopyImage(commandBuffer, source->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, snapshot.image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<Uint32>(copyRegions.size()), copyRegions.data());
if (!TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout,
ResolveSampledReadOnlyLayout(snapshot.aspect), VK_PIPELINE_STAGE_TRANSFER_BIT,
consumerShaderStageMask, VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_SHADER_READ_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel,
snapshot.sampledLevelCount) ||
!TransitionImageLayout(commandBuffer, source->image, source->layout, sourceLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, consumerShaderStageMask,
VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
source->aspect, 0, source->mipLevels)) {
return false;
}
StampResourceRecordingUse(*source);
outSnapshot = {.imageView = snapshot.sampledView, .layout = snapshot.layout};
DeferResourceRelease(Move(snapshot));
return true;
}
void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) { void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) {
m_storageImageTextures.insert(MakeTextureIdentity(&texture)); m_storageImageTextures.insert(MakeTextureIdentity(&texture));
} }
@@ -310,11 +310,6 @@ public:
static inline VmaAllocator s_allocator = VK_NULL_HANDLE; static inline VmaAllocator s_allocator = VK_NULL_HANDLE;
}; };
struct SampledTextureSnapshot {
VkImageView imageView = VK_NULL_HANDLE;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
};
Bool Initialize(const InitInfo& initInfo); Bool Initialize(const InitInfo& initInfo);
void Shutdown(); void Shutdown();
void BeginFrame(Uint32 frameIndex); void BeginFrame(Uint32 frameIndex);
@@ -348,13 +343,6 @@ public:
VkImageLayout newLayout); VkImageLayout newLayout);
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
// Copies the complete sampler-visible mip range into a transient sampled image. The source is
// restored to its prior layout, so image-store descriptors continue to name the original image.
// The transient ownership is tied to the current frame slot and is safe through its submission.
Bool SnapshotTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture,
SamplerNumericDomain numericDomain,
VkPipelineStageFlags consumerShaderStageMask,
SampledTextureSnapshot& outSnapshot);
// Recording-generation bookkeeping for the pre-pass command stream. The // Recording-generation bookkeeping for the pre-pass command stream. The
// generation advances every time the frame command buffer (re)begins // generation advances every time the frame command buffer (re)begins
@@ -5412,81 +5412,7 @@ void main() {
} }
return true; return true;
} }
Bool VulkanRenderer::PrepareSamplerImageFeedbackSnapshots(
FrameContext::FrameData& frame,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
VkPipelineStageFlags consumerShaderStageMask) {
auto& feedbackBindings = m_samplerImageFeedbackScratch;
auto& overrides = m_samplerImageBindingOverridesScratch;
overrides.clear();
if (!programObj.hasStorageImages) {
feedbackBindings.clear();
return true;
}
if (!m_uniformManager->CollectSamplerImageFeedback(program, programObj, feedbackBindings)) {
MGLOG_E_ONCE("%s: failed to collect sampler/image feedback for program=%u", __func__,
program.GetExternalIndex());
return false;
}
if (feedbackBindings.empty()) {
return true;
}
// Copy and layout barriers cannot be recorded inside a render pass. A graphics draw only
// gets here after an actual sampled/writable-image mip overlap was found, so ordinary
// graphics draws retain the active pass.
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
}
struct SnapshotCacheEntry {
MG_State::GLState::ITextureObject* texture = nullptr;
SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown;
VkTextureManager::SampledTextureSnapshot snapshot{};
};
Vector<SnapshotCacheEntry> snapshotCache;
snapshotCache.reserve(feedbackBindings.size());
overrides.reserve(feedbackBindings.size());
for (const auto& feedback : feedbackBindings) {
VkTextureManager::SampledTextureSnapshot snapshot{};
const auto existing = std::find_if(
snapshotCache.begin(), snapshotCache.end(), [&feedback](const SnapshotCacheEntry& candidate) {
return candidate.texture == feedback.texture && candidate.numericDomain == feedback.numericDomain;
});
if (existing != snapshotCache.end()) {
snapshot = existing->snapshot;
} else {
if (!m_textureManager->SnapshotTextureForSampling(frame.commandBuffer, *feedback.texture,
feedback.numericDomain, consumerShaderStageMask,
snapshot) ||
snapshot.imageView == VK_NULL_HANDLE) {
MGLOG_E_ONCE("%s: failed to snapshot textureId=%d for sampler binding=%u element=%u", __func__,
feedback.texture != nullptr ? feedback.texture->GetExternalIndex() : 0,
feedback.samplerBinding, feedback.samplerElement);
return false;
}
snapshotCache.push_back({.texture = feedback.texture,
.numericDomain = feedback.numericDomain,
.snapshot = snapshot});
}
overrides.push_back({
.binding = feedback.samplerBinding,
.element = feedback.samplerElement,
.texture = feedback.texture,
.sampler = feedback.sampler,
.imageView = snapshot.imageView,
.imageLayout = snapshot.layout,
.forceNearestFiltering = feedback.numericDomain == SamplerNumericDomain::SignedInteger ||
feedback.numericDomain == SamplerNumericDomain::UnsignedInteger,
});
if (program.GetExternalIndex() == 194 && feedback.texture->GetExternalIndex() == 75) {
MGLOG_D_ONCE("sampler/image feedback snapshot: program=194 texture=75 binding=%u element=%u view=%p",
feedback.samplerBinding, feedback.samplerElement, snapshot.imageView);
}
}
return true;
}
// The scissor rectangle Vulkan needs for ARB_viewport_array index `index`. Vulkan has no // The scissor rectangle Vulkan needs for ARB_viewport_array index `index`. Vulkan has no
// per-viewport scissor-test TOGGLE - a scissor rectangle always applies - so an index whose // per-viewport scissor-test TOGGLE - a scissor rectangle always applies - so an index whose
@@ -6168,11 +6094,6 @@ void main() {
MGLOG_E_ONCE("SetupDraw skipped: storage image preparation failed"); MGLOG_E_ONCE("SetupDraw skipped: storage image preparation failed");
return false; return false;
} }
if (!PrepareSamplerImageFeedbackSnapshots(frame, program, programObj,
VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT)) {
MGLOG_E_ONCE("SetupDraw skipped: sampler/image feedback snapshot failed");
return false;
}
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
@@ -6417,9 +6338,7 @@ void main() {
} }
const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers( const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers(
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(), frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex());
VK_PIPELINE_BIND_POINT_GRAPHICS, nullptr, false,
m_samplerImageBindingOverridesScratch.empty() ? nullptr : &m_samplerImageBindingOverridesScratch);
if (!boundUniforms) { if (!boundUniforms) {
MGLOG_E_ONCE("SetupDraw skipped: BindProgramUniformBuffers failed"); MGLOG_E_ONCE("SetupDraw skipped: BindProgramUniformBuffers failed");
return false; return false;
@@ -6542,11 +6461,6 @@ void main() {
MGLOG_E_ONCE("DispatchCompute skipped: storage image preparation failed"); MGLOG_E_ONCE("DispatchCompute skipped: storage image preparation failed");
return; return;
} }
if (!PrepareSamplerImageFeedbackSnapshots(frame, program, programObj,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT)) {
MGLOG_E_ONCE("DispatchCompute skipped: sampler/image feedback snapshot failed");
return;
}
const VkPipeline pipeline = GetOrCreateComputePipeline(programObj); const VkPipeline pipeline = GetOrCreateComputePipeline(programObj);
if (pipeline == VK_NULL_HANDLE) { if (pipeline == VK_NULL_HANDLE) {
@@ -6558,8 +6472,7 @@ void main() {
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers( const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers(
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(), frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(),
VK_PIPELINE_BIND_POINT_COMPUTE, nullptr, false, VK_PIPELINE_BIND_POINT_COMPUTE);
m_samplerImageBindingOverridesScratch.empty() ? nullptr : &m_samplerImageBindingOverridesScratch);
if (!boundUniforms) { if (!boundUniforms) {
MGLOG_E_ONCE("DispatchCompute skipped: BindProgramUniformBuffers failed"); MGLOG_E_ONCE("DispatchCompute skipped: BindProgramUniformBuffers failed");
return; return;
@@ -6594,11 +6507,6 @@ void main() {
MGLOG_E_ONCE("DispatchComputeIndirect skipped: storage image preparation failed"); MGLOG_E_ONCE("DispatchComputeIndirect skipped: storage image preparation failed");
return; return;
} }
if (!PrepareSamplerImageFeedbackSnapshots(frame, program, programObj,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT)) {
MGLOG_E_ONCE("DispatchComputeIndirect skipped: sampler/image feedback snapshot failed");
return;
}
const VkPipeline pipeline = GetOrCreateComputePipeline(programObj); const VkPipeline pipeline = GetOrCreateComputePipeline(programObj);
if (pipeline == VK_NULL_HANDLE) { if (pipeline == VK_NULL_HANDLE) {
@@ -6610,8 +6518,7 @@ void main() {
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers( const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers(
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(), frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(),
VK_PIPELINE_BIND_POINT_COMPUTE, nullptr, false, VK_PIPELINE_BIND_POINT_COMPUTE);
m_samplerImageBindingOverridesScratch.empty() ? nullptr : &m_samplerImageBindingOverridesScratch);
if (!boundUniforms) { if (!boundUniforms) {
MGLOG_E_ONCE("DispatchComputeIndirect skipped: BindProgramUniformBuffers failed"); MGLOG_E_ONCE("DispatchComputeIndirect skipped: BindProgramUniformBuffers failed");
return; return;
@@ -929,8 +929,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// already sampleable. // already sampleable.
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch; Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch; Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
Vector<UniformManager::SamplerImageFeedbackBinding> m_samplerImageFeedbackScratch;
Vector<UniformManager::SamplerBindingOverride> m_samplerImageBindingOverridesScratch;
Vector<VkBuffer> m_vertexBuffersScratch; Vector<VkBuffer> m_vertexBuffersScratch;
Vector<VkDeviceSize> m_vertexOffsetsScratch; Vector<VkDeviceSize> m_vertexOffsetsScratch;
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch; Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
@@ -1150,14 +1148,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
FrameContext::FrameData& frame, FrameContext::FrameData& frame,
const MG_State::GLState::ProgramObject& program, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj); const ProgramFactory::VkProgramObject& programObj);
// Vulkan forbids a sampled descriptor and writable storage descriptor from naming the
// same image subresource in one shader operation. Snapshot only the sampler side; the
// storage descriptor continues to name the application texture.
Bool PrepareSamplerImageFeedbackSnapshots(
FrameContext::FrameData& frame,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
VkPipelineStageFlags consumerShaderStageMask);
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth // The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
// bias, line width, stencil), gated behind one render-state-parameters-version // bias, line width, stencil), gated behind one render-state-parameters-version
+8
View File
@@ -10,6 +10,9 @@
#include <Config.h> #include <Config.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h> #include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Converters/MGToStr/GLExtensionConverter.h> #include <MG_Util/Converters/MGToStr/GLExtensionConverter.h>
#if defined(MOBILEGL_ENABLE_DILIGENT)
#include <MG_Backend/Diligent/BackendObject_Diligent.h>
#endif
namespace MobileGL::MG_Backend { namespace MobileGL::MG_Backend {
void LogBackendInfo() { void LogBackendInfo() {
@@ -55,6 +58,11 @@ namespace MobileGL::MG_Backend {
case BackendType::DirectVulkan: case BackendType::DirectVulkan:
pActiveBackendObject = MakeUnique<DirectVulkan::BackendObject_DirectVulkan>(); pActiveBackendObject = MakeUnique<DirectVulkan::BackendObject_DirectVulkan>();
break; break;
#if defined(MOBILEGL_ENABLE_DILIGENT)
case BackendType::DiligentVulkan:
pActiveBackendObject = MakeUnique<DiligentBackend::BackendObject_Diligent>();
break;
#endif
case BackendType::Unknown: case BackendType::Unknown:
default: default:
MGLOG_W("Unknown backend type, defaulting to unknown backend"); MGLOG_W("Unknown backend type, defaulting to unknown backend");
+10 -10
View File
@@ -175,14 +175,14 @@ namespace MobileGL::MG_Impl::GLXImpl {
struct ContextObject { struct ContextObject {
Display* XDisplay = nullptr; Display* XDisplay = nullptr;
EGLDisplay Display = EGL_NO_DISPLAY; EGLDisplay Dpy = EGL_NO_DISPLAY;
EGLConfig Config = nullptr; EGLConfig Config = nullptr;
EGLContext Context = EGL_NO_CONTEXT; EGLContext Context = EGL_NO_CONTEXT;
const FBConfigInfo* FBConfig = nullptr; const FBConfigInfo* FBConfig = nullptr;
}; };
struct DrawableSurface { struct DrawableSurface {
EGLDisplay Display = EGL_NO_DISPLAY; EGLDisplay Dpy = EGL_NO_DISPLAY;
EGLSurface Surface = EGL_NO_SURFACE; EGLSurface Surface = EGL_NO_SURFACE;
Uint32 Width = 0; Uint32 Width = 0;
Uint32 Height = 0; Uint32 Height = 0;
@@ -294,7 +294,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
if (width == surface.Width && height == surface.Height) { if (width == surface.Width && height == surface.Height) {
return; return;
} }
if (EGLImpl::ResizePlatformWindowSurface(surface.Display, surface.Surface, if (EGLImpl::ResizePlatformWindowSurface(surface.Dpy, surface.Surface,
static_cast<EGLint>(width), static_cast<EGLint>(width),
static_cast<EGLint>(height))) { static_cast<EGLint>(height))) {
surface.Width = width; surface.Width = width;
@@ -324,7 +324,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
EGL_NONE, EGL_NONE,
}; };
EGLSurface surface = EGLImpl::CreatePlatformWindowSurface( EGLSurface surface = EGLImpl::CreatePlatformWindowSurface(
context.Display, context.Config, reinterpret_cast<void*>(drawable), attribs); context.Dpy, context.Config, reinterpret_cast<void*>(drawable), attribs);
if (surface == EGL_NO_SURFACE) { if (surface == EGL_NO_SURFACE) {
MGLOG_E_ONCE("glx: failed to create window surface for drawable 0x%lx (%ux%u)", drawable, MGLOG_E_ONCE("glx: failed to create window surface for drawable 0x%lx (%ux%u)", drawable,
width, height); width, height);
@@ -332,7 +332,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
} }
DrawableSurface record; DrawableSurface record;
record.Display = context.Display; record.Dpy = context.Dpy;
record.Surface = surface; record.Surface = surface;
record.Width = width; record.Width = width;
record.Height = height; record.Height = height;
@@ -388,7 +388,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
ContextObject object; ContextObject object;
object.XDisplay = dpy; object.XDisplay = dpy;
object.Display = display; object.Dpy = display;
object.Config = config; object.Config = config;
object.Context = eglContext; object.Context = eglContext;
object.FBConfig = fbconfig; object.FBConfig = fbconfig;
@@ -895,7 +895,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
return; return;
} }
if (object->Context != EGL_NO_CONTEXT) { if (object->Context != EGL_NO_CONTEXT) {
EGLImpl::DestroyContext(object->Display, object->Context); EGLImpl::DestroyContext(object->Dpy, object->Context);
} }
Contexts().erase(context); Contexts().erase(context);
} }
@@ -929,7 +929,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
return 0; return 0;
} }
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface, if (!EGLImpl::MakeCurrent(object->Dpy, surface->Surface, surface->Surface,
object->Context)) { object->Context)) {
MGLOG_E_ONCE("glx: eglMakeCurrent failed (drawable=0x%lx, ctx=%p)", drawable, context); MGLOG_E_ONCE("glx: eglMakeCurrent failed (drawable=0x%lx, ctx=%p)", drawable, context);
return 0; return 0;
@@ -962,7 +962,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
return; return;
} }
SyncSurfaceSize(dpy, drawable, it->second); SyncSurfaceSize(dpy, drawable, it->second);
EGLImpl::SwapBuffers(it->second.Display, it->second.Surface); EGLImpl::SwapBuffers(it->second.Dpy, it->second.Surface);
} }
GLXDrawableHandle CreateWindow(Display*, GLXFBConfigHandle config, GLXDrawableHandle window, GLXDrawableHandle CreateWindow(Display*, GLXFBConfigHandle config, GLXDrawableHandle window,
@@ -988,7 +988,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
if (it == surfaces.end()) { if (it == surfaces.end()) {
return; return;
} }
EGLImpl::DestroySurface(it->second.Display, it->second.Surface); EGLImpl::DestroySurface(it->second.Dpy, it->second.Surface);
surfaces.erase(it); surfaces.erase(it);
} }
@@ -68,7 +68,6 @@ add_executable(MobileGLIntegrationTest
Scenarios/DoublePrecisionScenario.cpp Scenarios/DoublePrecisionScenario.cpp
Scenarios/UniformInitializerScenario.cpp Scenarios/UniformInitializerScenario.cpp
Scenarios/SwizzleAccessRoutineScenario.cpp Scenarios/SwizzleAccessRoutineScenario.cpp
Scenarios/Program203FirstReductionScenario.cpp
Scenarios/ProgramPipelineScenario.cpp Scenarios/ProgramPipelineScenario.cpp
Scenarios/ImageLoadStoreSsoScenario.cpp Scenarios/ImageLoadStoreSsoScenario.cpp
Scenarios/ImageTargetKindScenario.cpp Scenarios/ImageTargetKindScenario.cpp
@@ -15,11 +15,6 @@
#include <ostream> #include <ostream>
#include <sstream> #include <sstream>
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif
// MobileGL's own headers, in the order MobileGL/Includes.h uses them: GL/gl.h // MobileGL's own headers, in the order MobileGL/Includes.h uses them: GL/gl.h
// first, then glcorearb.h for the 3.x+ entry points. This binary links // first, then glcorearb.h for the 3.x+ entry points. This binary links
// MobileGL_s, so every gl*/egl* below binds to MobileGL's implementation, not // MobileGL_s, so every gl*/egl* below binds to MobileGL's implementation, not
@@ -58,37 +53,6 @@ namespace MGITest {
constexpr int kSurfaceWidth = 128; constexpr int kSurfaceWidth = 128;
constexpr int kSurfaceHeight = 96; constexpr int kSurfaceHeight = 96;
#if defined(_WIN32)
HWND g_testWindow = nullptr;
HWND CreateTestWindow() {
static const wchar_t* const kClassName = L"MobileGLIntegrationTestWindow";
static bool registered = false;
if (!registered) {
WNDCLASSW windowClass{};
windowClass.lpfnWndProc = DefWindowProcW;
windowClass.hInstance = GetModuleHandleW(nullptr);
windowClass.lpszClassName = kClassName;
if (RegisterClassW(&windowClass) == 0 && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) {
return nullptr;
}
registered = true;
}
return CreateWindowExW(0, kClassName, L"MobileGL Integration Test", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, kSurfaceWidth, kSurfaceHeight, nullptr, nullptr,
GetModuleHandleW(nullptr), nullptr);
}
#endif
bool UseWindowSurface() {
#if defined(_WIN32)
const char* value = std::getenv("MOBILEGL_ITEST_WINDOW_SURFACE");
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
#else
return false;
#endif
}
std::string EnvOr(const char* name, const char* fallback) { std::string EnvOr(const char* name, const char* fallback) {
const char* value = std::getenv(name); const char* value = std::getenv(name);
return (value != nullptr && value[0] != '\0') ? std::string(value) : std::string(fallback); return (value != nullptr && value[0] != '\0') ? std::string(value) : std::string(fallback);
@@ -170,9 +134,8 @@ namespace MGITest {
return 3; return 3;
} }
const bool useWindowSurface = UseWindowSurface();
const EGLint configAttribs[] = {EGL_SURFACE_TYPE, const EGLint configAttribs[] = {EGL_SURFACE_TYPE,
useWindowSurface ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT, EGL_PBUFFER_BIT,
EGL_RED_SIZE, EGL_RED_SIZE,
8, 8,
EGL_GREEN_SIZE, EGL_GREEN_SIZE,
@@ -189,9 +152,7 @@ namespace MGITest {
EGLConfig config = nullptr; EGLConfig config = nullptr;
EGLint configCount = 0; EGLint configCount = 0;
if (eglChooseConfig(display, configAttribs, &config, 1, &configCount) != EGL_TRUE || configCount < 1) { if (eglChooseConfig(display, configAttribs, &config, 1, &configCount) != EGL_TRUE || configCount < 1) {
outReason = WithEglError(useWindowSurface outReason = WithEglError("eglChooseConfig found no pbuffer-capable RGBA8/D24 config");
? "eglChooseConfig found no window-capable RGBA8/D24 config"
: "eglChooseConfig found no pbuffer-capable RGBA8/D24 config");
return 4; return 4;
} }
@@ -205,23 +166,10 @@ namespace MGITest {
return 5; return 5;
} }
EGLSurface surface = EGL_NO_SURFACE;
if (useWindowSurface) {
#if defined(_WIN32)
if (g_testWindow == nullptr) g_testWindow = CreateTestWindow();
if (g_testWindow == nullptr) {
outReason = "failed to create the Windows integration-test window";
return 6;
}
surface = eglCreateWindowSurface(display, config, g_testWindow, nullptr);
#endif
} else {
const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE}; const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE};
surface = eglCreatePbufferSurface(display, config, pbufferAttribs); EGLSurface surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
}
if (surface == EGL_NO_SURFACE) { if (surface == EGL_NO_SURFACE) {
outReason = WithEglError(useWindowSurface ? "eglCreateWindowSurface failed" outReason = WithEglError("eglCreatePbufferSurface failed");
: "eglCreatePbufferSurface failed");
return 6; return 6;
} }
// The step that brings the whole backend up (DirectVulkan creates its // The step that brings the whole backend up (DirectVulkan creates its
@@ -543,12 +491,6 @@ namespace MGITest {
if (m_context != nullptr) eglDestroyContext(display, static_cast<EGLContext>(m_context)); if (m_context != nullptr) eglDestroyContext(display, static_cast<EGLContext>(m_context));
if (m_surface != nullptr) eglDestroySurface(display, static_cast<EGLSurface>(m_surface)); if (m_surface != nullptr) eglDestroySurface(display, static_cast<EGLSurface>(m_surface));
eglTerminate(display); eglTerminate(display);
#if defined(_WIN32)
if (g_testWindow != nullptr) {
DestroyWindow(g_testWindow);
g_testWindow = nullptr;
}
#endif
m_context = nullptr; m_context = nullptr;
m_surface = nullptr; m_surface = nullptr;
m_display = nullptr; m_display = nullptr;
@@ -1,878 +0,0 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/Program203FirstReductionScenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - PROGRAM 203'S FIRST SUBGROUP REDUCTION.
//
// Program 203 reduces a 32 x 16 exposure tile with a vector subgroup inclusive add,
// then a shared-memory scan of subgroup totals. The source assumes that every
// subgroup has a last lane, that there are 2..32 subgroups, and that local index
// 511 belongs to the last subgroup and its last lane. Those are source assumptions,
// not API contracts. This probe intentionally does not repair them: it records the
// observed topology and makes each handoff independently observable.
#include <algorithm>
#include <array>
#include <bit>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <limits>
#include <sstream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr std::size_t kInvocationCount = 512;
constexpr std::size_t kScanStageCount = 6;
constexpr std::uint32_t kQuietNanBits = 0x7fc00000u;
constexpr std::size_t kNoSlot = std::numeric_limits<std::size_t>::max();
struct UVec4 {
std::uint32_t x;
std::uint32_t y;
std::uint32_t z;
std::uint32_t w;
};
struct Vec4 {
float x;
float y;
float z;
float w;
};
// Matches the std430 block exactly. uvec4/vec4 arrays have a 16-byte
// stride, floats are a dense scalar array, and the outer scan array is
// stage-major in both GLSL and C++.
struct ProbeOutput {
std::array<UVec4, kInvocationCount> invocation;
std::array<UVec4, kInvocationCount> subgroup;
std::array<Vec4, kInvocationCount> reduction;
std::array<float, kInvocationCount> finalAverage;
std::array<std::array<float, kInvocationCount>, kScanStageCount> scanAfter;
};
static_assert(sizeof(UVec4) == 16);
static_assert(sizeof(Vec4) == 16);
static_assert(std::is_standard_layout_v<ProbeOutput>);
static_assert(offsetof(ProbeOutput, invocation) == 0);
static_assert(offsetof(ProbeOutput, subgroup) == 8192);
static_assert(offsetof(ProbeOutput, reduction) == 16384);
static_assert(offsetof(ProbeOutput, finalAverage) == 24576);
static_assert(offsetof(ProbeOutput, scanAfter) == 26624);
static_assert(sizeof(ProbeOutput) == 38912);
enum class InputMode {
SampledRgba32f,
IndexedSsbo,
};
const char* InputModeName(InputMode mode) {
return mode == InputMode::SampledRgba32f ? "sampled RGBA32F" : "indexed SSBO";
}
std::uint32_t FloatBits(float value) {
return std::bit_cast<std::uint32_t>(value);
}
bool SameBits(float lhs, float rhs) {
return FloatBits(lhs) == FloatBits(rhs);
}
bool IsQuietNanSentinel(float value) {
return FloatBits(value) == kQuietNanBits;
}
bool DrainGlErrors() {
bool hadError = false;
while (glGetError() != GL_NO_ERROR) hadError = true;
return hadError;
}
bool HasExtension(const char* wanted) {
GLint extensionCount = 0;
glGetIntegerv(GL_NUM_EXTENSIONS, &extensionCount);
for (GLint i = 0; i < extensionCount; ++i) {
const auto* extension = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, static_cast<GLuint>(i)));
if (extension != nullptr && std::string(extension) == wanted) return true;
}
return false;
}
struct CapabilityInfo {
bool subgroupExtension = false;
GLint subgroupSize = 0;
GLint supportedStages = 0;
GLint supportedFeatures = 0;
GLint maxComputeStorageBlocks = 0;
GLint maxStorageBindings = 0;
GLint maxWorkGroupInvocations = 0;
std::array<GLint, 3> maxWorkGroupSize{};
bool queryHadError = false;
bool SupportsProbe() const {
const auto stages = static_cast<GLbitfield>(supportedStages);
const auto features = static_cast<GLbitfield>(supportedFeatures);
return !queryHadError && subgroupExtension &&
(stages & GL_COMPUTE_SHADER_BIT) != 0 &&
(features & (GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR)) ==
(GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR) &&
maxComputeStorageBlocks >= 2 && maxStorageBindings >= 2 &&
maxWorkGroupInvocations >= static_cast<GLint>(kInvocationCount) && maxWorkGroupSize[0] >= 32 &&
maxWorkGroupSize[1] >= 16 && maxWorkGroupSize[2] >= 1;
}
std::string MissingRequirements() const {
std::vector<std::string> missing;
const auto stages = static_cast<GLbitfield>(supportedStages);
const auto features = static_cast<GLbitfield>(supportedFeatures);
if (queryHadError) missing.emplace_back("a subgroup/compute capability query generated GL error");
if (!subgroupExtension) missing.emplace_back("GL_KHR_shader_subgroup");
if ((stages & GL_COMPUTE_SHADER_BIT) == 0) {
missing.emplace_back("GL_COMPUTE_SHADER_BIT in GL_SUBGROUP_SUPPORTED_STAGES_KHR");
}
const auto requiredFeatures =
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR;
if ((features & requiredFeatures) != requiredFeatures) {
missing.emplace_back("basic|arithmetic in GL_SUBGROUP_SUPPORTED_FEATURES_KHR");
}
if (maxComputeStorageBlocks < 2 || maxStorageBindings < 2) {
missing.emplace_back("two compute SSBO bindings");
}
if (maxWorkGroupInvocations < static_cast<GLint>(kInvocationCount) || maxWorkGroupSize[0] < 32 ||
maxWorkGroupSize[1] < 16 || maxWorkGroupSize[2] < 1) {
missing.emplace_back("a 32x16x1 / 512-invocation compute workgroup");
}
std::ostringstream message;
for (std::size_t i = 0; i < missing.size(); ++i) {
if (i != 0) message << ", ";
message << missing[i];
}
return message.str();
}
};
CapabilityInfo QueryCapabilities() {
CapabilityInfo info;
DrainGlErrors();
info.subgroupExtension = HasExtension("GL_KHR_shader_subgroup");
glGetIntegerv(GL_SUBGROUP_SIZE_KHR, &info.subgroupSize);
glGetIntegerv(GL_SUBGROUP_SUPPORTED_STAGES_KHR, &info.supportedStages);
glGetIntegerv(GL_SUBGROUP_SUPPORTED_FEATURES_KHR, &info.supportedFeatures);
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &info.maxComputeStorageBlocks);
glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, &info.maxStorageBindings);
glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &info.maxWorkGroupInvocations);
for (GLuint axis = 0; axis < info.maxWorkGroupSize.size(); ++axis) {
glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, axis, &info.maxWorkGroupSize[axis]);
}
info.queryHadError = DrainGlErrors();
return info;
}
void PrintMetadata(const CapabilityInfo& info, std::ostream& output) {
output << "Program203FirstReductionScenario metadata: "
<< "GL_SUBGROUP_SIZE_KHR=" << info.subgroupSize
<< ", GL_SUBGROUP_SUPPORTED_STAGES_KHR=0x" << std::hex
<< static_cast<GLbitfield>(info.supportedStages)
<< ", GL_SUBGROUP_SUPPORTED_FEATURES_KHR=0x"
<< static_cast<GLbitfield>(info.supportedFeatures) << std::dec
<< ", subgroupExtension=" << info.subgroupExtension
<< ", GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS=" << info.maxComputeStorageBlocks
<< ", GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS=" << info.maxStorageBindings
<< ", GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS=" << info.maxWorkGroupInvocations
<< ", GL_MAX_COMPUTE_WORK_GROUP_SIZE=" << info.maxWorkGroupSize[0] << 'x'
<< info.maxWorkGroupSize[1] << 'x' << info.maxWorkGroupSize[2]
<< ", queryHadError=" << info.queryHadError << '\n';
}
bool DumpRequested() {
const char* value = std::getenv("MOBILEGL_ITEST_SUBGROUP_PROBE_DUMP");
return value != nullptr && std::string(value) == "1";
}
constexpr const char* kShaderPreamble = R"(#version 430 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 1) buffer SubgroupProbeOutput {
uvec4 invocation[512];
uvec4 subgroup[512];
vec4 reduction[512];
float finalAverage[512];
float scanAfter[6][512];
} outProbe;
shared vec2 prefixSumCache[32];
)";
constexpr const char* kSampledInput = R"(
uniform sampler2D colortex2;
uniform vec2 pixelSize;
)";
constexpr const char* kIndexedInput = R"(
layout(std430, binding = 0) readonly buffer Input {
float value[512];
} inputData;
)";
// Only the expression producing tileExposure differs between the two
// tests. The remainder is the program-203 first reduction, with stores
// placed after its existing barriers to expose each handoff.
constexpr const char* kSampledTileExposure = R"(
vec2 texCoord = (vec2(gl_GlobalInvocationID.xy) + 0.5) *
vec2(1.0 / 32.0, 1.0 / 16.0);
vec2 sampleCoord = texCoord * (1.0 / 64.0);
sampleCoord.x += (15.0 / 32.0) + pixelSize.x * 12.0;
float tileExposure = dot(
textureLod(colortex2, sampleCoord, 0.0).rgb,
vec3(0.2125, 0.7154, 0.0721));
)";
constexpr const char* kIndexedTileExposure = R"(
float tileExposure = inputData.value[gl_LocalInvocationIndex];
)";
constexpr const char* kReductionBody = R"(
vec2 sampleLuminance = vec2(tileExposure, 0.0);
sampleLuminance = subgroupInclusiveAdd(sampleLuminance);
float nativeInclusive = sampleLuminance.x;
// This is a uniform, safety-only branch: it leaves an invalid source
// contract visible without indexing past the 32-entry cache or underflowing
// loopLength - 1. It is deliberately a failure on the CPU, not a skip.
bool sourceDomain = gl_NumSubgroups >= 2u && gl_NumSubgroups <= 32u;
if (!sourceDomain) {
float qNaN = uintBitsToFloat(0x7fc00000u);
uint localIndex = gl_LocalInvocationIndex;
outProbe.invocation[localIndex] = uvec4(localIndex, gl_LocalInvocationID);
outProbe.subgroup[localIndex] = uvec4(gl_SubgroupSize, gl_NumSubgroups, gl_SubgroupID,
gl_SubgroupInvocationID);
outProbe.reduction[localIndex] = vec4(tileExposure, nativeInclusive, qNaN, qNaN);
outProbe.finalAverage[localIndex] = qNaN;
for (uint stage = 0u; stage < 6u; ++stage)
outProbe.scanAfter[stage][localIndex] = qNaN;
return;
}
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
barrier();
float sourceRawSubtotal = prefixSumCache[gl_SubgroupID].x;
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (uint scanStage = 0u; scanStage < loopLength; ++scanStage) {
if ((gl_SubgroupID & (1u << scanStage)) > 0u) {
sampleLuminance += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
}
barrier();
outProbe.scanAfter[scanStage][gl_LocalInvocationIndex] = sampleLuminance.x;
}
float sourceMergedPrefix = sampleLuminance.x;
if (gl_LocalInvocationIndex == 511u)
prefixSumCache[0] = sampleLuminance / 512.0;
barrier();
float avg = prefixSumCache[0].x;
uint localIndex = gl_LocalInvocationIndex;
outProbe.invocation[localIndex] = uvec4(localIndex, gl_LocalInvocationID);
outProbe.subgroup[localIndex] = uvec4(gl_SubgroupSize, gl_NumSubgroups, gl_SubgroupID,
gl_SubgroupInvocationID);
outProbe.reduction[localIndex] = vec4(tileExposure, nativeInclusive, sourceRawSubtotal, sourceMergedPrefix);
outProbe.finalAverage[localIndex] = avg;
}
)";
std::string BuildProbeShader(InputMode mode) {
std::string source = kShaderPreamble;
source += mode == InputMode::SampledRgba32f ? kSampledInput : kIndexedInput;
source += "\nvoid main() {\n";
source += mode == InputMode::SampledRgba32f ? kSampledTileExposure : kIndexedTileExposure;
source += kReductionBody;
return source;
}
std::string FormatFloat(float value) {
std::ostringstream text;
text << std::hexfloat << value;
return text.str();
}
struct ValidationResult {
bool ok = true;
std::string phase;
std::string message;
bool scanStageMismatch = false;
int scanStage = -1;
bool ownerEvaluated = false;
bool index511IsSourceLastLaneWriter = false;
bool index511IsHighestSubgroupMember = false;
std::uint32_t highestObservedSubgroup = 0;
};
ValidationResult Failure(std::string phase, std::string message) {
ValidationResult result;
result.ok = false;
result.phase = std::move(phase);
result.message = std::move(message);
return result;
}
constexpr float kSampledLuminance = 0.2125f + 0.7154f + 0.0721f;
float ExpectedInput(InputMode mode, std::uint32_t localIndex) {
return mode == InputMode::SampledRgba32f ? kSampledLuminance : static_cast<float>(localIndex + 1u);
}
ValidationResult ValidateProbe(const ProbeOutput& output, InputMode mode) {
std::array<std::size_t, kInvocationCount> slotForLocal{};
slotForLocal.fill(kNoSlot);
// 1. Record identity. Slots are only used to locate each reported
// local index; all subgroup behavior below groups recorded IDs/lanes.
for (std::size_t slot = 0; slot < kInvocationCount; ++slot) {
const std::uint32_t localIndex = output.invocation[slot].x;
if (localIndex >= kInvocationCount) {
std::ostringstream message;
message << "output slot " << slot << " reports localIndex " << localIndex << " outside [0, 511]";
return Failure("record identity", message.str());
}
if (slotForLocal[localIndex] != kNoSlot) {
std::ostringstream message;
message << "localIndex " << localIndex << " appears in output slots " << slotForLocal[localIndex]
<< " and " << slot;
return Failure("record identity", message.str());
}
slotForLocal[localIndex] = slot;
}
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
if (slotForLocal[localIndex] == kNoSlot) {
std::ostringstream message;
message << "localIndex " << localIndex << " is missing from all 512 records";
return Failure("record identity", message.str());
}
}
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
const std::size_t slot = slotForLocal[localIndex];
const UVec4& invocation = output.invocation[slot];
const std::uint32_t expectedX = static_cast<std::uint32_t>(localIndex % 32u);
const std::uint32_t expectedY = static_cast<std::uint32_t>(localIndex / 32u);
if (invocation.y != expectedX || invocation.z != expectedY || invocation.w != 0u) {
std::ostringstream message;
message << "localIndex " << localIndex << " reports local invocation (" << invocation.y << ','
<< invocation.z << ',' << invocation.w << "), expected (" << expectedX << ',' << expectedY
<< ",0)";
return Failure("record identity", message.str());
}
const float expectedInput = ExpectedInput(mode, static_cast<std::uint32_t>(localIndex));
const float actualInput = output.reduction[slot].x;
if (!SameBits(actualInput, expectedInput)) {
std::ostringstream message;
message << "localIndex " << localIndex << " input was " << FormatFloat(actualInput) << ", expected "
<< FormatFloat(expectedInput);
return Failure("input", message.str());
}
}
// 2. Observed topology. Do not derive lanes or subgroup membership
// from local invocation indices: only the values the shader recorded
// participate in grouping.
const std::uint32_t reportedNumSubgroups = output.subgroup[slotForLocal[0]].y;
if (reportedNumSubgroups == 0u) {
return Failure("observed topology", "localIndex 0 reported gl_NumSubgroups == 0");
}
if (reportedNumSubgroups > kInvocationCount) {
std::ostringstream message;
message << "reported gl_NumSubgroups=" << reportedNumSubgroups
<< " exceeds the 512 recorded invocations, so at least one subgroup ID is missing";
return Failure("observed topology", message.str());
}
std::vector<std::vector<std::size_t>> subgroupSlots(reportedNumSubgroups);
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
const std::size_t slot = slotForLocal[localIndex];
const UVec4& subgroup = output.subgroup[slot];
if (subgroup.x == 0u || subgroup.y == 0u) {
std::ostringstream message;
message << "localIndex " << localIndex << " reported subgroupSize=" << subgroup.x
<< ", numSubgroups=" << subgroup.y;
return Failure("observed topology", message.str());
}
if (subgroup.y != reportedNumSubgroups) {
std::ostringstream message;
message << "localIndex " << localIndex << " reported numSubgroups=" << subgroup.y
<< ", while localIndex 0 reported " << reportedNumSubgroups;
return Failure("observed topology", message.str());
}
if (subgroup.z >= reportedNumSubgroups) {
std::ostringstream message;
message << "localIndex " << localIndex << " reported subgroupID=" << subgroup.z
<< " outside [0, " << (reportedNumSubgroups - 1u) << ']';
return Failure("observed topology", message.str());
}
if (subgroup.w >= subgroup.x) {
std::ostringstream message;
message << "localIndex " << localIndex << " reported laneID=" << subgroup.w
<< " outside its subgroupSize=" << subgroup.x;
return Failure("observed topology", message.str());
}
subgroupSlots[subgroup.z].push_back(slot);
}
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
if (subgroupSlots[subgroupID].empty()) {
std::ostringstream message;
message << "reported gl_NumSubgroups=" << reportedNumSubgroups
<< " but subgroupID " << subgroupID << " has no recorded members";
return Failure("observed topology", message.str());
}
auto& members = subgroupSlots[subgroupID];
std::sort(members.begin(), members.end(), [&output](std::size_t lhs, std::size_t rhs) {
return output.subgroup[lhs].w < output.subgroup[rhs].w;
});
for (std::size_t i = 1; i < members.size(); ++i) {
if (output.subgroup[members[i - 1]].w == output.subgroup[members[i]].w) {
std::ostringstream message;
message << "subgroupID " << subgroupID << " contains duplicate laneID "
<< output.subgroup[members[i]].w;
return Failure("observed topology", message.str());
}
}
}
// 3. Native subgroup arithmetic, in the actual lane ordering emitted
// by the driver. The fixture values and all partial sums are exactly
// representable binary32 values, so compare representation, not epsilon.
std::array<float, kInvocationCount> nativePrefix{};
std::vector<float> nativeSubtotal(reportedNumSubgroups, 0.0f);
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
float inclusive = 0.0f;
for (const std::size_t slot : subgroupSlots[subgroupID]) {
const std::uint32_t localIndex = output.invocation[slot].x;
inclusive += ExpectedInput(mode, localIndex);
nativePrefix[slot] = inclusive;
const float actualNative = output.reduction[slot].y;
if (!SameBits(actualNative, inclusive)) {
std::ostringstream message;
message << "subgroupID " << subgroupID << ", laneID " << output.subgroup[slot].w
<< ", localIndex " << localIndex << " nativeInclusive was " << FormatFloat(actualNative)
<< ", expected " << FormatFloat(inclusive);
return Failure("native subgroup arithmetic", message.str());
}
}
nativeSubtotal[subgroupID] = inclusive;
}
// sourceDomain is the narrow source-side safety branch. It is checked
// after native arithmetic so an unsupported source topology still
// reports native subgroup behavior before failing explicitly.
if (reportedNumSubgroups < 2u || reportedNumSubgroups > 32u) {
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
const std::size_t slot = slotForLocal[localIndex];
const Vec4& reduction = output.reduction[slot];
if (!IsQuietNanSentinel(reduction.z) || !IsQuietNanSentinel(reduction.w) ||
!IsQuietNanSentinel(output.finalAverage[slot])) {
std::ostringstream message;
message << "program 203 source reduction has no valid contract for gl_NumSubgroups="
<< reportedNumSubgroups << "; localIndex " << localIndex
<< " did not preserve its qNaN source-reduction sentinel";
return Failure("source domain", message.str());
}
for (std::size_t stage = 0; stage < kScanStageCount; ++stage) {
if (!IsQuietNanSentinel(output.scanAfter[stage][slot])) {
std::ostringstream message;
message << "program 203 source reduction has no valid contract for gl_NumSubgroups="
<< reportedNumSubgroups << "; localIndex " << localIndex << ", scan stage " << stage
<< " did not preserve its qNaN source-reduction sentinel";
return Failure("source domain", message.str());
}
}
}
std::ostringstream message;
message << "program 203 source reduction has no valid contract for observed gl_NumSubgroups="
<< reportedNumSubgroups << " (requires 2..32); native subgroup results were recorded";
return Failure("source domain", message.str());
}
// 4. Program-203 source writer and first shared-memory handoff.
std::vector<std::size_t> sourceWriter(reportedNumSubgroups, kNoSlot);
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
std::size_t writerCount = 0;
for (const std::size_t slot : subgroupSlots[subgroupID]) {
const UVec4& subgroup = output.subgroup[slot];
if (subgroup.w == subgroup.x - 1u) {
sourceWriter[subgroupID] = slot;
++writerCount;
}
}
if (writerCount != 1u) {
std::ostringstream message;
message << "subgroupID " << subgroupID << " has " << writerCount
<< " recorded lane(s) where laneID == subgroupSize - 1; program 203 leaves that "
"shared-cache entry unwritten";
return Failure("source writer", message.str());
}
for (const std::size_t slot : subgroupSlots[subgroupID]) {
const float actualRawSubtotal = output.reduction[slot].z;
if (!SameBits(actualRawSubtotal, nativeSubtotal[subgroupID])) {
std::ostringstream message;
message << "subgroupID " << subgroupID << ", localIndex " << output.invocation[slot].x
<< " sourceRawSubtotal was " << FormatFloat(actualRawSubtotal) << ", expected "
<< FormatFloat(nativeSubtotal[subgroupID]);
return Failure("source raw subtotal", message.str());
}
}
}
// 5. Reproduce the source loop exactly, including the redundant final
// scan iteration on power-of-two subgroup counts. Reads and writes in
// one iteration target disjoint cache entries, so update the cache at
// the CPU equivalent of the source barrier.
std::array<float, kInvocationCount> mergedPrefix = nativePrefix;
std::vector<float> cache = nativeSubtotal;
std::uint32_t loopLength = std::bit_width(reportedNumSubgroups) - 1u;
loopLength +=
static_cast<std::uint32_t>(reportedNumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (std::uint32_t scanStage = 0u; scanStage < loopLength; ++scanStage) {
std::vector<float> cacheAfterStage = cache;
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
if ((subgroupID & (1u << scanStage)) == 0u) continue;
const std::uint32_t sourceCacheIndex = (subgroupID >> scanStage << scanStage) - 1u;
const float sourcePrefix = cache[sourceCacheIndex];
for (const std::size_t slot : subgroupSlots[subgroupID]) {
mergedPrefix[slot] += sourcePrefix;
}
cacheAfterStage[subgroupID] = mergedPrefix[sourceWriter[subgroupID]];
}
cache.swap(cacheAfterStage);
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
const std::size_t slot = slotForLocal[localIndex];
const float actualAfterStage = output.scanAfter[scanStage][slot];
if (!SameBits(actualAfterStage, mergedPrefix[slot])) {
std::ostringstream message;
message << "scanStage " << scanStage << ", subgroupID " << output.subgroup[slot].z
<< ", laneID " << output.subgroup[slot].w << ", localIndex " << localIndex
<< " scanAfter was " << FormatFloat(actualAfterStage) << ", expected "
<< FormatFloat(mergedPrefix[slot]);
ValidationResult result = Failure("source scan", message.str());
result.scanStageMismatch = true;
result.scanStage = static_cast<int>(scanStage);
return result;
}
}
}
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
const std::size_t slot = slotForLocal[localIndex];
const float actualMergedPrefix = output.reduction[slot].w;
if (!SameBits(actualMergedPrefix, mergedPrefix[slot])) {
std::ostringstream message;
message << "localIndex " << localIndex << " sourceMergedPrefix was "
<< FormatFloat(actualMergedPrefix) << ", expected " << FormatFloat(mergedPrefix[slot]);
return Failure("source scan", message.str());
}
}
// 6. Final owner and average. The uniformity check is intentionally
// separate from the source's topology contract at local index 511.
const float firstAverage = output.finalAverage[slotForLocal[0]];
for (std::size_t localIndex = 1; localIndex < kInvocationCount; ++localIndex) {
const float actualAverage = output.finalAverage[slotForLocal[localIndex]];
if (!SameBits(actualAverage, firstAverage)) {
std::ostringstream message;
message << "finalAverage differs: localIndex 0 has " << FormatFloat(firstAverage)
<< ", localIndex " << localIndex << " has " << FormatFloat(actualAverage);
return Failure("final average", message.str());
}
}
ValidationResult ownerResult;
ownerResult.ownerEvaluated = true;
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
if (!subgroupSlots[subgroupID].empty()) {
ownerResult.highestObservedSubgroup = std::max(ownerResult.highestObservedSubgroup, subgroupID);
}
}
const std::size_t index511Slot = slotForLocal[kInvocationCount - 1u];
const UVec4& index511Subgroup = output.subgroup[index511Slot];
ownerResult.index511IsSourceLastLaneWriter =
index511Subgroup.w == index511Subgroup.x - 1u;
ownerResult.index511IsHighestSubgroupMember =
index511Subgroup.z == ownerResult.highestObservedSubgroup;
if (!ownerResult.index511IsSourceLastLaneWriter || !ownerResult.index511IsHighestSubgroupMember) {
std::ostringstream message;
message << "program 203 topology incompatibility: localIndex 511 is sourceLastLaneWriter="
<< ownerResult.index511IsSourceLastLaneWriter << ", highestSubgroupMember="
<< ownerResult.index511IsHighestSubgroupMember << " (subgroupID=" << index511Subgroup.z
<< ", highest observed subgroupID=" << ownerResult.highestObservedSubgroup << ')';
ownerResult.ok = false;
ownerResult.phase = "final average";
ownerResult.message = message.str();
return ownerResult;
}
float total = 0.0f;
for (const float subtotal : nativeSubtotal) total += subtotal;
float sampledExpectedTotal = 0.0f;
for (std::size_t i = 0; i < kInvocationCount; ++i) sampledExpectedTotal += kSampledLuminance;
const float expectedTotal = mode == InputMode::IndexedSsbo ? 131328.0f : sampledExpectedTotal;
if (!SameBits(total, expectedTotal) || !SameBits(mergedPrefix[index511Slot], expectedTotal)) {
std::ostringstream message;
message << "program 203 source total was " << FormatFloat(mergedPrefix[index511Slot])
<< " (native total " << FormatFloat(total) << "), expected " << FormatFloat(expectedTotal);
ownerResult.ok = false;
ownerResult.phase = "final average";
ownerResult.message = message.str();
return ownerResult;
}
const float expectedAverage = mode == InputMode::IndexedSsbo ? 256.5f : sampledExpectedTotal / 512.0f;
if (!SameBits(firstAverage, expectedAverage)) {
std::ostringstream message;
message << "finalAverage was " << FormatFloat(firstAverage) << ", expected "
<< FormatFloat(expectedAverage);
ownerResult.ok = false;
ownerResult.phase = "final average";
ownerResult.message = message.str();
return ownerResult;
}
return ownerResult;
}
void DumpProbe(const ProbeOutput& output, const CapabilityInfo& capabilities, const ValidationResult& validation,
bool includeScanStages) {
PrintMetadata(capabilities, std::cout);
if (validation.ok) {
std::cout << "Program203FirstReductionScenario firstFailure=none\n";
} else {
std::cout << "Program203FirstReductionScenario firstFailure=" << validation.phase << ": "
<< validation.message << '\n';
}
std::cout << "localIndex,localX,localY,localZ,subgroupSize,numSubgroups,subgroupID,laneID,input,"
"nativeInclusive,subgroupSubtotal,mergedPrefix,finalAverage\n";
for (std::size_t slot = 0; slot < kInvocationCount; ++slot) {
const UVec4& invocation = output.invocation[slot];
const UVec4& subgroup = output.subgroup[slot];
const Vec4& reduction = output.reduction[slot];
std::cout << invocation.x << ',' << invocation.y << ',' << invocation.z << ',' << invocation.w << ','
<< subgroup.x << ',' << subgroup.y << ',' << subgroup.z << ',' << subgroup.w << ','
<< std::hexfloat << reduction.x << ',' << reduction.y << ',' << reduction.z << ','
<< reduction.w << ',' << output.finalAverage[slot] << std::defaultfloat << '\n';
}
if (includeScanStages) {
std::cout << "scanStage,localIndex,scanAfter\n";
for (std::size_t scanStage = 0; scanStage < kScanStageCount; ++scanStage) {
for (std::size_t slot = 0; slot < kInvocationCount; ++slot) {
std::cout << scanStage << ',' << output.invocation[slot].x << ',' << std::hexfloat
<< output.scanAfter[scanStage][slot] << std::defaultfloat << '\n';
}
}
}
}
class Program203FirstReductionScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_capabilities = QueryCapabilities();
// GL_SUBGROUP_SIZE_KHR is diagnostic only. It is deliberately
// never used to infer lane placement or an expected group count.
PrintMetadata(m_capabilities, std::cout);
RecordProperty("program203_gl_subgroup_size_khr", std::to_string(m_capabilities.subgroupSize));
if (!m_capabilities.SupportsProbe()) {
GTEST_SKIP() << "subgroup probe requires " << m_capabilities.MissingRequirements();
}
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
if (m_texture != 0) glDeleteTextures(1, &m_texture);
if (m_inputBuffer != 0) glDeleteBuffers(1, &m_inputBuffer);
if (m_outputBuffer != 0) glDeleteBuffers(1, &m_outputBuffer);
if (m_program != 0) glDeleteProgram(m_program);
m_texture = 0;
m_inputBuffer = 0;
m_outputBuffer = 0;
m_program = 0;
}
GLuint CompileComputeProgram(const std::string& source, std::string* outError) {
const char* text = source.c_str();
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
if (shader == 0) {
*outError = "glCreateShader(GL_COMPUTE_SHADER) returned 0";
return 0;
}
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[8192] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
*outError = std::string("the subgroup probe compute shader did not compile: ") + log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[8192] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
*outError = std::string("the subgroup probe compute program did not link: ") + log;
glDeleteProgram(program);
return 0;
}
return program;
}
bool RunProbe(InputMode mode, ProbeOutput* output, std::string* outError) {
m_program = CompileComputeProgram(BuildProbeShader(mode), outError);
if (m_program == 0) return false;
ProbeOutput poison{};
std::memset(&poison, 0xa5, sizeof(poison));
glGenBuffers(1, &m_outputBuffer);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_outputBuffer);
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(ProbeOutput), &poison, GL_DYNAMIC_COPY);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_outputBuffer);
if (mode == InputMode::IndexedSsbo) {
std::array<float, kInvocationCount> values{};
for (std::size_t i = 0; i < values.size(); ++i) values[i] = static_cast<float>(i + 1u);
glGenBuffers(1, &m_inputBuffer);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_inputBuffer);
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(values), values.data(), GL_STATIC_DRAW);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_inputBuffer);
} else {
constexpr std::array<float, 4> kOneTexel = {1.0f, 1.0f, 1.0f, 1.0f};
glGenTextures(1, &m_texture);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, m_texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, 1, 1, 0, GL_RGBA, GL_FLOAT, kOneTexel.data());
}
if (const GLenum error = FirstGLError(); error != GL_NO_ERROR) {
std::ostringstream message;
message << "subgroup probe resource setup left " << GLErrorName(error);
*outError = message.str();
return false;
}
glUseProgram(m_program);
if (mode == InputMode::SampledRgba32f) {
const GLint sampler = glGetUniformLocation(m_program, "colortex2");
const GLint pixelSize = glGetUniformLocation(m_program, "pixelSize");
if (sampler == -1 || pixelSize == -1) {
*outError = "the sampled probe uniforms were optimized away or not reflected";
return false;
}
glUniform1i(sampler, 3);
glUniform2f(pixelSize, 1.0f / 854.0f, 1.0f / 480.0f);
}
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_outputBuffer);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(ProbeOutput), output);
if (const GLenum error = FirstGLError(); error != GL_NO_ERROR) {
std::ostringstream message;
message << "subgroup probe dispatch/readback left " << GLErrorName(error);
*outError = message.str();
return false;
}
return true;
}
void RunAndValidate(InputMode mode) {
ProbeOutput output{};
std::string error;
ASSERT_TRUE(RunProbe(mode, &output, &error)) << InputModeName(mode) << ": " << error;
const ValidationResult validation = ValidateProbe(output, mode);
if (validation.ownerEvaluated) {
RecordProperty("program203_index511_source_last_lane_writer",
validation.index511IsSourceLastLaneWriter ? "true" : "false");
RecordProperty("program203_index511_highest_subgroup_member",
validation.index511IsHighestSubgroupMember ? "true" : "false");
RecordProperty("program203_highest_observed_subgroup",
std::to_string(validation.highestObservedSubgroup));
std::cout << "Program203FirstReductionScenario owner: localIndex511 sourceLastLaneWriter="
<< validation.index511IsSourceLastLaneWriter << ", highestSubgroupMember="
<< validation.index511IsHighestSubgroupMember << ", highestObservedSubgroup="
<< validation.highestObservedSubgroup << '\n';
}
if (!validation.ok || DumpRequested()) {
DumpProbe(output, m_capabilities, validation, validation.scanStageMismatch || DumpRequested());
}
EXPECT_TRUE(validation.ok) << validation.phase << ": " << validation.message;
}
CapabilityInfo m_capabilities;
GLuint m_program = 0;
GLuint m_inputBuffer = 0;
GLuint m_outputBuffer = 0;
GLuint m_texture = 0;
};
} // namespace
TEST_F(Program203FirstReductionScenario, SampledRgba32fFirstAverage) {
if (!Ready() || IsSkipped()) return;
RunAndValidate(InputMode::SampledRgba32f);
}
TEST_F(Program203FirstReductionScenario, IndexedInputTopologyAndReduction) {
if (!Ready() || IsSkipped()) return;
RunAndValidate(InputMode::IndexedSsbo);
}
} // namespace MGITest
@@ -428,15 +428,15 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
std::vector<GLfloat> pixels(static_cast<size_t>(kWidth) * kHeight, 0.0f); std::vector<GLfloat> pixels(static_cast<size_t>(kWidth) * kHeight, 0.0f);
glReadPixels(0, 0, kWidth, kHeight, GL_RED, GL_FLOAT, pixels.data()); glReadPixels(0, 0, kWidth, kHeight, GL_RED, GL_FLOAT, pixels.data());
for (int i = 0; i < kViewportCount; ++i) { for (int i = 0; i < kViewportCount; ++i) {
const float nearDepth = static_cast<float>(i) / 16.0f; const float near = static_cast<float>(i) / 16.0f;
const float farDepth = 1.0f - static_cast<float>(i) / 16.0f; const float far = 1.0f - static_cast<float>(i) / 16.0f;
// The tolerance covers depth-buffer-free rasterization of gl_FragCoord.z on a // The tolerance covers depth-buffer-free rasterization of gl_FragCoord.z on a
// software rasterizer; the per-index values are 1/16 apart, so it cannot let a // software rasterizer; the per-index values are 1/16 apart, so it cannot let a
// neighbouring viewport's range through, and viewport 0's range (0, 1) differs // neighbouring viewport's range through, and viewport 0's range (0, 1) differs
// from every other index by at least 1/16. // from every other index by at least 1/16.
EXPECT_NEAR(pixels[i], nearDepth, 1.0e-3f) EXPECT_NEAR(pixels[i], near, 1.0e-3f)
<< "viewport " << i << " near-plane depth; got viewport 0's range if this is 0"; << "viewport " << i << " near-plane depth; got viewport 0's range if this is 0";
EXPECT_NEAR(pixels[static_cast<size_t>(kWidth) + i], farDepth, 1.0e-3f) EXPECT_NEAR(pixels[static_cast<size_t>(kWidth) + i], far, 1.0e-3f)
<< "viewport " << i << " far-plane depth; got viewport 0's range if this is 1"; << "viewport " << i << " far-plane depth; got viewport 0's range if this is 1";
} }
@@ -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
+3 -1
View File
@@ -78,10 +78,12 @@ add_subdirectory(Query)
add_subdirectory(Pipeline) add_subdirectory(Pipeline)
add_subdirectory(ShaderTranspiler) add_subdirectory(ShaderTranspiler)
add_subdirectory(Util) add_subdirectory(Util)
add_subdirectory(SelfTest)
# The DirectGLES post-transpile ESSL passes are pure String -> String, so unlike the # The DirectGLES post-transpile ESSL passes are pure String -> String, so unlike the
# DirectVulkan suite below this one needs no device and always builds. # DirectVulkan suite below this one needs no device and always builds.
add_subdirectory(Backend/DirectGLES) add_subdirectory(Backend/DirectGLES)
if (ENABLE_INTEGRATION_TESTS) if (ENABLE_INTEGRATION_TESTS)
add_subdirectory(Backend/DirectVulkan) add_subdirectory(Backend/DirectVulkan)
endif() endif()
if (MOBILEGL_ENABLE_DILIGENT)
add_subdirectory(Backend/Diligent)
endif()
-10
View File
@@ -2387,13 +2387,3 @@ TEST(DirectGLESTextureSync, UnitMemoRefusesToDriveATwinFromAnotherTexture) {
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
} }
TEST(DirectVulkanSanity, GraphicsSamplerFeedbackOnlyAliasesWritableOverlappingMip) {
using MobileGL::MG_Backend::DirectVulkan::UniformManager;
EXPECT_TRUE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 2, GL_WRITE_ONLY));
EXPECT_TRUE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 3, GL_READ_WRITE));
EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 2, GL_READ_ONLY));
EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 0, GL_WRITE_ONLY));
EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 4, GL_WRITE_ONLY));
}
-19
View File
@@ -1,19 +0,0 @@
# MobileGL - MobileGL/MG_Test/SelfTest/CMakeLists.txt
add_executable(
DriverPostProgram203WitnessTest
DriverPostProgram203WitnessTest.cpp
)
target_include_directories(DriverPostProgram203WitnessTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(DriverPostProgram203WitnessTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
include(GoogleTest)
gtest_discover_tests(DriverPostProgram203WitnessTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
@@ -1,188 +0,0 @@
// MobileGL - MobileGL/MG_Test/SelfTest/DriverPostProgram203WitnessTest.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#include <string>
#include "MG_Util/SelfTest/DriverPostProgram203Witness.h"
namespace MobileGL::MG_Util::SelfTest {
namespace {
Program203WitnessOutput MakeValidWitness(std::uint32_t numSubgroups) {
Program203WitnessOutput output{};
output.magic = kProgram203WitnessMagic;
output.numSubgroups = numSubgroups;
output.loopLength = ComputeProgram203WitnessLoopLength(numSubgroups);
output.seenSubgroupMask =
numSubgroups == kProgram203WitnessMaxSubgroups ? 0xffffffffu : (1u << numSubgroups) - 1u;
// Valid test layouts use equal contiguous groups of the indexed
// 1..512 input. The compact witness only needs their independent sums.
const std::uint32_t subgroupSize = kProgram203WitnessInvocationCount / numSubgroups;
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
const std::uint32_t first = subgroup * subgroupSize + 1u;
const std::uint32_t last = first + subgroupSize - 1u;
output.lastLaneWriterCount[subgroup] = 1u;
output.indexedInputTotal[subgroup] = subgroupSize * (first + last) / 2u;
output.rawPrefix[subgroup] = {static_cast<float>(output.indexedInputTotal[subgroup]), 0.0f};
}
output.owner511 = {subgroupSize, numSubgroups, numSubgroups - 1u, subgroupSize - 1u};
auto cache = output.rawPrefix;
for (std::uint32_t scanStage = 0u; scanStage < output.loopLength; ++scanStage) {
auto cacheAfterStage = cache;
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
if ((subgroup & (1u << scanStage)) == 0u) continue;
const std::uint32_t sourceCacheIndex = (subgroup >> scanStage << scanStage) - 1u;
cacheAfterStage[subgroup].x += cache[sourceCacheIndex].x;
cacheAfterStage[subgroup].y += cache[sourceCacheIndex].y;
}
cache = cacheAfterStage;
output.scanCache[scanStage] = cache;
}
output.finalAverage = {256.5f, 0.0f};
return output;
}
Program203WitnessLimits MakeSufficientLimits() {
Program203WitnessLimits limits;
limits.computeStageSupported = true;
limits.basicSubgroupSupported = true;
limits.arithmeticSubgroupSupported = true;
limits.subgroupSize = 32u;
limits.maxComputeWorkGroupInvocations = kProgram203WitnessInvocationCount;
limits.maxComputeWorkGroupSize = {32u, 16u, 1u};
limits.maxComputeSharedMemorySize = kProgram203WitnessSharedMemoryBytes;
limits.maxPerStageDescriptorStorageBuffers = 1u;
limits.maxDescriptorSetStorageBuffers = 1u;
limits.maxBoundDescriptorSets = 1u;
limits.maxStorageBufferRange = sizeof(Program203WitnessOutput);
return limits;
}
} // namespace
TEST(DriverPostProgram203WitnessTest, ValidTwoSubgroupWitness) {
const Program203WitnessValidationResult validation = ValidateProgram203Witness(MakeValidWitness(2u));
ASSERT_TRUE(validation.ok) << validation.detail;
EXPECT_EQ(validation.detail, "N=2, owner511=id1/lane255, 2 scan stages, average=(256.5,0)");
}
TEST(DriverPostProgram203WitnessTest, ValidThirtyTwoSubgroupWitness) {
const Program203WitnessValidationResult validation = ValidateProgram203Witness(MakeValidWitness(32u));
ASSERT_TRUE(validation.ok) << validation.detail;
EXPECT_EQ(validation.detail, "N=32, owner511=id31/lane15, 6 scan stages, average=(256.5,0)");
}
TEST(DriverPostProgram203WitnessTest, RejectsNonuniformNumSubgroups) {
Program203WitnessOutput output = MakeValidWitness(16u);
output.topologyFlags |= Program203WitnessNonuniformNumSubgroups;
const Program203WitnessValidationResult validation = ValidateProgram203Witness(output);
EXPECT_FALSE(validation.ok);
EXPECT_EQ(validation.failure, Program203WitnessValidationFailure::Topology);
EXPECT_NE(validation.detail.find("gl_NumSubgroups differed"), std::string::npos);
}
TEST(DriverPostProgram203WitnessTest, RejectsMissingAndOutOfRangeSubgroupIds) {
Program203WitnessOutput missing = MakeValidWitness(16u);
missing.seenSubgroupMask &= ~(1u << 7u);
Program203WitnessValidationResult validation = ValidateProgram203Witness(missing);
EXPECT_FALSE(validation.ok);
EXPECT_NE(validation.detail.find("seen subgroup-ID mask"), std::string::npos);
Program203WitnessOutput outOfRange = MakeValidWitness(16u);
outOfRange.topologyFlags |= Program203WitnessInvalidSubgroupId;
validation = ValidateProgram203Witness(outOfRange);
EXPECT_FALSE(validation.ok);
EXPECT_NE(validation.detail.find("invalid gl_SubgroupID"), std::string::npos);
}
TEST(DriverPostProgram203WitnessTest, RejectsInvalidMultipleAndMissingLastLaneWriters) {
Program203WitnessOutput invalidLane = MakeValidWitness(16u);
invalidLane.topologyFlags |= Program203WitnessInvalidSubgroupLane;
Program203WitnessValidationResult validation = ValidateProgram203Witness(invalidLane);
EXPECT_FALSE(validation.ok);
EXPECT_NE(validation.detail.find("invalid subgroup lane"), std::string::npos);
Program203WitnessOutput multiple = MakeValidWitness(16u);
multiple.lastLaneWriterCount[4] = 2u;
validation = ValidateProgram203Witness(multiple);
EXPECT_FALSE(validation.ok);
EXPECT_NE(validation.detail.find("subgroup 4 has 2 source last-lane writers"), std::string::npos);
Program203WitnessOutput missing = MakeValidWitness(16u);
missing.lastLaneWriterCount[6] = 0u;
validation = ValidateProgram203Witness(missing);
EXPECT_FALSE(validation.ok);
EXPECT_NE(validation.detail.find("subgroup 6 has 0 source last-lane writers"), std::string::npos);
}
TEST(DriverPostProgram203WitnessTest, ReportsEarliestCorruptSourceScanStage) {
Program203WitnessOutput output = MakeValidWitness(32u);
output.scanCache[0][1].x += 1.0f;
output.scanCache[3][5].x += 1.0f;
Program203WitnessValidationResult validation = ValidateProgram203Witness(output);
EXPECT_FALSE(validation.ok);
EXPECT_EQ(validation.failure, Program203WitnessValidationFailure::SourceScan);
EXPECT_EQ(validation.scanStage, 0u);
EXPECT_NE(validation.detail.find("source scan stage 0, subgroup 1"), std::string::npos);
output = MakeValidWitness(32u);
output.scanCache[3][5].x += 1.0f;
validation = ValidateProgram203Witness(output);
EXPECT_FALSE(validation.ok);
EXPECT_EQ(validation.failure, Program203WitnessValidationFailure::SourceScan);
EXPECT_EQ(validation.scanStage, 3u);
EXPECT_NE(validation.detail.find("source scan stage 3, subgroup 5"), std::string::npos);
}
TEST(DriverPostProgram203WitnessTest, RejectsOwner511OutsideHighestFinalLane) {
Program203WitnessOutput output = MakeValidWitness(16u);
output.owner511.z = 14u;
const Program203WitnessValidationResult validation = ValidateProgram203Witness(output);
EXPECT_FALSE(validation.ok);
EXPECT_EQ(validation.failure, Program203WitnessValidationFailure::FinalOwner);
EXPECT_NE(validation.detail.find("not in the highest subgroup"), std::string::npos);
}
TEST(DriverPostProgram203WitnessTest, RejectsIncorrectVectorFinalAverage) {
Program203WitnessOutput output = MakeValidWitness(16u);
output.finalAverage.y = 1.0f;
const Program203WitnessValidationResult validation = ValidateProgram203Witness(output);
EXPECT_FALSE(validation.ok);
EXPECT_EQ(validation.failure, Program203WitnessValidationFailure::FinalAverage);
EXPECT_NE(validation.detail.find("final average"), std::string::npos);
}
TEST(DriverPostProgram203WitnessTest, MissingNativeFeatureIsTheOnlySkipCondition) {
for (const auto toggleMissingFeature : {0u, 1u, 2u}) {
Program203WitnessLimits limits = MakeSufficientLimits();
if (toggleMissingFeature == 0u) limits.computeStageSupported = false;
if (toggleMissingFeature == 1u) limits.basicSubgroupSupported = false;
if (toggleMissingFeature == 2u) limits.arithmeticSubgroupSupported = false;
const Program203WitnessEligibilityResult eligibility = EvaluateProgram203WitnessEligibility(limits);
EXPECT_EQ(eligibility.eligibility, Program203WitnessEligibility::SkipUnsupportedNativeFeatureSet)
<< eligibility.detail;
}
Program203WitnessLimits zeroSubgroupSize = MakeSufficientLimits();
zeroSubgroupSize.subgroupSize = 0u;
Program203WitnessEligibilityResult eligibility = EvaluateProgram203WitnessEligibility(zeroSubgroupSize);
EXPECT_EQ(eligibility.eligibility, Program203WitnessEligibility::FailInadequateLimits) << eligibility.detail;
Program203WitnessLimits limits = MakeSufficientLimits();
limits.maxComputeWorkGroupInvocations = 511u;
eligibility = EvaluateProgram203WitnessEligibility(limits);
EXPECT_EQ(eligibility.eligibility, Program203WitnessEligibility::FailInadequateLimits) << eligibility.detail;
limits = MakeSufficientLimits();
limits.maxStorageBufferRange = sizeof(Program203WitnessOutput) - 1u;
eligibility = EvaluateProgram203WitnessEligibility(limits);
EXPECT_EQ(eligibility.eligibility, Program203WitnessEligibility::FailInadequateLimits) << eligibility.detail;
}
} // namespace MobileGL::MG_Util::SelfTest
@@ -3,7 +3,6 @@ cmake_minimum_required(VERSION 3.14)
add_executable( add_executable(
SpirvPassTest SpirvPassTest
SpirvPassTest.cpp SpirvPassTest.cpp
DeriveNumSubgroupsTest.cpp
DemoteFloat64Test.cpp DemoteFloat64Test.cpp
FlattenXfbInterfaceBlocksTest.cpp FlattenXfbInterfaceBlocksTest.cpp
) )
@@ -1,147 +0,0 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/DeriveNumSubgroupsTest.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#define SPV_ENABLE_UTILITY_CODE
#include "glslang/SPIRV/spirv.hpp11"
#undef SPV_ENABLE_UTILITY_CODE
#include "Includes.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
constexpr SizeT kSpirvHeaderWordCount = 5u;
template <typename Visitor>
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
const Uint32 wordCount = spirv[offset] >> 16u;
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
offset += wordCount;
}
}
Vector<Uint32> CompileCompute(const String& source) {
using namespace MobileGL::MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .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 = {GL_COMPUTE_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();
}
Uint32 FindBuiltinTarget(const Vector<Uint32>& spirv, spv::BuiltIn builtin) {
Uint32 target = 0u;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == spv::Op::OpDecorate && wordCount >= 4u &&
static_cast<spv::Decoration>(words[2]) == spv::Decoration::BuiltIn &&
static_cast<spv::BuiltIn>(words[3]) == builtin) {
target = words[1];
}
});
return target;
}
Uint32 CountLoadsFrom(const Vector<Uint32>& spirv, Uint32 pointerId) {
Uint32 count = 0u;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == spv::Op::OpLoad && wordCount >= 4u && words[3] == pointerId) ++count;
});
return count;
}
Uint32 CountOpcode(const Vector<Uint32>& spirv, spv::Op wanted) {
Uint32 count = 0u;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32*, Uint32) {
if (opcode == wanted) ++count;
});
return count;
}
bool Validates(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
return tools.Validate(spirv);
}
constexpr const char* kNumSubgroupsOnlySource = R"(#version 450 core
#extension GL_KHR_shader_subgroup_basic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { uint value; } outputData;
void main() {
if (gl_LocalInvocationIndex == 0u)
outputData.value = gl_NumSubgroups;
}
)";
constexpr const char* kNoNumSubgroupsSource = R"(#version 450 core
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 0) buffer Output { uint value; } outputData;
void main() {
if (gl_LocalInvocationIndex == 0u)
outputData.value = gl_WorkGroupSize.x;
}
)";
} // namespace
TEST(DeriveNumSubgroupsPass, ReplacesBuiltinLoadAndSynthesizesSubgroupSize) {
const Vector<Uint32> input = CompileCompute(kNumSubgroupsOnlySource);
ASSERT_FALSE(input.empty());
const Uint32 inputNumSubgroups = FindBuiltinTarget(input, spv::BuiltIn::NumSubgroups);
ASSERT_NE(inputNumSubgroups, 0u);
EXPECT_EQ(CountLoadsFrom(input, inputNumSubgroups), 1u);
EXPECT_EQ(FindBuiltinTarget(input, spv::BuiltIn::SubgroupSize), 0u);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(input, output, true));
ASSERT_TRUE(Validates(output));
const Uint32 outputNumSubgroups = FindBuiltinTarget(output, spv::BuiltIn::NumSubgroups);
const Uint32 outputSubgroupSize = FindBuiltinTarget(output, spv::BuiltIn::SubgroupSize);
ASSERT_NE(outputNumSubgroups, 0u);
ASSERT_NE(outputSubgroupSize, 0u);
EXPECT_EQ(CountLoadsFrom(output, outputNumSubgroups), 0u);
EXPECT_EQ(CountLoadsFrom(output, outputSubgroupSize), 1u);
EXPECT_EQ(CountOpcode(output, spv::Op::OpCompositeExtract), 3u);
EXPECT_EQ(CountOpcode(output, spv::Op::OpIMul), 2u);
EXPECT_EQ(CountOpcode(output, spv::Op::OpUDiv), 1u);
}
TEST(DeriveNumSubgroupsPass, IsIdempotent) {
Vector<Uint32> once;
ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(CompileCompute(kNumSubgroupsOnlySource), once, true));
Vector<Uint32> twice;
ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(once, twice, true));
EXPECT_EQ(twice, once);
}
TEST(DeriveNumSubgroupsPass, LeavesUnrelatedComputeShaderUntouched) {
const Vector<Uint32> input = CompileCompute(kNoNumSubgroupsSource);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DeriveNumSubgroupsForVulkan(input, output, true));
EXPECT_EQ(output, input);
EXPECT_EQ(FindBuiltinTarget(output, spv::BuiltIn::SubgroupSize), 0u);
}
+1 -449
View File
@@ -7,8 +7,6 @@
// End of Source File Header // End of Source File Header
#include "DriverPost.h" #include "DriverPost.h"
#include "DriverPostProgram203Witness.h"
#include "DriverPostProgram203WitnessSpv.h"
#include "MG_Util/BackendLoaders/OpenGL/Loader.h" #include "MG_Util/BackendLoaders/OpenGL/Loader.h"
#include <Config.h> #include <Config.h>
#include <MGGitHash.h> #include <MGGitHash.h>
@@ -26,8 +24,6 @@
#include <MG_Util/Texture/TextureFormatProcessor.h> #include <MG_Util/Texture/TextureFormatProcessor.h>
#include <MG_Util/Async/ShaderCompilePool.h> #include <MG_Util/Async/ShaderCompilePool.h>
#include <chrono> #include <chrono>
#include <cstring>
#include <limits>
#include <thread> #include <thread>
#if !defined(_WIN32) #if !defined(_WIN32)
@@ -1458,437 +1454,6 @@ namespace MobileGL::MG_Util::SelfTest {
disabledNote); disabledNote);
} }
// Native Program-203 compute witness. This deliberately uses a separate
// throwaway Vulkan device rather than the real renderer's queues, and it
// treats MOBILEGL_DISABLE_SUBGROUP as irrelevant: the row reports what the
// driver does, not what MobileGL elects to advertise to applications.
void ProbeVulkanProgram203Witness(ReportBuilder& builder, PFN_vkGetInstanceProcAddr getInstanceProcAddr,
VkInstance instance, VkPhysicalDevice physicalDevice,
Uint32 computeQueueFamilyIndex,
const VkPhysicalDeviceProperties& properties,
Bool subgroupPropertiesAvailable,
const VkPhysicalDeviceSubgroupProperties& subgroupProperties) {
constexpr const char* RowName = "Subgroup first-reduction witness";
const auto fail = [&](String detail) { builder.Fail(RowName, Move(detail)); };
if (!subgroupPropertiesAvailable) {
fail("vkGetPhysicalDeviceProperties2 could not provide raw Vulkan subgroup properties");
return;
}
Program203WitnessLimits limits{};
limits.computeStageSupported =
(subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0;
limits.basicSubgroupSupported =
(subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0;
limits.arithmeticSubgroupSupported =
(subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_ARITHMETIC_BIT) != 0;
limits.subgroupSize = subgroupProperties.subgroupSize;
limits.maxComputeWorkGroupInvocations = properties.limits.maxComputeWorkGroupInvocations;
limits.maxComputeWorkGroupSize = {properties.limits.maxComputeWorkGroupSize[0],
properties.limits.maxComputeWorkGroupSize[1],
properties.limits.maxComputeWorkGroupSize[2]};
limits.maxComputeSharedMemorySize = properties.limits.maxComputeSharedMemorySize;
limits.maxPerStageDescriptorStorageBuffers = properties.limits.maxPerStageDescriptorStorageBuffers;
limits.maxDescriptorSetStorageBuffers = properties.limits.maxDescriptorSetStorageBuffers;
limits.maxBoundDescriptorSets = properties.limits.maxBoundDescriptorSets;
limits.maxStorageBufferRange = properties.limits.maxStorageBufferRange;
const Program203WitnessEligibilityResult eligibility = EvaluateProgram203WitnessEligibility(limits);
if (eligibility.eligibility == Program203WitnessEligibility::SkipUnsupportedNativeFeatureSet) {
builder.Info(RowName, eligibility.detail);
return;
}
if (eligibility.eligibility == Program203WitnessEligibility::FailInadequateLimits) {
fail(eligibility.detail);
return;
}
if (computeQueueFamilyIndex == std::numeric_limits<Uint32>::max()) {
fail("no compute queue family is available for the native Vulkan witness");
return;
}
const auto vkGetPhysicalDeviceMemoryPropertiesFn =
reinterpret_cast<PFN_vkGetPhysicalDeviceMemoryProperties>(
getInstanceProcAddr(instance, "vkGetPhysicalDeviceMemoryProperties"));
const auto vkCreateDeviceFn =
reinterpret_cast<PFN_vkCreateDevice>(getInstanceProcAddr(instance, "vkCreateDevice"));
const auto vkDestroyDeviceFn =
reinterpret_cast<PFN_vkDestroyDevice>(getInstanceProcAddr(instance, "vkDestroyDevice"));
const auto vkGetDeviceQueueFn =
reinterpret_cast<PFN_vkGetDeviceQueue>(getInstanceProcAddr(instance, "vkGetDeviceQueue"));
const auto vkCreateBufferFn =
reinterpret_cast<PFN_vkCreateBuffer>(getInstanceProcAddr(instance, "vkCreateBuffer"));
const auto vkDestroyBufferFn =
reinterpret_cast<PFN_vkDestroyBuffer>(getInstanceProcAddr(instance, "vkDestroyBuffer"));
const auto vkGetBufferMemoryRequirementsFn = reinterpret_cast<PFN_vkGetBufferMemoryRequirements>(
getInstanceProcAddr(instance, "vkGetBufferMemoryRequirements"));
const auto vkAllocateMemoryFn =
reinterpret_cast<PFN_vkAllocateMemory>(getInstanceProcAddr(instance, "vkAllocateMemory"));
const auto vkFreeMemoryFn =
reinterpret_cast<PFN_vkFreeMemory>(getInstanceProcAddr(instance, "vkFreeMemory"));
const auto vkBindBufferMemoryFn =
reinterpret_cast<PFN_vkBindBufferMemory>(getInstanceProcAddr(instance, "vkBindBufferMemory"));
const auto vkMapMemoryFn =
reinterpret_cast<PFN_vkMapMemory>(getInstanceProcAddr(instance, "vkMapMemory"));
const auto vkUnmapMemoryFn =
reinterpret_cast<PFN_vkUnmapMemory>(getInstanceProcAddr(instance, "vkUnmapMemory"));
const auto vkCreateDescriptorSetLayoutFn = reinterpret_cast<PFN_vkCreateDescriptorSetLayout>(
getInstanceProcAddr(instance, "vkCreateDescriptorSetLayout"));
const auto vkDestroyDescriptorSetLayoutFn = reinterpret_cast<PFN_vkDestroyDescriptorSetLayout>(
getInstanceProcAddr(instance, "vkDestroyDescriptorSetLayout"));
const auto vkCreateDescriptorPoolFn =
reinterpret_cast<PFN_vkCreateDescriptorPool>(getInstanceProcAddr(instance, "vkCreateDescriptorPool"));
const auto vkDestroyDescriptorPoolFn = reinterpret_cast<PFN_vkDestroyDescriptorPool>(
getInstanceProcAddr(instance, "vkDestroyDescriptorPool"));
const auto vkAllocateDescriptorSetsFn = reinterpret_cast<PFN_vkAllocateDescriptorSets>(
getInstanceProcAddr(instance, "vkAllocateDescriptorSets"));
const auto vkUpdateDescriptorSetsFn =
reinterpret_cast<PFN_vkUpdateDescriptorSets>(getInstanceProcAddr(instance, "vkUpdateDescriptorSets"));
const auto vkCreateShaderModuleFn =
reinterpret_cast<PFN_vkCreateShaderModule>(getInstanceProcAddr(instance, "vkCreateShaderModule"));
const auto vkDestroyShaderModuleFn =
reinterpret_cast<PFN_vkDestroyShaderModule>(getInstanceProcAddr(instance, "vkDestroyShaderModule"));
const auto vkCreatePipelineLayoutFn =
reinterpret_cast<PFN_vkCreatePipelineLayout>(getInstanceProcAddr(instance, "vkCreatePipelineLayout"));
const auto vkDestroyPipelineLayoutFn = reinterpret_cast<PFN_vkDestroyPipelineLayout>(
getInstanceProcAddr(instance, "vkDestroyPipelineLayout"));
const auto vkCreateComputePipelinesFn = reinterpret_cast<PFN_vkCreateComputePipelines>(
getInstanceProcAddr(instance, "vkCreateComputePipelines"));
const auto vkDestroyPipelineFn =
reinterpret_cast<PFN_vkDestroyPipeline>(getInstanceProcAddr(instance, "vkDestroyPipeline"));
const auto vkCreateCommandPoolFn =
reinterpret_cast<PFN_vkCreateCommandPool>(getInstanceProcAddr(instance, "vkCreateCommandPool"));
const auto vkDestroyCommandPoolFn =
reinterpret_cast<PFN_vkDestroyCommandPool>(getInstanceProcAddr(instance, "vkDestroyCommandPool"));
const auto vkAllocateCommandBuffersFn = reinterpret_cast<PFN_vkAllocateCommandBuffers>(
getInstanceProcAddr(instance, "vkAllocateCommandBuffers"));
const auto vkBeginCommandBufferFn =
reinterpret_cast<PFN_vkBeginCommandBuffer>(getInstanceProcAddr(instance, "vkBeginCommandBuffer"));
const auto vkEndCommandBufferFn =
reinterpret_cast<PFN_vkEndCommandBuffer>(getInstanceProcAddr(instance, "vkEndCommandBuffer"));
const auto vkCmdBindPipelineFn =
reinterpret_cast<PFN_vkCmdBindPipeline>(getInstanceProcAddr(instance, "vkCmdBindPipeline"));
const auto vkCmdBindDescriptorSetsFn = reinterpret_cast<PFN_vkCmdBindDescriptorSets>(
getInstanceProcAddr(instance, "vkCmdBindDescriptorSets"));
const auto vkCmdDispatchFn =
reinterpret_cast<PFN_vkCmdDispatch>(getInstanceProcAddr(instance, "vkCmdDispatch"));
const auto vkCmdPipelineBarrierFn =
reinterpret_cast<PFN_vkCmdPipelineBarrier>(getInstanceProcAddr(instance, "vkCmdPipelineBarrier"));
const auto vkCreateFenceFn =
reinterpret_cast<PFN_vkCreateFence>(getInstanceProcAddr(instance, "vkCreateFence"));
const auto vkDestroyFenceFn =
reinterpret_cast<PFN_vkDestroyFence>(getInstanceProcAddr(instance, "vkDestroyFence"));
const auto vkQueueSubmitFn =
reinterpret_cast<PFN_vkQueueSubmit>(getInstanceProcAddr(instance, "vkQueueSubmit"));
const auto vkWaitForFencesFn =
reinterpret_cast<PFN_vkWaitForFences>(getInstanceProcAddr(instance, "vkWaitForFences"));
const auto vkDeviceWaitIdleFn =
reinterpret_cast<PFN_vkDeviceWaitIdle>(getInstanceProcAddr(instance, "vkDeviceWaitIdle"));
if (vkGetPhysicalDeviceMemoryPropertiesFn == nullptr || vkCreateDeviceFn == nullptr ||
vkDestroyDeviceFn == nullptr || vkGetDeviceQueueFn == nullptr || vkCreateBufferFn == nullptr ||
vkDestroyBufferFn == nullptr || vkGetBufferMemoryRequirementsFn == nullptr ||
vkAllocateMemoryFn == nullptr || vkFreeMemoryFn == nullptr || vkBindBufferMemoryFn == nullptr ||
vkMapMemoryFn == nullptr || vkUnmapMemoryFn == nullptr || vkCreateDescriptorSetLayoutFn == nullptr ||
vkDestroyDescriptorSetLayoutFn == nullptr || vkCreateDescriptorPoolFn == nullptr ||
vkDestroyDescriptorPoolFn == nullptr || vkAllocateDescriptorSetsFn == nullptr ||
vkUpdateDescriptorSetsFn == nullptr || vkCreateShaderModuleFn == nullptr ||
vkDestroyShaderModuleFn == nullptr || vkCreatePipelineLayoutFn == nullptr ||
vkDestroyPipelineLayoutFn == nullptr || vkCreateComputePipelinesFn == nullptr ||
vkDestroyPipelineFn == nullptr || vkCreateCommandPoolFn == nullptr || vkDestroyCommandPoolFn == nullptr ||
vkAllocateCommandBuffersFn == nullptr || vkBeginCommandBufferFn == nullptr ||
vkEndCommandBufferFn == nullptr || vkCmdBindPipelineFn == nullptr ||
vkCmdBindDescriptorSetsFn == nullptr || vkCmdDispatchFn == nullptr ||
vkCmdPipelineBarrierFn == nullptr || vkCreateFenceFn == nullptr || vkDestroyFenceFn == nullptr ||
vkQueueSubmitFn == nullptr || vkWaitForFencesFn == nullptr || vkDeviceWaitIdleFn == nullptr) {
fail("vkGetInstanceProcAddr could not resolve the Vulkan entry points required for the witness");
return;
}
const Float queuePriority = 1.0f;
VkDeviceQueueCreateInfo queueInfo{};
queueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueInfo.queueFamilyIndex = computeQueueFamilyIndex;
queueInfo.queueCount = 1;
queueInfo.pQueuePriorities = &queuePriority;
VkDeviceCreateInfo deviceInfo{};
deviceInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
deviceInfo.queueCreateInfoCount = 1;
deviceInfo.pQueueCreateInfos = &queueInfo;
VkDevice device = VK_NULL_HANDLE;
VkResult result = vkCreateDeviceFn(physicalDevice, &deviceInfo, nullptr, &device);
if (result != VK_SUCCESS || device == VK_NULL_HANDLE) {
fail(format("vkCreateDevice failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkBuffer outputBuffer = VK_NULL_HANDLE;
VkDeviceMemory outputMemory = VK_NULL_HANDLE;
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
VkShaderModule shaderModule = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkPipeline pipeline = VK_NULL_HANDLE;
VkCommandPool commandPool = VK_NULL_HANDLE;
VkFence fence = VK_NULL_HANDLE;
void* mappedOutput = nullptr;
Bool fenceWaitTimedOut = false;
const ScopeGuard destroyDeviceObjects([&]() {
if (fenceWaitTimedOut) {
// Match ProbeVulkanTimerQuery: the command may still execute
// after a timeout, so intentionally retain every device-owned
// resource rather than risking a forever wait or UAF in the ICD.
return;
}
vkDeviceWaitIdleFn(device);
if (fence != VK_NULL_HANDLE) vkDestroyFenceFn(device, fence, nullptr);
if (commandPool != VK_NULL_HANDLE) vkDestroyCommandPoolFn(device, commandPool, nullptr);
if (pipeline != VK_NULL_HANDLE) vkDestroyPipelineFn(device, pipeline, nullptr);
if (pipelineLayout != VK_NULL_HANDLE) vkDestroyPipelineLayoutFn(device, pipelineLayout, nullptr);
if (shaderModule != VK_NULL_HANDLE) vkDestroyShaderModuleFn(device, shaderModule, nullptr);
if (descriptorPool != VK_NULL_HANDLE) vkDestroyDescriptorPoolFn(device, descriptorPool, nullptr);
if (descriptorSetLayout != VK_NULL_HANDLE) {
vkDestroyDescriptorSetLayoutFn(device, descriptorSetLayout, nullptr);
}
if (mappedOutput != nullptr) vkUnmapMemoryFn(device, outputMemory);
if (outputBuffer != VK_NULL_HANDLE) vkDestroyBufferFn(device, outputBuffer, nullptr);
if (outputMemory != VK_NULL_HANDLE) vkFreeMemoryFn(device, outputMemory, nullptr);
vkDestroyDeviceFn(device, nullptr);
});
VkQueue queue = VK_NULL_HANDLE;
vkGetDeviceQueueFn(device, computeQueueFamilyIndex, 0, &queue);
if (queue == VK_NULL_HANDLE) {
fail("vkGetDeviceQueue returned a null compute queue");
return;
}
VkBufferCreateInfo bufferInfo{};
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferInfo.size = sizeof(Program203WitnessOutput);
bufferInfo.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
result = vkCreateBufferFn(device, &bufferInfo, nullptr, &outputBuffer);
if (result != VK_SUCCESS) {
fail(format("vkCreateBuffer(output SSBO) failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkMemoryRequirements memoryRequirements{};
vkGetBufferMemoryRequirementsFn(device, outputBuffer, &memoryRequirements);
VkPhysicalDeviceMemoryProperties memoryProperties{};
vkGetPhysicalDeviceMemoryPropertiesFn(physicalDevice, &memoryProperties);
Uint32 memoryTypeIndex = std::numeric_limits<Uint32>::max();
for (Uint32 index = 0; index < memoryProperties.memoryTypeCount; ++index) {
const Bool compatible = (memoryRequirements.memoryTypeBits & (1u << index)) != 0u;
const VkMemoryPropertyFlags required = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
if (compatible && (memoryProperties.memoryTypes[index].propertyFlags & required) == required) {
memoryTypeIndex = index;
break;
}
}
if (memoryTypeIndex == std::numeric_limits<Uint32>::max()) {
fail("no host-visible/coherent memory type is compatible with the output SSBO");
return;
}
VkMemoryAllocateInfo memoryInfo{};
memoryInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
memoryInfo.allocationSize = memoryRequirements.size;
memoryInfo.memoryTypeIndex = memoryTypeIndex;
result = vkAllocateMemoryFn(device, &memoryInfo, nullptr, &outputMemory);
if (result != VK_SUCCESS) {
fail(format("vkAllocateMemory(output SSBO) failed (VkResult = {})", static_cast<Int>(result)));
return;
}
result = vkBindBufferMemoryFn(device, outputBuffer, outputMemory, 0);
if (result != VK_SUCCESS) {
fail(format("vkBindBufferMemory(output SSBO) failed (VkResult = {})", static_cast<Int>(result)));
return;
}
result = vkMapMemoryFn(device, outputMemory, 0, sizeof(Program203WitnessOutput), 0, &mappedOutput);
if (result != VK_SUCCESS || mappedOutput == nullptr) {
fail(format("vkMapMemory(output SSBO) failed (VkResult = {})", static_cast<Int>(result)));
return;
}
std::memset(mappedOutput, 0xa5, sizeof(Program203WitnessOutput));
VkDescriptorSetLayoutBinding outputBinding{};
outputBinding.binding = 0;
outputBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
outputBinding.descriptorCount = 1;
outputBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
VkDescriptorSetLayoutCreateInfo descriptorSetLayoutInfo{};
descriptorSetLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
descriptorSetLayoutInfo.bindingCount = 1;
descriptorSetLayoutInfo.pBindings = &outputBinding;
result = vkCreateDescriptorSetLayoutFn(device, &descriptorSetLayoutInfo, nullptr, &descriptorSetLayout);
if (result != VK_SUCCESS) {
fail(format("vkCreateDescriptorSetLayout failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkDescriptorPoolSize poolSize{};
poolSize.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
poolSize.descriptorCount = 1;
VkDescriptorPoolCreateInfo descriptorPoolInfo{};
descriptorPoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
descriptorPoolInfo.maxSets = 1;
descriptorPoolInfo.poolSizeCount = 1;
descriptorPoolInfo.pPoolSizes = &poolSize;
result = vkCreateDescriptorPoolFn(device, &descriptorPoolInfo, nullptr, &descriptorPool);
if (result != VK_SUCCESS) {
fail(format("vkCreateDescriptorPool failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
VkDescriptorSetAllocateInfo descriptorSetInfo{};
descriptorSetInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
descriptorSetInfo.descriptorPool = descriptorPool;
descriptorSetInfo.descriptorSetCount = 1;
descriptorSetInfo.pSetLayouts = &descriptorSetLayout;
result = vkAllocateDescriptorSetsFn(device, &descriptorSetInfo, &descriptorSet);
if (result != VK_SUCCESS) {
fail(format("vkAllocateDescriptorSets failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkDescriptorBufferInfo outputDescriptor{};
outputDescriptor.buffer = outputBuffer;
outputDescriptor.offset = 0;
outputDescriptor.range = sizeof(Program203WitnessOutput);
VkWriteDescriptorSet descriptorWrite{};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = descriptorSet;
descriptorWrite.dstBinding = 0;
descriptorWrite.descriptorCount = 1;
descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
descriptorWrite.pBufferInfo = &outputDescriptor;
vkUpdateDescriptorSetsFn(device, 1, &descriptorWrite, 0, nullptr);
VkShaderModuleCreateInfo shaderModuleInfo{};
shaderModuleInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
shaderModuleInfo.codeSize = sizeof(kDriverPostProgram203WitnessSpv);
shaderModuleInfo.pCode = kDriverPostProgram203WitnessSpv;
result = vkCreateShaderModuleFn(device, &shaderModuleInfo, nullptr, &shaderModule);
if (result != VK_SUCCESS) {
fail(format("vkCreateShaderModule failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
result = vkCreatePipelineLayoutFn(device, &pipelineLayoutInfo, nullptr, &pipelineLayout);
if (result != VK_SUCCESS) {
fail(format("vkCreatePipelineLayout failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkPipelineShaderStageCreateInfo shaderStage{};
shaderStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
shaderStage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
shaderStage.module = shaderModule;
shaderStage.pName = "main";
VkComputePipelineCreateInfo pipelineInfo{};
pipelineInfo.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO;
pipelineInfo.stage = shaderStage;
pipelineInfo.layout = pipelineLayout;
result = vkCreateComputePipelinesFn(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline);
if (result != VK_SUCCESS) {
fail(format("vkCreateComputePipelines failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkCommandPoolCreateInfo commandPoolInfo{};
commandPoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
commandPoolInfo.queueFamilyIndex = computeQueueFamilyIndex;
result = vkCreateCommandPoolFn(device, &commandPoolInfo, nullptr, &commandPool);
if (result != VK_SUCCESS) {
fail(format("vkCreateCommandPool failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkCommandBufferAllocateInfo commandBufferInfo{};
commandBufferInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
commandBufferInfo.commandPool = commandPool;
commandBufferInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
commandBufferInfo.commandBufferCount = 1;
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
result = vkAllocateCommandBuffersFn(device, &commandBufferInfo, &commandBuffer);
if (result != VK_SUCCESS) {
fail(format("vkAllocateCommandBuffers failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkCommandBufferBeginInfo commandBufferBeginInfo{};
commandBufferBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
commandBufferBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
result = vkBeginCommandBufferFn(commandBuffer, &commandBufferBeginInfo);
if (result != VK_SUCCESS) {
fail(format("vkBeginCommandBuffer failed (VkResult = {})", static_cast<Int>(result)));
return;
}
vkCmdBindPipelineFn(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
vkCmdBindDescriptorSetsFn(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipelineLayout, 0, 1,
&descriptorSet, 0, nullptr);
vkCmdDispatchFn(commandBuffer, 1, 1, 1);
VkBufferMemoryBarrier hostReadBarrier{};
hostReadBarrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER;
hostReadBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
hostReadBarrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
hostReadBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
hostReadBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
hostReadBarrier.buffer = outputBuffer;
hostReadBarrier.offset = 0;
hostReadBarrier.size = sizeof(Program203WitnessOutput);
vkCmdPipelineBarrierFn(commandBuffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0,
0, nullptr, 1, &hostReadBarrier, 0, nullptr);
result = vkEndCommandBufferFn(commandBuffer);
if (result != VK_SUCCESS) {
fail(format("vkEndCommandBuffer failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
result = vkCreateFenceFn(device, &fenceInfo, nullptr, &fence);
if (result != VK_SUCCESS) {
fail(format("vkCreateFence failed (VkResult = {})", static_cast<Int>(result)));
return;
}
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;
result = vkQueueSubmitFn(queue, 1, &submitInfo, fence);
if (result != VK_SUCCESS) {
fail(format("vkQueueSubmit failed (VkResult = {})", static_cast<Int>(result)));
return;
}
constexpr Uint64 FenceTimeoutNs = 5'000'000'000ull;
result = vkWaitForFencesFn(device, 1, &fence, VK_TRUE, FenceTimeoutNs);
if (result != VK_SUCCESS) {
fenceWaitTimedOut = true;
fail(format("vkWaitForFences did not signal within 5 s (VkResult = {})", static_cast<Int>(result)));
return;
}
Program203WitnessOutput output{};
std::memcpy(&output, mappedOutput, sizeof(output));
const Program203WitnessValidationResult validation = ValidateProgram203Witness(output);
if (!validation.ok) {
fail(validation.detail);
return;
}
builder.Pass(RowName, validation.detail);
}
// Everything the "MobileGL reported ..." rows need from the Vulkan device probe. // Everything the "MobileGL reported ..." rows need from the Vulkan device probe.
struct VulkanProbeSummary { struct VulkanProbeSummary {
Bool devicePropsValid = false; Bool devicePropsValid = false;
@@ -2104,7 +1669,6 @@ namespace MobileGL::MG_Util::SelfTest {
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
Uint32 graphicsQueueFamilyIndex = 0; Uint32 graphicsQueueFamilyIndex = 0;
Uint32 graphicsQueueTimestampValidBits = 0; Uint32 graphicsQueueTimestampValidBits = 0;
Uint32 computeQueueFamilyIndex = std::numeric_limits<Uint32>::max();
for (VkPhysicalDevice candidate : devices) { for (VkPhysicalDevice candidate : devices) {
Uint32 queueFamilyCount = 0; Uint32 queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyPropertiesFn(candidate, &queueFamilyCount, nullptr); vkGetPhysicalDeviceQueueFamilyPropertiesFn(candidate, &queueFamilyCount, nullptr);
@@ -2120,13 +1684,6 @@ namespace MobileGL::MG_Util::SelfTest {
} }
} }
if (physicalDevice != VK_NULL_HANDLE) { if (physicalDevice != VK_NULL_HANDLE) {
for (Uint32 familyIndex = 0; familyIndex < queueFamilyCount; ++familyIndex) {
const VkQueueFamilyProperties& family = queueFamilies[familyIndex];
if (family.queueCount > 0 && (family.queueFlags & VK_QUEUE_COMPUTE_BIT) != 0) {
computeQueueFamilyIndex = familyIndex;
break;
}
}
break; break;
} }
} }
@@ -2463,15 +2020,13 @@ namespace MobileGL::MG_Util::SelfTest {
"change every N instances change every one"); "change every N instances change every one");
} }
VkPhysicalDeviceSubgroupProperties subgroupProperties{};
Bool subgroupPropertiesAvailable = false;
if (vkGetPhysicalDeviceProperties2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) { if (vkGetPhysicalDeviceProperties2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
VkPhysicalDeviceSubgroupProperties subgroupProperties{};
subgroupProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES; subgroupProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES;
VkPhysicalDeviceProperties2 properties2{}; VkPhysicalDeviceProperties2 properties2{};
properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
properties2.pNext = &subgroupProperties; properties2.pNext = &subgroupProperties;
vkGetPhysicalDeviceProperties2Fn(physicalDevice, &properties2); vkGetPhysicalDeviceProperties2Fn(physicalDevice, &properties2);
subgroupPropertiesAvailable = true;
const Bool subgroupUsable = subgroupProperties.subgroupSize > 0 && const Bool subgroupUsable = subgroupProperties.subgroupSize > 0 &&
(subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0 && (subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0 &&
(subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0; (subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0;
@@ -2491,9 +2046,6 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Warn("Compute shader subgroup", "subgroup properties could not be queried"); builder.Warn("Compute shader subgroup", "subgroup properties could not be queried");
} }
ProbeVulkanProgram203Witness(builder, getInstanceProcAddr, instance, physicalDevice, computeQueueFamilyIndex,
properties, subgroupPropertiesAvailable, subgroupProperties);
if (HasVkExtension(deviceExtensions, VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME)) { if (HasVkExtension(deviceExtensions, VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME)) {
builder.Pass("VK_KHR_draw_indirect_count", builder.Pass("VK_KHR_draw_indirect_count",
"supported (count-buffer indirect draws run as single native " "supported (count-buffer indirect draws run as single native "
@@ -1,164 +0,0 @@
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.comp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Native Vulkan GLSL 450 witness for Program 203's first subgroup reduction.
// It is intentionally independent of the GL 430 integration scenario. The body
// below preserves Program 203's source reduction; the surrounding diagnostics
// only observe its topology and cache handoffs.
#version 450
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
const uint kTopologyNonuniformNumSubgroups = 1u << 0u;
const uint kTopologyInvalidNumSubgroups = 1u << 1u;
const uint kTopologyInvalidSubgroupId = 1u << 2u;
const uint kTopologyInvalidSubgroupLane = 1u << 3u;
const uint kWitnessMagic = 0x50323033u;
layout(std430, set = 0, binding = 0) buffer Program203WitnessOutput {
uint magic;
uint topologyFlags;
uint numSubgroups;
uint loopLength;
uint seenSubgroupMask;
uvec4 owner511;
uint lastLaneWriterCount[32];
uint indexedInputTotal[32];
vec2 rawPrefix[32];
vec2 scanCache[6][32];
vec2 finalAverage;
} outWitness;
// Program 203's cache stays separate from all diagnostic shared state. In
// particular, no instrumentation stores through prefixSumCache except source
// writes retained below.
shared vec2 prefixSumCache[32];
shared uint canonicalNumSubgroups;
shared uint topologyFlagsShared;
shared uint seenSubgroupMaskShared;
shared uint lastLaneWriterCountShared[32];
shared uint indexedInputTotalShared[32];
void main() {
const uint localInvocationIndex = gl_LocalInvocationIndex;
// Host memory is deliberately poisoned before dispatch. Initialize only
// shared atomic diagnostic state; owner, average, and magic remain poisoned
// until their required post-source-barrier writes below.
if (localInvocationIndex == 0u) {
canonicalNumSubgroups = 0u;
topologyFlagsShared = 0u;
seenSubgroupMaskShared = 0u;
}
if (localInvocationIndex < 32u) {
lastLaneWriterCountShared[localInvocationIndex] = 0u;
indexedInputTotalShared[localInvocationIndex] = 0u;
}
memoryBarrierShared();
barrier();
// Invocation zero defines the canonical domain. It is broadcast through
// shared memory before every invocation records its own raw observation.
if (localInvocationIndex == 0u) {
canonicalNumSubgroups = gl_NumSubgroups;
outWitness.numSubgroups = gl_NumSubgroups;
}
barrier();
const uint canonicalN = canonicalNumSubgroups;
if (gl_NumSubgroups != canonicalN)
atomicOr(topologyFlagsShared, kTopologyNonuniformNumSubgroups);
if (gl_NumSubgroups < 2u || gl_NumSubgroups > 32u)
atomicOr(topologyFlagsShared, kTopologyInvalidNumSubgroups);
if (gl_SubgroupID >= canonicalN || gl_SubgroupID >= 32u)
atomicOr(topologyFlagsShared, kTopologyInvalidSubgroupId);
if (gl_SubgroupSize == 0u || gl_SubgroupInvocationID >= gl_SubgroupSize)
atomicOr(topologyFlagsShared, kTopologyInvalidSubgroupLane);
// Keep all atomic collection bounded by the canonical valid domain. A
// nonuniform/broken report reaches the uniform safety branch below instead
// of making some lanes return before a barrier.
const bool canonicalDomain = canonicalN >= 2u && canonicalN <= 32u;
const bool idInCanonicalDomain = canonicalDomain && gl_SubgroupID < canonicalN;
if (idInCanonicalDomain) {
atomicOr(seenSubgroupMaskShared, 1u << gl_SubgroupID);
atomicAdd(indexedInputTotalShared[gl_SubgroupID], localInvocationIndex + 1u);
if (gl_SubgroupSize != 0u && gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
atomicAdd(lastLaneWriterCountShared[gl_SubgroupID], 1u);
}
memoryBarrierShared();
barrier();
if (localInvocationIndex == 0u)
outWitness.seenSubgroupMask = seenSubgroupMaskShared;
if (localInvocationIndex < 32u) {
outWitness.lastLaneWriterCount[localInvocationIndex] = lastLaneWriterCountShared[localInvocationIndex];
outWitness.indexedInputTotal[localInvocationIndex] = indexedInputTotalShared[localInvocationIndex];
}
// This branch is uniform after collection and is solely a safety guard for
// broken topology reports. The valid side retains Program 203 verbatim.
const bool sourceDomain = canonicalDomain && topologyFlagsShared == 0u;
if (sourceDomain) {
vec2 sampleLuminance = vec2(float(gl_LocalInvocationIndex + 1u), 0.0);
sampleLuminance = subgroupInclusiveAdd(sampleLuminance);
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
barrier();
if (gl_LocalInvocationIndex < gl_NumSubgroups)
outWitness.rawPrefix[gl_LocalInvocationIndex] = prefixSumCache[gl_LocalInvocationIndex];
barrier();
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
if (gl_LocalInvocationIndex == 0u)
outWitness.loopLength = loopLength;
for (uint scanStage = 0u; scanStage < loopLength; ++scanStage) {
if ((gl_SubgroupID & (1u << scanStage)) > 0u) {
sampleLuminance += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
}
barrier();
if (gl_LocalInvocationIndex < gl_NumSubgroups)
outWitness.scanCache[scanStage][gl_LocalInvocationIndex] =
prefixSumCache[gl_LocalInvocationIndex];
// A second, diagnostic-only barrier prevents a faster invocation
// from entering the next source stage while another reads this cache.
barrier();
}
if (gl_LocalInvocationIndex == 511u)
prefixSumCache[0] = sampleLuminance / 512.0;
barrier();
if (gl_LocalInvocationIndex == 511u) {
outWitness.owner511 = uvec4(gl_SubgroupSize, gl_NumSubgroups, gl_SubgroupID,
gl_SubgroupInvocationID);
outWitness.finalAverage = prefixSumCache[0];
}
}
// Both sides of the uniform branch reach this barrier. The magic is the
// completion latch and therefore cannot be written before the final barrier.
barrier();
if (localInvocationIndex == 0u) {
outWitness.topologyFlags = topologyFlagsShared;
outWitness.magic = kWitnessMagic;
}
}
@@ -1,277 +0,0 @@
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "DriverPostProgram203Witness.h"
#include <bit>
#include <sstream>
#include <utility>
#include <vector>
namespace MobileGL::MG_Util::SelfTest {
namespace {
[[nodiscard]] Program203WitnessValidationResult Failure(Program203WitnessValidationFailure failure,
std::string detail,
std::uint32_t scanStage = 0u,
std::uint32_t subgroup = 0u) {
Program203WitnessValidationResult result;
result.ok = false;
result.failure = failure;
result.scanStage = scanStage;
result.subgroup = subgroup;
result.detail = std::move(detail);
return result;
}
[[nodiscard]] std::uint32_t FloatBits(float value) {
return std::bit_cast<std::uint32_t>(value);
}
[[nodiscard]] bool SameBits(float lhs, float rhs) {
return FloatBits(lhs) == FloatBits(rhs);
}
[[nodiscard]] bool SameBits(const Program203WitnessVec2& lhs, const Program203WitnessVec2& rhs) {
return SameBits(lhs.x, rhs.x) && SameBits(lhs.y, rhs.y);
}
[[nodiscard]] std::string Vec2String(const Program203WitnessVec2& value) {
std::ostringstream output;
output << '(' << value.x << ',' << value.y << ')';
return output.str();
}
[[nodiscard]] std::uint32_t ExpectedSeenSubgroupMask(std::uint32_t numSubgroups) {
return numSubgroups == kProgram203WitnessMaxSubgroups ? 0xffffffffu : (1u << numSubgroups) - 1u;
}
[[nodiscard]] std::string JoinRequirements(const std::vector<std::string>& requirements) {
std::ostringstream output;
for (std::size_t i = 0; i < requirements.size(); ++i) {
if (i != 0u) output << "; ";
output << requirements[i];
}
return output.str();
}
} // namespace
Program203WitnessEligibilityResult
EvaluateProgram203WitnessEligibility(const Program203WitnessLimits& limits) {
// This classification deliberately precedes numeric limits. An absent native
// compute/basic/arithmetic subgroup contract means there is nothing to witness,
// whereas every resource/entry-point failure on a capable device is a POST FAIL.
if (!limits.computeStageSupported || !limits.basicSubgroupSupported || !limits.arithmeticSubgroupSupported) {
std::vector<std::string> missing;
if (!limits.computeStageSupported) missing.emplace_back("VK_SHADER_STAGE_COMPUTE_BIT");
if (!limits.basicSubgroupSupported) missing.emplace_back("VK_SUBGROUP_FEATURE_BASIC_BIT");
if (!limits.arithmeticSubgroupSupported) missing.emplace_back("VK_SUBGROUP_FEATURE_ARITHMETIC_BIT");
return {Program203WitnessEligibility::SkipUnsupportedNativeFeatureSet,
"skipped because the native compute/basic/arithmetic subgroup feature set is unsupported (missing " +
JoinRequirements(missing) + ')'};
}
std::vector<std::string> inadequate;
if (limits.subgroupSize == 0u) {
inadequate.emplace_back("subgroupSize == 0");
}
if (limits.maxComputeWorkGroupInvocations < kProgram203WitnessInvocationCount) {
inadequate.emplace_back("maxComputeWorkGroupInvocations < 512");
}
if (limits.maxComputeWorkGroupSize[0] < 32u || limits.maxComputeWorkGroupSize[1] < 16u ||
limits.maxComputeWorkGroupSize[2] < 1u) {
inadequate.emplace_back("maxComputeWorkGroupSize does not cover 32x16x1");
}
if (limits.maxComputeSharedMemorySize < kProgram203WitnessSharedMemoryBytes) {
inadequate.emplace_back("maxComputeSharedMemorySize < " +
std::to_string(kProgram203WitnessSharedMemoryBytes));
}
if (limits.maxPerStageDescriptorStorageBuffers < 1u) {
inadequate.emplace_back("maxPerStageDescriptorStorageBuffers < 1");
}
if (limits.maxDescriptorSetStorageBuffers < 1u) {
inadequate.emplace_back("maxDescriptorSetStorageBuffers < 1");
}
if (limits.maxBoundDescriptorSets < 1u) {
inadequate.emplace_back("maxBoundDescriptorSets < 1");
}
if (limits.maxStorageBufferRange < sizeof(Program203WitnessOutput)) {
inadequate.emplace_back("maxStorageBufferRange < " +
std::to_string(sizeof(Program203WitnessOutput)));
}
if (!inadequate.empty()) {
return {Program203WitnessEligibility::FailInadequateLimits,
"insufficient Vulkan limits for a 32x16x1 workgroup, one output SSBO, and " +
std::to_string(kProgram203WitnessSharedMemoryBytes) + " bytes of shared memory: " +
JoinRequirements(inadequate)};
}
return {Program203WitnessEligibility::Execute, {}};
}
std::uint32_t ComputeProgram203WitnessLoopLength(std::uint32_t numSubgroups) {
if (numSubgroups < 2u || numSubgroups > kProgram203WitnessMaxSubgroups) return 0u;
// Exact C++ spelling of the source's findMSB-based calculation. In
// particular, its final iteration for powers of two is intentional.
std::uint32_t loopLength = 0u;
for (std::uint32_t value = numSubgroups; value > 1u; value >>= 1u) {
++loopLength;
}
loopLength += static_cast<std::uint32_t>(numSubgroups - (1u << (loopLength - 1u)) > 0u);
return loopLength;
}
Program203WitnessValidationResult ValidateProgram203Witness(const Program203WitnessOutput& output) {
// 1. Completion. A poisoned or unwritten result must never turn into a
// topology diagnosis, because it says nothing about execution.
if (output.magic != kProgram203WitnessMagic) {
std::ostringstream detail;
detail << "completion: magic was 0x" << std::hex << output.magic << ", expected 0x"
<< kProgram203WitnessMagic;
return Failure(Program203WitnessValidationFailure::Completion, detail.str());
}
// 2. Observed topology. All checks consume observations written by the
// shader, rather than inferring subgroup layout from invocation indices.
const std::uint32_t numSubgroups = output.numSubgroups;
if (numSubgroups < 2u || numSubgroups > kProgram203WitnessMaxSubgroups) {
std::ostringstream detail;
detail << "topology: canonical gl_NumSubgroups=" << numSubgroups << " is outside [2, 32]";
return Failure(Program203WitnessValidationFailure::Topology, detail.str());
}
if ((output.topologyFlags & Program203WitnessNonuniformNumSubgroups) != 0u) {
return Failure(Program203WitnessValidationFailure::Topology,
"topology: gl_NumSubgroups differed across workgroup");
}
if ((output.topologyFlags & Program203WitnessInvalidNumSubgroups) != 0u) {
return Failure(Program203WitnessValidationFailure::Topology,
"topology: an invocation reported gl_NumSubgroups outside [2, 32]");
}
if ((output.topologyFlags & Program203WitnessInvalidSubgroupId) != 0u) {
return Failure(Program203WitnessValidationFailure::Topology,
"topology: an invocation reported an invalid gl_SubgroupID");
}
if ((output.topologyFlags & Program203WitnessInvalidSubgroupLane) != 0u) {
return Failure(Program203WitnessValidationFailure::Topology,
"topology: an invocation reported an invalid subgroup lane");
}
if ((output.topologyFlags & ~(Program203WitnessNonuniformNumSubgroups |
Program203WitnessInvalidNumSubgroups |
Program203WitnessInvalidSubgroupId |
Program203WitnessInvalidSubgroupLane)) != 0u) {
std::ostringstream detail;
detail << "topology: unknown topology flags 0x" << std::hex << output.topologyFlags;
return Failure(Program203WitnessValidationFailure::Topology, detail.str());
}
const std::uint32_t expectedMask = ExpectedSeenSubgroupMask(numSubgroups);
if (output.seenSubgroupMask != expectedMask) {
std::ostringstream detail;
detail << "topology: seen subgroup-ID mask was 0x" << std::hex << output.seenSubgroupMask
<< ", expected 0x" << expectedMask;
return Failure(Program203WitnessValidationFailure::Topology, detail.str());
}
const std::uint32_t expectedLoopLength = ComputeProgram203WitnessLoopLength(numSubgroups);
if (output.loopLength != expectedLoopLength) {
std::ostringstream detail;
detail << "topology: loopLength was " << std::dec << output.loopLength << ", expected "
<< expectedLoopLength;
return Failure(Program203WitnessValidationFailure::Topology, detail.str());
}
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
if (output.lastLaneWriterCount[subgroup] != 1u) {
std::ostringstream detail;
detail << "topology: subgroup " << subgroup << " has "
<< output.lastLaneWriterCount[subgroup] << " source last-lane writers, expected exactly 1";
return Failure(Program203WitnessValidationFailure::Topology, detail.str(), 0u, subgroup);
}
}
if (output.owner511.y != numSubgroups) {
std::ostringstream detail;
detail << "final owner: invocation 511 reported gl_NumSubgroups=" << output.owner511.y << ", expected "
<< numSubgroups;
return Failure(Program203WitnessValidationFailure::FinalOwner, detail.str());
}
if (output.owner511.z != numSubgroups - 1u) {
std::ostringstream detail;
detail << "final owner: invocation 511 is not in the highest subgroup (id" << output.owner511.z
<< ", expected id" << (numSubgroups - 1u) << ')';
return Failure(Program203WitnessValidationFailure::FinalOwner, detail.str());
}
if (output.owner511.x == 0u || output.owner511.w != output.owner511.x - 1u) {
std::ostringstream detail;
detail << "final owner: invocation 511 is not the last lane of highest subgroup (size "
<< output.owner511.x << ", lane " << output.owner511.w << ')';
return Failure(Program203WitnessValidationFailure::FinalOwner, detail.str());
}
// 3. Initial subgroup handoff. The atomic scalar totals are independent
// of subgroupInclusiveAdd; their sum and the cache values establish that
// the final lanes handed off the native vector inclusive-add results.
std::uint64_t indexedTotal = 0u;
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
indexedTotal += output.indexedInputTotal[subgroup];
}
if (indexedTotal != 131328u) {
std::ostringstream detail;
detail << "initial subgroup handoff: indexed input total was " << indexedTotal << ", expected 131328";
return Failure(Program203WitnessValidationFailure::InitialSubgroupHandoff, detail.str());
}
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
const Program203WitnessVec2 expected = {static_cast<float>(output.indexedInputTotal[subgroup]), 0.0f};
if (!SameBits(output.rawPrefix[subgroup], expected)) {
std::ostringstream detail;
detail << "initial subgroup handoff: subgroup " << subgroup << " rawPrefix observed "
<< Vec2String(output.rawPrefix[subgroup]) << ", expected " << Vec2String(expected);
return Failure(Program203WitnessValidationFailure::InitialSubgroupHandoff, detail.str(), 0u,
subgroup);
}
}
// 4. Source scan. Do not substitute a conventional scan: this reproduces
// the source cache index expression and stage ordering word for word.
std::array<Program203WitnessVec2, kProgram203WitnessMaxSubgroups> expectedCache = output.rawPrefix;
for (std::uint32_t scanStage = 0u; scanStage < expectedLoopLength; ++scanStage) {
auto cacheAfterStage = expectedCache;
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
if ((subgroup & (1u << scanStage)) > 0u) {
const std::uint32_t sourceCacheIndex = (subgroup >> scanStage << scanStage) - 1u;
cacheAfterStage[subgroup].x += expectedCache[sourceCacheIndex].x;
cacheAfterStage[subgroup].y += expectedCache[sourceCacheIndex].y;
}
}
expectedCache = cacheAfterStage;
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
if (!SameBits(output.scanCache[scanStage][subgroup], expectedCache[subgroup])) {
std::ostringstream detail;
detail << "source scan stage " << scanStage << ", subgroup " << subgroup << ": observed "
<< Vec2String(output.scanCache[scanStage][subgroup]) << ", expected "
<< Vec2String(expectedCache[subgroup]);
return Failure(Program203WitnessValidationFailure::SourceScan, detail.str(), scanStage, subgroup);
}
}
}
// 5. The owner contract was checked above with the other topology facts;
// this final result remains a separate exact-vector check.
const Program203WitnessVec2 expectedAverage = {256.5f, 0.0f};
if (!SameBits(output.finalAverage, expectedAverage)) {
std::ostringstream detail;
detail << "final average: observed " << Vec2String(output.finalAverage) << ", expected "
<< Vec2String(expectedAverage);
return Failure(Program203WitnessValidationFailure::FinalAverage, detail.str());
}
std::ostringstream detail;
detail << "N=" << numSubgroups << ", owner511=id" << output.owner511.z << "/lane" << output.owner511.w
<< ", " << expectedLoopLength << " scan stages, average=" << Vec2String(output.finalAverage);
Program203WitnessValidationResult result;
result.ok = true;
result.failure = Program203WitnessValidationFailure::None;
result.detail = detail.str();
return result;
}
} // namespace MobileGL::MG_Util::SelfTest
@@ -1,151 +0,0 @@
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.h
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Compact, native-Vulkan Program-203 first-reduction witness ABI and its pure
// validator. The types below deliberately mirror DriverPostProgram203Witness.comp's
// single std430 storage block; changing either side requires updating the static
// layout assertions here.
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
#include <type_traits>
namespace MobileGL::MG_Util::SelfTest {
constexpr std::uint32_t kProgram203WitnessMagic = 0x50323033u; // "P203"
constexpr std::uint32_t kProgram203WitnessInvocationCount = 512u;
constexpr std::uint32_t kProgram203WitnessMaxSubgroups = 32u;
constexpr std::uint32_t kProgram203WitnessMaxScanStages = 6u;
// These bit values are shared with the GLSL source. They document failures in
// topology observations rather than guessing a topology from local IDs on the host.
enum Program203WitnessTopologyFlag : std::uint32_t {
Program203WitnessNonuniformNumSubgroups = 1u << 0u,
Program203WitnessInvalidNumSubgroups = 1u << 1u,
Program203WitnessInvalidSubgroupId = 1u << 2u,
Program203WitnessInvalidSubgroupLane = 1u << 3u,
};
struct alignas(8) Program203WitnessVec2 {
float x;
float y;
};
struct alignas(16) Program203WitnessUVec4 {
std::uint32_t x;
std::uint32_t y;
std::uint32_t z;
std::uint32_t w;
};
// std430 layout of DriverPostProgram203Witness.comp's Program203WitnessOutput block.
struct alignas(16) Program203WitnessOutput {
std::uint32_t magic;
std::uint32_t topologyFlags;
std::uint32_t numSubgroups;
std::uint32_t loopLength;
std::uint32_t seenSubgroupMask;
Program203WitnessUVec4 owner511;
std::array<std::uint32_t, kProgram203WitnessMaxSubgroups> lastLaneWriterCount;
std::array<std::uint32_t, kProgram203WitnessMaxSubgroups> indexedInputTotal;
std::array<Program203WitnessVec2, kProgram203WitnessMaxSubgroups> rawPrefix;
std::array<std::array<Program203WitnessVec2, kProgram203WitnessMaxSubgroups>,
kProgram203WitnessMaxScanStages>
scanCache;
Program203WitnessVec2 finalAverage;
};
static_assert(std::is_standard_layout_v<Program203WitnessVec2>);
static_assert(std::is_standard_layout_v<Program203WitnessUVec4>);
static_assert(std::is_standard_layout_v<Program203WitnessOutput>);
static_assert(sizeof(Program203WitnessVec2) == 8u);
static_assert(alignof(Program203WitnessVec2) == 8u);
static_assert(sizeof(Program203WitnessUVec4) == 16u);
static_assert(alignof(Program203WitnessUVec4) == 16u);
static_assert(offsetof(Program203WitnessOutput, magic) == 0u);
static_assert(offsetof(Program203WitnessOutput, topologyFlags) == 4u);
static_assert(offsetof(Program203WitnessOutput, numSubgroups) == 8u);
static_assert(offsetof(Program203WitnessOutput, loopLength) == 12u);
static_assert(offsetof(Program203WitnessOutput, seenSubgroupMask) == 16u);
static_assert(offsetof(Program203WitnessOutput, owner511) == 32u);
static_assert(offsetof(Program203WitnessOutput, lastLaneWriterCount) == 48u);
static_assert(offsetof(Program203WitnessOutput, indexedInputTotal) == 176u);
static_assert(offsetof(Program203WitnessOutput, rawPrefix) == 304u);
static_assert(offsetof(Program203WitnessOutput, scanCache) == 560u);
static_assert(offsetof(Program203WitnessOutput, finalAverage) == 2096u);
static_assert(sizeof(Program203WitnessOutput) == 2112u);
// The witness uses prefixSumCache[32], three scalar shared diagnostics, and
// two 32-entry scalar diagnostic arrays in the GLSL source. Keep this
// independent of the output SSBO size.
constexpr std::uint32_t kProgram203WitnessSharedMemoryBytes =
kProgram203WitnessMaxSubgroups * sizeof(Program203WitnessVec2) +
3u * sizeof(std::uint32_t) +
2u * kProgram203WitnessMaxSubgroups * sizeof(std::uint32_t);
enum class Program203WitnessEligibility {
Execute,
SkipUnsupportedNativeFeatureSet,
FailInadequateLimits,
};
// The raw physical-device conditions needed by the native witness. This is
// intentionally distinct from MobileGL's advertised-extension policy.
struct Program203WitnessLimits {
bool computeStageSupported = false;
bool basicSubgroupSupported = false;
bool arithmeticSubgroupSupported = false;
std::uint32_t subgroupSize = 0u;
std::uint32_t maxComputeWorkGroupInvocations = 0u;
std::array<std::uint32_t, 3> maxComputeWorkGroupSize{};
std::uint32_t maxComputeSharedMemorySize = 0u;
std::uint32_t maxPerStageDescriptorStorageBuffers = 0u;
std::uint32_t maxDescriptorSetStorageBuffers = 0u;
std::uint32_t maxBoundDescriptorSets = 0u;
std::uint64_t maxStorageBufferRange = 0u;
};
struct Program203WitnessEligibilityResult {
Program203WitnessEligibility eligibility = Program203WitnessEligibility::FailInadequateLimits;
std::string detail;
};
enum class Program203WitnessValidationFailure {
None,
Completion,
Topology,
InitialSubgroupHandoff,
SourceScan,
FinalOwner,
FinalAverage,
};
struct Program203WitnessValidationResult {
bool ok = false;
Program203WitnessValidationFailure failure = Program203WitnessValidationFailure::Completion;
std::uint32_t scanStage = 0u;
std::uint32_t subgroup = 0u;
std::string detail;
};
[[nodiscard]] Program203WitnessEligibilityResult
EvaluateProgram203WitnessEligibility(const Program203WitnessLimits& limits);
// Mirrors the source's findMSB expression for valid N in [2, 32].
[[nodiscard]] std::uint32_t ComputeProgram203WitnessLoopLength(std::uint32_t numSubgroups);
[[nodiscard]] Program203WitnessValidationResult
ValidateProgram203Witness(const Program203WitnessOutput& output);
} // namespace MobileGL::MG_Util::SelfTest
@@ -1,291 +0,0 @@
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostProgram203WitnessSpv.h
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Generated from DriverPostProgram203Witness.comp with:
// glslangValidator --target-env vulkan1.1 -V DriverPostProgram203Witness.comp
// Validated with spirv-val --target-env vulkan1.1. Do not edit words by hand.
#pragma once
#include <cstddef>
#include <cstdint>
namespace MobileGL::MG_Util::SelfTest {
inline constexpr std::uint32_t kDriverPostProgram203WitnessSpv[] = {
0x07230203u, 0x00010300u, 0x0008000bu, 0x00000145u, 0x00000000u, 0x00020011u, 0x00000001u, 0x00020011u,
0x0000003du, 0x00020011u, 0x0000003fu, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu,
0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x000a000fu, 0x00000005u, 0x00000004u, 0x6e69616du,
0x00000000u, 0x0000000au, 0x0000002au, 0x00000050u, 0x0000005eu, 0x00000064u, 0x00060010u, 0x00000004u,
0x00000011u, 0x00000020u, 0x00000010u, 0x00000001u, 0x00030003u, 0x00000002u, 0x000001c2u, 0x000a0004u,
0x4b5f4c47u, 0x735f5248u, 0x65646168u, 0x75735f72u, 0x6f726762u, 0x615f7075u, 0x68746972u, 0x6974656du,
0x00000063u, 0x00090004u, 0x4b5f4c47u, 0x735f5248u, 0x65646168u, 0x75735f72u, 0x6f726762u, 0x625f7075u,
0x63697361u, 0x00000000u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00080005u, 0x00000008u,
0x61636f6cu, 0x766e496cu, 0x7461636fu, 0x496e6f69u, 0x7865646eu, 0x00000000u, 0x00080005u, 0x0000000au,
0x4c5f6c67u, 0x6c61636fu, 0x6f766e49u, 0x69746163u, 0x6e496e6fu, 0x00786564u, 0x00080005u, 0x00000013u,
0x6f6e6163u, 0x6163696eu, 0x6d754e6cu, 0x67627553u, 0x70756f72u, 0x00000073u, 0x00070005u, 0x00000014u,
0x6f706f74u, 0x79676f6cu, 0x67616c46u, 0x61685373u, 0x00646572u, 0x00080005u, 0x00000015u, 0x6e656573u,
0x67627553u, 0x70756f72u, 0x6b73614du, 0x72616853u, 0x00006465u, 0x00090005u, 0x0000001du, 0x7473616cu,
0x656e614cu, 0x74697257u, 0x6f437265u, 0x53746e75u, 0x65726168u, 0x00000064u, 0x00080005u, 0x00000020u,
0x65646e69u, 0x49646578u, 0x7475706eu, 0x61746f54u, 0x6168536cu, 0x00646572u, 0x00060005u, 0x0000002au,
0x4e5f6c67u, 0x75536d75u, 0x6f726762u, 0x00737075u, 0x00080005u, 0x00000035u, 0x676f7250u, 0x326d6172u,
0x69573330u, 0x73656e74u, 0x74754f73u, 0x00747570u, 0x00050006u, 0x00000035u, 0x00000000u, 0x6967616du,
0x00000063u, 0x00070006u, 0x00000035u, 0x00000001u, 0x6f706f74u, 0x79676f6cu, 0x67616c46u, 0x00000073u,
0x00070006u, 0x00000035u, 0x00000002u, 0x536d756eu, 0x72676275u, 0x7370756fu, 0x00000000u, 0x00060006u,
0x00000035u, 0x00000003u, 0x706f6f6cu, 0x676e654cu, 0x00006874u, 0x00080006u, 0x00000035u, 0x00000004u,
0x6e656573u, 0x67627553u, 0x70756f72u, 0x6b73614du, 0x00000000u, 0x00060006u, 0x00000035u, 0x00000005u,
0x656e776fu, 0x31313572u, 0x00000000u, 0x00080006u, 0x00000035u, 0x00000006u, 0x7473616cu, 0x656e614cu,
0x74697257u, 0x6f437265u, 0x00746e75u, 0x00080006u, 0x00000035u, 0x00000007u, 0x65646e69u, 0x49646578u,
0x7475706eu, 0x61746f54u, 0x0000006cu, 0x00060006u, 0x00000035u, 0x00000008u, 0x50776172u, 0x69666572u,
0x00000078u, 0x00060006u, 0x00000035u, 0x00000009u, 0x6e616373u, 0x68636143u, 0x00000065u, 0x00070006u,
0x00000035u, 0x0000000au, 0x616e6966u, 0x6576416cu, 0x65676172u, 0x00000000u, 0x00050005u, 0x00000037u,
0x5774756fu, 0x656e7469u, 0x00007373u, 0x00050005u, 0x0000003du, 0x6f6e6163u, 0x6163696eu, 0x00004e6cu,
0x00060005u, 0x00000050u, 0x535f6c67u, 0x72676275u, 0x4970756fu, 0x00000044u, 0x00060005u, 0x0000005eu,
0x535f6c67u, 0x72676275u, 0x5370756fu, 0x00657a69u, 0x00080005u, 0x00000064u, 0x535f6c67u, 0x72676275u,
0x4970756fu, 0x636f766eu, 0x6f697461u, 0x0044496eu, 0x00060005u, 0x0000006eu, 0x6f6e6163u, 0x6163696eu,
0x6d6f446cu, 0x006e6961u, 0x00070005u, 0x00000074u, 0x6e496469u, 0x6f6e6143u, 0x6163696eu, 0x6d6f446cu,
0x006e6961u, 0x00060005u, 0x000000acu, 0x72756f73u, 0x6f446563u, 0x6e69616du, 0x00000000u, 0x00060005u,
0x000000b7u, 0x706d6173u, 0x754c656cu, 0x616e696du, 0x0065636eu, 0x00060005u, 0x000000c8u, 0x66657270u,
0x75537869u, 0x6361436du, 0x00006568u, 0x00050005u, 0x000000d9u, 0x706f6f6cu, 0x676e654cu, 0x00006874u,
0x00050005u, 0x000000edu, 0x6e616373u, 0x67617453u, 0x00000065u, 0x00040047u, 0x0000000au, 0x0000000bu,
0x0000001du, 0x00040047u, 0x0000002au, 0x0000000bu, 0x00000026u, 0x00040047u, 0x0000002du, 0x00000006u,
0x00000004u, 0x00040047u, 0x0000002eu, 0x00000006u, 0x00000004u, 0x00040047u, 0x00000031u, 0x00000006u,
0x00000008u, 0x00040047u, 0x00000032u, 0x00000006u, 0x00000008u, 0x00040047u, 0x00000034u, 0x00000006u,
0x00000100u, 0x00030047u, 0x00000035u, 0x00000002u, 0x00050048u, 0x00000035u, 0x00000000u, 0x00000023u,
0x00000000u, 0x00050048u, 0x00000035u, 0x00000001u, 0x00000023u, 0x00000004u, 0x00050048u, 0x00000035u,
0x00000002u, 0x00000023u, 0x00000008u, 0x00050048u, 0x00000035u, 0x00000003u, 0x00000023u, 0x0000000cu,
0x00050048u, 0x00000035u, 0x00000004u, 0x00000023u, 0x00000010u, 0x00050048u, 0x00000035u, 0x00000005u,
0x00000023u, 0x00000020u, 0x00050048u, 0x00000035u, 0x00000006u, 0x00000023u, 0x00000030u, 0x00050048u,
0x00000035u, 0x00000007u, 0x00000023u, 0x000000b0u, 0x00050048u, 0x00000035u, 0x00000008u, 0x00000023u,
0x00000130u, 0x00050048u, 0x00000035u, 0x00000009u, 0x00000023u, 0x00000230u, 0x00050048u, 0x00000035u,
0x0000000au, 0x00000023u, 0x00000830u, 0x00040047u, 0x00000037u, 0x00000021u, 0x00000000u, 0x00040047u,
0x00000037u, 0x00000022u, 0x00000000u, 0x00040047u, 0x00000050u, 0x0000000bu, 0x00000028u, 0x00030047u,
0x0000005eu, 0x00000000u, 0x00040047u, 0x0000005eu, 0x0000000bu, 0x00000024u, 0x00030047u, 0x0000005fu,
0x00000000u, 0x00030047u, 0x00000064u, 0x00000000u, 0x00040047u, 0x00000064u, 0x0000000bu, 0x00000029u,
0x00030047u, 0x00000065u, 0x00000000u, 0x00030047u, 0x00000066u, 0x00000000u, 0x00030047u, 0x00000087u,
0x00000000u, 0x00030047u, 0x0000008bu, 0x00000000u, 0x00030047u, 0x0000008cu, 0x00000000u, 0x00030047u,
0x0000008du, 0x00000000u, 0x00030047u, 0x000000c0u, 0x00000000u, 0x00030047u, 0x000000c1u, 0x00000000u,
0x00030047u, 0x000000c2u, 0x00000000u, 0x00030047u, 0x00000107u, 0x00000000u, 0x00030047u, 0x00000108u,
0x00000000u, 0x00030047u, 0x00000109u, 0x00000000u, 0x00030047u, 0x0000012fu, 0x00000000u, 0x00030047u,
0x00000132u, 0x00000000u, 0x00040047u, 0x00000144u, 0x0000000bu, 0x00000019u, 0x00020013u, 0x00000002u,
0x00030021u, 0x00000003u, 0x00000002u, 0x00040015u, 0x00000006u, 0x00000020u, 0x00000000u, 0x00040020u,
0x00000007u, 0x00000007u, 0x00000006u, 0x00040020u, 0x00000009u, 0x00000001u, 0x00000006u, 0x0004003bu,
0x00000009u, 0x0000000au, 0x00000001u, 0x0004002bu, 0x00000006u, 0x0000000du, 0x00000000u, 0x00020014u,
0x0000000eu, 0x00040020u, 0x00000012u, 0x00000004u, 0x00000006u, 0x0004003bu, 0x00000012u, 0x00000013u,
0x00000004u, 0x0004003bu, 0x00000012u, 0x00000014u, 0x00000004u, 0x0004003bu, 0x00000012u, 0x00000015u,
0x00000004u, 0x0004002bu, 0x00000006u, 0x00000017u, 0x00000020u, 0x0004001cu, 0x0000001bu, 0x00000006u,
0x00000017u, 0x00040020u, 0x0000001cu, 0x00000004u, 0x0000001bu, 0x0004003bu, 0x0000001cu, 0x0000001du,
0x00000004u, 0x0004003bu, 0x0000001cu, 0x00000020u, 0x00000004u, 0x0004002bu, 0x00000006u, 0x00000023u,
0x00000001u, 0x0004002bu, 0x00000006u, 0x00000024u, 0x00000108u, 0x0004002bu, 0x00000006u, 0x00000025u,
0x00000002u, 0x0004003bu, 0x00000009u, 0x0000002au, 0x00000001u, 0x00040017u, 0x0000002cu, 0x00000006u,
0x00000004u, 0x0004001cu, 0x0000002du, 0x00000006u, 0x00000017u, 0x0004001cu, 0x0000002eu, 0x00000006u,
0x00000017u, 0x00030016u, 0x0000002fu, 0x00000020u, 0x00040017u, 0x00000030u, 0x0000002fu, 0x00000002u,
0x0004001cu, 0x00000031u, 0x00000030u, 0x00000017u, 0x0004001cu, 0x00000032u, 0x00000030u, 0x00000017u,
0x0004002bu, 0x00000006u, 0x00000033u, 0x00000006u, 0x0004001cu, 0x00000034u, 0x00000032u, 0x00000033u,
0x000d001eu, 0x00000035u, 0x00000006u, 0x00000006u, 0x00000006u, 0x00000006u, 0x00000006u, 0x0000002cu,
0x0000002du, 0x0000002eu, 0x00000031u, 0x00000034u, 0x00000030u, 0x00040020u, 0x00000036u, 0x0000000cu,
0x00000035u, 0x0004003bu, 0x00000036u, 0x00000037u, 0x0000000cu, 0x00040015u, 0x00000038u, 0x00000020u,
0x00000001u, 0x0004002bu, 0x00000038u, 0x00000039u, 0x00000002u, 0x00040020u, 0x0000003bu, 0x0000000cu,
0x00000006u, 0x0004003bu, 0x00000009u, 0x00000050u, 0x00000001u, 0x0004002bu, 0x00000006u, 0x0000005cu,
0x00000004u, 0x0004003bu, 0x00000009u, 0x0000005eu, 0x00000001u, 0x0004003bu, 0x00000009u, 0x00000064u,
0x00000001u, 0x0004002bu, 0x00000006u, 0x0000006bu, 0x00000008u, 0x00040020u, 0x0000006du, 0x00000007u,
0x0000000eu, 0x0004002bu, 0x00000038u, 0x00000099u, 0x00000004u, 0x0004002bu, 0x00000038u, 0x000000a0u,
0x00000006u, 0x0004002bu, 0x00000038u, 0x000000a6u, 0x00000007u, 0x00040020u, 0x000000b6u, 0x00000007u,
0x00000030u, 0x0004002bu, 0x0000002fu, 0x000000bbu, 0x00000000u, 0x0004002bu, 0x00000006u, 0x000000beu,
0x00000003u, 0x0004001cu, 0x000000c6u, 0x00000030u, 0x00000017u, 0x00040020u, 0x000000c7u, 0x00000004u,
0x000000c6u, 0x0004003bu, 0x000000c7u, 0x000000c8u, 0x00000004u, 0x00040020u, 0x000000cbu, 0x00000004u,
0x00000030u, 0x0004002bu, 0x00000038u, 0x000000d2u, 0x00000008u, 0x00040020u, 0x000000d7u, 0x0000000cu,
0x00000030u, 0x0004002bu, 0x00000038u, 0x000000eau, 0x00000003u, 0x0004002bu, 0x00000038u, 0x00000115u,
0x00000009u, 0x0004002bu, 0x00000038u, 0x0000011du, 0x00000001u, 0x0004002bu, 0x00000006u, 0x00000120u,
0x000001ffu, 0x0004002bu, 0x00000038u, 0x00000124u, 0x00000000u, 0x0004002bu, 0x0000002fu, 0x00000126u,
0x44000000u, 0x0004002bu, 0x00000038u, 0x0000012eu, 0x00000005u, 0x00040020u, 0x00000134u, 0x0000000cu,
0x0000002cu, 0x0004002bu, 0x00000038u, 0x00000136u, 0x0000000au, 0x0004002bu, 0x00000006u, 0x00000140u,
0x50323033u, 0x00040017u, 0x00000142u, 0x00000006u, 0x00000003u, 0x0004002bu, 0x00000006u, 0x00000143u,
0x00000010u, 0x0006002cu, 0x00000142u, 0x00000144u, 0x00000017u, 0x00000143u, 0x00000023u, 0x00050036u,
0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003bu, 0x00000007u,
0x00000008u, 0x00000007u, 0x0004003bu, 0x00000007u, 0x0000003du, 0x00000007u, 0x0004003bu, 0x0000006du,
0x0000006eu, 0x00000007u, 0x0004003bu, 0x0000006du, 0x00000074u, 0x00000007u, 0x0004003bu, 0x0000006du,
0x000000acu, 0x00000007u, 0x0004003bu, 0x000000b6u, 0x000000b7u, 0x00000007u, 0x0004003bu, 0x00000007u,
0x000000d9u, 0x00000007u, 0x0004003bu, 0x00000007u, 0x000000edu, 0x00000007u, 0x0004003du, 0x00000006u,
0x0000000bu, 0x0000000au, 0x0003003eu, 0x00000008u, 0x0000000bu, 0x0004003du, 0x00000006u, 0x0000000cu,
0x00000008u, 0x000500aau, 0x0000000eu, 0x0000000fu, 0x0000000cu, 0x0000000du, 0x000300f7u, 0x00000011u,
0x00000000u, 0x000400fau, 0x0000000fu, 0x00000010u, 0x00000011u, 0x000200f8u, 0x00000010u, 0x0003003eu,
0x00000013u, 0x0000000du, 0x0003003eu, 0x00000014u, 0x0000000du, 0x0003003eu, 0x00000015u, 0x0000000du,
0x000200f9u, 0x00000011u, 0x000200f8u, 0x00000011u, 0x0004003du, 0x00000006u, 0x00000016u, 0x00000008u,
0x000500b0u, 0x0000000eu, 0x00000018u, 0x00000016u, 0x00000017u, 0x000300f7u, 0x0000001au, 0x00000000u,
0x000400fau, 0x00000018u, 0x00000019u, 0x0000001au, 0x000200f8u, 0x00000019u, 0x0004003du, 0x00000006u,
0x0000001eu, 0x00000008u, 0x00050041u, 0x00000012u, 0x0000001fu, 0x0000001du, 0x0000001eu, 0x0003003eu,
0x0000001fu, 0x0000000du, 0x0004003du, 0x00000006u, 0x00000021u, 0x00000008u, 0x00050041u, 0x00000012u,
0x00000022u, 0x00000020u, 0x00000021u, 0x0003003eu, 0x00000022u, 0x0000000du, 0x000200f9u, 0x0000001au,
0x000200f8u, 0x0000001au, 0x000300e1u, 0x00000023u, 0x00000024u, 0x000400e0u, 0x00000025u, 0x00000025u,
0x00000024u, 0x0004003du, 0x00000006u, 0x00000026u, 0x00000008u, 0x000500aau, 0x0000000eu, 0x00000027u,
0x00000026u, 0x0000000du, 0x000300f7u, 0x00000029u, 0x00000000u, 0x000400fau, 0x00000027u, 0x00000028u,
0x00000029u, 0x000200f8u, 0x00000028u, 0x0004003du, 0x00000006u, 0x0000002bu, 0x0000002au, 0x0003003eu,
0x00000013u, 0x0000002bu, 0x0004003du, 0x00000006u, 0x0000003au, 0x0000002au, 0x00050041u, 0x0000003bu,
0x0000003cu, 0x00000037u, 0x00000039u, 0x0003003eu, 0x0000003cu, 0x0000003au, 0x000200f9u, 0x00000029u,
0x000200f8u, 0x00000029u, 0x000400e0u, 0x00000025u, 0x00000025u, 0x00000024u, 0x0004003du, 0x00000006u,
0x0000003eu, 0x00000013u, 0x0003003eu, 0x0000003du, 0x0000003eu, 0x0004003du, 0x00000006u, 0x0000003fu,
0x0000002au, 0x0004003du, 0x00000006u, 0x00000040u, 0x0000003du, 0x000500abu, 0x0000000eu, 0x00000041u,
0x0000003fu, 0x00000040u, 0x000300f7u, 0x00000043u, 0x00000000u, 0x000400fau, 0x00000041u, 0x00000042u,
0x00000043u, 0x000200f8u, 0x00000042u, 0x000700f1u, 0x00000006u, 0x00000044u, 0x00000014u, 0x00000023u,
0x0000000du, 0x00000023u, 0x000200f9u, 0x00000043u, 0x000200f8u, 0x00000043u, 0x0004003du, 0x00000006u,
0x00000045u, 0x0000002au, 0x000500b0u, 0x0000000eu, 0x00000046u, 0x00000045u, 0x00000025u, 0x000400a8u,
0x0000000eu, 0x00000047u, 0x00000046u, 0x000300f7u, 0x00000049u, 0x00000000u, 0x000400fau, 0x00000047u,
0x00000048u, 0x00000049u, 0x000200f8u, 0x00000048u, 0x0004003du, 0x00000006u, 0x0000004au, 0x0000002au,
0x000500acu, 0x0000000eu, 0x0000004bu, 0x0000004au, 0x00000017u, 0x000200f9u, 0x00000049u, 0x000200f8u,
0x00000049u, 0x000700f5u, 0x0000000eu, 0x0000004cu, 0x00000046u, 0x00000043u, 0x0000004bu, 0x00000048u,
0x000300f7u, 0x0000004eu, 0x00000000u, 0x000400fau, 0x0000004cu, 0x0000004du, 0x0000004eu, 0x000200f8u,
0x0000004du, 0x000700f1u, 0x00000006u, 0x0000004fu, 0x00000014u, 0x00000023u, 0x0000000du, 0x00000025u,
0x000200f9u, 0x0000004eu, 0x000200f8u, 0x0000004eu, 0x0004003du, 0x00000006u, 0x00000051u, 0x00000050u,
0x0004003du, 0x00000006u, 0x00000052u, 0x0000003du, 0x000500aeu, 0x0000000eu, 0x00000053u, 0x00000051u,
0x00000052u, 0x000400a8u, 0x0000000eu, 0x00000054u, 0x00000053u, 0x000300f7u, 0x00000056u, 0x00000000u,
0x000400fau, 0x00000054u, 0x00000055u, 0x00000056u, 0x000200f8u, 0x00000055u, 0x0004003du, 0x00000006u,
0x00000057u, 0x00000050u, 0x000500aeu, 0x0000000eu, 0x00000058u, 0x00000057u, 0x00000017u, 0x000200f9u,
0x00000056u, 0x000200f8u, 0x00000056u, 0x000700f5u, 0x0000000eu, 0x00000059u, 0x00000053u, 0x0000004eu,
0x00000058u, 0x00000055u, 0x000300f7u, 0x0000005bu, 0x00000000u, 0x000400fau, 0x00000059u, 0x0000005au,
0x0000005bu, 0x000200f8u, 0x0000005au, 0x000700f1u, 0x00000006u, 0x0000005du, 0x00000014u, 0x00000023u,
0x0000000du, 0x0000005cu, 0x000200f9u, 0x0000005bu, 0x000200f8u, 0x0000005bu, 0x0004003du, 0x00000006u,
0x0000005fu, 0x0000005eu, 0x000500aau, 0x0000000eu, 0x00000060u, 0x0000005fu, 0x0000000du, 0x000400a8u,
0x0000000eu, 0x00000061u, 0x00000060u, 0x000300f7u, 0x00000063u, 0x00000000u, 0x000400fau, 0x00000061u,
0x00000062u, 0x00000063u, 0x000200f8u, 0x00000062u, 0x0004003du, 0x00000006u, 0x00000065u, 0x00000064u,
0x0004003du, 0x00000006u, 0x00000066u, 0x0000005eu, 0x000500aeu, 0x0000000eu, 0x00000067u, 0x00000065u,
0x00000066u, 0x000200f9u, 0x00000063u, 0x000200f8u, 0x00000063u, 0x000700f5u, 0x0000000eu, 0x00000068u,
0x00000060u, 0x0000005bu, 0x00000067u, 0x00000062u, 0x000300f7u, 0x0000006au, 0x00000000u, 0x000400fau,
0x00000068u, 0x00000069u, 0x0000006au, 0x000200f8u, 0x00000069u, 0x000700f1u, 0x00000006u, 0x0000006cu,
0x00000014u, 0x00000023u, 0x0000000du, 0x0000006bu, 0x000200f9u, 0x0000006au, 0x000200f8u, 0x0000006au,
0x0004003du, 0x00000006u, 0x0000006fu, 0x0000003du, 0x000500aeu, 0x0000000eu, 0x00000070u, 0x0000006fu,
0x00000025u, 0x0004003du, 0x00000006u, 0x00000071u, 0x0000003du, 0x000500b2u, 0x0000000eu, 0x00000072u,
0x00000071u, 0x00000017u, 0x000500a7u, 0x0000000eu, 0x00000073u, 0x00000070u, 0x00000072u, 0x0003003eu,
0x0000006eu, 0x00000073u, 0x0004003du, 0x0000000eu, 0x00000075u, 0x0000006eu, 0x000300f7u, 0x00000077u,
0x00000000u, 0x000400fau, 0x00000075u, 0x00000076u, 0x00000077u, 0x000200f8u, 0x00000076u, 0x0004003du,
0x00000006u, 0x00000078u, 0x00000050u, 0x0004003du, 0x00000006u, 0x00000079u, 0x0000003du, 0x000500b0u,
0x0000000eu, 0x0000007au, 0x00000078u, 0x00000079u, 0x000200f9u, 0x00000077u, 0x000200f8u, 0x00000077u,
0x000700f5u, 0x0000000eu, 0x0000007bu, 0x00000075u, 0x0000006au, 0x0000007au, 0x00000076u, 0x0003003eu,
0x00000074u, 0x0000007bu, 0x0004003du, 0x0000000eu, 0x0000007cu, 0x00000074u, 0x000300f7u, 0x0000007eu,
0x00000000u, 0x000400fau, 0x0000007cu, 0x0000007du, 0x0000007eu, 0x000200f8u, 0x0000007du, 0x0004003du,
0x00000006u, 0x0000007fu, 0x00000050u, 0x000500c4u, 0x00000006u, 0x00000080u, 0x00000023u, 0x0000007fu,
0x000700f1u, 0x00000006u, 0x00000081u, 0x00000015u, 0x00000023u, 0x0000000du, 0x00000080u, 0x0004003du,
0x00000006u, 0x00000082u, 0x00000050u, 0x00050041u, 0x00000012u, 0x00000083u, 0x00000020u, 0x00000082u,
0x0004003du, 0x00000006u, 0x00000084u, 0x00000008u, 0x00050080u, 0x00000006u, 0x00000085u, 0x00000084u,
0x00000023u, 0x000700eau, 0x00000006u, 0x00000086u, 0x00000083u, 0x00000023u, 0x0000000du, 0x00000085u,
0x0004003du, 0x00000006u, 0x00000087u, 0x0000005eu, 0x000500abu, 0x0000000eu, 0x00000088u, 0x00000087u,
0x0000000du, 0x000300f7u, 0x0000008au, 0x00000000u, 0x000400fau, 0x00000088u, 0x00000089u, 0x0000008au,
0x000200f8u, 0x00000089u, 0x0004003du, 0x00000006u, 0x0000008bu, 0x00000064u, 0x0004003du, 0x00000006u,
0x0000008cu, 0x0000005eu, 0x00050082u, 0x00000006u, 0x0000008du, 0x0000008cu, 0x00000023u, 0x000500aau,
0x0000000eu, 0x0000008eu, 0x0000008bu, 0x0000008du, 0x000200f9u, 0x0000008au, 0x000200f8u, 0x0000008au,
0x000700f5u, 0x0000000eu, 0x0000008fu, 0x00000088u, 0x0000007du, 0x0000008eu, 0x00000089u, 0x000300f7u,
0x00000091u, 0x00000000u, 0x000400fau, 0x0000008fu, 0x00000090u, 0x00000091u, 0x000200f8u, 0x00000090u,
0x0004003du, 0x00000006u, 0x00000092u, 0x00000050u, 0x00050041u, 0x00000012u, 0x00000093u, 0x0000001du,
0x00000092u, 0x000700eau, 0x00000006u, 0x00000094u, 0x00000093u, 0x00000023u, 0x0000000du, 0x00000023u,
0x000200f9u, 0x00000091u, 0x000200f8u, 0x00000091u, 0x000200f9u, 0x0000007eu, 0x000200f8u, 0x0000007eu,
0x000300e1u, 0x00000023u, 0x00000024u, 0x000400e0u, 0x00000025u, 0x00000025u, 0x00000024u, 0x0004003du,
0x00000006u, 0x00000095u, 0x00000008u, 0x000500aau, 0x0000000eu, 0x00000096u, 0x00000095u, 0x0000000du,
0x000300f7u, 0x00000098u, 0x00000000u, 0x000400fau, 0x00000096u, 0x00000097u, 0x00000098u, 0x000200f8u,
0x00000097u, 0x0004003du, 0x00000006u, 0x0000009au, 0x00000015u, 0x00050041u, 0x0000003bu, 0x0000009bu,
0x00000037u, 0x00000099u, 0x0003003eu, 0x0000009bu, 0x0000009au, 0x000200f9u, 0x00000098u, 0x000200f8u,
0x00000098u, 0x0004003du, 0x00000006u, 0x0000009cu, 0x00000008u, 0x000500b0u, 0x0000000eu, 0x0000009du,
0x0000009cu, 0x00000017u, 0x000300f7u, 0x0000009fu, 0x00000000u, 0x000400fau, 0x0000009du, 0x0000009eu,
0x0000009fu, 0x000200f8u, 0x0000009eu, 0x0004003du, 0x00000006u, 0x000000a1u, 0x00000008u, 0x0004003du,
0x00000006u, 0x000000a2u, 0x00000008u, 0x00050041u, 0x00000012u, 0x000000a3u, 0x0000001du, 0x000000a2u,
0x0004003du, 0x00000006u, 0x000000a4u, 0x000000a3u, 0x00060041u, 0x0000003bu, 0x000000a5u, 0x00000037u,
0x000000a0u, 0x000000a1u, 0x0003003eu, 0x000000a5u, 0x000000a4u, 0x0004003du, 0x00000006u, 0x000000a7u,
0x00000008u, 0x0004003du, 0x00000006u, 0x000000a8u, 0x00000008u, 0x00050041u, 0x00000012u, 0x000000a9u,
0x00000020u, 0x000000a8u, 0x0004003du, 0x00000006u, 0x000000aau, 0x000000a9u, 0x00060041u, 0x0000003bu,
0x000000abu, 0x00000037u, 0x000000a6u, 0x000000a7u, 0x0003003eu, 0x000000abu, 0x000000aau, 0x000200f9u,
0x0000009fu, 0x000200f8u, 0x0000009fu, 0x0004003du, 0x0000000eu, 0x000000adu, 0x0000006eu, 0x000300f7u,
0x000000afu, 0x00000000u, 0x000400fau, 0x000000adu, 0x000000aeu, 0x000000afu, 0x000200f8u, 0x000000aeu,
0x0004003du, 0x00000006u, 0x000000b0u, 0x00000014u, 0x000500aau, 0x0000000eu, 0x000000b1u, 0x000000b0u,
0x0000000du, 0x000200f9u, 0x000000afu, 0x000200f8u, 0x000000afu, 0x000700f5u, 0x0000000eu, 0x000000b2u,
0x000000adu, 0x0000009fu, 0x000000b1u, 0x000000aeu, 0x0003003eu, 0x000000acu, 0x000000b2u, 0x0004003du,
0x0000000eu, 0x000000b3u, 0x000000acu, 0x000300f7u, 0x000000b5u, 0x00000000u, 0x000400fau, 0x000000b3u,
0x000000b4u, 0x000000b5u, 0x000200f8u, 0x000000b4u, 0x0004003du, 0x00000006u, 0x000000b8u, 0x0000000au,
0x00050080u, 0x00000006u, 0x000000b9u, 0x000000b8u, 0x00000023u, 0x00040070u, 0x0000002fu, 0x000000bau,
0x000000b9u, 0x00050050u, 0x00000030u, 0x000000bcu, 0x000000bau, 0x000000bbu, 0x0003003eu, 0x000000b7u,
0x000000bcu, 0x0004003du, 0x00000030u, 0x000000bdu, 0x000000b7u, 0x0006015eu, 0x00000030u, 0x000000bfu,
0x000000beu, 0x00000001u, 0x000000bdu, 0x0003003eu, 0x000000b7u, 0x000000bfu, 0x0004003du, 0x00000006u,
0x000000c0u, 0x00000064u, 0x0004003du, 0x00000006u, 0x000000c1u, 0x0000005eu, 0x00050082u, 0x00000006u,
0x000000c2u, 0x000000c1u, 0x00000023u, 0x000500aau, 0x0000000eu, 0x000000c3u, 0x000000c0u, 0x000000c2u,
0x000300f7u, 0x000000c5u, 0x00000000u, 0x000400fau, 0x000000c3u, 0x000000c4u, 0x000000c5u, 0x000200f8u,
0x000000c4u, 0x0004003du, 0x00000006u, 0x000000c9u, 0x00000050u, 0x0004003du, 0x00000030u, 0x000000cau,
0x000000b7u, 0x00050041u, 0x000000cbu, 0x000000ccu, 0x000000c8u, 0x000000c9u, 0x0003003eu, 0x000000ccu,
0x000000cau, 0x000200f9u, 0x000000c5u, 0x000200f8u, 0x000000c5u, 0x000400e0u, 0x00000025u, 0x00000025u,
0x00000024u, 0x0004003du, 0x00000006u, 0x000000cdu, 0x0000000au, 0x0004003du, 0x00000006u, 0x000000ceu,
0x0000002au, 0x000500b0u, 0x0000000eu, 0x000000cfu, 0x000000cdu, 0x000000ceu, 0x000300f7u, 0x000000d1u,
0x00000000u, 0x000400fau, 0x000000cfu, 0x000000d0u, 0x000000d1u, 0x000200f8u, 0x000000d0u, 0x0004003du,
0x00000006u, 0x000000d3u, 0x0000000au, 0x0004003du, 0x00000006u, 0x000000d4u, 0x0000000au, 0x00050041u,
0x000000cbu, 0x000000d5u, 0x000000c8u, 0x000000d4u, 0x0004003du, 0x00000030u, 0x000000d6u, 0x000000d5u,
0x00060041u, 0x000000d7u, 0x000000d8u, 0x00000037u, 0x000000d2u, 0x000000d3u, 0x0003003eu, 0x000000d8u,
0x000000d6u, 0x000200f9u, 0x000000d1u, 0x000200f8u, 0x000000d1u, 0x000400e0u, 0x00000025u, 0x00000025u,
0x00000024u, 0x0004003du, 0x00000006u, 0x000000dau, 0x0000002au, 0x0006000cu, 0x00000038u, 0x000000dbu,
0x00000001u, 0x0000004bu, 0x000000dau, 0x0004007cu, 0x00000006u, 0x000000dcu, 0x000000dbu, 0x0003003eu,
0x000000d9u, 0x000000dcu, 0x0004003du, 0x00000006u, 0x000000ddu, 0x0000002au, 0x0004003du, 0x00000006u,
0x000000deu, 0x000000d9u, 0x00050082u, 0x00000006u, 0x000000dfu, 0x000000deu, 0x00000023u, 0x000500c4u,
0x00000006u, 0x000000e0u, 0x00000023u, 0x000000dfu, 0x00050082u, 0x00000006u, 0x000000e1u, 0x000000ddu,
0x000000e0u, 0x000500acu, 0x0000000eu, 0x000000e2u, 0x000000e1u, 0x0000000du, 0x000600a9u, 0x00000006u,
0x000000e3u, 0x000000e2u, 0x00000023u, 0x0000000du, 0x0004003du, 0x00000006u, 0x000000e4u, 0x000000d9u,
0x00050080u, 0x00000006u, 0x000000e5u, 0x000000e4u, 0x000000e3u, 0x0003003eu, 0x000000d9u, 0x000000e5u,
0x0004003du, 0x00000006u, 0x000000e6u, 0x0000000au, 0x000500aau, 0x0000000eu, 0x000000e7u, 0x000000e6u,
0x0000000du, 0x000300f7u, 0x000000e9u, 0x00000000u, 0x000400fau, 0x000000e7u, 0x000000e8u, 0x000000e9u,
0x000200f8u, 0x000000e8u, 0x0004003du, 0x00000006u, 0x000000ebu, 0x000000d9u, 0x00050041u, 0x0000003bu,
0x000000ecu, 0x00000037u, 0x000000eau, 0x0003003eu, 0x000000ecu, 0x000000ebu, 0x000200f9u, 0x000000e9u,
0x000200f8u, 0x000000e9u, 0x0003003eu, 0x000000edu, 0x0000000du, 0x000200f9u, 0x000000eeu, 0x000200f8u,
0x000000eeu, 0x000400f6u, 0x000000f0u, 0x000000f1u, 0x00000000u, 0x000200f9u, 0x000000f2u, 0x000200f8u,
0x000000f2u, 0x0004003du, 0x00000006u, 0x000000f3u, 0x000000edu, 0x0004003du, 0x00000006u, 0x000000f4u,
0x000000d9u, 0x000500b0u, 0x0000000eu, 0x000000f5u, 0x000000f3u, 0x000000f4u, 0x000400fau, 0x000000f5u,
0x000000efu, 0x000000f0u, 0x000200f8u, 0x000000efu, 0x0004003du, 0x00000006u, 0x000000f6u, 0x00000050u,
0x0004003du, 0x00000006u, 0x000000f7u, 0x000000edu, 0x000500c4u, 0x00000006u, 0x000000f8u, 0x00000023u,
0x000000f7u, 0x000500c7u, 0x00000006u, 0x000000f9u, 0x000000f6u, 0x000000f8u, 0x000500acu, 0x0000000eu,
0x000000fau, 0x000000f9u, 0x0000000du, 0x000300f7u, 0x000000fcu, 0x00000000u, 0x000400fau, 0x000000fau,
0x000000fbu, 0x000000fcu, 0x000200f8u, 0x000000fbu, 0x0004003du, 0x00000006u, 0x000000fdu, 0x00000050u,
0x0004003du, 0x00000006u, 0x000000feu, 0x000000edu, 0x000500c2u, 0x00000006u, 0x000000ffu, 0x000000fdu,
0x000000feu, 0x0004003du, 0x00000006u, 0x00000100u, 0x000000edu, 0x000500c4u, 0x00000006u, 0x00000101u,
0x000000ffu, 0x00000100u, 0x00050082u, 0x00000006u, 0x00000102u, 0x00000101u, 0x00000023u, 0x00050041u,
0x000000cbu, 0x00000103u, 0x000000c8u, 0x00000102u, 0x0004003du, 0x00000030u, 0x00000104u, 0x00000103u,
0x0004003du, 0x00000030u, 0x00000105u, 0x000000b7u, 0x00050081u, 0x00000030u, 0x00000106u, 0x00000105u,
0x00000104u, 0x0003003eu, 0x000000b7u, 0x00000106u, 0x0004003du, 0x00000006u, 0x00000107u, 0x00000064u,
0x0004003du, 0x00000006u, 0x00000108u, 0x0000005eu, 0x00050082u, 0x00000006u, 0x00000109u, 0x00000108u,
0x00000023u, 0x000500aau, 0x0000000eu, 0x0000010au, 0x00000107u, 0x00000109u, 0x000300f7u, 0x0000010cu,
0x00000000u, 0x000400fau, 0x0000010au, 0x0000010bu, 0x0000010cu, 0x000200f8u, 0x0000010bu, 0x0004003du,
0x00000006u, 0x0000010du, 0x00000050u, 0x0004003du, 0x00000030u, 0x0000010eu, 0x000000b7u, 0x00050041u,
0x000000cbu, 0x0000010fu, 0x000000c8u, 0x0000010du, 0x0003003eu, 0x0000010fu, 0x0000010eu, 0x000200f9u,
0x0000010cu, 0x000200f8u, 0x0000010cu, 0x000200f9u, 0x000000fcu, 0x000200f8u, 0x000000fcu, 0x000400e0u,
0x00000025u, 0x00000025u, 0x00000024u, 0x0004003du, 0x00000006u, 0x00000110u, 0x0000000au, 0x0004003du,
0x00000006u, 0x00000111u, 0x0000002au, 0x000500b0u, 0x0000000eu, 0x00000112u, 0x00000110u, 0x00000111u,
0x000300f7u, 0x00000114u, 0x00000000u, 0x000400fau, 0x00000112u, 0x00000113u, 0x00000114u, 0x000200f8u,
0x00000113u, 0x0004003du, 0x00000006u, 0x00000116u, 0x000000edu, 0x0004003du, 0x00000006u, 0x00000117u,
0x0000000au, 0x0004003du, 0x00000006u, 0x00000118u, 0x0000000au, 0x00050041u, 0x000000cbu, 0x00000119u,
0x000000c8u, 0x00000118u, 0x0004003du, 0x00000030u, 0x0000011au, 0x00000119u, 0x00070041u, 0x000000d7u,
0x0000011bu, 0x00000037u, 0x00000115u, 0x00000116u, 0x00000117u, 0x0003003eu, 0x0000011bu, 0x0000011au,
0x000200f9u, 0x00000114u, 0x000200f8u, 0x00000114u, 0x000400e0u, 0x00000025u, 0x00000025u, 0x00000024u,
0x000200f9u, 0x000000f1u, 0x000200f8u, 0x000000f1u, 0x0004003du, 0x00000006u, 0x0000011cu, 0x000000edu,
0x00050080u, 0x00000006u, 0x0000011eu, 0x0000011cu, 0x0000011du, 0x0003003eu, 0x000000edu, 0x0000011eu,
0x000200f9u, 0x000000eeu, 0x000200f8u, 0x000000f0u, 0x0004003du, 0x00000006u, 0x0000011fu, 0x0000000au,
0x000500aau, 0x0000000eu, 0x00000121u, 0x0000011fu, 0x00000120u, 0x000300f7u, 0x00000123u, 0x00000000u,
0x000400fau, 0x00000121u, 0x00000122u, 0x00000123u, 0x000200f8u, 0x00000122u, 0x0004003du, 0x00000030u,
0x00000125u, 0x000000b7u, 0x00050050u, 0x00000030u, 0x00000127u, 0x00000126u, 0x00000126u, 0x00050088u,
0x00000030u, 0x00000128u, 0x00000125u, 0x00000127u, 0x00050041u, 0x000000cbu, 0x00000129u, 0x000000c8u,
0x00000124u, 0x0003003eu, 0x00000129u, 0x00000128u, 0x000200f9u, 0x00000123u, 0x000200f8u, 0x00000123u,
0x000400e0u, 0x00000025u, 0x00000025u, 0x00000024u, 0x0004003du, 0x00000006u, 0x0000012au, 0x0000000au,
0x000500aau, 0x0000000eu, 0x0000012bu, 0x0000012au, 0x00000120u, 0x000300f7u, 0x0000012du, 0x00000000u,
0x000400fau, 0x0000012bu, 0x0000012cu, 0x0000012du, 0x000200f8u, 0x0000012cu, 0x0004003du, 0x00000006u,
0x0000012fu, 0x0000005eu, 0x0004003du, 0x00000006u, 0x00000130u, 0x0000002au, 0x0004003du, 0x00000006u,
0x00000131u, 0x00000050u, 0x0004003du, 0x00000006u, 0x00000132u, 0x00000064u, 0x00070050u, 0x0000002cu,
0x00000133u, 0x0000012fu, 0x00000130u, 0x00000131u, 0x00000132u, 0x00050041u, 0x00000134u, 0x00000135u,
0x00000037u, 0x0000012eu, 0x0003003eu, 0x00000135u, 0x00000133u, 0x00050041u, 0x000000cbu, 0x00000137u,
0x000000c8u, 0x00000124u, 0x0004003du, 0x00000030u, 0x00000138u, 0x00000137u, 0x00050041u, 0x000000d7u,
0x00000139u, 0x00000037u, 0x00000136u, 0x0003003eu, 0x00000139u, 0x00000138u, 0x000200f9u, 0x0000012du,
0x000200f8u, 0x0000012du, 0x000200f9u, 0x000000b5u, 0x000200f8u, 0x000000b5u, 0x000400e0u, 0x00000025u,
0x00000025u, 0x00000024u, 0x0004003du, 0x00000006u, 0x0000013au, 0x00000008u, 0x000500aau, 0x0000000eu,
0x0000013bu, 0x0000013au, 0x0000000du, 0x000300f7u, 0x0000013du, 0x00000000u, 0x000400fau, 0x0000013bu,
0x0000013cu, 0x0000013du, 0x000200f8u, 0x0000013cu, 0x0004003du, 0x00000006u, 0x0000013eu, 0x00000014u,
0x00050041u, 0x0000003bu, 0x0000013fu, 0x00000037u, 0x0000011du, 0x0003003eu, 0x0000013fu, 0x0000013eu,
0x00050041u, 0x0000003bu, 0x00000141u, 0x00000037u, 0x00000124u, 0x0003003eu, 0x00000141u, 0x00000140u,
0x000200f9u, 0x0000013du, 0x000200f8u, 0x0000013du, 0x000100fdu, 0x00010038u,
};
inline constexpr std::size_t kDriverPostProgram203WitnessSpvWordCount =
sizeof(kDriverPostProgram203WitnessSpv) / sizeof(kDriverPostProgram203WitnessSpv[0]);
} // namespace MobileGL::MG_Util::SelfTest
@@ -25,7 +25,6 @@
#include "SpirvPasses/SplitArrayVertexInputsPass.h" #include "SpirvPasses/SplitArrayVertexInputsPass.h"
#include "SpirvPasses/RebaseInstanceIndexPass.h" #include "SpirvPasses/RebaseInstanceIndexPass.h"
#include "SpirvPasses/ZeroBaseVertexPass.h" #include "SpirvPasses/ZeroBaseVertexPass.h"
#include "SpirvPasses/DeriveNumSubgroupsPass.h"
#include "SpirvPasses/NormalizeRectCoordinatesPass.h" #include "SpirvPasses/NormalizeRectCoordinatesPass.h"
#include "SpirvPasses/Lower1DArrayImagesPass.h" #include "SpirvPasses/Lower1DArrayImagesPass.h"
#include "SpirvPasses/BakeImageFormatsPass.h" #include "SpirvPasses/BakeImageFormatsPass.h"
@@ -884,17 +883,6 @@ namespace MobileGL {
return RunOptimizerChecked("ZeroBaseVertexForVulkan", optimizer, inputBinary, outputBinary, true, enableSpirvValidation); return RunOptimizerChecked("ZeroBaseVertexForVulkan", optimizer, inputBinary, outputBinary, true, enableSpirvValidation);
} }
bool ShaderCompiler::DeriveNumSubgroupsForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(DeriveNumSubgroupsPass::CreateDeriveNumSubgroupsPass());
return RunOptimizerChecked("DeriveNumSubgroupsForVulkan", optimizer, inputBinary,
outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary, bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) { Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) {
using namespace spvtools; using namespace spvtools;
@@ -145,13 +145,6 @@ namespace MobileGL {
static bool ZeroBaseVertexForVulkan(const Vector<Uint32>& inputBinary, static bool ZeroBaseVertexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary, Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false); bool enableSpirvValidation = false);
// Replaces compute gl_NumSubgroups loads with the value derived from the local
// workgroup dimensions and gl_SubgroupSize. DirectVulkan only; this avoids a
// driver builtin that can disagree with the subgroup IDs the same dispatch emits.
// See DeriveNumSubgroupsPass.
static bool DeriveNumSubgroupsForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Re-declares 64-bit float vertex inputs as their 32-bit unsigned word pair // Re-declares 64-bit float vertex inputs as their 32-bit unsigned word pair
// (double -> uvec2, dvec2 -> uvec4) and bitcasts them back to double at entry, so no // (double -> uvec2, dvec2 -> uvec4) and bitcasts them back to double at entry, so no
// VK_FORMAT_R64*_SFLOAT is needed - lavapipe advertises none of them for vertex // VK_FORMAT_R64*_SFLOAT is needed - lavapipe advertises none of them for vertex
@@ -1,251 +0,0 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "DeriveNumSubgroupsPass.h"
#include "spirv.hpp"
#include "source/opt/constants.h"
#include "source/opt/def_use_manager.h"
#include "source/opt/instruction.h"
#include "source/opt/ir_context.h"
#include "source/opt/module.h"
#include "source/util/make_unique.h"
#include <vector>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
namespace {
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
using spvtools::opt::Operand;
Instruction* FindBuiltinDefinition(IRContext* context, spv::BuiltIn builtin) {
auto* defUseMgr = context->get_def_use_mgr();
for (auto& annotation : context->annotations()) {
if (annotation.opcode() != spv::Op::OpDecorate || annotation.NumInOperands() < 3) {
continue;
}
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
spv::Decoration::BuiltIn) {
continue;
}
if (static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2)) != builtin) {
continue;
}
return defUseMgr->GetDef(annotation.GetSingleWordInOperand(0));
}
return nullptr;
}
bool IsInputPointerTo(IRContext* context, const Instruction* variable, uint32_t pointeeTypeId) {
if (variable == nullptr || variable->opcode() != spv::Op::OpVariable ||
variable->NumInOperands() < 1 ||
static_cast<spv::StorageClass>(variable->GetSingleWordInOperand(0)) !=
spv::StorageClass::Input) {
return false;
}
const Instruction* pointerType = context->get_def_use_mgr()->GetDef(variable->type_id());
return pointerType != nullptr && pointerType->opcode() == spv::Op::OpTypePointer &&
pointerType->NumInOperands() >= 2 &&
static_cast<spv::StorageClass>(pointerType->GetSingleWordInOperand(0)) ==
spv::StorageClass::Input &&
pointerType->GetSingleWordInOperand(1) == pointeeTypeId;
}
bool IsUnsignedInt32(IRContext* context, uint32_t typeId) {
const Instruction* type = context->get_def_use_mgr()->GetDef(typeId);
return type != nullptr && type->opcode() == spv::Op::OpTypeInt &&
type->NumInOperands() >= 2 && type->GetSingleWordInOperand(0) == 32u &&
type->GetSingleWordInOperand(1) == 0u;
}
uint32_t SynthesizeSubgroupSizeVariable(IRContext* context, uint32_t pointerTypeId) {
const uint32_t variableId = context->TakeNextId();
context->AddGlobalValue(spvtools::MakeUnique<Instruction>(
context, spv::Op::OpVariable, pointerTypeId, variableId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_STORAGE_CLASS,
{static_cast<uint32_t>(spv::StorageClass::Input)}}}));
context->AddAnnotationInst(spvtools::MakeUnique<Instruction>(
context, spv::Op::OpDecorate, 0, 0,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {variableId}},
{SPV_OPERAND_TYPE_DECORATION,
{static_cast<uint32_t>(spv::Decoration::BuiltIn)}},
{SPV_OPERAND_TYPE_LITERAL_INTEGER,
{static_cast<uint32_t>(spv::BuiltIn::SubgroupSize)}}}));
for (Instruction& entryPoint : context->module()->entry_points()) {
entryPoint.AddOperand({SPV_OPERAND_TYPE_ID, {variableId}});
}
return variableId;
}
} // namespace
spvtools::opt::Pass::Status DeriveNumSubgroupsPass::Process() {
auto* irContext = context();
auto* defUseMgr = irContext->get_def_use_mgr();
Instruction* numSubgroupsVar = FindBuiltinDefinition(irContext, spv::BuiltIn::NumSubgroups);
if (numSubgroupsVar == nullptr) {
return Status::SuccessWithoutChange;
}
std::vector<Instruction*> numSubgroupsLoads;
bool sawUnexpectedUser = false;
const uint32_t numSubgroupsVarId = numSubgroupsVar->result_id();
defUseMgr->ForEachUser(numSubgroupsVar, [&](Instruction* user) {
switch (user->opcode()) {
case spv::Op::OpLoad:
if (user->NumInOperands() >= 1 &&
user->GetSingleWordInOperand(0) == numSubgroupsVarId) {
numSubgroupsLoads.push_back(user);
} else {
sawUnexpectedUser = true;
}
return;
case spv::Op::OpDecorate:
case spv::Op::OpDecorateId:
case spv::Op::OpDecorateString:
case spv::Op::OpName:
case spv::Op::OpEntryPoint:
return;
default:
sawUnexpectedUser = true;
return;
}
});
if (sawUnexpectedUser) {
return Status::Failure;
}
if (numSubgroupsLoads.empty()) {
return Status::SuccessWithoutChange;
}
const uint32_t valueTypeId = numSubgroupsLoads.front()->type_id();
if (!IsUnsignedInt32(irContext, valueTypeId) ||
!IsInputPointerTo(irContext, numSubgroupsVar, valueTypeId)) {
return Status::Failure;
}
for (const Instruction* load : numSubgroupsLoads) {
if (load->type_id() != valueTypeId) {
return Status::Failure;
}
}
Instruction* workgroupSize = FindBuiltinDefinition(irContext, spv::BuiltIn::WorkgroupSize);
if (workgroupSize == nullptr ||
(workgroupSize->opcode() != spv::Op::OpConstantComposite &&
workgroupSize->opcode() != spv::Op::OpSpecConstantComposite)) {
return Status::Failure;
}
const Instruction* workgroupSizeType = defUseMgr->GetDef(workgroupSize->type_id());
if (workgroupSizeType == nullptr || workgroupSizeType->opcode() != spv::Op::OpTypeVector ||
workgroupSizeType->NumInOperands() < 2 ||
workgroupSizeType->GetSingleWordInOperand(0) != valueTypeId ||
workgroupSizeType->GetSingleWordInOperand(1) != 3u) {
return Status::Failure;
}
Instruction* subgroupSizeVar = FindBuiltinDefinition(irContext, spv::BuiltIn::SubgroupSize);
if (subgroupSizeVar != nullptr &&
!IsInputPointerTo(irContext, subgroupSizeVar, valueTypeId)) {
return Status::Failure;
}
auto* constantMgr = irContext->get_constant_mgr();
auto* typeMgr = irContext->get_type_mgr();
const auto* valueType = typeMgr->GetType(valueTypeId);
if (valueType == nullptr) {
return Status::Failure;
}
const auto* one = constantMgr->GetConstant(valueType, {1u});
const Instruction* oneInst =
one != nullptr ? constantMgr->GetDefiningInstruction(one, valueTypeId) : nullptr;
if (oneInst == nullptr) {
return Status::Failure;
}
const uint32_t oneId = oneInst->result_id();
const uint32_t subgroupSizeVarId = subgroupSizeVar != nullptr
? subgroupSizeVar->result_id()
: SynthesizeSubgroupSizeVariable(irContext, numSubgroupsVar->type_id());
const uint32_t workgroupSizeId = workgroupSize->result_id();
// The pipeline never enables ALLOW_VARYING_SUBGROUP_SIZE, so Vulkan's fixed
// subgroup partition is exactly ceil(local invocation count / SubgroupSize).
// `(count - 1) / size + 1` avoids an addition overflow at count + size - 1.
for (Instruction* load : numSubgroupsLoads) {
const uint32_t localSizeXId = irContext->TakeNextId();
const uint32_t localSizeYId = irContext->TakeNextId();
const uint32_t localSizeZId = irContext->TakeNextId();
const uint32_t localSizeXYId = irContext->TakeNextId();
const uint32_t invocationCountId = irContext->TakeNextId();
const uint32_t adjustedCountId = irContext->TakeNextId();
const uint32_t subgroupSizeId = irContext->TakeNextId();
const uint32_t quotientId = irContext->TakeNextId();
load->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpCompositeExtract, valueTypeId, localSizeXId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {workgroupSizeId}},
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {0u}}}));
load->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpCompositeExtract, valueTypeId, localSizeYId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {workgroupSizeId}},
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {1u}}}));
load->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpCompositeExtract, valueTypeId, localSizeZId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {workgroupSizeId}},
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {2u}}}));
load->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpIMul, valueTypeId, localSizeXYId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {localSizeXId}},
{SPV_OPERAND_TYPE_ID, {localSizeYId}}}));
load->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpIMul, valueTypeId, invocationCountId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {localSizeXYId}},
{SPV_OPERAND_TYPE_ID, {localSizeZId}}}));
load->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpISub, valueTypeId, adjustedCountId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {invocationCountId}},
{SPV_OPERAND_TYPE_ID, {oneId}}}));
load->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpLoad, valueTypeId, subgroupSizeId,
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {subgroupSizeVarId}}}));
load->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpUDiv, valueTypeId, quotientId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {adjustedCountId}},
{SPV_OPERAND_TYPE_ID, {subgroupSizeId}}}));
// Preserve the original result id so every downstream use automatically sees
// the derived value instead of the driver's NumSubgroups builtin.
load->SetOpcode(spv::Op::OpIAdd);
load->SetInOperands(Instruction::OperandList{
{SPV_OPERAND_TYPE_ID, {quotientId}},
{SPV_OPERAND_TYPE_ID, {oneId}}});
}
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken DeriveNumSubgroupsPass::CreateDeriveNumSubgroupsPass() {
return spvtools::Optimizer::PassToken(MakeUnique<DeriveNumSubgroupsPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -1,37 +0,0 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.h
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include "source/opt/pass.h"
#include "spirv-tools/optimizer.hpp"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// Replaces compute-stage NumSubgroups builtin loads with
// ceil(WorkgroupSize.x * WorkgroupSize.y * WorkgroupSize.z / SubgroupSize).
//
// That is the value Vulkan defines for NumSubgroups when the pipeline does not
// enable varying subgroup sizes, which MobileGL never does. Deriving it avoids
// drivers that expose the real SubgroupId topology but return an inconsistent
// NumSubgroups value. This is a DirectVulkan semantic repair, not a source-shader
// rewrite; the application's subgroup arithmetic and shared-memory logic remain
// unchanged.
class DeriveNumSubgroupsPass : public spvtools::opt::Pass {
public:
const char* name() const override { return "derive-num-subgroups"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateDeriveNumSubgroupsPass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
+1 -1
View File
@@ -86,7 +86,7 @@ val pluginRendererConfig = buildJsonValue {
selectable( selectable(
key = "MOBILEGL_BACKEND_TYPE", key = "MOBILEGL_BACKEND_TYPE",
title = RendererConfig.MetaString("mobilegl_backend_type_title"), title = RendererConfig.MetaString("mobilegl_backend_type_title"),
items = RendererConfig.EnvItems("DirectGLES", listOf("DirectVulkan")), items = RendererConfig.EnvItems("DirectGLES", listOf("DirectVulkan", "DiligentVulkan")),
) )
toggleable("MOBILEGL_DISABLE_TIMERQUERY", "1", false, RendererConfig.MetaString("mobilegl_disable_timerquery_title")) toggleable("MOBILEGL_DISABLE_TIMERQUERY", "1", false, RendererConfig.MetaString("mobilegl_disable_timerquery_title"))
toggleable("MOBILEGL_DISABLE_SUBGROUP", "1", false, RendererConfig.MetaString("mobilegl_disable_subgroup_title")) toggleable("MOBILEGL_DISABLE_SUBGROUP", "1", false, RendererConfig.MetaString("mobilegl_disable_subgroup_title"))
@@ -223,8 +223,7 @@ target_include_directories(glretrace_common PUBLIC
"${APITRACE_GENERATED_DIR}" "${APITRACE_GENERATED_DIR}"
"${APITRACE_ROOT}/dispatch" "${APITRACE_ROOT}/dispatch"
"${APITRACE_ROOT}/helpers" "${APITRACE_ROOT}/helpers"
"${APITRACE_ROOT}/retrace" "${APITRACE_ROOT}/retrace")
"${CMAKE_CURRENT_LIST_DIR}/../../../../../tools/trace_replay")
target_compile_definitions(glretrace_common PRIVATE target_compile_definitions(glretrace_common PRIVATE
main=mobilegl_apitrace_main) main=mobilegl_apitrace_main)
target_redirect_exit(glretrace_common) target_redirect_exit(glretrace_common)
@@ -232,16 +231,14 @@ target_link_libraries(glretrace_common PUBLIC retrace_common glhelpers glproc)
add_library(trace_replay_runner SHARED add_library(trace_replay_runner SHARED
trace_replay_core.cpp trace_replay_core.cpp
trace_replay_jni.cpp trace_replay_jni.cpp)
"${CMAKE_CURRENT_LIST_DIR}/../../../../../tools/trace_replay/apitrace_fbo_dump.cpp")
target_compile_features(trace_replay_runner PRIVATE cxx_std_17) target_compile_features(trace_replay_runner PRIVATE cxx_std_17)
target_compile_definitions(trace_replay_runner PRIVATE target_compile_definitions(trace_replay_runner PRIVATE
MOBILEGL_APITRACE_RETRACE_MAIN=mobilegl_apitrace_main) MOBILEGL_APITRACE_RETRACE_MAIN=mobilegl_apitrace_main)
target_include_directories(trace_replay_runner PRIVATE target_include_directories(trace_replay_runner PRIVATE
"${APITRACE_ROOT}/lib/image" "${APITRACE_ROOT}/lib/image")
"${CMAKE_CURRENT_LIST_DIR}/../../../../../tools/trace_replay")
target_link_libraries(trace_replay_runner target_link_libraries(trace_replay_runner
glretrace_common glretrace_common
@@ -1,4 +1,3 @@
#include "apitrace_fbo_dump.hpp"
#include "glws.hpp" #include "glws.hpp"
#include "retrace.hpp" #include "retrace.hpp"
@@ -376,7 +375,6 @@ bool makeCurrentInternal(Drawable *drawable, Drawable *readable, Context *contex
} }
gCurrentDrawable = drawable; gCurrentDrawable = drawable;
gCurrentContext = eglContext; gCurrentContext = eglContext;
mobilegl_trace_dump::InstallIfRequested();
return true; return true;
} }
@@ -164,11 +164,6 @@ bool LoadMobileGL(const Request& request, std::string& error) {
} else { } else {
unsetenv("MOBILEGL_COHERENT_AS_FLUSH"); unsetenv("MOBILEGL_COHERENT_AS_FLUSH");
} }
if (request.numSubgroupsQuirk) {
setenv("MOBILEGL_NUM_SUBGROUPS_QUIRK", "1", 1);
} else {
unsetenv("MOBILEGL_NUM_SUBGROUPS_QUIRK");
}
if (request.fboAttachmentDumps.empty()) { if (request.fboAttachmentDumps.empty()) {
unsetenv("MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS"); unsetenv("MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS");
} else { } else {
@@ -181,18 +176,6 @@ bool LoadMobileGL(const Request& request, std::string& error) {
} }
setenv("MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS", dumpPoints.c_str(), 1); setenv("MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS", dumpPoints.c_str(), 1);
} }
if (request.texture2dDumps.empty()) {
unsetenv("MOBILEGL_TRACE_DUMP_TEXTURE_2D");
} else {
std::string dumpPoints;
for (const std::string& dumpPoint : request.texture2dDumps) {
if (!dumpPoints.empty()) {
dumpPoints += ';';
}
dumpPoints += dumpPoint;
}
setenv("MOBILEGL_TRACE_DUMP_TEXTURE_2D", dumpPoints.c_str(), 1);
}
void* handle = dlopen(request.mobileGlLibrary.c_str(), RTLD_NOW | RTLD_GLOBAL); void* handle = dlopen(request.mobileGlLibrary.c_str(), RTLD_NOW | RTLD_GLOBAL);
if (handle == nullptr) { if (handle == nullptr) {
@@ -400,13 +383,6 @@ std::string SnapshotCallSet(const Request& request) {
callSet += "," + call; callSet += "," + call;
} }
} }
for (const std::string& dumpPoint : request.texture2dDumps) {
const std::size_t separator = dumpPoint.find(',');
const std::string call = dumpPoint.substr(0, separator);
if (!call.empty() && call != std::to_string(request.targetCall)) {
callSet += "," + call;
}
}
return callSet; return callSet;
} }
@@ -823,7 +799,6 @@ bool WriteResultJson(const Request& request, const Result& result) {
<< (request.avoidAngleLlvmpipeSamplerMipmapMinFilter ? "true" : "false") << ",\n"; << (request.avoidAngleLlvmpipeSamplerMipmapMinFilter ? "true" : "false") << ",\n";
file << " \"avoidAngleLlvmpipeExplicitLodBias\": " file << " \"avoidAngleLlvmpipeExplicitLodBias\": "
<< (request.avoidAngleLlvmpipeExplicitLodBias ? "true" : "false") << ",\n"; << (request.avoidAngleLlvmpipeExplicitLodBias ? "true" : "false") << ",\n";
file << " \"numSubgroupsQuirk\": " << (request.numSubgroupsQuirk ? "true" : "false") << ",\n";
file << " \"holdMs\": " << request.holdMs << ",\n"; file << " \"holdMs\": " << request.holdMs << ",\n";
file << " \"mismatchPixels\": " << result.mismatchPixels << "\n"; file << " \"mismatchPixels\": " << result.mismatchPixels << "\n";
file << "}\n"; file << "}\n";
@@ -27,9 +27,6 @@ struct Request {
// Framebuffer-attachment dump points, each `CALL:DIR[:FBO,FBO,...]`. Debug-only; the // Framebuffer-attachment dump points, each `CALL:DIR[:FBO,FBO,...]`. Debug-only; the
// replay behaves exactly as before when this is empty. // replay behaves exactly as before when this is empty.
std::vector<std::string> fboAttachmentDumps; std::vector<std::string> fboAttachmentDumps;
// Named GL_TEXTURE_2D dump points, each `CALL,TEXTURE,LEVEL,DIR`. Debug-only; the replay
// behaves exactly as before when this is empty.
std::vector<std::string> texture2dDumps;
int targetFrame = -1; int targetFrame = -1;
long long targetCall = -1; long long targetCall = -1;
int width = 0; int width = 0;
@@ -44,7 +41,6 @@ struct Request {
bool avoidAngleLlvmpipeSamplerMipmapMinFilter = false; bool avoidAngleLlvmpipeSamplerMipmapMinFilter = false;
bool avoidAngleLlvmpipeExplicitLodBias = false; bool avoidAngleLlvmpipeExplicitLodBias = false;
bool coherentAsFlush = false; bool coherentAsFlush = false;
bool numSubgroupsQuirk = false;
int holdMs = 0; int holdMs = 0;
}; };
@@ -6,7 +6,6 @@
#include <exception> #include <exception>
#include <string> #include <string>
#include <vector>
#include <sys/stat.h> #include <sys/stat.h>
extern "C" void mobilegl_trace_set_native_window(ANativeWindow *window); extern "C" void mobilegl_trace_set_native_window(ANativeWindow *window);
@@ -26,23 +25,6 @@ std::string ToString(JNIEnv* env, jstring value) {
return out; return out;
} }
std::vector<std::string> SplitSemicolonList(const std::string& value) {
std::vector<std::string> values;
std::size_t begin = 0;
while (begin < value.size()) {
const std::size_t end = value.find(';', begin);
const std::string entry = value.substr(begin, end - begin);
if (!entry.empty()) {
values.push_back(entry);
}
if (end == std::string::npos) {
break;
}
begin = end + 1;
}
return values;
}
jobject MakeResult(JNIEnv* env, const mobilegl_trace::Result& result) { jobject MakeResult(JNIEnv* env, const mobilegl_trace::Result& result) {
jclass clazz = env->FindClass("top/mobilegl/plugin/trace/TraceReplayActivity$TraceReplayResult"); jclass clazz = env->FindClass("top/mobilegl/plugin/trace/TraceReplayActivity$TraceReplayResult");
if (clazz == nullptr) { if (clazz == nullptr) {
@@ -121,9 +103,7 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
jboolean usePbuffer, jboolean usePbuffer,
jboolean avoidAngleLlvmpipeSamplerMipmapMinFilter, jboolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
jboolean avoidAngleLlvmpipeExplicitLodBias, jboolean avoidAngleLlvmpipeExplicitLodBias,
jboolean coherentAsFlush, jboolean coherentAsFlush) {
jboolean numSubgroupsQuirk,
jstring texture2dDumps) {
mobilegl_trace::Request request; mobilegl_trace::Request request;
request.tracePath = ToString(env, tracePath); request.tracePath = ToString(env, tracePath);
request.goldenPath = ToString(env, goldenPath); request.goldenPath = ToString(env, goldenPath);
@@ -135,7 +115,6 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
request.diffPath = ToString(env, diffPath); request.diffPath = ToString(env, diffPath);
request.backend = ToString(env, backend); request.backend = ToString(env, backend);
request.angleVariant = ToString(env, angleVariant); request.angleVariant = ToString(env, angleVariant);
request.texture2dDumps = SplitSemicolonList(ToString(env, texture2dDumps));
request.targetFrame = targetFrame; request.targetFrame = targetFrame;
request.targetCall = targetCall; request.targetCall = targetCall;
request.width = width; request.width = width;
@@ -151,7 +130,6 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
avoidAngleLlvmpipeSamplerMipmapMinFilter == JNI_TRUE; avoidAngleLlvmpipeSamplerMipmapMinFilter == JNI_TRUE;
request.avoidAngleLlvmpipeExplicitLodBias = avoidAngleLlvmpipeExplicitLodBias == JNI_TRUE; request.avoidAngleLlvmpipeExplicitLodBias = avoidAngleLlvmpipeExplicitLodBias == JNI_TRUE;
request.coherentAsFlush = coherentAsFlush == JNI_TRUE; request.coherentAsFlush = coherentAsFlush == JNI_TRUE;
request.numSubgroupsQuirk = numSubgroupsQuirk == JNI_TRUE;
ScopedTraceReplayState replayState; ScopedTraceReplayState replayState;
mobilegl_trace_set_requested_size(request.width, request.height); mobilegl_trace_set_requested_size(request.width, request.height);
@@ -115,9 +115,7 @@ public final class TraceReplayActivity extends Activity {
request.usePbuffer, request.usePbuffer,
request.avoidAngleLlvmpipeSamplerMipmapMinFilter, request.avoidAngleLlvmpipeSamplerMipmapMinFilter,
request.avoidAngleLlvmpipeExplicitLodBias, request.avoidAngleLlvmpipeExplicitLodBias,
request.coherentAsFlush, request.coherentAsFlush
request.numSubgroupsQuirk,
request.texture2dDumps
); );
Log.i(TAG, result.toString()); Log.i(TAG, result.toString());
TraceReplayResult finalResult = result; TraceReplayResult finalResult = result;
@@ -149,9 +147,7 @@ public final class TraceReplayActivity extends Activity {
boolean usePbuffer, boolean usePbuffer,
boolean avoidAngleLlvmpipeSamplerMipmapMinFilter, boolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
boolean avoidAngleLlvmpipeExplicitLodBias, boolean avoidAngleLlvmpipeExplicitLodBias,
boolean coherentAsFlush, boolean coherentAsFlush
boolean numSubgroupsQuirk,
String texture2dDumps
); );
private static final class TraceReplayRequest { private static final class TraceReplayRequest {
@@ -176,8 +172,6 @@ public final class TraceReplayActivity extends Activity {
final boolean avoidAngleLlvmpipeSamplerMipmapMinFilter; final boolean avoidAngleLlvmpipeSamplerMipmapMinFilter;
final boolean avoidAngleLlvmpipeExplicitLodBias; final boolean avoidAngleLlvmpipeExplicitLodBias;
final boolean coherentAsFlush; final boolean coherentAsFlush;
final boolean numSubgroupsQuirk;
final String texture2dDumps;
private TraceReplayRequest( private TraceReplayRequest(
String tracePath, String tracePath,
@@ -200,9 +194,7 @@ public final class TraceReplayActivity extends Activity {
boolean usePbuffer, boolean usePbuffer,
boolean avoidAngleLlvmpipeSamplerMipmapMinFilter, boolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
boolean avoidAngleLlvmpipeExplicitLodBias, boolean avoidAngleLlvmpipeExplicitLodBias,
boolean coherentAsFlush, boolean coherentAsFlush
boolean numSubgroupsQuirk,
String texture2dDumps
) { ) {
this.tracePath = tracePath; this.tracePath = tracePath;
this.goldenPath = goldenPath; this.goldenPath = goldenPath;
@@ -225,8 +217,6 @@ public final class TraceReplayActivity extends Activity {
this.avoidAngleLlvmpipeSamplerMipmapMinFilter = avoidAngleLlvmpipeSamplerMipmapMinFilter; this.avoidAngleLlvmpipeSamplerMipmapMinFilter = avoidAngleLlvmpipeSamplerMipmapMinFilter;
this.avoidAngleLlvmpipeExplicitLodBias = avoidAngleLlvmpipeExplicitLodBias; this.avoidAngleLlvmpipeExplicitLodBias = avoidAngleLlvmpipeExplicitLodBias;
this.coherentAsFlush = coherentAsFlush; this.coherentAsFlush = coherentAsFlush;
this.numSubgroupsQuirk = numSubgroupsQuirk;
this.texture2dDumps = texture2dDumps;
} }
static TraceReplayRequest from(Intent intent, File filesDir, String defaultBackend) { static TraceReplayRequest from(Intent intent, File filesDir, String defaultBackend) {
@@ -253,9 +243,7 @@ public final class TraceReplayActivity extends Activity {
intent.getBooleanExtra("use_pbuffer", false), intent.getBooleanExtra("use_pbuffer", false),
intent.getBooleanExtra("avoid_angle_llvmpipe_sampler_mipmap_min_filter", false), intent.getBooleanExtra("avoid_angle_llvmpipe_sampler_mipmap_min_filter", false),
intent.getBooleanExtra("avoid_angle_llvmpipe_explicit_lod_bias", false), intent.getBooleanExtra("avoid_angle_llvmpipe_explicit_lod_bias", false),
intent.getBooleanExtra("coherent_as_flush", false), intent.getBooleanExtra("coherent_as_flush", false)
intent.getBooleanExtra("num_subgroups_quirk", false),
readString(intent, "texture_2d_dumps", "")
); );
} }
-36
View File
@@ -31,8 +31,6 @@ Usage:
[--avoid-angle-llvmpipe-sampler-mipmap-min-filter] \ [--avoid-angle-llvmpipe-sampler-mipmap-min-filter] \
[--avoid-angle-llvmpipe-explicit-lod-bias] \ [--avoid-angle-llvmpipe-explicit-lod-bias] \
[--coherent-as-flush] \ [--coherent-as-flush] \
[--num-subgroups-quirk] \
[--dump-texture-2d CALL,TEXTURE,LEVEL,DIR] \
--timeout-seconds N --timeout-seconds N
Set MOBILEGL_USE_ANGLE=1 to run DirectGLES replay with packaged ANGLE Set MOBILEGL_USE_ANGLE=1 to run DirectGLES replay with packaged ANGLE
@@ -48,8 +46,6 @@ sample with an explicit LOD that ANGLE llvmpipe cannot take a LOD bias on
(MOBILEGL_AVOID_EXPLICIT_LOD_BIAS=1). (MOBILEGL_AVOID_EXPLICIT_LOD_BIAS=1).
Pass --coherent-as-flush for traces whose engine writes persistent Pass --coherent-as-flush for traces whose engine writes persistent
GL_MAP_FLUSH_EXPLICIT_BIT maps it never flushes (MOBILEGL_COHERENT_AS_FLUSH=1). GL_MAP_FLUSH_EXPLICIT_BIT maps it never flushes (MOBILEGL_COHERENT_AS_FLUSH=1).
Pass --num-subgroups-quirk to derive compute gl_NumSubgroups instead of reading
the Vulkan builtin (MOBILEGL_NUM_SUBGROUPS_QUIRK=1).
EOF EOF
} }
@@ -108,8 +104,6 @@ use_pbuffer=0
avoid_angle_llvmpipe_sampler_mipmap_min_filter=0 avoid_angle_llvmpipe_sampler_mipmap_min_filter=0
avoid_angle_llvmpipe_explicit_lod_bias=0 avoid_angle_llvmpipe_explicit_lod_bias=0
coherent_as_flush=0 coherent_as_flush=0
num_subgroups_quirk=0
texture_2d_dumps=""
timeout_seconds="" timeout_seconds=""
while [ "$#" -gt 0 ]; do while [ "$#" -gt 0 ]; do
@@ -150,8 +144,6 @@ while [ "$#" -gt 0 ]; do
shift 1 shift 1
;; ;;
--coherent-as-flush) coherent_as_flush=1; shift 1 ;; --coherent-as-flush) coherent_as_flush=1; shift 1 ;;
--num-subgroups-quirk) num_subgroups_quirk=1; shift 1 ;;
--dump-texture-2d) texture_2d_dumps="$(next_arg "$@")"; shift 2 ;;
--timeout-seconds) timeout_seconds="$(next_arg "$@")"; shift 2 ;; --timeout-seconds) timeout_seconds="$(next_arg "$@")"; shift 2 ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
*) die "unknown argument: $1" ;; *) die "unknown argument: $1" ;;
@@ -270,27 +262,6 @@ copy_app_artifact() {
fi fi
} }
copy_texture_2d_dumps() {
[ -n "${texture_2d_dumps}" ] || return 0
saved_ifs="${IFS}"
IFS=';'
set -- ${texture_2d_dumps}
IFS="${saved_ifs}"
for dump_point in "$@"; do
dump_dir="${dump_point#*,}"
dump_dir="${dump_dir#*,}"
dump_dir="${dump_dir#*,}"
[ -n "${dump_dir}" ] || continue
dump_name="$(basename "${dump_dir}")"
destination_dir="${result_dir}/${dump_name}"
mkdir -p "${destination_dir}"
if ! adb_device_path exec-out run-as "${package_name}" tar -C "${dump_dir}" -cf - . | tar -xf - -C "${destination_dir}"; then
echo "trace-replay-ci.sh: warning: failed to copy texture dump ${dump_dir}" >&2
rm -rf "${destination_dir}"
fi
done
}
prepare_fixture() { prepare_fixture() {
fixture_dir="${fixture_root}/${safe_case}" fixture_dir="${fixture_root}/${safe_case}"
rm -rf "${fixture_dir}" rm -rf "${fixture_dir}"
@@ -368,12 +339,6 @@ run_retrace() {
if [ "${coherent_as_flush}" -eq 1 ]; then if [ "${coherent_as_flush}" -eq 1 ]; then
set -- "$@" --ez coherent_as_flush true set -- "$@" --ez coherent_as_flush true
fi fi
if [ "${num_subgroups_quirk}" -eq 1 ]; then
set -- "$@" --ez num_subgroups_quirk true
fi
if [ -n "${texture_2d_dumps}" ]; then
set -- "$@" --es texture_2d_dumps "${texture_2d_dumps}"
fi
set -- "$@" \ set -- "$@" \
--es output_dir "${app_dir}/output" \ --es output_dir "${app_dir}/output" \
--es diff_path "${app_dir}/output/${safe_case}-diff.png" \ --es diff_path "${app_dir}/output/${safe_case}-diff.png" \
@@ -434,7 +399,6 @@ run_retrace() {
copy_app_artifact "${app_dir}/output/${safe_case}-diff.png" "${result_dir}/${safe_case}-${backend}-diff.png" copy_app_artifact "${app_dir}/output/${safe_case}-diff.png" "${result_dir}/${safe_case}-${backend}-diff.png"
copy_app_artifact "${app_dir}/output/retrace.log" "${result_dir}/retrace.log" copy_app_artifact "${app_dir}/output/retrace.log" "${result_dir}/retrace.log"
copy_app_artifact "${app_dir}/output/mobilegl.log" "${result_dir}/mobilegl.log" copy_app_artifact "${app_dir}/output/mobilegl.log" "${result_dir}/mobilegl.log"
copy_texture_2d_dumps
# A replay that wrote result.json but did not pass used to print nothing but # A replay that wrote result.json but did not pass used to print nothing but
# the JSON, which for a non-zero statusCode says only "retrace failed with # the JSON, which for a non-zero statusCode says only "retrace failed with
+10 -235
View File
@@ -13,13 +13,8 @@
#include <cstring> #include <cstring>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <limits>
#include <string> #include <string>
#if defined(_WIN32)
#include <direct.h>
#else
#include <sys/stat.h> #include <sys/stat.h>
#endif
#include <vector> #include <vector>
// Dumps every colour attachment (and the depth attachment) of every live framebuffer // Dumps every colour attachment (and the depth attachment) of every live framebuffer
@@ -39,7 +34,6 @@ using PfnGetIntegerv = void (*)(GLenum, GLint *);
using PfnGetError = GLenum (*)(void); using PfnGetError = GLenum (*)(void);
constexpr const char *kDumpPointsEnv = "MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS"; constexpr const char *kDumpPointsEnv = "MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS";
constexpr const char *kTexture2dDumpPointsEnv = "MOBILEGL_TRACE_DUMP_TEXTURE_2D";
constexpr const char *kScanLimitEnv = "MOBILEGL_TRACE_DUMP_FBO_SCAN_LIMIT"; constexpr const char *kScanLimitEnv = "MOBILEGL_TRACE_DUMP_FBO_SCAN_LIMIT";
constexpr unsigned kDefaultScanLimit = 1024; constexpr unsigned kDefaultScanLimit = 1024;
@@ -51,16 +45,6 @@ struct DumpPoint {
bool done = false; bool done = false;
}; };
// CALL,TEXTURE,LEVEL,DIR entries, separated by ';'. This deliberately does not share the
// framebuffer dump grammar: an absolute Windows directory contains ':' but not ','.
struct Texture2dDumpPoint {
unsigned call = 0;
unsigned texture = 0;
unsigned level = 0;
std::string directory;
bool done = false;
};
struct AttachmentDesc { struct AttachmentDesc {
GLint objectType = GL_NONE; GLint objectType = GL_NONE;
GLint objectName = 0; GLint objectName = 0;
@@ -72,7 +56,6 @@ struct AttachmentDesc {
}; };
std::vector<DumpPoint> gDumpPoints; std::vector<DumpPoint> gDumpPoints;
std::vector<Texture2dDumpPoint> gTexture2dDumpPoints;
bool gInstalled = false; bool gInstalled = false;
bool gConfigured = false; bool gConfigured = false;
retrace::Dumper *gInnerDumper = nullptr; retrace::Dumper *gInnerDumper = nullptr;
@@ -122,27 +105,13 @@ bool MakeDirectories(const std::string &path) {
for (std::size_t i = 0; i < path.size(); ++i) { for (std::size_t i = 0; i < path.size(); ++i) {
partial.push_back(path[i]); partial.push_back(path[i]);
const bool last = i + 1 == path.size(); const bool last = i + 1 == path.size();
#if defined(_WIN32) if (path[i] != '/' && !last) {
const bool separator = path[i] == '/' || path[i] == '\\';
#else
const bool separator = path[i] == '/';
#endif
if (!separator && !last) {
continue; continue;
} }
if (partial == "/") { if (partial == "/") {
continue; continue;
} }
#if defined(_WIN32)
if (separator && partial.size() == 3 && partial[1] == ':') {
continue;
}
#endif
#if defined(_WIN32)
if (_mkdir(partial.c_str()) != 0 && errno != EEXIST) {
#else
if (mkdir(partial.c_str(), 0755) != 0 && errno != EEXIST) { if (mkdir(partial.c_str(), 0755) != 0 && errno != EEXIST) {
#endif
return false; return false;
} }
} }
@@ -192,52 +161,6 @@ void ParseDumpPoints(const char *spec) {
} }
} }
bool IsDecimal(const std::string &value) {
return !value.empty() && value.find_first_not_of("0123456789") == std::string::npos;
}
void ParseTexture2dDumpPoints(const char *spec) {
for (const std::string &entry : Split(spec, ';')) {
if (entry.empty()) {
continue;
}
const std::vector<std::string> fields = Split(entry, ',');
if (fields.size() != 4 || !IsDecimal(fields[0]) || !IsDecimal(fields[1]) ||
!IsDecimal(fields[2]) || fields[3].empty()) {
std::cerr << "warning: ignoring malformed " << kTexture2dDumpPointsEnv
<< " entry: " << entry << "\n";
continue;
}
char *end = nullptr;
const unsigned long call = std::strtoul(fields[0].c_str(), &end, 10);
if (*end != '\0' || call > std::numeric_limits<unsigned>::max()) {
std::cerr << "warning: ignoring malformed " << kTexture2dDumpPointsEnv
<< " call: " << entry << "\n";
continue;
}
const unsigned long texture = std::strtoul(fields[1].c_str(), &end, 10);
if (*end != '\0' || texture == 0 || texture > std::numeric_limits<unsigned>::max()) {
std::cerr << "warning: ignoring malformed " << kTexture2dDumpPointsEnv
<< " texture: " << entry << "\n";
continue;
}
const unsigned long level = std::strtoul(fields[2].c_str(), &end, 10);
if (*end != '\0' || level > static_cast<unsigned long>(std::numeric_limits<GLint>::max())) {
std::cerr << "warning: ignoring malformed " << kTexture2dDumpPointsEnv
<< " level: " << entry << "\n";
continue;
}
Texture2dDumpPoint point;
point.call = static_cast<unsigned>(call);
point.texture = static_cast<unsigned>(texture);
point.level = static_cast<unsigned>(level);
point.directory = fields[3];
gTexture2dDumpPoints.push_back(point);
}
}
unsigned ScanLimit() { unsigned ScanLimit() {
const char *value = std::getenv(kScanLimitEnv); const char *value = std::getenv(kScanLimitEnv);
if (value == nullptr || value[0] == '\0') { if (value == nullptr || value[0] == '\0') {
@@ -312,45 +235,6 @@ bool DescribeAttachment(GLenum attachment, AttachmentDesc &desc) {
return desc.width > 0 && desc.height > 0; return desc.width > 0 && desc.height > 0;
} }
bool DescribeTexture2D(GLuint texture, GLint level, AttachmentDesc &desc) {
const GLint savedTexture = GetInteger(GL_TEXTURE_BINDING_2D);
glBindTexture(GL_TEXTURE_2D, texture);
desc.objectType = GL_TEXTURE;
desc.objectName = static_cast<GLint>(texture);
desc.level = level;
glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &desc.width);
glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_HEIGHT, &desc.height);
glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_INTERNAL_FORMAT, &desc.internalFormat);
glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_RED_TYPE, &desc.componentType);
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(savedTexture));
return DrainErrors() == 0 && desc.width > 0 && desc.height > 0;
}
bool ReadTexture2DFloats(const AttachmentDesc &desc, std::vector<float> &pixels) {
const std::size_t count = static_cast<std::size_t>(desc.width) * desc.height * 4;
pixels.assign(count, 0.0f);
const GLint savedTexture = GetInteger(GL_TEXTURE_BINDING_2D);
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(desc.objectName));
if (desc.componentType == GL_INT || desc.componentType == GL_UNSIGNED_INT) {
std::vector<std::int32_t> raw(count, 0);
const GLenum type = desc.componentType == GL_INT ? GL_INT : GL_UNSIGNED_INT;
glGetTexImage(GL_TEXTURE_2D, desc.level, GL_RGBA_INTEGER, type, raw.data());
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(savedTexture));
if (DrainErrors() != 0) {
return false;
}
for (std::size_t i = 0; i < count; ++i) {
pixels[i] = desc.componentType == GL_INT
? static_cast<float>(raw[i])
: static_cast<float>(static_cast<std::uint32_t>(raw[i]));
}
return true;
}
glGetTexImage(GL_TEXTURE_2D, desc.level, GL_RGBA, GL_FLOAT, pixels.data());
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(savedTexture));
return DrainErrors() == 0;
}
// Reads the attachment as floats regardless of its storage: normalised and float targets // Reads the attachment as floats regardless of its storage: normalised and float targets
// convert on the way out, integer targets are read as integers and widened. The float view // convert on the way out, integer targets are read as integers and widened. The float view
// keeps out-of-[0,1] accumulation buffers legible in the statistics even though the PNG // keeps out-of-[0,1] accumulation buffers legible in the statistics even though the PNG
@@ -424,17 +308,6 @@ std::string FormatStatistics(const std::vector<float> &pixels, unsigned channels
static_cast<double>(minimum[c]), static_cast<double>(maximum[c]), mean); static_cast<double>(minimum[c]), static_cast<double>(maximum[c]), mean);
text += buffer; text += buffer;
} }
if (pixelCount > 0) {
text += " first=(";
for (unsigned c = 0; c < channels; ++c) {
if (c > 0) {
text += ",";
}
std::snprintf(buffer, sizeof(buffer), "%.9g", static_cast<double>(pixels[c]));
text += buffer;
}
text += ")";
}
std::snprintf(buffer, sizeof(buffer), " nonfinite=%zu hash=%016llx", nonFinite, std::snprintf(buffer, sizeof(buffer), " nonfinite=%zu hash=%016llx", nonFinite,
static_cast<unsigned long long>(hash)); static_cast<unsigned long long>(hash));
text += buffer; text += buffer;
@@ -452,8 +325,8 @@ bool WriteFloatPng(const std::string &path, const AttachmentDesc &desc, unsigned
return snapshot.writePNG(path.c_str()); return snapshot.writePNG(path.c_str());
} }
void DumpOneAttachment(std::ofstream &manifest, const std::string &directory, void DumpOneAttachment(std::ofstream &manifest, const std::string &directory, unsigned framebuffer,
const std::string &identity, GLenum attachment, const char *label, bool depth) { GLenum attachment, const char *label, bool depth) {
AttachmentDesc desc; AttachmentDesc desc;
if (!DescribeAttachment(attachment, desc)) { if (!DescribeAttachment(attachment, desc)) {
return; return;
@@ -470,13 +343,14 @@ void DumpOneAttachment(std::ofstream &manifest, const std::string &directory,
std::vector<float> pixels; std::vector<float> pixels;
const bool read = ReadAttachmentFloats(desc, depth, channels, pixels); const bool read = ReadAttachmentFloats(desc, depth, channels, pixels);
const std::string path = directory + "/" + identity + "-" + label + ".png"; const std::string path =
directory + "/fbo" + std::to_string(framebuffer) + "-" + label + ".png";
const bool wrote = read && WriteFloatPng(path, desc, channels, pixels); const bool wrote = read && WriteFloatPng(path, desc, channels, pixels);
char header[512]; char header[512];
std::snprintf(header, sizeof(header), std::snprintf(header, sizeof(header),
"%s %s object=%s name=%d level=%d size=%dx%d internalformat=0x%04x component=%s", "fbo %u %s object=%s name=%d level=%d size=%dx%d internalformat=0x%04x component=%s",
identity.c_str(), label, framebuffer, label,
desc.objectType == GL_RENDERBUFFER ? "renderbuffer" : "texture", desc.objectName, desc.objectType == GL_RENDERBUFFER ? "renderbuffer" : "texture", desc.objectName,
desc.level, desc.width, desc.height, static_cast<unsigned>(desc.internalFormat), desc.level, desc.width, desc.height, static_cast<unsigned>(desc.internalFormat),
ComponentTypeName(desc.componentType)); ComponentTypeName(desc.componentType));
@@ -507,14 +381,13 @@ void DumpFramebuffer(std::ofstream &manifest, const std::string &directory, unsi
} }
const GLint savedReadBuffer = GetInteger(GL_READ_BUFFER); const GLint savedReadBuffer = GetInteger(GL_READ_BUFFER);
const std::string identity = "fbo" + std::to_string(framebuffer);
for (GLint index = 0; index < maxColorAttachments; ++index) { for (GLint index = 0; index < maxColorAttachments; ++index) {
char label[32]; char label[32];
std::snprintf(label, sizeof(label), "att%d", index); std::snprintf(label, sizeof(label), "att%d", index);
DumpOneAttachment(manifest, directory, identity, DumpOneAttachment(manifest, directory, framebuffer,
static_cast<GLenum>(GL_COLOR_ATTACHMENT0 + index), label, false); static_cast<GLenum>(GL_COLOR_ATTACHMENT0 + index), label, false);
} }
DumpOneAttachment(manifest, directory, identity, GL_DEPTH_ATTACHMENT, "depth", true); DumpOneAttachment(manifest, directory, framebuffer, GL_DEPTH_ATTACHMENT, "depth", true);
// The read buffer is per-framebuffer state the trace goes on using; put it back. // The read buffer is per-framebuffer state the trace goes on using; put it back.
if (framebuffer != 0 && savedReadBuffer != 0) { if (framebuffer != 0 && savedReadBuffer != 0) {
@@ -543,7 +416,6 @@ void RunDumpPoint(DumpPoint &point) {
const GLint savedPackSkipRows = GetInteger(GL_PACK_SKIP_ROWS); const GLint savedPackSkipRows = GetInteger(GL_PACK_SKIP_ROWS);
const GLint savedPackImageHeight = GetInteger(GL_PACK_IMAGE_HEIGHT); const GLint savedPackImageHeight = GetInteger(GL_PACK_IMAGE_HEIGHT);
const GLint savedPackSkipImages = GetInteger(GL_PACK_SKIP_IMAGES); const GLint savedPackSkipImages = GetInteger(GL_PACK_SKIP_IMAGES);
const GLint savedPackSwapBytes = GetInteger(GL_PACK_SWAP_BYTES);
DrainErrors(); DrainErrors();
if (savedPackBuffer != 0) { if (savedPackBuffer != 0) {
@@ -555,7 +427,6 @@ void RunDumpPoint(DumpPoint &point) {
glPixelStorei(GL_PACK_SKIP_ROWS, 0); glPixelStorei(GL_PACK_SKIP_ROWS, 0);
glPixelStorei(GL_PACK_IMAGE_HEIGHT, 0); glPixelStorei(GL_PACK_IMAGE_HEIGHT, 0);
glPixelStorei(GL_PACK_SKIP_IMAGES, 0); glPixelStorei(GL_PACK_SKIP_IMAGES, 0);
glPixelStorei(GL_PACK_SWAP_BYTES, GL_FALSE);
DrainErrors(); DrainErrors();
const GLint maxColorAttachments = GetInteger(GL_MAX_COLOR_ATTACHMENTS); const GLint maxColorAttachments = GetInteger(GL_MAX_COLOR_ATTACHMENTS);
@@ -591,7 +462,6 @@ void RunDumpPoint(DumpPoint &point) {
glPixelStorei(GL_PACK_SKIP_ROWS, savedPackSkipRows); glPixelStorei(GL_PACK_SKIP_ROWS, savedPackSkipRows);
glPixelStorei(GL_PACK_IMAGE_HEIGHT, savedPackImageHeight); glPixelStorei(GL_PACK_IMAGE_HEIGHT, savedPackImageHeight);
glPixelStorei(GL_PACK_SKIP_IMAGES, savedPackSkipImages); glPixelStorei(GL_PACK_SKIP_IMAGES, savedPackSkipImages);
glPixelStorei(GL_PACK_SWAP_BYTES, savedPackSwapBytes);
DrainErrors(); DrainErrors();
std::cerr << "MOBILEGL_TRACE_FBO_DUMP: call " << retrace::callNo << " -> " << manifestPath std::cerr << "MOBILEGL_TRACE_FBO_DUMP: call " << retrace::callNo << " -> " << manifestPath
@@ -599,98 +469,12 @@ void RunDumpPoint(DumpPoint &point) {
point.done = true; point.done = true;
} }
void RunTexture2dDumpPoint(Texture2dDumpPoint &point) {
if (!MakeDirectories(point.directory)) {
std::cerr << "warning: failed to create texture dump directory " << point.directory << "\n";
point.done = true;
return;
}
// glGetError is destructive. This debug-only snapshot hook deliberately starts from a
// clean error state so diagnostics below identify the dump rather than an earlier trace call.
DrainErrors();
const GLint savedReadFramebuffer = GetInteger(GL_READ_FRAMEBUFFER_BINDING);
const GLint savedPackBuffer = GetInteger(GL_PIXEL_PACK_BUFFER_BINDING);
const GLint savedPackAlignment = GetInteger(GL_PACK_ALIGNMENT);
const GLint savedPackRowLength = GetInteger(GL_PACK_ROW_LENGTH);
const GLint savedPackSkipPixels = GetInteger(GL_PACK_SKIP_PIXELS);
const GLint savedPackSkipRows = GetInteger(GL_PACK_SKIP_ROWS);
const GLint savedPackImageHeight = GetInteger(GL_PACK_IMAGE_HEIGHT);
const GLint savedPackSkipImages = GetInteger(GL_PACK_SKIP_IMAGES);
const GLint savedPackSwapBytes = GetInteger(GL_PACK_SWAP_BYTES);
DrainErrors();
if (savedPackBuffer != 0) {
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
}
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
glPixelStorei(GL_PACK_SKIP_ROWS, 0);
glPixelStorei(GL_PACK_IMAGE_HEIGHT, 0);
glPixelStorei(GL_PACK_SKIP_IMAGES, 0);
glPixelStorei(GL_PACK_SWAP_BYTES, GL_FALSE);
DrainErrors();
const std::string identity = "texture" + std::to_string(point.texture) +
"-level" + std::to_string(point.level);
const std::string manifestPath = point.directory + "/manifest.txt";
std::ofstream manifest(manifestPath, std::ios::trunc);
manifest << "call " << retrace::callNo << " texture " << point.texture << " level "
<< point.level << "\n";
AttachmentDesc desc;
if (glIsTexture(point.texture) == GL_FALSE ||
!DescribeTexture2D(point.texture, static_cast<GLint>(point.level), desc)) {
manifest << identity << " skipped=not-live-2d-texture\n";
} else {
std::vector<float> pixels;
const bool read = ReadTexture2DFloats(desc, pixels);
const bool wrote = read && WriteFloatPng(point.directory + "/" + identity + ".png", desc, 4, pixels);
manifest << identity << " object=texture name=" << desc.objectName << " level=" << desc.level
<< " size=" << desc.width << "x" << desc.height << " internalformat=0x" << std::hex
<< static_cast<unsigned>(desc.internalFormat) << std::dec
<< " component=" << ComponentTypeName(desc.componentType);
if (read) {
manifest << FormatStatistics(pixels, 4);
} else {
manifest << " read=failed";
}
if (!wrote) {
manifest << " png=failed";
}
manifest << "\n";
}
manifest.flush();
glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(savedReadFramebuffer));
if (savedPackBuffer != 0) {
glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast<GLuint>(savedPackBuffer));
}
glPixelStorei(GL_PACK_ALIGNMENT, savedPackAlignment);
glPixelStorei(GL_PACK_ROW_LENGTH, savedPackRowLength);
glPixelStorei(GL_PACK_SKIP_PIXELS, savedPackSkipPixels);
glPixelStorei(GL_PACK_SKIP_ROWS, savedPackSkipRows);
glPixelStorei(GL_PACK_IMAGE_HEIGHT, savedPackImageHeight);
glPixelStorei(GL_PACK_SKIP_IMAGES, savedPackSkipImages);
glPixelStorei(GL_PACK_SWAP_BYTES, savedPackSwapBytes);
DrainErrors();
std::cerr << "MOBILEGL_TRACE_TEXTURE_2D_DUMP: call " << retrace::callNo << " texture "
<< point.texture << " level " << point.level << " -> " << manifestPath << "\n";
point.done = true;
}
void RunPendingDumps() { void RunPendingDumps() {
for (DumpPoint &point : gDumpPoints) { for (DumpPoint &point : gDumpPoints) {
if (!point.done && point.call == retrace::callNo) { if (!point.done && point.call == retrace::callNo) {
RunDumpPoint(point); RunDumpPoint(point);
} }
} }
for (Texture2dDumpPoint &point : gTexture2dDumpPoints) {
if (!point.done && point.call == retrace::callNo) {
RunTexture2dDumpPoint(point);
}
}
} }
class DumpingDumper final : public retrace::Dumper { class DumpingDumper final : public retrace::Dumper {
@@ -727,12 +511,8 @@ void InstallIfRequested() {
if (spec != nullptr && spec[0] != '\0') { if (spec != nullptr && spec[0] != '\0') {
ParseDumpPoints(spec); ParseDumpPoints(spec);
} }
const char *textureSpec = std::getenv(kTexture2dDumpPointsEnv);
if (textureSpec != nullptr && textureSpec[0] != '\0') {
ParseTexture2dDumpPoints(textureSpec);
} }
} if (gDumpPoints.empty()) {
if (gDumpPoints.empty() && gTexture2dDumpPoints.empty()) {
gInstalled = true; gInstalled = true;
return; return;
} }
@@ -748,11 +528,6 @@ void InstallIfRequested() {
std::cerr << "MOBILEGL_TRACE_FBO_DUMP: armed for call " << point.call << " -> " std::cerr << "MOBILEGL_TRACE_FBO_DUMP: armed for call " << point.call << " -> "
<< point.directory << "\n"; << point.directory << "\n";
} }
for (const Texture2dDumpPoint &point : gTexture2dDumpPoints) {
std::cerr << "MOBILEGL_TRACE_TEXTURE_2D_DUMP: armed for call " << point.call
<< " texture " << point.texture << " level " << point.level << " -> "
<< point.directory << "\n";
}
} }
} // namespace mobilegl_trace_dump } // namespace mobilegl_trace_dump
+3 -4
View File
@@ -2,10 +2,9 @@
namespace mobilegl_trace_dump { namespace mobilegl_trace_dump {
// Installs the opt-in framebuffer-attachment and named-2D-texture dump hooks. The respective // Installs the framebuffer-attachment dump hook when MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS
// environments are MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS and MOBILEGL_TRACE_DUMP_TEXTURE_2D. // describes at least one dump point. Safe and cheap to call on every makeCurrent: the
// Safe and cheap to call on every makeCurrent: the environment is consulted once and the hook is // environment is consulted once and the hook is installed at most once.
// installed at most once.
void InstallIfRequested(); void InstallIfRequested();
} // namespace mobilegl_trace_dump } // namespace mobilegl_trace_dump
+1 -2
View File
@@ -284,8 +284,7 @@
"golden": "minecraft-1.21.4-fabric-iris-iterationrp-in-world.0000202020.png", "golden": "minecraft-1.21.4-fabric-iris-iterationrp-in-world.0000202020.png",
"target_call": 202020, "target_call": 202020,
"timeout_seconds": 1800, "timeout_seconds": 1800,
"ssim_threshold": 0.98, "ssim_threshold": 0.98
"num_subgroups_quirk": true
}, },
{ {
"name": "minecraft-1.21.4-fabric-iris-bsl-esc-menu-854", "name": "minecraft-1.21.4-fabric-iris-bsl-esc-menu-854",