Compare commits

...
16 Commits
Author SHA1 Message Date
swung0x48 4322427e78 [Fix] (DirectGLES): lower gl_ClipDistance for Adreno's ESSL compiler - shadow the builtin in a Private array with constant-index flushes before EmitVertex/return, loop-copy gl_in clip distances through dynamic indices (whole-array reads segfault the Qualcomm compiler, constant-index element reads miscompile), strip the SPIRV-Cross redeclaration Adreno rejects, and split const struct-array LUT initializers so they stay dynamically indexable; quirk-gated to Qualcomm with MOBILEGL_QUIRK_CLIP_DISTANCE override 2026-07-26 19:56:45 -04:00
swung0x48 203d4bce5e [Fix] (DirectGLES): piglit fixes batch 1 - keep enabled-but-unsourceable vertex attribs disabled on the backend VAO (Adreno memcpy-from-NULL SIGSEGV on gl-3.1-vao-broken-attrib), content-sync READ-framebuffer texture attachments before blits, clamp out-of-bounds access-chain indices via GraphicsRobustAccessPass before ESSL transpile (Adreno poisons whole-shader output on constant OOB), and fold ConstOffset into the coordinate for 1D texelFetch (SPIRV-Cross emulates 1D as 2D but leaves the scalar offset, which ESSL rejects) 2026-07-26 19:17:25 -04:00
swung0x48 761114d022 Merge remote-tracking branch 'origin/cts-gl33' into dev 2026-07-26 18:29:02 -04:00
swung0x48 9caf34d5b1 [Fix] (Logging): rank WARN/ERROR above INFO so release builds keep them - the old ordering (DEBUG=0, WARN=1, ERROR=2, INFO=3) compiled every MGLOG_W/MGLOG_E out of the default MOBILEGL_LOG_LEVEL_INFO build, silently hiding backend shader-compile failures, unsupported-path skips, and enum-conversion fallbacks during the piglit runs 2026-07-26 18:14:56 -04:00
swung0x48 5e676b338b [Fix] (DirectVulkan): back legacy low-bit formats (RGB565/RGB5A1/RGBA4/R3G3B2/RGB4/RGBA2/RGB10/12) with their UNorm8/16 canonical shadow layouts and add capability fallbacks - they mapped to VK_FORMAT_UNDEFINED and crashed or wedged the GPU on upload; also admit 2DMSArray/CubeMap/3D color attachment targets in the render pass 2026-07-26 13:28:30 -04:00
swung0x48 db01bfa3e8 [Fix] (DirectVulkan): general (format,type) readback conversion - hoist the CTS-verified StoreWideRowsToClient into shared ReadbackImpl and decode any color VkFormat to wide RGBA rows; readback previously supported only RGB/BGR/RGBA/BGRA x UNSIGNED_BYTE/FLOAT and silently returned zeros for everything else 2026-07-26 13:28:30 -04:00
swung0x48 cc3dcfd80e [Fix] (DirectVulkan): support UBO instance arrays as arrayed descriptors - uniform Block{...}b[N] reflected as one binding with descriptorCount=N, per-element GL block mapping, per-element buffer infos and dynamic offsets; non-UBO descriptor arrays now fail program creation cleanly instead of continuing corrupt 2026-07-26 12:25:24 -04:00
swung0x48 c8632dfefe [Test] (piglit-android): add on-device piglit harness for MobileGL - patched waffle (WAFFLE_EGL_LIBRARY/WAFFLE_GL_LIBRARY overrides so waffle drives libMobileGL.so directly, AImageReader-backed windows for DirectVulkan since Android ICDs lack VK_EXT_headless_surface, WAFFLE_FORCE_GL_CONTEXT_VERSION to upgrade piglit's low compat context requests to 3.3 core, meson cross fixes) and patched piglit (Android platform support, EGL support decoupled from the X11-dependent EGL tests, and a dispatch-init fix: the waffle resolvers were never installed because gl_fw is NULL during framework construction, so gl* silently bound to the system driver via the DT_NEEDED libEGL's eglGetProcAddress), plus the adb chunked runner with PIGLIT-result parsing, a results comparator, cross-file examples, and the piglit-on-android skill 2026-07-26 11:57:11 -04:00
swung0x48 d9556ff041 [Fix] (DirectVulkan): implement color renderbuffer attachments - render pass/pipeline/blit/copy/readback/clear paths treated color renderbuffers as absent (writes masked to VK_ATTACHMENT_UNUSED, glClear dropped, readback zeros) 2026-07-26 11:30:54 -04:00
swung0x48 b8a8a660e1 [Refactor] (Lifecycle): own MobileGL's lifecycle from the EGL layer instead of ELF static ctor/dtor - the first EGL/WGL entry point lazily initializes via a thread-safe, re-init-capable EnsureInitialized (AutoInit is gone), the last eglTerminate with no initialized display and nothing current tears the whole library down deterministically inside the EGL lifecycle, and the global singletons move to leak-at-exit heap storage so process exit runs no backend destructors at all (AutoDestroy and the Windows DllMain abandon hook are gone); fixes the exit-time SIGABRT from undefined static-destruction order - the DirectGLES buffer-pool mutex abort on Android clean exits, and the pre-existing macOS QueryTest/ProgramTest 'Subprocess aborted' gtest failures now pass (ctest 411/411) 2026-07-26 10:59:14 -04:00
swung0x48 39f21e52ea [Test] (CTS): isolate the DirectVulkan renderbuffer-FBO readback defect so the rest of KHR-GL33 can be measured 2026-07-26 10:43:48 -04:00
swung0x48 0e933b8f2f [Test] (CTS): run VK-GL-CTS KHR-GL33 against MobileGL on Android via a standalone glcts binary 2026-07-26 08:05:02 -04:00
swung0x48 7ab83861ca [Fix] (DirectVulkan): suspend presentation while the window is zero-area - a minimized window's out-of-date swapchain used to keep Present submitting on a signaled fence and presenting never-acquired images (adversarial review); also drop logging from the process-detach abandon path 2026-07-26 07:59:40 -04:00
swung0x48 eaeba556a3 [Test] (WGL): add manual Windows smoke tests - hand-rolled WGL bootstrap and GLFW-driven variant covering the zero-area helper-window path 2026-07-26 07:21:36 -04:00
swung0x48 72fa1221a5 [Fix] (DirectVulkan): survive zero-area windows at renderer init - skip the eager first acquire when RecreateSwapchain's minimize guard left no swapchain (GLFW's hidden helper window), and let Present bring the swapchain up once the window has real size 2026-07-26 07:16:11 -04:00
swung0x48 c4254c4bbd [Feat] (WGL): add Windows host layer - drop-in opengl32.dll with WGL over EGLImpl, Win32 window backend plumbing for both backends, ANGLE loader path, and leak-at-exit process teardown 2026-07-26 06:46:29 -04:00
76 changed files with 7360 additions and 277 deletions
+31 -1
View File
@@ -194,6 +194,9 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.cpp
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
@@ -303,6 +306,13 @@ if (ANDROID)
)
endif()
if (WIN32)
list(APPEND SOURCE_FILES
MobileGL/MG_Impl/WGLImpl/WGLImpl.cpp
MobileGL/MG_Impl/WGLImpl/Exporting/Definitions.cpp
)
endif()
set(MOBILEGL_LINK_LIBRARIES
glslang::glslang
spirv-cross-c
@@ -331,10 +341,18 @@ set(MOBILEGL_INCLUDE_DIR
${SPIRV-Headers_SOURCE_DIR}/include
)
add_library(${CMAKE_PROJECT_NAME} SHARED
add_library(${CMAKE_PROJECT_NAME} SHARED
${SOURCE_FILES}
)
if (WIN32)
# The wgl* entry points are exported via .def (see the comment in wgl.def);
# only the shared library links it.
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
MobileGL/MG_Impl/WGLImpl/Exporting/wgl.def
)
endif()
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
C_VISIBILITY_PRESET default
@@ -377,6 +395,18 @@ if(UNIX AND NOT APPLE AND NOT ANDROID)
endforeach()
endif()
if(WIN32)
# Drop-in for the classic GL loader path: a copy named opengl32.dll placed
# next to a host executable is what LoadLibrary("opengl32.dll") and gdi32's
# pixel-format forwarding will resolve.
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:${CMAKE_PROJECT_NAME}>"
"$<TARGET_FILE_DIR:${CMAKE_PROJECT_NAME}>/opengl32.dll"
COMMENT "Creating opengl32.dll drop-in copy"
)
endif()
if(NOT ANDROID)
add_library(${CMAKE_PROJECT_NAME}_s STATIC
${SOURCE_FILES}
+5
View File
@@ -80,6 +80,11 @@ namespace MobileGL::MG_Config {
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
// MOBILEGL_QUIRK_CLIP_DISTANCE: overrides the DirectGLES quirk that lowers
// gl_ClipDistance for Adreno's ESSL compiler (shadow Private arrays with
// constant-index builtin flushes, dynamic-index gl_in copy loop, redeclaration
// strip, and const struct-array LUT splitting). Auto detects Qualcomm.
QuirkOverride ClipDistanceQuirk = QuirkOverride::Auto;
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
+1
View File
@@ -135,6 +135,7 @@ namespace MobileGL::MG_ConfigLoader {
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
features.ClipDistanceQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_CLIP_DISTANCE");
features.MagmaDisableBlendedDepthWriteQuirk =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
+1
View File
@@ -34,6 +34,7 @@
#define MOBILEGL_EGL_API MOBILEGL_API
#define MOBILEGL_CGL_API MOBILEGL_API
#define MOBILEGL_NSOPENGL_API MOBILEGL_API
#define MOBILEGL_WGL_API MOBILEGL_API
// ====================== MobileGL configurations ======================= //
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL
+7 -1
View File
@@ -14,7 +14,13 @@ namespace MobileGL {
} // namespace MG_Config
namespace MG_Backend {
UniquePtr<BackendObject> pActiveBackendObject;
// Leak-at-exit storage: the UniquePtr itself lives on the heap and is
// never destroyed by the runtime, so process exit runs no backend
// destructors (static destruction order across TUs is undefined).
// Deterministic teardown happens inside the EGL lifecycle instead:
// the last eglTerminate calls MobileGL::Destroy(), which .reset()s
// these singletons while the process is still healthy.
UniquePtr<BackendObject>& pActiveBackendObject = *new UniquePtr<BackendObject>();
GlobalBackendFunctionsTable gBackendFunctionsTable;
} // namespace MG_Backend
} // namespace MobileGL
+38 -33
View File
@@ -9,14 +9,24 @@
#include "Init.h"
#include "Config.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_Backend/DirectVulkan/DirectVulkan.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/EGLState/Core.h>
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <atomic>
#include <mutex>
namespace MobileGL {
namespace {
Bool g_isInitialized = false;
std::atomic<Bool> g_isInitialized = false;
thread_local Bool tl_initializing = false;
std::mutex& InitMutex() {
static std::mutex mutex;
return mutex;
}
void DestroyImpl(Bool logLifecycle) {
if (!g_isInitialized) {
@@ -64,40 +74,35 @@ namespace MobileGL {
MGLOG_I("MobileGL initialized");
}
void EnsureInitialized() {
if (g_isInitialized.load(std::memory_order_acquire)) {
return;
}
// Re-entrant call while this thread is already inside Initialize()
// (e.g. an init step routing back through a public entry point).
if (tl_initializing) {
return;
}
const std::lock_guard<std::mutex> lock(InitMutex());
if (g_isInitialized.load(std::memory_order_acquire)) {
return;
}
tl_initializing = true;
Initialize();
tl_initializing = false;
}
void Destroy() {
DestroyImpl(true);
}
#if defined(__linux__) || defined(__APPLE__)
__attribute__((constructor)) static void AutoInit() {
Initialize();
}
__attribute__((destructor)) static void AutoDestroy() {
if (MG_Config::Features.TraceSkipAutodestroy) {
return;
}
#if defined(__APPLE__)
// macOS injected dylibs can run destructors after logging/backend static state is already torn down.
return;
#else
DestroyImpl(false);
#endif
}
#endif
#ifdef _WIN32
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
Initialize();
break;
case DLL_PROCESS_DETACH:
Destroy();
break;
}
return TRUE;
}
#endif
// MobileGL's lifecycle is owned entirely by the host-API layers
// (EGL/WGL/CGL): initialization happens lazily on the first entry point
// via EnsureInitialized(), and full teardown happens deterministically
// when the last EGL display is terminated with nothing current (EGLImpl
// calls Destroy()). There is intentionally no static constructor, no
// static destructor, and no DllMain: the global singletons use
// leak-at-exit storage (see GlobalObjects.cpp), so a process that exits
// without eglTerminate simply leaks them to the OS instead of running
// backend destructors during static teardown.
} // namespace MobileGL
+6
View File
@@ -11,6 +11,12 @@
namespace MobileGL {
void Initialize();
// Thread-safe, idempotent, and re-entrant wrapper around Initialize().
// Host layers (EGL/WGL/CGL entry points) call this lazily on first use so
// MobileGL's lifecycle never depends on ELF/DLL static constructors, and
// so a fresh init can follow a full Destroy() (e.g. after the last
// eglTerminate).
void EnsureInitialized();
void Destroy();
namespace MG_Util::Debug {
+2 -1
View File
@@ -313,7 +313,8 @@ namespace MobileGL {
Android,
X11,
MetalLayer,
// TODO: Wayland, Windows, etc.
Win32, // Handle is an HWND
// TODO: Wayland, etc.
WindowBackendCount,
Unknown = -1
};
+1 -1
View File
@@ -13,6 +13,6 @@
#include "DirectVulkan/BackendObject_DirectVulkan.h"
namespace MobileGL::MG_Backend {
extern UniquePtr<BackendObject> pActiveBackendObject;
extern UniquePtr<BackendObject>& pActiveBackendObject;
extern GlobalBackendFunctionsTable gBackendFunctionsTable;
} // namespace MobileGL::MG_Backend
@@ -701,9 +701,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
if ((handle.Backend != WindowBackend::Android &&
handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer) ||
handle.Backend != WindowBackend::MetalLayer &&
handle.Backend != WindowBackend::Win32) ||
!handle.Handle) {
MGLOG_E("DirectGLES backend only supports Android, X11, and CAMetalLayer native windows");
MGLOG_E("DirectGLES backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
return false;
}
+32 -88
View File
@@ -449,6 +449,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
}
// Textures attached only to the READ framebuffer (blit / ReadPixels sources) need
// their content synced too, or the backend reads stale texel data.
const auto& readFBO =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
if (readFBO && readFBO != currentFBO) {
for (const auto& attachment : readFBO->GetAllAttachmentObjects()) {
if (!attachment.IsTexture()) continue;
auto& textureObject = attachment.GetTexture();
if (textureObject) {
SyncTextureObjectToBackend(textureObject);
}
}
}
}
static Bool SupportsLayeredImageBinding(TextureTarget target) {
@@ -3435,90 +3449,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return componentType != 0 ? static_cast<GLenum>(componentType) : GL_UNSIGNED_NORMALIZED;
}
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
// 4 components x GetReadbackComponentSize(wideType) bytes each.
// applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage
// of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4).
// Per the GL addressing rules, slice k row j lands at
// SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes
// + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride.
static Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams) {
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
if (dstPixelBytes == 0) {
return false;
}
ReadbackImpl::PackedReadbackLayout packedLayout{};
const Bool isPackedType = ReadbackImpl::GetPackedReadbackLayout(type, packedLayout);
const SizeT dstComponentSize = GetReadbackComponentSize(type);
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
// rows are written so skip regions of the destination stay untouched.
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT dstRowStride = AlignPixelRow(rowPixels * dstPixelBytes, packParams.Alignment);
const SizeT imageRows =
applyPackImageParams && packParams.ImageHeight > 0
? static_cast<SizeT>(packParams.ImageHeight)
: static_cast<SizeT>(sliceHeight);
const SizeT dstImageStride = imageRows * dstRowStride;
const SizeT skipImages =
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
const SizeT dstSkipOffset = skipImages * dstImageStride +
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
if (pixelPackBufferObject) {
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
if (requiredSize > pixelPackBufferObject->GetSize()) {
MGLOG_E("Readback conversion: pixel pack buffer is too small");
return true;
}
}
const SizeT srcComponentSize = GetReadbackComponentSize(wideType);
const SizeT srcPixelBytes = 4 * srcComponentSize;
Vector<Uint8> convertedRow(dstRowBytes);
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
for (GLsizei row = 0; row < sliceHeight; ++row) {
const SizeT flatRow = static_cast<SizeT>(slice) * static_cast<SizeT>(sliceHeight) +
static_cast<SizeT>(row);
const Uint8* srcRow = wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
ReadbackImpl::ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast<SizeT>(width), wideType,
mapping, type);
if (packParams.SwapBytes) {
const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize;
if (groupSize > 1) {
for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) {
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize);
}
}
}
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
static_cast<SizeT>(row) * dstRowStride;
if (pixelPackBufferObject) {
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
pboBaseOffset + dstOffset);
} else {
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
}
}
}
return true;
}
// Reads the current READ framebuffer as wide RGBA(_INTEGER) and repacks the pixels into the client's
// (format, type) layout. Returns false when the combination is not convertible (the caller keeps its
@@ -3651,7 +3581,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
ExpandNarrowWideRead(wide, static_cast<SizeT>(width) * static_cast<SizeT>(height), readChannels, wideType);
}
if (!StoreWideRowsToClient(wide.data(), wideType, width, height, /*sliceCount=*/1, mapping, type, pixels,
if (!ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, height, /*sliceCount=*/1, mapping, type, pixels,
honorPackImageParams)) {
return false;
}
@@ -3705,7 +3635,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
const GLenum wideType = isInteger ? (isSigned ? GL_INT : GL_UNSIGNED_INT) : GL_FLOAT;
if (!StoreWideRowsToClient(wide.data(), wideType, width, sliceHeight, sliceCount, mapping, type, pixels,
if (!ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, sliceHeight, sliceCount, mapping, type, pixels,
applyPackImageParams)) {
return false;
}
@@ -4307,9 +4237,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!ChooseConfigForSurface(surfaceBit, g_Config, window)) return false;
const EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
// Negotiate the highest ES 3.x context. Version-strict EGL implementations
// (ANGLE) return exactly the requested minor, and a bare CLIENT_VERSION 3
// request yields a 3.0 context that lacks the 3.1/3.2 texture targets the
// capability probes exercise; mobile drivers ignore the minor and hand out
// their maximum either way.
for (const EGLint minorVersion : {2, 1, 0}) {
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3,
EGL_CONTEXT_MINOR_VERSION, minorVersion,
EGL_NONE};
g_Context = g_EGLFuncs.eglCreateContext(g_Display, g_Config, EGL_NO_CONTEXT, contextAttribs);
if (g_Context != EGL_NO_CONTEXT) {
return true;
}
}
g_Context = g_EGLFuncs.eglCreateContext(g_Display, g_Config, EGL_NO_CONTEXT, contextAttribs);
const EGLint legacyContextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
g_Context = g_EGLFuncs.eglCreateContext(g_Display, g_Config, EGL_NO_CONTEXT, legacyContextAttribs);
return g_Context != EGL_NO_CONTEXT;
}
+104 -2
View File
@@ -1262,15 +1262,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto& allAttributes = stateVAOObject->GetAllAttributes();
for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) {
const auto& attrib = allAttributes[attribIndex];
const Uint32 attribBit = 1u << attribIndex;
// An enabled attrib with neither a buffer object nor a client pointer has no
// source; GL tolerates the state (only draws consuming it are undefined), but
// Adreno's ES driver memcpys the "client array" from address 0 at draw time
// (SIGSEGV). Keep such attribs disabled on the backend VAO and re-enable them
// the moment they gain a source - the mask-vs-current compare below triggers
// the enable even when only the Buffer/Format versions changed.
const Bool unsourceable = attrib.Enabled && !attrib.Buffer && attrib.Offset == 0;
const Bool wasForceDisabled = (m_forceDisabledAttribsMask & attribBit) != 0;
Bool needsSyncSwitch = allAttributeVersions[attribIndex].SwitchVersion !=
m_syncedAttributeVersions[attribIndex].SwitchVersion;
if (needsSyncSwitch) {
if (attrib.Enabled) {
if (needsSyncSwitch || unsourceable != wasForceDisabled) {
if (attrib.Enabled && !unsourceable) {
g_GLESFuncs.glEnableVertexAttribArray(attribIndex);
} else {
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
}
}
if (unsourceable) {
m_forceDisabledAttribsMask |= attribBit;
} else {
m_forceDisabledAttribsMask &= ~attribBit;
}
Bool needsSyncFormat = allAttributeVersions[attribIndex].FormatVersion !=
m_syncedAttributeVersions[attribIndex].FormatVersion;
@@ -1278,7 +1294,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_syncedAttributeVersions[attribIndex].BufferVersion;
if (!needsSyncFormat && !needsSyncBuffer) continue;
if (unsourceable) continue;
// Client-side array with a non-null pointer: the pointer is uploaded and applied
// per draw by SyncClientSideAttributesForDrawArrays.
if (!attrib.Buffer) continue;
if (!BindAttributeBuffer(attrib)) {
if (attrib.Enabled) {
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
m_forceDisabledAttribsMask |= attribBit;
}
continue;
}
@@ -3265,6 +3291,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
auto& shaderSpirvs = stateProgramObject->GetGeneratedSpirv();
// Adreno's ESSL compiler mishandles gl_ClipDistance (rejects redeclarations,
// miscompiles non-constant-index writes and constant-index gl_in element reads,
// crashes on whole-array gl_in reads) and cannot dynamically index the global
// const struct[] LUTs SPIRV-Cross likes to emit. Gate the workarounds to
// Qualcomm; MOBILEGL_QUIRK_CLIP_DISTANCE overrides the device detection.
const MG_Config::QuirkOverride clipDistanceQuirkOverride =
MG_Config::Features.ClipDistanceQuirk;
const Bool applyClipDistanceQuirk =
clipDistanceQuirkOverride == MG_Config::QuirkOverride::ForceOn ||
(clipDistanceQuirkOverride == MG_Config::QuirkOverride::Auto &&
pActiveBackendObject &&
pActiveBackendObject->GetDynamicParameters().GpuVendor == GpuVendorKind::Qualcomm);
for (int index = 0; index < attachedShaders.size(); ++index) {
auto& shader = attachedShaders[index];
GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(shader->GetShaderStage());
@@ -3287,6 +3326,63 @@ namespace MobileGL::MG_Backend::DirectGLES {
effectiveSpirv = &loweredSpirv;
}
// GL 3.3 only promises undefined *values* for out-of-bounds array indexing, but
// Adreno's ESSL compiler constant-folds a provably out-of-bounds local-array
// index into poison that corrupts the whole shader's output. Clamp every
// access-chain index to its declared bounds before transpiling.
Vector<unsigned int> clampedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::ClampAccessChainIndicesForEssl(*effectiveSpirv,
clampedSpirv) &&
!clampedSpirv.empty()) {
effectiveSpirv = &clampedSpirv;
} else {
MGLOG_W("ClampAccessChainIndicesForEssl failed, continuing with unclamped SPIR-V.");
}
// SPIRV-Cross emulates 1D samplers as 2D for ES: it widens texelFetch coordinates
// to ivec2 but keeps the ConstOffset operand scalar, which is not a valid ESSL
// texelFetchOffset overload (Adreno rejects it). Fold the constant offset into the
// coordinate instead (texelFetchOffset(t,P,l,o) == texelFetch(t,P+o,l)).
Vector<unsigned int> foldedOffsetSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::FoldConstOffsetFor1DFetchForEssl(
*effectiveSpirv, foldedOffsetSpirv) &&
!foldedOffsetSpirv.empty()) {
effectiveSpirv = &foldedOffsetSpirv;
} else {
MGLOG_W("FoldConstOffsetFor1DFetchForEssl failed, continuing with unfolded SPIR-V.");
}
// Adreno quirk: shadow gl_ClipDistance in Private arrays so the transpiled
// ESSL only writes the builtin with literal constant indices (flushed before
// EmitVertex/return) and only reads gl_in clip distances through dynamic loop
// indices - the shapes this driver compiles correctly. Must run after the
// access-chain clamp above so the flush indices stay literal constants.
Vector<unsigned int> clipDistanceSpirv;
if (applyClipDistanceQuirk &&
(glShaderType == GL_VERTEX_SHADER || glShaderType == GL_GEOMETRY_SHADER)) {
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerClipDistanceForEssl(
*effectiveSpirv, clipDistanceSpirv) &&
!clipDistanceSpirv.empty()) {
effectiveSpirv = &clipDistanceSpirv;
} else {
MGLOG_W("LowerClipDistanceForEssl failed, continuing with unlowered SPIR-V.");
}
}
// Adreno quirk: split single constant-composite stores of struct arrays so
// SPIRV-Cross does not promote them to global const struct[] LUTs, which this
// driver cannot dynamically index ("Cannot offset into the structure").
Vector<unsigned int> structLutSpirv;
if (applyClipDistanceQuirk) {
if (MG_Util::ShaderTranspiler::ShaderCompiler::DefeatConstStructArrayLutForEssl(
*effectiveSpirv, structLutSpirv) &&
!structLutSpirv.empty()) {
effectiveSpirv = &structLutSpirv;
} else {
MGLOG_W("DefeatConstStructArrayLutForEssl failed, continuing with unsplit SPIR-V.");
}
}
// ESSL stage-matches uniform blocks by member precision, but SPIRV-Cross prints
// a RelaxedPrecision member as explicit "mediump" in the vertex stage and as
// UNQUALIFIED (mediump-by-default) in the fragment stage; after
@@ -3345,6 +3441,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
source = RemoveLayoutBinding(source);
if (applyClipDistanceQuirk) {
// Adreno rejects the gl_ClipDistance redeclaration SPIRV-Cross still emits
// ("reserved built-in name") but accepts plain usage with
// GL_EXT_clip_cull_distance required; drop the line, keep the #extension.
source = RemoveClipDistanceRedeclaration(source);
}
source = ProcessOutColorLocations(source);
source = ForceFlatIntegerVaryings(source, glShaderType);
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
@@ -258,6 +258,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
private:
Uint m_backendVAOId = 0;
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
// Attribs the frontend has Enabled but that have no source at all (no buffer object
// and NULL client pointer). GL keeps such attribs latently enabled, but Adreno's ES
// driver treats them as client arrays and memcpys from address 0 at draw time
// (SIGSEGV), so they are kept disabled on the backend VAO until they gain a source.
Uint32 m_forceDisabledAttribsMask = 0;
Bool m_isInitialized = false;
Uint16 m_syncedIndexBufferVersion = 0;
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
+125
View File
@@ -342,6 +342,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
return result;
}
String RemoveClipDistanceRedeclaration(const String& glslCode) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// Adreno rejects any redeclaration of gl_ClipDistance/gl_CullDistance ("reserved
// built-in name") even with GL_EXT_clip_cull_distance required, but accepts plain
// usage of the builtin. Drop the desktop-style redeclaration line SPIRV-Cross
// prints; the "#extension GL_EXT_clip_cull_distance : require" line stays.
static const std::regex redeclarationRegex(
R"(^\s*(?:out|in)\s+(?:(?:high|medium|low)p\s+)?float\s+gl_(?:Clip|Cull)Distance\[[0-9]+\];\s*$)");
String result;
result.reserve(glslCode.size());
SizeT lineStart = 0;
Bool firstLine = true;
while (lineStart <= glslCode.size()) {
SizeT lineEnd = glslCode.find('\n', lineStart);
const Bool lastLine = lineEnd == String::npos;
String line = glslCode.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart);
if (!std::regex_match(line, redeclarationRegex)) {
if (!firstLine) {
result += '\n';
}
result += line;
firstLine = false;
}
if (lastLine) {
break;
}
lineStart = lineEnd + 1;
}
return result;
}
} // namespace PrgramImpl
namespace Utils {
@@ -764,5 +799,95 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
}
static SizeT AlignReadbackRow(SizeT rowBytes, Int alignment) {
const SizeT align = alignment > 0 ? static_cast<SizeT>(alignment) : 1;
return (rowBytes + align - 1) / align * align;
}
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
// 4 components x GetReadbackComponentSize(wideType) bytes each.
// applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage
// of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4).
// Per the GL addressing rules, slice k row j lands at
// SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes
// + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride.
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams) {
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
if (dstPixelBytes == 0) {
return false;
}
PackedReadbackLayout packedLayout{};
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
const SizeT dstComponentSize = GetReadbackComponentSize(type);
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
// rows are written so skip regions of the destination stay untouched.
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
const SizeT imageRows =
applyPackImageParams && packParams.ImageHeight > 0
? static_cast<SizeT>(packParams.ImageHeight)
: static_cast<SizeT>(sliceHeight);
const SizeT dstImageStride = imageRows * dstRowStride;
const SizeT skipImages =
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
const SizeT dstSkipOffset = skipImages * dstImageStride +
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
if (pixelPackBufferObject) {
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
if (requiredSize > pixelPackBufferObject->GetSize()) {
MGLOG_E("Readback conversion: pixel pack buffer is too small");
return true;
}
}
const SizeT srcComponentSize = GetReadbackComponentSize(wideType);
const SizeT srcPixelBytes = 4 * srcComponentSize;
Vector<Uint8> convertedRow(dstRowBytes);
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
for (GLsizei row = 0; row < sliceHeight; ++row) {
const SizeT flatRow = static_cast<SizeT>(slice) * static_cast<SizeT>(sliceHeight) +
static_cast<SizeT>(row);
const Uint8* srcRow = wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast<SizeT>(width), wideType,
mapping, type);
if (packParams.SwapBytes) {
const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize;
if (groupSize > 1) {
for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) {
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize);
}
}
}
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
static_cast<SizeT>(row) * dstRowStride;
if (pixelPackBufferObject) {
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
pboBaseOffset + dstOffset);
} else {
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
}
}
}
return true;
}
} // namespace ReadbackImpl
} // namespace MobileGL::MG_Backend::DirectGLES
+9
View File
@@ -88,6 +88,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// bytes, dst receives width * GetReadbackDstPixelSize(mapping, type) bytes.
void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
const ReadbackChannelMapping& mapping, GLenum type);
// Stores wide RGBA(_INTEGER) rows into the client pointer or the bound PACK pixel buffer,
// honoring the client-side PACK pixel-store parameters (row length, alignment, skips,
// swap-bytes, and - when applyPackImageParams - image height/skip images). Shared by the
// DirectGLES and DirectVulkan readback conversion paths.
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams);
} // namespace ReadbackImpl
namespace PrgramImpl {
@@ -97,6 +105,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint32 unormOutputMask);
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
String RemoveLayoutBinding(const String& glslCode);
String RemoveClipDistanceRedeclaration(const String& glslCode);
} // namespace PrgramImpl
namespace Utils {
@@ -140,6 +140,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case TextureInternalFormat::RGB:
case TextureInternalFormat::RGB8:
return TextureInternalFormat::RGBA8;
// Legacy low-bit-depth formats with no (or rarely supported) native Vulkan
// encoding; a wider normalized fallback keeps at least the required precision.
case TextureInternalFormat::R3G3B2:
case TextureInternalFormat::RGB4:
case TextureInternalFormat::RGB5:
case TextureInternalFormat::RGBA2:
case TextureInternalFormat::RGBA4:
case TextureInternalFormat::RGB5A1:
return TextureInternalFormat::RGBA8;
case TextureInternalFormat::RGB10:
return TextureInternalFormat::RGB10A2;
case TextureInternalFormat::RGB12:
case TextureInternalFormat::RGBA12:
return TextureInternalFormat::RGBA16;
case TextureInternalFormat::SRGB8:
return TextureInternalFormat::SRGB8Alpha8;
case TextureInternalFormat::RGB8Snorm:
@@ -397,8 +411,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (!handle.Handle || (handle.Backend != WindowBackend::Android &&
handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer)) {
MGLOG_E("DirectVulkan backend only supports Android, X11, and CAMetalLayer native windows");
handle.Backend != WindowBackend::MetalLayer &&
handle.Backend != WindowBackend::Win32)) {
MGLOG_E("DirectVulkan backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
return false;
}
@@ -20,7 +20,8 @@
#include <spirv_reflect.h>
namespace MobileGL::MG_Backend::DirectVulkan {
UniquePtr<VulkanRenderer> pVulkanRenderer = nullptr;
// Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<VulkanRenderer>& pVulkanRenderer = *new UniquePtr<VulkanRenderer>();
namespace {
// Generation of the live VulkanRenderer instance, mirroring
@@ -12,7 +12,7 @@
#include "Renderer/VulkanRenderer.h"
namespace MobileGL::MG_Backend::DirectVulkan {
extern UniquePtr<VulkanRenderer> pVulkanRenderer;
extern UniquePtr<VulkanRenderer>& pVulkanRenderer;
// Generation of the live VulkanRenderer instance, mirroring DirectGLES's
// g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever
@@ -1094,9 +1094,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (auto* binding : bindings) {
MOBILEGL_ASSERT(binding != nullptr, "ProgramFactory: null descriptor binding reflection record");
const auto kind = ReflectDescriptorTypeToBindingKind(binding->descriptor_type);
MOBILEGL_ASSERT(binding->count == 1,
"ProgramFactory: descriptor arrays are unsupported (name='%s' count=%u)",
binding->name ? binding->name : "<null>", binding->count);
// UBO instance arrays (uniform Block {...} b[N];) occupy one binding with
// descriptorCount = N; other descriptor arrays stay unsupported and must
// fail program creation cleanly rather than continue with corrupt state.
if (binding->count != 1 && kind != ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) {
MGLOG_E("ProgramFactory: descriptor arrays are unsupported for this descriptor "
"kind (name='%s' count=%u type=%d)",
binding->name ? binding->name : "<null>", binding->count,
static_cast<Int>(binding->descriptor_type));
destroyReflectModules();
return false;
}
DescriptorKey key{};
key.kind = kind;
@@ -1613,6 +1621,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.storageBlockIndexByBinding.assign(m_maxBindings, -1);
entry.globalUboBinding = -1;
entry.dynamicBindings.clear();
entry.bindingDescriptorCounts.assign(m_maxBindings, 1);
entry.arrayedUniformBlockIndicesByBinding.clear();
// Use SpvcSession (Reflection mode) to reflect all SPIR-V modules in a single pass per module
for (const auto& module : spirv) {
@@ -1628,6 +1638,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"ProgramFactory::ReflectLayout: failed to create reflection module (result=%d)",
static_cast<Int>(createReflectResult));
// Descriptor counts per binding (UBO instance arrays reflect count > 1).
UnorderedMap<Uint32, Uint32> descriptorCountByBinding;
{
uint32_t countProbe = 0;
if (spvReflectEnumerateDescriptorBindings(&reflectModule, &countProbe, nullptr) ==
SPV_REFLECT_RESULT_SUCCESS &&
countProbe > 0) {
Vector<SpvReflectDescriptorBinding*> probeBindings(countProbe);
if (spvReflectEnumerateDescriptorBindings(&reflectModule, &countProbe,
probeBindings.data()) ==
SPV_REFLECT_RESULT_SUCCESS) {
for (const auto* probeBinding : probeBindings) {
if (probeBinding != nullptr) {
descriptorCountByBinding[probeBinding->binding] =
std::max<Uint32>(1, probeBinding->count);
}
}
}
}
}
// Reflect uniform buffers
auto ubos = session.GetShaderInterface(SPVC_RESOURCE_TYPE_UNIFORM_BUFFER);
for (const auto& ubo : ubos) {
@@ -1653,9 +1684,69 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue;
}
const Uint blockIndex = program.GetUniformBlockIndex(ubo.name.c_str());
if (blockIndex == 0xFFFFFFFFu) {
MGLOG_D("ProgramFactory::ReflectLayout: skipping inactive UBO '%s' at binding %u",
const auto countIt = descriptorCountByBinding.find(binding);
const Uint32 descriptorCount =
countIt != descriptorCountByBinding.end() ? countIt->second : 1u;
if (descriptorCount <= 1) {
const Uint blockIndex = program.GetUniformBlockIndex(ubo.name.c_str());
if (blockIndex == 0xFFFFFFFFu) {
MGLOG_D("ProgramFactory::ReflectLayout: skipping inactive UBO '%s' at binding %u",
ubo.name.c_str(), binding);
continue;
}
MOBILEGL_ASSERT(entry.bindingKinds[binding] == DescriptorBindingKind::None ||
entry.bindingKinds[binding] == DescriptorBindingKind::UniformBufferDynamic,
"ProgramFactory::ReflectLayout: descriptor binding %u has conflicting kinds for UBO '%s'",
binding, ubo.name.c_str());
entry.bindingKinds[binding] = DescriptorBindingKind::UniformBufferDynamic;
MOBILEGL_ASSERT(entry.globalUboBinding != static_cast<Int>(binding),
"ProgramFactory::ReflectLayout: regular UBO '%s' collides with global UBO binding %u",
ubo.name.c_str(), binding);
MOBILEGL_ASSERT(entry.uniformBlockIndexByBinding[binding] < 0 ||
entry.uniformBlockIndexByBinding[binding] == static_cast<Int>(blockIndex),
"ProgramFactory::ReflectLayout: descriptor binding %u maps to conflicting UBO blocks (%d vs %u)",
binding, entry.uniformBlockIndexByBinding[binding], blockIndex);
entry.uniformBlockIndexByBinding[binding] = static_cast<Int>(blockIndex);
continue;
}
// UBO instance array: one binding, descriptorCount elements. GL exposes each
// element as its own active block named "Name[i]"; map every element to its
// GL block index so the descriptor write can gather per-element buffer ranges.
if (descriptorCount > m_maxBindings) {
MGLOG_E("ProgramFactory::ReflectLayout: UBO array '%s' count %u exceeds maxBindings=%u; "
"leaving binding %u unmapped",
ubo.name.c_str(), descriptorCount, m_maxBindings, binding);
continue;
}
Vector<Int> elementBlockIndices;
elementBlockIndices.reserve(descriptorCount);
for (Uint32 element = 0; element < descriptorCount; ++element) {
String elementName = ubo.name + "[" + std::to_string(element) + "]";
Uint elementBlockIndex = program.GetUniformBlockIndex(elementName.c_str());
if (elementBlockIndex == 0xFFFFFFFFu && element == 0) {
// Some frontends report the first element under the bare block name.
elementBlockIndex = program.GetUniformBlockIndex(ubo.name.c_str());
}
if (elementBlockIndex == 0xFFFFFFFFu) {
// Degrade rather than corrupt: reuse element 0's block if we have one,
// otherwise give up on the binding (same observable behavior as an
// inactive block: wrong values, but no crash).
MGLOG_E("ProgramFactory::ReflectLayout: UBO array '%s' element %u has no active "
"GL uniform block",
ubo.name.c_str(), element);
if (!elementBlockIndices.empty()) {
elementBlockIndex = static_cast<Uint>(elementBlockIndices.front());
} else {
break;
}
}
elementBlockIndices.push_back(static_cast<Int>(elementBlockIndex));
}
if (elementBlockIndices.size() != descriptorCount) {
MGLOG_E("ProgramFactory::ReflectLayout: skipping unresolved UBO array '%s' at binding %u",
ubo.name.c_str(), binding);
continue;
}
@@ -1665,14 +1756,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"ProgramFactory::ReflectLayout: descriptor binding %u has conflicting kinds for UBO '%s'",
binding, ubo.name.c_str());
entry.bindingKinds[binding] = DescriptorBindingKind::UniformBufferDynamic;
MOBILEGL_ASSERT(entry.globalUboBinding != static_cast<Int>(binding),
"ProgramFactory::ReflectLayout: regular UBO '%s' collides with global UBO binding %u",
ubo.name.c_str(), binding);
MOBILEGL_ASSERT(entry.uniformBlockIndexByBinding[binding] < 0 ||
entry.uniformBlockIndexByBinding[binding] == static_cast<Int>(blockIndex),
"ProgramFactory::ReflectLayout: descriptor binding %u maps to conflicting UBO blocks (%d vs %u)",
binding, entry.uniformBlockIndexByBinding[binding], blockIndex);
entry.uniformBlockIndexByBinding[binding] = static_cast<Int>(blockIndex);
entry.bindingDescriptorCounts[binding] = static_cast<Uint16>(descriptorCount);
entry.uniformBlockIndexByBinding[binding] = elementBlockIndices[0];
entry.arrayedUniformBlockIndicesByBinding[binding] = Move(elementBlockIndices);
}
// Reflect sampled images, storage images, samplerBuffer uniforms, and SSBOs.
@@ -1819,7 +1905,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDescriptorSetLayoutBinding layoutBinding{};
layoutBinding.binding = binding;
layoutBinding.descriptorCount = 1;
layoutBinding.descriptorCount = entry.bindingDescriptorCounts[binding];
layoutBinding.stageFlags = VK_SHADER_STAGE_ALL;
layoutBinding.pImmutableSamplers = nullptr;
if (kind == DescriptorBindingKind::UniformBufferDynamic) {
@@ -59,6 +59,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<DescriptorBindingKind> bindingKinds;
Vector<Uint32> dynamicBindings;
Vector<Int> uniformBlockIndexByBinding;
// Descriptor count per binding (1 except for UBO instance arrays, which occupy one
// binding with descriptorCount = N).
Vector<Uint16> bindingDescriptorCounts;
// Per-element GL uniform block indices for arrayed UBO bindings (count > 1);
// element 0 of a non-arrayed binding stays in uniformBlockIndexByBinding.
UnorderedMap<Uint32, Vector<Int>> arrayedUniformBlockIndicesByBinding;
Vector<String> samplerNameByBinding;
Vector<Int> samplerUniformLocationByBinding;
Vector<TextureTarget> samplerTextureTargetByBinding;
@@ -97,6 +103,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
bindingKinds = std::move(other.bindingKinds);
dynamicBindings = std::move(other.dynamicBindings);
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
arrayedUniformBlockIndicesByBinding = std::move(other.arrayedUniformBlockIndicesByBinding);
samplerNameByBinding = std::move(other.samplerNameByBinding);
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
@@ -141,6 +149,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
bindingKinds = std::move(other.bindingKinds);
dynamicBindings = std::move(other.dynamicBindings);
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
arrayedUniformBlockIndicesByBinding = std::move(other.arrayedUniformBlockIndicesByBinding);
samplerNameByBinding = std::move(other.samplerNameByBinding);
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
@@ -765,7 +765,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
UboBindResult& out) const {
Uint32 arrayElement, UboBindResult& out) const {
const void* outData = nullptr;
VkDeviceSize outSize = 0;
@@ -791,7 +791,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(binding < programObj.uniformBlockIndexByBinding.size(),
"ResolveUniformBufferPayload: UBO mapping binding %u out of range", binding);
const Int blockIndex = programObj.uniformBlockIndexByBinding[binding];
Int blockIndex = programObj.uniformBlockIndexByBinding[binding];
if (arrayElement > 0) {
const auto arrayIt = programObj.arrayedUniformBlockIndicesByBinding.find(binding);
const Bool elementValid = arrayIt != programObj.arrayedUniformBlockIndicesByBinding.end() &&
arrayElement < arrayIt->second.size();
MOBILEGL_ASSERT(elementValid,
"ResolveUniformBufferPayload: UBO binding %u has no array element %u", binding,
arrayElement);
if (!elementValid) {
return false;
}
blockIndex = arrayIt->second[arrayElement];
}
MOBILEGL_ASSERT(blockIndex >= 0,
"ResolveUniformBufferPayload: no uniform block mapped to descriptor binding %u", binding);
@@ -1038,11 +1050,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
imageInfos.clear();
texelBufferViews.clear();
dynamicOffsets.clear();
// Arrayed UBO bindings contribute extra buffer infos and dynamic offsets; reserve for
// the worst case so the pBufferInfo pointers taken below never dangle on reallocation.
Uint32 uboArrayExtra = 0;
for (const auto& arrayEntry : programObj.arrayedUniformBlockIndicesByBinding) {
uboArrayExtra += static_cast<Uint32>(arrayEntry.second.size()) - 1u;
}
writes.reserve(m_maxBindings);
bufferInfos.reserve(m_maxBindings);
bufferInfos.reserve(m_maxBindings + uboArrayExtra);
imageInfos.reserve(m_maxBindings);
texelBufferViews.reserve(m_maxBindings);
dynamicOffsets.reserve(programObj.dynamicBindings.size());
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
const Uint32 bindingCount =
std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size()));
@@ -1060,40 +1078,51 @@ namespace MobileGL::MG_Backend::DirectVulkan {
write.descriptorCount = 1;
if (kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) {
UboBindResult ubo{};
const Bool hasPayload = ResolveUniformBufferPayload(program, programObj, binding, ubo);
MOBILEGL_ASSERT(hasPayload && ubo.payload != nullptr && ubo.payloadSize > 0,
"UniformDescriptorBinder::BindProgramUniformBuffers failed: missing UBO payload on binding %u",
binding);
const Uint32 descriptorCount =
binding < programObj.bindingDescriptorCounts.size()
? std::max<Uint32>(1, programObj.bindingDescriptorCounts[binding])
: 1u;
const SizeT firstBufferInfoIndex = bufferInfos.size();
for (Uint32 element = 0; element < descriptorCount; ++element) {
UboBindResult ubo{};
const Bool hasPayload =
ResolveUniformBufferPayload(program, programObj, binding, element, ubo);
MOBILEGL_ASSERT(hasPayload && ubo.payload != nullptr && ubo.payloadSize > 0,
"UniformDescriptorBinder::BindProgramUniformBuffers failed: missing UBO payload on binding %u element %u",
binding, element);
VkDescriptorBufferInfo bufferInfo{};
// Keep offset 0 (sub-range selected via the dynamic offset) so the hashed bufferInfo
// is stable across draws and the descriptor-set reuse cache keeps hitting.
bufferInfo.offset = 0;
Uint32 dynOffset;
if (ubo.directBindable) {
// Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy.
bufferInfo.buffer = ubo.buffer;
bufferInfo.range = ubo.range;
dynOffset = static_cast<Uint32>(ubo.dynamicOffset);
} else {
BufferSlice slice{};
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u",
binding);
return false;
VkDescriptorBufferInfo bufferInfo{};
// Keep offset 0 (sub-range selected via the dynamic offset) so the hashed bufferInfo
// is stable across draws and the descriptor-set reuse cache keeps hitting.
bufferInfo.offset = 0;
Uint32 dynOffset;
if (ubo.directBindable) {
// Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy.
bufferInfo.buffer = ubo.buffer;
bufferInfo.range = ubo.range;
dynOffset = static_cast<Uint32>(ubo.dynamicOffset);
} else {
BufferSlice slice{};
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u element %u",
binding, element);
return false;
}
bufferInfo.buffer = slice.buffer;
bufferInfo.range = ubo.payloadSize;
dynOffset = static_cast<Uint32>(slice.offset);
}
bufferInfo.buffer = slice.buffer;
bufferInfo.range = ubo.payloadSize;
dynOffset = static_cast<Uint32>(slice.offset);
bufferInfos.push_back(bufferInfo);
// Dynamic offsets are consumed in binding order, then array element order,
// matching Vulkan's dynamic-offset consumption rules.
dynamicOffsets.push_back(dynOffset);
}
bufferInfos.push_back(bufferInfo);
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
write.pBufferInfo = &bufferInfos.back();
write.descriptorCount = descriptorCount;
write.pBufferInfo = &bufferInfos[firstBufferInfoIndex];
writes.push_back(write);
dynamicOffsets.push_back(dynOffset);
} else if (kind == ProgramFactory::DescriptorBindingKind::UniformTexelBuffer) {
VkBufferView bufferView = VK_NULL_HANDLE;
if (!ResolveTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) ||
@@ -116,7 +116,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
};
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
UboBindResult& out) const;
Uint32 arrayElement, UboBindResult& out) const;
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
VkResult AllocateDescriptorSetsFromActivePool(
@@ -251,11 +251,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const auto internalFormat = renderbuffer->GetInternalFormat();
const VkFormat format = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
if ((aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
MGLOG_E("GetOrCreateRenderbufferResource: color renderbuffer %u is not supported by DirectVulkan render passes yet",
renderbuffer->GetExternalIndex());
return nullptr;
}
// Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the
// usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer),
// BlitFramebuffer, CopyTexImage sources, and out-of-render-pass clear materialization.
const VkImageUsageFlags imageUsage =
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
: VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
auto& resource = m_renderbufferResources[renderbuffer.get()];
const Bool needsCreate =
@@ -285,7 +287,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
imageInfo.format = format;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
imageInfo.usage = imageUsage;
imageInfo.samples = sampleCount;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
@@ -386,6 +388,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void VkRenderPassManager::QueueRenderbufferClear(
GLbitfield mask, const ClearFramebufferPayload& clearPayload,
const MG_State::GLState::FramebufferObject& drawFbo) {
if ((mask & GL_COLOR_BUFFER_BIT) != 0) {
// Color renderbuffer draw buffers take the framebuffer-level clear too; texture
// attachments are skipped by the per-attachment overload's IsRenderbuffer guard.
for (const auto attachmentType : drawFbo.GetDrawBuffers()) {
if (attachmentType == FramebufferAttachmentType::None) {
continue;
}
QueueRenderbufferClear(
ClearAttachmentPayload{.mask = GL_COLOR_BUFFER_BIT, .color = clearPayload.color},
drawFbo.GetAttachment(attachmentType));
}
}
if ((mask & GL_DEPTH_BUFFER_BIT) != 0) {
QueueRenderbufferClear(
ClearAttachmentPayload{.mask = GL_DEPTH_BUFFER_BIT, .depth = clearPayload.depth},
@@ -682,6 +696,83 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// assuming default FBO has the right param
for (Uint32 i = 0; i < colorAttachmentSlotCount; ++i) {
auto drawbuf = drawbufs[i];
// Renderbuffer color attachments mirror the texture path below, with the
// resource (image/view/format/layout) coming from the render-pass manager's
// renderbuffer store instead of the texture manager.
if (drawbuf != FramebufferAttachmentType::None && !isDefaultFbo) {
const auto& rbAtt = fbo.GetAttachment(drawbuf);
if (rbAtt.IsRenderbuffer() && rbAtt.IsComplete()) {
const auto& renderbuffer = rbAtt.GetRenderbuffer();
auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer);
if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
MGLOG_E("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
"renderbuffer %u; using VK_ATTACHMENT_UNUSED",
i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex());
continue;
}
const Uint32 rbAttachmentIndex = static_cast<Uint32>(attachmentDescriptions.size());
attachmentDescriptions.emplace_back();
VkAttachmentDescription& rbDesc = attachmentDescriptions.back();
ClearAttachmentPayload rbClearPayload{};
Bool rbHasClear = GetPendingRenderbufferClear(renderbuffer.get(), rbClearPayload) &&
(rbClearPayload.mask & GL_COLOR_BUFFER_BIT) != 0;
if (rbHasClear &&
MG_Util::GetBaseInternalFormatComponentCount(renderbuffer->GetInternalFormat()) == 3) {
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
rbClearPayload.color =
FloatVec4(rbClearPayload.color.x(), rbClearPayload.color.y(),
rbClearPayload.color.z(), 1.0f);
}
const VkImageLayout trackedRbLayout = rbResource->layout;
rbDesc.flags = 0;
rbDesc.format = rbResource->format;
rbDesc.samples = rbResource->sampleCount;
rbDesc.loadOp = rbHasClear ? VK_ATTACHMENT_LOAD_OP_CLEAR :
(trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ATTACHMENT_LOAD_OP_DONT_CARE
: VK_ATTACHMENT_LOAD_OP_LOAD);
rbDesc.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
rbDesc.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
rbDesc.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
rbDesc.initialLayout = (rbHasClear || trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED) ?
VK_IMAGE_LAYOUT_UNDEFINED : trackedRbLayout;
rbDesc.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
adoptRenderPassSampleCount(rbResource->sampleCount, "color",
static_cast<Int>(renderbuffer->GetExternalIndex()));
if (rbHasClear) {
pendingClearAttachments.emplace_back(PendingClearAttachmentInfo {
.attachmentIndex = rbAttachmentIndex,
.colorAttachmentSlot = i,
.renderbuffer = renderbuffer.get(),
.hasInlinePayload = true,
.inlinePayload = rbClearPayload,
});
}
if (width == 0)
width = static_cast<Int>(rbResource->extent.width);
if (height == 0)
height = static_cast<Int>(rbResource->extent.height);
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
.target = TrackedAttachmentTarget::Renderbuffer,
.renderbuffer = renderbuffer,
.finalLayout = rbDesc.finalLayout,
});
textureResources.emplace_back(nullptr);
attachmentViews.emplace_back(rbResource->view);
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
"GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i);
colorAttachmentRefs[i].attachment = rbAttachmentIndex;
continue;
}
}
auto* texture = ResolveCompleteColorAttachmentTexture(fbo, drawbuf, i);
if (texture == nullptr)
continue;
@@ -700,6 +791,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case TextureTarget::Texture2D:
case TextureTarget::Texture2DArray:
case TextureTarget::Texture2DMultisample:
case TextureTarget::Texture2DMultisampleArray:
case TextureTarget::Texture3D:
case TextureTarget::TextureCubeMap:
case TextureTarget::TextureCubeMapArray:
case TextureTarget::TextureRectangle: {
desc.flags = 0;
desc.format = isDefaultFbo ?
@@ -211,6 +211,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 m_rpFastRbEpoch = 0;
Uint64 m_rpFastRenderPassHash = 0;
public:
struct RenderbufferResource {
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
VkImage image = VK_NULL_HANDLE;
@@ -227,6 +228,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void Destroy(VkDevice device, VmaAllocator allocator);
};
// Public so the renderer's blit/copy/readback bindings can source renderbuffer
// attachments the same way texture attachments go through the texture manager.
RenderbufferResource* GetOrCreateRenderbufferResource(
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer,
ClearAttachmentPayload& outPayload) const;
private:
struct PendingRenderbufferClear {
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
ClearAttachmentPayload payload{};
@@ -235,10 +244,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
RenderbufferResource* GetOrCreateRenderbufferResource(
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer,
ClearAttachmentPayload& outPayload) const;
Bool HasPendingRenderbufferClear(
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
void CollectRenderbufferGarbage();
@@ -375,7 +375,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
switch (format) {
case TextureInternalFormat::RGB:
case TextureInternalFormat::RGB8:
// Legacy low-bit RGB formats share the UNorm8 canonical shadow layout (see
// TextureFormatProcessor), so they upload exactly like RGB8 with an alpha expand.
case TextureInternalFormat::R3G3B2:
case TextureInternalFormat::RGB4:
case TextureInternalFormat::RGB5:
return {VK_FORMAT_R8G8B8A8_UNORM, true, 1, {0xFF, 0x00, 0x00, 0x00}};
// Low-bit RGBA formats: UNorm8x4 canonical shadow, no expansion needed.
case TextureInternalFormat::RGBA2:
case TextureInternalFormat::RGBA4:
case TextureInternalFormat::RGB5A1:
return {VK_FORMAT_R8G8B8A8_UNORM, false, 0, {0, 0, 0, 0}};
// 10/12-bit RGB(A): UNorm16 canonical shadow.
case TextureInternalFormat::RGB10:
case TextureInternalFormat::RGB12:
return {VK_FORMAT_R16G16B16A16_UNORM, true, 2, {0xFF, 0xFF, 0x00, 0x00}};
case TextureInternalFormat::RGBA12:
return {VK_FORMAT_R16G16B16A16_UNORM, false, 0, {0, 0, 0, 0}};
case TextureInternalFormat::SRGB8:
return {VK_FORMAT_R8G8B8A8_SRGB, true, 1, {0xFF, 0x00, 0x00, 0x00}};
case TextureInternalFormat::RGB8Snorm:
@@ -7,6 +7,8 @@
// End of Source File Header
#include "VulkanRenderer.h"
#include "MG_Backend/DirectGLES/Utils.h"
#include "VertexInputStateFactory.h"
#include "VertexInputStateBuilder.h"
@@ -1274,7 +1276,8 @@ void main() {
static Bool ResolveColorBlitBinding(MG_State::GLState::FramebufferObject& fbo, Bool isReadFramebuffer,
Uint32 swapchainImageIndex, SwapchainObject& swapchainObject,
VkTextureManager& textureManager, BlitImageBinding& outBinding) {
VkTextureManager& textureManager,
VkRenderPassManager& renderPassManager, BlitImageBinding& outBinding) {
const Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
const FramebufferAttachmentType attachmentType =
isReadFramebuffer ? fbo.GetReadBuffer() : fbo.GetDrawBuffers()[0];
@@ -1316,8 +1319,24 @@ void main() {
return false;
}
if (attachment.IsRenderbuffer()) {
MGLOG_E("BlitFramebuffer skipped: renderbuffer attachments are not supported yet");
return false;
const auto& renderbuffer = attachment.GetRenderbuffer();
auto* rbResource = renderPassManager.GetOrCreateRenderbufferResource(renderbuffer);
if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
MGLOG_E("BlitFramebuffer skipped: %s framebuffer color renderbuffer %u is unsupported",
outBinding.label, renderbuffer->GetExternalIndex());
return false;
}
outBinding.image = rbResource->image;
outBinding.trackedLayout = &rbResource->layout;
outBinding.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
outBinding.format = rbResource->format;
outBinding.extent = {static_cast<Int>(rbResource->extent.width),
static_cast<Int>(rbResource->extent.height)};
outBinding.mipLevel = 0;
outBinding.mipLevelCount = 1;
outBinding.baseArrayLayer = 0;
outBinding.layerCount = 1;
return true;
}
if (!attachment.IsTexture()) {
MGLOG_E("BlitFramebuffer skipped: unsupported framebuffer attachment type");
@@ -1355,6 +1374,7 @@ void main() {
static Bool ResolveFramebufferBlitBinding(MG_State::GLState::FramebufferObject& fbo, Bool isReadFramebuffer,
Uint32 swapchainImageIndex, SwapchainObject& swapchainObject,
VkTextureManager& textureManager,
VkRenderPassManager& renderPassManager,
VkImageAspectFlags requiredAspectMask,
BlitImageBinding& outBinding) {
const Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
@@ -1398,8 +1418,29 @@ void main() {
return false;
}
if (attachment.IsRenderbuffer()) {
MGLOG_E("BlitFramebuffer skipped: renderbuffer attachments are not supported yet");
return false;
const auto& renderbuffer = attachment.GetRenderbuffer();
auto* rbResource = renderPassManager.GetOrCreateRenderbufferResource(renderbuffer);
if (rbResource == nullptr) {
MGLOG_E("BlitFramebuffer skipped: %s framebuffer renderbuffer %u is unsupported",
outBinding.label, renderbuffer->GetExternalIndex());
return false;
}
if ((rbResource->aspect & requiredAspectMask) != requiredAspectMask) {
MGLOG_E("BlitFramebuffer skipped: %s framebuffer renderbuffer %u is missing aspect mask=0x%x",
outBinding.label, renderbuffer->GetExternalIndex(),
static_cast<Uint32>(requiredAspectMask));
return false;
}
outBinding.image = rbResource->image;
outBinding.trackedLayout = &rbResource->layout;
outBinding.aspectMask = requiredAspectMask;
outBinding.extent = {static_cast<Int>(rbResource->extent.width),
static_cast<Int>(rbResource->extent.height)};
outBinding.mipLevel = 0;
outBinding.mipLevelCount = 1;
outBinding.baseArrayLayer = 0;
outBinding.layerCount = 1;
return true;
}
if (!attachment.IsTexture()) {
MGLOG_E("BlitFramebuffer skipped: unsupported framebuffer attachment type");
@@ -1470,6 +1511,7 @@ void main() {
static Bool ResolveTextureCopySourceBinding(MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex,
SwapchainObject& swapchainObject,
VkTextureManager& textureManager,
VkRenderPassManager& renderPassManager,
VkImageAspectFlags requiredAspectMask,
BlitImageBinding& outBinding) {
const Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
@@ -1514,8 +1556,30 @@ void main() {
return false;
}
if (attachment.IsRenderbuffer()) {
MGLOG_E("CopyTexSubImage2D skipped: renderbuffer read attachments are not supported yet");
return false;
const auto& renderbuffer = attachment.GetRenderbuffer();
auto* rbResource = renderPassManager.GetOrCreateRenderbufferResource(renderbuffer);
if (rbResource == nullptr) {
MGLOG_E("CopyTexSubImage2D skipped: read framebuffer renderbuffer %u is unsupported",
renderbuffer->GetExternalIndex());
return false;
}
if ((rbResource->aspect & requiredAspectMask) != requiredAspectMask) {
MGLOG_E("CopyTexSubImage2D skipped: read framebuffer renderbuffer %u aspect mask=0x%x "
"does not satisfy requested mask=0x%x",
renderbuffer->GetExternalIndex(), static_cast<Uint32>(rbResource->aspect),
static_cast<Uint32>(requiredAspectMask));
return false;
}
outBinding.image = rbResource->image;
outBinding.trackedLayout = &rbResource->layout;
outBinding.aspectMask = requiredAspectMask;
outBinding.extent = {static_cast<Int>(rbResource->extent.width),
static_cast<Int>(rbResource->extent.height)};
outBinding.mipLevel = 0;
outBinding.mipLevelCount = 1;
outBinding.baseArrayLayer = 0;
outBinding.layerCount = 1;
return true;
}
if (!attachment.IsTexture()) {
MGLOG_E("CopyTexSubImage2D skipped: unsupported read framebuffer attachment type");
@@ -1819,58 +1883,369 @@ void main() {
}
}
// Generic VkFormat texel decode into the wide RGBA row layouts the shared readback
// store expects: GL_FLOAT rows for normalized/float sources, GL_INT / GL_UNSIGNED_INT
// rows for integer sources. Missing channels take GL defaults (0,0,0,1).
enum class ReadbackSourceClass : Uint8 { Unsupported, Float, SignedInt, UnsignedInt };
struct ReadbackSourceDesc {
ReadbackSourceClass sourceClass = ReadbackSourceClass::Unsupported;
Int channels = 0; // component count stored per texel
Int componentBits = 0; // per-component bits for regular formats; 0 for special packed
Bool isSnorm = false;
Bool isSrgb = false;
Bool bgraSwizzle = false;
VkFormat special = VK_FORMAT_UNDEFINED; // set for packed/special formats
};
static Bool GetReadbackSourceDesc(VkFormat format, ReadbackSourceDesc& out) {
out = ReadbackSourceDesc{};
switch (format) {
// --- regular UNORM ---
case VK_FORMAT_R8_UNORM: out = {ReadbackSourceClass::Float, 1, 8}; return true;
case VK_FORMAT_R8G8_UNORM: out = {ReadbackSourceClass::Float, 2, 8}; return true;
case VK_FORMAT_R8G8B8A8_UNORM: out = {ReadbackSourceClass::Float, 4, 8}; return true;
case VK_FORMAT_B8G8R8A8_UNORM: out = {ReadbackSourceClass::Float, 4, 8, false, false, true}; return true;
case VK_FORMAT_R16_UNORM: out = {ReadbackSourceClass::Float, 1, 16}; return true;
case VK_FORMAT_R16G16_UNORM: out = {ReadbackSourceClass::Float, 2, 16}; return true;
case VK_FORMAT_R16G16B16A16_UNORM: out = {ReadbackSourceClass::Float, 4, 16}; return true;
// --- SRGB (decode to linear like GL readback of sRGB textures) ---
case VK_FORMAT_R8G8B8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8, false, true}; return true;
case VK_FORMAT_B8G8R8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8, false, true, true}; return true;
// --- SNORM ---
case VK_FORMAT_R8_SNORM: out = {ReadbackSourceClass::Float, 1, 8, true}; return true;
case VK_FORMAT_R8G8_SNORM: out = {ReadbackSourceClass::Float, 2, 8, true}; return true;
case VK_FORMAT_R8G8B8A8_SNORM: out = {ReadbackSourceClass::Float, 4, 8, true}; return true;
case VK_FORMAT_R16_SNORM: out = {ReadbackSourceClass::Float, 1, 16, true}; return true;
case VK_FORMAT_R16G16_SNORM: out = {ReadbackSourceClass::Float, 2, 16, true}; return true;
case VK_FORMAT_R16G16B16A16_SNORM: out = {ReadbackSourceClass::Float, 4, 16, true}; return true;
// --- SFLOAT ---
case VK_FORMAT_R16_SFLOAT: out = {ReadbackSourceClass::Float, 1, 16}; out.special = format; return true;
case VK_FORMAT_R16G16_SFLOAT: out = {ReadbackSourceClass::Float, 2, 16}; out.special = format; return true;
case VK_FORMAT_R16G16B16A16_SFLOAT: out = {ReadbackSourceClass::Float, 4, 16}; out.special = format; return true;
case VK_FORMAT_R32_SFLOAT: out = {ReadbackSourceClass::Float, 1, 32}; out.special = format; return true;
case VK_FORMAT_R32G32_SFLOAT: out = {ReadbackSourceClass::Float, 2, 32}; out.special = format; return true;
case VK_FORMAT_R32G32B32A32_SFLOAT: out = {ReadbackSourceClass::Float, 4, 32}; out.special = format; return true;
// --- UINT ---
case VK_FORMAT_R8_UINT: out = {ReadbackSourceClass::UnsignedInt, 1, 8}; return true;
case VK_FORMAT_R8G8_UINT: out = {ReadbackSourceClass::UnsignedInt, 2, 8}; return true;
case VK_FORMAT_R8G8B8A8_UINT: out = {ReadbackSourceClass::UnsignedInt, 4, 8}; return true;
case VK_FORMAT_R16_UINT: out = {ReadbackSourceClass::UnsignedInt, 1, 16}; return true;
case VK_FORMAT_R16G16_UINT: out = {ReadbackSourceClass::UnsignedInt, 2, 16}; return true;
case VK_FORMAT_R16G16B16A16_UINT: out = {ReadbackSourceClass::UnsignedInt, 4, 16}; return true;
case VK_FORMAT_R32_UINT: out = {ReadbackSourceClass::UnsignedInt, 1, 32}; return true;
case VK_FORMAT_R32G32_UINT: out = {ReadbackSourceClass::UnsignedInt, 2, 32}; return true;
case VK_FORMAT_R32G32B32A32_UINT: out = {ReadbackSourceClass::UnsignedInt, 4, 32}; return true;
// --- SINT ---
case VK_FORMAT_R8_SINT: out = {ReadbackSourceClass::SignedInt, 1, 8}; return true;
case VK_FORMAT_R8G8_SINT: out = {ReadbackSourceClass::SignedInt, 2, 8}; return true;
case VK_FORMAT_R8G8B8A8_SINT: out = {ReadbackSourceClass::SignedInt, 4, 8}; return true;
case VK_FORMAT_R16_SINT: out = {ReadbackSourceClass::SignedInt, 1, 16}; return true;
case VK_FORMAT_R16G16_SINT: out = {ReadbackSourceClass::SignedInt, 2, 16}; return true;
case VK_FORMAT_R16G16B16A16_SINT: out = {ReadbackSourceClass::SignedInt, 4, 16}; return true;
case VK_FORMAT_R32_SINT: out = {ReadbackSourceClass::SignedInt, 1, 32}; return true;
case VK_FORMAT_R32G32_SINT: out = {ReadbackSourceClass::SignedInt, 2, 32}; return true;
case VK_FORMAT_R32G32B32A32_SINT: out = {ReadbackSourceClass::SignedInt, 4, 32}; return true;
// --- packed / special ---
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
case VK_FORMAT_A2B10G10R10_UINT_PACK32:
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
case VK_FORMAT_R5G6B5_UNORM_PACK16:
case VK_FORMAT_B5G6R5_UNORM_PACK16:
case VK_FORMAT_A1R5G5B5_UNORM_PACK16:
case VK_FORMAT_R5G5B5A1_UNORM_PACK16:
case VK_FORMAT_B5G5R5A1_UNORM_PACK16:
case VK_FORMAT_R4G4B4A4_UNORM_PACK16:
case VK_FORMAT_B4G4R4A4_UNORM_PACK16:
out.sourceClass = format == VK_FORMAT_A2B10G10R10_UINT_PACK32 ?
ReadbackSourceClass::UnsignedInt : ReadbackSourceClass::Float;
out.special = format;
return true;
default:
return false;
}
}
static Float SrgbToLinear(Float value) {
if (value <= 0.04045f) {
return value / 12.92f;
}
return std::pow((value + 0.055f) / 1.055f, 2.4f);
}
static Float DecodeUnsignedF11(Uint32 bits) {
const Uint32 exponent = (bits >> 6) & 0x1F;
const Uint32 mantissa = bits & 0x3F;
if (exponent == 0) {
return static_cast<Float>(mantissa) / 64.0f * std::pow(2.0f, -14.0f);
}
if (exponent == 31) {
return mantissa == 0 ? std::numeric_limits<Float>::infinity()
: std::numeric_limits<Float>::quiet_NaN();
}
return (1.0f + static_cast<Float>(mantissa) / 64.0f) *
std::pow(2.0f, static_cast<Float>(static_cast<Int>(exponent)) - 15.0f);
}
static Float DecodeUnsignedF10(Uint32 bits) {
const Uint32 exponent = (bits >> 5) & 0x1F;
const Uint32 mantissa = bits & 0x1F;
if (exponent == 0) {
return static_cast<Float>(mantissa) / 32.0f * std::pow(2.0f, -14.0f);
}
if (exponent == 31) {
return mantissa == 0 ? std::numeric_limits<Float>::infinity()
: std::numeric_limits<Float>::quiet_NaN();
}
return (1.0f + static_cast<Float>(mantissa) / 32.0f) *
std::pow(2.0f, static_cast<Float>(static_cast<Int>(exponent)) - 15.0f);
}
static void DecodeReadbackTexelSpecialFloat(const Uint8* source, VkFormat format, Float* rgba) {
rgba[0] = 0.0f; rgba[1] = 0.0f; rgba[2] = 0.0f; rgba[3] = 1.0f;
switch (format) {
case VK_FORMAT_R16_SFLOAT:
case VK_FORMAT_R16G16_SFLOAT:
case VK_FORMAT_R16G16B16A16_SFLOAT: {
const Int channels = format == VK_FORMAT_R16_SFLOAT ? 1 :
(format == VK_FORMAT_R16G16_SFLOAT ? 2 : 4);
for (Int c = 0; c < channels; ++c) {
Uint16 bits = 0;
Memcpy(&bits, source + static_cast<SizeT>(c) * sizeof(bits), sizeof(bits));
rgba[c] = MG_Util::DecodeHalfBitsToFloat(bits);
}
return;
}
case VK_FORMAT_R32_SFLOAT:
case VK_FORMAT_R32G32_SFLOAT:
case VK_FORMAT_R32G32B32A32_SFLOAT: {
const Int channels = format == VK_FORMAT_R32_SFLOAT ? 1 :
(format == VK_FORMAT_R32G32_SFLOAT ? 2 : 4);
Memcpy(rgba, source, static_cast<SizeT>(channels) * sizeof(Float));
return;
}
case VK_FORMAT_A2B10G10R10_UNORM_PACK32: {
Uint32 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[0] = static_cast<Float>(word & 0x3FFu) / 1023.0f;
rgba[1] = static_cast<Float>((word >> 10) & 0x3FFu) / 1023.0f;
rgba[2] = static_cast<Float>((word >> 20) & 0x3FFu) / 1023.0f;
rgba[3] = static_cast<Float>((word >> 30) & 0x3u) / 3.0f;
return;
}
case VK_FORMAT_B10G11R11_UFLOAT_PACK32: {
Uint32 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[0] = DecodeUnsignedF11(word & 0x7FFu);
rgba[1] = DecodeUnsignedF11((word >> 11) & 0x7FFu);
rgba[2] = DecodeUnsignedF10((word >> 22) & 0x3FFu);
return;
}
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32: {
Uint32 word = 0;
Memcpy(&word, source, sizeof(word));
const Int exponent = static_cast<Int>((word >> 27) & 0x1Fu) - 15 - 9;
const Float scale = std::pow(2.0f, static_cast<Float>(exponent));
rgba[0] = static_cast<Float>(word & 0x1FFu) * scale;
rgba[1] = static_cast<Float>((word >> 9) & 0x1FFu) * scale;
rgba[2] = static_cast<Float>((word >> 18) & 0x1FFu) * scale;
return;
}
case VK_FORMAT_R5G6B5_UNORM_PACK16:
case VK_FORMAT_B5G6R5_UNORM_PACK16: {
Uint16 word = 0;
Memcpy(&word, source, sizeof(word));
const Float c0 = static_cast<Float>((word >> 11) & 0x1Fu) / 31.0f;
const Float c1 = static_cast<Float>((word >> 5) & 0x3Fu) / 63.0f;
const Float c2 = static_cast<Float>(word & 0x1Fu) / 31.0f;
const Bool bgr = format == VK_FORMAT_B5G6R5_UNORM_PACK16;
rgba[0] = bgr ? c2 : c0;
rgba[1] = c1;
rgba[2] = bgr ? c0 : c2;
return;
}
case VK_FORMAT_A1R5G5B5_UNORM_PACK16: {
Uint16 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[3] = static_cast<Float>((word >> 15) & 0x1u);
rgba[0] = static_cast<Float>((word >> 10) & 0x1Fu) / 31.0f;
rgba[1] = static_cast<Float>((word >> 5) & 0x1Fu) / 31.0f;
rgba[2] = static_cast<Float>(word & 0x1Fu) / 31.0f;
return;
}
case VK_FORMAT_R5G5B5A1_UNORM_PACK16: {
Uint16 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[0] = static_cast<Float>((word >> 11) & 0x1Fu) / 31.0f;
rgba[1] = static_cast<Float>((word >> 6) & 0x1Fu) / 31.0f;
rgba[2] = static_cast<Float>((word >> 1) & 0x1Fu) / 31.0f;
rgba[3] = static_cast<Float>(word & 0x1u);
return;
}
case VK_FORMAT_B5G5R5A1_UNORM_PACK16: {
Uint16 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[2] = static_cast<Float>((word >> 11) & 0x1Fu) / 31.0f;
rgba[1] = static_cast<Float>((word >> 6) & 0x1Fu) / 31.0f;
rgba[0] = static_cast<Float>((word >> 1) & 0x1Fu) / 31.0f;
rgba[3] = static_cast<Float>(word & 0x1u);
return;
}
case VK_FORMAT_R4G4B4A4_UNORM_PACK16: {
Uint16 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[0] = static_cast<Float>((word >> 12) & 0xFu) / 15.0f;
rgba[1] = static_cast<Float>((word >> 8) & 0xFu) / 15.0f;
rgba[2] = static_cast<Float>((word >> 4) & 0xFu) / 15.0f;
rgba[3] = static_cast<Float>(word & 0xFu) / 15.0f;
return;
}
case VK_FORMAT_B4G4R4A4_UNORM_PACK16: {
Uint16 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[2] = static_cast<Float>((word >> 12) & 0xFu) / 15.0f;
rgba[1] = static_cast<Float>((word >> 8) & 0xFu) / 15.0f;
rgba[0] = static_cast<Float>((word >> 4) & 0xFu) / 15.0f;
rgba[3] = static_cast<Float>(word & 0xFu) / 15.0f;
return;
}
default:
return;
}
}
static Bool DecodeReadbackRowsToWide(const Uint8* srcPixels, VkFormat srcFormat, GLsizei width,
GLsizei height, Vector<Uint8>& outWide, GLenum& outWideType) {
ReadbackSourceDesc desc{};
if (!GetReadbackSourceDesc(srcFormat, desc)) {
return false;
}
const SizeT texelSize = VulkanRenderer::GetReadbackTexelSize(srcFormat);
if (texelSize == 0) {
return false;
}
const SizeT pixelCount = static_cast<SizeT>(width) * static_cast<SizeT>(height);
outWide.assign(pixelCount * 4 * sizeof(Uint32), 0);
if (desc.sourceClass == ReadbackSourceClass::Float) {
outWideType = GL_FLOAT;
Float* wide = reinterpret_cast<Float*>(outWide.data());
for (SizeT i = 0; i < pixelCount; ++i) {
const Uint8* source = srcPixels + i * texelSize;
Float rgba[4] = {0.0f, 0.0f, 0.0f, 1.0f};
if (desc.special != VK_FORMAT_UNDEFINED) {
DecodeReadbackTexelSpecialFloat(source, desc.special, rgba);
} else {
for (Int c = 0; c < desc.channels; ++c) {
Float value = 0.0f;
if (desc.componentBits == 8) {
if (desc.isSnorm) {
Int8 raw = 0;
Memcpy(&raw, source + c, sizeof(raw));
value = std::max(static_cast<Float>(raw) / 127.0f, -1.0f);
} else {
value = static_cast<Float>(source[c]) / 255.0f;
}
} else { // 16
if (desc.isSnorm) {
Int16 raw = 0;
Memcpy(&raw, source + static_cast<SizeT>(c) * 2, sizeof(raw));
value = std::max(static_cast<Float>(raw) / 32767.0f, -1.0f);
} else {
Uint16 raw = 0;
Memcpy(&raw, source + static_cast<SizeT>(c) * 2, sizeof(raw));
value = static_cast<Float>(raw) / 65535.0f;
}
}
if (desc.isSrgb && c < 3) {
value = SrgbToLinear(value);
}
rgba[c] = value;
}
if (desc.bgraSwizzle) {
std::swap(rgba[0], rgba[2]);
}
}
Memcpy(wide + i * 4, rgba, sizeof(rgba));
}
return true;
}
// Integer classes: decode to 4 x (U)Int32 per texel; missing alpha reads 1.
outWideType = desc.sourceClass == ReadbackSourceClass::SignedInt ? GL_INT : GL_UNSIGNED_INT;
Uint32* wide = reinterpret_cast<Uint32*>(outWide.data());
for (SizeT i = 0; i < pixelCount; ++i) {
const Uint8* source = srcPixels + i * texelSize;
Uint32 rgba[4] = {0, 0, 0, 1};
if (srcFormat == VK_FORMAT_A2B10G10R10_UINT_PACK32) {
Uint32 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[0] = word & 0x3FFu;
rgba[1] = (word >> 10) & 0x3FFu;
rgba[2] = (word >> 20) & 0x3FFu;
rgba[3] = (word >> 30) & 0x3u;
} else {
for (Int c = 0; c < desc.channels; ++c) {
if (desc.componentBits == 8) {
if (desc.sourceClass == ReadbackSourceClass::SignedInt) {
Int8 raw = 0;
Memcpy(&raw, source + c, sizeof(raw));
rgba[c] = static_cast<Uint32>(static_cast<Int32>(raw));
} else {
rgba[c] = source[c];
}
} else if (desc.componentBits == 16) {
if (desc.sourceClass == ReadbackSourceClass::SignedInt) {
Int16 raw = 0;
Memcpy(&raw, source + static_cast<SizeT>(c) * 2, sizeof(raw));
rgba[c] = static_cast<Uint32>(static_cast<Int32>(raw));
} else {
Uint16 raw = 0;
Memcpy(&raw, source + static_cast<SizeT>(c) * 2, sizeof(raw));
rgba[c] = raw;
}
} else {
Memcpy(&rgba[c], source + static_cast<SizeT>(c) * 4, sizeof(Uint32));
}
}
}
Memcpy(wide + i * 4, rgba, sizeof(rgba));
}
return true;
}
static Bool PackReadbackToClientOrPbo(const Uint8* srcPixels, VkFormat srcFormat, GLsizei width,
GLsizei height, GLenum format, GLenum type, void* pixels) {
if (width <= 0 || height <= 0) {
return true;
}
if (type != GL_UNSIGNED_BYTE && type != GL_FLOAT) {
MGLOG_E("DirectVulkan readback skipped: unsupported type=0x%x", type);
DirectGLES::ReadbackImpl::ReadbackChannelMapping mapping{};
if (!DirectGLES::ReadbackImpl::GetReadbackChannelMapping(format, mapping) ||
DirectGLES::ReadbackImpl::GetReadbackDstPixelSize(mapping, type) == 0) {
MGLOG_E("DirectVulkan readback skipped: unsupported format=0x%x type=0x%x", format, type);
return false;
}
const Int dstChannels = GetReadbackChannelCount(format);
if (dstChannels == 0) {
MGLOG_E("DirectVulkan readback skipped: unsupported format=0x%x", format);
return false;
}
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const SizeT dstComponentBytes = type == GL_FLOAT ? sizeof(Float) : sizeof(Uint8);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT dstRowStride = AlignPixelRow(rowPixels * static_cast<SizeT>(dstChannels) * dstComponentBytes,
packParams.Alignment);
const SizeT dstOffset = static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) *
static_cast<SizeT>(dstChannels) * dstComponentBytes;
const SizeT packedSize = dstOffset +
(static_cast<SizeT>(height - 1) * dstRowStride) +
(static_cast<SizeT>(width) * static_cast<SizeT>(dstChannels) * dstComponentBytes);
Vector<Uint8> packed(packedSize, 0);
if (!VulkanRenderer::ConvertReadbackPixels(srcPixels, srcFormat, width, height, format, type,
dstRowStride, packed.data() + dstOffset)) {
Vector<Uint8> wide;
GLenum wideType = GL_FLOAT;
if (!DecodeReadbackRowsToWide(srcPixels, srcFormat, width, height, wide, wideType)) {
MGLOG_E("DirectVulkan readback skipped: unsupported source format=%d",
static_cast<Int>(srcFormat));
return false;
}
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
if (pixelPackBufferObject) {
const SizeT pboOffset = reinterpret_cast<SizeT>(pixels);
if (pboOffset + packed.size() > pixelPackBufferObject->GetSize()) {
MGLOG_E("DirectVulkan readback skipped: pixel pack buffer is too small");
return false;
}
pixelPackBufferObject->WritebackFromBackend({packed.data(), packed.size()}, pboOffset);
return true;
const Bool sourceIsInteger = wideType == GL_INT || wideType == GL_UNSIGNED_INT;
if (sourceIsInteger != mapping.isInteger) {
MGLOG_E("DirectVulkan readback skipped: integerness mismatch (format=0x%x source=%d)",
format, static_cast<Int>(srcFormat));
return false;
}
if (pixels != nullptr && !packed.empty()) {
Memcpy(pixels, packed.data(), packed.size());
}
return true;
return DirectGLES::ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, height,
/*sliceCount=*/1, mapping, type, pixels,
/*applyPackImageParams=*/false);
}
} // namespace
@@ -2153,15 +2528,23 @@ void main() {
MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed.");
// Prime the first frame so Render() always targets an acquired swapchain image.
VkResult acquireResult =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
if (acquireResult == VK_ERROR_OUT_OF_DATE_KHR || acquireResult == VK_SUBOPTIMAL_KHR) {
MGLOG_D("Initialize, vkAcquireNextImageKHR got %d, recreating swapchain", acquireResult);
RecreateSwapchain();
acquireResult =
// A zero-area window (GLFW's hidden helper window during the WGL bootstrap, or a
// window that is already minimized) legitimately yields no swapchain here; defer
// the first acquire to Present in that case instead of acquiring from a null
// swapchain handle.
if (m_swapchainObject.GetHandle() != VK_NULL_HANDLE) {
VkResult acquireResult =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
if (acquireResult == VK_ERROR_OUT_OF_DATE_KHR || acquireResult == VK_SUBOPTIMAL_KHR) {
MGLOG_D("Initialize, vkAcquireNextImageKHR got %d, recreating swapchain", acquireResult);
RecreateSwapchain();
acquireResult =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
}
VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage");
} else {
MGLOG_W("DirectVulkan: no swapchain at initialization (zero-area window); deferring first acquire");
}
VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage");
m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_convertedVertexStreams.clear();
@@ -3552,6 +3935,7 @@ void main() {
(bufferMask.a() ? VK_COLOR_COMPONENT_A_BIT : 0u));
Bool effectiveBlendEnabled = blendEnabled;
MG_State::GLState::ITextureObject* colorAttachmentTexture = nullptr;
MG_State::GLState::RenderbufferObject* colorAttachmentRenderbuffer = nullptr;
if (isDefaultDrawFbo && i < drawBuffers.size() &&
drawBuffers[i] == FramebufferAttachmentType::None) {
// The default framebuffer spans the same MAX_DRAW_BUFFERS slots as an FBO
@@ -3566,12 +3950,24 @@ void main() {
if (!isDefaultDrawFbo && i < drawBuffers.size()) {
const auto drawBuffer = drawBuffers[i];
colorAttachmentTexture = resolveCompleteColorAttachmentTexture(i);
if (drawBuffer == FramebufferAttachmentType::None || colorAttachmentTexture == nullptr) {
if (colorAttachmentTexture == nullptr && drawBuffer != FramebufferAttachmentType::None) {
const auto& attachment = drawFboBinding->GetAttachment(drawBuffer);
if (attachment.IsRenderbuffer() && attachment.IsComplete()) {
colorAttachmentRenderbuffer = attachment.GetRenderbuffer().get();
}
}
if (drawBuffer == FramebufferAttachmentType::None ||
(colorAttachmentTexture == nullptr && colorAttachmentRenderbuffer == nullptr)) {
// GL ignores writes and per-target blend state for GL_NONE draw buffer slots.
// Depth-only or otherwise unattached draw buffers should also discard color writes.
attachmentColorWriteMask = 0;
effectiveBlendEnabled = false;
}
if (colorAttachmentRenderbuffer != nullptr) {
const SizeT componentCount = MG_Util::GetBaseInternalFormatComponentCount(
colorAttachmentRenderbuffer->GetInternalFormat());
attachmentColorWriteMask &= GetSupportedColorWriteMaskForComponentCount(componentCount);
}
if (colorAttachmentTexture != nullptr) {
auto* texture = colorAttachmentTexture;
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
@@ -3648,10 +4044,14 @@ void main() {
Int textureExternalIndex = -1;
if (isDefaultDrawFbo) {
colorAttachmentFormat = m_swapchainObject.GetSurfaceFormat().format;
} else if (colorAttachmentRenderbuffer != nullptr) {
textureExternalIndex = static_cast<Int>(colorAttachmentRenderbuffer->GetExternalIndex());
colorAttachmentFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(
colorAttachmentRenderbuffer->GetInternalFormat());
} else {
auto* texture = colorAttachmentTexture;
MOBILEGL_ASSERT(texture != nullptr,
"GetOrCreatePipeline: blend is enabled on draw buffer %u but no complete texture attachment is bound",
"GetOrCreatePipeline: blend is enabled on draw buffer %u but no complete color attachment is bound",
i);
textureExternalIndex = texture->GetExternalIndex();
auto* textureResource = m_textureManager->SyncTextureAndGetDescriptor(*texture);
@@ -4802,6 +5202,98 @@ void main() {
return true;
}
Bool VulkanRenderer::MaterializePendingClearForRenderbuffer(
VkCommandBuffer commandBuffer, const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer) {
if (renderbuffer == nullptr) {
return true;
}
ClearAttachmentPayload clearPayload{};
if (!m_renderPassManager->GetPendingRenderbufferClear(renderbuffer.get(), clearPayload)) {
return true;
}
MOBILEGL_ASSERT(VkRenderPassManager::GetActiveRenderPass() == nullptr,
"MaterializePendingClearForRenderbuffer requires no active render pass");
auto* resource = m_renderPassManager->GetOrCreateRenderbufferResource(renderbuffer);
if (resource == nullptr) {
MGLOG_E("MaterializePendingClearForRenderbuffer: no resource for renderbuffer %u",
renderbuffer->GetExternalIndex());
return false;
}
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags srcAccessMask = 0;
GetImageTransitionSourceState(resource->layout, srcStageMask, srcAccessMask);
Bool ok = VkTextureManager::TransitionImageLayout(
commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
resource->aspect, 0, 1, 1);
MOBILEGL_ASSERT(ok,
"MaterializePendingClearForRenderbuffer: failed to transition renderbuffer %u to TRANSFER_DST",
renderbuffer->GetExternalIndex());
VkImageSubresourceRange subresourceRange{};
subresourceRange.baseMipLevel = 0;
subresourceRange.levelCount = 1;
subresourceRange.baseArrayLayer = 0;
subresourceRange.layerCount = 1;
VkImageLayout steadyLayout;
if ((resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
VkClearColorValue clearValue{};
clearValue.float32[0] = clearPayload.color.x();
clearValue.float32[1] = clearPayload.color.y();
clearValue.float32[2] = clearPayload.color.z();
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
clearValue.float32[3] =
MG_Util::GetBaseInternalFormatComponentCount(renderbuffer->GetInternalFormat()) == 3 ?
1.0f : clearPayload.color.w();
vkCmdClearColorImage(commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
&clearValue, 1, &subresourceRange);
steadyLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
} else {
VkImageAspectFlags clearAspectMask = 0;
if ((resource->aspect & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 &&
(clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0) {
clearAspectMask |= VK_IMAGE_ASPECT_DEPTH_BIT;
}
if ((resource->aspect & VK_IMAGE_ASPECT_STENCIL_BIT) != 0 &&
(clearPayload.mask & GL_STENCIL_BUFFER_BIT) != 0) {
clearAspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT;
}
if (clearAspectMask != 0) {
subresourceRange.aspectMask = clearAspectMask;
VkClearDepthStencilValue clearValue{};
clearValue.depth = clearPayload.depth;
clearValue.stencil = clearPayload.stencil;
vkCmdClearDepthStencilImage(commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
&clearValue, 1, &subresourceRange);
}
steadyLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
}
VkImageLayout clearLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
ok = VkTextureManager::TransitionImageLayout(
commandBuffer, resource->image, clearLayout, steadyLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
VK_ACCESS_TRANSFER_READ_BIT,
resource->aspect, 0, 1, 1);
MOBILEGL_ASSERT(ok,
"MaterializePendingClearForRenderbuffer: failed to transition renderbuffer %u to steady layout",
renderbuffer->GetExternalIndex());
resource->layout = steadyLayout;
m_renderPassManager->PopPendingRenderbufferClear(renderbuffer.get());
MGLOG_D("MaterializePendingClearForRenderbuffer: renderbuffer %u pending clear materialized",
renderbuffer->GetExternalIndex());
return true;
}
Bool VulkanRenderer::TryBlitToDefaultFramebufferWithShader(FrameContext::FrameData& frame,
MG_State::GLState::FramebufferObject& readFbo,
MG_State::GLState::FramebufferObject& drawFbo,
@@ -4815,8 +5307,10 @@ void main() {
BlitImageBinding srcBinding{};
BlitImageBinding dstBinding{};
if (!ResolveColorBlitBinding(readFbo, true, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, srcBinding) ||
!ResolveColorBlitBinding(drawFbo, false, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, dstBinding)) {
if (!ResolveColorBlitBinding(readFbo, true, m_imageIndexAcquired, m_swapchainObject, *m_textureManager,
*m_renderPassManager, srcBinding) ||
!ResolveColorBlitBinding(drawFbo, false, m_imageIndexAcquired, m_swapchainObject, *m_textureManager,
*m_renderPassManager, dstBinding)) {
return false;
}
if (srcBinding.trackedLayout == nullptr) {
@@ -5002,9 +5496,11 @@ void main() {
BlitImageBinding srcBinding{};
BlitImageBinding dstBinding{};
if (!ResolveFramebufferBlitBinding(*readFbo, true, m_imageIndexAcquired, m_swapchainObject,
*m_textureManager, VK_IMAGE_ASPECT_DEPTH_BIT, srcBinding) ||
*m_textureManager, *m_renderPassManager,
VK_IMAGE_ASPECT_DEPTH_BIT, srcBinding) ||
!ResolveFramebufferBlitBinding(*drawFbo, false, m_imageIndexAcquired, m_swapchainObject,
*m_textureManager, VK_IMAGE_ASPECT_DEPTH_BIT, dstBinding)) {
*m_textureManager, *m_renderPassManager,
VK_IMAGE_ASPECT_DEPTH_BIT, dstBinding)) {
return;
}
@@ -5169,8 +5665,10 @@ void main() {
BlitImageBinding srcBinding{};
BlitImageBinding dstBinding{};
if (!ResolveColorBlitBinding(*readFbo, true, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, srcBinding) ||
!ResolveColorBlitBinding(*drawFbo, false, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, dstBinding)) {
if (!ResolveColorBlitBinding(*readFbo, true, m_imageIndexAcquired, m_swapchainObject, *m_textureManager,
*m_renderPassManager, srcBinding) ||
!ResolveColorBlitBinding(*drawFbo, false, m_imageIndexAcquired, m_swapchainObject, *m_textureManager,
*m_renderPassManager, dstBinding)) {
return;
}
@@ -5374,7 +5872,7 @@ void main() {
BlitImageBinding srcBinding{};
if (!ResolveTextureCopySourceBinding(*readFbo, m_imageIndexAcquired, m_swapchainObject, *m_textureManager,
dstBinding.aspectMask, srcBinding)) {
*m_renderPassManager, dstBinding.aspectMask, srcBinding)) {
RecordTextureCopyError(__func__, ErrorCode::InvalidOperation,
"CopyTexSubImage2D requires a complete read attachment compatible with the destination texture.");
return;
@@ -5715,7 +6213,7 @@ void main() {
const Bool readIsDefaultFbo = readFbo->IsDefaultFramebuffer();
BlitImageBinding srcBinding{};
if (!ResolveColorBlitBinding(*readFbo, true, m_imageIndexAcquired, m_swapchainObject, *m_textureManager,
srcBinding)) {
*m_renderPassManager, srcBinding)) {
return;
}
if (!readIsDefaultFbo) {
@@ -5726,6 +6224,12 @@ void main() {
MOBILEGL_ASSERT(clearReady,
"ReadPixels: failed to materialize pending clear for source textureId=%d",
sourceTexture->GetExternalIndex());
} else if (sourceAttachment.IsRenderbuffer()) {
const Bool clearReady =
MaterializePendingClearForRenderbuffer(frame.commandBuffer, sourceAttachment.GetRenderbuffer());
MOBILEGL_ASSERT(clearReady,
"ReadPixels: failed to materialize pending clear for source renderbuffer %u",
sourceAttachment.GetRenderbuffer()->GetExternalIndex());
}
}
@@ -6857,6 +7361,33 @@ void main() {
}
void VulkanRenderer::Present() {
if (m_swapchainObject.GetHandle() == VK_NULL_HANDLE || m_presentSuspended) {
// No usable swapchain: the window was zero-area at initialization, or
// presentation was suspended when the window minimized. Try to bring a
// swapchain up now that the window may have a real size; until then, drop
// this frame's recording instead of submitting - a submit would wait on a
// never-signaled acquire semaphore and reuse a still-signaled fence.
if (!RecreateSwapchain()) {
auto& suspendedFrame = m_frameContext.GetCurrent();
if (VkRenderPassManager::GetActiveRenderPass()) {
VkRenderPassManager::EndRenderPass(suspendedFrame.commandBuffer);
}
if (suspendedFrame.isCommandRecording) {
m_frameContext.EndCommandRecording();
}
suspendedFrame.isCommandRecording = false;
suspendedFrame.hasCommandBufferRecorded = false;
m_lastPipelineValid = false;
MGLOG_D("Present skipped: no usable swapchain (zero-area window)");
return;
}
m_presentSuspended = false;
const VkResult acquireResult =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
if (acquireResult != VK_SUBOPTIMAL_KHR) {
VK_VERIFY(acquireResult, "Present, deferred first WaitAndAcquireNextImage");
}
}
MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(),
"Present, acquired image index out of range");
m_renderPassManager->OnPresent();
@@ -6890,14 +7421,26 @@ void main() {
auto result = vkQueuePresentKHR(m_presentQueue, &presentPacket.presentInfo);
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
MGLOG_D("Present, vkQueuePresentKHR got %d, recreating swapchain", result);
RecreateSwapchain();
if (!RecreateSwapchain()) {
// Window went zero-area (minimize) with the swapchain out of date:
// stop submitting/acquiring until it has a size again.
m_presentSuspended = true;
m_swapchainResizeRequested = false;
MGLOG_D("Present, zero-area window with out-of-date swapchain; suspending presentation");
return;
}
m_swapchainResizeRequested = false;
result = VK_SUCCESS;
}
VK_VERIFY(result, "Present, vkQueuePresentKHR");
if (m_swapchainResizeRequested) {
MGLOG_D("Present, processing requested swapchain resize");
RecreateSwapchain();
if (!RecreateSwapchain()) {
m_presentSuspended = true;
m_swapchainResizeRequested = false;
MGLOG_D("Present, zero-area window on requested resize; suspending presentation");
return;
}
m_swapchainResizeRequested = false;
}
@@ -6908,7 +7451,12 @@ void main() {
result = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
MGLOG_D("Present, vkAcquireNextImageKHR got %d, recreating swapchain", result);
RecreateSwapchain();
if (!RecreateSwapchain()) {
m_presentSuspended = true;
m_swapchainResizeRequested = false;
MGLOG_D("Present, zero-area window on next-frame acquire; suspending presentation");
return;
}
m_swapchainResizeRequested = false;
result =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
@@ -7560,6 +8108,7 @@ void main() {
MOBILEGL_ASSERT(hwnd, "HWND is null");
VkWin32SurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR};
sci.hinstance = GetModuleHandleW(nullptr);
sci.hwnd = hwnd;
VK_VERIFY(vkCreateWin32SurfaceKHR(m_instance, &sci, nullptr, &m_surface), "vkCreateWin32SurfaceKHR failed");
#elif defined VK_USE_PLATFORM_METAL_EXT
@@ -7721,13 +8270,13 @@ void main() {
m_swapchainObject.Shutdown(m_device);
}
void VulkanRenderer::RecreateSwapchain() {
Bool VulkanRenderer::RecreateSwapchain() {
// Handle cases like minimize on Windows, where swapchain could return a 0x0 extent
const auto swapchainCapabilities =
SwapchainObject::GetSwapchainCapabilities(m_physicalDevice.handle, m_surface);
if (swapchainCapabilities.capabilities.currentExtent.width == 0 ||
swapchainCapabilities.capabilities.currentExtent.height == 0) {
return;
return false;
}
vkDeviceWaitIdle(m_device);
@@ -7771,6 +8320,7 @@ void main() {
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_convertedVertexStreams.clear();
}
return true;
}
const PhysicalDevice& VulkanRenderer::GetPhysicalDevice() const {
@@ -257,7 +257,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;
void RequestSwapchainResize(Uint32 width, Uint32 height);
void RecreateSwapchain();
// Returns false when the surface is zero-area (minimized/hidden window):
// no new swapchain is installed and presentation must stay suspended.
Bool RecreateSwapchain();
private:
struct BlitUniformData {
@@ -355,6 +357,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void* m_platformCloseDisplay = nullptr;
VulkanRendererConfig m_config;
Bool m_swapchainResizeRequested = false;
// Presentation is suspended while the window is zero-area (minimized): the
// swapchain is unusable/out of date, so Present drops frames instead of
// submitting on a signaled fence / presenting never-acquired images.
Bool m_presentSuspended = false;
// Vulkan objects
Bool m_validationLayersEnabled = false;
@@ -555,6 +561,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLenum filter);
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture);
Bool MaterializePendingClearForRenderbuffer(
VkCommandBuffer commandBuffer,
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
MG_State::GLState::ITextureObject& texture,
+28 -5
View File
@@ -8,6 +8,7 @@
#include "EGLImpl.h"
#include "../GetProcAddress.h"
#include <Init.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_State/EGLState/Core.h>
#include <mutex>
@@ -25,6 +26,17 @@ namespace MobileGL::MG_Impl::EGLImpl {
return MG_State::pEGLContext.get();
}
// Entry points that can legitimately be an application's FIRST EGL
// call (display/proc-address/string queries) lazily bring MobileGL
// up here, so the library needs no static constructor and can
// re-initialize after the last eglTerminate tore everything down.
// Teardown-ish entry points keep using GetState() and fail benignly
// when MobileGL is not initialized.
EGLStateContext* GetStateEnsureInitialized() {
MobileGL::EnsureInitialized();
return GetState();
}
MG_Backend::BackendObject* GetBackendObject(EGLStateContext* state) {
auto* backendObject = MG_Backend::pActiveBackendObject.get();
if (!backendObject && state) {
@@ -49,6 +61,8 @@ namespace MobileGL::MG_Impl::EGLImpl {
return MG_Backend::WindowBackend::Android;
#elif defined(__APPLE__)
return MG_Backend::WindowBackend::MetalLayer;
#elif defined(_WIN32)
return MG_Backend::WindowBackend::Win32;
#elif defined(__linux__)
return MG_Backend::WindowBackend::X11;
#else
@@ -187,7 +201,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_FALSE;
}
@@ -208,7 +222,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLDisplay GetDisplay(NativeDisplayType display) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_NO_DISPLAY;
}
@@ -313,6 +327,14 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
backendObject->ReleaseEGLResources();
}
// The last initialized display is gone and nothing is current on any
// thread: tear the whole library down deterministically inside the
// EGL lifecycle (backend, GL/EGL state, glslang). A later EGL call
// re-initializes lazily via GetStateEnsureInitialized(); process exit
// then has nothing left to destroy.
if (!state->HasAnyInitializedDisplay() && !state->HasAnyCurrentContext()) {
MobileGL::Destroy();
}
return EGL_TRUE;
}
@@ -345,7 +367,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLBoolean BindAPI(EGLenum api) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_FALSE;
}
@@ -378,7 +400,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
char const* QueryString(EGLDisplay display, EGLint name) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return nullptr;
}
@@ -641,7 +663,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list) {
(void)attrib_list;
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_NO_DISPLAY;
}
@@ -737,6 +759,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!name) {
return nullptr;
}
MobileGL::EnsureInitialized();
MGLOG_D("eglGetProcAddress(%s)", name);
void* proc = MG_Impl::GetProcAddress(name);
@@ -2144,6 +2144,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
namespace FramebufferImpl {
UniquePtr<DefaultFramebufferInfo> pDefaultFramebufferInfo;
// Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<DefaultFramebufferInfo>& pDefaultFramebufferInfo = *new UniquePtr<DefaultFramebufferInfo>();
} // namespace FramebufferImpl
} // namespace MobileGL::MG_Impl::GLImpl
@@ -78,6 +78,6 @@ namespace MobileGL::MG_Impl::GLImpl {
SharedPtr<MG_State::GLState::ITextureObject> stencilAttachment;
};
extern UniquePtr<DefaultFramebufferInfo> pDefaultFramebufferInfo;
extern UniquePtr<DefaultFramebufferInfo>& pDefaultFramebufferInfo;
} // namespace FramebufferImpl
} // namespace MobileGL::MG_Impl::GLImpl
@@ -14,7 +14,8 @@
#include <MG_State/GLState/TextureState/TextureObjectStubs.h>
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
UniquePtr<ProxyTextureManager> pProxyTextureManager;
// Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<ProxyTextureManager>& pProxyTextureManager = *new UniquePtr<ProxyTextureManager>();
Bool IsProxyTextureTarget(TextureUploadTarget target) {
switch (target) {
@@ -23,5 +23,5 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
UnorderedMap<TextureUploadTarget, SharedPtr<MG_State::GLState::ITextureObject>> m_proxyTexturesMap;
};
extern UniquePtr<ProxyTextureManager> pProxyTextureManager;
extern UniquePtr<ProxyTextureManager>& pProxyTextureManager;
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
@@ -0,0 +1,156 @@
// MobileGL - MobileGL/MG_Impl/WGLImpl/Exporting/Definitions.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// wingdi.h declares most wgl* entry points as WINGDIAPI (__declspec(dllimport)),
// which would reject our definitions. _GDI32_ is the SDK's "I am the module that
// implements these" switch: it turns WINGDIAPI into a plain declaration. It must
// be defined before the first windows.h inclusion in this translation unit.
#if defined(_WIN32) && !defined(_GDI32_)
#define _GDI32_ 1
#endif
#include <Includes.h>
#if defined(_WIN32)
#include "../WGLImpl.h"
namespace WGL = MobileGL::MG_Impl::WGLImpl;
// ---- Pixel-format entry points (gdi32 forwards ChoosePixelFormat/SetPixelFormat/
// ---- DescribePixelFormat/GetPixelFormat/SwapBuffers into these exports) ----
extern "C" int WINAPI wglChoosePixelFormat(HDC hdc, CONST PIXELFORMATDESCRIPTOR* ppfd) {
return WGL::ChoosePixelFormat(hdc, ppfd);
}
extern "C" int WINAPI wglDescribePixelFormat(HDC hdc, int iPixelFormat, UINT nBytes,
LPPIXELFORMATDESCRIPTOR ppfd) {
return WGL::DescribePixelFormat(hdc, iPixelFormat, nBytes, ppfd);
}
extern "C" int WINAPI wglGetPixelFormat(HDC hdc) {
return WGL::GetPixelFormat(hdc);
}
extern "C" BOOL WINAPI wglSetPixelFormat(HDC hdc, int iPixelFormat, CONST PIXELFORMATDESCRIPTOR* ppfd) {
return WGL::SetPixelFormat(hdc, iPixelFormat, ppfd);
}
extern "C" BOOL WINAPI wglSwapBuffers(HDC hdc) {
return WGL::SwapBuffers(hdc);
}
// ---- Context management ----
extern "C" HGLRC WINAPI wglCreateContext(HDC hdc) {
return WGL::CreateContext(hdc);
}
extern "C" HGLRC WINAPI wglCreateLayerContext(HDC hdc, int iLayerPlane) {
return iLayerPlane == 0 ? WGL::CreateContext(hdc) : nullptr;
}
extern "C" BOOL WINAPI wglCopyContext(HGLRC, HGLRC, UINT) {
MGLOG_W("wglCopyContext is not supported");
SetLastError(ERROR_NOT_SUPPORTED);
return FALSE;
}
extern "C" BOOL WINAPI wglDeleteContext(HGLRC hglrc) {
return WGL::DeleteContext(hglrc);
}
extern "C" HGLRC WINAPI wglGetCurrentContext(VOID) {
return WGL::GetCurrentContext();
}
extern "C" HDC WINAPI wglGetCurrentDC(VOID) {
return WGL::GetCurrentDC();
}
extern "C" BOOL WINAPI wglMakeCurrent(HDC hdc, HGLRC hglrc) {
return WGL::MakeCurrent(hdc, hglrc);
}
extern "C" BOOL WINAPI wglShareLists(HGLRC hglrcShare, HGLRC hglrcDest) {
return WGL::ShareLists(hglrcShare, hglrcDest);
}
// ---- Proc address ----
extern "C" PROC WINAPI wglGetProcAddress(LPCSTR lpszProc) {
return WGL::GetProcAddress(lpszProc);
}
extern "C" PROC WINAPI wglGetDefaultProcAddress(LPCSTR lpszProc) {
return WGL::GetProcAddress(lpszProc);
}
// ---- Layer planes and palettes (unsupported; overlay planes do not exist here) ----
extern "C" BOOL WINAPI wglDescribeLayerPlane(HDC, int, int, UINT, LPLAYERPLANEDESCRIPTOR) {
return FALSE;
}
extern "C" int WINAPI wglSetLayerPaletteEntries(HDC, int, int, int, CONST COLORREF*) {
return 0;
}
extern "C" int WINAPI wglGetLayerPaletteEntries(HDC, int, int, int, COLORREF*) {
return 0;
}
extern "C" BOOL WINAPI wglRealizeLayerPalette(HDC, int, BOOL) {
return FALSE;
}
extern "C" BOOL WINAPI wglSwapLayerBuffers(HDC hdc, UINT fuPlanes) {
if (fuPlanes & WGL_SWAP_MAIN_PLANE) {
return WGL::SwapBuffers(hdc);
}
return FALSE;
}
extern "C" DWORD WINAPI wglSwapMultipleBuffers(UINT n, CONST WGLSWAP* ps) {
if (!ps) {
return 0;
}
DWORD swapped = 0;
for (UINT i = 0; i < n; ++i) {
if (WGL::SwapBuffers(ps[i].hdc)) {
++swapped;
}
}
return swapped;
}
// ---- Font rendering (legacy immediate-mode feature; not supported) ----
extern "C" BOOL WINAPI wglUseFontBitmapsA(HDC, DWORD, DWORD, DWORD) {
MGLOG_W("wglUseFontBitmapsA is not supported");
return FALSE;
}
extern "C" BOOL WINAPI wglUseFontBitmapsW(HDC, DWORD, DWORD, DWORD) {
MGLOG_W("wglUseFontBitmapsW is not supported");
return FALSE;
}
extern "C" BOOL WINAPI wglUseFontOutlinesA(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
LPGLYPHMETRICSFLOAT) {
MGLOG_W("wglUseFontOutlinesA is not supported");
return FALSE;
}
extern "C" BOOL WINAPI wglUseFontOutlinesW(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
LPGLYPHMETRICSFLOAT) {
MGLOG_W("wglUseFontOutlinesW is not supported");
return FALSE;
}
#endif // _WIN32
@@ -0,0 +1,30 @@
; MobileGL WGL exports. The wgl* entry points are defined without
; __declspec(dllexport) because wingdi.h pre-declares them (with _GDI32_ they
; become plain declarations, and MSVC rejects adding dllexport afterwards),
; so this .def file is what actually exports them from the DLL.
EXPORTS
wglChoosePixelFormat
wglCopyContext
wglCreateContext
wglCreateLayerContext
wglDeleteContext
wglDescribeLayerPlane
wglDescribePixelFormat
wglGetCurrentContext
wglGetCurrentDC
wglGetDefaultProcAddress
wglGetLayerPaletteEntries
wglGetPixelFormat
wglGetProcAddress
wglMakeCurrent
wglRealizeLayerPalette
wglSetLayerPaletteEntries
wglSetPixelFormat
wglShareLists
wglSwapBuffers
wglSwapLayerBuffers
wglSwapMultipleBuffers
wglUseFontBitmapsA
wglUseFontBitmapsW
wglUseFontOutlinesA
wglUseFontOutlinesW
+732
View File
@@ -0,0 +1,732 @@
// MobileGL - MobileGL/MG_Impl/WGLImpl/WGLImpl.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "WGLImpl.h"
#if defined(_WIN32)
#include "../EGLImpl/EGLImpl.h"
#include "../GetProcAddress.h"
#include <Init.h>
namespace MobileGL::MG_Impl::WGLImpl {
namespace {
// WGL_ARB_pixel_format
constexpr int WGL_NUMBER_PIXEL_FORMATS_ARB = 0x2000;
constexpr int WGL_DRAW_TO_WINDOW_ARB = 0x2001;
constexpr int WGL_DRAW_TO_BITMAP_ARB = 0x2002;
constexpr int WGL_ACCELERATION_ARB = 0x2003;
constexpr int WGL_NEED_PALETTE_ARB = 0x2004;
constexpr int WGL_NEED_SYSTEM_PALETTE_ARB = 0x2005;
constexpr int WGL_SWAP_LAYER_BUFFERS_ARB = 0x2006;
constexpr int WGL_SWAP_METHOD_ARB = 0x2007;
constexpr int WGL_NUMBER_OVERLAYS_ARB = 0x2008;
constexpr int WGL_NUMBER_UNDERLAYS_ARB = 0x2009;
constexpr int WGL_TRANSPARENT_ARB = 0x200A;
constexpr int WGL_SHARE_DEPTH_ARB = 0x200C;
constexpr int WGL_SHARE_STENCIL_ARB = 0x200D;
constexpr int WGL_SHARE_ACCUM_ARB = 0x200E;
constexpr int WGL_SUPPORT_GDI_ARB = 0x200F;
constexpr int WGL_SUPPORT_OPENGL_ARB = 0x2010;
constexpr int WGL_DOUBLE_BUFFER_ARB = 0x2011;
constexpr int WGL_STEREO_ARB = 0x2012;
constexpr int WGL_PIXEL_TYPE_ARB = 0x2013;
constexpr int WGL_COLOR_BITS_ARB = 0x2014;
constexpr int WGL_RED_BITS_ARB = 0x2015;
constexpr int WGL_RED_SHIFT_ARB = 0x2016;
constexpr int WGL_GREEN_BITS_ARB = 0x2017;
constexpr int WGL_GREEN_SHIFT_ARB = 0x2018;
constexpr int WGL_BLUE_BITS_ARB = 0x2019;
constexpr int WGL_BLUE_SHIFT_ARB = 0x201A;
constexpr int WGL_ALPHA_BITS_ARB = 0x201B;
constexpr int WGL_ALPHA_SHIFT_ARB = 0x201C;
constexpr int WGL_ACCUM_BITS_ARB = 0x201D;
constexpr int WGL_ACCUM_RED_BITS_ARB = 0x201E;
constexpr int WGL_ACCUM_GREEN_BITS_ARB = 0x201F;
constexpr int WGL_ACCUM_BLUE_BITS_ARB = 0x2020;
constexpr int WGL_ACCUM_ALPHA_BITS_ARB = 0x2021;
constexpr int WGL_DEPTH_BITS_ARB = 0x2022;
constexpr int WGL_STENCIL_BITS_ARB = 0x2023;
constexpr int WGL_AUX_BUFFERS_ARB = 0x2024;
constexpr int WGL_NO_ACCELERATION_ARB = 0x2025;
constexpr int WGL_FULL_ACCELERATION_ARB = 0x2027;
constexpr int WGL_SWAP_EXCHANGE_ARB = 0x2028;
constexpr int WGL_TYPE_RGBA_ARB = 0x202B;
// WGL_ARB_multisample
constexpr int WGL_SAMPLE_BUFFERS_ARB = 0x2041;
constexpr int WGL_SAMPLES_ARB = 0x2042;
// WGL_ARB_create_context / _profile / _no_error
constexpr int WGL_CONTEXT_MAJOR_VERSION_ARB = 0x2091;
constexpr int WGL_CONTEXT_MINOR_VERSION_ARB = 0x2092;
constexpr int WGL_CONTEXT_LAYER_PLANE_ARB = 0x2093;
constexpr int WGL_CONTEXT_FLAGS_ARB = 0x2094;
constexpr int WGL_CONTEXT_PROFILE_MASK_ARB = 0x9126;
constexpr int WGL_CONTEXT_DEBUG_BIT_ARB = 0x0001;
constexpr int WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB = 0x0002;
constexpr int WGL_CONTEXT_CORE_PROFILE_BIT_ARB = 0x00000001;
constexpr int WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB = 0x00000002;
constexpr int WGL_CONTEXT_OPENGL_NO_ERROR_ARB = 0x31B3;
constexpr DWORD ERROR_INVALID_VERSION_ARB = 0x2095;
constexpr DWORD ERROR_INVALID_PROFILE_ARB = 0x2096;
struct PixelFormatInfo {
GLint AlphaBits;
GLint DepthBits;
GLint StencilBits;
};
// Mirrors the two EGLState configs (RGBA8 + depth24, stencil 8 / stencil 0).
constexpr PixelFormatInfo kPixelFormats[] = {
{8, 24, 8},
{8, 24, 0},
};
constexpr int kPixelFormatCount = static_cast<int>(std::size(kPixelFormats));
struct ContextObject {
EGLDisplay Display = EGL_NO_DISPLAY;
EGLConfig Config = nullptr;
EGLContext Context = EGL_NO_CONTEXT;
};
struct WindowSurface {
EGLDisplay Display = EGL_NO_DISPLAY;
EGLSurface Surface = EGL_NO_SURFACE;
Uint32 Width = 0;
Uint32 Height = 0;
};
std::recursive_mutex& RegistryMutex() {
static auto* mutex = new std::recursive_mutex();
return *mutex;
}
UnorderedMap<HGLRC, ContextObject>& Contexts() {
static auto* contexts = new UnorderedMap<HGLRC, ContextObject>();
return *contexts;
}
UnorderedMap<HWND, WindowSurface>& WindowSurfaces() {
static auto* surfaces = new UnorderedMap<HWND, WindowSurface>();
return *surfaces;
}
UnorderedMap<HWND, int>& WindowPixelFormats() {
static auto* formats = new UnorderedMap<HWND, int>();
return *formats;
}
Uint64& NextContextHandle() {
static auto* handle = new Uint64(0x10000);
return *handle;
}
struct ThreadCurrent {
HDC DC = nullptr;
HGLRC Context = nullptr;
};
thread_local ThreadCurrent t_current;
Int& SwapIntervalShadow() {
static auto* interval = new Int(1);
return *interval;
}
void EnsureInitialized() {
// Initialize() loads backend libraries and glslang, which must not run
// under the loader lock; first WGL call is the earliest safe moment.
// MobileGL::EnsureInitialized (not a local once_flag) so a fresh init
// can follow a full teardown from the last eglTerminate.
MobileGL::EnsureInitialized();
}
EGLDisplay EnsureDisplay() {
EnsureInitialized();
EGLDisplay display = EGLImpl::GetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
return EGL_NO_DISPLAY;
}
if (!EGLImpl::Initialize(display, nullptr, nullptr)) {
return EGL_NO_DISPLAY;
}
return display;
}
HGLRC EncodeContext(Uint64 handle) {
return reinterpret_cast<HGLRC>(static_cast<SizeT>(handle));
}
ContextObject* TryGetContext(HGLRC hglrc) {
auto& contexts = Contexts();
auto it = contexts.find(hglrc);
return it == contexts.end() ? nullptr : &it->second;
}
const PixelFormatInfo& PixelFormatForWindow(HWND hwnd) {
auto& formats = WindowPixelFormats();
auto it = formats.find(hwnd);
int index = it == formats.end() ? 1 : it->second;
if (index < 1 || index > kPixelFormatCount) {
index = 1;
}
return kPixelFormats[index - 1];
}
Bool QueryClientSize(HWND hwnd, Uint32& width, Uint32& height) {
RECT rect{};
if (!GetClientRect(hwnd, &rect)) {
return false;
}
width = static_cast<Uint32>(std::max<LONG>(rect.right - rect.left, 1));
height = static_cast<Uint32>(std::max<LONG>(rect.bottom - rect.top, 1));
return true;
}
// The backends never query the HWND client size themselves; the WGL layer
// owns size discovery and pushes changes through the internal resize hook
// (same contract as the macOS CGL layer).
void SyncSurfaceSize(HWND hwnd, WindowSurface& surface) {
Uint32 width = 0;
Uint32 height = 0;
if (!QueryClientSize(hwnd, width, height)) {
return;
}
if (width == surface.Width && height == surface.Height) {
return;
}
if (EGLImpl::ResizePlatformWindowSurface(surface.Display, surface.Surface,
static_cast<EGLint>(width), static_cast<EGLint>(height))) {
surface.Width = width;
surface.Height = height;
}
}
WindowSurface* EnsureWindowSurface(HWND hwnd, const ContextObject& context) {
auto& surfaces = WindowSurfaces();
auto it = surfaces.find(hwnd);
if (it != surfaces.end()) {
SyncSurfaceSize(hwnd, it->second);
return &it->second;
}
Uint32 width = 0;
Uint32 height = 0;
if (!QueryClientSize(hwnd, width, height)) {
MGLOG_E("wgl: GetClientRect failed for HWND %p", hwnd);
return nullptr;
}
const EGLAttrib attribs[] = {
EGL_WIDTH, static_cast<EGLAttrib>(width),
EGL_HEIGHT, static_cast<EGLAttrib>(height),
EGL_NONE,
};
EGLSurface surface =
EGLImpl::CreatePlatformWindowSurface(context.Display, context.Config, hwnd, attribs);
if (surface == EGL_NO_SURFACE) {
MGLOG_E("wgl: failed to create window surface for HWND %p (%ux%u)", hwnd, width, height);
return nullptr;
}
WindowSurface record;
record.Display = context.Display;
record.Surface = surface;
record.Width = width;
record.Height = height;
auto [inserted, _] = surfaces.emplace(hwnd, record);
return &inserted->second;
}
HGLRC CreateContextFromEGLAttribs(HDC hdc, HGLRC share, const EGLint* contextAttribs) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
EGLDisplay display = EnsureDisplay();
if (display == EGL_NO_DISPLAY) {
MGLOG_E("wgl: no EGL display");
return nullptr;
}
EGLImpl::BindAPI(EGL_OPENGL_API);
EGLContext shareContext = EGL_NO_CONTEXT;
if (share) {
auto* shareObject = TryGetContext(share);
if (!shareObject) {
SetLastError(ERROR_INVALID_HANDLE);
return nullptr;
}
shareContext = shareObject->Context;
}
HWND hwnd = WindowFromDC(hdc);
const PixelFormatInfo& pixelFormat = PixelFormatForWindow(hwnd);
const EGLint configAttribs[] = {
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, pixelFormat.AlphaBits,
EGL_DEPTH_SIZE, pixelFormat.DepthBits,
EGL_STENCIL_SIZE, pixelFormat.StencilBits,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT | EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
EGL_NONE,
};
EGLConfig config = nullptr;
EGLint configCount = 0;
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) || configCount <= 0) {
MGLOG_E("wgl: eglChooseConfig failed");
return nullptr;
}
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
if (eglContext == EGL_NO_CONTEXT) {
MGLOG_E("wgl: eglCreateContext failed");
return nullptr;
}
ContextObject object;
object.Display = display;
object.Config = config;
object.Context = eglContext;
const auto handle = EncodeContext(NextContextHandle()++);
Contexts()[handle] = object;
MGLOG_I("wgl: created context %p (EGL context %p)", handle, eglContext);
return handle;
}
// ---- WGL extension entry points (resolved via wglGetProcAddress only) ----
const char* WINAPI Ext_GetExtensionsStringARB(HDC) {
return "WGL_ARB_create_context WGL_ARB_create_context_no_error WGL_ARB_create_context_profile "
"WGL_ARB_extensions_string WGL_ARB_pixel_format WGL_EXT_extensions_string WGL_EXT_swap_control";
}
const char* WINAPI Ext_GetExtensionsStringEXT() {
return Ext_GetExtensionsStringARB(nullptr);
}
HGLRC WINAPI Ext_CreateContextAttribsARB(HDC hdc, HGLRC hShareContext, const int* attribList) {
EnsureInitialized();
int major = 1;
int minor = 0;
int profileMask = 0;
int flags = 0;
if (attribList) {
for (SizeT i = 0; attribList[i] != 0; i += 2) {
const int attrib = attribList[i];
const int value = attribList[i + 1];
switch (attrib) {
case WGL_CONTEXT_MAJOR_VERSION_ARB:
major = value;
break;
case WGL_CONTEXT_MINOR_VERSION_ARB:
minor = value;
break;
case WGL_CONTEXT_PROFILE_MASK_ARB:
profileMask = value;
break;
case WGL_CONTEXT_FLAGS_ARB:
flags = value;
break;
case WGL_CONTEXT_LAYER_PLANE_ARB:
if (value != 0) {
SetLastError(ERROR_INVALID_PARAMETER);
return nullptr;
}
break;
case WGL_CONTEXT_OPENGL_NO_ERROR_ARB:
// Accepted and ignored: MobileGL always validates.
break;
default:
MGLOG_D("wglCreateContextAttribsARB: ignoring attrib 0x%04x = 0x%x", attrib, value);
break;
}
}
}
if (major < 1 || (profileMask & ~(WGL_CONTEXT_CORE_PROFILE_BIT_ARB |
WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB))) {
SetLastError(profileMask ? ERROR_INVALID_PROFILE_ARB : ERROR_INVALID_VERSION_ARB);
return nullptr;
}
Vector<EGLint> attribs = {
EGL_CONTEXT_MAJOR_VERSION, major,
EGL_CONTEXT_MINOR_VERSION, minor,
};
const Bool wantsCompat = (profileMask & WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB) != 0;
if (major > 3 || (major == 3 && minor >= 2) || profileMask != 0) {
attribs.push_back(EGL_CONTEXT_OPENGL_PROFILE_MASK);
attribs.push_back(wantsCompat ? EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT
: EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT);
}
if (flags & WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB) {
attribs.push_back(EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE);
attribs.push_back(EGL_TRUE);
}
if (flags & WGL_CONTEXT_DEBUG_BIT_ARB) {
attribs.push_back(EGL_CONTEXT_OPENGL_DEBUG);
attribs.push_back(EGL_TRUE);
}
attribs.push_back(EGL_NONE);
return CreateContextFromEGLAttribs(hdc, hShareContext, attribs.data());
}
BOOL WINAPI Ext_SwapIntervalEXT(int interval) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
EGLDisplay display = EnsureDisplay();
if (display == EGL_NO_DISPLAY) {
return FALSE;
}
if (interval < 0) {
// Adaptive vsync is not supported; clamp to regular vsync.
interval = 1;
}
EGLImpl::SwapInterval(display, interval);
SwapIntervalShadow() = interval;
return TRUE;
}
int WINAPI Ext_GetSwapIntervalEXT() {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
return SwapIntervalShadow();
}
int PixelFormatAttribValue(int format, int attrib) {
const PixelFormatInfo& info = kPixelFormats[format - 1];
switch (attrib) {
case WGL_NUMBER_PIXEL_FORMATS_ARB:
return kPixelFormatCount;
case WGL_SUPPORT_OPENGL_ARB:
case WGL_DRAW_TO_WINDOW_ARB:
case WGL_DOUBLE_BUFFER_ARB:
return 1;
case WGL_ACCELERATION_ARB:
return WGL_FULL_ACCELERATION_ARB;
case WGL_PIXEL_TYPE_ARB:
return WGL_TYPE_RGBA_ARB;
case WGL_COLOR_BITS_ARB:
return 32;
case WGL_RED_BITS_ARB:
case WGL_GREEN_BITS_ARB:
case WGL_BLUE_BITS_ARB:
return 8;
case WGL_RED_SHIFT_ARB:
return 16;
case WGL_GREEN_SHIFT_ARB:
return 8;
case WGL_BLUE_SHIFT_ARB:
return 0;
case WGL_ALPHA_BITS_ARB:
return info.AlphaBits;
case WGL_ALPHA_SHIFT_ARB:
return 24;
case WGL_DEPTH_BITS_ARB:
return info.DepthBits;
case WGL_STENCIL_BITS_ARB:
return info.StencilBits;
case WGL_SWAP_METHOD_ARB:
return WGL_SWAP_EXCHANGE_ARB;
case WGL_DRAW_TO_BITMAP_ARB:
case WGL_NEED_PALETTE_ARB:
case WGL_NEED_SYSTEM_PALETTE_ARB:
case WGL_SWAP_LAYER_BUFFERS_ARB:
case WGL_NUMBER_OVERLAYS_ARB:
case WGL_NUMBER_UNDERLAYS_ARB:
case WGL_TRANSPARENT_ARB:
case WGL_SHARE_DEPTH_ARB:
case WGL_SHARE_STENCIL_ARB:
case WGL_SHARE_ACCUM_ARB:
case WGL_SUPPORT_GDI_ARB:
case WGL_STEREO_ARB:
case WGL_ACCUM_BITS_ARB:
case WGL_ACCUM_RED_BITS_ARB:
case WGL_ACCUM_GREEN_BITS_ARB:
case WGL_ACCUM_BLUE_BITS_ARB:
case WGL_ACCUM_ALPHA_BITS_ARB:
case WGL_AUX_BUFFERS_ARB:
case WGL_SAMPLE_BUFFERS_ARB:
case WGL_SAMPLES_ARB:
default:
return 0;
}
}
BOOL WINAPI Ext_GetPixelFormatAttribivARB(HDC, int iPixelFormat, int iLayerPlane, UINT nAttributes,
const int* piAttributes, int* piValues) {
if (iLayerPlane != 0 || !piAttributes || !piValues) {
return FALSE;
}
// Format 0 is only valid for WGL_NUMBER_PIXEL_FORMATS_ARB queries.
if (iPixelFormat < 0 || iPixelFormat > kPixelFormatCount) {
return FALSE;
}
const int format = iPixelFormat == 0 ? 1 : iPixelFormat;
for (UINT i = 0; i < nAttributes; ++i) {
piValues[i] = PixelFormatAttribValue(format, piAttributes[i]);
}
return TRUE;
}
BOOL WINAPI Ext_GetPixelFormatAttribfvARB(HDC hdc, int iPixelFormat, int iLayerPlane, UINT nAttributes,
const int* piAttributes, FLOAT* pfValues) {
if (!pfValues) {
return FALSE;
}
Vector<int> values(nAttributes);
if (!Ext_GetPixelFormatAttribivARB(hdc, iPixelFormat, iLayerPlane, nAttributes, piAttributes,
values.data())) {
return FALSE;
}
for (UINT i = 0; i < nAttributes; ++i) {
pfValues[i] = static_cast<FLOAT>(values[i]);
}
return TRUE;
}
BOOL WINAPI Ext_ChoosePixelFormatARB(HDC, const int* piAttribIList, const FLOAT*, UINT nMaxFormats,
int* piFormats, UINT* nNumFormats) {
if (!piFormats || !nNumFormats) {
return FALSE;
}
int wantedStencil = 0;
if (piAttribIList) {
for (SizeT i = 0; piAttribIList[i] != 0; i += 2) {
if (piAttribIList[i] == WGL_STENCIL_BITS_ARB) {
wantedStencil = piAttribIList[i + 1];
}
}
}
UINT count = 0;
const int preferred = wantedStencil > 0 ? 1 : 2;
const int fallback = wantedStencil > 0 ? 2 : 1;
if (count < nMaxFormats) {
piFormats[count++] = preferred;
}
if (count < nMaxFormats) {
piFormats[count++] = fallback;
}
*nNumFormats = count;
return TRUE;
}
struct WGLExtensionProc {
const char* Name;
PROC Proc;
};
const WGLExtensionProc kWGLExtensionProcs[] = {
{"wglGetExtensionsStringARB", reinterpret_cast<PROC>(Ext_GetExtensionsStringARB)},
{"wglGetExtensionsStringEXT", reinterpret_cast<PROC>(Ext_GetExtensionsStringEXT)},
{"wglCreateContextAttribsARB", reinterpret_cast<PROC>(Ext_CreateContextAttribsARB)},
{"wglSwapIntervalEXT", reinterpret_cast<PROC>(Ext_SwapIntervalEXT)},
{"wglGetSwapIntervalEXT", reinterpret_cast<PROC>(Ext_GetSwapIntervalEXT)},
{"wglGetPixelFormatAttribivARB", reinterpret_cast<PROC>(Ext_GetPixelFormatAttribivARB)},
{"wglGetPixelFormatAttribfvARB", reinterpret_cast<PROC>(Ext_GetPixelFormatAttribfvARB)},
{"wglChoosePixelFormatARB", reinterpret_cast<PROC>(Ext_ChoosePixelFormatARB)},
};
} // namespace
int ChoosePixelFormat(HDC hdc, const PIXELFORMATDESCRIPTOR* pfd) {
EnsureInitialized();
MGLOG_D("wglChoosePixelFormat(hdc=%p)", hdc);
// Format 1 (RGBA8 + depth24/stencil8) satisfies every request; a format
// exceeding the asked-for capabilities is a legal ChoosePixelFormat answer.
(void)pfd;
return 1;
}
int DescribePixelFormat(HDC hdc, int format, UINT size, PIXELFORMATDESCRIPTOR* pfd) {
EnsureInitialized();
MGLOG_D("wglDescribePixelFormat(hdc=%p, format=%d)", hdc, format);
if (!pfd) {
return kPixelFormatCount;
}
if (size < sizeof(PIXELFORMATDESCRIPTOR) || format < 1 || format > kPixelFormatCount) {
return 0;
}
const PixelFormatInfo& info = kPixelFormats[format - 1];
std::memset(pfd, 0, sizeof(PIXELFORMATDESCRIPTOR));
pfd->nSize = sizeof(PIXELFORMATDESCRIPTOR);
pfd->nVersion = 1;
pfd->dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER | PFD_SWAP_EXCHANGE
#if defined(PFD_SUPPORT_COMPOSITION)
| PFD_SUPPORT_COMPOSITION
#endif
;
pfd->iPixelType = PFD_TYPE_RGBA;
pfd->cColorBits = 32;
pfd->cRedBits = 8;
pfd->cRedShift = 16;
pfd->cGreenBits = 8;
pfd->cGreenShift = 8;
pfd->cBlueBits = 8;
pfd->cBlueShift = 0;
pfd->cAlphaBits = static_cast<BYTE>(info.AlphaBits);
pfd->cAlphaShift = 24;
pfd->cDepthBits = static_cast<BYTE>(info.DepthBits);
pfd->cStencilBits = static_cast<BYTE>(info.StencilBits);
pfd->iLayerType = PFD_MAIN_PLANE;
return kPixelFormatCount;
}
int GetPixelFormat(HDC hdc) {
EnsureInitialized();
HWND hwnd = WindowFromDC(hdc);
if (!hwnd) {
return 0;
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto& formats = WindowPixelFormats();
auto it = formats.find(hwnd);
return it == formats.end() ? 0 : it->second;
}
BOOL SetPixelFormat(HDC hdc, int format, const PIXELFORMATDESCRIPTOR*) {
EnsureInitialized();
MGLOG_D("wglSetPixelFormat(hdc=%p, format=%d)", hdc, format);
if (format < 1 || format > kPixelFormatCount) {
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}
HWND hwnd = WindowFromDC(hdc);
if (!hwnd) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
WindowPixelFormats()[hwnd] = format;
return TRUE;
}
BOOL SwapBuffers(HDC hdc) {
HWND hwnd = WindowFromDC(hdc);
if (!hwnd) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto& surfaces = WindowSurfaces();
auto it = surfaces.find(hwnd);
if (it == surfaces.end()) {
MGLOG_W("wglSwapBuffers: no surface for HWND %p", hwnd);
return FALSE;
}
SyncSurfaceSize(hwnd, it->second);
return EGLImpl::SwapBuffers(it->second.Display, it->second.Surface) == EGL_TRUE ? TRUE : FALSE;
}
HGLRC CreateContext(HDC hdc) {
EnsureInitialized();
MGLOG_I("wglCreateContext(hdc=%p)", hdc);
// A legacy WGL context is a compatibility-profile context; MobileGL keys
// its relaxed-semantics mode off the explicit compatibility bit.
const EGLint attribs[] = {
EGL_CONTEXT_MAJOR_VERSION, 3,
EGL_CONTEXT_MINOR_VERSION, 3,
EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT,
EGL_NONE,
};
return CreateContextFromEGLAttribs(hdc, nullptr, attribs);
}
BOOL DeleteContext(HGLRC hglrc) {
EnsureInitialized();
MGLOG_I("wglDeleteContext(%p)", hglrc);
if (t_current.Context == hglrc) {
MakeCurrent(nullptr, nullptr);
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(hglrc);
if (!object) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
if (object->Context != EGL_NO_CONTEXT) {
EGLImpl::DestroyContext(object->Display, object->Context);
}
Contexts().erase(hglrc);
return TRUE;
}
BOOL MakeCurrent(HDC hdc, HGLRC hglrc) {
EnsureInitialized();
MGLOG_D("wglMakeCurrent(hdc=%p, hglrc=%p)", hdc, hglrc);
if (!hglrc) {
if (!t_current.Context) {
t_current = {};
return TRUE;
}
const EGLBoolean released =
EGLImpl::MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
t_current = {};
return released == EGL_TRUE ? TRUE : FALSE;
}
HWND hwnd = WindowFromDC(hdc);
if (!hwnd) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(hglrc);
if (!object) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
WindowSurface* surface = EnsureWindowSurface(hwnd, *object);
if (!surface) {
return FALSE;
}
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface, object->Context)) {
MGLOG_E("wglMakeCurrent: eglMakeCurrent failed (hdc=%p, hglrc=%p)", hdc, hglrc);
return FALSE;
}
t_current = {hdc, hglrc};
return TRUE;
}
HGLRC GetCurrentContext() {
return t_current.Context;
}
HDC GetCurrentDC() {
return t_current.DC;
}
BOOL ShareLists(HGLRC hglrcShare, HGLRC hglrcDest) {
// All MobileGL contexts alias one global GL object namespace, so every
// pair of contexts already shares.
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
if (!TryGetContext(hglrcShare) || !TryGetContext(hglrcDest)) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
return TRUE;
}
PROC GetProcAddress(const char* name) {
EnsureInitialized();
if (!name) {
return nullptr;
}
if (name[0] == 'w' && name[1] == 'g' && name[2] == 'l') {
for (const auto& entry : kWGLExtensionProcs) {
if (std::strcmp(entry.Name, name) == 0) {
return entry.Proc;
}
}
MGLOG_D("wglGetProcAddress: unknown wgl entry point %s", name);
return nullptr;
}
return reinterpret_cast<PROC>(MG_Impl::GetProcAddress(name));
}
} // namespace MobileGL::MG_Impl::WGLImpl
#endif // _WIN32
+34
View File
@@ -0,0 +1,34 @@
// MobileGL - MobileGL/MG_Impl/WGLImpl/WGLImpl.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#if defined(_WIN32)
namespace MobileGL::MG_Impl::WGLImpl {
// Classic opengl32.dll surface. gdi32's ChoosePixelFormat/SetPixelFormat/
// DescribePixelFormat/GetPixelFormat/SwapBuffers forward into the loaded
// opengl32.dll's wgl* exports, so these back both call paths.
int ChoosePixelFormat(HDC hdc, const PIXELFORMATDESCRIPTOR* pfd);
int DescribePixelFormat(HDC hdc, int format, UINT size, PIXELFORMATDESCRIPTOR* pfd);
int GetPixelFormat(HDC hdc);
BOOL SetPixelFormat(HDC hdc, int format, const PIXELFORMATDESCRIPTOR* pfd);
BOOL SwapBuffers(HDC hdc);
HGLRC CreateContext(HDC hdc);
BOOL DeleteContext(HGLRC hglrc);
BOOL MakeCurrent(HDC hdc, HGLRC hglrc);
HGLRC GetCurrentContext();
HDC GetCurrentDC();
BOOL ShareLists(HGLRC hglrcShare, HGLRC hglrcDest);
PROC GetProcAddress(const char* name);
} // namespace MobileGL::MG_Impl::WGLImpl
#endif // _WIN32
+22 -1
View File
@@ -368,6 +368,26 @@ namespace MobileGL {
return true;
}
Bool EGLContext::HasAnyInitializedDisplay() const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
for (const auto& [handle, displayObject] : m_displays) {
if (displayObject.Initialized) {
return true;
}
}
return false;
}
Bool EGLContext::HasAnyCurrentContext() const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
for (const auto& [threadId, current] : m_threadCurrents) {
if (current.Context != nullptr) {
return true;
}
}
return false;
}
Bool EGLContext::ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs,
EGLint configSize, EGLint* numConfig) {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
@@ -1415,6 +1435,7 @@ namespace MobileGL {
}
} // namespace EGLState
UniquePtr<EGLState::EGLContext> pEGLContext;
// Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<EGLState::EGLContext>& pEGLContext = *new UniquePtr<EGLState::EGLContext>();
} // namespace MG_State
} // namespace MobileGL
+5 -1
View File
@@ -39,6 +39,10 @@ namespace MobileGL {
Bool IsDisplayInitialized(EGLDisplayHandle display) const;
Bool InitializeDisplay(EGLDisplayHandle display, EGLint* major, EGLint* minor);
Bool TerminateDisplay(EGLDisplayHandle display);
// Whole-library idle checks used by EGLImpl::Terminate to decide
// when the last eglTerminate may tear MobileGL down entirely.
Bool HasAnyInitializedDisplay() const;
Bool HasAnyCurrentContext() const;
// Config
Bool ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs,
@@ -262,6 +266,6 @@ namespace MobileGL {
};
} // namespace EGLState
extern UniquePtr<EGLState::EGLContext> pEGLContext;
extern UniquePtr<EGLState::EGLContext>& pEGLContext;
} // namespace MG_State
} // namespace MobileGL
+2 -1
View File
@@ -721,5 +721,6 @@ namespace MobileGL::MG_State {
}
} // namespace GLState
UniquePtr<GLState::GLContext> pGLContext;
// Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<GLState::GLContext>& pGLContext = *new UniquePtr<GLState::GLContext>();
} // namespace MobileGL::MG_State
+1 -1
View File
@@ -252,7 +252,7 @@ namespace MobileGL {
};
} // namespace GLState
extern UniquePtr<GLState::GLContext> pGLContext;
extern UniquePtr<GLState::GLContext>& pGLContext;
// True when relaxed GL semantics apply. Strict core rules are enforced only when the
// current EGL context explicitly requested a core profile (core bit in
@@ -9,7 +9,12 @@
#include "Loader.h"
#include "MG_Util/Types.h"
#include <Config.h>
#if !defined(__WIN32) && !defined(_WIN32)
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN 1
#endif
#include <windows.h>
#else
#include <dlfcn.h>
#endif
@@ -60,7 +65,14 @@ namespace MobileGL::MG_Util::BackendLoader {
#endif
static void* OpenLib(const Vector<String>& names) {
#if !defined(__WIN32) && !defined(_WIN32) && (!defined(__APPLE__) || defined(MOBILEGL_IOS))
#if defined(_WIN32)
for (const auto& name : names) {
if (HMODULE lib = LoadLibraryA(name.c_str())) {
MGLOG_I("Loaded GL backend library: %s", name.c_str());
return reinterpret_cast<void*>(lib);
}
}
#elif !defined(__APPLE__) || defined(MOBILEGL_IOS)
static const String LibPathPrefixes[] = {
#if defined(MOBILEGL_IOS)
"@rpath/", "@executable_path/Frameworks/", "@loader_path/Frameworks/",
@@ -104,7 +116,9 @@ namespace MobileGL::MG_Util::BackendLoader {
}
inline void* ProcAddress(void* lib, const char* name) {
#if !defined(__WIN32) && !defined(_WIN32) && (!defined(__APPLE__) || defined(MOBILEGL_IOS))
#if defined(_WIN32)
return reinterpret_cast<void*>(::GetProcAddress(reinterpret_cast<HMODULE>(lib), name));
#elif !defined(__APPLE__) || defined(MOBILEGL_IOS)
return dlsym(lib, name);
#else
return nullptr;
@@ -520,6 +534,15 @@ namespace MobileGL::MG_Util::BackendLoader {
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS) && defined(__ANDROID__)
void* angleGlesLib = nullptr;
#endif
#if defined(_WIN32)
// ANGLE is the GLES provider on Windows regardless of UseAngle(). Preload
// libGLESv2.dll so libEGL.dll resolves its dependency from the same directory.
if (!OpenLib({"libGLESv2.dll"})) {
MGLOG_E("Failed to open ANGLE libGLESv2.dll");
return;
}
eglLib = OpenLib({"libEGL.dll"});
#else
if (UseAngle()) {
void* glesLib = OpenLib({"libGLESv2_angle.so"});
if (!glesLib) {
@@ -541,6 +564,7 @@ namespace MobileGL::MG_Util::BackendLoader {
eglLib = OpenLib({"libEGL.so"});
#endif
}
#endif // !_WIN32
if (!eglLib) {
MGLOG_E("Failed to open EGL library");
+7 -3
View File
@@ -9,10 +9,14 @@
#pragma once
#include <Includes.h>
// Severity-ordered: a build compiled at level X keeps X and everything MORE
// severe. INFO builds must keep WARN/ERROR/FATAL — the old ordering
// (WARN=1/ERROR=2 below INFO=3) compiled every warning and error out of
// release builds and hid real backend failures.
#define MOBILEGL_LOG_LEVEL_DEBUG 0
#define MOBILEGL_LOG_LEVEL_WARN 1
#define MOBILEGL_LOG_LEVEL_ERROR 2
#define MOBILEGL_LOG_LEVEL_INFO 3
#define MOBILEGL_LOG_LEVEL_INFO 1
#define MOBILEGL_LOG_LEVEL_WARN 2
#define MOBILEGL_LOG_LEVEL_ERROR 3
#define MOBILEGL_LOG_LEVEL_FATAL 4
#define MOBILEGL_LOG_INTERNAL(levelTag, androidLogLevel, fmt, ...) \
@@ -22,6 +22,9 @@
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
#include "SpirvPasses/StripNoPerspectivePass.h"
#include "SpirvPasses/EmulateNoPerspectivePass.h"
#include "SpirvPasses/FoldConstOffsetFor1DFetchPass.h"
#include "SpirvPasses/LowerClipDistanceForEsslPass.h"
#include "SpirvPasses/DefeatConstStructArrayLutPass.h"
#include "spirv-tools/libspirv.h"
#include "spirv-tools/optimizer.hpp"
@@ -323,6 +326,55 @@ namespace MobileGL {
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::ClampAccessChainIndicesForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
OptimizerOptions options;
options.set_run_validator(false);
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(CreateGraphicsRobustAccessPass());
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::FoldConstOffsetFor1DFetchForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
OptimizerOptions options;
options.set_run_validator(false);
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(FoldConstOffsetFor1DFetchPass::CreateFoldConstOffsetFor1DFetchPass());
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::LowerClipDistanceForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
OptimizerOptions options;
options.set_run_validator(false);
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(LowerClipDistanceForEsslPass::CreateLowerClipDistanceForEsslPass());
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::DefeatConstStructArrayLutForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
OptimizerOptions options;
options.set_run_validator(false);
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(
DefeatConstStructArrayLutPass::CreateDefeatConstStructArrayLutPass());
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
@@ -27,6 +27,36 @@ namespace MobileGL {
// Only for backends without native draw-parameter support (DirectGLES).
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Clamps every access-chain index to its declared bounds (spirv-tools
// GraphicsRobustAccessPass). GL 3.3 only promises undefined *values* for
// out-of-bounds indexing, but Adreno's ESSL compiler constant-folds a provably
// out-of-bounds local-array index into poison that corrupts the whole shader's
// output; clamping restores the "some value from the array" contract. Only for
// the DirectGLES transpile path.
static bool ClampAccessChainIndicesForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Folds the ConstOffset image operand of Dim1D OpImageFetch into the integer
// coordinate (texelFetchOffset(t,P,l,o) == texelFetch(t,P+o,l)). SPIRV-Cross
// emulates 1D samplers as 2D for ES: it widens the coordinate to ivec2 but keeps
// the scalar offset, and ESSL has no texelFetchOffset(sampler2D, ivec2, int,
// scalar) overload, so Adreno rejects the shader. Only for the DirectGLES
// transpile path.
static bool FoldConstOffsetFor1DFetchForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Shadows gl_ClipDistance in Private mg_ClipDistance/mg_ClipDistanceIn arrays so
// the decompiled ESSL only writes the builtin with literal constant indices
// (flush before EmitVertex/return) and only reads gl_in clip distances with
// dynamic loop indices (copy loop): the other shapes miscompile or crash
// Adreno's ESSL compiler. Vertex/geometry stages; DirectGLES transpile path on
// Qualcomm only (quirk-gated). See LowerClipDistanceForEsslPass.
static bool LowerClipDistanceForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Splits a Function-storage array-of-structs variable's single constant-composite
// store into per-element stores so SPIRV-Cross does not hoist it into a global
// const struct[] LUT, which Adreno cannot dynamically index. DirectGLES
// transpile path on Qualcomm only (quirk-gated). See DefeatConstStructArrayLutPass.
static bool DefeatConstStructArrayLutForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Drops RelaxedPrecision member decorations from uniform-block structs so
// SPIRV-Cross prints the same (highp) member precision in every stage; ES
// drivers reject cross-stage uniform blocks whose member precisions differ.
@@ -0,0 +1,175 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "DefeatConstStructArrayLutPass.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/opt/type_manager.h"
#include "source/opt/types.h"
#include "source/util/make_unique.h"
#include <vector>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
namespace {
using spvtools::opt::BasicBlock;
using spvtools::opt::Function;
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
using spvtools::opt::Operand;
namespace analysis = spvtools::opt::analysis;
uint32_t PointerTypeTo(IRContext* ctx, uint32_t pointeeId, spv::StorageClass sc) {
analysis::Type* pointee = ctx->get_type_mgr()->GetType(pointeeId);
analysis::Pointer ptr(pointee, sc);
return ctx->get_type_mgr()->GetTypeInstruction(&ptr);
}
uint32_t SignedIntConstant(IRContext* ctx, uint32_t value) {
analysis::Integer i(32, true);
analysis::Type* reg = ctx->get_type_mgr()->GetRegisteredType(&i);
const analysis::Constant* c = ctx->get_constant_mgr()->GetConstant(reg, {value});
return ctx->get_constant_mgr()->GetDefiningInstruction(c)->result_id();
}
// True when |var| (a Function-storage OpVariable) points to an array of structs.
// Reports the struct type id on success.
bool IsArrayOfStructsVariable(IRContext* ctx, Instruction* var, uint32_t& structTypeId) {
auto* defUse = ctx->get_def_use_mgr();
Instruction* ptrType = defUse->GetDef(var->type_id());
if (ptrType == nullptr || ptrType->opcode() != spv::Op::OpTypePointer) return false;
Instruction* pointee = defUse->GetDef(ptrType->GetSingleWordInOperand(1));
if (pointee == nullptr || pointee->opcode() != spv::Op::OpTypeArray) return false;
Instruction* element = defUse->GetDef(pointee->GetSingleWordInOperand(0));
if (element == nullptr || element->opcode() != spv::Op::OpTypeStruct) return false;
structTypeId = element->result_id();
return true;
}
} // namespace
spvtools::opt::Pass::Status DefeatConstStructArrayLutPass::Process() {
auto* ctx = context();
auto* defUse = ctx->get_def_use_mgr();
bool modified = false;
for (Function& function : *get_module()) {
if (function.begin() == function.end()) continue;
BasicBlock* entryBlock = &*function.begin();
// Candidate variables: Function-storage arrays of structs declared in this
// function's entry block (where OpVariables must live).
struct Candidate {
Instruction* var;
uint32_t structTypeId;
};
std::vector<Candidate> candidates;
for (Instruction& inst : *entryBlock) {
if (inst.opcode() != spv::Op::OpVariable) break;
// Variables with initializers keep SPIRV-Cross's initializer path; the
// glslang pattern under attack is initializer-free with one OpStore.
if (inst.NumInOperands() > 1) continue;
uint32_t structTypeId = 0;
if (IsArrayOfStructsVariable(ctx, &inst, structTypeId)) {
candidates.push_back({&inst, structTypeId});
}
}
for (const Candidate& candidate : candidates) {
Instruction* var = candidate.var;
// The variable qualifies only when its single write is one direct
// OpStore of an OpConstantComposite; any other write shape already
// defeats SPIRV-Cross's LUT promotion, so it is left untouched.
Instruction* singleStore = nullptr;
bool disqualified = false;
defUse->ForEachUser(var, [&](Instruction* user) {
if (user->opcode() == spv::Op::OpStore &&
user->GetSingleWordInOperand(0) == var->result_id()) {
if (singleStore != nullptr) {
disqualified = true;
} else {
singleStore = user;
}
} else if (user->opcode() == spv::Op::OpCopyMemory) {
disqualified = true;
} else if (user->opcode() == spv::Op::OpAccessChain ||
user->opcode() == spv::Op::OpInBoundsAccessChain) {
defUse->ForEachUser(user, [&](Instruction* chainUser) {
if (chainUser->opcode() == spv::Op::OpStore ||
chainUser->opcode() == spv::Op::OpCopyMemory) {
disqualified = true;
}
});
}
});
if (disqualified || singleStore == nullptr) continue;
Instruction* composite = defUse->GetDef(singleStore->GetSingleWordInOperand(1));
if (composite == nullptr ||
composite->opcode() != spv::Op::OpConstantComposite) {
continue;
}
// The store must sit in the entry block: that is the only placement
// SPIRV-Cross treats as a LUT initializer.
bool storeInEntryBlock = false;
for (Instruction& inst : *entryBlock) {
if (&inst == singleStore) {
storeInEntryBlock = true;
break;
}
}
if (!storeInEntryBlock) continue;
// Split the composite store into one constant-index store per element.
const uint32_t ptrFnStruct =
PointerTypeTo(ctx, candidate.structTypeId, spv::StorageClass::Function);
for (uint32_t element = 0; element < composite->NumInOperands(); ++element) {
const uint32_t elementConstId = composite->GetSingleWordInOperand(element);
const uint32_t chainId = ctx->TakeNextId();
Instruction* chain =
singleStore->InsertBefore(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpAccessChain, ptrFnStruct, chainId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {var->result_id()}},
{SPV_OPERAND_TYPE_ID, {SignedIntConstant(ctx, element)}}}));
ctx->AnalyzeDefUse(chain);
Instruction* store =
singleStore->InsertBefore(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpStore, 0, 0,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {chainId}},
{SPV_OPERAND_TYPE_ID, {elementConstId}}}));
ctx->AnalyzeDefUse(store);
}
ctx->KillInst(singleStore);
modified = true;
}
}
if (!modified) {
return Status::SuccessWithoutChange;
}
ctx->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken
DefeatConstStructArrayLutPass::CreateDefeatConstStructArrayLutPass() {
return spvtools::Optimizer::PassToken(MakeUnique<DefeatConstStructArrayLutPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,36 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include "source/opt/pass.h"
#include "spirv-tools/optimizer.hpp"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// SPIRV-Cross hoists a Function-storage array variable whose only write is a single
// constant-composite store into a global `const struct[]` LUT (variable_is_lut).
// Adreno's ESSL compiler cannot dynamically index such a global const struct array
// ("Cannot offset into the structure" - device-verified on Adreno 750). Splitting
// the one composite store into per-element constant-index stores makes
// variable_is_lut fail, so SPIRV-Cross keeps the array as an ordinary local that
// Adreno indexes fine. Scalar/vector const arrays are unaffected on Adreno and are
// left alone - only arrays OF STRUCTS are rewritten. Only meant for the DirectGLES
// transpile path on Qualcomm devices.
class DefeatConstStructArrayLutPass : public spvtools::opt::Pass {
public:
const char* name() const override { return "defeat-const-struct-array-lut"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateDefeatConstStructArrayLutPass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,131 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "FoldConstOffsetFor1DFetchPass.h"
#include "spirv.hpp"
#include "source/opt/def_use_manager.h"
#include "source/opt/instruction.h"
#include "source/opt/ir_builder.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::InstructionBuilder;
using spvtools::opt::IRContext;
using spvtools::opt::Operand;
// Number of ImageOperands ids that precede the ConstOffset id: one per
// lower-order bit set in the mask, except Grad which carries two ids.
uint32_t CountIdsBeforeConstOffset(uint32_t mask) {
uint32_t count = 0;
if (mask & static_cast<uint32_t>(spv::ImageOperandsMask::Bias)) count += 1;
if (mask & static_cast<uint32_t>(spv::ImageOperandsMask::Lod)) count += 1;
if (mask & static_cast<uint32_t>(spv::ImageOperandsMask::Grad)) count += 2;
return count;
}
} // namespace
spvtools::opt::Pass::Status FoldConstOffsetFor1DFetchPass::Process() {
auto* irContext = context();
auto* defUseMgr = irContext->get_def_use_mgr();
Bool modified = false;
constexpr uint32_t kConstOffsetBit =
static_cast<uint32_t>(spv::ImageOperandsMask::ConstOffset);
for (auto& function : *get_module()) {
for (auto& block : function) {
for (auto& inst : block) {
if (inst.opcode() != spv::Op::OpImageFetch) continue;
// In-operands: image, coordinate, [ImageOperands mask, ids...].
if (inst.NumInOperands() < 3) continue;
const uint32_t operandsMask = inst.GetSingleWordInOperand(2);
if ((operandsMask & kConstOffsetBit) == 0) continue;
Instruction* imageInst = defUseMgr->GetDef(inst.GetSingleWordInOperand(0));
if (imageInst == nullptr) continue;
Instruction* imageType = defUseMgr->GetDef(imageInst->type_id());
if (imageType == nullptr || imageType->opcode() != spv::Op::OpTypeImage ||
static_cast<spv::Dim>(imageType->GetSingleWordInOperand(1)) != spv::Dim::Dim1D) {
continue;
}
const uint32_t offsetOperandIndex = 3 + CountIdsBeforeConstOffset(operandsMask);
const uint32_t offsetId = inst.GetSingleWordInOperand(offsetOperandIndex);
const uint32_t coordId = inst.GetSingleWordInOperand(1);
Instruction* coordType = defUseMgr->GetDef(defUseMgr->GetDef(coordId)->type_id());
InstructionBuilder builder(
irContext, &inst,
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
uint32_t newCoordId = 0;
if (coordType->opcode() == spv::Op::OpTypeVector) {
// Arrayed 1D fetch: component 0 is the texel coordinate,
// component 1 the layer - only component 0 takes the offset.
const uint32_t componentTypeId = coordType->GetSingleWordInOperand(0);
Instruction* extracted = builder.AddCompositeExtract(componentTypeId, coordId, {0});
Instruction* sum =
builder.AddIAdd(componentTypeId, extracted->result_id(), offsetId);
Instruction* inserted = builder.AddInstruction(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpCompositeInsert, coordType->result_id(),
irContext->TakeNextId(),
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {sum->result_id()}},
{SPV_OPERAND_TYPE_ID, {coordId}},
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {0}}}));
newCoordId = inserted->result_id();
} else {
Instruction* sum = builder.AddIAdd(coordType->result_id(), coordId, offsetId);
newCoordId = sum->result_id();
}
const uint32_t newMask = operandsMask & ~kConstOffsetBit;
// 3 fixed operands + the offset id: anything beyond that is another
// image-operand id that must keep the mask word alive.
const Bool otherOperandIdsRemain = inst.NumInOperands() > 4;
irContext->ForgetUses(&inst);
std::vector<Operand> newOperands;
newOperands.push_back(inst.GetInOperand(0));
newOperands.push_back({SPV_OPERAND_TYPE_ID, {newCoordId}});
if (newMask != 0 || otherOperandIdsRemain) {
Operand maskOperand = inst.GetInOperand(2);
maskOperand.words[0] = newMask;
newOperands.push_back(maskOperand);
for (uint32_t i = 3; i < inst.NumInOperands(); ++i) {
if (i == offsetOperandIndex) continue;
newOperands.push_back(inst.GetInOperand(i));
}
}
inst.SetInOperands(std::move(newOperands));
irContext->AnalyzeUses(&inst);
modified = true;
}
}
}
return modified ? Status::SuccessWithChange : Status::SuccessWithoutChange;
}
spvtools::Optimizer::PassToken FoldConstOffsetFor1DFetchPass::CreateFoldConstOffsetFor1DFetchPass() {
return spvtools::Optimizer::PassToken(MakeUnique<FoldConstOffsetFor1DFetchPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,36 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include "source/opt/pass.h"
#include "spirv-tools/optimizer.hpp"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// SPIRV-Cross emulates 1D textures as 2D for ES targets: it widens the texelFetch
// coordinate to ivec2 but keeps the ConstOffset image operand scalar, and ESSL has
// no texelFetchOffset(sampler2D, ivec2, int, scalar-offset) overload, so drivers
// (Adreno) reject the transpiled shader. This pass folds the constant offset into
// the integer coordinate before the fetch - texelFetchOffset(t, P, l, o) ==
// texelFetch(t, P + o, l) per the GLSL spec - and drops the ConstOffset operand,
// so SPIRV-Cross emits a plain texelFetch. For arrayed 1D fetches only coordinate
// component 0 is offset (component 1 is the layer). Only meant for the DirectGLES
// transpile path.
class FoldConstOffsetFor1DFetchPass : public spvtools::opt::Pass {
public:
const char* name() const override { return "fold-const-offset-for-1d-fetch"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateFoldConstOffsetFor1DFetchPass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,613 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "LowerClipDistanceForEsslPass.h"
#include "spirv.hpp"
#include "source/opt/basic_block.h"
#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/opt/type_manager.h"
#include "source/opt/types.h"
#include "source/util/make_unique.h"
#include <memory>
#include <vector>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
namespace {
using spvtools::opt::BasicBlock;
using spvtools::opt::Function;
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
using spvtools::opt::Operand;
namespace analysis = spvtools::opt::analysis;
spv::ExecutionModel EntryExecutionModel(IRContext* ctx) {
for (Instruction& ep : ctx->module()->entry_points()) {
return static_cast<spv::ExecutionModel>(ep.GetSingleWordInOperand(0));
}
return spv::ExecutionModel::Max;
}
uint32_t EntryFunctionId(IRContext* ctx) {
for (Instruction& ep : ctx->module()->entry_points()) {
// OpEntryPoint <model> <function> "name" <interface...>
return ep.GetSingleWordInOperand(1);
}
return 0;
}
uint32_t VariablePointeeType(IRContext* ctx, Instruction* var) {
Instruction* ptrType = ctx->get_def_use_mgr()->GetDef(var->type_id());
// OpTypePointer <storage-class> <pointee>
return ptrType->GetSingleWordInOperand(1);
}
uint32_t PointerTypeTo(IRContext* ctx, uint32_t pointeeId, spv::StorageClass sc) {
analysis::Type* pointee = ctx->get_type_mgr()->GetType(pointeeId);
analysis::Pointer ptr(pointee, sc);
return ctx->get_type_mgr()->GetTypeInstruction(&ptr);
}
uint32_t IntConstant(IRContext* ctx, bool isSigned, uint32_t value) {
analysis::Integer i(32, isSigned);
analysis::Type* reg = ctx->get_type_mgr()->GetRegisteredType(&i);
const analysis::Constant* c = ctx->get_constant_mgr()->GetConstant(reg, {value});
return ctx->get_constant_mgr()->GetDefiningInstruction(c)->result_id();
}
uint32_t UintType(IRContext* ctx) {
analysis::Integer i(32, false);
return ctx->get_type_mgr()->GetTypeInstruction(&i);
}
uint32_t BoolType(IRContext* ctx) {
analysis::Bool b;
return ctx->get_type_mgr()->GetTypeInstruction(&b);
}
// Constant length of OpTypeArray |arrayTypeId| (0 when not a sized constant).
uint32_t ArrayLength(IRContext* ctx, uint32_t arrayTypeId) {
Instruction* arrayType = ctx->get_def_use_mgr()->GetDef(arrayTypeId);
if (arrayType == nullptr || arrayType->opcode() != spv::Op::OpTypeArray) {
return 0;
}
Instruction* length = ctx->get_def_use_mgr()->GetDef(arrayType->GetSingleWordInOperand(1));
if (length == nullptr || length->opcode() != spv::Op::OpConstant) {
return 0;
}
return length->GetSingleWordInOperand(0);
}
bool IsConstantWithValue(IRContext* ctx, uint32_t id, uint32_t value) {
Instruction* def = ctx->get_def_use_mgr()->GetDef(id);
return def != nullptr && def->opcode() == spv::Op::OpConstant &&
def->GetSingleWordInOperand(0) == value;
}
bool IsAccessChain(const Instruction* inst) {
return inst->opcode() == spv::Op::OpAccessChain ||
inst->opcode() == spv::Op::OpInBoundsAccessChain;
}
Instruction* AddPrivateVariable(IRContext* ctx, uint32_t pointeeTypeId, const char* name) {
const uint32_t ptrType = PointerTypeTo(ctx, pointeeTypeId, spv::StorageClass::Private);
const uint32_t varId = ctx->TakeNextId();
ctx->AddGlobalValue(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpVariable, ptrType, varId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_STORAGE_CLASS,
{static_cast<uint32_t>(spv::StorageClass::Private)}}}));
ctx->AddDebug2Inst(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpName, 0, 0,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {varId}},
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(name)}}));
return ctx->get_def_use_mgr()->GetDef(varId);
}
// Retargets |chain| onto |newBaseId|, dropping the first |dropIndexCount| index
// operands and switching the result pointer's storage class to Private.
void RetargetChainToPrivate(IRContext* ctx, Instruction* chain, uint32_t newBaseId,
uint32_t dropIndexCount) {
Instruction* chainPtrType = ctx->get_def_use_mgr()->GetDef(chain->type_id());
const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1);
const uint32_t newPtrType = PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private);
ctx->ForgetUses(chain);
std::vector<Operand> newOperands;
newOperands.push_back({SPV_OPERAND_TYPE_ID, {newBaseId}});
for (uint32_t i = 1 + dropIndexCount; i < chain->NumInOperands(); ++i) {
newOperands.push_back(chain->GetInOperand(i));
}
chain->SetResultType(newPtrType);
chain->SetInOperands(std::move(newOperands));
ctx->AnalyzeUses(chain);
}
// ---- Output side --------------------------------------------------------------
struct OutputTarget {
Instruction* var = nullptr; // Output gl_PerVertex block or standalone builtin
bool isBlockMember = false;
uint32_t memberIndex = 0; // valid when isBlockMember
uint32_t arrayTypeId = 0; // float[N]
uint32_t elemTypeId = 0; // float
uint32_t arrayLen = 0; // N
};
// Inserts "gl_ClipDistance[k] = mg_ClipDistance[k]" for every literal k before
// |before|. Constant-index writes are the only write shape Adreno links correctly.
void InsertFlushBefore(IRContext* ctx, Instruction* before, const OutputTarget& target,
uint32_t mgVarId) {
const uint32_t ptrPrivElem =
PointerTypeTo(ctx, target.elemTypeId, spv::StorageClass::Private);
const uint32_t ptrOutElem =
PointerTypeTo(ctx, target.elemTypeId, spv::StorageClass::Output);
for (uint32_t k = 0; k < target.arrayLen; ++k) {
const uint32_t kConst = IntConstant(ctx, true, k);
const uint32_t srcChainId = ctx->TakeNextId();
before->InsertBefore(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpAccessChain, ptrPrivElem, srcChainId,
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {mgVarId}},
{SPV_OPERAND_TYPE_ID, {kConst}}}));
const uint32_t valId = ctx->TakeNextId();
before->InsertBefore(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpLoad, target.elemTypeId, valId,
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {srcChainId}}}));
const uint32_t dstChainId = ctx->TakeNextId();
std::vector<Operand> dstOperands;
dstOperands.push_back({SPV_OPERAND_TYPE_ID, {target.var->result_id()}});
if (target.isBlockMember) {
dstOperands.push_back(
{SPV_OPERAND_TYPE_ID, {IntConstant(ctx, true, target.memberIndex)}});
}
dstOperands.push_back({SPV_OPERAND_TYPE_ID, {kConst}});
before->InsertBefore(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpAccessChain, ptrOutElem, dstChainId, dstOperands));
before->InsertBefore(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpStore, 0, 0,
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {dstChainId}},
{SPV_OPERAND_TYPE_ID, {valId}}}));
}
}
bool LowerOutputClipDistance(IRContext* ctx, bool isGeometry) {
auto* defUse = ctx->get_def_use_mgr();
// Collect (struct type, member) pairs decorated BuiltIn ClipDistance and
// standalone variables decorated BuiltIn ClipDistance.
std::vector<std::pair<uint32_t, uint32_t>> memberTargets; // (structId, member)
std::vector<uint32_t> plainTargets; // variable ids
for (Instruction& ann : ctx->annotations()) {
if (ann.opcode() == spv::Op::OpMemberDecorate && ann.NumInOperands() >= 4 &&
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) ==
spv::Decoration::BuiltIn &&
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(3)) ==
spv::BuiltIn::ClipDistance) {
memberTargets.emplace_back(ann.GetSingleWordInOperand(0),
ann.GetSingleWordInOperand(1));
} else if (ann.opcode() == spv::Op::OpDecorate && ann.NumInOperands() >= 3 &&
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) ==
spv::Decoration::BuiltIn &&
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(2)) ==
spv::BuiltIn::ClipDistance) {
plainTargets.push_back(ann.GetSingleWordInOperand(0));
}
}
std::vector<OutputTarget> targets;
for (Instruction& inst : ctx->module()->types_values()) {
if (inst.opcode() != spv::Op::OpVariable ||
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) !=
spv::StorageClass::Output) {
continue;
}
const uint32_t pointee = VariablePointeeType(ctx, &inst);
for (const auto& [structId, member] : memberTargets) {
if (pointee != structId) continue;
Instruction* structType = defUse->GetDef(structId);
if (structType == nullptr || member >= structType->NumInOperands()) continue;
OutputTarget target;
target.var = &inst;
target.isBlockMember = true;
target.memberIndex = member;
target.arrayTypeId = structType->GetSingleWordInOperand(member);
target.arrayLen = ArrayLength(ctx, target.arrayTypeId);
targets.push_back(target);
}
for (const uint32_t varId : plainTargets) {
if (inst.result_id() != varId) continue;
OutputTarget target;
target.var = &inst;
target.isBlockMember = false;
target.arrayTypeId = pointee;
target.arrayLen = ArrayLength(ctx, target.arrayTypeId);
targets.push_back(target);
}
}
bool changed = false;
for (OutputTarget& target : targets) {
if (target.arrayLen == 0) continue;
Instruction* arrayType = defUse->GetDef(target.arrayTypeId);
target.elemTypeId = arrayType->GetSingleWordInOperand(0);
// Collect the accesses to redirect. For the block form only chains whose
// leading index selects the ClipDistance member count; for the standalone
// form every chain plus whole-variable loads/stores.
std::vector<Instruction*> chains;
std::vector<Instruction*> directAccesses;
bool unsupportedUse = false;
defUse->ForEachUser(target.var, [&](Instruction* user) {
if (IsAccessChain(user) &&
user->GetSingleWordInOperand(0) == target.var->result_id()) {
if (target.isBlockMember) {
if (user->NumInOperands() >= 2 &&
IsConstantWithValue(ctx, user->GetSingleWordInOperand(1),
target.memberIndex)) {
chains.push_back(user);
}
} else {
chains.push_back(user);
}
} else if (!target.isBlockMember) {
if (user->opcode() == spv::Op::OpLoad ||
(user->opcode() == spv::Op::OpStore &&
user->GetSingleWordInOperand(0) == target.var->result_id())) {
directAccesses.push_back(user);
} else if (user->opcode() == spv::Op::OpCopyMemory) {
unsupportedUse = true;
}
}
});
if (unsupportedUse || (chains.empty() && directAccesses.empty())) {
continue;
}
Instruction* mgVar = AddPrivateVariable(ctx, target.arrayTypeId, "mg_ClipDistance");
const uint32_t mgVarId = mgVar->result_id();
for (Instruction* chain : chains) {
const uint32_t dropCount = target.isBlockMember ? 1u : 0u;
if (chain->NumInOperands() == 1 + dropCount) {
// Pointer to the whole float[N]: reuse the private variable itself.
ctx->ReplaceAllUsesWith(chain->result_id(), mgVarId);
ctx->KillInst(chain);
} else {
RetargetChainToPrivate(ctx, chain, mgVarId, dropCount);
}
}
for (Instruction* access : directAccesses) {
ctx->ForgetUses(access);
access->SetInOperand(0, {mgVarId});
ctx->AnalyzeUses(access);
}
// Flush the shadow into the real builtin: geometry right before every
// EmitVertex, vertex before every return of the entry point. The flush is
// also what keeps the builtin statically used for cross-stage IO matching.
std::vector<Instruction*> flushSites;
if (isGeometry) {
for (Function& function : *ctx->module()) {
function.ForEachInst([&](Instruction* inst) {
if (inst->opcode() == spv::Op::OpEmitVertex) {
flushSites.push_back(inst);
}
});
}
} else {
const uint32_t entryFuncId = EntryFunctionId(ctx);
for (Function& function : *ctx->module()) {
if (function.result_id() != entryFuncId) continue;
function.ForEachInst([&](Instruction* inst) {
if (inst->opcode() == spv::Op::OpReturn ||
inst->opcode() == spv::Op::OpReturnValue) {
flushSites.push_back(inst);
}
});
}
}
for (Instruction* site : flushSites) {
InsertFlushBefore(ctx, site, target, mgVarId);
}
changed = true;
}
return changed;
}
// ---- Input side (geometry gl_in) ----------------------------------------------
bool LowerInputClipDistance(IRContext* ctx) {
auto* defUse = ctx->get_def_use_mgr();
auto* typeMgr = ctx->get_type_mgr();
// Locate the gl_in block member decorated ClipDistance.
Instruction* glInVar = nullptr;
uint32_t memberIndex = 0;
uint32_t arrayTypeId = 0; // float[N]
for (Instruction& ann : ctx->annotations()) {
if (ann.opcode() != spv::Op::OpMemberDecorate || ann.NumInOperands() < 4 ||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) !=
spv::Decoration::BuiltIn ||
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(3)) !=
spv::BuiltIn::ClipDistance) {
continue;
}
const uint32_t structId = ann.GetSingleWordInOperand(0);
const uint32_t member = ann.GetSingleWordInOperand(1);
for (Instruction& inst : ctx->module()->types_values()) {
if (inst.opcode() != spv::Op::OpVariable ||
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) !=
spv::StorageClass::Input) {
continue;
}
const uint32_t pointee = VariablePointeeType(ctx, &inst);
Instruction* pointeeType = defUse->GetDef(pointee);
if (pointeeType == nullptr || pointeeType->opcode() != spv::Op::OpTypeArray ||
pointeeType->GetSingleWordInOperand(0) != structId) {
continue;
}
Instruction* structType = defUse->GetDef(structId);
if (structType == nullptr || member >= structType->NumInOperands()) continue;
glInVar = &inst;
memberIndex = member;
arrayTypeId = structType->GetSingleWordInOperand(member);
break;
}
if (glInVar != nullptr) break;
}
if (glInVar == nullptr) {
return false;
}
const uint32_t clipCount = ArrayLength(ctx, arrayTypeId);
const uint32_t vertexCount = ArrayLength(ctx, VariablePointeeType(ctx, glInVar));
if (clipCount == 0 || vertexCount == 0) {
return false;
}
// Every gl_in chain that selects the ClipDistance member:
// (vertex, member) yields a whole float[N], (vertex, member, k) an element.
std::vector<Instruction*> chains;
defUse->ForEachUser(glInVar, [&](Instruction* user) {
if (IsAccessChain(user) && user->GetSingleWordInOperand(0) == glInVar->result_id() &&
user->NumInOperands() >= 3 &&
IsConstantWithValue(ctx, user->GetSingleWordInOperand(2), memberIndex)) {
chains.push_back(user);
}
});
if (chains.empty()) {
return false;
}
Instruction* arrayTypeInst = defUse->GetDef(arrayTypeId);
const uint32_t elemTypeId = arrayTypeInst->GetSingleWordInOperand(0);
// Private mg_ClipDistanceIn = float[vertexCount][clipCount].
const uint32_t vertexCountConst = IntConstant(ctx, false, vertexCount);
analysis::Type* innerType = typeMgr->GetType(arrayTypeId);
analysis::Array outerArray(
innerType, analysis::Array::LengthInfo{
vertexCountConst,
{analysis::Array::LengthInfo::kConstant, vertexCount}});
const uint32_t outerArrayTypeId = typeMgr->GetTypeInstruction(&outerArray);
Instruction* mgInVar = AddPrivateVariable(ctx, outerArrayTypeId, "mg_ClipDistanceIn");
const uint32_t mgInVarId = mgInVar->result_id();
// Copy loop at the top of the entry point:
// for (uint t = 0; t < vertexCount * clipCount; ++t)
// mg_ClipDistanceIn[t / clipCount][t % clipCount] =
// gl_in[t / clipCount].gl_ClipDistance[t % clipCount];
// Both gl_in indices are loop-derived (dynamic): constant-index element reads
// miscompile and whole-array reads crash the Adreno compiler.
const uint32_t entryFuncId = EntryFunctionId(ctx);
Function* entryFn = nullptr;
for (Function& function : *ctx->module()) {
if (function.result_id() == entryFuncId) {
entryFn = &function;
break;
}
}
if (entryFn == nullptr || entryFn->begin() == entryFn->end()) {
return false;
}
const uint32_t uintTypeId = UintType(ctx);
const uint32_t boolTypeId = BoolType(ctx);
const uint32_t ptrFnUint = PointerTypeTo(ctx, uintTypeId, spv::StorageClass::Function);
const uint32_t ptrInElem = PointerTypeTo(ctx, elemTypeId, spv::StorageClass::Input);
const uint32_t ptrPrivElem = PointerTypeTo(ctx, elemTypeId, spv::StorageClass::Private);
const uint32_t uint0 = IntConstant(ctx, false, 0);
const uint32_t uint1 = IntConstant(ctx, false, 1);
const uint32_t uintN = IntConstant(ctx, false, clipCount);
const uint32_t uintTotal = IntConstant(ctx, false, vertexCount * clipCount);
const uint32_t memberConst = IntConstant(ctx, true, memberIndex);
BasicBlock* entryBlock = &*entryFn->begin();
auto splitPoint = entryBlock->begin();
while (splitPoint != entryBlock->end() &&
splitPoint->opcode() == spv::Op::OpVariable) {
++splitPoint;
}
// Loop counter lives with the other function-local variables.
const uint32_t counterVarId = ctx->TakeNextId();
splitPoint->InsertBefore(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpVariable, ptrFnUint, counterVarId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_STORAGE_CLASS,
{static_cast<uint32_t>(spv::StorageClass::Function)}}}));
const uint32_t restLabelId = ctx->TakeNextId();
BasicBlock* restBlock = entryBlock->SplitBasicBlock(ctx, restLabelId, splitPoint);
const uint32_t headerLabelId = ctx->TakeNextId();
const uint32_t checkLabelId = ctx->TakeNextId();
const uint32_t bodyLabelId = ctx->TakeNextId();
const uint32_t continueLabelId = ctx->TakeNextId();
auto makeBlock = [&](uint32_t labelId) {
return spvtools::MakeUnique<BasicBlock>(spvtools::MakeUnique<Instruction>(
ctx, spv::Op::OpLabel, 0, labelId, std::initializer_list<Operand>{}));
};
auto addInst = [&](BasicBlock* block, spv::Op opcode, uint32_t typeId,
uint32_t resultId, std::vector<Operand> operands) {
block->AddInstruction(spvtools::MakeUnique<Instruction>(
ctx, opcode, typeId, resultId, std::move(operands)));
};
// entry: t = 0; branch header
addInst(entryBlock, spv::Op::OpStore, 0, 0,
{{SPV_OPERAND_TYPE_ID, {counterVarId}}, {SPV_OPERAND_TYPE_ID, {uint0}}});
addInst(entryBlock, spv::Op::OpBranch, 0, 0, {{SPV_OPERAND_TYPE_ID, {headerLabelId}}});
// header: structured loop header
auto headerBlock = makeBlock(headerLabelId);
addInst(headerBlock.get(), spv::Op::OpLoopMerge, 0, 0,
{{SPV_OPERAND_TYPE_ID, {restLabelId}},
{SPV_OPERAND_TYPE_ID, {continueLabelId}},
{SPV_OPERAND_TYPE_LOOP_CONTROL,
{static_cast<uint32_t>(spv::LoopControlMask::MaskNone)}}});
addInst(headerBlock.get(), spv::Op::OpBranch, 0, 0,
{{SPV_OPERAND_TYPE_ID, {checkLabelId}}});
// check: t < vertexCount * clipCount ?
auto checkBlock = makeBlock(checkLabelId);
const uint32_t tCheckId = ctx->TakeNextId();
addInst(checkBlock.get(), spv::Op::OpLoad, uintTypeId, tCheckId,
{{SPV_OPERAND_TYPE_ID, {counterVarId}}});
const uint32_t condId = ctx->TakeNextId();
addInst(checkBlock.get(), spv::Op::OpULessThan, boolTypeId, condId,
{{SPV_OPERAND_TYPE_ID, {tCheckId}}, {SPV_OPERAND_TYPE_ID, {uintTotal}}});
addInst(checkBlock.get(), spv::Op::OpBranchConditional, 0, 0,
{{SPV_OPERAND_TYPE_ID, {condId}},
{SPV_OPERAND_TYPE_ID, {bodyLabelId}},
{SPV_OPERAND_TYPE_ID, {restLabelId}}});
// body: mg_ClipDistanceIn[t / N][t % N] = gl_in[t / N].gl_ClipDistance[t % N]
auto bodyBlock = makeBlock(bodyLabelId);
const uint32_t tBodyId = ctx->TakeNextId();
addInst(bodyBlock.get(), spv::Op::OpLoad, uintTypeId, tBodyId,
{{SPV_OPERAND_TYPE_ID, {counterVarId}}});
const uint32_t vertexIdxId = ctx->TakeNextId();
addInst(bodyBlock.get(), spv::Op::OpUDiv, uintTypeId, vertexIdxId,
{{SPV_OPERAND_TYPE_ID, {tBodyId}}, {SPV_OPERAND_TYPE_ID, {uintN}}});
const uint32_t clipIdxId = ctx->TakeNextId();
addInst(bodyBlock.get(), spv::Op::OpUMod, uintTypeId, clipIdxId,
{{SPV_OPERAND_TYPE_ID, {tBodyId}}, {SPV_OPERAND_TYPE_ID, {uintN}}});
const uint32_t srcChainId = ctx->TakeNextId();
addInst(bodyBlock.get(), spv::Op::OpAccessChain, ptrInElem, srcChainId,
{{SPV_OPERAND_TYPE_ID, {glInVar->result_id()}},
{SPV_OPERAND_TYPE_ID, {vertexIdxId}},
{SPV_OPERAND_TYPE_ID, {memberConst}},
{SPV_OPERAND_TYPE_ID, {clipIdxId}}});
const uint32_t valId = ctx->TakeNextId();
addInst(bodyBlock.get(), spv::Op::OpLoad, elemTypeId, valId,
{{SPV_OPERAND_TYPE_ID, {srcChainId}}});
const uint32_t dstChainId = ctx->TakeNextId();
addInst(bodyBlock.get(), spv::Op::OpAccessChain, ptrPrivElem, dstChainId,
{{SPV_OPERAND_TYPE_ID, {mgInVarId}},
{SPV_OPERAND_TYPE_ID, {vertexIdxId}},
{SPV_OPERAND_TYPE_ID, {clipIdxId}}});
addInst(bodyBlock.get(), spv::Op::OpStore, 0, 0,
{{SPV_OPERAND_TYPE_ID, {dstChainId}}, {SPV_OPERAND_TYPE_ID, {valId}}});
addInst(bodyBlock.get(), spv::Op::OpBranch, 0, 0,
{{SPV_OPERAND_TYPE_ID, {continueLabelId}}});
// continue: ++t
auto continueBlock = makeBlock(continueLabelId);
const uint32_t tContinueId = ctx->TakeNextId();
addInst(continueBlock.get(), spv::Op::OpLoad, uintTypeId, tContinueId,
{{SPV_OPERAND_TYPE_ID, {counterVarId}}});
const uint32_t tIncId = ctx->TakeNextId();
addInst(continueBlock.get(), spv::Op::OpIAdd, uintTypeId, tIncId,
{{SPV_OPERAND_TYPE_ID, {tContinueId}}, {SPV_OPERAND_TYPE_ID, {uint1}}});
addInst(continueBlock.get(), spv::Op::OpStore, 0, 0,
{{SPV_OPERAND_TYPE_ID, {counterVarId}}, {SPV_OPERAND_TYPE_ID, {tIncId}}});
addInst(continueBlock.get(), spv::Op::OpBranch, 0, 0,
{{SPV_OPERAND_TYPE_ID, {headerLabelId}}});
BasicBlock* headerPtr = entryFn->InsertBasicBlockBefore(std::move(headerBlock), restBlock);
BasicBlock* checkPtr = entryFn->InsertBasicBlockAfter(std::move(checkBlock), headerPtr);
BasicBlock* bodyPtr = entryFn->InsertBasicBlockAfter(std::move(bodyBlock), checkPtr);
entryFn->InsertBasicBlockAfter(std::move(continueBlock), bodyPtr);
// Redirect the pre-existing accesses to the shadow copy.
for (Instruction* chain : chains) {
if (chain->NumInOperands() == 3) {
// (vertex, member): whole float[N] of one vertex.
Instruction* chainPtrType = defUse->GetDef(chain->type_id());
const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1);
const uint32_t newPtrType =
PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private);
ctx->ForgetUses(chain);
std::vector<Operand> newOperands;
newOperands.push_back({SPV_OPERAND_TYPE_ID, {mgInVarId}});
newOperands.push_back(chain->GetInOperand(1));
chain->SetResultType(newPtrType);
chain->SetInOperands(std::move(newOperands));
ctx->AnalyzeUses(chain);
} else {
// (vertex, member, k, ...): drop the member index.
Instruction* chainPtrType = defUse->GetDef(chain->type_id());
const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1);
const uint32_t newPtrType =
PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private);
ctx->ForgetUses(chain);
std::vector<Operand> newOperands;
newOperands.push_back({SPV_OPERAND_TYPE_ID, {mgInVarId}});
newOperands.push_back(chain->GetInOperand(1));
for (uint32_t i = 3; i < chain->NumInOperands(); ++i) {
newOperands.push_back(chain->GetInOperand(i));
}
chain->SetResultType(newPtrType);
chain->SetInOperands(std::move(newOperands));
ctx->AnalyzeUses(chain);
}
}
return true;
}
} // namespace
spvtools::opt::Pass::Status LowerClipDistanceForEsslPass::Process() {
auto* ctx = context();
const spv::ExecutionModel model = EntryExecutionModel(ctx);
const bool isVertex = model == spv::ExecutionModel::Vertex;
const bool isGeometry = model == spv::ExecutionModel::Geometry;
if (!isVertex && !isGeometry) {
return Status::SuccessWithoutChange;
}
bool changed = LowerOutputClipDistance(ctx, isGeometry);
if (isGeometry) {
changed |= LowerInputClipDistance(ctx);
}
if (!changed) {
return Status::SuccessWithoutChange;
}
ctx->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken
LowerClipDistanceForEsslPass::CreateLowerClipDistanceForEsslPass() {
return spvtools::Optimizer::PassToken(MakeUnique<LowerClipDistanceForEsslPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,44 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include "source/opt/pass.h"
#include "spirv-tools/optimizer.hpp"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// Adreno's ESSL compiler mishandles gl_ClipDistance (device-verified on Adreno 750):
// - writes through non-constant indices silently fail to link,
// - reads of gl_in[i].gl_ClipDistance[k] with a CONSTANT k >= 1 fail to compile
// ("array indexing out of boundary") while dynamic-index reads work,
// - compiling a whole-array read of gl_in[i].gl_ClipDistance segfaults the
// compiler backend (libllvm-qgl.so).
// This pass shadows the builtin so the decompiled ESSL only ever touches it in the
// shapes Adreno accepts. Output side (vertex + geometry): all accesses to the
// Output ClipDistance (gl_PerVertex member or standalone variable) are redirected
// to a Private mg_ClipDistance array, and a flush writing the real builtin with
// literal constant indices is inserted before every OpEmitVertex (geometry) or
// every return of the entry point (vertex). Input side (geometry): accesses to
// gl_in[...].gl_ClipDistance are redirected to a Private mg_ClipDistanceIn
// array-of-arrays filled once at the top of the entry point by a structured loop
// whose gl_in reads use dynamic (loop-variable) indices. The builtin members stay
// statically referenced by the flush/copy so cross-stage IO matching is intact.
// Only meant for the DirectGLES transpile path on Qualcomm devices.
class LowerClipDistanceForEsslPass : public spvtools::opt::Pass {
public:
const char* name() const override { return "lower-clip-distance-for-essl"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateLowerClipDistanceForEsslPass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
+85
View File
@@ -0,0 +1,85 @@
# Running the OpenGL CTS (VK-GL-CTS / KHR-GL33) against MobileGL on Android
Goal: measure how much of the OpenGL 3.3 core-profile conformance suite MobileGL
passes, separately for each backend (`DirectGLES`, `DirectVulkan`).
## How MobileGL is reached from a test binary
MobileGL ships its own EGL implementation alongside its desktop-GL implementation
in a single `libMobileGL.so`. A plain arm64 ELF in `/data/local/tmp` can therefore
drive it with no APK and no Activity:
1. `setenv("MOBILEGL_BACKEND_TYPE", "DirectGLES"|"DirectVulkan")` **before** the
library is mapped — MobileGL parses its configuration from an ELF constructor.
2. `dlopen("libMobileGL.so")`, then `dlsym` the `egl*` and `gl*` entry points.
MobileGL exports 45 EGL symbols and the desktop GL functions directly;
`eglGetProcAddress` resolves the same set.
3. `eglBindAPI(EGL_OPENGL_API)`, choose a config with `EGL_RENDERABLE_TYPE =
EGL_OPENGL_BIT`, then `eglCreateContext` with
`EGL_CONTEXT_OPENGL_PROFILE_MASK = EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT` and
major/minor `3`/`3`.
This yields a genuine GL 3.3 core context (`GL_CONTEXT_PROFILE_MASK == 0x1`).
## Surface type, per backend
| backend | pbuffer (headless) | window |
|---|---|---|
| `DirectGLES` | works | works |
| `DirectVulkan` | **unusable** | works |
`DirectVulkan`'s pbuffer path builds a headless `VkSurfaceKHR` and so requires the
`VK_EXT_headless_surface` instance extension, which Adreno's Android driver does
not expose. It fails inside `eglMakeCurrent`, not at surface creation.
The workaround that keeps everything in a shell process: obtain a real
`ANativeWindow` from **`AImageReader`** (`AImageReader_newWithUsage` +
`AImageReader_getWindow`). It is an ordinary BufferQueue producer, so
`vkCreateAndroidSurfaceKHR` accepts it, and no Activity is involved. Register an
`onImageAvailable` listener that acquires and deletes each image — otherwise the
producer blocks once `maxImages` buffers are in flight and the next swap hangs.
## Why the suite must render into an FBO
On a window surface, `DirectVulkan`'s `glReadPixels` from the **default
framebuffer** returns all zeros, with no GL error, both before and after
`eglSwapBuffers`. `DirectGLES` on the identical window is correct, and readback
from a **user FBO is correct on both backends**.
Verified on two SoCs and two drivers, so this is MobileGL's behaviour rather than
a driver quirk:
| device | GPU | driver | default-FB | user FBO |
|---|---|---|---|---|
| Xiaomi 24129PN74C | Adreno 830 | Vulkan 1.3.284 / 512.800.46 | zeros | ok |
| Lenovo TB321FU | Adreno 750 | Vulkan 1.3.128 / 512.762.28 | zeros | ok |
dEQP verifies nearly every case through `glReadPixels`, so running it against the
default framebuffer would score `DirectVulkan` near zero for a reason unrelated to
conformance. The runs therefore use `--deqp-surface-type=fbo`, uniformly for both
backends so the two numbers stay comparable.
## Other constraints the harness must respect
- `eglMakeCurrent` requires **draw == read** and rejects `EGL_NO_SURFACE` with
`EGL_BAD_MATCH`. dEQP's `surfaceless` platform is therefore unusable, which is
why this port supplies its own `tcu::Platform`.
- MobileGL aborts during static teardown (`FORTIFY: pthread_mutex_lock called on a
destroyed mutex`) *after* all work completes. Flush and `_exit()` so the exit
code and the `.qpa` log survive.
## Contents
probe/mgprobe.c preflight gate: one backend x one surface type, checks
context version/profile and both readback paths
scripts/qpa_report.py .qpa -> pass rate, status histogram, worst groups
### Preflight
aarch64-linux-android26-clang -O1 -o mgprobe mgprobe.c -ldl -llog -landroid -lmediandk
adb push mgprobe libMobileGL.so /data/local/tmp/mgcts/
adb shell 'cd /data/local/tmp/mgcts && LD_LIBRARY_PATH=. ./mgprobe \
--backend DirectVulkan --surface imagereader --lib ./libMobileGL.so'
Exit status is 0 when a 3.3 core context came up and FBO readback is correct.
Default-framebuffer readback is reported but deliberately does not gate.
@@ -0,0 +1,109 @@
diff --git a/framework/opengl/gluFboRenderContext.cpp b/framework/opengl/gluFboRenderContext.cpp
index 588cf7d2a..0721ffee7 100644
--- a/framework/opengl/gluFboRenderContext.cpp
+++ b/framework/opengl/gluFboRenderContext.cpp
@@ -132,6 +132,7 @@ FboRenderContext::FboRenderContext(RenderContext *context, const RenderConfig &c
: m_context(context)
, m_framebuffer(0)
, m_colorBuffer(0)
+ , m_colorIsTexture(false)
, m_depthStencilBuffer(0)
, m_renderTarget()
{
@@ -151,6 +152,7 @@ FboRenderContext::FboRenderContext(const ContextFactory &factory, const RenderCo
: m_context(nullptr)
, m_framebuffer(0)
, m_colorBuffer(0)
+ , m_colorIsTexture(false)
, m_depthStencilBuffer(0)
, m_renderTarget()
{
@@ -215,19 +217,41 @@ void FboRenderContext::createFramebuffer(const RenderConfig &config)
height = (height == glu::RenderConfig::DONT_CARE) ? maxSize : height;
}
+ // MOBILEGL: allow the colour attachment to be a texture instead of a
+ // renderbuffer. MobileGL's DirectVulkan backend returns zeros when reading
+ // back a renderbuffer-attached FBO, which makes every image comparison fail
+ // for one reason and hides everything else. Setting
+ // MOBILEGL_CTS_FBO_COLOR_TEXTURE=1 isolates that single defect so the rest
+ // of the suite can be measured. Off by default: stock behaviour.
{
- pixelFormat = getPixelFormat(colorFormat);
+ const char *useTexEnv = getenv("MOBILEGL_CTS_FBO_COLOR_TEXTURE");
+ m_colorIsTexture = (useTexEnv && useTexEnv[0] == '1' && config.numSamples <= 0);
- gl.genRenderbuffers(1, &m_colorBuffer);
- gl.bindRenderbuffer(GL_RENDERBUFFER, m_colorBuffer);
+ pixelFormat = getPixelFormat(colorFormat);
- if (config.numSamples > 0)
- gl.renderbufferStorageMultisample(GL_RENDERBUFFER, config.numSamples, colorFormat, width, height);
+ if (m_colorIsTexture)
+ {
+ gl.genTextures(1, &m_colorBuffer);
+ gl.bindTexture(GL_TEXTURE_2D, m_colorBuffer);
+ gl.texStorage2D(GL_TEXTURE_2D, 1, colorFormat, width, height);
+ gl.texParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ gl.texParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ gl.bindTexture(GL_TEXTURE_2D, 0);
+ GLU_EXPECT_NO_ERROR(gl.getError(), "Creating color texture");
+ }
else
- gl.renderbufferStorage(GL_RENDERBUFFER, colorFormat, width, height);
-
- gl.bindRenderbuffer(GL_RENDERBUFFER, 0);
- GLU_EXPECT_NO_ERROR(gl.getError(), "Creating color renderbuffer");
+ {
+ gl.genRenderbuffers(1, &m_colorBuffer);
+ gl.bindRenderbuffer(GL_RENDERBUFFER, m_colorBuffer);
+
+ if (config.numSamples > 0)
+ gl.renderbufferStorageMultisample(GL_RENDERBUFFER, config.numSamples, colorFormat, width, height);
+ else
+ gl.renderbufferStorage(GL_RENDERBUFFER, colorFormat, width, height);
+
+ gl.bindRenderbuffer(GL_RENDERBUFFER, 0);
+ GLU_EXPECT_NO_ERROR(gl.getError(), "Creating color renderbuffer");
+ }
}
if (depthStencilFormat != GL_NONE)
@@ -250,7 +274,12 @@ void FboRenderContext::createFramebuffer(const RenderConfig &config)
gl.bindFramebuffer(GL_FRAMEBUFFER, m_framebuffer);
if (m_colorBuffer)
- gl.framebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_colorBuffer);
+ {
+ if (m_colorIsTexture)
+ gl.framebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_colorBuffer, 0);
+ else
+ gl.framebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_colorBuffer);
+ }
if (m_depthStencilBuffer)
{
@@ -290,7 +319,10 @@ void FboRenderContext::destroyFramebuffer(void)
if (m_colorBuffer)
{
- gl.deleteRenderbuffers(1, &m_colorBuffer);
+ if (m_colorIsTexture)
+ gl.deleteTextures(1, &m_colorBuffer);
+ else
+ gl.deleteRenderbuffers(1, &m_colorBuffer);
m_colorBuffer = 0;
}
}
diff --git a/framework/opengl/gluFboRenderContext.hpp b/framework/opengl/gluFboRenderContext.hpp
index 75a0ff6b7..09ff1e7a9 100644
--- a/framework/opengl/gluFboRenderContext.hpp
+++ b/framework/opengl/gluFboRenderContext.hpp
@@ -80,6 +80,7 @@ private:
RenderContext *m_context;
uint32_t m_framebuffer;
uint32_t m_colorBuffer;
+ bool m_colorIsTexture;
uint32_t m_depthStencilBuffer;
tcu::RenderTarget m_renderTarget;
};
+473
View File
@@ -0,0 +1,473 @@
/*-------------------------------------------------------------------------
* dEQP platform port for MobileGL on Android
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*//*!
* \file
* \brief MobileGL platform.
*
* Modelled on the surfaceless platform, but adapted to MobileGL, which ships
* its own EGL implementation inside libMobileGL.so:
*
* - Every EGL call goes through the dynamically loaded library. The
* surfaceless port mixes wrapper calls with globally linked egl* symbols;
* doing that here would silently reach Android's system EGL instead.
* - Desktop-GL configs are selected with EGL_OPENGL_BIT. The surfaceless port
* always asks for an ES bit, which cannot satisfy a GL 3.3 core context.
* - A real surface is always created. MobileGL rejects EGL_NO_SURFACE with
* EGL_BAD_MATCH, and --deqp-surface-type=fbo asks the platform for
* SURFACETYPE_DONT_CARE, so "no surface" is not an option.
* - Window surfaces are backed by an AImageReader rather than an Activity,
* which is what lets the suite run as a plain adb-shell binary. DirectVulkan
* needs this: its pbuffer path requires VK_EXT_headless_surface, which
* Adreno's Android driver does not expose.
*
* Environment:
* MOBILEGL_CTS_LIB path/soname of the MobileGL library (default libMobileGL.so)
* MOBILEGL_CTS_SURFACE "window" (default) or "pbuffer"
* MOBILEGL_BACKEND_TYPE read by MobileGL itself; set it before launching
*//*--------------------------------------------------------------------*/
#include "tcuMobileGLPlatform.hpp"
#include <cstdlib>
#include <string>
#include <vector>
#include "deDynamicLibrary.hpp"
#include "egluUtil.hpp"
#include "eglwEnums.hpp"
#include "eglwLibrary.hpp"
#include "gluPlatform.hpp"
#include "gluRenderConfig.hpp"
#include "gluRenderContext.hpp"
#include "glwInitFunctions.hpp"
#include "tcuCommandLine.hpp"
#include "tcuPixelFormat.hpp"
#include "tcuPlatform.hpp"
#include "tcuRenderTarget.hpp"
#include <android/hardware_buffer.h>
#include <android/native_window.h>
#include <media/NdkImageReader.h>
using std::string;
using std::vector;
#if !defined(EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR)
#define EGL_CONTEXT_FLAGS_KHR 0x30FC
#define EGL_CONTEXT_MAJOR_VERSION_KHR 0x3098
#define EGL_CONTEXT_MINOR_VERSION_KHR 0x30FB
#define EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT_KHR 0x00000002
#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR 0x00000001
#define EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR 0x00000001
#define EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR 0x00000002
#define EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR 0x30FD
#define EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR 0x00000004
#endif
namespace tcu
{
namespace mobilegl
{
static string getLibraryName(void)
{
const char *env = std::getenv("MOBILEGL_CTS_LIB");
return (env && env[0]) ? string(env) : string("libMobileGL.so");
}
//! Window surfaces default on: they are the only kind DirectVulkan can use.
static bool useWindowSurface(void)
{
const char *env = std::getenv("MOBILEGL_CTS_SURFACE");
return !(env && string(env) == "pbuffer");
}
/*--------------------------------------------------------------------*//*!
* \brief A real ANativeWindow with no Activity behind it.
*
* AImageReader's window is an ordinary BufferQueue producer, so both
* eglCreateWindowSurface and vkCreateAndroidSurfaceKHR accept it. The image
* listener must drain the queue: without it the producer blocks once maxImages
* buffers are in flight and the next swap deadlocks.
*//*--------------------------------------------------------------------*/
class ImageReaderWindow
{
public:
ImageReaderWindow(int width, int height) : m_reader(nullptr), m_window(nullptr)
{
const media_status_t status =
AImageReader_newWithUsage(width, height, AIMAGE_FORMAT_RGBA_8888,
AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE |
AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT,
kMaxImages, &m_reader);
if (status != AMEDIA_OK || m_reader == nullptr)
throw tcu::ResourceError("AImageReader_newWithUsage() failed");
AImageReader_ImageListener listener = {this, onImageAvailable};
AImageReader_setImageListener(m_reader, &listener);
if (AImageReader_getWindow(m_reader, &m_window) != AMEDIA_OK || m_window == nullptr)
{
AImageReader_delete(m_reader);
m_reader = nullptr;
throw tcu::ResourceError("AImageReader_getWindow() failed");
}
ANativeWindow_acquire(m_window);
}
~ImageReaderWindow(void)
{
if (m_window != nullptr)
ANativeWindow_release(m_window);
if (m_reader != nullptr)
{
AImageReader_setImageListener(m_reader, nullptr);
AImageReader_delete(m_reader);
}
}
ANativeWindow *getWindow(void) const
{
return m_window;
}
private:
static const int kMaxImages = 4;
static void onImageAvailable(void *, AImageReader *reader)
{
AImage *image = nullptr;
if (AImageReader_acquireNextImage(reader, &image) == AMEDIA_OK && image != nullptr)
AImage_delete(image);
}
ImageReaderWindow(const ImageReaderWindow &);
ImageReaderWindow &operator=(const ImageReaderWindow &);
AImageReader *m_reader;
ANativeWindow *m_window;
};
class GetProcFuncLoader : public glw::FunctionLoader
{
public:
GetProcFuncLoader(const eglw::Library &egl) : m_egl(egl)
{
}
glw::GenericFuncType get(const char *name) const
{
return (glw::GenericFuncType)m_egl.getProcAddress(name);
}
protected:
const eglw::Library &m_egl;
};
class EglRenderContext : public glu::RenderContext
{
public:
EglRenderContext(const glu::RenderConfig &config, const tcu::CommandLine &cmdLine,
const glu::RenderContext *sharedContext);
~EglRenderContext(void);
glu::ContextType getType(void) const
{
return m_contextType;
}
eglw::EGLContext getEglContext(void) const
{
return m_eglContext;
}
const glw::Functions &getFunctions(void) const
{
return m_glFunctions;
}
const tcu::RenderTarget &getRenderTarget(void) const
{
return m_renderTarget;
}
void postIterate(void);
void makeCurrent(void);
glw::GenericFuncType getProcAddress(const char *name) const
{
return (glw::GenericFuncType)m_egl.getProcAddress(name);
}
private:
const eglw::DefaultLibrary m_egl;
const glu::ContextType m_contextType;
eglw::EGLDisplay m_eglDisplay;
eglw::EGLContext m_eglContext;
eglw::EGLSurface m_eglSurface;
ImageReaderWindow *m_window;
glw::Functions m_glFunctions;
tcu::RenderTarget m_renderTarget;
eglw::EGLContext m_sharedEglContext;
};
class ContextFactory : public glu::ContextFactory
{
public:
ContextFactory(void) : glu::ContextFactory("default", "MobileGL EGL context")
{
}
glu::RenderContext *createContext(const glu::RenderConfig &config, const tcu::CommandLine &cmdLine,
const glu::RenderContext *sharedContext) const
{
return new EglRenderContext(config, cmdLine, sharedContext);
}
};
class Platform : public tcu::Platform, public glu::Platform
{
public:
Platform(void)
{
m_contextFactoryRegistry.registerFactory(new ContextFactory());
}
const glu::Platform &getGLPlatform(void) const
{
return *this;
}
};
EglRenderContext::EglRenderContext(const glu::RenderConfig &config, const tcu::CommandLine &cmdLine,
const glu::RenderContext *sharedContext)
: m_egl(getLibraryName().c_str())
, m_contextType(config.type)
, m_eglDisplay(EGL_NO_DISPLAY)
, m_eglContext(EGL_NO_CONTEXT)
, m_eglSurface(EGL_NO_SURFACE)
, m_window(nullptr)
, m_renderTarget(config.width, config.height,
tcu::PixelFormat(config.redBits, config.greenBits, config.blueBits, config.alphaBits),
config.depthBits, config.stencilBits, config.numSamples)
, m_sharedEglContext(EGL_NO_CONTEXT)
{
DE_UNREF(cmdLine);
const glu::ContextType &contextType = config.type;
const bool isES = glu::isContextTypeES(contextType);
eglw::EGLint eglMajorVersion = 0;
eglw::EGLint eglMinorVersion = 0;
m_eglDisplay = m_egl.getDisplay(EGL_DEFAULT_DISPLAY);
EGLU_CHECK_MSG(m_egl, "eglGetDisplay()");
if (m_eglDisplay == EGL_NO_DISPLAY)
throw tcu::ResourceError("eglGetDisplay() failed");
EGLU_CHECK_CALL(m_egl, initialize(m_eglDisplay, &eglMajorVersion, &eglMinorVersion));
// MobileGL cannot make a context current without a surface, so
// SURFACETYPE_DONT_CARE (which is what --deqp-surface-type=fbo requests)
// still gets a real one.
bool wantWindow = false;
switch (config.surfaceType)
{
case glu::RenderConfig::SURFACETYPE_WINDOW:
wantWindow = true;
break;
case glu::RenderConfig::SURFACETYPE_OFFSCREEN_NATIVE:
case glu::RenderConfig::SURFACETYPE_OFFSCREEN_GENERIC:
wantWindow = false;
break;
case glu::RenderConfig::SURFACETYPE_DONT_CARE:
wantWindow = useWindowSurface();
break;
default:
TCU_CHECK_INTERNAL(false);
}
const int width = (config.width == glu::RenderConfig::DONT_CARE) ? 256 : config.width;
const int height = (config.height == glu::RenderConfig::DONT_CARE) ? 256 : config.height;
vector<eglw::EGLint> cfgAttribs;
cfgAttribs.push_back(EGL_RENDERABLE_TYPE);
if (isES)
{
switch (contextType.getMajorVersion())
{
case 3:
cfgAttribs.push_back(EGL_OPENGL_ES3_BIT);
break;
case 2:
cfgAttribs.push_back(EGL_OPENGL_ES2_BIT);
break;
default:
cfgAttribs.push_back(EGL_OPENGL_ES_BIT);
}
}
else
{
// Desktop GL, which is the whole point of this port.
cfgAttribs.push_back(EGL_OPENGL_BIT);
}
cfgAttribs.push_back(EGL_SURFACE_TYPE);
cfgAttribs.push_back(wantWindow ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT);
static const struct
{
eglw::EGLint attrib;
int glu::RenderConfig::*field;
} s_sizeAttribs[] = {
{EGL_RED_SIZE, &glu::RenderConfig::redBits}, {EGL_GREEN_SIZE, &glu::RenderConfig::greenBits},
{EGL_BLUE_SIZE, &glu::RenderConfig::blueBits}, {EGL_ALPHA_SIZE, &glu::RenderConfig::alphaBits},
{EGL_DEPTH_SIZE, &glu::RenderConfig::depthBits}, {EGL_STENCIL_SIZE, &glu::RenderConfig::stencilBits},
{EGL_SAMPLES, &glu::RenderConfig::numSamples},
};
for (size_t ndx = 0; ndx < DE_LENGTH_OF_ARRAY(s_sizeAttribs); ndx++)
{
const int value = config.*(s_sizeAttribs[ndx].field);
if (value != glu::RenderConfig::DONT_CARE)
{
cfgAttribs.push_back(s_sizeAttribs[ndx].attrib);
cfgAttribs.push_back(value);
}
}
cfgAttribs.push_back(EGL_NONE);
eglw::EGLConfig eglConfig = nullptr;
eglw::EGLint numConfigs = 0;
EGLU_CHECK_CALL(m_egl, chooseConfig(m_eglDisplay, &cfgAttribs[0], &eglConfig, 1, &numConfigs));
if (numConfigs < 1)
throw tcu::NotSupportedError("No matching EGL config for the requested context");
if (wantWindow)
{
m_window = new ImageReaderWindow(width, height);
eglw::EGLint visualId = 0;
if (m_egl.getConfigAttrib(m_eglDisplay, eglConfig, EGL_NATIVE_VISUAL_ID, &visualId) && visualId != 0)
ANativeWindow_setBuffersGeometry(m_window->getWindow(), width, height, visualId);
m_eglSurface = m_egl.createWindowSurface(m_eglDisplay, eglConfig,
(eglw::EGLNativeWindowType)m_window->getWindow(), nullptr);
EGLU_CHECK_MSG(m_egl, "eglCreateWindowSurface()");
}
else
{
const eglw::EGLint surfaceAttribs[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE};
m_eglSurface = m_egl.createPbufferSurface(m_eglDisplay, eglConfig, surfaceAttribs);
EGLU_CHECK_MSG(m_egl, "eglCreatePbufferSurface()");
}
if (m_eglSurface == EGL_NO_SURFACE)
throw tcu::ResourceError("Failed to create EGL surface");
vector<eglw::EGLint> ctxAttribs;
ctxAttribs.push_back(EGL_CONTEXT_MAJOR_VERSION_KHR);
ctxAttribs.push_back(contextType.getMajorVersion());
ctxAttribs.push_back(EGL_CONTEXT_MINOR_VERSION_KHR);
ctxAttribs.push_back(contextType.getMinorVersion());
switch (contextType.getProfile())
{
case glu::PROFILE_ES:
EGLU_CHECK_CALL(m_egl, bindAPI(EGL_OPENGL_ES_API));
break;
case glu::PROFILE_CORE:
EGLU_CHECK_CALL(m_egl, bindAPI(EGL_OPENGL_API));
ctxAttribs.push_back(EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR);
ctxAttribs.push_back(EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR);
break;
case glu::PROFILE_COMPATIBILITY:
EGLU_CHECK_CALL(m_egl, bindAPI(EGL_OPENGL_API));
ctxAttribs.push_back(EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR);
ctxAttribs.push_back(EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT_KHR);
break;
default:
TCU_CHECK_INTERNAL(false);
}
eglw::EGLint flags = 0;
if ((contextType.getFlags() & glu::CONTEXT_DEBUG) != 0)
flags |= EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR;
if ((contextType.getFlags() & glu::CONTEXT_ROBUST) != 0)
flags |= EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR;
if ((contextType.getFlags() & glu::CONTEXT_FORWARD_COMPATIBLE) != 0)
flags |= EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR;
if (flags != 0)
{
ctxAttribs.push_back(EGL_CONTEXT_FLAGS_KHR);
ctxAttribs.push_back(flags);
}
ctxAttribs.push_back(EGL_NONE);
const EglRenderContext *sharedEglRenderContext = dynamic_cast<const EglRenderContext *>(sharedContext);
m_sharedEglContext = sharedEglRenderContext ? sharedEglRenderContext->getEglContext() : EGL_NO_CONTEXT;
m_eglContext = m_egl.createContext(m_eglDisplay, eglConfig, m_sharedEglContext, &ctxAttribs[0]);
EGLU_CHECK_MSG(m_egl, "eglCreateContext()");
if (!m_eglContext)
throw tcu::ResourceError("eglCreateContext() failed");
// MobileGL requires draw == read.
EGLU_CHECK_CALL(m_egl, makeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglContext));
// MobileGL advertises EGL 1.5, so eglGetProcAddress resolves core entry
// points too; there is no separate GL library to dlopen.
GetProcFuncLoader funcLoader(m_egl);
glu::initCoreFunctions(&m_glFunctions, &funcLoader, contextType.getAPI());
glu::initExtensionFunctions(&m_glFunctions, &funcLoader, contextType.getAPI());
}
EglRenderContext::~EglRenderContext(void)
{
try
{
if (m_eglDisplay != EGL_NO_DISPLAY)
{
m_egl.makeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
if (m_eglContext != EGL_NO_CONTEXT)
m_egl.destroyContext(m_eglDisplay, m_eglContext);
if (m_eglSurface != EGL_NO_SURFACE)
m_egl.destroySurface(m_eglDisplay, m_eglSurface);
if (m_sharedEglContext == EGL_NO_CONTEXT)
m_egl.terminate(m_eglDisplay);
}
}
catch (...)
{
}
delete m_window;
}
void EglRenderContext::makeCurrent(void)
{
EGLU_CHECK_CALL(m_egl, makeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglContext));
}
void EglRenderContext::postIterate(void)
{
m_glFunctions.finish();
}
} // namespace mobilegl
} // namespace tcu
tcu::Platform *createPlatform(void)
{
return new tcu::mobilegl::Platform();
}
@@ -0,0 +1,33 @@
#ifndef _TCUMOBILEGLPLATFORM_HPP
#define _TCUMOBILEGLPLATFORM_HPP
/*-------------------------------------------------------------------------
* dEQP platform port for MobileGL on Android
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*//*!
* \file
* \brief MobileGL platform - drives libMobileGL.so's own EGL from a bare
* Android process, with no Activity and no system EGL involved.
*//*--------------------------------------------------------------------*/
#include "tcuDefs.hpp"
namespace tcu
{
class Platform;
}
tcu::Platform *createPlatform(void);
#endif // _TCUMOBILEGLPLATFORM_HPP
+2
View File
@@ -0,0 +1,2 @@
mgprobe
*.o
+359
View File
@@ -0,0 +1,359 @@
/* mgprobe - preflight gate for running a GL conformance suite against MobileGL
* from a bare adb-shell process (no APK, no Activity).
*
* Verifies, for one backend and one surface type, that MobileGL can hand out a
* GL 3.3 core context and that pixels read back correctly - both from the
* default framebuffer and from a user FBO. Run this before burning hours on a
* CTS run; it catches a broken device/library pairing in about a second.
*
* mgprobe --backend DirectGLES|DirectVulkan --surface pbuffer|imagereader
* [--lib /path/to/libMobileGL.so]
*
* Exit status: 0 if a context came up and FBO readback is correct, non-zero
* otherwise. Default-framebuffer readback is reported but does NOT gate, because
* DirectVulkan is known to return zeros there while FBO readback is sound.
*
* Build (NDK, arm64):
* $NDK/toolchains/llvm/prebuilt/<host>/bin/aarch64-linux-android26-clang \
* -O1 -o mgprobe mgprobe.c -ldl -llog -landroid -lmediandk
*/
#include <android/native_window.h>
#include <dlfcn.h>
#include <media/NdkImageReader.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
typedef void *EGLDisplay;
typedef void *EGLConfig;
typedef void *EGLSurface;
typedef void *EGLContext;
typedef int EGLint;
typedef unsigned int EGLBoolean;
typedef unsigned int EGLenum;
typedef void *EGLNativeDisplayType;
typedef void *EGLNativeWindowType;
#define EGL_DEFAULT_DISPLAY ((EGLNativeDisplayType)0)
#define EGL_NO_CONTEXT ((EGLContext)0)
#define EGL_NO_SURFACE ((EGLSurface)0)
#define EGL_NONE 0x3038
#define EGL_WIDTH 0x3057
#define EGL_HEIGHT 0x3056
#define EGL_RENDERABLE_TYPE 0x3040
#define EGL_SURFACE_TYPE 0x3033
#define EGL_WINDOW_BIT 0x0004
#define EGL_PBUFFER_BIT 0x0001
#define EGL_OPENGL_BIT 0x0008
#define EGL_OPENGL_API 0x30A2
#define EGL_RED_SIZE 0x3024
#define EGL_GREEN_SIZE 0x3023
#define EGL_BLUE_SIZE 0x3022
#define EGL_ALPHA_SIZE 0x3021
#define EGL_DEPTH_SIZE 0x3025
#define EGL_STENCIL_SIZE 0x3026
#define EGL_NATIVE_VISUAL_ID 0x302E
#define EGL_CONTEXT_MAJOR_VERSION 0x3098
#define EGL_CONTEXT_MINOR_VERSION 0x30FB
#define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD
#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001
#define GL_VENDOR 0x1F00
#define GL_RENDERER 0x1F01
#define GL_VERSION 0x1F02
#define GL_SHADING_LANGUAGE_VERSION 0x8B8C
#define GL_CONTEXT_PROFILE_MASK 0x9126
#define GL_MAJOR_VERSION 0x821B
#define GL_MINOR_VERSION 0x821C
#define GL_COLOR_BUFFER_BIT 0x00004000
#define GL_RGBA 0x1908
#define GL_RGBA8 0x8058
#define GL_UNSIGNED_BYTE 0x1401
#define GL_TEXTURE_2D 0x0DE1
#define GL_FRAMEBUFFER 0x8D40
#define GL_COLOR_ATTACHMENT0 0x8CE0
#define GL_FRAMEBUFFER_COMPLETE 0x8CD5
#define GL_TEXTURE_MIN_FILTER 0x2801
#define GL_TEXTURE_MAG_FILTER 0x2800
#define GL_NEAREST 0x2600
#define GL_RENDERBUFFER 0x8D41
typedef EGLDisplay (*P_getDisplay)(EGLNativeDisplayType);
typedef EGLBoolean (*P_initialize)(EGLDisplay, EGLint *, EGLint *);
typedef EGLBoolean (*P_bindAPI)(EGLenum);
typedef EGLBoolean (*P_chooseConfig)(EGLDisplay, const EGLint *, EGLConfig *, EGLint, EGLint *);
typedef EGLBoolean (*P_getConfigAttrib)(EGLDisplay, EGLConfig, EGLint, EGLint *);
typedef EGLSurface (*P_createWindowSurface)(EGLDisplay, EGLConfig, EGLNativeWindowType, const EGLint *);
typedef EGLSurface (*P_createPbufferSurface)(EGLDisplay, EGLConfig, const EGLint *);
typedef EGLContext (*P_createContext)(EGLDisplay, EGLConfig, EGLContext, const EGLint *);
typedef EGLBoolean (*P_makeCurrent)(EGLDisplay, EGLSurface, EGLSurface, EGLContext);
typedef EGLint (*P_getError)(void);
typedef const unsigned char *(*P_glGetString)(unsigned int);
typedef void (*P_glGetIntegerv)(unsigned int, int *);
typedef void (*P_glClearColor)(float, float, float, float);
typedef void (*P_glClear)(unsigned int);
typedef void (*P_glFinish)(void);
typedef void (*P_glReadPixels)(int, int, int, int, unsigned int, unsigned int, void *);
typedef unsigned int (*P_glGetError)(void);
typedef void (*P_glGenTextures)(int, unsigned int *);
typedef void (*P_glBindTexture)(unsigned int, unsigned int);
typedef void (*P_glTexImage2D)(unsigned int, int, int, int, int, int, unsigned int, unsigned int, const void *);
typedef void (*P_glTexParameteri)(unsigned int, unsigned int, int);
typedef void (*P_glGenFramebuffers)(int, unsigned int *);
typedef void (*P_glBindFramebuffer)(unsigned int, unsigned int);
typedef void (*P_glFramebufferTexture2D)(unsigned int, unsigned int, unsigned int, unsigned int, int);
typedef unsigned int (*P_glCheckFramebufferStatus)(unsigned int);
typedef void (*P_glViewport)(int, int, int, int);
typedef void (*P_glGenRenderbuffers)(int, unsigned int *);
typedef void (*P_glBindRenderbuffer)(unsigned int, unsigned int);
typedef void (*P_glRenderbufferStorage)(unsigned int, unsigned int, int, int);
typedef void (*P_glFramebufferRenderbuffer)(unsigned int, unsigned int, unsigned int, unsigned int);
static void *g_lib;
static void *S(const char *n) { return dlsym(g_lib, n); }
static void on_image(void *ctx, AImageReader *r) {
(void)ctx;
AImage *img = NULL;
/* Drain the queue, or the producer blocks once maxImages are in flight. */
if (AImageReader_acquireNextImage(r, &img) == AMEDIA_OK && img) AImage_delete(img);
}
#define DIM 256
static int near8(unsigned got, int want, int tol) {
int d = (int)got - want;
return d <= tol && d >= -tol;
}
int main(int argc, char **argv) {
const char *backend = "DirectGLES";
const char *surface = "pbuffer";
const char *libpath = "libMobileGL.so";
for (int i = 1; i < argc; ++i) {
if (!strcmp(argv[i], "--backend") && i + 1 < argc) backend = argv[++i];
else if (!strcmp(argv[i], "--surface") && i + 1 < argc) surface = argv[++i];
else if (!strcmp(argv[i], "--lib") && i + 1 < argc) libpath = argv[++i];
else {
fprintf(stderr, "usage: %s [--backend DirectGLES|DirectVulkan]"
" [--surface pbuffer|imagereader] [--lib path]\n", argv[0]);
return 2;
}
}
setvbuf(stdout, NULL, _IONBF, 0);
/* MobileGL parses its config from an ELF constructor, so the backend must be
* selected before the library is mapped. */
setenv("MOBILEGL_BACKEND_TYPE", backend, 1);
printf("mgprobe backend=%s surface=%s lib=%s\n", backend, surface, libpath);
int useWindow = !strcmp(surface, "imagereader");
ANativeWindow *win = NULL;
AImageReader *reader = NULL;
if (useWindow) {
if (AImageReader_newWithUsage(DIM, DIM, AIMAGE_FORMAT_RGBA_8888,
AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE |
AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT,
4, &reader) != AMEDIA_OK || !reader) {
printf("FAIL AImageReader_newWithUsage\n");
return 3;
}
AImageReader_ImageListener l = {NULL, on_image};
AImageReader_setImageListener(reader, &l);
if (AImageReader_getWindow(reader, &win) != AMEDIA_OK || !win) {
printf("FAIL AImageReader_getWindow\n");
return 3;
}
}
g_lib = dlopen(libpath, RTLD_NOW | RTLD_LOCAL);
if (!g_lib) {
printf("FAIL dlopen: %s\n", dlerror());
return 4;
}
P_getDisplay eglGetDisplay_ = (P_getDisplay)S("eglGetDisplay");
P_initialize eglInitialize_ = (P_initialize)S("eglInitialize");
P_bindAPI eglBindAPI_ = (P_bindAPI)S("eglBindAPI");
P_chooseConfig eglChooseConfig_ = (P_chooseConfig)S("eglChooseConfig");
P_getConfigAttrib eglGetConfigAttrib_ = (P_getConfigAttrib)S("eglGetConfigAttrib");
P_createWindowSurface eglCreateWindowSurface_ = (P_createWindowSurface)S("eglCreateWindowSurface");
P_createPbufferSurface eglCreatePbufferSurface_ = (P_createPbufferSurface)S("eglCreatePbufferSurface");
P_createContext eglCreateContext_ = (P_createContext)S("eglCreateContext");
P_makeCurrent eglMakeCurrent_ = (P_makeCurrent)S("eglMakeCurrent");
P_getError eglGetError_ = (P_getError)S("eglGetError");
if (!eglGetDisplay_ || !eglInitialize_ || !eglChooseConfig_ || !eglCreateContext_ || !eglMakeCurrent_) {
printf("FAIL missing core EGL exports\n");
return 5;
}
EGLDisplay dpy = eglGetDisplay_(EGL_DEFAULT_DISPLAY);
EGLint vmaj = 0, vmin = 0;
if (!eglInitialize_(dpy, &vmaj, &vmin)) {
printf("FAIL eglInitialize err=0x%x\n", eglGetError_ ? eglGetError_() : 0);
return 6;
}
if (eglBindAPI_ && !eglBindAPI_(EGL_OPENGL_API)) {
printf("FAIL eglBindAPI(EGL_OPENGL_API) err=0x%x\n", eglGetError_ ? eglGetError_() : 0);
return 7;
}
const EGLint cfgAttribs[] = {
EGL_SURFACE_TYPE, useWindow ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8,
EGL_DEPTH_SIZE, 24, EGL_STENCIL_SIZE, 8,
EGL_NONE};
EGLConfig cfg = 0;
EGLint ncfg = 0;
if (!eglChooseConfig_(dpy, cfgAttribs, &cfg, 1, &ncfg) || ncfg < 1) {
printf("FAIL eglChooseConfig n=%d err=0x%x\n", ncfg, eglGetError_ ? eglGetError_() : 0);
return 8;
}
EGLSurface surf;
if (useWindow) {
EGLint vis = 0;
if (eglGetConfigAttrib_ && eglGetConfigAttrib_(dpy, cfg, EGL_NATIVE_VISUAL_ID, &vis) && vis)
ANativeWindow_setBuffersGeometry(win, DIM, DIM, vis);
surf = eglCreateWindowSurface_(dpy, cfg, (EGLNativeWindowType)win, NULL);
} else {
const EGLint sa[] = {EGL_WIDTH, DIM, EGL_HEIGHT, DIM, EGL_NONE};
surf = eglCreatePbufferSurface_(dpy, cfg, sa);
}
if (surf == EGL_NO_SURFACE) {
printf("FAIL create%sSurface err=0x%x\n", useWindow ? "Window" : "Pbuffer",
eglGetError_ ? eglGetError_() : 0);
return 9;
}
const EGLint ctxAttribs[] = {
EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3,
EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, EGL_NONE};
EGLContext ctx = eglCreateContext_(dpy, cfg, EGL_NO_CONTEXT, ctxAttribs);
if (ctx == EGL_NO_CONTEXT) {
printf("FAIL eglCreateContext(3.3 core) err=0x%x\n", eglGetError_ ? eglGetError_() : 0);
return 10;
}
/* MobileGL requires draw == read and rejects EGL_NO_SURFACE. */
if (!eglMakeCurrent_(dpy, surf, surf, ctx)) {
printf("FAIL eglMakeCurrent err=0x%x\n", eglGetError_ ? eglGetError_() : 0);
return 11;
}
P_glGetString glGetString_ = (P_glGetString)S("glGetString");
P_glGetIntegerv glGetIntegerv_ = (P_glGetIntegerv)S("glGetIntegerv");
P_glClearColor glClearColor_ = (P_glClearColor)S("glClearColor");
P_glClear glClear_ = (P_glClear)S("glClear");
P_glFinish glFinish_ = (P_glFinish)S("glFinish");
P_glReadPixels glReadPixels_ = (P_glReadPixels)S("glReadPixels");
P_glGetError glGetError_ = (P_glGetError)S("glGetError");
P_glGenTextures glGenTextures_ = (P_glGenTextures)S("glGenTextures");
P_glBindTexture glBindTexture_ = (P_glBindTexture)S("glBindTexture");
P_glTexImage2D glTexImage2D_ = (P_glTexImage2D)S("glTexImage2D");
P_glTexParameteri glTexParameteri_ = (P_glTexParameteri)S("glTexParameteri");
P_glGenFramebuffers glGenFramebuffers_ = (P_glGenFramebuffers)S("glGenFramebuffers");
P_glBindFramebuffer glBindFramebuffer_ = (P_glBindFramebuffer)S("glBindFramebuffer");
P_glFramebufferTexture2D glFramebufferTexture2D_ = (P_glFramebufferTexture2D)S("glFramebufferTexture2D");
P_glCheckFramebufferStatus glCheckFramebufferStatus_ = (P_glCheckFramebufferStatus)S("glCheckFramebufferStatus");
P_glViewport glViewport_ = (P_glViewport)S("glViewport");
P_glGenRenderbuffers glGenRenderbuffers_ = (P_glGenRenderbuffers)S("glGenRenderbuffers");
P_glBindRenderbuffer glBindRenderbuffer_ = (P_glBindRenderbuffer)S("glBindRenderbuffer");
P_glRenderbufferStorage glRenderbufferStorage_ = (P_glRenderbufferStorage)S("glRenderbufferStorage");
P_glFramebufferRenderbuffer glFramebufferRenderbuffer_ = (P_glFramebufferRenderbuffer)S("glFramebufferRenderbuffer");
int major = -1, minor = -1, profile = -1;
glGetIntegerv_(GL_MAJOR_VERSION, &major);
glGetIntegerv_(GL_MINOR_VERSION, &minor);
glGetIntegerv_(GL_CONTEXT_PROFILE_MASK, &profile);
printf(" GL_VENDOR %s\n", (const char *)glGetString_(GL_VENDOR));
printf(" GL_RENDERER %s\n", (const char *)glGetString_(GL_RENDERER));
printf(" GL_VERSION %s\n", (const char *)glGetString_(GL_VERSION));
printf(" GLSL %s\n", (const char *)glGetString_(GL_SHADING_LANGUAGE_VERSION));
printf(" version %d.%d profile_mask 0x%x %s\n", major, minor, profile,
(profile & 1) ? "(core)" : "(NOT CORE)");
unsigned char px[4];
/* Default framebuffer. */
glClearColor_(0.25f, 0.5f, 0.75f, 1.0f);
glClear_(GL_COLOR_BUFFER_BIT);
if (glFinish_) glFinish_();
memset(px, 0, sizeof px);
glReadPixels_(DIM / 2, DIM / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px);
int defOk = near8(px[0], 64, 10) && near8(px[1], 128, 10) && near8(px[2], 191, 10);
printf(" default-FB readback (%u,%u,%u,%u) %s\n", px[0], px[1], px[2], px[3],
defOk ? "ok" : "BROKEN");
/* User FBO - this is what dEQP uses with --deqp-surface-type=fbo. */
unsigned int tex = 0, fbo = 0;
glGenTextures_(1, &tex);
glBindTexture_(GL_TEXTURE_2D, tex);
glTexImage2D_(GL_TEXTURE_2D, 0, GL_RGBA8, DIM, DIM, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glTexParameteri_(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri_(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glGenFramebuffers_(1, &fbo);
glBindFramebuffer_(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
unsigned int fbst = glCheckFramebufferStatus_(GL_FRAMEBUFFER);
int fboOk = 0;
if (fbst == GL_FRAMEBUFFER_COMPLETE) {
glViewport_(0, 0, DIM, DIM);
glClearColor_(0.9f, 0.2f, 0.4f, 1.0f);
glClear_(GL_COLOR_BUFFER_BIT);
if (glFinish_) glFinish_();
memset(px, 0, sizeof px);
glReadPixels_(DIM / 2, DIM / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px);
fboOk = near8(px[0], 230, 10) && near8(px[1], 51, 10) && near8(px[2], 102, 10);
printf(" user-FBO readback (%u,%u,%u,%u) %s\n", px[0], px[1], px[2], px[3],
fboOk ? "ok" : "BROKEN");
} else {
printf(" user-FBO incomplete status=0x%x\n", fbst);
}
/* FBO with a RENDERBUFFER colour attachment. This is what dEQP's
* FboRenderContext allocates for --deqp-surface-type=fbo, so it is the path
* that actually decides a conformance run - a texture-attached FBO working
* says nothing about it. */
unsigned int rbo = 0, rfbo = 0;
int rboOk = 0;
if (glGenRenderbuffers_ && glBindRenderbuffer_ && glRenderbufferStorage_ && glFramebufferRenderbuffer_) {
glGenRenderbuffers_(1, &rbo);
glBindRenderbuffer_(GL_RENDERBUFFER, rbo);
glRenderbufferStorage_(GL_RENDERBUFFER, GL_RGBA8, DIM, DIM);
glBindRenderbuffer_(GL_RENDERBUFFER, 0);
glGenFramebuffers_(1, &rfbo);
glBindFramebuffer_(GL_FRAMEBUFFER, rfbo);
glFramebufferRenderbuffer_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo);
unsigned int rst = glCheckFramebufferStatus_(GL_FRAMEBUFFER);
if (rst == GL_FRAMEBUFFER_COMPLETE) {
glViewport_(0, 0, DIM, DIM);
glClearColor_(0.1f, 0.7f, 0.3f, 1.0f);
glClear_(GL_COLOR_BUFFER_BIT);
if (glFinish_) glFinish_();
memset(px, 0, sizeof px);
glReadPixels_(DIM / 2, DIM / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px);
rboOk = near8(px[0], 26, 10) && near8(px[1], 179, 10) && near8(px[2], 77, 10);
printf(" rbo-FBO readback (%u,%u,%u,%u) %s\n", px[0], px[1], px[2], px[3],
rboOk ? "ok" : "BROKEN");
} else {
printf(" rbo-FBO incomplete status=0x%x\n", rst);
}
} else {
printf(" rbo-FBO skipped (renderbuffer entry points unavailable)\n");
}
unsigned glerr = glGetError_ ? glGetError_() : 0;
int ok = fboOk && rboOk && (major > 3 || (major == 3 && minor >= 3)) && (profile & 1) && glerr == 0;
printf("%s backend=%s surface=%s default_fb=%s user_fbo=%s rbo_fbo=%s glerr=0x%x\n",
ok ? "PASS" : "FAIL", backend, surface, defOk ? "ok" : "broken",
fboOk ? "ok" : "broken", rboOk ? "ok" : "broken", glerr);
fflush(stdout);
/* MobileGL aborts in static teardown; leave before that runs. */
_exit(ok ? 0 : 1);
}
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env python
"""Summarise dEQP/glcts .qpa logs into a conformance pass rate.
Handles the two ways a case can end in a .qpa: a normal
``#beginTestCaseResult``/``#endTestCaseResult`` pair carrying a
``<Result StatusCode="...">`` element, and ``#terminateTestCaseResult <reason>``,
which is what the log contains when the process died partway through a case.
Cases that were started but never terminated (the run was killed) are reported
separately so a truncated chunk is never silently scored as a pass.
Usage:
python qpa_report.py <file-or-dir> [<file-or-dir> ...] [--json out.json] [--top N]
"""
import argparse
import json
import os
import re
import sys
from collections import Counter, defaultdict
# Khronos conformance treats these as non-failures: the test either passed or
# the implementation legitimately does not expose the feature under test.
NON_FAILURE = {
"Pass",
"NotSupported",
"QualityWarning",
"CompatibilityWarning",
"Waiver",
}
# Statuses that indicate the case did not merely fail but destabilised the run.
HARD = {"Crash", "Timeout", "InternalError", "ResourceError", "DeviceHang"}
CASE_START = re.compile(r"^#beginTestCaseResult\s+(\S+)")
CASE_END = re.compile(r"^#endTestCaseResult")
CASE_TERM = re.compile(r"^#terminateTestCaseResult\s+(.*)")
RESULT = re.compile(r'<Result\s+StatusCode="([^"]+)"')
def parse_qpa(path):
"""Yield (case_name, status) for every case recorded in one .qpa file."""
current = None
status = None
with open(path, "r", encoding="utf-8", errors="replace") as fh:
for line in fh:
m = CASE_START.match(line)
if m:
if current is not None:
# A new case started before the previous one closed.
yield current, status or "Incomplete"
current, status = m.group(1), None
continue
if current is None:
continue
m = RESULT.search(line)
if m:
status = m.group(1)
continue
m = CASE_TERM.match(line)
if m:
reason = m.group(1).strip() or "Terminated"
# dEQP writes e.g. "Crash" / "Timeout" here.
yield current, reason if reason in HARD else "Crash"
current, status = None, None
continue
if CASE_END.match(line):
yield current, status or "Incomplete"
current, status = None, None
if current is not None:
# File ended mid-case: the runner was killed.
yield current, "Incomplete"
def collect(paths):
files = []
for p in paths:
if os.path.isdir(p):
for root, _dirs, names in os.walk(p):
files.extend(os.path.join(root, n) for n in sorted(names) if n.endswith(".qpa"))
else:
files.append(p)
return files
def group_of(case):
"""The case's parent group, e.g. KHR-GL33.shaders.arrays for ...arrays.foo."""
parts = case.split(".")
return ".".join(parts[:-1]) if len(parts) > 1 else case
def load_sidecar(paths, name):
"""Case names run_cts.py recorded in one of its sidecar lists."""
out = set()
for p in paths:
d = p if os.path.isdir(p) else os.path.dirname(p)
f = os.path.join(d, name)
if os.path.isfile(f):
with open(f, "r", encoding="utf-8") as fh:
out.update(l.strip() for l in fh if l.strip() and not l.strip().startswith("#"))
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument("paths", nargs="+")
ap.add_argument("--json", dest="json_out")
ap.add_argument("--top", type=int, default=25)
ap.add_argument("--label", default="")
args = ap.parse_args()
files = collect(args.paths)
if not files:
print("no .qpa files found", file=sys.stderr)
return 2
# Later chunks may re-run a case; last result wins.
results = {}
for f in files:
for case, status in parse_qpa(f):
results[case] = status
# A case the runner saw take the process down is a Crash, not merely an
# unterminated log entry - but a real result from a later retry wins.
for case in load_sidecar(args.paths, "crashed.txt"):
if results.get(case, "Incomplete") == "Incomplete":
results[case] = "Crash"
# Worse than a crash: these rebooted the device.
for case in load_sidecar(args.paths, "hung.txt"):
if results.get(case, "Incomplete") in ("Incomplete", "Crash"):
results[case] = "DeviceHang"
# Cases excluded up front, and cases the run never reached, are not results.
# Report them separately so a partial run is never read as a complete one.
skipped = load_sidecar(args.paths, "skipped.txt")
unrun = load_sidecar(args.paths, "unrun.txt") - set(results)
counts = Counter(results.values())
total = len(results)
non_fail = sum(counts[s] for s in NON_FAILURE)
strict_pass = counts["Pass"]
failures = total - non_fail
by_group_fail = defaultdict(int)
by_group_total = defaultdict(int)
for case, status in results.items():
g = group_of(case)
by_group_total[g] += 1
if status not in NON_FAILURE:
by_group_fail[g] += 1
label = f" [{args.label}]" if args.label else ""
print(f"=== glcts conformance summary{label} ===")
print(f"files parsed : {len(files)}")
print(f"cases with result : {total}")
print()
for status, n in counts.most_common():
mark = " " if status in NON_FAILURE else " ! "
print(f"{mark}{status:<22} {n:>7} {100.0 * n / total:6.2f}%")
print()
if total:
print(f"conformance pass rate (Pass+NotSupported+warnings) : {100.0 * non_fail / total:6.2f}% ({non_fail}/{total})")
print(f"strict pass rate (Pass only) : {100.0 * strict_pass / total:6.2f}% ({strict_pass}/{total})")
print(f"failures : {failures}")
if skipped or unrun:
print("\n--- NOT MEASURED (excluded from the rates above) ---")
if skipped:
print(f" quarantined up front : {len(skipped)}")
if unrun:
print(f" never reached : {len(unrun)}")
print(" The rates above cover only cases that produced a result.")
if failures:
print(f"\n--- worst groups (of {len(by_group_total)}) ---")
worst = sorted(by_group_fail.items(), key=lambda kv: -kv[1])[: args.top]
for g, nf in worst:
nt = by_group_total[g]
print(f" {g:<52} {nf:>6}/{nt:<6} fail ({100.0 * nf / nt:5.1f}%)")
if args.json_out:
with open(args.json_out, "w", encoding="utf-8") as fh:
json.dump(
{
"label": args.label,
"files": len(files),
"total": total,
"counts": dict(counts),
"non_failure": non_fail,
"strict_pass": strict_pass,
"failures": failures,
"pass_rate": (non_fail / total) if total else 0.0,
"strict_pass_rate": (strict_pass / total) if total else 0.0,
"results": results,
},
fh,
indent=1,
)
print(f"\nwrote {args.json_out}")
return 0
if __name__ == "__main__":
sys.exit(main())
+269
View File
@@ -0,0 +1,269 @@
#!/usr/bin/env python
"""Drive a glcts run on a device, resuming across crashes.
MobileGL crashes on some cases, and glcts takes the whole process down with it.
A single invocation would therefore stop at the first crash and leave most of
the suite unmeasured. This runner re-invokes glcts with only the cases that have
not produced a result yet, records each crashed case as "Crash", and repeats
until the list is exhausted, so one bad case costs one case rather than the run.
Usage:
python run_cts.py --serial <adb-serial> --backend DirectGLES|DirectVulkan \\
--caselist <host-path-to-mustpass.txt> --outdir <host-dir> [--device-dir /data/local/tmp/mgcts]
"""
import argparse
import os
import re
import subprocess
import sys
import time
CASE_START = re.compile(r"^#beginTestCaseResult\s+(\S+)")
CASE_END = re.compile(r"^#endTestCaseResult")
CASE_TERM = re.compile(r"^#terminateTestCaseResult\s+(.*)")
def adb(serial, *args, timeout=None):
try:
return subprocess.run(["adb", "-s", serial, *args], capture_output=True, text=True, timeout=timeout)
except subprocess.TimeoutExpired:
return subprocess.CompletedProcess(args, returncode=124, stdout="", stderr="adb timeout")
def device_alive(serial, timeout=30):
"""True only if the device answers a trivial shell command.
Distinguishes "glcts crashed" from "the device fell over". Without this a
dead device looks like every remaining case crashing, which silently turns a
broken run into a plausible-looking conformance number.
"""
r = adb(serial, "shell", "echo alive", timeout=timeout)
return r.returncode == 0 and "alive" in (r.stdout or "")
def wait_for_device(serial, attempts=20, delay=15):
for i in range(attempts):
if device_alive(serial):
return True
print(f"[run_cts] device {serial} unresponsive, waiting ({i + 1}/{attempts})")
time.sleep(delay)
return False
def mem_available_kb(serial):
r = adb(serial, "shell", "grep MemAvailable /proc/meminfo", timeout=30)
m = re.search(r"(\d+)", r.stdout or "")
return int(m.group(1)) if m else None
def completed_cases(qpa_path):
"""Return (finished_case_names, last_started_case_or_None).
A case that was started but never closed is the one the process died in.
"""
finished = []
current = None
if not os.path.exists(qpa_path):
return finished, None
with open(qpa_path, "r", encoding="utf-8", errors="replace") as fh:
for line in fh:
m = CASE_START.match(line)
if m:
current = m.group(1)
continue
if current is not None and (CASE_END.match(line) or CASE_TERM.match(line)):
finished.append(current)
current = None
return finished, current
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--serial", required=True)
ap.add_argument("--backend", required=True, choices=["DirectGLES", "DirectVulkan"])
ap.add_argument("--caselist", required=True)
ap.add_argument("--outdir", required=True)
ap.add_argument("--device-dir", default="/data/local/tmp/mgcts")
ap.add_argument("--surface", default="fbo", help="--deqp-surface-type value")
ap.add_argument("--max-rounds", type=int, default=4000)
ap.add_argument("--max-empty-streak", type=int, default=8,
help="abort after this many consecutive chunks that produce no log at all")
ap.add_argument("--min-mem-kb", type=int, default=400000,
help="pause when the device drops below this much available memory")
ap.add_argument("--chunk-timeout", type=int, default=900,
help="seconds before giving up on one glcts invocation (a GPU hang never returns)")
ap.add_argument("--skip-file", default=None,
help="file of case names to exclude, e.g. cases known to hang the device")
ap.add_argument("--env", action="append", default=[], metavar="K=V",
help="extra environment variable for glcts (repeatable)")
args = ap.parse_args()
os.makedirs(args.outdir, exist_ok=True)
with open(args.caselist, "r", encoding="utf-8") as fh:
remaining = [l.strip() for l in fh if l.strip() and not l.strip().startswith("#")]
skipped = []
if args.skip_file and os.path.isfile(args.skip_file):
with open(args.skip_file, "r", encoding="utf-8") as fh:
skip = {l.strip() for l in fh if l.strip() and not l.strip().startswith("#")}
skipped = [c for c in remaining if c in skip]
remaining = [c for c in remaining if c not in skip]
print(f"[run_cts] skipping {len(skipped)} case(s) from {args.skip_file}")
total = len(remaining)
print(f"[run_cts] {args.backend} on {args.serial}: {total} cases")
crashed = []
hung = []
done = set()
chunk = 0
started = time.time()
empty_streak = 0
if not wait_for_device(args.serial):
print("[run_cts] device not responding before start; aborting", file=sys.stderr)
return 3
while remaining and chunk < args.max_rounds:
listfile = os.path.join(args.outdir, "remaining.txt")
with open(listfile, "w", encoding="utf-8", newline="\n") as fh:
fh.write("\n".join(remaining) + "\n")
# Repeated process launches plus crash tombstones can drive the device
# into memory pressure; give it room rather than pushing it over.
mem = mem_available_kb(args.serial)
if mem is not None and mem < args.min_mem_kb:
print(f"[run_cts] low memory ({mem} kB available); pausing 30 s")
time.sleep(30)
dev_list = f"{args.device_dir}/remaining.txt"
dev_qpa = f"{args.device_dir}/chunk.qpa"
push = adb(args.serial, "push", listfile, dev_list, timeout=120)
if push.returncode != 0:
print(f"[run_cts] push failed ({push.stderr.strip()}); treating as device trouble",
file=sys.stderr)
if not wait_for_device(args.serial):
print("[run_cts] ABORTING: device unreachable.", file=sys.stderr)
break
continue
adb(args.serial, "shell", f"rm -f {dev_qpa}", timeout=60)
extra_env = "".join(f"{kv} " for kv in args.env)
cmd = (
f"cd {args.device_dir} && "
f"MOBILEGL_BACKEND_TYPE={args.backend} LD_LIBRARY_PATH=. {extra_env}"
f"./glcts --deqp-caselist-file={dev_list} "
f"--deqp-surface-type={args.surface} "
f"--deqp-terminate-on-device-lost=disable "
f"--deqp-log-images=disable --deqp-log-shader-sources=disable "
f"--deqp-log-filename={dev_qpa} > /dev/null 2>&1; echo RC=$?"
)
run = adb(args.serial, "shell", cmd, timeout=args.chunk_timeout)
if run.returncode == 124:
print(f"[run_cts] chunk {chunk:04d} timed out after {args.chunk_timeout}s "
f"(likely a GPU hang)", file=sys.stderr)
# Some cases hang the GPU hard enough to reboot the device. The log on
# /data/local/tmp survives that, so wait for the device to come back and
# pull it anyway rather than losing the whole chunk.
rebooted = False
if not device_alive(args.serial, timeout=30):
print(f"[run_cts] device went away during chunk {chunk:04d}; waiting for it",
file=sys.stderr)
if not wait_for_device(args.serial, attempts=40, delay=15):
print("[run_cts] ABORTING: device never came back. Results are incomplete; "
"do NOT treat the remaining cases as failures.", file=sys.stderr)
break
rebooted = True
print("[run_cts] device is back")
local_qpa = os.path.join(args.outdir, f"chunk{chunk:04d}.qpa")
pull = adb(args.serial, "pull", dev_qpa, local_qpa, timeout=300)
if pull.returncode != 0 and rebooted:
time.sleep(10)
adb(args.serial, "pull", dev_qpa, local_qpa, timeout=300)
finished, in_flight = completed_cases(local_qpa)
for c in finished:
done.add(c)
progressed = len(finished)
if progressed > 0:
empty_streak = 0
if in_flight is not None:
# The case that was open when the process (or the device) died.
if rebooted:
# It took the whole device down: quarantine it, or the next
# invocation walks straight back into it.
print(f"[run_cts] DEVICE HANG in {in_flight} - quarantining it")
hung.append(in_flight)
else:
crashed.append(in_flight)
done.add(in_flight)
progressed += 1
elif progressed == 0:
# Nothing at all came back. Either the first remaining case takes
# the process down before the log is flushed, or the device died.
# Those look identical from here, so confirm the device is alive
# before blaming the test.
if not device_alive(args.serial):
print(f"[run_cts] device went away during chunk {chunk:04d}", file=sys.stderr)
if not wait_for_device(args.serial):
print("[run_cts] ABORTING: device never came back. Results are "
"incomplete; do NOT treat the remaining cases as crashes.", file=sys.stderr)
break
print("[run_cts] device recovered; retrying the same chunk")
continue
empty_streak += 1
if empty_streak >= args.max_empty_streak:
print(f"[run_cts] ABORTING: {empty_streak} consecutive chunks produced no output "
f"while the device stayed reachable. Something systemic is wrong; refusing "
f"to label the rest of the suite as crashes.", file=sys.stderr)
break
victim = remaining[0]
print(f"[run_cts] no output at all; recording {victim} as Crash")
crashed.append(victim)
done.add(victim)
progressed = 1
remaining = [c for c in remaining if c not in done]
elapsed = time.time() - started
print(
f"[run_cts] chunk {chunk:04d}: +{progressed} (done {len(done)}/{total}, "
f"crashes {len(crashed)}, {elapsed / 60:.1f} min)"
)
chunk += 1
with open(os.path.join(args.outdir, "crashed.txt"), "w", encoding="utf-8", newline="\n") as fh:
fh.write("\n".join(crashed) + ("\n" if crashed else ""))
# Cases that rebooted the device. Feed this back in via --skip-file to avoid
# paying for the same reboot on the next run.
with open(os.path.join(args.outdir, "hung.txt"), "w", encoding="utf-8", newline="\n") as fh:
fh.write("\n".join(hung) + ("\n" if hung else ""))
if hung:
print(f"[run_cts] {len(hung)} case(s) hung the device (see hung.txt):")
for c in hung:
print(f" {c}")
# Anything still in `remaining` was never measured. Record it so the report
# cannot quietly present a partial run as a complete one.
with open(os.path.join(args.outdir, "unrun.txt"), "w", encoding="utf-8", newline="\n") as fh:
fh.write("\n".join(remaining) + ("\n" if remaining else ""))
if skipped:
with open(os.path.join(args.outdir, "skipped.txt"), "w", encoding="utf-8", newline="\n") as fh:
fh.write("\n".join(skipped) + "\n")
if remaining:
print(f"[run_cts] WARNING: {len(remaining)} cases were never run (see unrun.txt)", file=sys.stderr)
print(f"[run_cts] finished: {len(done)}/{total} cases, {len(crashed)} crashes, {chunk} invocations")
print(f"[run_cts] qpa chunks in {args.outdir}")
return 0
if __name__ == "__main__":
sys.exit(main())
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env python
"""Copy the MobileGL dEQP platform port into a VK-GL-CTS checkout.
The port is version-controlled here, in the MobileGL repo, so it survives a
throwaway CTS clone. This drops it into the places VK-GL-CTS expects:
framework/platform/mobilegl/ <- platform sources
targets/mobilegl/mobilegl.cmake <- target definition (-DDEQP_TARGET=mobilegl)
Usage:
python sync_to_cts.py <path-to-VK-GL-CTS>
"""
import os
import shutil
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
CTS_TOOLS = os.path.dirname(HERE)
COPIES = [
(os.path.join(CTS_TOOLS, "platform"), "framework/platform/mobilegl", None),
(os.path.join(CTS_TOOLS, "targets"), "targets/mobilegl", ["mobilegl.cmake", "ndk-modern.cmake"]),
]
def main():
if len(sys.argv) != 2:
print(__doc__)
return 2
cts = sys.argv[1]
if not os.path.isfile(os.path.join(cts, "CMakeLists.txt")):
print(f"error: {cts} does not look like a VK-GL-CTS checkout", file=sys.stderr)
return 1
for src, reldst, only in COPIES:
dst = os.path.join(cts, reldst)
os.makedirs(dst, exist_ok=True)
for name in sorted(os.listdir(src)):
if only is not None and name not in only:
continue
s = os.path.join(src, name)
if not os.path.isfile(s):
continue
shutil.copy2(s, os.path.join(dst, name))
print(f" {reldst}/{name}")
print("\nsynced. configure with -DDEQP_TARGET=mobilegl")
return 0
if __name__ == "__main__":
sys.exit(main())
+18
View File
@@ -0,0 +1,18 @@
# MobileGL conformance-suite skills
Task-focused skills for running Khronos conformance suites against MobileGL.
Each skill is a self-contained package, matching the layout used by
`tools/trace_replay/skills/`:
- `SKILL.md` — the skill (frontmatter `name` + `description`, then the body). The
directory name equals the frontmatter `name`.
- `agents/openai.yaml` — OpenAI agent descriptor (`display_name`,
`short_description`, `default_prompt`).
- `scripts/` and/or `references/` — bundled tooling and supporting docs, when the
skill has them.
## Skills
| Skill | What it does |
| --- | --- |
| [gl-cts-on-mobilegl](gl-cts-on-mobilegl/SKILL.md) | Build VK-GL-CTS `glcts` as a standalone Android arm64 binary against MobileGL's own EGL, run KHR-GL33, and report a per-backend OpenGL 3.3 core conformance rate. |
@@ -0,0 +1,214 @@
---
name: gl-cts-on-mobilegl
description: Run the Khronos OpenGL CTS (VK-GL-CTS glcts, KHR-GL33) against MobileGL on an Android device and compute a per-backend conformance rate. Use when measuring OpenGL 3.3 core conformance for DirectGLES or DirectVulkan, building glcts for Android arm64, porting a dEQP tcu::Platform onto MobileGL, or triaging CTS failures, crashes, and cases that hang the device.
---
# OpenGL CTS on MobileGL (Android)
## Overview
`glcts` from VK-GL-CTS is built as a **standalone arm64 executable** and run from
`adb shell`. It reaches OpenGL only through `libMobileGL.so`, which supplies both
EGL and desktop GL, so a result is unambiguously MobileGL's and never the system
GL stack's. No APK and no Activity are involved.
The port lives in this repository under `MobileGL/tools/cts/` and is copied into
a VK-GL-CTS checkout by `scripts/sync_to_cts.py`, so it survives a throwaway CTS
clone.
Set up paths first:
```sh
export MG=<path-to-MobileGL-worktree> # do builds in a worktree, not the shared tree
export CTS=<path-to-VK-GL-CTS-checkout>
export NDK="$ANDROID_HOME/ndk/27.3.13750724"
export SERIAL=<adb-device-serial>
```
## Prerequisites
- Android NDK r27 (the repo builds MobileGL with 27.3.13750724), CMake, Ninja, Python 3.
- A rooted-or-not Android device with `adb`; ~600 MB free under `/data/local/tmp`.
- **A device you can physically power-cycle.** Some cases hang the GPU hard
enough to reboot it — see "Cases that take the device down".
- On Windows, invoke `python`, not `python3`: the latter resolves to the
Microsoft Store alias stub and exits 49.
## Step 1 — build libMobileGL.so
Build in a git worktree (other agents share the main tree). A fresh worktree is
missing glslang's bundled SPIR-V Tools, which is a hard configure blocker
because `ENABLE_OPT` is forced on:
```sh
cp -r <main-tree>/3rdparty/glslang/External/* "$MG/3rdparty/glslang/External/"
./gradlew -p "$MG/android-plugin" :app:assembleTraceRelease
```
The stripped library lands in
`android-plugin/app/build/intermediates/stripped_native_libs/traceRelease/.../arm64-v8a/libMobileGL.so`.
## Step 2 — get VK-GL-CTS and its externals
Use a **release tag**, not `main`, so the mustpass list — and therefore the
reported rate — is citable:
```sh
git -C "$CTS" checkout opengl-cts-4.6.8.1
cd "$CTS" && python external/fetch_sources.py
```
## Step 3 — build glcts for Android arm64
```sh
python "$MG/tools/cts/scripts/sync_to_cts.py" "$CTS"
cmake -S "$CTS" -B build-cts-a64 -G Ninja \
-DDEQP_TARGET=mobilegl -DDEQP_TARGET_TOOLCHAIN=ndk-modern \
-DANDROID_NDK_PATH="$NDK" -DDE_ANDROID_API=26 -DANDROID_ABI=arm64-v8a \
-DCMAKE_BUILD_TYPE=Release
ninja -C build-cts-a64 glcts
"$NDK"/toolchains/llvm/prebuilt/*/bin/llvm-strip build-cts-a64/external/openglcts/modules/glcts
```
Confirm the configure output says `DE_OS = DE_OS_ANDROID`, `DE_CPU =
DE_CPU_ARM_64` and `DEQP_ANDROID_BUILD = EXE`. Two things make that work and
both are easy to get wrong:
- `DEQP_TARGET_TOOLCHAIN=ndk-modern` is required. dEQP includes `Defs.cmake`
*before* the target file, so a target cannot set `DE_OS` itself. Without the
toolchain hook the build mis-detects as `DE_OS_UNIX`/`x86_64` and dies on
`__assert_fail` (bionic has `__assert2`).
- The target sets `DEQP_ANDROID_EXE ON`. Otherwise dEQP builds the modules into
the `libdeqp.so` an APK would load and no `glcts` executable exists.
`KHR-GL33` needs no ungating — the package registry registers it unconditionally;
only the `dEQP-*` packages are `#if DE_OS != DE_OS_ANDROID`.
## Step 4 — deploy
```sh
adb -s $SERIAL shell mkdir -p /data/local/tmp/mgcts
adb -s $SERIAL push build-cts-a64/external/openglcts/modules/glcts /data/local/tmp/mgcts/
adb -s $SERIAL push build-cts-a64/external/openglcts/modules/gl_cts /data/local/tmp/mgcts/
adb -s $SERIAL push <libMobileGL.so> /data/local/tmp/mgcts/
adb -s $SERIAL shell chmod 755 /data/local/tmp/mgcts/glcts
```
## Step 5 — preflight
Never start a multi-hour run without this. It proves the device/library pair
yields a 3.3 core context and that FBO readback is correct, in about a second:
```sh
adb -s $SERIAL shell 'cd /data/local/tmp/mgcts && LD_LIBRARY_PATH=. ./mgprobe \
--backend DirectVulkan --surface imagereader --lib ./libMobileGL.so'
```
Expect `PASS ... user_fbo=ok`. `default_fb=broken` on DirectVulkan is expected
and does not gate — see below.
## Step 6 — run
```sh
python "$MG/tools/cts/scripts/run_cts.py" \
--serial $SERIAL --backend DirectGLES \
--caselist .../mustpass/gl/khronos_mustpass/main/gl33-main.txt \
--outdir runs/gles --skip-file runs/skip.txt
```
The runner re-invokes `glcts` with only the cases that have no result yet, so a
crash costs one case rather than the run. It distinguishes a crashed *case* from
a dead *device* by checking the device still answers a shell command — without
that check a dead device looks like every remaining case crashing, which yields
a completely bogus but plausible-looking conformance number. On a device reboot
it waits, re-pulls the partial `.qpa` (which survives on `/data/local/tmp`),
records the case that was open as `DeviceHang`, and quarantines it.
## Step 7 — report
```sh
python "$MG/tools/cts/scripts/qpa_report.py" runs/gles --label DirectGLES
```
Pass rate counts `Pass`, `NotSupported`, `QualityWarning`, `CompatibilityWarning`
and `Waiver` as non-failures, matching how Khronos scores a submission; the
strict rate counts only `Pass`. Quarantined and never-reached cases are reported
separately and excluded from the rates, so a partial run cannot read as a
complete one.
## Required flags, and why
| Flag | Why it is not optional |
| --- | --- |
| `--deqp-surface-type=fbo` | On DirectVulkan, `glReadPixels` from the **default framebuffer returns all zeros** with no GL error. dEQP verifies nearly everything through `glReadPixels`, so rendering to the surface scores DirectVulkan near zero for a reason unrelated to conformance. Use it for **both** backends so the two numbers stay comparable. |
| `MOBILEGL_CTS_FBO_COLOR_TEXTURE=1` | **`--deqp-surface-type=fbo` alone is not enough.** dEQP's `FboRenderContext` allocates a *renderbuffer* colour attachment, and DirectVulkan returns zeros from a renderbuffer-attached FBO too — only a *texture*-attached FBO reads back correctly. This env var (a patch to `framework/opengl/gluFboRenderContext.cpp`, off by default) switches the attachment to a texture and isolates that single defect. Measured effect: `KHR-GL33.shaders.loops.for_constant_iterations.*` goes 0/62 → 62/62, and the whole-suite DirectVulkan conformance rate goes 46.15% → 72.74%. DirectGLES is bit-identical either way (93.05%), which is the control proving the switch is neutral where readback works. |
| `--deqp-terminate-on-device-lost=disable` | Defaults to *enable*, which calls `glGetGraphicsResetStatus()` after every case. That is GL 4.5 / `KHR_robustness`, absent from GL 3.3 core, so the pointer is null and the process segfaults on the first case. Desktop drivers expose the extension, which is why upstream never trips on it. |
## Cases that take the device down
Some cases hang the GPU hard enough that the device reboots or stops answering
adb entirely. Keep them in a `--skip-file`, and expect to find more:
- `KHR-GL33.clip_distance.functional` — wedged an Adreno 750 tablet; it rebooted
and then stopped responding to adb altogether.
- `KHR-GL33.framebuffer_blit.multisampled_to_singlesampled_blit_color_config_test`
— rebooted an Adreno 830 phone after 862 cases, on DirectGLES.
- `KHR-GL33.framebuffer_blit.multisampled_to_singlesampled_blit_depth_config_test`
— same, on both backends (found and quarantined automatically by the runner).
- `KHR-GL33.texture_repeat_mode.rgb565_11x131_0_clamp_to_edge` — on DirectVulkan.
The whole `framebuffer_blit.multisampled_to_singlesampled_*` family is suspect;
treat a new variant as a device-hang candidate rather than a normal failure.
When a run dies, pull `/data/local/tmp/mgcts/chunk.qpa` — it survives the reboot,
and the last `#beginTestCaseResult` with no matching `#endTestCaseResult` names
the case that did it.
## Reference results
`opengl-cts-4.6.8.1`, KHR-GL33 mustpass (`gl33-main.txt`, 9886 cases), Adreno 830
/ Android 15, MobileGL `dev`@199164c2, 9884 measured / 0 unrun / 2 quarantined.
Conformance rate = Pass + NotSupported, as Khronos scores a submission.
| backend | conformance | strict Pass | Fail | Crash | InternalError | DeviceHang |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| DirectGLES | **93.05%** | 85.94% | 679 | 1 | 6 | 1 |
| DirectVulkan (texture FBO) | **72.74%** | 65.71% | 2394 | 294 | 5 | 1 |
| DirectVulkan (stock renderbuffer FBO) | 46.15% | 39.11% | 5024 | 292 | 5 | 2 |
The third row is what stock dEQP reports; the gap to the second row is entirely
the renderbuffer-FBO readback defect.
## MobileGL constraints the port works around
- **DirectVulkan cannot use an EGL pbuffer.** That path needs
`VK_EXT_headless_surface`, which Adreno's Android driver does not expose; it
fails inside `eglMakeCurrent`. The platform therefore gets a real
`ANativeWindow` from **`AImageReader`** — an ordinary BufferQueue producer that
`vkCreateAndroidSurfaceKHR` accepts, with no Activity. An `onImageAvailable`
listener must drain the queue or the producer blocks once `maxImages` buffers
are in flight and the next swap deadlocks.
- **`eglMakeCurrent` requires draw == read** and rejects `EGL_NO_SURFACE` with
`EGL_BAD_MATCH`, so dEQP's `surfaceless` platform cannot be used at all, and
`--deqp-surface-type=fbo` (which asks the platform for `SURFACETYPE_DONT_CARE`)
must still be given a real surface.
- **Every EGL call must go through the dynamically loaded library.** dEQP's
`surfaceless` platform mixes wrapper calls with globally linked `egl*` symbols;
copying that on Android silently reaches the system EGL and invalidates the
measurement. The `mobilegl` target links no `libEGL`/`libGLESv*` at all.
- **Desktop-GL configs need `EGL_OPENGL_BIT`.** The surfaceless port always asks
for an ES bit, which can never satisfy a GL 3.3 core context.
- MobileGL aborts during static teardown (`FORTIFY: pthread_mutex_lock called on
a destroyed mutex`) *after* the work is done; flush and `_exit()` in any small
tool, or its exit code and output are lost.
## Contents
platform/tcuMobileGLPlatform.{cpp,hpp} dEQP tcu::Platform for MobileGL
targets/mobilegl.cmake VK-GL-CTS target (-DDEQP_TARGET=mobilegl)
targets/ndk-modern.cmake NDK toolchain hook (sets DE_OS/DE_CPU)
probe/mgprobe.c preflight gate
scripts/sync_to_cts.py inject the port into a CTS checkout
scripts/run_cts.py crash- and reboot-resuming runner
scripts/qpa_report.py .qpa -> conformance rate
@@ -0,0 +1,4 @@
interface:
display_name: "OpenGL CTS on MobileGL (Android)"
short_description: "Build and run VK-GL-CTS KHR-GL33 against MobileGL and report per-backend conformance"
default_prompt: "Use $gl-cts-on-mobilegl to run the OpenGL 3.3 core CTS against MobileGL on my Android device and report the conformance rate for DirectGLES and DirectVulkan."
+36
View File
@@ -0,0 +1,36 @@
#-------------------------------------------------------------------------
# VK-GL-CTS target: MobileGL on Android
#
# Builds a standalone arm64 ELF that reaches OpenGL exclusively through
# libMobileGL.so, loaded at runtime. Nothing here links libEGL or libGLESv*:
# the whole point is that the system GL stack must not be reachable, so that a
# conformance result is unambiguously MobileGL's.
#-------------------------------------------------------------------------
message("*** Using MobileGL target")
set(DEQP_TARGET_NAME "MobileGL")
# Build the modules as standalone executables instead of the libdeqp.so an APK
# would load. The suite runs from adb shell, with no Activity.
set(DEQP_ANDROID_EXE ON)
# EGL comes from libMobileGL.so via the eglw dynamic wrapper, so the support
# flag is on but no import library is supplied.
set(DEQP_SUPPORT_EGL ON)
set(DEQP_EGL_LIBRARIES)
set(DEQP_GLES2_LIBRARIES)
set(DEQP_GLES3_LIBRARIES)
set(TCUTIL_PLATFORM_SRCS
mobilegl/tcuMobileGLPlatform.cpp
mobilegl/tcuMobileGLPlatform.hpp
)
find_library(LOG_LIBRARY NAMES log)
find_library(ANDROID_LIBRARY NAMES android)
find_library(MEDIANDK_LIBRARY NAMES mediandk)
# libmediandk supplies AImageReader, which is how a process with no Activity
# gets a real ANativeWindow.
list(APPEND TCUTIL_PLATFORM_LIBS ${ANDROID_LIBRARY} ${MEDIANDK_LIBRARY} ${LOG_LIBRARY})
+61
View File
@@ -0,0 +1,61 @@
#-------------------------------------------------------------------------
# drawElements CMake utilities
# ----------------------------
#
# Copyright 2016 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#-------------------------------------------------------------------------
# Delegate most things to the NDK's cmake toolchain script
if (NOT DEFINED ANDROID_NDK_PATH)
message(FATAL_ERROR "Please provide ANDROID_NDK_PATH")
endif ()
set(ANDROID_PLATFORM "android-${DE_ANDROID_API}")
set(ANDROID_STL c++_static)
set(ANDROID_CPP_FEATURES "rtti exceptions")
include(${ANDROID_NDK_PATH}/build/cmake/android.toolchain.cmake)
# The try_compile() used to verify the C/C++ compilers are sane tries to
# generate an executable, but doesn't seem to use the right compiler/linker
# options when cross-compiling, so it fails even when building an actual
# shared library or executable succeeds.
#
# I don't know why this doesn't affect simpler projects that use the NDK
# toolchain.
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
# Set variables used by other parts of dEQP's build scripts
set(DE_OS "DE_OS_ANDROID")
if (NOT DEFINED DE_COMPILER)
set(DE_COMPILER "DE_COMPILER_CLANG")
endif ()
if (ANDROID_ABI STREQUAL "x86")
set(DE_CPU "DE_CPU_X86")
elseif (ANDROID_ABI STREQUAL "armeabi" OR
ANDROID_ABI STREQUAL "armeabi-v7a")
set(DE_CPU "DE_CPU_ARM")
elseif (ANDROID_ABI STREQUAL "arm64-v8a")
set(DE_CPU "DE_CPU_ARM_64")
elseif (ANDROID_ABI STREQUAL "x86_64")
set(DE_CPU "DE_CPU_X86_64")
else ()
message(FATAL_ERROR "Unknown ABI \"${ANDROID_ABI}\"")
endif ()
+157
View File
@@ -0,0 +1,157 @@
# piglit on Android against MobileGL
Run [piglit](https://gitlab.freedesktop.org/mesa/piglit) desktop-GL tests on a
connected Android device with **MobileGL as the OpenGL implementation**, for
both backends:
- `DirectGLES` — MobileGL over the system GLES driver (or ANGLE with
`--use-angle`)
- `DirectVulkan` — MobileGL over the system Vulkan driver
No APK and no on-device python: piglit test binaries run as the adb shell user
from `/data/local/tmp`, create contexts through waffle's `surfaceless_egl`
platform, and waffle is pointed at `libMobileGL.so` (which exports the full
`egl*`/`gl*` API under real names).
## How it fits together
```
piglit test binary (aarch64, bionic)
└─ waffle surfaceless_egl (patched)
├─ WAFFLE_EGL_LIBRARY=libMobileGL.so → dlopen MobileGL as the EGL impl
├─ WAFFLE_GL_LIBRARY=libMobileGL.so → waffle_dl_sym resolves gl* here
├─ WAFFLE_FORCE_GL_CONTEXT_VERSION=33core
│ upgrades low compat context requests to GL 3.3 core (never
│ downgrades) so piglit's supports_gl_compat_version=10 tests run
└─ WAFFLE_ANDROID_WINDOW=imagereader (DirectVulkan only)
windows are AImageReader ANativeWindows instead of EGL pbuffers,
because Android ICDs lack VK_EXT_headless_surface which the
MobileGL pbuffer path needs
└─ libMobileGL.so
├─ DirectGLES: dlopens the real system libEGL.so internally
└─ DirectVulkan: links libvulkan.so
```
Key rule: **never name MobileGL `libEGL.so`** anywhere on `LD_LIBRARY_PATH`
the DirectGLES backend loads the system driver with a bare-soname
`dlopen("libEGL.so")` and would recursively pick itself up.
## One-time setup (host: macOS/Linux with the Android NDK)
```sh
WORK=path/to/workdir && cd $WORK
git clone --depth 1 https://gitlab.freedesktop.org/mesa/piglit.git
git clone --depth 1 https://gitlab.freedesktop.org/mesa/waffle.git
git -C waffle apply $MOBILEGL/tools/piglit-android/patches/waffle-mobilegl-android.patch
git -C piglit apply $MOBILEGL/tools/piglit-android/patches/piglit-mobilegl-android.patch
python3 -m venv venv && ./venv/bin/pip install mako numpy packaging
```
The piglit patch matters beyond build fixes: upstream's
`piglit_dispatch_default_init` runs while the waffle framework is still being
constructed (`gl_fw` is NULL), so the waffle resolvers were never installed and
GL functions bound through the **system** libEGL's `eglGetProcAddress` — every
test silently ran on the raw GLES driver instead of MobileGL.
Build MobileGL for Android:
```sh
cmake -S $MOBILEGL -B $MOBILEGL/build-android-arm64 -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-26 \
-DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build $MOBILEGL/build-android-arm64 --target MobileGL -j
$NDK/toolchains/llvm/prebuilt/*/bin/llvm-strip --strip-unneeded \
-o $WORK/libMobileGL-stripped.so $MOBILEGL/build-android-arm64/libMobileGL.so
```
Cross-build waffle (meson; a cross file and a stub `egl.pc` pointing at
MobileGL's bundled EGL 1.5 headers are needed — see `cross-example/`):
```sh
cd $WORK/waffle
meson setup build-android --cross-file $WORK/cross/android-arm64.ini \
-Dbuildtype=release -Dsurfaceless_egl=enabled \
-Dglx=disabled -Dx11_egl=disabled -Dgbm=disabled -Dwayland=disabled \
-Dbuild-tests=false -Dbuild-examples=false -Dprefix=$WORK/prefix
ninja -C build-android && meson install -C build-android
```
Cross-build piglit (needs `PKG_CONFIG_LIBDIR` with the installed `waffle-1.pc`
plus the stub `egl.pc`):
```sh
cd $WORK/piglit && export PKG_CONFIG_LIBDIR=$WORK/prefix/lib/pkgconfig:$WORK/cross/pkgconfig
cmake -S . -B build-android -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-26 \
-DCMAKE_BUILD_TYPE=Release \
-DPIGLIT_USE_WAFFLE=ON -DPIGLIT_BUILD_GL_TESTS=ON \
-DPIGLIT_BUILD_GLES1_TESTS=OFF -DPIGLIT_BUILD_GLES2_TESTS=OFF \
-DPIGLIT_BUILD_GLES3_TESTS=OFF -DPIGLIT_BUILD_EGL_TESTS=OFF \
-DPIGLIT_BUILD_GLX_TESTS=OFF -DPIGLIT_BUILD_WGL_TESTS=OFF \
-DPIGLIT_BUILD_CL_TESTS=OFF -DPIGLIT_BUILD_VK_TESTS=OFF \
-DPIGLIT_BUILD_DMA_BUF_TESTS=OFF -DPIGLIT_USE_GBM=OFF \
-DPIGLIT_USE_WAYLAND=OFF -DPIGLIT_USE_X11=OFF \
-DPYTHON_EXECUTABLE=$WORK/venv/bin/python \
-DOPENGL_INCLUDE_DIR=$MOBILEGL/include \
-DOPENGL_gl_LIBRARY=$SYSROOT/usr/lib/aarch64-linux-android/26/libEGL.so \
-DGLEXT_INCLUDE_DIR=$MOBILEGL/include
ninja -C build-android
```
## Selecting tests
Enumerate on the host with piglit's own profiles (no device needed):
```sh
cd $WORK/piglit
for prof in opengl shader glslparser; do
PIGLIT_BUILD_DIR=$PWD/build-android ./venv/bin/python ./piglit print-cmd \
-t "spec@!opengl 3[.]" -t "spec@glsl-3[.]30" $prof
done > /tmp/gl33.list
```
Group names use `@` separators (`spec@!opengl 3.3@minmax`). The version groups
(`spec@!opengl 1.x…3.3`), GLSL groups (`spec@glsl-1.10…3.30`) plus the ARB
extension groups folded into GL 3.13.3 core give a comprehensive "GL 3.3
core" suite (~15k tests).
## Running
```sh
python3 $MOBILEGL/tools/piglit-android/run_piglit_android.py \
--piglit-root $WORK/piglit --list /tmp/gl33.list \
--backend DirectGLES \
--mobilegl-lib $WORK/libMobileGL-stripped.so \
--waffle-lib $WORK/waffle/build-android/src/waffle/libwaffle-1.so \
--out results-gles
# then the same with --backend DirectVulkan --out results-vk
```
The runner pushes binaries/libs/data (incremental; `--repush` forces), executes
tests serially in chunked on-device shell scripts under `timeout`, parses the
`PIGLIT: {...}` result lines, and writes `results.json` + `summary.txt` +
`raw.log`. Exit-code semantics: parsed result wins; nonzero exit without a
result line = `crash`; toybox timeout exits = `timeout`.
Quick sanity check for the whole stack (waffle build also produces `wflinfo`):
```sh
adb shell 'cd /data/local/tmp/piglit-mgl && env LD_LIBRARY_PATH=$PWD/lib \
WAFFLE_EGL_LIBRARY=libMobileGL.so WAFFLE_GL_LIBRARY=libMobileGL.so \
MOBILEGL_BACKEND_TYPE=DirectVulkan WAFFLE_ANDROID_WINDOW=imagereader \
./wflinfo --platform surfaceless_egl --api gl --version 3.3 --profile core'
```
Expect `OpenGL version string: 3.3.0 MobileGL …, Direct (Vulkan) Backend`.
## Known caveats
- MSAA winsys configs never match (MobileGL exposes two RGBA8888 configs,
samples=0); MSAA FBO tests are unaffected.
- Tests that genuinely require compatibility-profile features will fail on the
forced 3.3 core context; that is honest for a core-only implementation.
- `eglTerminate` at test exit now tears MobileGL down deterministically (see
the EGL-lifecycle refactor); a device-side `mobilegl.log` is written per run
directory for debugging.
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
# MobileGL - tools/piglit-android/compare_results.py
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
"""Compare two run_piglit_android.py results.json files (e.g. DirectGLES vs
DirectVulkan) and write a markdown report of totals plus categorized diffs."""
import argparse
import json
from collections import Counter
from pathlib import Path
BAD = ('crash', 'timeout', 'fail', 'missing', 'notrun', 'warn')
def load(path):
d = json.loads(Path(path).read_text())
return d
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('a', help='first results.json')
ap.add_argument('b', help='second results.json')
ap.add_argument('-o', '--out', help='markdown output path')
args = ap.parse_args()
da, db = load(args.a), load(args.b)
na, nb = da['backend'], db['backend']
ta, tb = da['tests'], db['tests']
names = sorted(set(ta) | set(tb))
lines = [f'# piglit: {na} vs {nb}', '']
lines.append(f'| | {na} | {nb} |')
lines.append('|---|---|---|')
ca = Counter(o["status"] for o in ta.values())
cb = Counter(o["status"] for o in tb.values())
for k in sorted(set(ca) | set(cb)):
lines.append(f'| {k} | {ca.get(k, 0)} | {cb.get(k, 0)} |')
lines.append(f'| total | {len(ta)} | {len(tb)} |')
lines.append(f'| elapsed | {da.get("elapsed_sec")}s | {db.get("elapsed_sec")}s |')
lines.append('')
def bucket(pred, title):
rows = [n for n in names
if pred(ta.get(n, {}).get('status', 'absent'),
tb.get(n, {}).get('status', 'absent'))]
if rows:
lines.append(f'## {title} ({len(rows)})')
lines.append('')
for n in rows:
sa = ta.get(n, {}).get('status', 'absent')
sb = tb.get(n, {}).get('status', 'absent')
lines.append(f'- `{n}` — {na}: {sa}, {nb}: {sb}')
lines.append('')
bucket(lambda a, b: a in BAD and b in BAD, 'Bad on both (likely frontend/state-tracker)')
bucket(lambda a, b: a in BAD and b == 'pass', f'Bad only on {na}')
bucket(lambda a, b: a == 'pass' and b in BAD, f'Bad only on {nb}')
bucket(lambda a, b: a == 'skip' and b == 'pass' or a == 'pass' and b == 'skip',
'Skip on one side only')
text = '\n'.join(lines) + '\n'
if args.out:
Path(args.out).write_text(text)
print(f'wrote {args.out}')
else:
print(text)
if __name__ == '__main__':
main()
@@ -0,0 +1,19 @@
; meson cross file for waffle -> aarch64 Android
; Replace NDK_TOOLCHAIN with e.g.
; $HOME/Library/Android/sdk/ndk/27.3.13750724/toolchains/llvm/prebuilt/darwin-x86_64
; and PKGCONFIG_DIR with the directory holding the stub egl.pc.
[binaries]
c = 'NDK_TOOLCHAIN/bin/aarch64-linux-android26-clang'
cpp = 'NDK_TOOLCHAIN/bin/aarch64-linux-android26-clang++'
ar = 'NDK_TOOLCHAIN/bin/llvm-ar'
strip = 'NDK_TOOLCHAIN/bin/llvm-strip'
pkg-config = '/usr/bin/pkg-config'
[host_machine]
system = 'android'
cpu_family = 'aarch64'
cpu = 'aarch64'
endian = 'little'
[properties]
pkg_config_libdir = 'PKGCONFIG_DIR'
@@ -0,0 +1,8 @@
# Stub egl.pc for cross-building waffle/piglit against the NDK.
# The NDK's own EGL headers are 1.4-era; point Cflags at MobileGL's bundled
# EGL 1.5 headers (include/EGL) instead. -lEGL resolves to the NDK stub.
Name: egl
Description: EGL 1.5 headers (MobileGL bundled) + NDK libEGL stub
Version: 1.5
Libs: -lEGL
Cflags: -IMOBILEGL_REPO/include
@@ -0,0 +1,104 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1a38c21..b0b6ce2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -22,7 +22,7 @@ INCLUDE (FindPkgConfig)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
-if(${CMAKE_SYSTEM_NAME} MATCHES "Linux|FreeBSD")
+if(${CMAKE_SYSTEM_NAME} MATCHES "Linux|FreeBSD|Android")
set(DEFAULT_EGL ON)
set(DEFAULT_GLX ON)
set(DEFAULT_WGL OFF)
@@ -499,13 +499,18 @@ if(GBM_FOUND)
endif(HAVE_LIBCACA)
endif(GBM_FOUND)
-if(PIGLIT_BUILD_EGL_TESTS)
- pkg_check_modules(EGL REQUIRED egl)
+# EGL *support* (PIGLIT_HAS_EGL, e.g. the surfaceless_egl waffle platform) is
+# independent of building the EGL test binaries, which additionally need X11.
+pkg_check_modules(EGL egl)
+if(EGL_FOUND)
set(PIGLIT_HAS_EGL True)
add_definitions(-DPIGLIT_HAS_EGL)
include_directories(${EGL_INCLUDE_DIRS})
add_definitions (${EGL_CFLAGS_OTHER})
endif()
+if(PIGLIT_BUILD_EGL_TESTS AND NOT EGL_FOUND)
+ message(FATAL_ERROR "PIGLIT_BUILD_EGL_TESTS requires EGL")
+endif()
# Put all executables into the bin subdirectory
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${piglit_BINARY_DIR}/bin)
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index e3c964a..36e2109 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -35,9 +35,9 @@ add_subdirectory (llvmpipe)
add_subdirectory (perf)
add_subdirectory (wgl)
-IF(EGL_FOUND)
+IF(PIGLIT_BUILD_EGL_TESTS)
add_subdirectory (egl)
-ENDIF(EGL_FOUND)
+ENDIF(PIGLIT_BUILD_EGL_TESTS)
IF(PIGLIT_BUILD_CL_TESTS)
add_subdirectory (cl)
diff --git a/tests/util/piglit-dispatch-init.c b/tests/util/piglit-dispatch-init.c
index 3d9772d..e036eac 100644
--- a/tests/util/piglit-dispatch-init.c
+++ b/tests/util/piglit-dispatch-init.c
@@ -322,14 +322,19 @@ piglit_dispatch_default_init(piglit_dispatch_api api)
break;
}
- if (gl_fw) {
- piglit_dispatch_init(api,
- get_wfl_core_proc,
- get_wfl_ext_proc,
- default_unsupported,
- default_get_proc_address_failure);
- } else
-#endif
+ /* MobileGL piglit harness: always use the waffle resolvers in waffle
+ * builds. This function runs while the waffle framework is still being
+ * constructed, so gl_fw is NULL here and the gl_fw branch never fired;
+ * the fallback resolvers bind GL functions via the SYSTEM libEGL's
+ * eglGetProcAddress (a DT_NEEDED symbol), which on Android is the raw
+ * GLES driver rather than the waffle-selected EGL implementation.
+ */
+ piglit_dispatch_init(api,
+ get_wfl_core_proc,
+ get_wfl_ext_proc,
+ default_unsupported,
+ default_get_proc_address_failure);
+#else
{
piglit_dispatch_init(api,
@@ -338,6 +343,7 @@ piglit_dispatch_default_init(piglit_dispatch_api api)
default_unsupported,
default_get_proc_address_failure);
}
+#endif
already_initialized = true;
}
diff --git a/tests/util/piglit-util-gl.c b/tests/util/piglit-util-gl.c
index 16eb425..7f2796c 100644
--- a/tests/util/piglit-util-gl.c
+++ b/tests/util/piglit-util-gl.c
@@ -47,6 +47,9 @@ bool piglit_is_core_profile;
bool piglit_is_gles(void)
{
const char *version_string = (const char *) glGetString(GL_VERSION);
+ if (getenv("PIGLIT_DEBUG_VERSION_STRING"))
+ printf("piglit: debug: GL_VERSION = \"%s\"\n",
+ version_string ? version_string : "(null)");
return strncmp("OpenGL ES", version_string, 9) == 0;
}
@@ -0,0 +1,445 @@
diff --git a/meson.build b/meson.build
index 7060fce..dcd45ed 100644
--- a/meson.build
+++ b/meson.build
@@ -78,7 +78,11 @@ else
dep_egl = dependency('egl', required : get_option('surfaceless_egl'))
build_surfaceless = dep_egl.found()
- dep_egl = dependency('egl', required : get_option('wayland'))
+ # Don't clobber a dep_egl found for surfaceless_egl when wayland is
+ # disabled (a disabled-feature dependency() returns not-found).
+ if not dep_egl.found()
+ dep_egl = dependency('egl', required : get_option('wayland'))
+ endif
dep_wayland_client = dependency(
'wayland-client', version : '>= 1.10', required : get_option('wayland'),
)
diff --git a/src/waffle/egl/wegl_context.c b/src/waffle/egl/wegl_context.c
index 26362a9..a26c27f 100644
--- a/src/waffle/egl/wegl_context.c
+++ b/src/waffle/egl/wegl_context.c
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: BSD-2-Clause
#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>
@@ -19,6 +21,25 @@
#define EGL_CONTEXT_OPENGL_ROBUST_ACCESS 0x31B2
#endif
+// MobileGL piglit harness: WAFFLE_FORCE_GL_CONTEXT_VERSION="33core" upgrades
+// every desktop-GL context request to at least that version/profile (never
+// downgrades). MobileGL implements exactly GL 3.3 core; piglit tests that ask
+// for a low compat context (supports_gl_compat_version=10) would otherwise
+// get a context whose version string reflects the raw backend, and skip.
+static bool
+force_gl_context_version(EGLint *major, EGLint *minor, bool *core)
+{
+ const char *env = getenv("WAFFLE_FORCE_GL_CONTEXT_VERSION");
+ if (!env || strlen(env) < 2)
+ return false;
+ if (env[0] < '1' || env[0] > '9' || env[1] < '0' || env[1] > '9')
+ return false;
+ *major = env[0] - '0';
+ *minor = env[1] - '0';
+ *core = strstr(env, "core") != NULL;
+ return true;
+}
+
static bool
bind_api(struct wegl_platform *plat, int32_t waffle_context_api)
{
@@ -61,13 +82,30 @@ create_real_context(struct wegl_config *config,
context_flags |= EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR;
}
+ // Effective version/profile for desktop GL, possibly upgraded by
+ // WAFFLE_FORCE_GL_CONTEXT_VERSION (never downgraded).
+ EGLint eff_major = attrs->context_major_version;
+ EGLint eff_minor = attrs->context_minor_version;
+ int32_t eff_profile = attrs->context_profile;
+ if (waffle_context_api == WAFFLE_CONTEXT_OPENGL) {
+ EGLint f_major, f_minor;
+ bool f_core;
+ if (force_gl_context_version(&f_major, &f_minor, &f_core) &&
+ 10 * f_major + f_minor > 10 * eff_major + eff_minor) {
+ eff_major = f_major;
+ eff_minor = f_minor;
+ if (f_core)
+ eff_profile = WAFFLE_CONTEXT_CORE_PROFILE;
+ }
+ }
+
switch (waffle_context_api) {
case WAFFLE_CONTEXT_OPENGL:
if (dpy->KHR_create_context) {
attrib_list[i++] = EGL_CONTEXT_MAJOR_VERSION_KHR;
- attrib_list[i++] = attrs->context_major_version;
+ attrib_list[i++] = eff_major;
attrib_list[i++] = EGL_CONTEXT_MINOR_VERSION_KHR;
- attrib_list[i++] = attrs->context_minor_version;
+ attrib_list[i++] = eff_minor;
}
else {
assert(attrs->context_major_version == 1);
@@ -92,9 +130,9 @@ create_real_context(struct wegl_config *config,
}
}
- if (wcore_config_attrs_version_ge(attrs, 32)) {
+ if (10 * eff_major + eff_minor >= 32) {
assert(dpy->KHR_create_context);
- switch (attrs->context_profile) {
+ switch (eff_profile) {
case WAFFLE_CONTEXT_CORE_PROFILE:
attrib_list[i++] = EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR;
attrib_list[i++] = EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR;
diff --git a/src/waffle/egl/wegl_platform.c b/src/waffle/egl/wegl_platform.c
index 6fe72ba..c3d72b1 100644
--- a/src/waffle/egl/wegl_platform.c
+++ b/src/waffle/egl/wegl_platform.c
@@ -13,11 +13,24 @@
#include "wegl_platform.h"
#ifdef WAFFLE_HAS_ANDROID
-static const char *libEGL_filename = "libEGL.so";
+static const char *libEGL_default_filename = "libEGL.so";
#else
-static const char *libEGL_filename = "libEGL.so.1";
+static const char *libEGL_default_filename = "libEGL.so.1";
#endif
+// MobileGL piglit harness: allow overriding which library provides the EGL
+// entry points (e.g. WAFFLE_EGL_LIBRARY=libMobileGL.so). MobileGL exports the
+// full EGL API under the real egl* names, so waffle can drive it directly
+// while the system libEGL.so stays untouched for MobileGL's own backend use.
+static const char *
+egl_library_name(void)
+{
+ const char *env = getenv("WAFFLE_EGL_LIBRARY");
+ if (env && env[0])
+ return env;
+ return libEGL_default_filename;
+}
+
static bool
supports_egl_khr_display(const struct wegl_platform *plat,
const char *client_extensions)
@@ -120,7 +133,7 @@ wegl_platform_teardown(struct wegl_platform *self)
ok = false;
wcore_errorf(WAFFLE_ERROR_UNKNOWN,
"dlclose(\"%s\") failed: %s",
- libEGL_filename, dlerror());
+ egl_library_name(), dlerror());
}
self->egl.handle = NULL;
}
@@ -132,12 +145,19 @@ wegl_platform_teardown(struct wegl_platform *self)
bool
wegl_platform_init(struct wegl_platform *self, EGLenum egl_platform)
{
- static const char *const dso_names[] = {
+ static const char *dso_names[] = {
"libGL.so.1",
"libGLESv1_CM.so.1",
"libGLESv2.so.2",
};
+ // MobileGL piglit harness: let waffle_dl_sym(WAFFLE_DL_OPENGL, ...)
+ // resolve desktop-GL symbols from an alternate library (MobileGL exports
+ // real gl* names). Android has no libGL.so.1.
+ const char *gl_override = getenv("WAFFLE_GL_LIBRARY");
+ if (gl_override && gl_override[0])
+ dso_names[0] = gl_override;
+
wcore_platform_init(&self->wcore);
posix_platform_init(&self->wcore, dso_names);
@@ -146,11 +166,11 @@ wegl_platform_init(struct wegl_platform *self, EGLenum egl_platform)
// Most Waffle platforms will call eglCreateWindowSurface.
self->egl_surface_type_mask = EGL_WINDOW_BIT;
- self->egl.handle = dlopen(libEGL_filename, RTLD_LAZY | RTLD_LOCAL);
+ self->egl.handle = dlopen(egl_library_name(), RTLD_LAZY | RTLD_LOCAL);
if (!self->egl.handle) {
wcore_errorf(WAFFLE_ERROR_FATAL,
"dlopen(\"%s\") failed: %s",
- libEGL_filename, dlerror());
+ egl_library_name(), dlerror());
return false;
}
@@ -158,7 +178,7 @@ wegl_platform_init(struct wegl_platform *self, EGLenum egl_platform)
self->egl.function = dlsym(self->egl.handle, "egl" #function); \
if (!self->egl.function) { \
wcore_errorf(WAFFLE_ERROR_FATAL, "dlsym(\"%s\", \"%s\") failed: %s", \
- libEGL_filename, "egl" #function, dlerror()); \
+ egl_library_name(), "egl" #function, dlerror()); \
goto error; \
}
diff --git a/src/waffle/meson.build b/src/waffle/meson.build
index 2ae4c40..e336566 100644
--- a/src/waffle/meson.build
+++ b/src/waffle/meson.build
@@ -22,6 +22,16 @@ deps_for_waffle = [
idep_threads,
]
+# MobileGL piglit harness: the Android imagereader window mode in
+# surfaceless_egl needs AImageReader (libmediandk) and ANativeWindow
+# (libandroid).
+if build_surfaceless and host_machine.system() == 'android'
+ deps_for_waffle += [
+ cc.find_library('mediandk'),
+ cc.find_library('android'),
+ ]
+endif
+
files_libwaffle = files(
'api/api_priv.c',
'api/waffle_attrib_list.c',
diff --git a/src/waffle/surfaceless_egl/sl_platform.c b/src/waffle/surfaceless_egl/sl_platform.c
index 30018c1..2c0024c 100644
--- a/src/waffle/surfaceless_egl/sl_platform.c
+++ b/src/waffle/surfaceless_egl/sl_platform.c
@@ -101,7 +101,7 @@ static const struct wcore_platform_vtbl sl_platform_vtbl = {
.destroy = sl_window_destroy,
.show = sl_window_show,
.resize = sl_window_resize,
- .swap_buffers = wegl_surface_swap_buffers,
+ .swap_buffers = sl_window_swap_buffers,
.get_native = sl_window_get_native,
},
};
diff --git a/src/waffle/surfaceless_egl/sl_platform.h b/src/waffle/surfaceless_egl/sl_platform.h
index a97fabe..a1c91aa 100644
--- a/src/waffle/surfaceless_egl/sl_platform.h
+++ b/src/waffle/surfaceless_egl/sl_platform.h
@@ -5,7 +5,6 @@
#include <stdbool.h>
#include <stdlib.h>
-#include <gbm.h>
#undef linux
diff --git a/src/waffle/surfaceless_egl/sl_window.c b/src/waffle/surfaceless_egl/sl_window.c
index 0c54613..d18f8b3 100644
--- a/src/waffle/surfaceless_egl/sl_window.c
+++ b/src/waffle/surfaceless_egl/sl_window.c
@@ -9,18 +9,108 @@
#include "wcore_tinfo.h"
#include "wegl_config.h"
+#include "wegl_display.h"
+#include "wegl_platform.h"
#include "wegl_util.h"
#include "sl_display.h"
#include "sl_platform.h"
#include "sl_window.h"
+#ifdef __ANDROID__
+#include <android/hardware_buffer.h>
+#include <android/native_window.h>
+#include <media/NdkImageReader.h>
+
+// MobileGL piglit harness: back "windows" with an AImageReader-provided
+// ANativeWindow instead of an EGL pbuffer. MobileGL's DirectVulkan backend
+// needs a real ANativeWindow on Android because the device ICD lacks
+// VK_EXT_headless_surface (which its pbuffer path requires).
+static bool
+sl_android_window_mode(void)
+{
+ const char *env = getenv("WAFFLE_ANDROID_WINDOW");
+ return env && strcmp(env, "imagereader") == 0;
+}
+
+static bool
+sl_imagereader_init(struct sl_window *self,
+ struct wcore_config *wc_config,
+ int32_t width, int32_t height)
+{
+ struct wegl_config *config = wegl_config(wc_config);
+ struct wegl_display *dpy = wegl_display(wc_config->display);
+ struct wegl_platform *plat = wegl_platform(dpy->wcore.platform);
+ AImageReader *reader = NULL;
+ ANativeWindow *window = NULL;
+
+ media_status_t st = AImageReader_newWithUsage(
+ width, height, AIMAGE_FORMAT_RGBA_8888,
+ AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT |
+ AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE,
+ 8, &reader);
+ if (st != AMEDIA_OK || !reader) {
+ wcore_errorf(WAFFLE_ERROR_UNKNOWN,
+ "AImageReader_newWithUsage(%dx%d) failed: %d",
+ width, height, (int)st);
+ return false;
+ }
+
+ st = AImageReader_getWindow(reader, &window);
+ if (st != AMEDIA_OK || !window) {
+ wcore_errorf(WAFFLE_ERROR_UNKNOWN,
+ "AImageReader_getWindow failed: %d", (int)st);
+ AImageReader_delete(reader);
+ return false;
+ }
+
+ // MobileGL reads EGL_WIDTH/EGL_HEIGHT from the window-surface attrib
+ // list (non-standard); unknown attribs are ignored.
+ EGLint attrib_list[] = {
+ EGL_RENDER_BUFFER, EGL_BACK_BUFFER,
+ EGL_WIDTH, width,
+ EGL_HEIGHT, height,
+ EGL_NONE,
+ };
+
+ self->wegl.egl = plat->egl.CreateWindowSurface(
+ dpy->egl, config->egl, (EGLNativeWindowType)window, attrib_list);
+ if (!self->wegl.egl) {
+ wegl_emit_error(plat, "eglCreateWindowSurface");
+ AImageReader_delete(reader);
+ return false;
+ }
+
+ self->reader = reader;
+ return true;
+}
+
+static void
+sl_imagereader_drain(struct sl_window *self)
+{
+ if (!self->reader)
+ return;
+
+ // Keep the BufferQueue from filling up: nobody displays these frames,
+ // so consume-and-drop the latest (which also releases all older ones).
+ AImage *image = NULL;
+ if (AImageReader_acquireLatestImage(self->reader, &image) == AMEDIA_OK &&
+ image)
+ AImage_delete(image);
+}
+#endif // __ANDROID__
+
bool
sl_window_destroy(struct wcore_window *wc_self)
{
struct sl_window *self = sl_window(wegl_surface(wc_self));
bool ok = wegl_surface_teardown(&self->wegl);
+#ifdef __ANDROID__
+ if (self->reader)
+ AImageReader_delete(self->reader);
+#endif
+
free(self);
return ok;
}
@@ -52,6 +142,11 @@ sl_window_create(struct wcore_platform *wc_plat,
wcore_window_init(&self->wegl.wcore, wc_config);
+#ifdef __ANDROID__
+ if (sl_android_window_mode())
+ ok = sl_imagereader_init(self, wc_config, width, height);
+ else
+#endif
ok = wegl_pbuffer_init(&self->wegl, wc_config, width, height);
if (!ok)
goto error;
@@ -71,6 +166,18 @@ sl_window_show(struct wcore_window *wc_self)
return true;
}
+bool
+sl_window_swap_buffers(struct wcore_window *wc_self)
+{
+ bool ok = wegl_surface_swap_buffers(wc_self);
+
+#ifdef __ANDROID__
+ sl_imagereader_drain(sl_window(wegl_surface(wc_self)));
+#endif
+
+ return ok;
+}
+
bool
sl_window_resize(struct wcore_window *wc_self,
int32_t width, int32_t height)
@@ -83,6 +190,36 @@ sl_window_resize(struct wcore_window *wc_self,
struct wcore_tinfo *tinfo;
bool ok = true;
+#ifdef __ANDROID__
+ if (self->reader) {
+ // ImageReader-backed window: build a fresh reader + EGL window
+ // surface at the new size, make it current, then retire the old one.
+ struct sl_window new_win;
+ memset(&new_win, 0, sizeof(new_win));
+ wcore_window_init(&new_win.wegl.wcore, self->wc_config);
+
+ ok = sl_imagereader_init(&new_win, self->wc_config, width, height);
+ if (!ok)
+ return false;
+
+ tinfo = wcore_tinfo_get();
+ wc_ctx = tinfo->current_context;
+
+ ok = wegl_make_current(wc_plat, wc_dpy, &new_win.wegl.wcore, wc_ctx);
+ if (!ok) {
+ wegl_surface_teardown(&new_win.wegl);
+ AImageReader_delete(new_win.reader);
+ return false;
+ }
+
+ wegl_surface_teardown(&self->wegl);
+ AImageReader_delete(self->reader);
+ self->wegl.egl = new_win.wegl.egl;
+ self->reader = new_win.reader;
+ return true;
+ }
+#endif
+
// Create a new pbuffer for the resized window.
ok = wegl_pbuffer_init(&new_wegl, self->wc_config, width, height);
if (!ok)
diff --git a/src/waffle/surfaceless_egl/sl_window.h b/src/waffle/surfaceless_egl/sl_window.h
index 8a96239..65a86ad 100644
--- a/src/waffle/surfaceless_egl/sl_window.h
+++ b/src/waffle/surfaceless_egl/sl_window.h
@@ -9,9 +9,18 @@
struct wcore_platform;
+#ifdef __ANDROID__
+typedef struct AImageReader AImageReader;
+#endif
+
struct sl_window {
struct wegl_surface wegl;
struct wcore_config *wc_config;
+#ifdef __ANDROID__
+ // Non-NULL when the window is backed by an AImageReader ANativeWindow
+ // (WAFFLE_ANDROID_WINDOW=imagereader) instead of an EGL pbuffer.
+ AImageReader *reader;
+#endif
};
DEFINE_CONTAINER_CAST_FUNC(sl_window,
@@ -31,6 +40,9 @@ sl_window_destroy(struct wcore_window *wc_self);
bool
sl_window_show(struct wcore_window *wc_self);
+bool
+sl_window_swap_buffers(struct wcore_window *wc_self);
+
bool
sl_window_resize(struct wcore_window *wc_self,
int32_t width, int32_t height);
+347
View File
@@ -0,0 +1,347 @@
#!/usr/bin/env python3
# MobileGL - tools/piglit-android/run_piglit_android.py
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
"""Run piglit GL tests on a connected Android device against MobileGL.
Pipeline:
1. Read a test list produced by `piglit print-cmd` ("name ::: command" lines).
2. Rewrite host paths to on-device paths and collect referenced data files.
3. Push binaries/libs/data to the device (incremental, marker-based).
4. Execute tests serially on-device in chunked shell scripts under `timeout`,
with MobileGL served to waffle via WAFFLE_EGL_LIBRARY=libMobileGL.so.
5. Parse "PIGLIT: {...}" result lines, classify, and write results + summary.
The device never needs python or an APK; everything runs as the adb shell user
from /data/local/tmp.
"""
import argparse
import json
import os
import re
import shlex
import subprocess
import sys
import tarfile
import tempfile
import time
from pathlib import Path
RESULT_LINE = re.compile(rb'PIGLIT: *({.*})')
MARK_START = re.compile(rb'@@@T (\d+) START')
MARK_EXIT = re.compile(rb'@@@T (\d+) EXIT (\d+)')
# toybox timeout exit codes: 124 = timed out (SIGTERM), 137 = SIGKILL after -k.
TIMEOUT_EXITS = {124, 137, 142}
def adb(args, serial=None, **kw):
cmd = ['adb'] + (['-s', serial] if serial else []) + args
return subprocess.run(cmd, **kw)
def adb_check(args, serial=None):
r = adb(args, serial=serial, capture_output=True)
if r.returncode != 0:
raise RuntimeError(f"adb {' '.join(args)} failed: {r.stderr.decode(errors='replace')}")
return r.stdout
def parse_list(path):
tests = []
for line in Path(path).read_text().splitlines():
line = line.strip()
if not line or ' ::: ' not in line:
continue
name, cmd = line.split(' ::: ', 1)
tests.append((name.strip(), shlex.split(cmd)))
return tests
def rewrite_cmd(argv, piglit_root, build_dir, device_dir):
"""Map host paths in a test command to device paths.
Returns (device_argv, referenced_host_files).
"""
build_abs = str((piglit_root / build_dir).resolve())
src_abs = str(piglit_root.resolve())
out = []
refs = []
for i, arg in enumerate(argv):
a = arg
if i == 0:
# program: "build-android/bin/foo" or absolute
prog = a if os.path.isabs(a) else str((piglit_root / a).resolve())
refs.append(prog)
out.append(f'{device_dir}/bin/{os.path.basename(prog)}')
continue
p = a if os.path.isabs(a) else None
if p and p.startswith(build_abs + '/generated_tests/'):
refs.append(p)
out.append(p.replace(build_abs + '/generated_tests', device_dir + '/generated_tests', 1))
elif p and p.startswith(build_abs + '/tests/'):
# Serialized profiles record data paths under the build dir, but
# only *generated* data lives there in an out-of-tree build;
# plain test files stay in the source tests/ dir.
if not os.path.exists(p):
p = p.replace(build_abs + '/tests', src_abs + '/tests', 1)
refs.append(p)
out.append(p.replace(src_abs + '/tests', device_dir + '/tests', 1))
else:
refs.append(p)
out.append(p.replace(build_abs + '/tests', device_dir + '/build-tests', 1))
elif p and p.startswith(src_abs + '/tests/'):
refs.append(p)
out.append(p.replace(src_abs + '/tests', device_dir + '/tests', 1))
else:
out.append(a)
return out, refs
def backend_env(backend, device_dir, use_angle=False):
env = {
'LD_LIBRARY_PATH': f'{device_dir}/lib',
'WAFFLE_EGL_LIBRARY': 'libMobileGL.so',
'WAFFLE_GL_LIBRARY': 'libMobileGL.so',
# Upgrade low compat context requests to what MobileGL implements;
# without this, piglit's supports_gl_compat_version=10 tests get the
# raw backend version string and skip themselves.
'WAFFLE_FORCE_GL_CONTEXT_VERSION': '33core',
'PIGLIT_PLATFORM': 'surfaceless_egl',
'PIGLIT_SOURCE_DIR': device_dir,
'MOBILEGL_BACKEND_TYPE': backend,
'MOBILEGL_LOG_FILE_PATH': f'{device_dir}/mobilegl.log',
}
if backend == 'DirectGLES':
env['MOBILEGL_USE_ANGLE'] = '1' if use_angle else '0'
if backend == 'DirectVulkan':
# The device ICD usually lacks VK_EXT_headless_surface, which the
# MobileGL pbuffer path needs; use a real ANativeWindow from
# AImageReader instead (patched waffle surfaceless_egl platform).
env['WAFFLE_ANDROID_WINDOW'] = 'imagereader'
return env
def push_tree(host, dev, serial, marker_name, force=False):
"""Push a directory once, tracked by a marker file on the device."""
marker = f'{dev}.pushed-{marker_name}'
if not force:
r = adb(['shell', f'test -e {shlex.quote(marker)} && echo yes'],
serial=serial, capture_output=True)
if r.stdout.strip() == b'yes':
return False
print(f' pushing {host} -> {dev}')
adb_check(['shell', f'rm -rf {shlex.quote(dev)}'], serial=serial)
adb_check(['push', str(host), dev], serial=serial)
adb_check(['shell', f'touch {shlex.quote(marker)}'], serial=serial)
return True
def push_files(files, strip_prefix, dev_prefix, serial):
"""Tar up referenced data files (relative to strip_prefix) and unpack on device."""
files = sorted(set(files))
if not files:
return
with tempfile.NamedTemporaryFile(suffix='.tar', delete=False) as tf:
tar_path = tf.name
with tarfile.open(tar_path, 'w') as tar:
for f in files:
rel = os.path.relpath(f, strip_prefix)
if rel.startswith('..'):
raise RuntimeError(f'file {f} not under {strip_prefix}')
tar.add(f, arcname=rel)
dev_tar = dev_prefix + '/.data.tar'
adb_check(['shell', f'mkdir -p {shlex.quote(dev_prefix)}'], serial=serial)
adb_check(['push', tar_path, dev_tar], serial=serial)
adb_check(['shell', f'cd {shlex.quote(dev_prefix)} && tar xf .data.tar && rm .data.tar'],
serial=serial)
os.unlink(tar_path)
def classify(exit_code, piglit_results, timed_out):
if timed_out:
return 'timeout'
result = None
for r in piglit_results:
if 'result' in r:
result = r['result']
if result is None:
return 'crash' if exit_code != 0 else 'notrun'
if exit_code not in (0, 1):
return 'crash'
return result
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('--piglit-root', required=True, type=Path,
help='piglit source checkout (with the Android build dir inside)')
ap.add_argument('--build-dir', default='build-android',
help='Android build dir name inside piglit root')
ap.add_argument('--list', required=True,
help='test list file from `piglit print-cmd` (name ::: cmd)')
ap.add_argument('--backend', required=True, choices=['DirectGLES', 'DirectVulkan'])
ap.add_argument('--use-angle', action='store_true',
help='DirectGLES only: let MobileGL load ANGLE instead of the system driver')
ap.add_argument('--mobilegl-lib', required=True, type=Path,
help='stripped libMobileGL.so for the device')
ap.add_argument('--waffle-lib', required=True, type=Path,
help='cross-built libwaffle-1.so')
ap.add_argument('--device-dir', default='/data/local/tmp/piglit-mgl')
ap.add_argument('--out', required=True, type=Path, help='host results directory')
ap.add_argument('--serial', default=None, help='adb device serial')
ap.add_argument('--timeout', type=int, default=60, help='per-test timeout (seconds)')
ap.add_argument('--chunk', type=int, default=200, help='tests per device-side script')
ap.add_argument('--repush', action='store_true', help='force re-push of bin/lib/tests trees')
args = ap.parse_args()
piglit_root = args.piglit_root.resolve()
build = piglit_root / args.build_dir
dev = args.device_dir.rstrip('/')
args.out.mkdir(parents=True, exist_ok=True)
tests = parse_list(args.list)
if not tests:
print('no tests in list', file=sys.stderr)
return 2
print(f'[1/4] {len(tests)} tests; rewriting paths')
dev_cmds = []
data_refs = []
for name, argv in tests:
dcmd, refs = rewrite_cmd(argv, piglit_root, args.build_dir, dev)
dev_cmds.append((name, dcmd))
data_refs.extend(r for r in refs if not r.split('/')[-2:][0] == 'bin')
print('[2/4] pushing artifacts')
adb_check(['shell', f'mkdir -p {dev} {dev}/lib {dev}/chunks {dev}/logs'], serial=args.serial)
push_tree(build / 'bin', f'{dev}/bin', args.serial, 'bin', args.repush)
adb_check(['shell', f'chmod -R 755 {dev}/bin'], serial=args.serial)
# libs: piglit utils + waffle + MobileGL in one LD_LIBRARY_PATH dir
for lib in sorted((build / 'lib').glob('*.so')):
adb_check(['push', str(lib), f'{dev}/lib/'], serial=args.serial)
adb_check(['push', str(args.waffle_lib), f'{dev}/lib/libwaffle-1.so'], serial=args.serial)
adb_check(['push', str(args.mobilegl_lib), f'{dev}/lib/libMobileGL.so'], serial=args.serial)
# data files referenced by this run (tests/, build-tests/, generated_tests/)
src_abs = str(piglit_root)
build_abs = str(build)
groups = {
(src_abs + '/tests', f'{dev}/tests'): [],
(build_abs + '/tests', f'{dev}/build-tests'): [],
(build_abs + '/generated_tests', f'{dev}/generated_tests'): [],
}
for r in set(data_refs):
for (host_prefix, dev_prefix), bucket in groups.items():
if r.startswith(host_prefix + '/'):
bucket.append(r)
break
for (host_prefix, dev_prefix), bucket in groups.items():
push_files(bucket, host_prefix, dev_prefix, args.serial)
env = backend_env(args.backend, dev, args.use_angle)
env_lines = '\n'.join(f'export {k}={shlex.quote(v)}' for k, v in env.items())
print(f'[3/4] running {len(tests)} tests on {args.backend} '
f'(timeout {args.timeout}s/test, chunks of {args.chunk})')
raw_log = (args.out / 'raw.log').open('wb')
t0 = time.time()
outcomes = {}
chunks = [dev_cmds[i:i + args.chunk] for i in range(0, len(dev_cmds), args.chunk)]
idx_base = 0
for ci, chunk in enumerate(chunks):
lines = ['#!/system/bin/sh', env_lines, f'cd {dev}']
for j, (name, dcmd) in enumerate(chunk):
idx = idx_base + j
quoted = ' '.join(shlex.quote(a) for a in dcmd)
lines.append(f'echo "@@@T {idx} START"')
lines.append(f'timeout -k 5 {args.timeout} {quoted} </dev/null 2>&1')
lines.append(f'echo "@@@T {idx} EXIT $?"')
script = '\n'.join(lines) + '\n'
with tempfile.NamedTemporaryFile('w', suffix='.sh', delete=False) as tf:
tf.write(script)
host_script = tf.name
dev_script = f'{dev}/chunks/chunk{ci}.sh'
adb_check(['push', host_script, dev_script], serial=args.serial)
os.unlink(host_script)
r = adb(['shell', f'sh {dev_script}'], serial=args.serial, capture_output=True)
raw_log.write(r.stdout)
raw_log.flush()
# parse this chunk
cur = None
cur_results = []
cur_out = []
for line in r.stdout.splitlines():
m = MARK_START.search(line)
if m:
cur = int(m.group(1))
cur_results = []
cur_out = []
continue
m = MARK_EXIT.search(line)
if m and cur is not None and int(m.group(1)) == cur:
code = int(m.group(2))
name = dev_cmds[cur][0]
status = classify(code, cur_results, code in TIMEOUT_EXITS)
outcomes[name] = {
'status': status,
'exit': code,
'subtests': {k: v for r_ in cur_results if 'subtest' in r_
for k, v in r_['subtest'].items()},
'tail': b'\n'.join(cur_out[-8:]).decode(errors='replace')
if status in ('crash', 'timeout', 'fail', 'notrun') else '',
}
cur = None
continue
if cur is not None:
cur_out.append(line)
m = RESULT_LINE.search(line)
if m:
try:
cur_results.append(json.loads(m.group(1)))
except json.JSONDecodeError:
pass
# tests whose markers never appeared (adb drop, device reboot)
for j, (name, _) in enumerate(chunk):
outcomes.setdefault(dev_cmds[idx_base + j][0], {
'status': 'missing', 'exit': -1, 'subtests': {}, 'tail': ''})
idx_base += len(chunk)
done = idx_base
print(f' chunk {ci + 1}/{len(chunks)} done ({done}/{len(dev_cmds)}, '
f'{time.time() - t0:.0f}s elapsed)')
raw_log.close()
print('[4/4] writing results')
counts = {}
for name, o in outcomes.items():
counts[o['status']] = counts.get(o['status'], 0) + 1
result_doc = {
'backend': args.backend,
'use_angle': args.use_angle,
'device_dir': dev,
'timeout': args.timeout,
'elapsed_sec': round(time.time() - t0, 1),
'totals': counts,
'tests': outcomes,
}
(args.out / 'results.json').write_text(json.dumps(result_doc, indent=1, sort_keys=True))
lines = [f"backend: {args.backend} tests: {len(outcomes)} elapsed: {result_doc['elapsed_sec']}s"]
lines.append('totals: ' + ', '.join(f'{k}={v}' for k, v in sorted(counts.items())))
for status in ('crash', 'timeout', 'fail', 'missing', 'notrun', 'warn'):
bad = sorted(n for n, o in outcomes.items() if o['status'] == status)
if bad:
lines.append(f'\n== {status} ({len(bad)}):')
lines.extend(f' {n}' for n in bad)
(args.out / 'summary.txt').write_text('\n'.join(lines) + '\n')
print('\n'.join(lines[:2]))
print(f"results: {args.out / 'results.json'}")
return 0
if __name__ == '__main__':
sys.exit(main())
@@ -0,0 +1,112 @@
---
name: piglit-on-android
description: Run piglit desktop-GL tests on a connected Android device with MobileGL as the OpenGL implementation - cross-build waffle (patched) and piglit for aarch64, push to /data/local/tmp, execute over adb against the DirectGLES (system driver or ANGLE) and DirectVulkan backends, and produce pass/fail/crash summaries. Use when validating MobileGL's OpenGL 3.3 core conformance on-device or bisecting a piglit regression.
---
# piglit on Android against MobileGL
## Variables
```sh
export REPO="$PWD" # MobileGL checkout
export WORK="$HOME/piglit-android" # piglit/waffle workdir (outside the repo)
export NDK="$HOME/Library/Android/sdk/ndk/27.3.13750724"
export TOOLS="$REPO/tools/piglit-android"
export DEVICE_DIR="/data/local/tmp/piglit-mgl"
```
## Architecture (read first)
- MobileGL exports the full `egl*`/`gl*` API with real symbol names from
`libMobileGL.so`; waffle's `surfaceless_egl` platform is pointed at it via
`WAFFLE_EGL_LIBRARY=libMobileGL.so` and `WAFFLE_GL_LIBRARY=libMobileGL.so`.
- **Never ship MobileGL under the name `libEGL.so`**: the DirectGLES backend
dlopens the system driver by the bare soname `libEGL.so` and would load
itself recursively.
- `WAFFLE_FORCE_GL_CONTEXT_VERSION=33core` (patched waffle) upgrades piglit's
low compat context requests (`supports_gl_compat_version=10`) to GL 3.3
core; without it those tests see a raw backend version string and skip.
- DirectVulkan cannot use MobileGL's EGL-pbuffer path on real devices (Android
ICDs lack `VK_EXT_headless_surface`), so the patched waffle creates windows
from an `AImageReader` ANativeWindow: `WAFFLE_ANDROID_WINDOW=imagereader`.
The runner sets this automatically for `--backend DirectVulkan`.
- The piglit patch also fixes `piglit_dispatch_default_init` to always install
the waffle resolvers; upstream silently bound gl* through the SYSTEM
libEGL's `eglGetProcAddress`, running every test on the raw GLES driver.
- MobileGL's lifecycle is owned by the EGL layer (lazy init on first EGL call,
full teardown on the last `eglTerminate`), so piglit processes start and
exit cleanly with no static ctor/dtor involvement.
## Steps
1. **Clone + patch** piglit and waffle (shallow clones are fine; never pull
git-LFS):
```sh
mkdir -p "$WORK" && cd "$WORK"
git clone --depth 1 https://gitlab.freedesktop.org/mesa/piglit.git
git clone --depth 1 https://gitlab.freedesktop.org/mesa/waffle.git
git -C waffle apply "$TOOLS/patches/waffle-mobilegl-android.patch"
git -C piglit apply "$TOOLS/patches/piglit-mobilegl-android.patch"
python3 -m venv venv && ./venv/bin/pip install mako numpy packaging
```
2. **Build libMobileGL.so** for arm64 (plain CMake + NDK; gradle not needed)
and strip it. See `$TOOLS/README.md` for exact invocations.
3. **Cross-build waffle** with meson (`surfaceless_egl` enabled, everything
else disabled) using `$TOOLS/cross-example/android-arm64.ini` and the stub
`egl.pc` (Cflags → `$REPO/include` for EGL 1.5 headers). `meson install` to
`$WORK/prefix` so piglit's pkg-config finds `waffle-1.pc`.
4. **Cross-build piglit** with the NDK toolchain file; the full CMake flag set
is in `$TOOLS/README.md`. All GL test binaries land in
`piglit/build-android/bin` (~1600), shared utils in `lib/`.
5. **Smoke test** with wflinfo on both backends before any long run:
```sh
adb shell "cd $DEVICE_DIR && env LD_LIBRARY_PATH=$DEVICE_DIR/lib \
WAFFLE_EGL_LIBRARY=libMobileGL.so WAFFLE_GL_LIBRARY=libMobileGL.so \
MOBILEGL_BACKEND_TYPE=DirectVulkan WAFFLE_ANDROID_WINDOW=imagereader \
./wflinfo --platform surfaceless_egl --api gl --version 3.3 --profile core"
```
Expect `3.3.0 MobileGL … Direct (Vulkan) Backend` and exit code 0.
6. **Enumerate tests on the host** with `piglit print-cmd` (profiles: `opengl`,
`shader`, `glslparser`; group separator is `@`). For the GL 3.3 core suite
use the version groups + GLSL groups + the ARB extension groups folded into
3.13.3 core (~15k tests).
7. **Run per backend** with the driver script (serial, chunked, per-test
timeout; DirectGLES first, then DirectVulkan):
```sh
python3 "$TOOLS/run_piglit_android.py" \
--piglit-root "$WORK/piglit" --list gl33-core.list \
--backend DirectGLES \
--mobilegl-lib "$WORK/libMobileGL-stripped.so" \
--waffle-lib "$WORK/waffle/build-android/src/waffle/libwaffle-1.so" \
--out results-gles
```
`--use-angle` switches DirectGLES to packaged ANGLE; default is the system
GLES driver. `--repush` forces re-pushing bin/lib/tests trees (needed after
rebuilds or if the device wiped `/data/local/tmp`).
8. **Compare backends**: `results.json` has per-test status
(`pass/fail/skip/crash/timeout/missing`) and output tails for non-passes;
`summary.txt` lists the bad tests. Diff the two runs' failing sets to
separate frontend issues (fail on both) from backend-specific ones.
## Gotchas
- A test crash after `PIGLIT: {"result": ...}` was printed is classified by
the result line, not the exit code, except that unknown nonzero exits count
as `crash`.
- The device may clear `/data/local/tmp` (vendor cleaners); `--repush` recovers.
- Keep chunks ≤ ~250 tests: one adb connection per chunk bounds the damage of
USB hiccups, and progress prints per chunk.
- MobileGL writes `$DEVICE_DIR/mobilegl.log` (set by the runner); check it and
`adb logcat -b crash` when triaging crashes.
@@ -0,0 +1,4 @@
interface:
display_name: "MobileGL piglit on Android"
short_description: "Run piglit GL tests on-device against MobileGL backends"
default_prompt: "Use $piglit-on-android to run the piglit OpenGL 3.3 core suite on the connected Android device against MobileGL's DirectGLES and DirectVulkan backends and summarize the results."
+25
View File
@@ -0,0 +1,25 @@
# WGL smoke tests
Manual bring-up tests for the Windows host layer (drop-in `opengl32.dll`).
Both replicate the GLFW/LWJGL bootstrap Minecraft uses and verify a rendered
triangle via `glReadPixels`; exit code 0 means pass.
- `wgl_smoke.cpp` - hand-rolled Win32/WGL sequence (dummy context on a hidden
helper window, ARB pixel format + 3.2 core forward-compatible context,
gdi32 pixel-format forwarding, swap loop, readback).
- `glfw_smoke.cpp` - the same flow driven by a real GLFW binary, which also
exercises GLFW's zero-area helper window (the case that needs the
DirectVulkan no-swapchain-at-init path).
Build (x64 Native Tools prompt), with GLFW binaries for the second test:
cl /nologo /EHsc /W3 wgl_smoke.cpp user32.lib gdi32.lib
cl /nologo /EHsc /W3 glfw_smoke.cpp /I%GLFW%\include %GLFW%\lib-vc2022\glfw3dll.lib user32.lib gdi32.lib
Run with the MobileGL `opengl32.dll` copy (and `glfw3.dll`, plus ANGLE's
`libEGL.dll`/`libGLESv2.dll`/`d3dcompiler_47.dll` for DirectGLES) in the exe
directory:
set MOBILEGL_BACKEND_TYPE=DirectVulkan (or DirectGLES)
set MOBILEGL_LOG_FILE_PATH=smoke.log
wgl_smoke.exe && glfw_smoke.exe
+189
View File
@@ -0,0 +1,189 @@
// GLFW-driven smoke test: exercises the exact GLFW WGL bootstrap Minecraft uses
// (helper-window dummy context, ARB pixel format, 3.2 core forward-compatible
// no-error context) against the MobileGL opengl32.dll drop-in.
//
// Build: cl /nologo /EHsc /W3 glfw_smoke.cpp /I<GLFW>/include <GLFW>/lib-vc2022/glfw3dll.lib user32.lib gdi32.lib
// Run with opengl32.dll + glfw3.dll in the exe directory.
#include <GLFW/glfw3.h>
#include <windows.h>
#include <cstdio>
typedef const unsigned char*(WINAPI* PFN_glGetString)(unsigned int);
typedef void(WINAPI* PFN_glClearColor)(float, float, float, float);
typedef void(WINAPI* PFN_glClear)(unsigned int);
typedef void(WINAPI* PFN_glReadPixels)(int, int, int, int, unsigned int, unsigned int, void*);
typedef void(WINAPI* PFN_glFinish)(void);
typedef unsigned int(WINAPI* PFN_glCreateShader)(unsigned int);
typedef void(WINAPI* PFN_glShaderSource)(unsigned int, int, const char* const*, const int*);
typedef void(WINAPI* PFN_glCompileShader)(unsigned int);
typedef unsigned int(WINAPI* PFN_glCreateProgram)(void);
typedef void(WINAPI* PFN_glAttachShader)(unsigned int, unsigned int);
typedef void(WINAPI* PFN_glLinkProgram)(unsigned int);
typedef void(WINAPI* PFN_glUseProgram)(unsigned int);
typedef void(WINAPI* PFN_glGenVertexArrays)(int, unsigned int*);
typedef void(WINAPI* PFN_glBindVertexArray)(unsigned int);
typedef void(WINAPI* PFN_glGenBuffers)(int, unsigned int*);
typedef void(WINAPI* PFN_glBindBuffer)(unsigned int, unsigned int);
typedef void(WINAPI* PFN_glBufferData)(unsigned int, long long, const void*, unsigned int);
typedef void(WINAPI* PFN_glVertexAttribPointer)(unsigned int, int, unsigned int, unsigned char, int, const void*);
typedef void(WINAPI* PFN_glEnableVertexAttribArray)(unsigned int);
typedef void(WINAPI* PFN_glDrawArrays)(unsigned int, int, int);
#define GL_COLOR_BUFFER_BIT 0x00004000
#define GL_VERSION 0x1F02
#define GL_RENDERER 0x1F01
#define GL_RGBA 0x1908
#define GL_UNSIGNED_BYTE 0x1401
#define GL_TRIANGLES 0x0004
#define GL_FLOAT 0x1406
#define GL_ARRAY_BUFFER 0x8892
#define GL_STATIC_DRAW 0x88E4
#define GL_VERTEX_SHADER 0x8B31
#define GL_FRAGMENT_SHADER 0x8B30
static void RawPrint(HANDLE f, const char* s) {
DWORD written = 0;
WriteFile(f, s, (DWORD)lstrlenA(s), &written, nullptr);
}
static void RawHex(HANDLE f, unsigned long long v) {
char buf[20];
for (int i = 0; i < 16; ++i) {
int nib = (int)((v >> ((15 - i) * 4)) & 0xF);
buf[i] = (char)(nib < 10 ? '0' + nib : 'a' + nib - 10);
}
buf[16] = 0;
RawPrint(f, buf);
}
static LONG WINAPI CrashDump(EXCEPTION_POINTERS* ep) {
static volatile LONG entered = 0;
if (ep->ExceptionRecord->ExceptionCode != 0xC0000005) return EXCEPTION_CONTINUE_SEARCH;
if (InterlockedExchange(&entered, 1)) return EXCEPTION_CONTINUE_SEARCH;
HANDLE f = CreateFileA("glfw-crash-stack.txt", GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (f == INVALID_HANDLE_VALUE) TerminateProcess(GetCurrentProcess(), 0x43);
RawPrint(f, "AV at ");
RawHex(f, (unsigned long long)ep->ExceptionRecord->ExceptionAddress);
RawPrint(f, " reading ");
RawHex(f, (unsigned long long)ep->ExceptionRecord->ExceptionInformation[1]);
RawPrint(f, "\n");
const unsigned long long* sp = (const unsigned long long*)ep->ContextRecord->Rsp;
for (int i = 0; i < 512; ++i) {
MEMORY_BASIC_INFORMATION mbi;
if (!VirtualQuery(sp + i, &mbi, sizeof(mbi)) || mbi.State != MEM_COMMIT ||
(mbi.Protect & (PAGE_READONLY | PAGE_READWRITE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE)) == 0)
break;
unsigned long long v = sp[i];
HMODULE mod = nullptr;
if (v > 0x10000 &&
GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPCSTR)v, &mod) &&
mod) {
char name[MAX_PATH];
name[0] = 0;
GetModuleFileNameA(mod, name, MAX_PATH);
RawPrint(f, "stack ");
RawHex(f, v);
RawPrint(f, " ");
RawPrint(f, name);
RawPrint(f, "+");
RawHex(f, v - (unsigned long long)mod);
RawPrint(f, "\n");
}
}
CloseHandle(f);
TerminateProcess(GetCurrentProcess(), 0x42);
return EXCEPTION_EXECUTE_HANDLER;
}
int main() {
AddVectoredExceptionHandler(1, CrashDump);
if (!glfwInit()) {
std::fprintf(stderr, "GLFW-SMOKE FAIL: glfwInit\n");
return 1;
}
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
glfwWindowHint(GLFW_CONTEXT_NO_ERROR, GLFW_TRUE);
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
GLFWwindow* win = glfwCreateWindow(854, 480, "MobileGL GLFW smoke", nullptr, nullptr);
if (!win) {
const char* desc = nullptr;
int code = glfwGetError(&desc);
std::fprintf(stderr, "GLFW-SMOKE FAIL: glfwCreateWindow (0x%x %s)\n", code, desc ? desc : "?");
return 1;
}
glfwMakeContextCurrent(win);
glfwShowWindow(win);
glfwSwapInterval(0);
auto glGetString_ = (PFN_glGetString)glfwGetProcAddress("glGetString");
auto glClearColor_ = (PFN_glClearColor)glfwGetProcAddress("glClearColor");
auto glClear_ = (PFN_glClear)glfwGetProcAddress("glClear");
auto glReadPixels_ = (PFN_glReadPixels)glfwGetProcAddress("glReadPixels");
auto glFinish_ = (PFN_glFinish)glfwGetProcAddress("glFinish");
std::printf("GL_VERSION: %s\nGL_RENDERER: %s\n", glGetString_(GL_VERSION), glGetString_(GL_RENDERER));
auto glCreateShader_ = (PFN_glCreateShader)glfwGetProcAddress("glCreateShader");
auto glShaderSource_ = (PFN_glShaderSource)glfwGetProcAddress("glShaderSource");
auto glCompileShader_ = (PFN_glCompileShader)glfwGetProcAddress("glCompileShader");
auto glCreateProgram_ = (PFN_glCreateProgram)glfwGetProcAddress("glCreateProgram");
auto glAttachShader_ = (PFN_glAttachShader)glfwGetProcAddress("glAttachShader");
auto glLinkProgram_ = (PFN_glLinkProgram)glfwGetProcAddress("glLinkProgram");
auto glUseProgram_ = (PFN_glUseProgram)glfwGetProcAddress("glUseProgram");
auto glGenVertexArrays_ = (PFN_glGenVertexArrays)glfwGetProcAddress("glGenVertexArrays");
auto glBindVertexArray_ = (PFN_glBindVertexArray)glfwGetProcAddress("glBindVertexArray");
auto glGenBuffers_ = (PFN_glGenBuffers)glfwGetProcAddress("glGenBuffers");
auto glBindBuffer_ = (PFN_glBindBuffer)glfwGetProcAddress("glBindBuffer");
auto glBufferData_ = (PFN_glBufferData)glfwGetProcAddress("glBufferData");
auto glVertexAttribPointer_ = (PFN_glVertexAttribPointer)glfwGetProcAddress("glVertexAttribPointer");
auto glEnableVertexAttribArray_ = (PFN_glEnableVertexAttribArray)glfwGetProcAddress("glEnableVertexAttribArray");
auto glDrawArrays_ = (PFN_glDrawArrays)glfwGetProcAddress("glDrawArrays");
const char* vsSrc = "#version 150 core\nin vec2 aPos;\nvoid main(){ gl_Position = vec4(aPos, 0.0, 1.0); }\n";
const char* fsSrc = "#version 150 core\nout vec4 c;\nvoid main(){ c = vec4(1.0, 0.5, 0.0, 1.0); }\n";
unsigned int vs = glCreateShader_(GL_VERTEX_SHADER);
glShaderSource_(vs, 1, &vsSrc, nullptr);
glCompileShader_(vs);
unsigned int fs = glCreateShader_(GL_FRAGMENT_SHADER);
glShaderSource_(fs, 1, &fsSrc, nullptr);
glCompileShader_(fs);
unsigned int prog = glCreateProgram_();
glAttachShader_(prog, vs);
glAttachShader_(prog, fs);
glLinkProgram_(prog);
glUseProgram_(prog);
const float verts[] = {-0.9f, -0.9f, 0.9f, -0.9f, 0.0f, 0.9f};
unsigned int vao = 0, vbo = 0;
glGenVertexArrays_(1, &vao);
glBindVertexArray_(vao);
glGenBuffers_(1, &vbo);
glBindBuffer_(GL_ARRAY_BUFFER, vbo);
glBufferData_(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
glVertexAttribPointer_(0, 2, GL_FLOAT, 0, 0, nullptr);
glEnableVertexAttribArray_(0);
unsigned char px[4] = {};
for (int i = 0; i < 60 && !glfwWindowShouldClose(win); ++i) {
glClearColor_(0.1f, 0.2f, 0.4f, 1.0f);
glClear_(GL_COLOR_BUFFER_BIT);
glDrawArrays_(GL_TRIANGLES, 0, 3);
if (i == 58) {
glFinish_();
glReadPixels_(427, 240, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px);
}
glfwSwapBuffers(win);
glfwPollEvents();
}
std::printf("pixel: %u,%u,%u,%u\n", px[0], px[1], px[2], px[3]);
glfwDestroyWindow(win);
glfwTerminate();
const bool ok = px[0] > 200 && px[1] > 90 && px[1] < 160 && px[2] < 50;
std::printf(ok ? "GLFW-SMOKE PASS\n" : "GLFW-SMOKE FAIL: wrong pixel\n");
return ok ? 0 : 1;
}
+472
View File
@@ -0,0 +1,472 @@
// WGL smoke test for the MobileGL opengl32.dll drop-in.
// Replicates GLFW 3.3.3's WGL bootstrap exactly as Minecraft 1.21.4 drives it:
// 1. dummy window: gdi32 ChoosePixelFormat/SetPixelFormat -> wglCreateContext
// -> wglMakeCurrent -> probe wglGetProcAddress/extension strings
// -> wglMakeCurrent(NULL) -> wglDeleteContext
// 2. real window: SetPixelFormat -> wglCreateContextAttribsARB (3.2 core,
// forward-compatible) -> wglMakeCurrent -> render loop with SwapBuffers
// Verifies rendering via glReadPixels. Exit code 0 = pass.
//
// Build: cl /nologo /EHsc /W3 wgl_smoke.cpp user32.lib gdi32.lib
// Run with opengl32.dll (MobileGL) in the exe directory.
#include <windows.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
typedef unsigned int GLenum;
typedef unsigned int GLuint;
typedef int GLint;
typedef int GLsizei;
typedef float GLfloat;
typedef unsigned char GLubyte;
typedef char GLchar;
typedef signed long long GLsizeiptr;
typedef unsigned int GLbitfield;
#define GL_COLOR_BUFFER_BIT 0x00004000
#define GL_DEPTH_BUFFER_BIT 0x00000100
#define GL_TRIANGLES 0x0004
#define GL_FLOAT 0x1406
#define GL_FALSE 0
#define GL_ARRAY_BUFFER 0x8892
#define GL_STATIC_DRAW 0x88E4
#define GL_FRAGMENT_SHADER 0x8B30
#define GL_VERTEX_SHADER 0x8B31
#define GL_COMPILE_STATUS 0x8B81
#define GL_LINK_STATUS 0x8B82
#define GL_VENDOR 0x1F00
#define GL_RENDERER 0x1F01
#define GL_VERSION 0x1F02
#define GL_RGBA 0x1908
#define GL_UNSIGNED_BYTE 0x1401
#define GL_MAJOR_VERSION 0x821B
#define GL_MINOR_VERSION 0x821C
#define GL_NO_ERROR 0
#define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091
#define WGL_CONTEXT_MINOR_VERSION_ARB 0x2092
#define WGL_CONTEXT_FLAGS_ARB 0x2094
#define WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB 0x0002
#define WGL_CONTEXT_PROFILE_MASK_ARB 0x9126
#define WGL_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001
#define WGL_CONTEXT_OPENGL_NO_ERROR_ARB 0x31B3
#define WGL_NUMBER_PIXEL_FORMATS_ARB 0x2000
#define WGL_SUPPORT_OPENGL_ARB 0x2010
#define WGL_DRAW_TO_WINDOW_ARB 0x2001
#define WGL_PIXEL_TYPE_ARB 0x2013
#define WGL_TYPE_RGBA_ARB 0x202B
#define WGL_DEPTH_BITS_ARB 0x2022
#define WGL_STENCIL_BITS_ARB 0x2023
#define WGL_DOUBLE_BUFFER_ARB 0x2011
static HMODULE g_opengl32;
typedef HGLRC(WINAPI* PFN_wglCreateContext)(HDC);
typedef BOOL(WINAPI* PFN_wglDeleteContext)(HGLRC);
typedef PROC(WINAPI* PFN_wglGetProcAddress)(LPCSTR);
typedef BOOL(WINAPI* PFN_wglMakeCurrent)(HDC, HGLRC);
typedef HGLRC(WINAPI* PFN_wglGetCurrentContext)(void);
typedef HDC(WINAPI* PFN_wglGetCurrentDC)(void);
static PFN_wglCreateContext p_wglCreateContext;
static PFN_wglDeleteContext p_wglDeleteContext;
static PFN_wglGetProcAddress p_wglGetProcAddress;
static PFN_wglMakeCurrent p_wglMakeCurrent;
static PFN_wglGetCurrentContext p_wglGetCurrentContext;
static PFN_wglGetCurrentDC p_wglGetCurrentDC;
typedef HGLRC(WINAPI* PFN_wglCreateContextAttribsARB)(HDC, HGLRC, const int*);
typedef const char*(WINAPI* PFN_wglGetExtensionsStringARB)(HDC);
typedef BOOL(WINAPI* PFN_wglSwapIntervalEXT)(int);
typedef BOOL(WINAPI* PFN_wglGetPixelFormatAttribivARB)(HDC, int, int, UINT, const int*, int*);
// GL function pointers, loaded LWJGL-style: wglGetProcAddress first, then
// GetProcAddress on the opengl32 module.
static void* GetGLProc(const char* name) {
PROC proc = p_wglGetProcAddress(name);
if (!proc) {
proc = ::GetProcAddress(g_opengl32, name);
}
return reinterpret_cast<void*>(proc);
}
typedef const GLubyte*(WINAPI* PFN_glGetString)(GLenum);
typedef void(WINAPI* PFN_glGetIntegerv)(GLenum, GLint*);
typedef void(WINAPI* PFN_glClearColor)(GLfloat, GLfloat, GLfloat, GLfloat);
typedef void(WINAPI* PFN_glClear)(GLbitfield);
typedef void(WINAPI* PFN_glViewport)(GLint, GLint, GLsizei, GLsizei);
typedef GLenum(WINAPI* PFN_glGetError)(void);
typedef void(WINAPI* PFN_glReadPixels)(GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, void*);
typedef void(WINAPI* PFN_glFinish)(void);
typedef GLuint(WINAPI* PFN_glCreateShader)(GLenum);
typedef void(WINAPI* PFN_glShaderSource)(GLuint, GLsizei, const GLchar* const*, const GLint*);
typedef void(WINAPI* PFN_glCompileShader)(GLuint);
typedef void(WINAPI* PFN_glGetShaderiv)(GLuint, GLenum, GLint*);
typedef void(WINAPI* PFN_glGetShaderInfoLog)(GLuint, GLsizei, GLsizei*, GLchar*);
typedef GLuint(WINAPI* PFN_glCreateProgram)(void);
typedef void(WINAPI* PFN_glAttachShader)(GLuint, GLuint);
typedef void(WINAPI* PFN_glLinkProgram)(GLuint);
typedef void(WINAPI* PFN_glGetProgramiv)(GLuint, GLenum, GLint*);
typedef void(WINAPI* PFN_glGetProgramInfoLog)(GLuint, GLsizei, GLsizei*, GLchar*);
typedef void(WINAPI* PFN_glUseProgram)(GLuint);
typedef void(WINAPI* PFN_glGenVertexArrays)(GLsizei, GLuint*);
typedef void(WINAPI* PFN_glBindVertexArray)(GLuint);
typedef void(WINAPI* PFN_glGenBuffers)(GLsizei, GLuint*);
typedef void(WINAPI* PFN_glBindBuffer)(GLenum, GLuint);
typedef void(WINAPI* PFN_glBufferData)(GLenum, GLsizeiptr, const void*, GLenum);
typedef void(WINAPI* PFN_glVertexAttribPointer)(GLuint, GLint, GLenum, unsigned char, GLsizei, const void*);
typedef void(WINAPI* PFN_glEnableVertexAttribArray)(GLuint);
typedef void(WINAPI* PFN_glDrawArrays)(GLenum, GLint, GLsizei);
static PFN_glGetString glGetString_;
static PFN_glGetIntegerv glGetIntegerv_;
static PFN_glClearColor glClearColor_;
static PFN_glClear glClear_;
static PFN_glViewport glViewport_;
static PFN_glGetError glGetError_;
static PFN_glReadPixels glReadPixels_;
static PFN_glFinish glFinish_;
static PFN_glCreateShader glCreateShader_;
static PFN_glShaderSource glShaderSource_;
static PFN_glCompileShader glCompileShader_;
static PFN_glGetShaderiv glGetShaderiv_;
static PFN_glGetShaderInfoLog glGetShaderInfoLog_;
static PFN_glCreateProgram glCreateProgram_;
static PFN_glAttachShader glAttachShader_;
static PFN_glLinkProgram glLinkProgram_;
static PFN_glGetProgramiv glGetProgramiv_;
static PFN_glGetProgramInfoLog glGetProgramInfoLog_;
static PFN_glUseProgram glUseProgram_;
static PFN_glGenVertexArrays glGenVertexArrays_;
static PFN_glBindVertexArray glBindVertexArray_;
static PFN_glGenBuffers glGenBuffers_;
static PFN_glBindBuffer glBindBuffer_;
static PFN_glBufferData glBufferData_;
static PFN_glVertexAttribPointer glVertexAttribPointer_;
static PFN_glEnableVertexAttribArray glEnableVertexAttribArray_;
static PFN_glDrawArrays glDrawArrays_;
#define FAIL(...) \
do { \
std::fprintf(stderr, "SMOKE FAIL: " __VA_ARGS__); \
std::fprintf(stderr, "\n"); \
return 1; \
} while (0)
// CRT-free crash dumper: runs during process teardown when stdio is already
// dead, so it may only touch raw Win32.
static void RawPrint(HANDLE f, const char* s) {
DWORD written = 0;
WriteFile(f, s, (DWORD)lstrlenA(s), &written, nullptr);
}
static void RawHex(HANDLE f, DWORD64 v) {
char buf[20];
for (int i = 0; i < 16; ++i) {
int nib = (int)((v >> ((15 - i) * 4)) & 0xF);
buf[i] = (char)(nib < 10 ? '0' + nib : 'a' + nib - 10);
}
buf[16] = 0;
RawPrint(f, buf);
}
static LONG WINAPI CrashDump(EXCEPTION_POINTERS* ep) {
static volatile LONG entered = 0;
if (ep->ExceptionRecord->ExceptionCode != 0xC0000005) return EXCEPTION_CONTINUE_SEARCH;
if (InterlockedExchange(&entered, 1)) return EXCEPTION_CONTINUE_SEARCH;
HANDLE f = CreateFileA(
"C:\\Users\\yello\\AndroidStudioProjects\\FoldCraftLauncher\\MobileGL\\.claude\\worktrees\\wgl-host\\wgl-smoke\\crash-stack.txt",
GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (f == INVALID_HANDLE_VALUE) {
TerminateProcess(GetCurrentProcess(), 0x43);
}
RawPrint(f, "AV at ");
RawHex(f, (DWORD64)ep->ExceptionRecord->ExceptionAddress);
RawPrint(f, "\n");
const DWORD64* sp = reinterpret_cast<const DWORD64*>(ep->ContextRecord->Rsp);
for (int i = 0; i < 512; ++i) {
MEMORY_BASIC_INFORMATION mbi;
if (!VirtualQuery(sp + i, &mbi, sizeof(mbi)) || mbi.State != MEM_COMMIT ||
(mbi.Protect & (PAGE_READONLY | PAGE_READWRITE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE)) == 0)
break;
DWORD64 v = sp[i];
HMODULE mod = nullptr;
if (v > 0x10000 &&
GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCSTR>(v), &mod) &&
mod) {
char name[MAX_PATH];
name[0] = 0;
GetModuleFileNameA(mod, name, MAX_PATH);
RawPrint(f, "stack ");
RawHex(f, v);
RawPrint(f, " ");
RawPrint(f, name);
RawPrint(f, "+");
RawHex(f, v - (DWORD64)mod);
RawPrint(f, "\n");
}
}
CloseHandle(f);
TerminateProcess(GetCurrentProcess(), 0x42);
return EXCEPTION_EXECUTE_HANDLER;
}
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
return DefWindowProcA(hwnd, msg, wp, lp);
}
static HWND MakeWindow(const char* title, int w, int h, bool visible) {
WNDCLASSA wc{};
wc.style = CS_OWNDC;
wc.lpfnWndProc = WndProc;
wc.hInstance = GetModuleHandleA(nullptr);
wc.lpszClassName = "MGWGLSmoke";
static bool registered = false;
if (!registered) {
RegisterClassA(&wc);
registered = true;
}
DWORD style = WS_OVERLAPPEDWINDOW | (visible ? WS_VISIBLE : 0);
RECT rect{0, 0, w, h};
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE);
return CreateWindowExA(0, "MGWGLSmoke", title, style, CW_USEDEFAULT, CW_USEDEFAULT, rect.right - rect.left,
rect.bottom - rect.top, nullptr, nullptr, GetModuleHandleA(nullptr), nullptr);
}
int main() {
AddVectoredExceptionHandler(1, CrashDump);
// Load exactly like GLFW: the app-dir opengl32.dll must win the search.
g_opengl32 = LoadLibraryA("opengl32.dll");
if (!g_opengl32) FAIL("LoadLibraryA(opengl32.dll) failed");
char modulePath[MAX_PATH]{};
GetModuleFileNameA(g_opengl32, modulePath, MAX_PATH);
std::printf("opengl32.dll -> %s\n", modulePath);
p_wglCreateContext = (PFN_wglCreateContext)::GetProcAddress(g_opengl32, "wglCreateContext");
p_wglDeleteContext = (PFN_wglDeleteContext)::GetProcAddress(g_opengl32, "wglDeleteContext");
p_wglGetProcAddress = (PFN_wglGetProcAddress)::GetProcAddress(g_opengl32, "wglGetProcAddress");
p_wglMakeCurrent = (PFN_wglMakeCurrent)::GetProcAddress(g_opengl32, "wglMakeCurrent");
p_wglGetCurrentContext = (PFN_wglGetCurrentContext)::GetProcAddress(g_opengl32, "wglGetCurrentContext");
p_wglGetCurrentDC = (PFN_wglGetCurrentDC)::GetProcAddress(g_opengl32, "wglGetCurrentDC");
if (!p_wglCreateContext || !p_wglDeleteContext || !p_wglGetProcAddress || !p_wglMakeCurrent ||
!p_wglGetCurrentContext || !p_wglGetCurrentDC)
FAIL("missing classic wgl exports");
// ---- Phase 1: GLFW-style dummy context on a hidden helper window ----
HWND helper = MakeWindow("helper", 1, 1, false);
if (!helper) FAIL("helper window creation failed");
HDC helperDC = GetDC(helper);
PIXELFORMATDESCRIPTOR pfd{};
pfd.nSize = sizeof(pfd);
pfd.nVersion = 1;
pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
pfd.iPixelType = PFD_TYPE_RGBA;
pfd.cColorBits = 24;
int dummyFormat = ChoosePixelFormat(helperDC, &pfd); // gdi32 -> wglChoosePixelFormat
if (dummyFormat <= 0) FAIL("gdi32 ChoosePixelFormat returned %d", dummyFormat);
if (!SetPixelFormat(helperDC, dummyFormat, &pfd)) FAIL("gdi32 SetPixelFormat failed");
std::printf("dummy pixel format: %d (DescribePixelFormat count=%d)\n", dummyFormat,
DescribePixelFormat(helperDC, 1, sizeof(pfd), nullptr));
HGLRC dummyCtx = p_wglCreateContext(helperDC);
if (!dummyCtx) FAIL("wglCreateContext (dummy) failed");
if (!p_wglMakeCurrent(helperDC, dummyCtx)) {
typedef int(WINAPI * PFN_eglGetError)(void);
auto eglGetError_ = (PFN_eglGetError)::GetProcAddress(g_opengl32, "eglGetError");
FAIL("wglMakeCurrent (dummy) failed (GetLastError=%lu eglError=0x%x)", GetLastError(),
eglGetError_ ? eglGetError_() : -1);
}
auto wglGetExtensionsStringARB =
(PFN_wglGetExtensionsStringARB)p_wglGetProcAddress("wglGetExtensionsStringARB");
auto wglCreateContextAttribsARB =
(PFN_wglCreateContextAttribsARB)p_wglGetProcAddress("wglCreateContextAttribsARB");
auto wglSwapIntervalEXT = (PFN_wglSwapIntervalEXT)p_wglGetProcAddress("wglSwapIntervalEXT");
auto wglGetPixelFormatAttribivARB =
(PFN_wglGetPixelFormatAttribivARB)p_wglGetProcAddress("wglGetPixelFormatAttribivARB");
if (!wglGetExtensionsStringARB) FAIL("wglGetExtensionsStringARB unavailable");
if (!wglCreateContextAttribsARB) FAIL("wglCreateContextAttribsARB unavailable");
if (!wglSwapIntervalEXT) FAIL("wglSwapIntervalEXT unavailable");
if (!wglGetPixelFormatAttribivARB) FAIL("wglGetPixelFormatAttribivARB unavailable");
std::printf("WGL extensions: %s\n", wglGetExtensionsStringARB(helperDC));
glGetString_ = (PFN_glGetString)GetGLProc("glGetString");
if (!glGetString_) FAIL("glGetString unavailable");
std::printf("dummy ctx GL_VERSION: %s\n", glGetString_(GL_VERSION));
std::printf("dummy ctx GL_RENDERER: %s\n", glGetString_(GL_RENDERER));
int nfmtAttrib = WGL_NUMBER_PIXEL_FORMATS_ARB;
int nativeCount = 0;
if (!wglGetPixelFormatAttribivARB(helperDC, 1, 0, 1, &nfmtAttrib, &nativeCount) || nativeCount < 1)
FAIL("WGL_NUMBER_PIXEL_FORMATS_ARB query failed (%d)", nativeCount);
std::printf("ARB pixel formats: %d\n", nativeCount);
if (!p_wglMakeCurrent(nullptr, nullptr)) FAIL("wglMakeCurrent(NULL) failed");
if (!p_wglDeleteContext(dummyCtx)) FAIL("wglDeleteContext (dummy) failed");
// ---- Phase 2: real window + 3.2 core forward-compatible context ----
HWND window = MakeWindow("MobileGL WGL smoke", 640, 480, true);
if (!window) FAIL("main window creation failed");
HDC dc = GetDC(window);
// Pick the depth24/stencil8 RGBA format via the ARB query, GLFW-style.
int chosenFormat = 0;
for (int i = 1; i <= nativeCount; ++i) {
const int attribs[] = {WGL_SUPPORT_OPENGL_ARB, WGL_DRAW_TO_WINDOW_ARB, WGL_PIXEL_TYPE_ARB,
WGL_DOUBLE_BUFFER_ARB, WGL_DEPTH_BITS_ARB, WGL_STENCIL_BITS_ARB};
int values[6] = {};
if (!wglGetPixelFormatAttribivARB(dc, i, 0, 6, attribs, values)) continue;
if (values[0] && values[1] && values[2] == WGL_TYPE_RGBA_ARB && values[3] && values[4] >= 24 &&
values[5] >= 8) {
chosenFormat = i;
break;
}
}
if (!chosenFormat) FAIL("no ARB pixel format with depth24+stencil8");
PIXELFORMATDESCRIPTOR realPfd{};
if (!DescribePixelFormat(dc, chosenFormat, sizeof(realPfd), &realPfd))
FAIL("DescribePixelFormat(%d) failed", chosenFormat);
if (!SetPixelFormat(dc, chosenFormat, &realPfd)) FAIL("SetPixelFormat (real) failed");
const int ctxAttribs[] = {
WGL_CONTEXT_OPENGL_NO_ERROR_ARB, 1,
WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
WGL_CONTEXT_MINOR_VERSION_ARB, 2,
WGL_CONTEXT_FLAGS_ARB, WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB,
WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
0,
};
HGLRC ctx = wglCreateContextAttribsARB(dc, nullptr, ctxAttribs);
if (!ctx) FAIL("wglCreateContextAttribsARB failed (err=0x%lx)", GetLastError());
if (!p_wglMakeCurrent(dc, ctx)) FAIL("wglMakeCurrent (real) failed");
if (p_wglGetCurrentContext() != ctx || p_wglGetCurrentDC() != dc) FAIL("current context/DC mismatch");
if (!wglSwapIntervalEXT(0)) FAIL("wglSwapIntervalEXT(0) failed");
#define LOAD(fn) \
fn##_ = (PFN_##fn)GetGLProc(#fn); \
if (!fn##_) FAIL("missing GL function " #fn);
LOAD(glGetIntegerv)
LOAD(glClearColor)
LOAD(glClear)
LOAD(glViewport)
LOAD(glGetError)
LOAD(glReadPixels)
LOAD(glFinish)
LOAD(glCreateShader)
LOAD(glShaderSource)
LOAD(glCompileShader)
LOAD(glGetShaderiv)
LOAD(glGetShaderInfoLog)
LOAD(glCreateProgram)
LOAD(glAttachShader)
LOAD(glLinkProgram)
LOAD(glGetProgramiv)
LOAD(glGetProgramInfoLog)
LOAD(glUseProgram)
LOAD(glGenVertexArrays)
LOAD(glBindVertexArray)
LOAD(glGenBuffers)
LOAD(glBindBuffer)
LOAD(glBufferData)
LOAD(glVertexAttribPointer)
LOAD(glEnableVertexAttribArray)
LOAD(glDrawArrays)
#undef LOAD
GLint major = 0, minor = 0;
glGetIntegerv_(GL_MAJOR_VERSION, &major);
glGetIntegerv_(GL_MINOR_VERSION, &minor);
std::printf("real ctx: GL %d.%d | %s | %s\n", major, minor, glGetString_(GL_RENDERER),
glGetString_(GL_VERSION));
if (major < 3 || (major == 3 && minor < 2)) FAIL("context version below 3.2");
const char* vsSrc = "#version 150 core\n"
"in vec2 aPos;\n"
"void main() { gl_Position = vec4(aPos, 0.0, 1.0); }\n";
const char* fsSrc = "#version 150 core\n"
"out vec4 fragColor;\n"
"void main() { fragColor = vec4(1.0, 0.5, 0.0, 1.0); }\n";
GLuint vs = glCreateShader_(GL_VERTEX_SHADER);
glShaderSource_(vs, 1, &vsSrc, nullptr);
glCompileShader_(vs);
GLint ok = 0;
glGetShaderiv_(vs, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[2048];
glGetShaderInfoLog_(vs, sizeof(log), nullptr, log);
FAIL("vertex shader compile failed: %s", log);
}
GLuint fs = glCreateShader_(GL_FRAGMENT_SHADER);
glShaderSource_(fs, 1, &fsSrc, nullptr);
glCompileShader_(fs);
glGetShaderiv_(fs, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[2048];
glGetShaderInfoLog_(fs, sizeof(log), nullptr, log);
FAIL("fragment shader compile failed: %s", log);
}
GLuint prog = glCreateProgram_();
glAttachShader_(prog, vs);
glAttachShader_(prog, fs);
glLinkProgram_(prog);
glGetProgramiv_(prog, GL_LINK_STATUS, &ok);
if (!ok) {
char log[2048];
glGetProgramInfoLog_(prog, sizeof(log), nullptr, log);
FAIL("program link failed: %s", log);
}
const GLfloat verts[] = {-0.8f, -0.8f, 0.8f, -0.8f, 0.0f, 0.8f};
GLuint vao = 0, vbo = 0;
glGenVertexArrays_(1, &vao);
glBindVertexArray_(vao);
glGenBuffers_(1, &vbo);
glBindBuffer_(GL_ARRAY_BUFFER, vbo);
glBufferData_(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
glVertexAttribPointer_(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
glEnableVertexAttribArray_(0);
glViewport_(0, 0, 640, 480);
glUseProgram_(prog);
unsigned char centerPixel[4] = {}, cornerPixel[4] = {};
for (int frame = 0; frame < 60; ++frame) {
MSG msg;
while (PeekMessageA(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessageA(&msg);
}
glClearColor_(0.1f, 0.2f, 0.4f, 1.0f);
glClear_(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glDrawArrays_(GL_TRIANGLES, 0, 3);
if (frame == 58) {
glFinish_();
glReadPixels_(320, 240, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, centerPixel);
glReadPixels_(5, 5, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, cornerPixel);
}
if (!SwapBuffers(dc)) FAIL("gdi32 SwapBuffers failed at frame %d", frame); // gdi32 -> wglSwapBuffers
}
GLenum err = glGetError_();
std::printf("center pixel: %u,%u,%u,%u corner pixel: %u,%u,%u,%u glGetError=0x%x\n", centerPixel[0],
centerPixel[1], centerPixel[2], centerPixel[3], cornerPixel[0], cornerPixel[1], cornerPixel[2],
cornerPixel[3], err);
if (err != GL_NO_ERROR) FAIL("GL error 0x%x", err);
// Orange triangle at center (255,127±3,0), clear color at corner (~25,51,102).
if (!(centerPixel[0] > 200 && centerPixel[1] > 90 && centerPixel[1] < 160 && centerPixel[2] < 50))
FAIL("center pixel is not the triangle color");
if (!(cornerPixel[2] > 60 && cornerPixel[0] < 60)) FAIL("corner pixel is not the clear color");
if (!p_wglMakeCurrent(nullptr, nullptr)) FAIL("final wglMakeCurrent(NULL) failed");
if (!p_wglDeleteContext(ctx)) FAIL("final wglDeleteContext failed");
std::printf("SMOKE PASS\n");
return 0;
}