Compare commits

..
10 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 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 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 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
55 changed files with 4474 additions and 79 deletions
+30
View File
@@ -194,6 +194,9 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
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/OpenGL/Loader.cpp
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
@@ -303,6 +306,13 @@ if (ANDROID)
) )
endif() 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 set(MOBILEGL_LINK_LIBRARIES
glslang::glslang glslang::glslang
spirv-cross-c spirv-cross-c
@@ -335,6 +345,14 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
${SOURCE_FILES} ${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") if (CMAKE_BUILD_TYPE STREQUAL "Debug")
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
C_VISIBILITY_PRESET default C_VISIBILITY_PRESET default
@@ -377,6 +395,18 @@ if(UNIX AND NOT APPLE AND NOT ANDROID)
endforeach() endforeach()
endif() 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) if(NOT ANDROID)
add_library(${CMAKE_PROJECT_NAME}_s STATIC add_library(${CMAKE_PROJECT_NAME}_s STATIC
${SOURCE_FILES} ${SOURCE_FILES}
+5
View File
@@ -80,6 +80,11 @@ namespace MobileGL::MG_Config {
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with // rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry). // subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto; 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 // MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive // strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
// ONE+ONE - the multi-pass depth-equality signature) on drivers without // 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.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS"); features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN"); features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
features.ClipDistanceQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_CLIP_DISTANCE");
features.MagmaDisableBlendedDepthWriteQuirk = features.MagmaDisableBlendedDepthWriteQuirk =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE"); QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS"); features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
+1
View File
@@ -34,6 +34,7 @@
#define MOBILEGL_EGL_API MOBILEGL_API #define MOBILEGL_EGL_API MOBILEGL_API
#define MOBILEGL_CGL_API MOBILEGL_API #define MOBILEGL_CGL_API MOBILEGL_API
#define MOBILEGL_NSOPENGL_API MOBILEGL_API #define MOBILEGL_NSOPENGL_API MOBILEGL_API
#define MOBILEGL_WGL_API MOBILEGL_API
// ====================== MobileGL configurations ======================= // // ====================== MobileGL configurations ======================= //
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL #ifndef MOBILEGL_LOG_ACTIVE_LEVEL
+7 -1
View File
@@ -14,7 +14,13 @@ namespace MobileGL {
} // namespace MG_Config } // namespace MG_Config
namespace MG_Backend { 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; GlobalBackendFunctionsTable gBackendFunctionsTable;
} // namespace MG_Backend } // namespace MG_Backend
} // namespace MobileGL } // namespace MobileGL
+38 -33
View File
@@ -9,14 +9,24 @@
#include "Init.h" #include "Init.h"
#include "Config.h" #include "Config.h"
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
#include <MG_Backend/DirectVulkan/DirectVulkan.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_State/EGLState/Core.h> #include <MG_State/EGLState/Core.h>
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h> #include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h> #include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <atomic>
#include <mutex>
namespace MobileGL { namespace MobileGL {
namespace { 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) { void DestroyImpl(Bool logLifecycle) {
if (!g_isInitialized) { if (!g_isInitialized) {
@@ -64,40 +74,35 @@ namespace MobileGL {
MGLOG_I("MobileGL initialized"); 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() { void Destroy() {
DestroyImpl(true); DestroyImpl(true);
} }
#if defined(__linux__) || defined(__APPLE__) // MobileGL's lifecycle is owned entirely by the host-API layers
__attribute__((constructor)) static void AutoInit() { // (EGL/WGL/CGL): initialization happens lazily on the first entry point
Initialize(); // 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
__attribute__((destructor)) static void AutoDestroy() { // static destructor, and no DllMain: the global singletons use
if (MG_Config::Features.TraceSkipAutodestroy) { // leak-at-exit storage (see GlobalObjects.cpp), so a process that exits
return; // without eglTerminate simply leaks them to the OS instead of running
} // backend destructors during static teardown.
#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
} // namespace MobileGL } // namespace MobileGL
+6
View File
@@ -11,6 +11,12 @@
namespace MobileGL { namespace MobileGL {
void Initialize(); 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(); void Destroy();
namespace MG_Util::Debug { namespace MG_Util::Debug {
+2 -1
View File
@@ -313,7 +313,8 @@ namespace MobileGL {
Android, Android,
X11, X11,
MetalLayer, MetalLayer,
// TODO: Wayland, Windows, etc. Win32, // Handle is an HWND
// TODO: Wayland, etc.
WindowBackendCount, WindowBackendCount,
Unknown = -1 Unknown = -1
}; };
+1 -1
View File
@@ -13,6 +13,6 @@
#include "DirectVulkan/BackendObject_DirectVulkan.h" #include "DirectVulkan/BackendObject_DirectVulkan.h"
namespace MobileGL::MG_Backend { namespace MobileGL::MG_Backend {
extern UniquePtr<BackendObject> pActiveBackendObject; extern UniquePtr<BackendObject>& pActiveBackendObject;
extern GlobalBackendFunctionsTable gBackendFunctionsTable; extern GlobalBackendFunctionsTable gBackendFunctionsTable;
} // namespace MobileGL::MG_Backend } // namespace MobileGL::MG_Backend
@@ -701,9 +701,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
if ((handle.Backend != WindowBackend::Android && if ((handle.Backend != WindowBackend::Android &&
handle.Backend != WindowBackend::X11 && handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer) || handle.Backend != WindowBackend::MetalLayer &&
handle.Backend != WindowBackend::Win32) ||
!handle.Handle) { !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; return false;
} }
+30 -2
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) { static Bool SupportsLayeredImageBinding(TextureTarget target) {
@@ -4223,9 +4237,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!ChooseConfigForSurface(surfaceBit, g_Config, window)) return false; 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); g_Context = g_EGLFuncs.eglCreateContext(g_Display, g_Config, EGL_NO_CONTEXT, contextAttribs);
if (g_Context != EGL_NO_CONTEXT) {
return true;
}
}
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; return g_Context != EGL_NO_CONTEXT;
} }
+104 -2
View File
@@ -1262,15 +1262,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto& allAttributes = stateVAOObject->GetAllAttributes(); const auto& allAttributes = stateVAOObject->GetAllAttributes();
for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) { for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) {
const auto& attrib = allAttributes[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 != Bool needsSyncSwitch = allAttributeVersions[attribIndex].SwitchVersion !=
m_syncedAttributeVersions[attribIndex].SwitchVersion; m_syncedAttributeVersions[attribIndex].SwitchVersion;
if (needsSyncSwitch) { if (needsSyncSwitch || unsourceable != wasForceDisabled) {
if (attrib.Enabled) { if (attrib.Enabled && !unsourceable) {
g_GLESFuncs.glEnableVertexAttribArray(attribIndex); g_GLESFuncs.glEnableVertexAttribArray(attribIndex);
} else { } else {
g_GLESFuncs.glDisableVertexAttribArray(attribIndex); g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
} }
} }
if (unsourceable) {
m_forceDisabledAttribsMask |= attribBit;
} else {
m_forceDisabledAttribsMask &= ~attribBit;
}
Bool needsSyncFormat = allAttributeVersions[attribIndex].FormatVersion != Bool needsSyncFormat = allAttributeVersions[attribIndex].FormatVersion !=
m_syncedAttributeVersions[attribIndex].FormatVersion; m_syncedAttributeVersions[attribIndex].FormatVersion;
@@ -1278,7 +1294,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_syncedAttributeVersions[attribIndex].BufferVersion; m_syncedAttributeVersions[attribIndex].BufferVersion;
if (!needsSyncFormat && !needsSyncBuffer) continue; 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 (!BindAttributeBuffer(attrib)) {
if (attrib.Enabled) {
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
m_forceDisabledAttribsMask |= attribBit;
}
continue; continue;
} }
@@ -3265,6 +3291,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
auto& shaderSpirvs = stateProgramObject->GetGeneratedSpirv(); 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) { for (int index = 0; index < attachedShaders.size(); ++index) {
auto& shader = attachedShaders[index]; auto& shader = attachedShaders[index];
GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(shader->GetShaderStage()); GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(shader->GetShaderStage());
@@ -3287,6 +3326,63 @@ namespace MobileGL::MG_Backend::DirectGLES {
effectiveSpirv = &loweredSpirv; 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 // ESSL stage-matches uniform blocks by member precision, but SPIRV-Cross prints
// a RelaxedPrecision member as explicit "mediump" in the vertex stage and as // a RelaxedPrecision member as explicit "mediump" in the vertex stage and as
// UNQUALIFIED (mediump-by-default) in the fragment stage; after // 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 = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
source = RemoveLayoutBinding(source); 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 = ProcessOutColorLocations(source);
source = ForceFlatIntegerVaryings(source, glShaderType); source = ForceFlatIntegerVaryings(source, glShaderType);
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType); source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
@@ -258,6 +258,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
private: private:
Uint m_backendVAOId = 0; Uint m_backendVAOId = 0;
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds; Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
// 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; Bool m_isInitialized = false;
Uint16 m_syncedIndexBufferVersion = 0; Uint16 m_syncedIndexBufferVersion = 0;
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
+35
View File
@@ -342,6 +342,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
return result; 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 PrgramImpl
namespace Utils { namespace Utils {
+1
View File
@@ -105,6 +105,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint32 unormOutputMask); Uint32 unormOutputMask);
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType); String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
String RemoveLayoutBinding(const String& glslCode); String RemoveLayoutBinding(const String& glslCode);
String RemoveClipDistanceRedeclaration(const String& glslCode);
} // namespace PrgramImpl } // namespace PrgramImpl
namespace Utils { namespace Utils {
@@ -411,8 +411,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
if (!handle.Handle || (handle.Backend != WindowBackend::Android && if (!handle.Handle || (handle.Backend != WindowBackend::Android &&
handle.Backend != WindowBackend::X11 && handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer)) { handle.Backend != WindowBackend::MetalLayer &&
MGLOG_E("DirectVulkan backend only supports Android, X11, and CAMetalLayer native windows"); handle.Backend != WindowBackend::Win32)) {
MGLOG_E("DirectVulkan backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
return false; return false;
} }
@@ -20,7 +20,8 @@
#include <spirv_reflect.h> #include <spirv_reflect.h>
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
UniquePtr<VulkanRenderer> pVulkanRenderer = nullptr; // Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<VulkanRenderer>& pVulkanRenderer = *new UniquePtr<VulkanRenderer>();
namespace { namespace {
// Generation of the live VulkanRenderer instance, mirroring // Generation of the live VulkanRenderer instance, mirroring
@@ -12,7 +12,7 @@
#include "Renderer/VulkanRenderer.h" #include "Renderer/VulkanRenderer.h"
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
extern UniquePtr<VulkanRenderer> pVulkanRenderer; extern UniquePtr<VulkanRenderer>& pVulkanRenderer;
// Generation of the live VulkanRenderer instance, mirroring DirectGLES's // Generation of the live VulkanRenderer instance, mirroring DirectGLES's
// g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever // g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever
@@ -2528,6 +2528,11 @@ void main() {
MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed."); MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed.");
// Prime the first frame so Render() always targets an acquired swapchain image. // Prime the first frame so Render() always targets an acquired swapchain image.
// 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 = VkResult acquireResult =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
if (acquireResult == VK_ERROR_OUT_OF_DATE_KHR || acquireResult == VK_SUBOPTIMAL_KHR) { if (acquireResult == VK_ERROR_OUT_OF_DATE_KHR || acquireResult == VK_SUBOPTIMAL_KHR) {
@@ -2537,6 +2542,9 @@ void main() {
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
} }
VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage"); VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage");
} else {
MGLOG_W("DirectVulkan: no swapchain at initialization (zero-area window); deferring first acquire");
}
m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_convertedVertexStreams.clear(); m_convertedVertexStreams.clear();
@@ -7353,6 +7361,33 @@ void main() {
} }
void VulkanRenderer::Present() { 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(), MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(),
"Present, acquired image index out of range"); "Present, acquired image index out of range");
m_renderPassManager->OnPresent(); m_renderPassManager->OnPresent();
@@ -7386,14 +7421,26 @@ void main() {
auto result = vkQueuePresentKHR(m_presentQueue, &presentPacket.presentInfo); auto result = vkQueuePresentKHR(m_presentQueue, &presentPacket.presentInfo);
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
MGLOG_D("Present, vkQueuePresentKHR got %d, recreating swapchain", result); 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; m_swapchainResizeRequested = false;
result = VK_SUCCESS; result = VK_SUCCESS;
} }
VK_VERIFY(result, "Present, vkQueuePresentKHR"); VK_VERIFY(result, "Present, vkQueuePresentKHR");
if (m_swapchainResizeRequested) { if (m_swapchainResizeRequested) {
MGLOG_D("Present, processing requested swapchain resize"); 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; m_swapchainResizeRequested = false;
} }
@@ -7404,7 +7451,12 @@ void main() {
result = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); result = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
MGLOG_D("Present, vkAcquireNextImageKHR got %d, recreating swapchain", result); 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; m_swapchainResizeRequested = false;
result = result =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
@@ -8056,6 +8108,7 @@ void main() {
MOBILEGL_ASSERT(hwnd, "HWND is null"); MOBILEGL_ASSERT(hwnd, "HWND is null");
VkWin32SurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR}; VkWin32SurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR};
sci.hinstance = GetModuleHandleW(nullptr);
sci.hwnd = hwnd; sci.hwnd = hwnd;
VK_VERIFY(vkCreateWin32SurfaceKHR(m_instance, &sci, nullptr, &m_surface), "vkCreateWin32SurfaceKHR failed"); VK_VERIFY(vkCreateWin32SurfaceKHR(m_instance, &sci, nullptr, &m_surface), "vkCreateWin32SurfaceKHR failed");
#elif defined VK_USE_PLATFORM_METAL_EXT #elif defined VK_USE_PLATFORM_METAL_EXT
@@ -8217,13 +8270,13 @@ void main() {
m_swapchainObject.Shutdown(m_device); m_swapchainObject.Shutdown(m_device);
} }
void VulkanRenderer::RecreateSwapchain() { Bool VulkanRenderer::RecreateSwapchain() {
// Handle cases like minimize on Windows, where swapchain could return a 0x0 extent // Handle cases like minimize on Windows, where swapchain could return a 0x0 extent
const auto swapchainCapabilities = const auto swapchainCapabilities =
SwapchainObject::GetSwapchainCapabilities(m_physicalDevice.handle, m_surface); SwapchainObject::GetSwapchainCapabilities(m_physicalDevice.handle, m_surface);
if (swapchainCapabilities.capabilities.currentExtent.width == 0 || if (swapchainCapabilities.capabilities.currentExtent.width == 0 ||
swapchainCapabilities.capabilities.currentExtent.height == 0) { swapchainCapabilities.capabilities.currentExtent.height == 0) {
return; return false;
} }
vkDeviceWaitIdle(m_device); vkDeviceWaitIdle(m_device);
@@ -8267,6 +8320,7 @@ void main() {
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_convertedVertexStreams.clear(); m_convertedVertexStreams.clear();
} }
return true;
} }
const PhysicalDevice& VulkanRenderer::GetPhysicalDevice() const { const PhysicalDevice& VulkanRenderer::GetPhysicalDevice() const {
@@ -257,7 +257,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const; Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;
void RequestSwapchainResize(Uint32 width, Uint32 height); 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: private:
struct BlitUniformData { struct BlitUniformData {
@@ -355,6 +357,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void* m_platformCloseDisplay = nullptr; void* m_platformCloseDisplay = nullptr;
VulkanRendererConfig m_config; VulkanRendererConfig m_config;
Bool m_swapchainResizeRequested = false; 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 // Vulkan objects
Bool m_validationLayersEnabled = false; Bool m_validationLayersEnabled = false;
+28 -5
View File
@@ -8,6 +8,7 @@
#include "EGLImpl.h" #include "EGLImpl.h"
#include "../GetProcAddress.h" #include "../GetProcAddress.h"
#include <Init.h>
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
#include <MG_State/EGLState/Core.h> #include <MG_State/EGLState/Core.h>
#include <mutex> #include <mutex>
@@ -25,6 +26,17 @@ namespace MobileGL::MG_Impl::EGLImpl {
return MG_State::pEGLContext.get(); 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) { MG_Backend::BackendObject* GetBackendObject(EGLStateContext* state) {
auto* backendObject = MG_Backend::pActiveBackendObject.get(); auto* backendObject = MG_Backend::pActiveBackendObject.get();
if (!backendObject && state) { if (!backendObject && state) {
@@ -49,6 +61,8 @@ namespace MobileGL::MG_Impl::EGLImpl {
return MG_Backend::WindowBackend::Android; return MG_Backend::WindowBackend::Android;
#elif defined(__APPLE__) #elif defined(__APPLE__)
return MG_Backend::WindowBackend::MetalLayer; return MG_Backend::WindowBackend::MetalLayer;
#elif defined(_WIN32)
return MG_Backend::WindowBackend::Win32;
#elif defined(__linux__) #elif defined(__linux__)
return MG_Backend::WindowBackend::X11; return MG_Backend::WindowBackend::X11;
#else #else
@@ -187,7 +201,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
} }
EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) { EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) {
auto* state = GetState(); auto* state = GetStateEnsureInitialized();
if (!state) { if (!state) {
return EGL_FALSE; return EGL_FALSE;
} }
@@ -208,7 +222,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
} }
EGLDisplay GetDisplay(NativeDisplayType display) { EGLDisplay GetDisplay(NativeDisplayType display) {
auto* state = GetState(); auto* state = GetStateEnsureInitialized();
if (!state) { if (!state) {
return EGL_NO_DISPLAY; return EGL_NO_DISPLAY;
} }
@@ -313,6 +327,14 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) { if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
backendObject->ReleaseEGLResources(); 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; return EGL_TRUE;
} }
@@ -345,7 +367,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
} }
EGLBoolean BindAPI(EGLenum api) { EGLBoolean BindAPI(EGLenum api) {
auto* state = GetState(); auto* state = GetStateEnsureInitialized();
if (!state) { if (!state) {
return EGL_FALSE; return EGL_FALSE;
} }
@@ -378,7 +400,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
} }
char const* QueryString(EGLDisplay display, EGLint name) { char const* QueryString(EGLDisplay display, EGLint name) {
auto* state = GetState(); auto* state = GetStateEnsureInitialized();
if (!state) { if (!state) {
return nullptr; return nullptr;
} }
@@ -641,7 +663,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list) { EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list) {
(void)attrib_list; (void)attrib_list;
auto* state = GetState(); auto* state = GetStateEnsureInitialized();
if (!state) { if (!state) {
return EGL_NO_DISPLAY; return EGL_NO_DISPLAY;
} }
@@ -737,6 +759,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!name) { if (!name) {
return nullptr; return nullptr;
} }
MobileGL::EnsureInitialized();
MGLOG_D("eglGetProcAddress(%s)", name); MGLOG_D("eglGetProcAddress(%s)", name);
void* proc = MG_Impl::GetProcAddress(name); void* proc = MG_Impl::GetProcAddress(name);
@@ -2144,6 +2144,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
namespace FramebufferImpl { namespace FramebufferImpl {
UniquePtr<DefaultFramebufferInfo> pDefaultFramebufferInfo; // Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<DefaultFramebufferInfo>& pDefaultFramebufferInfo = *new UniquePtr<DefaultFramebufferInfo>();
} // namespace FramebufferImpl } // namespace FramebufferImpl
} // namespace MobileGL::MG_Impl::GLImpl } // namespace MobileGL::MG_Impl::GLImpl
@@ -78,6 +78,6 @@ namespace MobileGL::MG_Impl::GLImpl {
SharedPtr<MG_State::GLState::ITextureObject> stencilAttachment; SharedPtr<MG_State::GLState::ITextureObject> stencilAttachment;
}; };
extern UniquePtr<DefaultFramebufferInfo> pDefaultFramebufferInfo; extern UniquePtr<DefaultFramebufferInfo>& pDefaultFramebufferInfo;
} // namespace FramebufferImpl } // namespace FramebufferImpl
} // namespace MobileGL::MG_Impl::GLImpl } // namespace MobileGL::MG_Impl::GLImpl
@@ -14,7 +14,8 @@
#include <MG_State/GLState/TextureState/TextureObjectStubs.h> #include <MG_State/GLState/TextureState/TextureObjectStubs.h>
namespace MobileGL::MG_Impl::GLImpl::TextureImpl { 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) { Bool IsProxyTextureTarget(TextureUploadTarget target) {
switch (target) { switch (target) {
@@ -23,5 +23,5 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
UnorderedMap<TextureUploadTarget, SharedPtr<MG_State::GLState::ITextureObject>> m_proxyTexturesMap; UnorderedMap<TextureUploadTarget, SharedPtr<MG_State::GLState::ITextureObject>> m_proxyTexturesMap;
}; };
extern UniquePtr<ProxyTextureManager> pProxyTextureManager; extern UniquePtr<ProxyTextureManager>& pProxyTextureManager;
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl } // 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; 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, Bool EGLContext::ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs,
EGLint configSize, EGLint* numConfig) { EGLint configSize, EGLint* numConfig) {
const std::lock_guard<std::recursive_mutex> lock(m_mutex); const std::lock_guard<std::recursive_mutex> lock(m_mutex);
@@ -1415,6 +1435,7 @@ namespace MobileGL {
} }
} // namespace EGLState } // 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 MG_State
} // namespace MobileGL } // namespace MobileGL
+5 -1
View File
@@ -39,6 +39,10 @@ namespace MobileGL {
Bool IsDisplayInitialized(EGLDisplayHandle display) const; Bool IsDisplayInitialized(EGLDisplayHandle display) const;
Bool InitializeDisplay(EGLDisplayHandle display, EGLint* major, EGLint* minor); Bool InitializeDisplay(EGLDisplayHandle display, EGLint* major, EGLint* minor);
Bool TerminateDisplay(EGLDisplayHandle display); 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 // Config
Bool ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs, Bool ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs,
@@ -262,6 +266,6 @@ namespace MobileGL {
}; };
} // namespace EGLState } // namespace EGLState
extern UniquePtr<EGLState::EGLContext> pEGLContext; extern UniquePtr<EGLState::EGLContext>& pEGLContext;
} // namespace MG_State } // namespace MG_State
} // namespace MobileGL } // namespace MobileGL
+2 -1
View File
@@ -721,5 +721,6 @@ namespace MobileGL::MG_State {
} }
} // namespace GLState } // 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 } // namespace MobileGL::MG_State
+1 -1
View File
@@ -252,7 +252,7 @@ namespace MobileGL {
}; };
} // namespace GLState } // 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 // 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 // current EGL context explicitly requested a core profile (core bit in
@@ -9,7 +9,12 @@
#include "Loader.h" #include "Loader.h"
#include "MG_Util/Types.h" #include "MG_Util/Types.h"
#include <Config.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> #include <dlfcn.h>
#endif #endif
@@ -60,7 +65,14 @@ namespace MobileGL::MG_Util::BackendLoader {
#endif #endif
static void* OpenLib(const Vector<String>& names) { 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[] = { static const String LibPathPrefixes[] = {
#if defined(MOBILEGL_IOS) #if defined(MOBILEGL_IOS)
"@rpath/", "@executable_path/Frameworks/", "@loader_path/Frameworks/", "@rpath/", "@executable_path/Frameworks/", "@loader_path/Frameworks/",
@@ -104,7 +116,9 @@ namespace MobileGL::MG_Util::BackendLoader {
} }
inline void* ProcAddress(void* lib, const char* name) { 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); return dlsym(lib, name);
#else #else
return nullptr; return nullptr;
@@ -520,6 +534,15 @@ namespace MobileGL::MG_Util::BackendLoader {
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS) && defined(__ANDROID__) #if defined(MOBILEGL_TRACE_ANGLE_VARIANTS) && defined(__ANDROID__)
void* angleGlesLib = nullptr; void* angleGlesLib = nullptr;
#endif #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()) { if (UseAngle()) {
void* glesLib = OpenLib({"libGLESv2_angle.so"}); void* glesLib = OpenLib({"libGLESv2_angle.so"});
if (!glesLib) { if (!glesLib) {
@@ -541,6 +564,7 @@ namespace MobileGL::MG_Util::BackendLoader {
eglLib = OpenLib({"libEGL.so"}); eglLib = OpenLib({"libEGL.so"});
#endif #endif
} }
#endif // !_WIN32
if (!eglLib) { if (!eglLib) {
MGLOG_E("Failed to open EGL library"); MGLOG_E("Failed to open EGL library");
+7 -3
View File
@@ -9,10 +9,14 @@
#pragma once #pragma once
#include <Includes.h> #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_DEBUG 0
#define MOBILEGL_LOG_LEVEL_WARN 1 #define MOBILEGL_LOG_LEVEL_INFO 1
#define MOBILEGL_LOG_LEVEL_ERROR 2 #define MOBILEGL_LOG_LEVEL_WARN 2
#define MOBILEGL_LOG_LEVEL_INFO 3 #define MOBILEGL_LOG_LEVEL_ERROR 3
#define MOBILEGL_LOG_LEVEL_FATAL 4 #define MOBILEGL_LOG_LEVEL_FATAL 4
#define MOBILEGL_LOG_INTERNAL(levelTag, androidLogLevel, fmt, ...) \ #define MOBILEGL_LOG_INTERNAL(levelTag, androidLogLevel, fmt, ...) \
@@ -22,6 +22,9 @@
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h" #include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
#include "SpirvPasses/StripNoPerspectivePass.h" #include "SpirvPasses/StripNoPerspectivePass.h"
#include "SpirvPasses/EmulateNoPerspectivePass.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/libspirv.h"
#include "spirv-tools/optimizer.hpp" #include "spirv-tools/optimizer.hpp"
@@ -323,6 +326,55 @@ namespace MobileGL {
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); 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, bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) { Vector<uint32_t>& outputBinary) {
using namespace spvtools; using namespace spvtools;
@@ -27,6 +27,36 @@ namespace MobileGL {
// Only for backends without native draw-parameter support (DirectGLES). // Only for backends without native draw-parameter support (DirectGLES).
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary, static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary); 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 // Drops RelaxedPrecision member decorations from uniform-block structs so
// SPIRV-Cross prints the same (highp) member precision in every stage; ES // SPIRV-Cross prints the same (highp) member precision in every stage; ES
// drivers reject cross-stage uniform blocks whose member precisions differ. // 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
+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;
}