Compare commits

...
6 Commits
Author SHA1 Message Date
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
40 changed files with 3182 additions and 74 deletions
+28 -1
View File
@@ -303,6 +303,13 @@ if (ANDROID)
)
endif()
if (WIN32)
list(APPEND SOURCE_FILES
MobileGL/MG_Impl/WGLImpl/WGLImpl.cpp
MobileGL/MG_Impl/WGLImpl/Exporting/Definitions.cpp
)
endif()
set(MOBILEGL_LINK_LIBRARIES
glslang::glslang
spirv-cross-c
@@ -331,10 +338,18 @@ set(MOBILEGL_INCLUDE_DIR
${SPIRV-Headers_SOURCE_DIR}/include
)
add_library(${CMAKE_PROJECT_NAME} SHARED
add_library(${CMAKE_PROJECT_NAME} SHARED
${SOURCE_FILES}
)
if (WIN32)
# The wgl* entry points are exported via .def (see the comment in wgl.def);
# only the shared library links it.
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
MobileGL/MG_Impl/WGLImpl/Exporting/wgl.def
)
endif()
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
C_VISIBILITY_PRESET default
@@ -377,6 +392,18 @@ if(UNIX AND NOT APPLE AND NOT ANDROID)
endforeach()
endif()
if(WIN32)
# Drop-in for the classic GL loader path: a copy named opengl32.dll placed
# next to a host executable is what LoadLibrary("opengl32.dll") and gdi32's
# pixel-format forwarding will resolve.
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:${CMAKE_PROJECT_NAME}>"
"$<TARGET_FILE_DIR:${CMAKE_PROJECT_NAME}>/opengl32.dll"
COMMENT "Creating opengl32.dll drop-in copy"
)
endif()
if(NOT ANDROID)
add_library(${CMAKE_PROJECT_NAME}_s STATIC
${SOURCE_FILES}
+1
View File
@@ -34,6 +34,7 @@
#define MOBILEGL_EGL_API MOBILEGL_API
#define MOBILEGL_CGL_API MOBILEGL_API
#define MOBILEGL_NSOPENGL_API MOBILEGL_API
#define MOBILEGL_WGL_API MOBILEGL_API
// ====================== MobileGL configurations ======================= //
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL
+7 -1
View File
@@ -14,7 +14,13 @@ namespace MobileGL {
} // namespace MG_Config
namespace MG_Backend {
UniquePtr<BackendObject> pActiveBackendObject;
// Leak-at-exit storage: the UniquePtr itself lives on the heap and is
// never destroyed by the runtime, so process exit runs no backend
// destructors (static destruction order across TUs is undefined).
// Deterministic teardown happens inside the EGL lifecycle instead:
// the last eglTerminate calls MobileGL::Destroy(), which .reset()s
// these singletons while the process is still healthy.
UniquePtr<BackendObject>& pActiveBackendObject = *new UniquePtr<BackendObject>();
GlobalBackendFunctionsTable gBackendFunctionsTable;
} // namespace MG_Backend
} // namespace MobileGL
+38 -33
View File
@@ -9,14 +9,24 @@
#include "Init.h"
#include "Config.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_Backend/DirectVulkan/DirectVulkan.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/EGLState/Core.h>
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <atomic>
#include <mutex>
namespace MobileGL {
namespace {
Bool g_isInitialized = false;
std::atomic<Bool> g_isInitialized = false;
thread_local Bool tl_initializing = false;
std::mutex& InitMutex() {
static std::mutex mutex;
return mutex;
}
void DestroyImpl(Bool logLifecycle) {
if (!g_isInitialized) {
@@ -64,40 +74,35 @@ namespace MobileGL {
MGLOG_I("MobileGL initialized");
}
void EnsureInitialized() {
if (g_isInitialized.load(std::memory_order_acquire)) {
return;
}
// Re-entrant call while this thread is already inside Initialize()
// (e.g. an init step routing back through a public entry point).
if (tl_initializing) {
return;
}
const std::lock_guard<std::mutex> lock(InitMutex());
if (g_isInitialized.load(std::memory_order_acquire)) {
return;
}
tl_initializing = true;
Initialize();
tl_initializing = false;
}
void Destroy() {
DestroyImpl(true);
}
#if defined(__linux__) || defined(__APPLE__)
__attribute__((constructor)) static void AutoInit() {
Initialize();
}
__attribute__((destructor)) static void AutoDestroy() {
if (MG_Config::Features.TraceSkipAutodestroy) {
return;
}
#if defined(__APPLE__)
// macOS injected dylibs can run destructors after logging/backend static state is already torn down.
return;
#else
DestroyImpl(false);
#endif
}
#endif
#ifdef _WIN32
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
Initialize();
break;
case DLL_PROCESS_DETACH:
Destroy();
break;
}
return TRUE;
}
#endif
// MobileGL's lifecycle is owned entirely by the host-API layers
// (EGL/WGL/CGL): initialization happens lazily on the first entry point
// via EnsureInitialized(), and full teardown happens deterministically
// when the last EGL display is terminated with nothing current (EGLImpl
// calls Destroy()). There is intentionally no static constructor, no
// static destructor, and no DllMain: the global singletons use
// leak-at-exit storage (see GlobalObjects.cpp), so a process that exits
// without eglTerminate simply leaks them to the OS instead of running
// backend destructors during static teardown.
} // namespace MobileGL
+6
View File
@@ -11,6 +11,12 @@
namespace MobileGL {
void Initialize();
// Thread-safe, idempotent, and re-entrant wrapper around Initialize().
// Host layers (EGL/WGL/CGL entry points) call this lazily on first use so
// MobileGL's lifecycle never depends on ELF/DLL static constructors, and
// so a fresh init can follow a full Destroy() (e.g. after the last
// eglTerminate).
void EnsureInitialized();
void Destroy();
namespace MG_Util::Debug {
+2 -1
View File
@@ -313,7 +313,8 @@ namespace MobileGL {
Android,
X11,
MetalLayer,
// TODO: Wayland, Windows, etc.
Win32, // Handle is an HWND
// TODO: Wayland, etc.
WindowBackendCount,
Unknown = -1
};
+1 -1
View File
@@ -13,6 +13,6 @@
#include "DirectVulkan/BackendObject_DirectVulkan.h"
namespace MobileGL::MG_Backend {
extern UniquePtr<BackendObject> pActiveBackendObject;
extern UniquePtr<BackendObject>& pActiveBackendObject;
extern GlobalBackendFunctionsTable gBackendFunctionsTable;
} // namespace MobileGL::MG_Backend
@@ -701,9 +701,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
if ((handle.Backend != WindowBackend::Android &&
handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer) ||
handle.Backend != WindowBackend::MetalLayer &&
handle.Backend != WindowBackend::Win32) ||
!handle.Handle) {
MGLOG_E("DirectGLES backend only supports Android, X11, and CAMetalLayer native windows");
MGLOG_E("DirectGLES backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
return false;
}
+16 -2
View File
@@ -4307,9 +4307,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!ChooseConfigForSurface(surfaceBit, g_Config, window)) return false;
const EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
// Negotiate the highest ES 3.x context. Version-strict EGL implementations
// (ANGLE) return exactly the requested minor, and a bare CLIENT_VERSION 3
// request yields a 3.0 context that lacks the 3.1/3.2 texture targets the
// capability probes exercise; mobile drivers ignore the minor and hand out
// their maximum either way.
for (const EGLint minorVersion : {2, 1, 0}) {
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3,
EGL_CONTEXT_MINOR_VERSION, minorVersion,
EGL_NONE};
g_Context = g_EGLFuncs.eglCreateContext(g_Display, g_Config, EGL_NO_CONTEXT, contextAttribs);
if (g_Context != EGL_NO_CONTEXT) {
return true;
}
}
g_Context = g_EGLFuncs.eglCreateContext(g_Display, g_Config, EGL_NO_CONTEXT, contextAttribs);
const EGLint legacyContextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
g_Context = g_EGLFuncs.eglCreateContext(g_Display, g_Config, EGL_NO_CONTEXT, legacyContextAttribs);
return g_Context != EGL_NO_CONTEXT;
}
@@ -397,8 +397,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (!handle.Handle || (handle.Backend != WindowBackend::Android &&
handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer)) {
MGLOG_E("DirectVulkan backend only supports Android, X11, and CAMetalLayer native windows");
handle.Backend != WindowBackend::MetalLayer &&
handle.Backend != WindowBackend::Win32)) {
MGLOG_E("DirectVulkan backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
return false;
}
@@ -20,7 +20,8 @@
#include <spirv_reflect.h>
namespace MobileGL::MG_Backend::DirectVulkan {
UniquePtr<VulkanRenderer> pVulkanRenderer = nullptr;
// Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<VulkanRenderer>& pVulkanRenderer = *new UniquePtr<VulkanRenderer>();
namespace {
// Generation of the live VulkanRenderer instance, mirroring
@@ -12,7 +12,7 @@
#include "Renderer/VulkanRenderer.h"
namespace MobileGL::MG_Backend::DirectVulkan {
extern UniquePtr<VulkanRenderer> pVulkanRenderer;
extern UniquePtr<VulkanRenderer>& pVulkanRenderer;
// Generation of the live VulkanRenderer instance, mirroring DirectGLES's
// g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever
@@ -2153,15 +2153,23 @@ void main() {
MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed.");
// Prime the first frame so Render() always targets an acquired swapchain image.
VkResult acquireResult =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
if (acquireResult == VK_ERROR_OUT_OF_DATE_KHR || acquireResult == VK_SUBOPTIMAL_KHR) {
MGLOG_D("Initialize, vkAcquireNextImageKHR got %d, recreating swapchain", acquireResult);
RecreateSwapchain();
acquireResult =
// A zero-area window (GLFW's hidden helper window during the WGL bootstrap, or a
// window that is already minimized) legitimately yields no swapchain here; defer
// the first acquire to Present in that case instead of acquiring from a null
// swapchain handle.
if (m_swapchainObject.GetHandle() != VK_NULL_HANDLE) {
VkResult acquireResult =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
if (acquireResult == VK_ERROR_OUT_OF_DATE_KHR || acquireResult == VK_SUBOPTIMAL_KHR) {
MGLOG_D("Initialize, vkAcquireNextImageKHR got %d, recreating swapchain", acquireResult);
RecreateSwapchain();
acquireResult =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
}
VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage");
} else {
MGLOG_W("DirectVulkan: no swapchain at initialization (zero-area window); deferring first acquire");
}
VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage");
m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_convertedVertexStreams.clear();
@@ -6857,6 +6865,33 @@ void main() {
}
void VulkanRenderer::Present() {
if (m_swapchainObject.GetHandle() == VK_NULL_HANDLE || m_presentSuspended) {
// No usable swapchain: the window was zero-area at initialization, or
// presentation was suspended when the window minimized. Try to bring a
// swapchain up now that the window may have a real size; until then, drop
// this frame's recording instead of submitting - a submit would wait on a
// never-signaled acquire semaphore and reuse a still-signaled fence.
if (!RecreateSwapchain()) {
auto& suspendedFrame = m_frameContext.GetCurrent();
if (VkRenderPassManager::GetActiveRenderPass()) {
VkRenderPassManager::EndRenderPass(suspendedFrame.commandBuffer);
}
if (suspendedFrame.isCommandRecording) {
m_frameContext.EndCommandRecording();
}
suspendedFrame.isCommandRecording = false;
suspendedFrame.hasCommandBufferRecorded = false;
m_lastPipelineValid = false;
MGLOG_D("Present skipped: no usable swapchain (zero-area window)");
return;
}
m_presentSuspended = false;
const VkResult acquireResult =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
if (acquireResult != VK_SUBOPTIMAL_KHR) {
VK_VERIFY(acquireResult, "Present, deferred first WaitAndAcquireNextImage");
}
}
MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(),
"Present, acquired image index out of range");
m_renderPassManager->OnPresent();
@@ -6890,14 +6925,26 @@ void main() {
auto result = vkQueuePresentKHR(m_presentQueue, &presentPacket.presentInfo);
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
MGLOG_D("Present, vkQueuePresentKHR got %d, recreating swapchain", result);
RecreateSwapchain();
if (!RecreateSwapchain()) {
// Window went zero-area (minimize) with the swapchain out of date:
// stop submitting/acquiring until it has a size again.
m_presentSuspended = true;
m_swapchainResizeRequested = false;
MGLOG_D("Present, zero-area window with out-of-date swapchain; suspending presentation");
return;
}
m_swapchainResizeRequested = false;
result = VK_SUCCESS;
}
VK_VERIFY(result, "Present, vkQueuePresentKHR");
if (m_swapchainResizeRequested) {
MGLOG_D("Present, processing requested swapchain resize");
RecreateSwapchain();
if (!RecreateSwapchain()) {
m_presentSuspended = true;
m_swapchainResizeRequested = false;
MGLOG_D("Present, zero-area window on requested resize; suspending presentation");
return;
}
m_swapchainResizeRequested = false;
}
@@ -6908,7 +6955,12 @@ void main() {
result = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
MGLOG_D("Present, vkAcquireNextImageKHR got %d, recreating swapchain", result);
RecreateSwapchain();
if (!RecreateSwapchain()) {
m_presentSuspended = true;
m_swapchainResizeRequested = false;
MGLOG_D("Present, zero-area window on next-frame acquire; suspending presentation");
return;
}
m_swapchainResizeRequested = false;
result =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
@@ -7560,6 +7612,7 @@ void main() {
MOBILEGL_ASSERT(hwnd, "HWND is null");
VkWin32SurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR};
sci.hinstance = GetModuleHandleW(nullptr);
sci.hwnd = hwnd;
VK_VERIFY(vkCreateWin32SurfaceKHR(m_instance, &sci, nullptr, &m_surface), "vkCreateWin32SurfaceKHR failed");
#elif defined VK_USE_PLATFORM_METAL_EXT
@@ -7721,13 +7774,13 @@ void main() {
m_swapchainObject.Shutdown(m_device);
}
void VulkanRenderer::RecreateSwapchain() {
Bool VulkanRenderer::RecreateSwapchain() {
// Handle cases like minimize on Windows, where swapchain could return a 0x0 extent
const auto swapchainCapabilities =
SwapchainObject::GetSwapchainCapabilities(m_physicalDevice.handle, m_surface);
if (swapchainCapabilities.capabilities.currentExtent.width == 0 ||
swapchainCapabilities.capabilities.currentExtent.height == 0) {
return;
return false;
}
vkDeviceWaitIdle(m_device);
@@ -7771,6 +7824,7 @@ void main() {
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_convertedVertexStreams.clear();
}
return true;
}
const PhysicalDevice& VulkanRenderer::GetPhysicalDevice() const {
@@ -257,7 +257,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;
void RequestSwapchainResize(Uint32 width, Uint32 height);
void RecreateSwapchain();
// Returns false when the surface is zero-area (minimized/hidden window):
// no new swapchain is installed and presentation must stay suspended.
Bool RecreateSwapchain();
private:
struct BlitUniformData {
@@ -355,6 +357,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void* m_platformCloseDisplay = nullptr;
VulkanRendererConfig m_config;
Bool m_swapchainResizeRequested = false;
// Presentation is suspended while the window is zero-area (minimized): the
// swapchain is unusable/out of date, so Present drops frames instead of
// submitting on a signaled fence / presenting never-acquired images.
Bool m_presentSuspended = false;
// Vulkan objects
Bool m_validationLayersEnabled = false;
+28 -5
View File
@@ -8,6 +8,7 @@
#include "EGLImpl.h"
#include "../GetProcAddress.h"
#include <Init.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_State/EGLState/Core.h>
#include <mutex>
@@ -25,6 +26,17 @@ namespace MobileGL::MG_Impl::EGLImpl {
return MG_State::pEGLContext.get();
}
// Entry points that can legitimately be an application's FIRST EGL
// call (display/proc-address/string queries) lazily bring MobileGL
// up here, so the library needs no static constructor and can
// re-initialize after the last eglTerminate tore everything down.
// Teardown-ish entry points keep using GetState() and fail benignly
// when MobileGL is not initialized.
EGLStateContext* GetStateEnsureInitialized() {
MobileGL::EnsureInitialized();
return GetState();
}
MG_Backend::BackendObject* GetBackendObject(EGLStateContext* state) {
auto* backendObject = MG_Backend::pActiveBackendObject.get();
if (!backendObject && state) {
@@ -49,6 +61,8 @@ namespace MobileGL::MG_Impl::EGLImpl {
return MG_Backend::WindowBackend::Android;
#elif defined(__APPLE__)
return MG_Backend::WindowBackend::MetalLayer;
#elif defined(_WIN32)
return MG_Backend::WindowBackend::Win32;
#elif defined(__linux__)
return MG_Backend::WindowBackend::X11;
#else
@@ -187,7 +201,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_FALSE;
}
@@ -208,7 +222,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLDisplay GetDisplay(NativeDisplayType display) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_NO_DISPLAY;
}
@@ -313,6 +327,14 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
backendObject->ReleaseEGLResources();
}
// The last initialized display is gone and nothing is current on any
// thread: tear the whole library down deterministically inside the
// EGL lifecycle (backend, GL/EGL state, glslang). A later EGL call
// re-initializes lazily via GetStateEnsureInitialized(); process exit
// then has nothing left to destroy.
if (!state->HasAnyInitializedDisplay() && !state->HasAnyCurrentContext()) {
MobileGL::Destroy();
}
return EGL_TRUE;
}
@@ -345,7 +367,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLBoolean BindAPI(EGLenum api) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_FALSE;
}
@@ -378,7 +400,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
char const* QueryString(EGLDisplay display, EGLint name) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return nullptr;
}
@@ -641,7 +663,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list) {
(void)attrib_list;
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_NO_DISPLAY;
}
@@ -737,6 +759,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!name) {
return nullptr;
}
MobileGL::EnsureInitialized();
MGLOG_D("eglGetProcAddress(%s)", name);
void* proc = MG_Impl::GetProcAddress(name);
@@ -2144,6 +2144,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
namespace FramebufferImpl {
UniquePtr<DefaultFramebufferInfo> pDefaultFramebufferInfo;
// Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<DefaultFramebufferInfo>& pDefaultFramebufferInfo = *new UniquePtr<DefaultFramebufferInfo>();
} // namespace FramebufferImpl
} // namespace MobileGL::MG_Impl::GLImpl
@@ -78,6 +78,6 @@ namespace MobileGL::MG_Impl::GLImpl {
SharedPtr<MG_State::GLState::ITextureObject> stencilAttachment;
};
extern UniquePtr<DefaultFramebufferInfo> pDefaultFramebufferInfo;
extern UniquePtr<DefaultFramebufferInfo>& pDefaultFramebufferInfo;
} // namespace FramebufferImpl
} // namespace MobileGL::MG_Impl::GLImpl
@@ -14,7 +14,8 @@
#include <MG_State/GLState/TextureState/TextureObjectStubs.h>
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
UniquePtr<ProxyTextureManager> pProxyTextureManager;
// Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<ProxyTextureManager>& pProxyTextureManager = *new UniquePtr<ProxyTextureManager>();
Bool IsProxyTextureTarget(TextureUploadTarget target) {
switch (target) {
@@ -23,5 +23,5 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
UnorderedMap<TextureUploadTarget, SharedPtr<MG_State::GLState::ITextureObject>> m_proxyTexturesMap;
};
extern UniquePtr<ProxyTextureManager> pProxyTextureManager;
extern UniquePtr<ProxyTextureManager>& pProxyTextureManager;
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
@@ -0,0 +1,156 @@
// MobileGL - MobileGL/MG_Impl/WGLImpl/Exporting/Definitions.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// wingdi.h declares most wgl* entry points as WINGDIAPI (__declspec(dllimport)),
// which would reject our definitions. _GDI32_ is the SDK's "I am the module that
// implements these" switch: it turns WINGDIAPI into a plain declaration. It must
// be defined before the first windows.h inclusion in this translation unit.
#if defined(_WIN32) && !defined(_GDI32_)
#define _GDI32_ 1
#endif
#include <Includes.h>
#if defined(_WIN32)
#include "../WGLImpl.h"
namespace WGL = MobileGL::MG_Impl::WGLImpl;
// ---- Pixel-format entry points (gdi32 forwards ChoosePixelFormat/SetPixelFormat/
// ---- DescribePixelFormat/GetPixelFormat/SwapBuffers into these exports) ----
extern "C" int WINAPI wglChoosePixelFormat(HDC hdc, CONST PIXELFORMATDESCRIPTOR* ppfd) {
return WGL::ChoosePixelFormat(hdc, ppfd);
}
extern "C" int WINAPI wglDescribePixelFormat(HDC hdc, int iPixelFormat, UINT nBytes,
LPPIXELFORMATDESCRIPTOR ppfd) {
return WGL::DescribePixelFormat(hdc, iPixelFormat, nBytes, ppfd);
}
extern "C" int WINAPI wglGetPixelFormat(HDC hdc) {
return WGL::GetPixelFormat(hdc);
}
extern "C" BOOL WINAPI wglSetPixelFormat(HDC hdc, int iPixelFormat, CONST PIXELFORMATDESCRIPTOR* ppfd) {
return WGL::SetPixelFormat(hdc, iPixelFormat, ppfd);
}
extern "C" BOOL WINAPI wglSwapBuffers(HDC hdc) {
return WGL::SwapBuffers(hdc);
}
// ---- Context management ----
extern "C" HGLRC WINAPI wglCreateContext(HDC hdc) {
return WGL::CreateContext(hdc);
}
extern "C" HGLRC WINAPI wglCreateLayerContext(HDC hdc, int iLayerPlane) {
return iLayerPlane == 0 ? WGL::CreateContext(hdc) : nullptr;
}
extern "C" BOOL WINAPI wglCopyContext(HGLRC, HGLRC, UINT) {
MGLOG_W("wglCopyContext is not supported");
SetLastError(ERROR_NOT_SUPPORTED);
return FALSE;
}
extern "C" BOOL WINAPI wglDeleteContext(HGLRC hglrc) {
return WGL::DeleteContext(hglrc);
}
extern "C" HGLRC WINAPI wglGetCurrentContext(VOID) {
return WGL::GetCurrentContext();
}
extern "C" HDC WINAPI wglGetCurrentDC(VOID) {
return WGL::GetCurrentDC();
}
extern "C" BOOL WINAPI wglMakeCurrent(HDC hdc, HGLRC hglrc) {
return WGL::MakeCurrent(hdc, hglrc);
}
extern "C" BOOL WINAPI wglShareLists(HGLRC hglrcShare, HGLRC hglrcDest) {
return WGL::ShareLists(hglrcShare, hglrcDest);
}
// ---- Proc address ----
extern "C" PROC WINAPI wglGetProcAddress(LPCSTR lpszProc) {
return WGL::GetProcAddress(lpszProc);
}
extern "C" PROC WINAPI wglGetDefaultProcAddress(LPCSTR lpszProc) {
return WGL::GetProcAddress(lpszProc);
}
// ---- Layer planes and palettes (unsupported; overlay planes do not exist here) ----
extern "C" BOOL WINAPI wglDescribeLayerPlane(HDC, int, int, UINT, LPLAYERPLANEDESCRIPTOR) {
return FALSE;
}
extern "C" int WINAPI wglSetLayerPaletteEntries(HDC, int, int, int, CONST COLORREF*) {
return 0;
}
extern "C" int WINAPI wglGetLayerPaletteEntries(HDC, int, int, int, COLORREF*) {
return 0;
}
extern "C" BOOL WINAPI wglRealizeLayerPalette(HDC, int, BOOL) {
return FALSE;
}
extern "C" BOOL WINAPI wglSwapLayerBuffers(HDC hdc, UINT fuPlanes) {
if (fuPlanes & WGL_SWAP_MAIN_PLANE) {
return WGL::SwapBuffers(hdc);
}
return FALSE;
}
extern "C" DWORD WINAPI wglSwapMultipleBuffers(UINT n, CONST WGLSWAP* ps) {
if (!ps) {
return 0;
}
DWORD swapped = 0;
for (UINT i = 0; i < n; ++i) {
if (WGL::SwapBuffers(ps[i].hdc)) {
++swapped;
}
}
return swapped;
}
// ---- Font rendering (legacy immediate-mode feature; not supported) ----
extern "C" BOOL WINAPI wglUseFontBitmapsA(HDC, DWORD, DWORD, DWORD) {
MGLOG_W("wglUseFontBitmapsA is not supported");
return FALSE;
}
extern "C" BOOL WINAPI wglUseFontBitmapsW(HDC, DWORD, DWORD, DWORD) {
MGLOG_W("wglUseFontBitmapsW is not supported");
return FALSE;
}
extern "C" BOOL WINAPI wglUseFontOutlinesA(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
LPGLYPHMETRICSFLOAT) {
MGLOG_W("wglUseFontOutlinesA is not supported");
return FALSE;
}
extern "C" BOOL WINAPI wglUseFontOutlinesW(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
LPGLYPHMETRICSFLOAT) {
MGLOG_W("wglUseFontOutlinesW is not supported");
return FALSE;
}
#endif // _WIN32
@@ -0,0 +1,30 @@
; MobileGL WGL exports. The wgl* entry points are defined without
; __declspec(dllexport) because wingdi.h pre-declares them (with _GDI32_ they
; become plain declarations, and MSVC rejects adding dllexport afterwards),
; so this .def file is what actually exports them from the DLL.
EXPORTS
wglChoosePixelFormat
wglCopyContext
wglCreateContext
wglCreateLayerContext
wglDeleteContext
wglDescribeLayerPlane
wglDescribePixelFormat
wglGetCurrentContext
wglGetCurrentDC
wglGetDefaultProcAddress
wglGetLayerPaletteEntries
wglGetPixelFormat
wglGetProcAddress
wglMakeCurrent
wglRealizeLayerPalette
wglSetLayerPaletteEntries
wglSetPixelFormat
wglShareLists
wglSwapBuffers
wglSwapLayerBuffers
wglSwapMultipleBuffers
wglUseFontBitmapsA
wglUseFontBitmapsW
wglUseFontOutlinesA
wglUseFontOutlinesW
+732
View File
@@ -0,0 +1,732 @@
// MobileGL - MobileGL/MG_Impl/WGLImpl/WGLImpl.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "WGLImpl.h"
#if defined(_WIN32)
#include "../EGLImpl/EGLImpl.h"
#include "../GetProcAddress.h"
#include <Init.h>
namespace MobileGL::MG_Impl::WGLImpl {
namespace {
// WGL_ARB_pixel_format
constexpr int WGL_NUMBER_PIXEL_FORMATS_ARB = 0x2000;
constexpr int WGL_DRAW_TO_WINDOW_ARB = 0x2001;
constexpr int WGL_DRAW_TO_BITMAP_ARB = 0x2002;
constexpr int WGL_ACCELERATION_ARB = 0x2003;
constexpr int WGL_NEED_PALETTE_ARB = 0x2004;
constexpr int WGL_NEED_SYSTEM_PALETTE_ARB = 0x2005;
constexpr int WGL_SWAP_LAYER_BUFFERS_ARB = 0x2006;
constexpr int WGL_SWAP_METHOD_ARB = 0x2007;
constexpr int WGL_NUMBER_OVERLAYS_ARB = 0x2008;
constexpr int WGL_NUMBER_UNDERLAYS_ARB = 0x2009;
constexpr int WGL_TRANSPARENT_ARB = 0x200A;
constexpr int WGL_SHARE_DEPTH_ARB = 0x200C;
constexpr int WGL_SHARE_STENCIL_ARB = 0x200D;
constexpr int WGL_SHARE_ACCUM_ARB = 0x200E;
constexpr int WGL_SUPPORT_GDI_ARB = 0x200F;
constexpr int WGL_SUPPORT_OPENGL_ARB = 0x2010;
constexpr int WGL_DOUBLE_BUFFER_ARB = 0x2011;
constexpr int WGL_STEREO_ARB = 0x2012;
constexpr int WGL_PIXEL_TYPE_ARB = 0x2013;
constexpr int WGL_COLOR_BITS_ARB = 0x2014;
constexpr int WGL_RED_BITS_ARB = 0x2015;
constexpr int WGL_RED_SHIFT_ARB = 0x2016;
constexpr int WGL_GREEN_BITS_ARB = 0x2017;
constexpr int WGL_GREEN_SHIFT_ARB = 0x2018;
constexpr int WGL_BLUE_BITS_ARB = 0x2019;
constexpr int WGL_BLUE_SHIFT_ARB = 0x201A;
constexpr int WGL_ALPHA_BITS_ARB = 0x201B;
constexpr int WGL_ALPHA_SHIFT_ARB = 0x201C;
constexpr int WGL_ACCUM_BITS_ARB = 0x201D;
constexpr int WGL_ACCUM_RED_BITS_ARB = 0x201E;
constexpr int WGL_ACCUM_GREEN_BITS_ARB = 0x201F;
constexpr int WGL_ACCUM_BLUE_BITS_ARB = 0x2020;
constexpr int WGL_ACCUM_ALPHA_BITS_ARB = 0x2021;
constexpr int WGL_DEPTH_BITS_ARB = 0x2022;
constexpr int WGL_STENCIL_BITS_ARB = 0x2023;
constexpr int WGL_AUX_BUFFERS_ARB = 0x2024;
constexpr int WGL_NO_ACCELERATION_ARB = 0x2025;
constexpr int WGL_FULL_ACCELERATION_ARB = 0x2027;
constexpr int WGL_SWAP_EXCHANGE_ARB = 0x2028;
constexpr int WGL_TYPE_RGBA_ARB = 0x202B;
// WGL_ARB_multisample
constexpr int WGL_SAMPLE_BUFFERS_ARB = 0x2041;
constexpr int WGL_SAMPLES_ARB = 0x2042;
// WGL_ARB_create_context / _profile / _no_error
constexpr int WGL_CONTEXT_MAJOR_VERSION_ARB = 0x2091;
constexpr int WGL_CONTEXT_MINOR_VERSION_ARB = 0x2092;
constexpr int WGL_CONTEXT_LAYER_PLANE_ARB = 0x2093;
constexpr int WGL_CONTEXT_FLAGS_ARB = 0x2094;
constexpr int WGL_CONTEXT_PROFILE_MASK_ARB = 0x9126;
constexpr int WGL_CONTEXT_DEBUG_BIT_ARB = 0x0001;
constexpr int WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB = 0x0002;
constexpr int WGL_CONTEXT_CORE_PROFILE_BIT_ARB = 0x00000001;
constexpr int WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB = 0x00000002;
constexpr int WGL_CONTEXT_OPENGL_NO_ERROR_ARB = 0x31B3;
constexpr DWORD ERROR_INVALID_VERSION_ARB = 0x2095;
constexpr DWORD ERROR_INVALID_PROFILE_ARB = 0x2096;
struct PixelFormatInfo {
GLint AlphaBits;
GLint DepthBits;
GLint StencilBits;
};
// Mirrors the two EGLState configs (RGBA8 + depth24, stencil 8 / stencil 0).
constexpr PixelFormatInfo kPixelFormats[] = {
{8, 24, 8},
{8, 24, 0},
};
constexpr int kPixelFormatCount = static_cast<int>(std::size(kPixelFormats));
struct ContextObject {
EGLDisplay Display = EGL_NO_DISPLAY;
EGLConfig Config = nullptr;
EGLContext Context = EGL_NO_CONTEXT;
};
struct WindowSurface {
EGLDisplay Display = EGL_NO_DISPLAY;
EGLSurface Surface = EGL_NO_SURFACE;
Uint32 Width = 0;
Uint32 Height = 0;
};
std::recursive_mutex& RegistryMutex() {
static auto* mutex = new std::recursive_mutex();
return *mutex;
}
UnorderedMap<HGLRC, ContextObject>& Contexts() {
static auto* contexts = new UnorderedMap<HGLRC, ContextObject>();
return *contexts;
}
UnorderedMap<HWND, WindowSurface>& WindowSurfaces() {
static auto* surfaces = new UnorderedMap<HWND, WindowSurface>();
return *surfaces;
}
UnorderedMap<HWND, int>& WindowPixelFormats() {
static auto* formats = new UnorderedMap<HWND, int>();
return *formats;
}
Uint64& NextContextHandle() {
static auto* handle = new Uint64(0x10000);
return *handle;
}
struct ThreadCurrent {
HDC DC = nullptr;
HGLRC Context = nullptr;
};
thread_local ThreadCurrent t_current;
Int& SwapIntervalShadow() {
static auto* interval = new Int(1);
return *interval;
}
void EnsureInitialized() {
// Initialize() loads backend libraries and glslang, which must not run
// under the loader lock; first WGL call is the earliest safe moment.
// MobileGL::EnsureInitialized (not a local once_flag) so a fresh init
// can follow a full teardown from the last eglTerminate.
MobileGL::EnsureInitialized();
}
EGLDisplay EnsureDisplay() {
EnsureInitialized();
EGLDisplay display = EGLImpl::GetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
return EGL_NO_DISPLAY;
}
if (!EGLImpl::Initialize(display, nullptr, nullptr)) {
return EGL_NO_DISPLAY;
}
return display;
}
HGLRC EncodeContext(Uint64 handle) {
return reinterpret_cast<HGLRC>(static_cast<SizeT>(handle));
}
ContextObject* TryGetContext(HGLRC hglrc) {
auto& contexts = Contexts();
auto it = contexts.find(hglrc);
return it == contexts.end() ? nullptr : &it->second;
}
const PixelFormatInfo& PixelFormatForWindow(HWND hwnd) {
auto& formats = WindowPixelFormats();
auto it = formats.find(hwnd);
int index = it == formats.end() ? 1 : it->second;
if (index < 1 || index > kPixelFormatCount) {
index = 1;
}
return kPixelFormats[index - 1];
}
Bool QueryClientSize(HWND hwnd, Uint32& width, Uint32& height) {
RECT rect{};
if (!GetClientRect(hwnd, &rect)) {
return false;
}
width = static_cast<Uint32>(std::max<LONG>(rect.right - rect.left, 1));
height = static_cast<Uint32>(std::max<LONG>(rect.bottom - rect.top, 1));
return true;
}
// The backends never query the HWND client size themselves; the WGL layer
// owns size discovery and pushes changes through the internal resize hook
// (same contract as the macOS CGL layer).
void SyncSurfaceSize(HWND hwnd, WindowSurface& surface) {
Uint32 width = 0;
Uint32 height = 0;
if (!QueryClientSize(hwnd, width, height)) {
return;
}
if (width == surface.Width && height == surface.Height) {
return;
}
if (EGLImpl::ResizePlatformWindowSurface(surface.Display, surface.Surface,
static_cast<EGLint>(width), static_cast<EGLint>(height))) {
surface.Width = width;
surface.Height = height;
}
}
WindowSurface* EnsureWindowSurface(HWND hwnd, const ContextObject& context) {
auto& surfaces = WindowSurfaces();
auto it = surfaces.find(hwnd);
if (it != surfaces.end()) {
SyncSurfaceSize(hwnd, it->second);
return &it->second;
}
Uint32 width = 0;
Uint32 height = 0;
if (!QueryClientSize(hwnd, width, height)) {
MGLOG_E("wgl: GetClientRect failed for HWND %p", hwnd);
return nullptr;
}
const EGLAttrib attribs[] = {
EGL_WIDTH, static_cast<EGLAttrib>(width),
EGL_HEIGHT, static_cast<EGLAttrib>(height),
EGL_NONE,
};
EGLSurface surface =
EGLImpl::CreatePlatformWindowSurface(context.Display, context.Config, hwnd, attribs);
if (surface == EGL_NO_SURFACE) {
MGLOG_E("wgl: failed to create window surface for HWND %p (%ux%u)", hwnd, width, height);
return nullptr;
}
WindowSurface record;
record.Display = context.Display;
record.Surface = surface;
record.Width = width;
record.Height = height;
auto [inserted, _] = surfaces.emplace(hwnd, record);
return &inserted->second;
}
HGLRC CreateContextFromEGLAttribs(HDC hdc, HGLRC share, const EGLint* contextAttribs) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
EGLDisplay display = EnsureDisplay();
if (display == EGL_NO_DISPLAY) {
MGLOG_E("wgl: no EGL display");
return nullptr;
}
EGLImpl::BindAPI(EGL_OPENGL_API);
EGLContext shareContext = EGL_NO_CONTEXT;
if (share) {
auto* shareObject = TryGetContext(share);
if (!shareObject) {
SetLastError(ERROR_INVALID_HANDLE);
return nullptr;
}
shareContext = shareObject->Context;
}
HWND hwnd = WindowFromDC(hdc);
const PixelFormatInfo& pixelFormat = PixelFormatForWindow(hwnd);
const EGLint configAttribs[] = {
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, pixelFormat.AlphaBits,
EGL_DEPTH_SIZE, pixelFormat.DepthBits,
EGL_STENCIL_SIZE, pixelFormat.StencilBits,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT | EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
EGL_NONE,
};
EGLConfig config = nullptr;
EGLint configCount = 0;
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) || configCount <= 0) {
MGLOG_E("wgl: eglChooseConfig failed");
return nullptr;
}
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
if (eglContext == EGL_NO_CONTEXT) {
MGLOG_E("wgl: eglCreateContext failed");
return nullptr;
}
ContextObject object;
object.Display = display;
object.Config = config;
object.Context = eglContext;
const auto handle = EncodeContext(NextContextHandle()++);
Contexts()[handle] = object;
MGLOG_I("wgl: created context %p (EGL context %p)", handle, eglContext);
return handle;
}
// ---- WGL extension entry points (resolved via wglGetProcAddress only) ----
const char* WINAPI Ext_GetExtensionsStringARB(HDC) {
return "WGL_ARB_create_context WGL_ARB_create_context_no_error WGL_ARB_create_context_profile "
"WGL_ARB_extensions_string WGL_ARB_pixel_format WGL_EXT_extensions_string WGL_EXT_swap_control";
}
const char* WINAPI Ext_GetExtensionsStringEXT() {
return Ext_GetExtensionsStringARB(nullptr);
}
HGLRC WINAPI Ext_CreateContextAttribsARB(HDC hdc, HGLRC hShareContext, const int* attribList) {
EnsureInitialized();
int major = 1;
int minor = 0;
int profileMask = 0;
int flags = 0;
if (attribList) {
for (SizeT i = 0; attribList[i] != 0; i += 2) {
const int attrib = attribList[i];
const int value = attribList[i + 1];
switch (attrib) {
case WGL_CONTEXT_MAJOR_VERSION_ARB:
major = value;
break;
case WGL_CONTEXT_MINOR_VERSION_ARB:
minor = value;
break;
case WGL_CONTEXT_PROFILE_MASK_ARB:
profileMask = value;
break;
case WGL_CONTEXT_FLAGS_ARB:
flags = value;
break;
case WGL_CONTEXT_LAYER_PLANE_ARB:
if (value != 0) {
SetLastError(ERROR_INVALID_PARAMETER);
return nullptr;
}
break;
case WGL_CONTEXT_OPENGL_NO_ERROR_ARB:
// Accepted and ignored: MobileGL always validates.
break;
default:
MGLOG_D("wglCreateContextAttribsARB: ignoring attrib 0x%04x = 0x%x", attrib, value);
break;
}
}
}
if (major < 1 || (profileMask & ~(WGL_CONTEXT_CORE_PROFILE_BIT_ARB |
WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB))) {
SetLastError(profileMask ? ERROR_INVALID_PROFILE_ARB : ERROR_INVALID_VERSION_ARB);
return nullptr;
}
Vector<EGLint> attribs = {
EGL_CONTEXT_MAJOR_VERSION, major,
EGL_CONTEXT_MINOR_VERSION, minor,
};
const Bool wantsCompat = (profileMask & WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB) != 0;
if (major > 3 || (major == 3 && minor >= 2) || profileMask != 0) {
attribs.push_back(EGL_CONTEXT_OPENGL_PROFILE_MASK);
attribs.push_back(wantsCompat ? EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT
: EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT);
}
if (flags & WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB) {
attribs.push_back(EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE);
attribs.push_back(EGL_TRUE);
}
if (flags & WGL_CONTEXT_DEBUG_BIT_ARB) {
attribs.push_back(EGL_CONTEXT_OPENGL_DEBUG);
attribs.push_back(EGL_TRUE);
}
attribs.push_back(EGL_NONE);
return CreateContextFromEGLAttribs(hdc, hShareContext, attribs.data());
}
BOOL WINAPI Ext_SwapIntervalEXT(int interval) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
EGLDisplay display = EnsureDisplay();
if (display == EGL_NO_DISPLAY) {
return FALSE;
}
if (interval < 0) {
// Adaptive vsync is not supported; clamp to regular vsync.
interval = 1;
}
EGLImpl::SwapInterval(display, interval);
SwapIntervalShadow() = interval;
return TRUE;
}
int WINAPI Ext_GetSwapIntervalEXT() {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
return SwapIntervalShadow();
}
int PixelFormatAttribValue(int format, int attrib) {
const PixelFormatInfo& info = kPixelFormats[format - 1];
switch (attrib) {
case WGL_NUMBER_PIXEL_FORMATS_ARB:
return kPixelFormatCount;
case WGL_SUPPORT_OPENGL_ARB:
case WGL_DRAW_TO_WINDOW_ARB:
case WGL_DOUBLE_BUFFER_ARB:
return 1;
case WGL_ACCELERATION_ARB:
return WGL_FULL_ACCELERATION_ARB;
case WGL_PIXEL_TYPE_ARB:
return WGL_TYPE_RGBA_ARB;
case WGL_COLOR_BITS_ARB:
return 32;
case WGL_RED_BITS_ARB:
case WGL_GREEN_BITS_ARB:
case WGL_BLUE_BITS_ARB:
return 8;
case WGL_RED_SHIFT_ARB:
return 16;
case WGL_GREEN_SHIFT_ARB:
return 8;
case WGL_BLUE_SHIFT_ARB:
return 0;
case WGL_ALPHA_BITS_ARB:
return info.AlphaBits;
case WGL_ALPHA_SHIFT_ARB:
return 24;
case WGL_DEPTH_BITS_ARB:
return info.DepthBits;
case WGL_STENCIL_BITS_ARB:
return info.StencilBits;
case WGL_SWAP_METHOD_ARB:
return WGL_SWAP_EXCHANGE_ARB;
case WGL_DRAW_TO_BITMAP_ARB:
case WGL_NEED_PALETTE_ARB:
case WGL_NEED_SYSTEM_PALETTE_ARB:
case WGL_SWAP_LAYER_BUFFERS_ARB:
case WGL_NUMBER_OVERLAYS_ARB:
case WGL_NUMBER_UNDERLAYS_ARB:
case WGL_TRANSPARENT_ARB:
case WGL_SHARE_DEPTH_ARB:
case WGL_SHARE_STENCIL_ARB:
case WGL_SHARE_ACCUM_ARB:
case WGL_SUPPORT_GDI_ARB:
case WGL_STEREO_ARB:
case WGL_ACCUM_BITS_ARB:
case WGL_ACCUM_RED_BITS_ARB:
case WGL_ACCUM_GREEN_BITS_ARB:
case WGL_ACCUM_BLUE_BITS_ARB:
case WGL_ACCUM_ALPHA_BITS_ARB:
case WGL_AUX_BUFFERS_ARB:
case WGL_SAMPLE_BUFFERS_ARB:
case WGL_SAMPLES_ARB:
default:
return 0;
}
}
BOOL WINAPI Ext_GetPixelFormatAttribivARB(HDC, int iPixelFormat, int iLayerPlane, UINT nAttributes,
const int* piAttributes, int* piValues) {
if (iLayerPlane != 0 || !piAttributes || !piValues) {
return FALSE;
}
// Format 0 is only valid for WGL_NUMBER_PIXEL_FORMATS_ARB queries.
if (iPixelFormat < 0 || iPixelFormat > kPixelFormatCount) {
return FALSE;
}
const int format = iPixelFormat == 0 ? 1 : iPixelFormat;
for (UINT i = 0; i < nAttributes; ++i) {
piValues[i] = PixelFormatAttribValue(format, piAttributes[i]);
}
return TRUE;
}
BOOL WINAPI Ext_GetPixelFormatAttribfvARB(HDC hdc, int iPixelFormat, int iLayerPlane, UINT nAttributes,
const int* piAttributes, FLOAT* pfValues) {
if (!pfValues) {
return FALSE;
}
Vector<int> values(nAttributes);
if (!Ext_GetPixelFormatAttribivARB(hdc, iPixelFormat, iLayerPlane, nAttributes, piAttributes,
values.data())) {
return FALSE;
}
for (UINT i = 0; i < nAttributes; ++i) {
pfValues[i] = static_cast<FLOAT>(values[i]);
}
return TRUE;
}
BOOL WINAPI Ext_ChoosePixelFormatARB(HDC, const int* piAttribIList, const FLOAT*, UINT nMaxFormats,
int* piFormats, UINT* nNumFormats) {
if (!piFormats || !nNumFormats) {
return FALSE;
}
int wantedStencil = 0;
if (piAttribIList) {
for (SizeT i = 0; piAttribIList[i] != 0; i += 2) {
if (piAttribIList[i] == WGL_STENCIL_BITS_ARB) {
wantedStencil = piAttribIList[i + 1];
}
}
}
UINT count = 0;
const int preferred = wantedStencil > 0 ? 1 : 2;
const int fallback = wantedStencil > 0 ? 2 : 1;
if (count < nMaxFormats) {
piFormats[count++] = preferred;
}
if (count < nMaxFormats) {
piFormats[count++] = fallback;
}
*nNumFormats = count;
return TRUE;
}
struct WGLExtensionProc {
const char* Name;
PROC Proc;
};
const WGLExtensionProc kWGLExtensionProcs[] = {
{"wglGetExtensionsStringARB", reinterpret_cast<PROC>(Ext_GetExtensionsStringARB)},
{"wglGetExtensionsStringEXT", reinterpret_cast<PROC>(Ext_GetExtensionsStringEXT)},
{"wglCreateContextAttribsARB", reinterpret_cast<PROC>(Ext_CreateContextAttribsARB)},
{"wglSwapIntervalEXT", reinterpret_cast<PROC>(Ext_SwapIntervalEXT)},
{"wglGetSwapIntervalEXT", reinterpret_cast<PROC>(Ext_GetSwapIntervalEXT)},
{"wglGetPixelFormatAttribivARB", reinterpret_cast<PROC>(Ext_GetPixelFormatAttribivARB)},
{"wglGetPixelFormatAttribfvARB", reinterpret_cast<PROC>(Ext_GetPixelFormatAttribfvARB)},
{"wglChoosePixelFormatARB", reinterpret_cast<PROC>(Ext_ChoosePixelFormatARB)},
};
} // namespace
int ChoosePixelFormat(HDC hdc, const PIXELFORMATDESCRIPTOR* pfd) {
EnsureInitialized();
MGLOG_D("wglChoosePixelFormat(hdc=%p)", hdc);
// Format 1 (RGBA8 + depth24/stencil8) satisfies every request; a format
// exceeding the asked-for capabilities is a legal ChoosePixelFormat answer.
(void)pfd;
return 1;
}
int DescribePixelFormat(HDC hdc, int format, UINT size, PIXELFORMATDESCRIPTOR* pfd) {
EnsureInitialized();
MGLOG_D("wglDescribePixelFormat(hdc=%p, format=%d)", hdc, format);
if (!pfd) {
return kPixelFormatCount;
}
if (size < sizeof(PIXELFORMATDESCRIPTOR) || format < 1 || format > kPixelFormatCount) {
return 0;
}
const PixelFormatInfo& info = kPixelFormats[format - 1];
std::memset(pfd, 0, sizeof(PIXELFORMATDESCRIPTOR));
pfd->nSize = sizeof(PIXELFORMATDESCRIPTOR);
pfd->nVersion = 1;
pfd->dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER | PFD_SWAP_EXCHANGE
#if defined(PFD_SUPPORT_COMPOSITION)
| PFD_SUPPORT_COMPOSITION
#endif
;
pfd->iPixelType = PFD_TYPE_RGBA;
pfd->cColorBits = 32;
pfd->cRedBits = 8;
pfd->cRedShift = 16;
pfd->cGreenBits = 8;
pfd->cGreenShift = 8;
pfd->cBlueBits = 8;
pfd->cBlueShift = 0;
pfd->cAlphaBits = static_cast<BYTE>(info.AlphaBits);
pfd->cAlphaShift = 24;
pfd->cDepthBits = static_cast<BYTE>(info.DepthBits);
pfd->cStencilBits = static_cast<BYTE>(info.StencilBits);
pfd->iLayerType = PFD_MAIN_PLANE;
return kPixelFormatCount;
}
int GetPixelFormat(HDC hdc) {
EnsureInitialized();
HWND hwnd = WindowFromDC(hdc);
if (!hwnd) {
return 0;
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto& formats = WindowPixelFormats();
auto it = formats.find(hwnd);
return it == formats.end() ? 0 : it->second;
}
BOOL SetPixelFormat(HDC hdc, int format, const PIXELFORMATDESCRIPTOR*) {
EnsureInitialized();
MGLOG_D("wglSetPixelFormat(hdc=%p, format=%d)", hdc, format);
if (format < 1 || format > kPixelFormatCount) {
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}
HWND hwnd = WindowFromDC(hdc);
if (!hwnd) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
WindowPixelFormats()[hwnd] = format;
return TRUE;
}
BOOL SwapBuffers(HDC hdc) {
HWND hwnd = WindowFromDC(hdc);
if (!hwnd) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto& surfaces = WindowSurfaces();
auto it = surfaces.find(hwnd);
if (it == surfaces.end()) {
MGLOG_W("wglSwapBuffers: no surface for HWND %p", hwnd);
return FALSE;
}
SyncSurfaceSize(hwnd, it->second);
return EGLImpl::SwapBuffers(it->second.Display, it->second.Surface) == EGL_TRUE ? TRUE : FALSE;
}
HGLRC CreateContext(HDC hdc) {
EnsureInitialized();
MGLOG_I("wglCreateContext(hdc=%p)", hdc);
// A legacy WGL context is a compatibility-profile context; MobileGL keys
// its relaxed-semantics mode off the explicit compatibility bit.
const EGLint attribs[] = {
EGL_CONTEXT_MAJOR_VERSION, 3,
EGL_CONTEXT_MINOR_VERSION, 3,
EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT,
EGL_NONE,
};
return CreateContextFromEGLAttribs(hdc, nullptr, attribs);
}
BOOL DeleteContext(HGLRC hglrc) {
EnsureInitialized();
MGLOG_I("wglDeleteContext(%p)", hglrc);
if (t_current.Context == hglrc) {
MakeCurrent(nullptr, nullptr);
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(hglrc);
if (!object) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
if (object->Context != EGL_NO_CONTEXT) {
EGLImpl::DestroyContext(object->Display, object->Context);
}
Contexts().erase(hglrc);
return TRUE;
}
BOOL MakeCurrent(HDC hdc, HGLRC hglrc) {
EnsureInitialized();
MGLOG_D("wglMakeCurrent(hdc=%p, hglrc=%p)", hdc, hglrc);
if (!hglrc) {
if (!t_current.Context) {
t_current = {};
return TRUE;
}
const EGLBoolean released =
EGLImpl::MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
t_current = {};
return released == EGL_TRUE ? TRUE : FALSE;
}
HWND hwnd = WindowFromDC(hdc);
if (!hwnd) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(hglrc);
if (!object) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
WindowSurface* surface = EnsureWindowSurface(hwnd, *object);
if (!surface) {
return FALSE;
}
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface, object->Context)) {
MGLOG_E("wglMakeCurrent: eglMakeCurrent failed (hdc=%p, hglrc=%p)", hdc, hglrc);
return FALSE;
}
t_current = {hdc, hglrc};
return TRUE;
}
HGLRC GetCurrentContext() {
return t_current.Context;
}
HDC GetCurrentDC() {
return t_current.DC;
}
BOOL ShareLists(HGLRC hglrcShare, HGLRC hglrcDest) {
// All MobileGL contexts alias one global GL object namespace, so every
// pair of contexts already shares.
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
if (!TryGetContext(hglrcShare) || !TryGetContext(hglrcDest)) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
return TRUE;
}
PROC GetProcAddress(const char* name) {
EnsureInitialized();
if (!name) {
return nullptr;
}
if (name[0] == 'w' && name[1] == 'g' && name[2] == 'l') {
for (const auto& entry : kWGLExtensionProcs) {
if (std::strcmp(entry.Name, name) == 0) {
return entry.Proc;
}
}
MGLOG_D("wglGetProcAddress: unknown wgl entry point %s", name);
return nullptr;
}
return reinterpret_cast<PROC>(MG_Impl::GetProcAddress(name));
}
} // namespace MobileGL::MG_Impl::WGLImpl
#endif // _WIN32
+34
View File
@@ -0,0 +1,34 @@
// MobileGL - MobileGL/MG_Impl/WGLImpl/WGLImpl.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#if defined(_WIN32)
namespace MobileGL::MG_Impl::WGLImpl {
// Classic opengl32.dll surface. gdi32's ChoosePixelFormat/SetPixelFormat/
// DescribePixelFormat/GetPixelFormat/SwapBuffers forward into the loaded
// opengl32.dll's wgl* exports, so these back both call paths.
int ChoosePixelFormat(HDC hdc, const PIXELFORMATDESCRIPTOR* pfd);
int DescribePixelFormat(HDC hdc, int format, UINT size, PIXELFORMATDESCRIPTOR* pfd);
int GetPixelFormat(HDC hdc);
BOOL SetPixelFormat(HDC hdc, int format, const PIXELFORMATDESCRIPTOR* pfd);
BOOL SwapBuffers(HDC hdc);
HGLRC CreateContext(HDC hdc);
BOOL DeleteContext(HGLRC hglrc);
BOOL MakeCurrent(HDC hdc, HGLRC hglrc);
HGLRC GetCurrentContext();
HDC GetCurrentDC();
BOOL ShareLists(HGLRC hglrcShare, HGLRC hglrcDest);
PROC GetProcAddress(const char* name);
} // namespace MobileGL::MG_Impl::WGLImpl
#endif // _WIN32
+22 -1
View File
@@ -368,6 +368,26 @@ namespace MobileGL {
return true;
}
Bool EGLContext::HasAnyInitializedDisplay() const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
for (const auto& [handle, displayObject] : m_displays) {
if (displayObject.Initialized) {
return true;
}
}
return false;
}
Bool EGLContext::HasAnyCurrentContext() const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
for (const auto& [threadId, current] : m_threadCurrents) {
if (current.Context != nullptr) {
return true;
}
}
return false;
}
Bool EGLContext::ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs,
EGLint configSize, EGLint* numConfig) {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
@@ -1415,6 +1435,7 @@ namespace MobileGL {
}
} // namespace EGLState
UniquePtr<EGLState::EGLContext> pEGLContext;
// Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<EGLState::EGLContext>& pEGLContext = *new UniquePtr<EGLState::EGLContext>();
} // namespace MG_State
} // namespace MobileGL
+5 -1
View File
@@ -39,6 +39,10 @@ namespace MobileGL {
Bool IsDisplayInitialized(EGLDisplayHandle display) const;
Bool InitializeDisplay(EGLDisplayHandle display, EGLint* major, EGLint* minor);
Bool TerminateDisplay(EGLDisplayHandle display);
// Whole-library idle checks used by EGLImpl::Terminate to decide
// when the last eglTerminate may tear MobileGL down entirely.
Bool HasAnyInitializedDisplay() const;
Bool HasAnyCurrentContext() const;
// Config
Bool ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs,
@@ -262,6 +266,6 @@ namespace MobileGL {
};
} // namespace EGLState
extern UniquePtr<EGLState::EGLContext> pEGLContext;
extern UniquePtr<EGLState::EGLContext>& pEGLContext;
} // namespace MG_State
} // namespace MobileGL
+2 -1
View File
@@ -721,5 +721,6 @@ namespace MobileGL::MG_State {
}
} // namespace GLState
UniquePtr<GLState::GLContext> pGLContext;
// Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<GLState::GLContext>& pGLContext = *new UniquePtr<GLState::GLContext>();
} // namespace MobileGL::MG_State
+1 -1
View File
@@ -252,7 +252,7 @@ namespace MobileGL {
};
} // namespace GLState
extern UniquePtr<GLState::GLContext> pGLContext;
extern UniquePtr<GLState::GLContext>& pGLContext;
// True when relaxed GL semantics apply. Strict core rules are enforced only when the
// current EGL context explicitly requested a core profile (core bit in
@@ -9,7 +9,12 @@
#include "Loader.h"
#include "MG_Util/Types.h"
#include <Config.h>
#if !defined(__WIN32) && !defined(_WIN32)
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN 1
#endif
#include <windows.h>
#else
#include <dlfcn.h>
#endif
@@ -60,7 +65,14 @@ namespace MobileGL::MG_Util::BackendLoader {
#endif
static void* OpenLib(const Vector<String>& names) {
#if !defined(__WIN32) && !defined(_WIN32) && (!defined(__APPLE__) || defined(MOBILEGL_IOS))
#if defined(_WIN32)
for (const auto& name : names) {
if (HMODULE lib = LoadLibraryA(name.c_str())) {
MGLOG_I("Loaded GL backend library: %s", name.c_str());
return reinterpret_cast<void*>(lib);
}
}
#elif !defined(__APPLE__) || defined(MOBILEGL_IOS)
static const String LibPathPrefixes[] = {
#if defined(MOBILEGL_IOS)
"@rpath/", "@executable_path/Frameworks/", "@loader_path/Frameworks/",
@@ -104,7 +116,9 @@ namespace MobileGL::MG_Util::BackendLoader {
}
inline void* ProcAddress(void* lib, const char* name) {
#if !defined(__WIN32) && !defined(_WIN32) && (!defined(__APPLE__) || defined(MOBILEGL_IOS))
#if defined(_WIN32)
return reinterpret_cast<void*>(::GetProcAddress(reinterpret_cast<HMODULE>(lib), name));
#elif !defined(__APPLE__) || defined(MOBILEGL_IOS)
return dlsym(lib, name);
#else
return nullptr;
@@ -520,6 +534,15 @@ namespace MobileGL::MG_Util::BackendLoader {
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS) && defined(__ANDROID__)
void* angleGlesLib = nullptr;
#endif
#if defined(_WIN32)
// ANGLE is the GLES provider on Windows regardless of UseAngle(). Preload
// libGLESv2.dll so libEGL.dll resolves its dependency from the same directory.
if (!OpenLib({"libGLESv2.dll"})) {
MGLOG_E("Failed to open ANGLE libGLESv2.dll");
return;
}
eglLib = OpenLib({"libEGL.dll"});
#else
if (UseAngle()) {
void* glesLib = OpenLib({"libGLESv2_angle.so"});
if (!glesLib) {
@@ -541,6 +564,7 @@ namespace MobileGL::MG_Util::BackendLoader {
eglLib = OpenLib({"libEGL.so"});
#endif
}
#endif // !_WIN32
if (!eglLib) {
MGLOG_E("Failed to open EGL library");
+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;
}