mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 22:28:32 +09:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4322427e78 | ||
|
|
203d4bce5e | ||
|
|
761114d022 | ||
|
|
9caf34d5b1 | ||
|
|
c8632dfefe | ||
|
|
b8a8a660e1 | ||
|
|
7ab83861ca | ||
|
|
eaeba556a3 | ||
|
|
72fa1221a5 | ||
|
|
c4254c4bbd |
@@ -194,6 +194,9 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.cpp
|
||||||
|
|
||||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||||
@@ -303,6 +306,13 @@ if (ANDROID)
|
|||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (WIN32)
|
||||||
|
list(APPEND SOURCE_FILES
|
||||||
|
MobileGL/MG_Impl/WGLImpl/WGLImpl.cpp
|
||||||
|
MobileGL/MG_Impl/WGLImpl/Exporting/Definitions.cpp
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
set(MOBILEGL_LINK_LIBRARIES
|
set(MOBILEGL_LINK_LIBRARIES
|
||||||
glslang::glslang
|
glslang::glslang
|
||||||
spirv-cross-c
|
spirv-cross-c
|
||||||
@@ -335,6 +345,14 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
|
|||||||
${SOURCE_FILES}
|
${SOURCE_FILES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (WIN32)
|
||||||
|
# The wgl* entry points are exported via .def (see the comment in wgl.def);
|
||||||
|
# only the shared library links it.
|
||||||
|
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
|
||||||
|
MobileGL/MG_Impl/WGLImpl/Exporting/wgl.def
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
|
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||||
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
|
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
|
||||||
C_VISIBILITY_PRESET default
|
C_VISIBILITY_PRESET default
|
||||||
@@ -377,6 +395,18 @@ if(UNIX AND NOT APPLE AND NOT ANDROID)
|
|||||||
endforeach()
|
endforeach()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
# Drop-in for the classic GL loader path: a copy named opengl32.dll placed
|
||||||
|
# next to a host executable is what LoadLibrary("opengl32.dll") and gdi32's
|
||||||
|
# pixel-format forwarding will resolve.
|
||||||
|
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"$<TARGET_FILE:${CMAKE_PROJECT_NAME}>"
|
||||||
|
"$<TARGET_FILE_DIR:${CMAKE_PROJECT_NAME}>/opengl32.dll"
|
||||||
|
COMMENT "Creating opengl32.dll drop-in copy"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(NOT ANDROID)
|
if(NOT ANDROID)
|
||||||
add_library(${CMAKE_PROJECT_NAME}_s STATIC
|
add_library(${CMAKE_PROJECT_NAME}_s STATIC
|
||||||
${SOURCE_FILES}
|
${SOURCE_FILES}
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ namespace MobileGL::MG_Config {
|
|||||||
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
|
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
|
||||||
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
|
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
|
||||||
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
|
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
|
||||||
|
// MOBILEGL_QUIRK_CLIP_DISTANCE: overrides the DirectGLES quirk that lowers
|
||||||
|
// gl_ClipDistance for Adreno's ESSL compiler (shadow Private arrays with
|
||||||
|
// constant-index builtin flushes, dynamic-index gl_in copy loop, redeclaration
|
||||||
|
// strip, and const struct-array LUT splitting). Auto detects Qualcomm.
|
||||||
|
QuirkOverride ClipDistanceQuirk = QuirkOverride::Auto;
|
||||||
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
|
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
|
||||||
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
|
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
|
||||||
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
|
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||||
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
|
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
|
||||||
|
features.ClipDistanceQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_CLIP_DISTANCE");
|
||||||
features.MagmaDisableBlendedDepthWriteQuirk =
|
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
||||||
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
#define MOBILEGL_EGL_API MOBILEGL_API
|
#define MOBILEGL_EGL_API MOBILEGL_API
|
||||||
#define MOBILEGL_CGL_API MOBILEGL_API
|
#define MOBILEGL_CGL_API MOBILEGL_API
|
||||||
#define MOBILEGL_NSOPENGL_API MOBILEGL_API
|
#define MOBILEGL_NSOPENGL_API MOBILEGL_API
|
||||||
|
#define MOBILEGL_WGL_API MOBILEGL_API
|
||||||
|
|
||||||
// ====================== MobileGL configurations ======================= //
|
// ====================== MobileGL configurations ======================= //
|
||||||
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL
|
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL
|
||||||
|
|||||||
@@ -14,7 +14,13 @@ namespace MobileGL {
|
|||||||
} // namespace MG_Config
|
} // namespace MG_Config
|
||||||
|
|
||||||
namespace MG_Backend {
|
namespace MG_Backend {
|
||||||
UniquePtr<BackendObject> pActiveBackendObject;
|
// Leak-at-exit storage: the UniquePtr itself lives on the heap and is
|
||||||
|
// never destroyed by the runtime, so process exit runs no backend
|
||||||
|
// destructors (static destruction order across TUs is undefined).
|
||||||
|
// Deterministic teardown happens inside the EGL lifecycle instead:
|
||||||
|
// the last eglTerminate calls MobileGL::Destroy(), which .reset()s
|
||||||
|
// these singletons while the process is still healthy.
|
||||||
|
UniquePtr<BackendObject>& pActiveBackendObject = *new UniquePtr<BackendObject>();
|
||||||
GlobalBackendFunctionsTable gBackendFunctionsTable;
|
GlobalBackendFunctionsTable gBackendFunctionsTable;
|
||||||
} // namespace MG_Backend
|
} // namespace MG_Backend
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
+38
-33
@@ -9,14 +9,24 @@
|
|||||||
#include "Init.h"
|
#include "Init.h"
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
#include <MG_Backend/DirectVulkan/DirectVulkan.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_State/EGLState/Core.h>
|
#include <MG_State/EGLState/Core.h>
|
||||||
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
|
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
|
||||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
namespace MobileGL {
|
namespace MobileGL {
|
||||||
namespace {
|
namespace {
|
||||||
Bool g_isInitialized = false;
|
std::atomic<Bool> g_isInitialized = false;
|
||||||
|
thread_local Bool tl_initializing = false;
|
||||||
|
|
||||||
|
std::mutex& InitMutex() {
|
||||||
|
static std::mutex mutex;
|
||||||
|
return mutex;
|
||||||
|
}
|
||||||
|
|
||||||
void DestroyImpl(Bool logLifecycle) {
|
void DestroyImpl(Bool logLifecycle) {
|
||||||
if (!g_isInitialized) {
|
if (!g_isInitialized) {
|
||||||
@@ -64,40 +74,35 @@ namespace MobileGL {
|
|||||||
MGLOG_I("MobileGL initialized");
|
MGLOG_I("MobileGL initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EnsureInitialized() {
|
||||||
|
if (g_isInitialized.load(std::memory_order_acquire)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Re-entrant call while this thread is already inside Initialize()
|
||||||
|
// (e.g. an init step routing back through a public entry point).
|
||||||
|
if (tl_initializing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::lock_guard<std::mutex> lock(InitMutex());
|
||||||
|
if (g_isInitialized.load(std::memory_order_acquire)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tl_initializing = true;
|
||||||
|
Initialize();
|
||||||
|
tl_initializing = false;
|
||||||
|
}
|
||||||
|
|
||||||
void Destroy() {
|
void Destroy() {
|
||||||
DestroyImpl(true);
|
DestroyImpl(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(__linux__) || defined(__APPLE__)
|
// MobileGL's lifecycle is owned entirely by the host-API layers
|
||||||
__attribute__((constructor)) static void AutoInit() {
|
// (EGL/WGL/CGL): initialization happens lazily on the first entry point
|
||||||
Initialize();
|
// via EnsureInitialized(), and full teardown happens deterministically
|
||||||
}
|
// when the last EGL display is terminated with nothing current (EGLImpl
|
||||||
|
// calls Destroy()). There is intentionally no static constructor, no
|
||||||
__attribute__((destructor)) static void AutoDestroy() {
|
// static destructor, and no DllMain: the global singletons use
|
||||||
if (MG_Config::Features.TraceSkipAutodestroy) {
|
// leak-at-exit storage (see GlobalObjects.cpp), so a process that exits
|
||||||
return;
|
// without eglTerminate simply leaks them to the OS instead of running
|
||||||
}
|
// backend destructors during static teardown.
|
||||||
#if defined(__APPLE__)
|
|
||||||
// macOS injected dylibs can run destructors after logging/backend static state is already torn down.
|
|
||||||
return;
|
|
||||||
#else
|
|
||||||
DestroyImpl(false);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef _WIN32
|
|
||||||
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
|
|
||||||
switch (ul_reason_for_call) {
|
|
||||||
case DLL_PROCESS_ATTACH:
|
|
||||||
Initialize();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case DLL_PROCESS_DETACH:
|
|
||||||
Destroy();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
@@ -11,6 +11,12 @@
|
|||||||
|
|
||||||
namespace MobileGL {
|
namespace MobileGL {
|
||||||
void Initialize();
|
void Initialize();
|
||||||
|
// Thread-safe, idempotent, and re-entrant wrapper around Initialize().
|
||||||
|
// Host layers (EGL/WGL/CGL entry points) call this lazily on first use so
|
||||||
|
// MobileGL's lifecycle never depends on ELF/DLL static constructors, and
|
||||||
|
// so a fresh init can follow a full Destroy() (e.g. after the last
|
||||||
|
// eglTerminate).
|
||||||
|
void EnsureInitialized();
|
||||||
void Destroy();
|
void Destroy();
|
||||||
|
|
||||||
namespace MG_Util::Debug {
|
namespace MG_Util::Debug {
|
||||||
|
|||||||
@@ -313,7 +313,8 @@ namespace MobileGL {
|
|||||||
Android,
|
Android,
|
||||||
X11,
|
X11,
|
||||||
MetalLayer,
|
MetalLayer,
|
||||||
// TODO: Wayland, Windows, etc.
|
Win32, // Handle is an HWND
|
||||||
|
// TODO: Wayland, etc.
|
||||||
WindowBackendCount,
|
WindowBackendCount,
|
||||||
Unknown = -1
|
Unknown = -1
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,6 @@
|
|||||||
#include "DirectVulkan/BackendObject_DirectVulkan.h"
|
#include "DirectVulkan/BackendObject_DirectVulkan.h"
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend {
|
namespace MobileGL::MG_Backend {
|
||||||
extern UniquePtr<BackendObject> pActiveBackendObject;
|
extern UniquePtr<BackendObject>& pActiveBackendObject;
|
||||||
extern GlobalBackendFunctionsTable gBackendFunctionsTable;
|
extern GlobalBackendFunctionsTable gBackendFunctionsTable;
|
||||||
} // namespace MobileGL::MG_Backend
|
} // namespace MobileGL::MG_Backend
|
||||||
|
|||||||
@@ -701,9 +701,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
if ((handle.Backend != WindowBackend::Android &&
|
if ((handle.Backend != WindowBackend::Android &&
|
||||||
handle.Backend != WindowBackend::X11 &&
|
handle.Backend != WindowBackend::X11 &&
|
||||||
handle.Backend != WindowBackend::MetalLayer) ||
|
handle.Backend != WindowBackend::MetalLayer &&
|
||||||
|
handle.Backend != WindowBackend::Win32) ||
|
||||||
!handle.Handle) {
|
!handle.Handle) {
|
||||||
MGLOG_E("DirectGLES backend only supports Android, X11, and CAMetalLayer native windows");
|
MGLOG_E("DirectGLES backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -449,6 +449,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Textures attached only to the READ framebuffer (blit / ReadPixels sources) need
|
||||||
|
// their content synced too, or the backend reads stale texel data.
|
||||||
|
const auto& readFBO =
|
||||||
|
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||||
|
if (readFBO && readFBO != currentFBO) {
|
||||||
|
for (const auto& attachment : readFBO->GetAllAttachmentObjects()) {
|
||||||
|
if (!attachment.IsTexture()) continue;
|
||||||
|
auto& textureObject = attachment.GetTexture();
|
||||||
|
if (textureObject) {
|
||||||
|
SyncTextureObjectToBackend(textureObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Bool SupportsLayeredImageBinding(TextureTarget target) {
|
static Bool SupportsLayeredImageBinding(TextureTarget target) {
|
||||||
@@ -4223,9 +4237,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
if (!ChooseConfigForSurface(surfaceBit, g_Config, window)) return false;
|
if (!ChooseConfigForSurface(surfaceBit, g_Config, window)) return false;
|
||||||
|
|
||||||
const EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
|
// Negotiate the highest ES 3.x context. Version-strict EGL implementations
|
||||||
|
// (ANGLE) return exactly the requested minor, and a bare CLIENT_VERSION 3
|
||||||
|
// request yields a 3.0 context that lacks the 3.1/3.2 texture targets the
|
||||||
|
// capability probes exercise; mobile drivers ignore the minor and hand out
|
||||||
|
// their maximum either way.
|
||||||
|
for (const EGLint minorVersion : {2, 1, 0}) {
|
||||||
|
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3,
|
||||||
|
EGL_CONTEXT_MINOR_VERSION, minorVersion,
|
||||||
|
EGL_NONE};
|
||||||
|
g_Context = g_EGLFuncs.eglCreateContext(g_Display, g_Config, EGL_NO_CONTEXT, contextAttribs);
|
||||||
|
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;
|
return g_Context != EGL_NO_CONTEXT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1262,15 +1262,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const auto& allAttributes = stateVAOObject->GetAllAttributes();
|
const auto& allAttributes = stateVAOObject->GetAllAttributes();
|
||||||
for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) {
|
for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) {
|
||||||
const auto& attrib = allAttributes[attribIndex];
|
const auto& attrib = allAttributes[attribIndex];
|
||||||
|
const Uint32 attribBit = 1u << attribIndex;
|
||||||
|
|
||||||
|
// An enabled attrib with neither a buffer object nor a client pointer has no
|
||||||
|
// source; GL tolerates the state (only draws consuming it are undefined), but
|
||||||
|
// Adreno's ES driver memcpys the "client array" from address 0 at draw time
|
||||||
|
// (SIGSEGV). Keep such attribs disabled on the backend VAO and re-enable them
|
||||||
|
// the moment they gain a source - the mask-vs-current compare below triggers
|
||||||
|
// the enable even when only the Buffer/Format versions changed.
|
||||||
|
const Bool unsourceable = attrib.Enabled && !attrib.Buffer && attrib.Offset == 0;
|
||||||
|
const Bool wasForceDisabled = (m_forceDisabledAttribsMask & attribBit) != 0;
|
||||||
|
|
||||||
Bool needsSyncSwitch = allAttributeVersions[attribIndex].SwitchVersion !=
|
Bool needsSyncSwitch = allAttributeVersions[attribIndex].SwitchVersion !=
|
||||||
m_syncedAttributeVersions[attribIndex].SwitchVersion;
|
m_syncedAttributeVersions[attribIndex].SwitchVersion;
|
||||||
if (needsSyncSwitch) {
|
if (needsSyncSwitch || unsourceable != wasForceDisabled) {
|
||||||
if (attrib.Enabled) {
|
if (attrib.Enabled && !unsourceable) {
|
||||||
g_GLESFuncs.glEnableVertexAttribArray(attribIndex);
|
g_GLESFuncs.glEnableVertexAttribArray(attribIndex);
|
||||||
} else {
|
} else {
|
||||||
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
|
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (unsourceable) {
|
||||||
|
m_forceDisabledAttribsMask |= attribBit;
|
||||||
|
} else {
|
||||||
|
m_forceDisabledAttribsMask &= ~attribBit;
|
||||||
|
}
|
||||||
|
|
||||||
Bool needsSyncFormat = allAttributeVersions[attribIndex].FormatVersion !=
|
Bool needsSyncFormat = allAttributeVersions[attribIndex].FormatVersion !=
|
||||||
m_syncedAttributeVersions[attribIndex].FormatVersion;
|
m_syncedAttributeVersions[attribIndex].FormatVersion;
|
||||||
@@ -1278,7 +1294,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
m_syncedAttributeVersions[attribIndex].BufferVersion;
|
m_syncedAttributeVersions[attribIndex].BufferVersion;
|
||||||
if (!needsSyncFormat && !needsSyncBuffer) continue;
|
if (!needsSyncFormat && !needsSyncBuffer) continue;
|
||||||
|
|
||||||
|
if (unsourceable) continue;
|
||||||
|
|
||||||
|
// Client-side array with a non-null pointer: the pointer is uploaded and applied
|
||||||
|
// per draw by SyncClientSideAttributesForDrawArrays.
|
||||||
|
if (!attrib.Buffer) continue;
|
||||||
|
|
||||||
if (!BindAttributeBuffer(attrib)) {
|
if (!BindAttributeBuffer(attrib)) {
|
||||||
|
if (attrib.Enabled) {
|
||||||
|
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
|
||||||
|
m_forceDisabledAttribsMask |= attribBit;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3265,6 +3291,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
auto& shaderSpirvs = stateProgramObject->GetGeneratedSpirv();
|
auto& shaderSpirvs = stateProgramObject->GetGeneratedSpirv();
|
||||||
|
|
||||||
|
// Adreno's ESSL compiler mishandles gl_ClipDistance (rejects redeclarations,
|
||||||
|
// miscompiles non-constant-index writes and constant-index gl_in element reads,
|
||||||
|
// crashes on whole-array gl_in reads) and cannot dynamically index the global
|
||||||
|
// const struct[] LUTs SPIRV-Cross likes to emit. Gate the workarounds to
|
||||||
|
// Qualcomm; MOBILEGL_QUIRK_CLIP_DISTANCE overrides the device detection.
|
||||||
|
const MG_Config::QuirkOverride clipDistanceQuirkOverride =
|
||||||
|
MG_Config::Features.ClipDistanceQuirk;
|
||||||
|
const Bool applyClipDistanceQuirk =
|
||||||
|
clipDistanceQuirkOverride == MG_Config::QuirkOverride::ForceOn ||
|
||||||
|
(clipDistanceQuirkOverride == MG_Config::QuirkOverride::Auto &&
|
||||||
|
pActiveBackendObject &&
|
||||||
|
pActiveBackendObject->GetDynamicParameters().GpuVendor == GpuVendorKind::Qualcomm);
|
||||||
|
|
||||||
for (int index = 0; index < attachedShaders.size(); ++index) {
|
for (int index = 0; index < attachedShaders.size(); ++index) {
|
||||||
auto& shader = attachedShaders[index];
|
auto& shader = attachedShaders[index];
|
||||||
GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(shader->GetShaderStage());
|
GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(shader->GetShaderStage());
|
||||||
@@ -3287,6 +3326,63 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
effectiveSpirv = &loweredSpirv;
|
effectiveSpirv = &loweredSpirv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL 3.3 only promises undefined *values* for out-of-bounds array indexing, but
|
||||||
|
// Adreno's ESSL compiler constant-folds a provably out-of-bounds local-array
|
||||||
|
// index into poison that corrupts the whole shader's output. Clamp every
|
||||||
|
// access-chain index to its declared bounds before transpiling.
|
||||||
|
Vector<unsigned int> clampedSpirv;
|
||||||
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::ClampAccessChainIndicesForEssl(*effectiveSpirv,
|
||||||
|
clampedSpirv) &&
|
||||||
|
!clampedSpirv.empty()) {
|
||||||
|
effectiveSpirv = &clampedSpirv;
|
||||||
|
} else {
|
||||||
|
MGLOG_W("ClampAccessChainIndicesForEssl failed, continuing with unclamped SPIR-V.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// SPIRV-Cross emulates 1D samplers as 2D for ES: it widens texelFetch coordinates
|
||||||
|
// to ivec2 but keeps the ConstOffset operand scalar, which is not a valid ESSL
|
||||||
|
// texelFetchOffset overload (Adreno rejects it). Fold the constant offset into the
|
||||||
|
// coordinate instead (texelFetchOffset(t,P,l,o) == texelFetch(t,P+o,l)).
|
||||||
|
Vector<unsigned int> foldedOffsetSpirv;
|
||||||
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::FoldConstOffsetFor1DFetchForEssl(
|
||||||
|
*effectiveSpirv, foldedOffsetSpirv) &&
|
||||||
|
!foldedOffsetSpirv.empty()) {
|
||||||
|
effectiveSpirv = &foldedOffsetSpirv;
|
||||||
|
} else {
|
||||||
|
MGLOG_W("FoldConstOffsetFor1DFetchForEssl failed, continuing with unfolded SPIR-V.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adreno quirk: shadow gl_ClipDistance in Private arrays so the transpiled
|
||||||
|
// ESSL only writes the builtin with literal constant indices (flushed before
|
||||||
|
// EmitVertex/return) and only reads gl_in clip distances through dynamic loop
|
||||||
|
// indices - the shapes this driver compiles correctly. Must run after the
|
||||||
|
// access-chain clamp above so the flush indices stay literal constants.
|
||||||
|
Vector<unsigned int> clipDistanceSpirv;
|
||||||
|
if (applyClipDistanceQuirk &&
|
||||||
|
(glShaderType == GL_VERTEX_SHADER || glShaderType == GL_GEOMETRY_SHADER)) {
|
||||||
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerClipDistanceForEssl(
|
||||||
|
*effectiveSpirv, clipDistanceSpirv) &&
|
||||||
|
!clipDistanceSpirv.empty()) {
|
||||||
|
effectiveSpirv = &clipDistanceSpirv;
|
||||||
|
} else {
|
||||||
|
MGLOG_W("LowerClipDistanceForEssl failed, continuing with unlowered SPIR-V.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adreno quirk: split single constant-composite stores of struct arrays so
|
||||||
|
// SPIRV-Cross does not promote them to global const struct[] LUTs, which this
|
||||||
|
// driver cannot dynamically index ("Cannot offset into the structure").
|
||||||
|
Vector<unsigned int> structLutSpirv;
|
||||||
|
if (applyClipDistanceQuirk) {
|
||||||
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::DefeatConstStructArrayLutForEssl(
|
||||||
|
*effectiveSpirv, structLutSpirv) &&
|
||||||
|
!structLutSpirv.empty()) {
|
||||||
|
effectiveSpirv = &structLutSpirv;
|
||||||
|
} else {
|
||||||
|
MGLOG_W("DefeatConstStructArrayLutForEssl failed, continuing with unsplit SPIR-V.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ESSL stage-matches uniform blocks by member precision, but SPIRV-Cross prints
|
// ESSL stage-matches uniform blocks by member precision, but SPIRV-Cross prints
|
||||||
// a RelaxedPrecision member as explicit "mediump" in the vertex stage and as
|
// a RelaxedPrecision member as explicit "mediump" in the vertex stage and as
|
||||||
// UNQUALIFIED (mediump-by-default) in the fragment stage; after
|
// UNQUALIFIED (mediump-by-default) in the fragment stage; after
|
||||||
@@ -3345,6 +3441,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
|
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
|
||||||
source = RemoveLayoutBinding(source);
|
source = RemoveLayoutBinding(source);
|
||||||
|
if (applyClipDistanceQuirk) {
|
||||||
|
// Adreno rejects the gl_ClipDistance redeclaration SPIRV-Cross still emits
|
||||||
|
// ("reserved built-in name") but accepts plain usage with
|
||||||
|
// GL_EXT_clip_cull_distance required; drop the line, keep the #extension.
|
||||||
|
source = RemoveClipDistanceRedeclaration(source);
|
||||||
|
}
|
||||||
source = ProcessOutColorLocations(source);
|
source = ProcessOutColorLocations(source);
|
||||||
source = ForceFlatIntegerVaryings(source, glShaderType);
|
source = ForceFlatIntegerVaryings(source, glShaderType);
|
||||||
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
|
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
|
||||||
|
|||||||
@@ -258,6 +258,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
private:
|
private:
|
||||||
Uint m_backendVAOId = 0;
|
Uint m_backendVAOId = 0;
|
||||||
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
||||||
|
// Attribs the frontend has Enabled but that have no source at all (no buffer object
|
||||||
|
// and NULL client pointer). GL keeps such attribs latently enabled, but Adreno's ES
|
||||||
|
// driver treats them as client arrays and memcpys from address 0 at draw time
|
||||||
|
// (SIGSEGV), so they are kept disabled on the backend VAO until they gain a source.
|
||||||
|
Uint32 m_forceDisabledAttribsMask = 0;
|
||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
Uint16 m_syncedIndexBufferVersion = 0;
|
Uint16 m_syncedIndexBufferVersion = 0;
|
||||||
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
|
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
|
||||||
|
|||||||
@@ -342,6 +342,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String RemoveClipDistanceRedeclaration(const String& glslCode) {
|
||||||
|
#ifdef TRACY_ENABLE
|
||||||
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
|
#endif
|
||||||
|
// Adreno rejects any redeclaration of gl_ClipDistance/gl_CullDistance ("reserved
|
||||||
|
// built-in name") even with GL_EXT_clip_cull_distance required, but accepts plain
|
||||||
|
// usage of the builtin. Drop the desktop-style redeclaration line SPIRV-Cross
|
||||||
|
// prints; the "#extension GL_EXT_clip_cull_distance : require" line stays.
|
||||||
|
static const std::regex redeclarationRegex(
|
||||||
|
R"(^\s*(?:out|in)\s+(?:(?:high|medium|low)p\s+)?float\s+gl_(?:Clip|Cull)Distance\[[0-9]+\];\s*$)");
|
||||||
|
|
||||||
|
String result;
|
||||||
|
result.reserve(glslCode.size());
|
||||||
|
SizeT lineStart = 0;
|
||||||
|
Bool firstLine = true;
|
||||||
|
while (lineStart <= glslCode.size()) {
|
||||||
|
SizeT lineEnd = glslCode.find('\n', lineStart);
|
||||||
|
const Bool lastLine = lineEnd == String::npos;
|
||||||
|
String line = glslCode.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart);
|
||||||
|
|
||||||
|
if (!std::regex_match(line, redeclarationRegex)) {
|
||||||
|
if (!firstLine) {
|
||||||
|
result += '\n';
|
||||||
|
}
|
||||||
|
result += line;
|
||||||
|
firstLine = false;
|
||||||
|
}
|
||||||
|
if (lastLine) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lineStart = lineEnd + 1;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
} // namespace PrgramImpl
|
} // namespace PrgramImpl
|
||||||
|
|
||||||
namespace Utils {
|
namespace Utils {
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Uint32 unormOutputMask);
|
Uint32 unormOutputMask);
|
||||||
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
|
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
|
||||||
String RemoveLayoutBinding(const String& glslCode);
|
String RemoveLayoutBinding(const String& glslCode);
|
||||||
|
String RemoveClipDistanceRedeclaration(const String& glslCode);
|
||||||
} // namespace PrgramImpl
|
} // namespace PrgramImpl
|
||||||
|
|
||||||
namespace Utils {
|
namespace Utils {
|
||||||
|
|||||||
@@ -411,8 +411,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
if (!handle.Handle || (handle.Backend != WindowBackend::Android &&
|
if (!handle.Handle || (handle.Backend != WindowBackend::Android &&
|
||||||
handle.Backend != WindowBackend::X11 &&
|
handle.Backend != WindowBackend::X11 &&
|
||||||
handle.Backend != WindowBackend::MetalLayer)) {
|
handle.Backend != WindowBackend::MetalLayer &&
|
||||||
MGLOG_E("DirectVulkan backend only supports Android, X11, and CAMetalLayer native windows");
|
handle.Backend != WindowBackend::Win32)) {
|
||||||
|
MGLOG_E("DirectVulkan backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
#include <spirv_reflect.h>
|
#include <spirv_reflect.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
UniquePtr<VulkanRenderer> pVulkanRenderer = nullptr;
|
// Leak-at-exit storage; see GlobalObjects.cpp.
|
||||||
|
UniquePtr<VulkanRenderer>& pVulkanRenderer = *new UniquePtr<VulkanRenderer>();
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Generation of the live VulkanRenderer instance, mirroring
|
// Generation of the live VulkanRenderer instance, mirroring
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
#include "Renderer/VulkanRenderer.h"
|
#include "Renderer/VulkanRenderer.h"
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
extern UniquePtr<VulkanRenderer> pVulkanRenderer;
|
extern UniquePtr<VulkanRenderer>& pVulkanRenderer;
|
||||||
|
|
||||||
// Generation of the live VulkanRenderer instance, mirroring DirectGLES's
|
// Generation of the live VulkanRenderer instance, mirroring DirectGLES's
|
||||||
// g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever
|
// g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever
|
||||||
|
|||||||
@@ -2528,15 +2528,23 @@ void main() {
|
|||||||
MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed.");
|
MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed.");
|
||||||
|
|
||||||
// Prime the first frame so Render() always targets an acquired swapchain image.
|
// Prime the first frame so Render() always targets an acquired swapchain image.
|
||||||
VkResult acquireResult =
|
// A zero-area window (GLFW's hidden helper window during the WGL bootstrap, or a
|
||||||
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
|
// window that is already minimized) legitimately yields no swapchain here; defer
|
||||||
if (acquireResult == VK_ERROR_OUT_OF_DATE_KHR || acquireResult == VK_SUBOPTIMAL_KHR) {
|
// the first acquire to Present in that case instead of acquiring from a null
|
||||||
MGLOG_D("Initialize, vkAcquireNextImageKHR got %d, recreating swapchain", acquireResult);
|
// swapchain handle.
|
||||||
RecreateSwapchain();
|
if (m_swapchainObject.GetHandle() != VK_NULL_HANDLE) {
|
||||||
acquireResult =
|
VkResult acquireResult =
|
||||||
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
|
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
|
||||||
|
if (acquireResult == VK_ERROR_OUT_OF_DATE_KHR || acquireResult == VK_SUBOPTIMAL_KHR) {
|
||||||
|
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_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
|
||||||
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
|
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
|
||||||
m_convertedVertexStreams.clear();
|
m_convertedVertexStreams.clear();
|
||||||
@@ -7353,6 +7361,33 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VulkanRenderer::Present() {
|
void VulkanRenderer::Present() {
|
||||||
|
if (m_swapchainObject.GetHandle() == VK_NULL_HANDLE || m_presentSuspended) {
|
||||||
|
// No usable swapchain: the window was zero-area at initialization, or
|
||||||
|
// presentation was suspended when the window minimized. Try to bring a
|
||||||
|
// swapchain up now that the window may have a real size; until then, drop
|
||||||
|
// this frame's recording instead of submitting - a submit would wait on a
|
||||||
|
// never-signaled acquire semaphore and reuse a still-signaled fence.
|
||||||
|
if (!RecreateSwapchain()) {
|
||||||
|
auto& suspendedFrame = m_frameContext.GetCurrent();
|
||||||
|
if (VkRenderPassManager::GetActiveRenderPass()) {
|
||||||
|
VkRenderPassManager::EndRenderPass(suspendedFrame.commandBuffer);
|
||||||
|
}
|
||||||
|
if (suspendedFrame.isCommandRecording) {
|
||||||
|
m_frameContext.EndCommandRecording();
|
||||||
|
}
|
||||||
|
suspendedFrame.isCommandRecording = false;
|
||||||
|
suspendedFrame.hasCommandBufferRecorded = false;
|
||||||
|
m_lastPipelineValid = false;
|
||||||
|
MGLOG_D("Present skipped: no usable swapchain (zero-area window)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_presentSuspended = false;
|
||||||
|
const VkResult acquireResult =
|
||||||
|
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
|
||||||
|
if (acquireResult != VK_SUBOPTIMAL_KHR) {
|
||||||
|
VK_VERIFY(acquireResult, "Present, deferred first WaitAndAcquireNextImage");
|
||||||
|
}
|
||||||
|
}
|
||||||
MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(),
|
MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(),
|
||||||
"Present, acquired image index out of range");
|
"Present, acquired image index out of range");
|
||||||
m_renderPassManager->OnPresent();
|
m_renderPassManager->OnPresent();
|
||||||
@@ -7386,14 +7421,26 @@ void main() {
|
|||||||
auto result = vkQueuePresentKHR(m_presentQueue, &presentPacket.presentInfo);
|
auto result = vkQueuePresentKHR(m_presentQueue, &presentPacket.presentInfo);
|
||||||
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
|
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
|
||||||
MGLOG_D("Present, vkQueuePresentKHR got %d, recreating swapchain", result);
|
MGLOG_D("Present, vkQueuePresentKHR got %d, recreating swapchain", result);
|
||||||
RecreateSwapchain();
|
if (!RecreateSwapchain()) {
|
||||||
|
// Window went zero-area (minimize) with the swapchain out of date:
|
||||||
|
// stop submitting/acquiring until it has a size again.
|
||||||
|
m_presentSuspended = true;
|
||||||
|
m_swapchainResizeRequested = false;
|
||||||
|
MGLOG_D("Present, zero-area window with out-of-date swapchain; suspending presentation");
|
||||||
|
return;
|
||||||
|
}
|
||||||
m_swapchainResizeRequested = false;
|
m_swapchainResizeRequested = false;
|
||||||
result = VK_SUCCESS;
|
result = VK_SUCCESS;
|
||||||
}
|
}
|
||||||
VK_VERIFY(result, "Present, vkQueuePresentKHR");
|
VK_VERIFY(result, "Present, vkQueuePresentKHR");
|
||||||
if (m_swapchainResizeRequested) {
|
if (m_swapchainResizeRequested) {
|
||||||
MGLOG_D("Present, processing requested swapchain resize");
|
MGLOG_D("Present, processing requested swapchain resize");
|
||||||
RecreateSwapchain();
|
if (!RecreateSwapchain()) {
|
||||||
|
m_presentSuspended = true;
|
||||||
|
m_swapchainResizeRequested = false;
|
||||||
|
MGLOG_D("Present, zero-area window on requested resize; suspending presentation");
|
||||||
|
return;
|
||||||
|
}
|
||||||
m_swapchainResizeRequested = false;
|
m_swapchainResizeRequested = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7404,7 +7451,12 @@ void main() {
|
|||||||
result = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
|
result = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
|
||||||
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
|
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
|
||||||
MGLOG_D("Present, vkAcquireNextImageKHR got %d, recreating swapchain", result);
|
MGLOG_D("Present, vkAcquireNextImageKHR got %d, recreating swapchain", result);
|
||||||
RecreateSwapchain();
|
if (!RecreateSwapchain()) {
|
||||||
|
m_presentSuspended = true;
|
||||||
|
m_swapchainResizeRequested = false;
|
||||||
|
MGLOG_D("Present, zero-area window on next-frame acquire; suspending presentation");
|
||||||
|
return;
|
||||||
|
}
|
||||||
m_swapchainResizeRequested = false;
|
m_swapchainResizeRequested = false;
|
||||||
result =
|
result =
|
||||||
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
|
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
|
||||||
@@ -8056,6 +8108,7 @@ void main() {
|
|||||||
MOBILEGL_ASSERT(hwnd, "HWND is null");
|
MOBILEGL_ASSERT(hwnd, "HWND is null");
|
||||||
|
|
||||||
VkWin32SurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR};
|
VkWin32SurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR};
|
||||||
|
sci.hinstance = GetModuleHandleW(nullptr);
|
||||||
sci.hwnd = hwnd;
|
sci.hwnd = hwnd;
|
||||||
VK_VERIFY(vkCreateWin32SurfaceKHR(m_instance, &sci, nullptr, &m_surface), "vkCreateWin32SurfaceKHR failed");
|
VK_VERIFY(vkCreateWin32SurfaceKHR(m_instance, &sci, nullptr, &m_surface), "vkCreateWin32SurfaceKHR failed");
|
||||||
#elif defined VK_USE_PLATFORM_METAL_EXT
|
#elif defined VK_USE_PLATFORM_METAL_EXT
|
||||||
@@ -8217,13 +8270,13 @@ void main() {
|
|||||||
m_swapchainObject.Shutdown(m_device);
|
m_swapchainObject.Shutdown(m_device);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VulkanRenderer::RecreateSwapchain() {
|
Bool VulkanRenderer::RecreateSwapchain() {
|
||||||
// Handle cases like minimize on Windows, where swapchain could return a 0x0 extent
|
// Handle cases like minimize on Windows, where swapchain could return a 0x0 extent
|
||||||
const auto swapchainCapabilities =
|
const auto swapchainCapabilities =
|
||||||
SwapchainObject::GetSwapchainCapabilities(m_physicalDevice.handle, m_surface);
|
SwapchainObject::GetSwapchainCapabilities(m_physicalDevice.handle, m_surface);
|
||||||
if (swapchainCapabilities.capabilities.currentExtent.width == 0 ||
|
if (swapchainCapabilities.capabilities.currentExtent.width == 0 ||
|
||||||
swapchainCapabilities.capabilities.currentExtent.height == 0) {
|
swapchainCapabilities.capabilities.currentExtent.height == 0) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
vkDeviceWaitIdle(m_device);
|
vkDeviceWaitIdle(m_device);
|
||||||
@@ -8267,6 +8320,7 @@ void main() {
|
|||||||
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
|
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
|
||||||
m_convertedVertexStreams.clear();
|
m_convertedVertexStreams.clear();
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PhysicalDevice& VulkanRenderer::GetPhysicalDevice() const {
|
const PhysicalDevice& VulkanRenderer::GetPhysicalDevice() const {
|
||||||
|
|||||||
@@ -257,7 +257,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;
|
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;
|
||||||
|
|
||||||
void RequestSwapchainResize(Uint32 width, Uint32 height);
|
void RequestSwapchainResize(Uint32 width, Uint32 height);
|
||||||
void RecreateSwapchain();
|
// Returns false when the surface is zero-area (minimized/hidden window):
|
||||||
|
// no new swapchain is installed and presentation must stay suspended.
|
||||||
|
Bool RecreateSwapchain();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct BlitUniformData {
|
struct BlitUniformData {
|
||||||
@@ -355,6 +357,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void* m_platformCloseDisplay = nullptr;
|
void* m_platformCloseDisplay = nullptr;
|
||||||
VulkanRendererConfig m_config;
|
VulkanRendererConfig m_config;
|
||||||
Bool m_swapchainResizeRequested = false;
|
Bool m_swapchainResizeRequested = false;
|
||||||
|
// Presentation is suspended while the window is zero-area (minimized): the
|
||||||
|
// swapchain is unusable/out of date, so Present drops frames instead of
|
||||||
|
// submitting on a signaled fence / presenting never-acquired images.
|
||||||
|
Bool m_presentSuspended = false;
|
||||||
|
|
||||||
// Vulkan objects
|
// Vulkan objects
|
||||||
Bool m_validationLayersEnabled = false;
|
Bool m_validationLayersEnabled = false;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "EGLImpl.h"
|
#include "EGLImpl.h"
|
||||||
#include "../GetProcAddress.h"
|
#include "../GetProcAddress.h"
|
||||||
|
#include <Init.h>
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
#include <MG_State/EGLState/Core.h>
|
#include <MG_State/EGLState/Core.h>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -25,6 +26,17 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
return MG_State::pEGLContext.get();
|
return MG_State::pEGLContext.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Entry points that can legitimately be an application's FIRST EGL
|
||||||
|
// call (display/proc-address/string queries) lazily bring MobileGL
|
||||||
|
// up here, so the library needs no static constructor and can
|
||||||
|
// re-initialize after the last eglTerminate tore everything down.
|
||||||
|
// Teardown-ish entry points keep using GetState() and fail benignly
|
||||||
|
// when MobileGL is not initialized.
|
||||||
|
EGLStateContext* GetStateEnsureInitialized() {
|
||||||
|
MobileGL::EnsureInitialized();
|
||||||
|
return GetState();
|
||||||
|
}
|
||||||
|
|
||||||
MG_Backend::BackendObject* GetBackendObject(EGLStateContext* state) {
|
MG_Backend::BackendObject* GetBackendObject(EGLStateContext* state) {
|
||||||
auto* backendObject = MG_Backend::pActiveBackendObject.get();
|
auto* backendObject = MG_Backend::pActiveBackendObject.get();
|
||||||
if (!backendObject && state) {
|
if (!backendObject && state) {
|
||||||
@@ -49,6 +61,8 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
return MG_Backend::WindowBackend::Android;
|
return MG_Backend::WindowBackend::Android;
|
||||||
#elif defined(__APPLE__)
|
#elif defined(__APPLE__)
|
||||||
return MG_Backend::WindowBackend::MetalLayer;
|
return MG_Backend::WindowBackend::MetalLayer;
|
||||||
|
#elif defined(_WIN32)
|
||||||
|
return MG_Backend::WindowBackend::Win32;
|
||||||
#elif defined(__linux__)
|
#elif defined(__linux__)
|
||||||
return MG_Backend::WindowBackend::X11;
|
return MG_Backend::WindowBackend::X11;
|
||||||
#else
|
#else
|
||||||
@@ -187,7 +201,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) {
|
EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) {
|
||||||
auto* state = GetState();
|
auto* state = GetStateEnsureInitialized();
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return EGL_FALSE;
|
return EGL_FALSE;
|
||||||
}
|
}
|
||||||
@@ -208,7 +222,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EGLDisplay GetDisplay(NativeDisplayType display) {
|
EGLDisplay GetDisplay(NativeDisplayType display) {
|
||||||
auto* state = GetState();
|
auto* state = GetStateEnsureInitialized();
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return EGL_NO_DISPLAY;
|
return EGL_NO_DISPLAY;
|
||||||
}
|
}
|
||||||
@@ -313,6 +327,14 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
|
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
|
||||||
backendObject->ReleaseEGLResources();
|
backendObject->ReleaseEGLResources();
|
||||||
}
|
}
|
||||||
|
// The last initialized display is gone and nothing is current on any
|
||||||
|
// thread: tear the whole library down deterministically inside the
|
||||||
|
// EGL lifecycle (backend, GL/EGL state, glslang). A later EGL call
|
||||||
|
// re-initializes lazily via GetStateEnsureInitialized(); process exit
|
||||||
|
// then has nothing left to destroy.
|
||||||
|
if (!state->HasAnyInitializedDisplay() && !state->HasAnyCurrentContext()) {
|
||||||
|
MobileGL::Destroy();
|
||||||
|
}
|
||||||
return EGL_TRUE;
|
return EGL_TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,7 +367,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EGLBoolean BindAPI(EGLenum api) {
|
EGLBoolean BindAPI(EGLenum api) {
|
||||||
auto* state = GetState();
|
auto* state = GetStateEnsureInitialized();
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return EGL_FALSE;
|
return EGL_FALSE;
|
||||||
}
|
}
|
||||||
@@ -378,7 +400,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
char const* QueryString(EGLDisplay display, EGLint name) {
|
char const* QueryString(EGLDisplay display, EGLint name) {
|
||||||
auto* state = GetState();
|
auto* state = GetStateEnsureInitialized();
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -641,7 +663,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list) {
|
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list) {
|
||||||
(void)attrib_list;
|
(void)attrib_list;
|
||||||
|
|
||||||
auto* state = GetState();
|
auto* state = GetStateEnsureInitialized();
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return EGL_NO_DISPLAY;
|
return EGL_NO_DISPLAY;
|
||||||
}
|
}
|
||||||
@@ -737,6 +759,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
if (!name) {
|
if (!name) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
MobileGL::EnsureInitialized();
|
||||||
|
|
||||||
MGLOG_D("eglGetProcAddress(%s)", name);
|
MGLOG_D("eglGetProcAddress(%s)", name);
|
||||||
void* proc = MG_Impl::GetProcAddress(name);
|
void* proc = MG_Impl::GetProcAddress(name);
|
||||||
|
|||||||
@@ -2144,6 +2144,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace FramebufferImpl {
|
namespace FramebufferImpl {
|
||||||
UniquePtr<DefaultFramebufferInfo> pDefaultFramebufferInfo;
|
// Leak-at-exit storage; see GlobalObjects.cpp.
|
||||||
|
UniquePtr<DefaultFramebufferInfo>& pDefaultFramebufferInfo = *new UniquePtr<DefaultFramebufferInfo>();
|
||||||
} // namespace FramebufferImpl
|
} // namespace FramebufferImpl
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -78,6 +78,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
SharedPtr<MG_State::GLState::ITextureObject> stencilAttachment;
|
SharedPtr<MG_State::GLState::ITextureObject> stencilAttachment;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern UniquePtr<DefaultFramebufferInfo> pDefaultFramebufferInfo;
|
extern UniquePtr<DefaultFramebufferInfo>& pDefaultFramebufferInfo;
|
||||||
} // namespace FramebufferImpl
|
} // namespace FramebufferImpl
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
#include <MG_State/GLState/TextureState/TextureObjectStubs.h>
|
#include <MG_State/GLState/TextureState/TextureObjectStubs.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||||
UniquePtr<ProxyTextureManager> pProxyTextureManager;
|
// Leak-at-exit storage; see GlobalObjects.cpp.
|
||||||
|
UniquePtr<ProxyTextureManager>& pProxyTextureManager = *new UniquePtr<ProxyTextureManager>();
|
||||||
|
|
||||||
Bool IsProxyTextureTarget(TextureUploadTarget target) {
|
Bool IsProxyTextureTarget(TextureUploadTarget target) {
|
||||||
switch (target) {
|
switch (target) {
|
||||||
|
|||||||
@@ -23,5 +23,5 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
UnorderedMap<TextureUploadTarget, SharedPtr<MG_State::GLState::ITextureObject>> m_proxyTexturesMap;
|
UnorderedMap<TextureUploadTarget, SharedPtr<MG_State::GLState::ITextureObject>> m_proxyTexturesMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern UniquePtr<ProxyTextureManager> pProxyTextureManager;
|
extern UniquePtr<ProxyTextureManager>& pProxyTextureManager;
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/WGLImpl/Exporting/Definitions.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// wingdi.h declares most wgl* entry points as WINGDIAPI (__declspec(dllimport)),
|
||||||
|
// which would reject our definitions. _GDI32_ is the SDK's "I am the module that
|
||||||
|
// implements these" switch: it turns WINGDIAPI into a plain declaration. It must
|
||||||
|
// be defined before the first windows.h inclusion in this translation unit.
|
||||||
|
#if defined(_WIN32) && !defined(_GDI32_)
|
||||||
|
#define _GDI32_ 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#include "../WGLImpl.h"
|
||||||
|
|
||||||
|
namespace WGL = MobileGL::MG_Impl::WGLImpl;
|
||||||
|
|
||||||
|
// ---- Pixel-format entry points (gdi32 forwards ChoosePixelFormat/SetPixelFormat/
|
||||||
|
// ---- DescribePixelFormat/GetPixelFormat/SwapBuffers into these exports) ----
|
||||||
|
|
||||||
|
extern "C" int WINAPI wglChoosePixelFormat(HDC hdc, CONST PIXELFORMATDESCRIPTOR* ppfd) {
|
||||||
|
return WGL::ChoosePixelFormat(hdc, ppfd);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" int WINAPI wglDescribePixelFormat(HDC hdc, int iPixelFormat, UINT nBytes,
|
||||||
|
LPPIXELFORMATDESCRIPTOR ppfd) {
|
||||||
|
return WGL::DescribePixelFormat(hdc, iPixelFormat, nBytes, ppfd);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" int WINAPI wglGetPixelFormat(HDC hdc) {
|
||||||
|
return WGL::GetPixelFormat(hdc);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" BOOL WINAPI wglSetPixelFormat(HDC hdc, int iPixelFormat, CONST PIXELFORMATDESCRIPTOR* ppfd) {
|
||||||
|
return WGL::SetPixelFormat(hdc, iPixelFormat, ppfd);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" BOOL WINAPI wglSwapBuffers(HDC hdc) {
|
||||||
|
return WGL::SwapBuffers(hdc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Context management ----
|
||||||
|
|
||||||
|
extern "C" HGLRC WINAPI wglCreateContext(HDC hdc) {
|
||||||
|
return WGL::CreateContext(hdc);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" HGLRC WINAPI wglCreateLayerContext(HDC hdc, int iLayerPlane) {
|
||||||
|
return iLayerPlane == 0 ? WGL::CreateContext(hdc) : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" BOOL WINAPI wglCopyContext(HGLRC, HGLRC, UINT) {
|
||||||
|
MGLOG_W("wglCopyContext is not supported");
|
||||||
|
SetLastError(ERROR_NOT_SUPPORTED);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" BOOL WINAPI wglDeleteContext(HGLRC hglrc) {
|
||||||
|
return WGL::DeleteContext(hglrc);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" HGLRC WINAPI wglGetCurrentContext(VOID) {
|
||||||
|
return WGL::GetCurrentContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" HDC WINAPI wglGetCurrentDC(VOID) {
|
||||||
|
return WGL::GetCurrentDC();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" BOOL WINAPI wglMakeCurrent(HDC hdc, HGLRC hglrc) {
|
||||||
|
return WGL::MakeCurrent(hdc, hglrc);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" BOOL WINAPI wglShareLists(HGLRC hglrcShare, HGLRC hglrcDest) {
|
||||||
|
return WGL::ShareLists(hglrcShare, hglrcDest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Proc address ----
|
||||||
|
|
||||||
|
extern "C" PROC WINAPI wglGetProcAddress(LPCSTR lpszProc) {
|
||||||
|
return WGL::GetProcAddress(lpszProc);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" PROC WINAPI wglGetDefaultProcAddress(LPCSTR lpszProc) {
|
||||||
|
return WGL::GetProcAddress(lpszProc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Layer planes and palettes (unsupported; overlay planes do not exist here) ----
|
||||||
|
|
||||||
|
extern "C" BOOL WINAPI wglDescribeLayerPlane(HDC, int, int, UINT, LPLAYERPLANEDESCRIPTOR) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" int WINAPI wglSetLayerPaletteEntries(HDC, int, int, int, CONST COLORREF*) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" int WINAPI wglGetLayerPaletteEntries(HDC, int, int, int, COLORREF*) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" BOOL WINAPI wglRealizeLayerPalette(HDC, int, BOOL) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" BOOL WINAPI wglSwapLayerBuffers(HDC hdc, UINT fuPlanes) {
|
||||||
|
if (fuPlanes & WGL_SWAP_MAIN_PLANE) {
|
||||||
|
return WGL::SwapBuffers(hdc);
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" DWORD WINAPI wglSwapMultipleBuffers(UINT n, CONST WGLSWAP* ps) {
|
||||||
|
if (!ps) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
DWORD swapped = 0;
|
||||||
|
for (UINT i = 0; i < n; ++i) {
|
||||||
|
if (WGL::SwapBuffers(ps[i].hdc)) {
|
||||||
|
++swapped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return swapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Font rendering (legacy immediate-mode feature; not supported) ----
|
||||||
|
|
||||||
|
extern "C" BOOL WINAPI wglUseFontBitmapsA(HDC, DWORD, DWORD, DWORD) {
|
||||||
|
MGLOG_W("wglUseFontBitmapsA is not supported");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" BOOL WINAPI wglUseFontBitmapsW(HDC, DWORD, DWORD, DWORD) {
|
||||||
|
MGLOG_W("wglUseFontBitmapsW is not supported");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" BOOL WINAPI wglUseFontOutlinesA(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
|
||||||
|
LPGLYPHMETRICSFLOAT) {
|
||||||
|
MGLOG_W("wglUseFontOutlinesA is not supported");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" BOOL WINAPI wglUseFontOutlinesW(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
|
||||||
|
LPGLYPHMETRICSFLOAT) {
|
||||||
|
MGLOG_W("wglUseFontOutlinesW is not supported");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // _WIN32
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
; MobileGL WGL exports. The wgl* entry points are defined without
|
||||||
|
; __declspec(dllexport) because wingdi.h pre-declares them (with _GDI32_ they
|
||||||
|
; become plain declarations, and MSVC rejects adding dllexport afterwards),
|
||||||
|
; so this .def file is what actually exports them from the DLL.
|
||||||
|
EXPORTS
|
||||||
|
wglChoosePixelFormat
|
||||||
|
wglCopyContext
|
||||||
|
wglCreateContext
|
||||||
|
wglCreateLayerContext
|
||||||
|
wglDeleteContext
|
||||||
|
wglDescribeLayerPlane
|
||||||
|
wglDescribePixelFormat
|
||||||
|
wglGetCurrentContext
|
||||||
|
wglGetCurrentDC
|
||||||
|
wglGetDefaultProcAddress
|
||||||
|
wglGetLayerPaletteEntries
|
||||||
|
wglGetPixelFormat
|
||||||
|
wglGetProcAddress
|
||||||
|
wglMakeCurrent
|
||||||
|
wglRealizeLayerPalette
|
||||||
|
wglSetLayerPaletteEntries
|
||||||
|
wglSetPixelFormat
|
||||||
|
wglShareLists
|
||||||
|
wglSwapBuffers
|
||||||
|
wglSwapLayerBuffers
|
||||||
|
wglSwapMultipleBuffers
|
||||||
|
wglUseFontBitmapsA
|
||||||
|
wglUseFontBitmapsW
|
||||||
|
wglUseFontOutlinesA
|
||||||
|
wglUseFontOutlinesW
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -368,6 +368,26 @@ namespace MobileGL {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool EGLContext::HasAnyInitializedDisplay() const {
|
||||||
|
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||||
|
for (const auto& [handle, displayObject] : m_displays) {
|
||||||
|
if (displayObject.Initialized) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool EGLContext::HasAnyCurrentContext() const {
|
||||||
|
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||||
|
for (const auto& [threadId, current] : m_threadCurrents) {
|
||||||
|
if (current.Context != nullptr) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Bool EGLContext::ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs,
|
Bool EGLContext::ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs,
|
||||||
EGLint configSize, EGLint* numConfig) {
|
EGLint configSize, EGLint* numConfig) {
|
||||||
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||||
@@ -1415,6 +1435,7 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
} // namespace EGLState
|
} // namespace EGLState
|
||||||
|
|
||||||
UniquePtr<EGLState::EGLContext> pEGLContext;
|
// Leak-at-exit storage; see GlobalObjects.cpp.
|
||||||
|
UniquePtr<EGLState::EGLContext>& pEGLContext = *new UniquePtr<EGLState::EGLContext>();
|
||||||
} // namespace MG_State
|
} // namespace MG_State
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ namespace MobileGL {
|
|||||||
Bool IsDisplayInitialized(EGLDisplayHandle display) const;
|
Bool IsDisplayInitialized(EGLDisplayHandle display) const;
|
||||||
Bool InitializeDisplay(EGLDisplayHandle display, EGLint* major, EGLint* minor);
|
Bool InitializeDisplay(EGLDisplayHandle display, EGLint* major, EGLint* minor);
|
||||||
Bool TerminateDisplay(EGLDisplayHandle display);
|
Bool TerminateDisplay(EGLDisplayHandle display);
|
||||||
|
// Whole-library idle checks used by EGLImpl::Terminate to decide
|
||||||
|
// when the last eglTerminate may tear MobileGL down entirely.
|
||||||
|
Bool HasAnyInitializedDisplay() const;
|
||||||
|
Bool HasAnyCurrentContext() const;
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
Bool ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs,
|
Bool ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs,
|
||||||
@@ -262,6 +266,6 @@ namespace MobileGL {
|
|||||||
};
|
};
|
||||||
} // namespace EGLState
|
} // namespace EGLState
|
||||||
|
|
||||||
extern UniquePtr<EGLState::EGLContext> pEGLContext;
|
extern UniquePtr<EGLState::EGLContext>& pEGLContext;
|
||||||
} // namespace MG_State
|
} // namespace MG_State
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
@@ -721,5 +721,6 @@ namespace MobileGL::MG_State {
|
|||||||
}
|
}
|
||||||
} // namespace GLState
|
} // namespace GLState
|
||||||
|
|
||||||
UniquePtr<GLState::GLContext> pGLContext;
|
// Leak-at-exit storage; see GlobalObjects.cpp.
|
||||||
|
UniquePtr<GLState::GLContext>& pGLContext = *new UniquePtr<GLState::GLContext>();
|
||||||
} // namespace MobileGL::MG_State
|
} // namespace MobileGL::MG_State
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ namespace MobileGL {
|
|||||||
};
|
};
|
||||||
} // namespace GLState
|
} // namespace GLState
|
||||||
|
|
||||||
extern UniquePtr<GLState::GLContext> pGLContext;
|
extern UniquePtr<GLState::GLContext>& pGLContext;
|
||||||
|
|
||||||
// True when relaxed GL semantics apply. Strict core rules are enforced only when the
|
// True when relaxed GL semantics apply. Strict core rules are enforced only when the
|
||||||
// current EGL context explicitly requested a core profile (core bit in
|
// current EGL context explicitly requested a core profile (core bit in
|
||||||
|
|||||||
@@ -9,7 +9,12 @@
|
|||||||
#include "Loader.h"
|
#include "Loader.h"
|
||||||
#include "MG_Util/Types.h"
|
#include "MG_Util/Types.h"
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#if !defined(__WIN32) && !defined(_WIN32)
|
#if defined(_WIN32)
|
||||||
|
#ifndef WIN32_LEAN_AND_MEAN
|
||||||
|
#define WIN32_LEAN_AND_MEAN 1
|
||||||
|
#endif
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -60,7 +65,14 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
static void* OpenLib(const Vector<String>& names) {
|
static void* OpenLib(const Vector<String>& names) {
|
||||||
#if !defined(__WIN32) && !defined(_WIN32) && (!defined(__APPLE__) || defined(MOBILEGL_IOS))
|
#if defined(_WIN32)
|
||||||
|
for (const auto& name : names) {
|
||||||
|
if (HMODULE lib = LoadLibraryA(name.c_str())) {
|
||||||
|
MGLOG_I("Loaded GL backend library: %s", name.c_str());
|
||||||
|
return reinterpret_cast<void*>(lib);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#elif !defined(__APPLE__) || defined(MOBILEGL_IOS)
|
||||||
static const String LibPathPrefixes[] = {
|
static const String LibPathPrefixes[] = {
|
||||||
#if defined(MOBILEGL_IOS)
|
#if defined(MOBILEGL_IOS)
|
||||||
"@rpath/", "@executable_path/Frameworks/", "@loader_path/Frameworks/",
|
"@rpath/", "@executable_path/Frameworks/", "@loader_path/Frameworks/",
|
||||||
@@ -104,7 +116,9 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline void* ProcAddress(void* lib, const char* name) {
|
inline void* ProcAddress(void* lib, const char* name) {
|
||||||
#if !defined(__WIN32) && !defined(_WIN32) && (!defined(__APPLE__) || defined(MOBILEGL_IOS))
|
#if defined(_WIN32)
|
||||||
|
return reinterpret_cast<void*>(::GetProcAddress(reinterpret_cast<HMODULE>(lib), name));
|
||||||
|
#elif !defined(__APPLE__) || defined(MOBILEGL_IOS)
|
||||||
return dlsym(lib, name);
|
return dlsym(lib, name);
|
||||||
#else
|
#else
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -520,6 +534,15 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS) && defined(__ANDROID__)
|
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS) && defined(__ANDROID__)
|
||||||
void* angleGlesLib = nullptr;
|
void* angleGlesLib = nullptr;
|
||||||
#endif
|
#endif
|
||||||
|
#if defined(_WIN32)
|
||||||
|
// ANGLE is the GLES provider on Windows regardless of UseAngle(). Preload
|
||||||
|
// libGLESv2.dll so libEGL.dll resolves its dependency from the same directory.
|
||||||
|
if (!OpenLib({"libGLESv2.dll"})) {
|
||||||
|
MGLOG_E("Failed to open ANGLE libGLESv2.dll");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
eglLib = OpenLib({"libEGL.dll"});
|
||||||
|
#else
|
||||||
if (UseAngle()) {
|
if (UseAngle()) {
|
||||||
void* glesLib = OpenLib({"libGLESv2_angle.so"});
|
void* glesLib = OpenLib({"libGLESv2_angle.so"});
|
||||||
if (!glesLib) {
|
if (!glesLib) {
|
||||||
@@ -541,6 +564,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
eglLib = OpenLib({"libEGL.so"});
|
eglLib = OpenLib({"libEGL.so"});
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
#endif // !_WIN32
|
||||||
|
|
||||||
if (!eglLib) {
|
if (!eglLib) {
|
||||||
MGLOG_E("Failed to open EGL library");
|
MGLOG_E("Failed to open EGL library");
|
||||||
|
|||||||
@@ -9,10 +9,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
|
|
||||||
|
// Severity-ordered: a build compiled at level X keeps X and everything MORE
|
||||||
|
// severe. INFO builds must keep WARN/ERROR/FATAL — the old ordering
|
||||||
|
// (WARN=1/ERROR=2 below INFO=3) compiled every warning and error out of
|
||||||
|
// release builds and hid real backend failures.
|
||||||
#define MOBILEGL_LOG_LEVEL_DEBUG 0
|
#define MOBILEGL_LOG_LEVEL_DEBUG 0
|
||||||
#define MOBILEGL_LOG_LEVEL_WARN 1
|
#define MOBILEGL_LOG_LEVEL_INFO 1
|
||||||
#define MOBILEGL_LOG_LEVEL_ERROR 2
|
#define MOBILEGL_LOG_LEVEL_WARN 2
|
||||||
#define MOBILEGL_LOG_LEVEL_INFO 3
|
#define MOBILEGL_LOG_LEVEL_ERROR 3
|
||||||
#define MOBILEGL_LOG_LEVEL_FATAL 4
|
#define MOBILEGL_LOG_LEVEL_FATAL 4
|
||||||
|
|
||||||
#define MOBILEGL_LOG_INTERNAL(levelTag, androidLogLevel, fmt, ...) \
|
#define MOBILEGL_LOG_INTERNAL(levelTag, androidLogLevel, fmt, ...) \
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||||
|
#include "SpirvPasses/FoldConstOffsetFor1DFetchPass.h"
|
||||||
|
#include "SpirvPasses/LowerClipDistanceForEsslPass.h"
|
||||||
|
#include "SpirvPasses/DefeatConstStructArrayLutPass.h"
|
||||||
#include "spirv-tools/libspirv.h"
|
#include "spirv-tools/libspirv.h"
|
||||||
#include "spirv-tools/optimizer.hpp"
|
#include "spirv-tools/optimizer.hpp"
|
||||||
|
|
||||||
@@ -323,6 +326,55 @@ namespace MobileGL {
|
|||||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ShaderCompiler::ClampAccessChainIndicesForEssl(const Vector<Uint32>& inputBinary,
|
||||||
|
Vector<uint32_t>& outputBinary) {
|
||||||
|
using namespace spvtools;
|
||||||
|
OptimizerOptions options;
|
||||||
|
options.set_run_validator(false);
|
||||||
|
|
||||||
|
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||||
|
optimizer.RegisterPass(CreateGraphicsRobustAccessPass());
|
||||||
|
|
||||||
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShaderCompiler::FoldConstOffsetFor1DFetchForEssl(const Vector<Uint32>& inputBinary,
|
||||||
|
Vector<uint32_t>& outputBinary) {
|
||||||
|
using namespace spvtools;
|
||||||
|
OptimizerOptions options;
|
||||||
|
options.set_run_validator(false);
|
||||||
|
|
||||||
|
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||||
|
optimizer.RegisterPass(FoldConstOffsetFor1DFetchPass::CreateFoldConstOffsetFor1DFetchPass());
|
||||||
|
|
||||||
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShaderCompiler::LowerClipDistanceForEssl(const Vector<Uint32>& inputBinary,
|
||||||
|
Vector<uint32_t>& outputBinary) {
|
||||||
|
using namespace spvtools;
|
||||||
|
OptimizerOptions options;
|
||||||
|
options.set_run_validator(false);
|
||||||
|
|
||||||
|
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||||
|
optimizer.RegisterPass(LowerClipDistanceForEsslPass::CreateLowerClipDistanceForEsslPass());
|
||||||
|
|
||||||
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShaderCompiler::DefeatConstStructArrayLutForEssl(const Vector<Uint32>& inputBinary,
|
||||||
|
Vector<uint32_t>& outputBinary) {
|
||||||
|
using namespace spvtools;
|
||||||
|
OptimizerOptions options;
|
||||||
|
options.set_run_validator(false);
|
||||||
|
|
||||||
|
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||||
|
optimizer.RegisterPass(
|
||||||
|
DefeatConstStructArrayLutPass::CreateDefeatConstStructArrayLutPass());
|
||||||
|
|
||||||
|
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||||
|
}
|
||||||
|
|
||||||
bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
|
bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary) {
|
Vector<uint32_t>& outputBinary) {
|
||||||
using namespace spvtools;
|
using namespace spvtools;
|
||||||
|
|||||||
@@ -27,6 +27,36 @@ namespace MobileGL {
|
|||||||
// Only for backends without native draw-parameter support (DirectGLES).
|
// Only for backends without native draw-parameter support (DirectGLES).
|
||||||
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary);
|
Vector<uint32_t>& outputBinary);
|
||||||
|
// Clamps every access-chain index to its declared bounds (spirv-tools
|
||||||
|
// GraphicsRobustAccessPass). GL 3.3 only promises undefined *values* for
|
||||||
|
// out-of-bounds indexing, but Adreno's ESSL compiler constant-folds a provably
|
||||||
|
// out-of-bounds local-array index into poison that corrupts the whole shader's
|
||||||
|
// output; clamping restores the "some value from the array" contract. Only for
|
||||||
|
// the DirectGLES transpile path.
|
||||||
|
static bool ClampAccessChainIndicesForEssl(const Vector<Uint32>& inputBinary,
|
||||||
|
Vector<uint32_t>& outputBinary);
|
||||||
|
// Folds the ConstOffset image operand of Dim1D OpImageFetch into the integer
|
||||||
|
// coordinate (texelFetchOffset(t,P,l,o) == texelFetch(t,P+o,l)). SPIRV-Cross
|
||||||
|
// emulates 1D samplers as 2D for ES: it widens the coordinate to ivec2 but keeps
|
||||||
|
// the scalar offset, and ESSL has no texelFetchOffset(sampler2D, ivec2, int,
|
||||||
|
// scalar) overload, so Adreno rejects the shader. Only for the DirectGLES
|
||||||
|
// transpile path.
|
||||||
|
static bool FoldConstOffsetFor1DFetchForEssl(const Vector<Uint32>& inputBinary,
|
||||||
|
Vector<uint32_t>& outputBinary);
|
||||||
|
// Shadows gl_ClipDistance in Private mg_ClipDistance/mg_ClipDistanceIn arrays so
|
||||||
|
// the decompiled ESSL only writes the builtin with literal constant indices
|
||||||
|
// (flush before EmitVertex/return) and only reads gl_in clip distances with
|
||||||
|
// dynamic loop indices (copy loop): the other shapes miscompile or crash
|
||||||
|
// Adreno's ESSL compiler. Vertex/geometry stages; DirectGLES transpile path on
|
||||||
|
// Qualcomm only (quirk-gated). See LowerClipDistanceForEsslPass.
|
||||||
|
static bool LowerClipDistanceForEssl(const Vector<Uint32>& inputBinary,
|
||||||
|
Vector<uint32_t>& outputBinary);
|
||||||
|
// Splits a Function-storage array-of-structs variable's single constant-composite
|
||||||
|
// store into per-element stores so SPIRV-Cross does not hoist it into a global
|
||||||
|
// const struct[] LUT, which Adreno cannot dynamically index. DirectGLES
|
||||||
|
// transpile path on Qualcomm only (quirk-gated). See DefeatConstStructArrayLutPass.
|
||||||
|
static bool DefeatConstStructArrayLutForEssl(const Vector<Uint32>& inputBinary,
|
||||||
|
Vector<uint32_t>& outputBinary);
|
||||||
// Drops RelaxedPrecision member decorations from uniform-block structs so
|
// Drops RelaxedPrecision member decorations from uniform-block structs so
|
||||||
// SPIRV-Cross prints the same (highp) member precision in every stage; ES
|
// SPIRV-Cross prints the same (highp) member precision in every stage; ES
|
||||||
// drivers reject cross-stage uniform blocks whose member precisions differ.
|
// drivers reject cross-stage uniform blocks whose member precisions differ.
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "DefeatConstStructArrayLutPass.h"
|
||||||
|
|
||||||
|
#include "spirv.hpp"
|
||||||
|
#include "source/opt/constants.h"
|
||||||
|
#include "source/opt/def_use_manager.h"
|
||||||
|
#include "source/opt/instruction.h"
|
||||||
|
#include "source/opt/ir_context.h"
|
||||||
|
#include "source/opt/module.h"
|
||||||
|
#include "source/opt/type_manager.h"
|
||||||
|
#include "source/opt/types.h"
|
||||||
|
#include "source/util/make_unique.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
namespace {
|
||||||
|
using spvtools::opt::BasicBlock;
|
||||||
|
using spvtools::opt::Function;
|
||||||
|
using spvtools::opt::Instruction;
|
||||||
|
using spvtools::opt::IRContext;
|
||||||
|
using spvtools::opt::Operand;
|
||||||
|
namespace analysis = spvtools::opt::analysis;
|
||||||
|
|
||||||
|
uint32_t PointerTypeTo(IRContext* ctx, uint32_t pointeeId, spv::StorageClass sc) {
|
||||||
|
analysis::Type* pointee = ctx->get_type_mgr()->GetType(pointeeId);
|
||||||
|
analysis::Pointer ptr(pointee, sc);
|
||||||
|
return ctx->get_type_mgr()->GetTypeInstruction(&ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t SignedIntConstant(IRContext* ctx, uint32_t value) {
|
||||||
|
analysis::Integer i(32, true);
|
||||||
|
analysis::Type* reg = ctx->get_type_mgr()->GetRegisteredType(&i);
|
||||||
|
const analysis::Constant* c = ctx->get_constant_mgr()->GetConstant(reg, {value});
|
||||||
|
return ctx->get_constant_mgr()->GetDefiningInstruction(c)->result_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
// True when |var| (a Function-storage OpVariable) points to an array of structs.
|
||||||
|
// Reports the struct type id on success.
|
||||||
|
bool IsArrayOfStructsVariable(IRContext* ctx, Instruction* var, uint32_t& structTypeId) {
|
||||||
|
auto* defUse = ctx->get_def_use_mgr();
|
||||||
|
Instruction* ptrType = defUse->GetDef(var->type_id());
|
||||||
|
if (ptrType == nullptr || ptrType->opcode() != spv::Op::OpTypePointer) return false;
|
||||||
|
Instruction* pointee = defUse->GetDef(ptrType->GetSingleWordInOperand(1));
|
||||||
|
if (pointee == nullptr || pointee->opcode() != spv::Op::OpTypeArray) return false;
|
||||||
|
Instruction* element = defUse->GetDef(pointee->GetSingleWordInOperand(0));
|
||||||
|
if (element == nullptr || element->opcode() != spv::Op::OpTypeStruct) return false;
|
||||||
|
structTypeId = element->result_id();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
spvtools::opt::Pass::Status DefeatConstStructArrayLutPass::Process() {
|
||||||
|
auto* ctx = context();
|
||||||
|
auto* defUse = ctx->get_def_use_mgr();
|
||||||
|
bool modified = false;
|
||||||
|
|
||||||
|
for (Function& function : *get_module()) {
|
||||||
|
if (function.begin() == function.end()) continue;
|
||||||
|
BasicBlock* entryBlock = &*function.begin();
|
||||||
|
|
||||||
|
// Candidate variables: Function-storage arrays of structs declared in this
|
||||||
|
// function's entry block (where OpVariables must live).
|
||||||
|
struct Candidate {
|
||||||
|
Instruction* var;
|
||||||
|
uint32_t structTypeId;
|
||||||
|
};
|
||||||
|
std::vector<Candidate> candidates;
|
||||||
|
for (Instruction& inst : *entryBlock) {
|
||||||
|
if (inst.opcode() != spv::Op::OpVariable) break;
|
||||||
|
// Variables with initializers keep SPIRV-Cross's initializer path; the
|
||||||
|
// glslang pattern under attack is initializer-free with one OpStore.
|
||||||
|
if (inst.NumInOperands() > 1) continue;
|
||||||
|
uint32_t structTypeId = 0;
|
||||||
|
if (IsArrayOfStructsVariable(ctx, &inst, structTypeId)) {
|
||||||
|
candidates.push_back({&inst, structTypeId});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const Candidate& candidate : candidates) {
|
||||||
|
Instruction* var = candidate.var;
|
||||||
|
|
||||||
|
// The variable qualifies only when its single write is one direct
|
||||||
|
// OpStore of an OpConstantComposite; any other write shape already
|
||||||
|
// defeats SPIRV-Cross's LUT promotion, so it is left untouched.
|
||||||
|
Instruction* singleStore = nullptr;
|
||||||
|
bool disqualified = false;
|
||||||
|
defUse->ForEachUser(var, [&](Instruction* user) {
|
||||||
|
if (user->opcode() == spv::Op::OpStore &&
|
||||||
|
user->GetSingleWordInOperand(0) == var->result_id()) {
|
||||||
|
if (singleStore != nullptr) {
|
||||||
|
disqualified = true;
|
||||||
|
} else {
|
||||||
|
singleStore = user;
|
||||||
|
}
|
||||||
|
} else if (user->opcode() == spv::Op::OpCopyMemory) {
|
||||||
|
disqualified = true;
|
||||||
|
} else if (user->opcode() == spv::Op::OpAccessChain ||
|
||||||
|
user->opcode() == spv::Op::OpInBoundsAccessChain) {
|
||||||
|
defUse->ForEachUser(user, [&](Instruction* chainUser) {
|
||||||
|
if (chainUser->opcode() == spv::Op::OpStore ||
|
||||||
|
chainUser->opcode() == spv::Op::OpCopyMemory) {
|
||||||
|
disqualified = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (disqualified || singleStore == nullptr) continue;
|
||||||
|
|
||||||
|
Instruction* composite = defUse->GetDef(singleStore->GetSingleWordInOperand(1));
|
||||||
|
if (composite == nullptr ||
|
||||||
|
composite->opcode() != spv::Op::OpConstantComposite) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The store must sit in the entry block: that is the only placement
|
||||||
|
// SPIRV-Cross treats as a LUT initializer.
|
||||||
|
bool storeInEntryBlock = false;
|
||||||
|
for (Instruction& inst : *entryBlock) {
|
||||||
|
if (&inst == singleStore) {
|
||||||
|
storeInEntryBlock = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!storeInEntryBlock) continue;
|
||||||
|
|
||||||
|
// Split the composite store into one constant-index store per element.
|
||||||
|
const uint32_t ptrFnStruct =
|
||||||
|
PointerTypeTo(ctx, candidate.structTypeId, spv::StorageClass::Function);
|
||||||
|
for (uint32_t element = 0; element < composite->NumInOperands(); ++element) {
|
||||||
|
const uint32_t elementConstId = composite->GetSingleWordInOperand(element);
|
||||||
|
const uint32_t chainId = ctx->TakeNextId();
|
||||||
|
Instruction* chain =
|
||||||
|
singleStore->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpAccessChain, ptrFnStruct, chainId,
|
||||||
|
std::initializer_list<Operand>{
|
||||||
|
{SPV_OPERAND_TYPE_ID, {var->result_id()}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {SignedIntConstant(ctx, element)}}}));
|
||||||
|
ctx->AnalyzeDefUse(chain);
|
||||||
|
Instruction* store =
|
||||||
|
singleStore->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpStore, 0, 0,
|
||||||
|
std::initializer_list<Operand>{
|
||||||
|
{SPV_OPERAND_TYPE_ID, {chainId}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {elementConstId}}}));
|
||||||
|
ctx->AnalyzeDefUse(store);
|
||||||
|
}
|
||||||
|
ctx->KillInst(singleStore);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!modified) {
|
||||||
|
return Status::SuccessWithoutChange;
|
||||||
|
}
|
||||||
|
ctx->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
|
||||||
|
return Status::SuccessWithChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
spvtools::Optimizer::PassToken
|
||||||
|
DefeatConstStructArrayLutPass::CreateDefeatConstStructArrayLutPass() {
|
||||||
|
return spvtools::Optimizer::PassToken(MakeUnique<DefeatConstStructArrayLutPass>());
|
||||||
|
}
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "source/opt/pass.h"
|
||||||
|
#include "spirv-tools/optimizer.hpp"
|
||||||
|
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
// SPIRV-Cross hoists a Function-storage array variable whose only write is a single
|
||||||
|
// constant-composite store into a global `const struct[]` LUT (variable_is_lut).
|
||||||
|
// Adreno's ESSL compiler cannot dynamically index such a global const struct array
|
||||||
|
// ("Cannot offset into the structure" - device-verified on Adreno 750). Splitting
|
||||||
|
// the one composite store into per-element constant-index stores makes
|
||||||
|
// variable_is_lut fail, so SPIRV-Cross keeps the array as an ordinary local that
|
||||||
|
// Adreno indexes fine. Scalar/vector const arrays are unaffected on Adreno and are
|
||||||
|
// left alone - only arrays OF STRUCTS are rewritten. Only meant for the DirectGLES
|
||||||
|
// transpile path on Qualcomm devices.
|
||||||
|
class DefeatConstStructArrayLutPass : public spvtools::opt::Pass {
|
||||||
|
public:
|
||||||
|
const char* name() const override { return "defeat-const-struct-array-lut"; }
|
||||||
|
Status Process() override;
|
||||||
|
|
||||||
|
static spvtools::Optimizer::PassToken CreateDefeatConstStructArrayLutPass();
|
||||||
|
};
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "FoldConstOffsetFor1DFetchPass.h"
|
||||||
|
|
||||||
|
#include "spirv.hpp"
|
||||||
|
#include "source/opt/def_use_manager.h"
|
||||||
|
#include "source/opt/instruction.h"
|
||||||
|
#include "source/opt/ir_builder.h"
|
||||||
|
#include "source/opt/ir_context.h"
|
||||||
|
#include "source/opt/module.h"
|
||||||
|
#include "source/util/make_unique.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
namespace {
|
||||||
|
using spvtools::opt::Instruction;
|
||||||
|
using spvtools::opt::InstructionBuilder;
|
||||||
|
using spvtools::opt::IRContext;
|
||||||
|
using spvtools::opt::Operand;
|
||||||
|
|
||||||
|
// Number of ImageOperands ids that precede the ConstOffset id: one per
|
||||||
|
// lower-order bit set in the mask, except Grad which carries two ids.
|
||||||
|
uint32_t CountIdsBeforeConstOffset(uint32_t mask) {
|
||||||
|
uint32_t count = 0;
|
||||||
|
if (mask & static_cast<uint32_t>(spv::ImageOperandsMask::Bias)) count += 1;
|
||||||
|
if (mask & static_cast<uint32_t>(spv::ImageOperandsMask::Lod)) count += 1;
|
||||||
|
if (mask & static_cast<uint32_t>(spv::ImageOperandsMask::Grad)) count += 2;
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
spvtools::opt::Pass::Status FoldConstOffsetFor1DFetchPass::Process() {
|
||||||
|
auto* irContext = context();
|
||||||
|
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||||
|
Bool modified = false;
|
||||||
|
|
||||||
|
constexpr uint32_t kConstOffsetBit =
|
||||||
|
static_cast<uint32_t>(spv::ImageOperandsMask::ConstOffset);
|
||||||
|
|
||||||
|
for (auto& function : *get_module()) {
|
||||||
|
for (auto& block : function) {
|
||||||
|
for (auto& inst : block) {
|
||||||
|
if (inst.opcode() != spv::Op::OpImageFetch) continue;
|
||||||
|
// In-operands: image, coordinate, [ImageOperands mask, ids...].
|
||||||
|
if (inst.NumInOperands() < 3) continue;
|
||||||
|
const uint32_t operandsMask = inst.GetSingleWordInOperand(2);
|
||||||
|
if ((operandsMask & kConstOffsetBit) == 0) continue;
|
||||||
|
|
||||||
|
Instruction* imageInst = defUseMgr->GetDef(inst.GetSingleWordInOperand(0));
|
||||||
|
if (imageInst == nullptr) continue;
|
||||||
|
Instruction* imageType = defUseMgr->GetDef(imageInst->type_id());
|
||||||
|
if (imageType == nullptr || imageType->opcode() != spv::Op::OpTypeImage ||
|
||||||
|
static_cast<spv::Dim>(imageType->GetSingleWordInOperand(1)) != spv::Dim::Dim1D) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t offsetOperandIndex = 3 + CountIdsBeforeConstOffset(operandsMask);
|
||||||
|
const uint32_t offsetId = inst.GetSingleWordInOperand(offsetOperandIndex);
|
||||||
|
|
||||||
|
const uint32_t coordId = inst.GetSingleWordInOperand(1);
|
||||||
|
Instruction* coordType = defUseMgr->GetDef(defUseMgr->GetDef(coordId)->type_id());
|
||||||
|
|
||||||
|
InstructionBuilder builder(
|
||||||
|
irContext, &inst,
|
||||||
|
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||||
|
|
||||||
|
uint32_t newCoordId = 0;
|
||||||
|
if (coordType->opcode() == spv::Op::OpTypeVector) {
|
||||||
|
// Arrayed 1D fetch: component 0 is the texel coordinate,
|
||||||
|
// component 1 the layer - only component 0 takes the offset.
|
||||||
|
const uint32_t componentTypeId = coordType->GetSingleWordInOperand(0);
|
||||||
|
Instruction* extracted = builder.AddCompositeExtract(componentTypeId, coordId, {0});
|
||||||
|
Instruction* sum =
|
||||||
|
builder.AddIAdd(componentTypeId, extracted->result_id(), offsetId);
|
||||||
|
Instruction* inserted = builder.AddInstruction(spvtools::MakeUnique<Instruction>(
|
||||||
|
irContext, spv::Op::OpCompositeInsert, coordType->result_id(),
|
||||||
|
irContext->TakeNextId(),
|
||||||
|
std::initializer_list<Operand>{
|
||||||
|
{SPV_OPERAND_TYPE_ID, {sum->result_id()}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {coordId}},
|
||||||
|
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {0}}}));
|
||||||
|
newCoordId = inserted->result_id();
|
||||||
|
} else {
|
||||||
|
Instruction* sum = builder.AddIAdd(coordType->result_id(), coordId, offsetId);
|
||||||
|
newCoordId = sum->result_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t newMask = operandsMask & ~kConstOffsetBit;
|
||||||
|
// 3 fixed operands + the offset id: anything beyond that is another
|
||||||
|
// image-operand id that must keep the mask word alive.
|
||||||
|
const Bool otherOperandIdsRemain = inst.NumInOperands() > 4;
|
||||||
|
|
||||||
|
irContext->ForgetUses(&inst);
|
||||||
|
std::vector<Operand> newOperands;
|
||||||
|
newOperands.push_back(inst.GetInOperand(0));
|
||||||
|
newOperands.push_back({SPV_OPERAND_TYPE_ID, {newCoordId}});
|
||||||
|
if (newMask != 0 || otherOperandIdsRemain) {
|
||||||
|
Operand maskOperand = inst.GetInOperand(2);
|
||||||
|
maskOperand.words[0] = newMask;
|
||||||
|
newOperands.push_back(maskOperand);
|
||||||
|
for (uint32_t i = 3; i < inst.NumInOperands(); ++i) {
|
||||||
|
if (i == offsetOperandIndex) continue;
|
||||||
|
newOperands.push_back(inst.GetInOperand(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inst.SetInOperands(std::move(newOperands));
|
||||||
|
irContext->AnalyzeUses(&inst);
|
||||||
|
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return modified ? Status::SuccessWithChange : Status::SuccessWithoutChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
spvtools::Optimizer::PassToken FoldConstOffsetFor1DFetchPass::CreateFoldConstOffsetFor1DFetchPass() {
|
||||||
|
return spvtools::Optimizer::PassToken(MakeUnique<FoldConstOffsetFor1DFetchPass>());
|
||||||
|
}
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "source/opt/pass.h"
|
||||||
|
#include "spirv-tools/optimizer.hpp"
|
||||||
|
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
// SPIRV-Cross emulates 1D textures as 2D for ES targets: it widens the texelFetch
|
||||||
|
// coordinate to ivec2 but keeps the ConstOffset image operand scalar, and ESSL has
|
||||||
|
// no texelFetchOffset(sampler2D, ivec2, int, scalar-offset) overload, so drivers
|
||||||
|
// (Adreno) reject the transpiled shader. This pass folds the constant offset into
|
||||||
|
// the integer coordinate before the fetch - texelFetchOffset(t, P, l, o) ==
|
||||||
|
// texelFetch(t, P + o, l) per the GLSL spec - and drops the ConstOffset operand,
|
||||||
|
// so SPIRV-Cross emits a plain texelFetch. For arrayed 1D fetches only coordinate
|
||||||
|
// component 0 is offset (component 1 is the layer). Only meant for the DirectGLES
|
||||||
|
// transpile path.
|
||||||
|
class FoldConstOffsetFor1DFetchPass : public spvtools::opt::Pass {
|
||||||
|
public:
|
||||||
|
const char* name() const override { return "fold-const-offset-for-1d-fetch"; }
|
||||||
|
Status Process() override;
|
||||||
|
|
||||||
|
static spvtools::Optimizer::PassToken CreateFoldConstOffsetFor1DFetchPass();
|
||||||
|
};
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -0,0 +1,613 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "LowerClipDistanceForEsslPass.h"
|
||||||
|
|
||||||
|
#include "spirv.hpp"
|
||||||
|
#include "source/opt/basic_block.h"
|
||||||
|
#include "source/opt/constants.h"
|
||||||
|
#include "source/opt/def_use_manager.h"
|
||||||
|
#include "source/opt/instruction.h"
|
||||||
|
#include "source/opt/ir_context.h"
|
||||||
|
#include "source/opt/module.h"
|
||||||
|
#include "source/opt/type_manager.h"
|
||||||
|
#include "source/opt/types.h"
|
||||||
|
#include "source/util/make_unique.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
namespace {
|
||||||
|
using spvtools::opt::BasicBlock;
|
||||||
|
using spvtools::opt::Function;
|
||||||
|
using spvtools::opt::Instruction;
|
||||||
|
using spvtools::opt::IRContext;
|
||||||
|
using spvtools::opt::Operand;
|
||||||
|
namespace analysis = spvtools::opt::analysis;
|
||||||
|
|
||||||
|
spv::ExecutionModel EntryExecutionModel(IRContext* ctx) {
|
||||||
|
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||||
|
return static_cast<spv::ExecutionModel>(ep.GetSingleWordInOperand(0));
|
||||||
|
}
|
||||||
|
return spv::ExecutionModel::Max;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t EntryFunctionId(IRContext* ctx) {
|
||||||
|
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||||
|
// OpEntryPoint <model> <function> "name" <interface...>
|
||||||
|
return ep.GetSingleWordInOperand(1);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t VariablePointeeType(IRContext* ctx, Instruction* var) {
|
||||||
|
Instruction* ptrType = ctx->get_def_use_mgr()->GetDef(var->type_id());
|
||||||
|
// OpTypePointer <storage-class> <pointee>
|
||||||
|
return ptrType->GetSingleWordInOperand(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t PointerTypeTo(IRContext* ctx, uint32_t pointeeId, spv::StorageClass sc) {
|
||||||
|
analysis::Type* pointee = ctx->get_type_mgr()->GetType(pointeeId);
|
||||||
|
analysis::Pointer ptr(pointee, sc);
|
||||||
|
return ctx->get_type_mgr()->GetTypeInstruction(&ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t IntConstant(IRContext* ctx, bool isSigned, uint32_t value) {
|
||||||
|
analysis::Integer i(32, isSigned);
|
||||||
|
analysis::Type* reg = ctx->get_type_mgr()->GetRegisteredType(&i);
|
||||||
|
const analysis::Constant* c = ctx->get_constant_mgr()->GetConstant(reg, {value});
|
||||||
|
return ctx->get_constant_mgr()->GetDefiningInstruction(c)->result_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t UintType(IRContext* ctx) {
|
||||||
|
analysis::Integer i(32, false);
|
||||||
|
return ctx->get_type_mgr()->GetTypeInstruction(&i);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t BoolType(IRContext* ctx) {
|
||||||
|
analysis::Bool b;
|
||||||
|
return ctx->get_type_mgr()->GetTypeInstruction(&b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constant length of OpTypeArray |arrayTypeId| (0 when not a sized constant).
|
||||||
|
uint32_t ArrayLength(IRContext* ctx, uint32_t arrayTypeId) {
|
||||||
|
Instruction* arrayType = ctx->get_def_use_mgr()->GetDef(arrayTypeId);
|
||||||
|
if (arrayType == nullptr || arrayType->opcode() != spv::Op::OpTypeArray) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
Instruction* length = ctx->get_def_use_mgr()->GetDef(arrayType->GetSingleWordInOperand(1));
|
||||||
|
if (length == nullptr || length->opcode() != spv::Op::OpConstant) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return length->GetSingleWordInOperand(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsConstantWithValue(IRContext* ctx, uint32_t id, uint32_t value) {
|
||||||
|
Instruction* def = ctx->get_def_use_mgr()->GetDef(id);
|
||||||
|
return def != nullptr && def->opcode() == spv::Op::OpConstant &&
|
||||||
|
def->GetSingleWordInOperand(0) == value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsAccessChain(const Instruction* inst) {
|
||||||
|
return inst->opcode() == spv::Op::OpAccessChain ||
|
||||||
|
inst->opcode() == spv::Op::OpInBoundsAccessChain;
|
||||||
|
}
|
||||||
|
|
||||||
|
Instruction* AddPrivateVariable(IRContext* ctx, uint32_t pointeeTypeId, const char* name) {
|
||||||
|
const uint32_t ptrType = PointerTypeTo(ctx, pointeeTypeId, spv::StorageClass::Private);
|
||||||
|
const uint32_t varId = ctx->TakeNextId();
|
||||||
|
ctx->AddGlobalValue(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpVariable, ptrType, varId,
|
||||||
|
std::initializer_list<Operand>{
|
||||||
|
{SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||||
|
{static_cast<uint32_t>(spv::StorageClass::Private)}}}));
|
||||||
|
ctx->AddDebug2Inst(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpName, 0, 0,
|
||||||
|
std::initializer_list<Operand>{
|
||||||
|
{SPV_OPERAND_TYPE_ID, {varId}},
|
||||||
|
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(name)}}));
|
||||||
|
return ctx->get_def_use_mgr()->GetDef(varId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retargets |chain| onto |newBaseId|, dropping the first |dropIndexCount| index
|
||||||
|
// operands and switching the result pointer's storage class to Private.
|
||||||
|
void RetargetChainToPrivate(IRContext* ctx, Instruction* chain, uint32_t newBaseId,
|
||||||
|
uint32_t dropIndexCount) {
|
||||||
|
Instruction* chainPtrType = ctx->get_def_use_mgr()->GetDef(chain->type_id());
|
||||||
|
const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1);
|
||||||
|
const uint32_t newPtrType = PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private);
|
||||||
|
|
||||||
|
ctx->ForgetUses(chain);
|
||||||
|
std::vector<Operand> newOperands;
|
||||||
|
newOperands.push_back({SPV_OPERAND_TYPE_ID, {newBaseId}});
|
||||||
|
for (uint32_t i = 1 + dropIndexCount; i < chain->NumInOperands(); ++i) {
|
||||||
|
newOperands.push_back(chain->GetInOperand(i));
|
||||||
|
}
|
||||||
|
chain->SetResultType(newPtrType);
|
||||||
|
chain->SetInOperands(std::move(newOperands));
|
||||||
|
ctx->AnalyzeUses(chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Output side --------------------------------------------------------------
|
||||||
|
|
||||||
|
struct OutputTarget {
|
||||||
|
Instruction* var = nullptr; // Output gl_PerVertex block or standalone builtin
|
||||||
|
bool isBlockMember = false;
|
||||||
|
uint32_t memberIndex = 0; // valid when isBlockMember
|
||||||
|
uint32_t arrayTypeId = 0; // float[N]
|
||||||
|
uint32_t elemTypeId = 0; // float
|
||||||
|
uint32_t arrayLen = 0; // N
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inserts "gl_ClipDistance[k] = mg_ClipDistance[k]" for every literal k before
|
||||||
|
// |before|. Constant-index writes are the only write shape Adreno links correctly.
|
||||||
|
void InsertFlushBefore(IRContext* ctx, Instruction* before, const OutputTarget& target,
|
||||||
|
uint32_t mgVarId) {
|
||||||
|
const uint32_t ptrPrivElem =
|
||||||
|
PointerTypeTo(ctx, target.elemTypeId, spv::StorageClass::Private);
|
||||||
|
const uint32_t ptrOutElem =
|
||||||
|
PointerTypeTo(ctx, target.elemTypeId, spv::StorageClass::Output);
|
||||||
|
for (uint32_t k = 0; k < target.arrayLen; ++k) {
|
||||||
|
const uint32_t kConst = IntConstant(ctx, true, k);
|
||||||
|
const uint32_t srcChainId = ctx->TakeNextId();
|
||||||
|
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpAccessChain, ptrPrivElem, srcChainId,
|
||||||
|
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {mgVarId}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {kConst}}}));
|
||||||
|
const uint32_t valId = ctx->TakeNextId();
|
||||||
|
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpLoad, target.elemTypeId, valId,
|
||||||
|
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {srcChainId}}}));
|
||||||
|
const uint32_t dstChainId = ctx->TakeNextId();
|
||||||
|
std::vector<Operand> dstOperands;
|
||||||
|
dstOperands.push_back({SPV_OPERAND_TYPE_ID, {target.var->result_id()}});
|
||||||
|
if (target.isBlockMember) {
|
||||||
|
dstOperands.push_back(
|
||||||
|
{SPV_OPERAND_TYPE_ID, {IntConstant(ctx, true, target.memberIndex)}});
|
||||||
|
}
|
||||||
|
dstOperands.push_back({SPV_OPERAND_TYPE_ID, {kConst}});
|
||||||
|
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpAccessChain, ptrOutElem, dstChainId, dstOperands));
|
||||||
|
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpStore, 0, 0,
|
||||||
|
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {dstChainId}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {valId}}}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LowerOutputClipDistance(IRContext* ctx, bool isGeometry) {
|
||||||
|
auto* defUse = ctx->get_def_use_mgr();
|
||||||
|
|
||||||
|
// Collect (struct type, member) pairs decorated BuiltIn ClipDistance and
|
||||||
|
// standalone variables decorated BuiltIn ClipDistance.
|
||||||
|
std::vector<std::pair<uint32_t, uint32_t>> memberTargets; // (structId, member)
|
||||||
|
std::vector<uint32_t> plainTargets; // variable ids
|
||||||
|
for (Instruction& ann : ctx->annotations()) {
|
||||||
|
if (ann.opcode() == spv::Op::OpMemberDecorate && ann.NumInOperands() >= 4 &&
|
||||||
|
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) ==
|
||||||
|
spv::Decoration::BuiltIn &&
|
||||||
|
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(3)) ==
|
||||||
|
spv::BuiltIn::ClipDistance) {
|
||||||
|
memberTargets.emplace_back(ann.GetSingleWordInOperand(0),
|
||||||
|
ann.GetSingleWordInOperand(1));
|
||||||
|
} else if (ann.opcode() == spv::Op::OpDecorate && ann.NumInOperands() >= 3 &&
|
||||||
|
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) ==
|
||||||
|
spv::Decoration::BuiltIn &&
|
||||||
|
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(2)) ==
|
||||||
|
spv::BuiltIn::ClipDistance) {
|
||||||
|
plainTargets.push_back(ann.GetSingleWordInOperand(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<OutputTarget> targets;
|
||||||
|
for (Instruction& inst : ctx->module()->types_values()) {
|
||||||
|
if (inst.opcode() != spv::Op::OpVariable ||
|
||||||
|
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) !=
|
||||||
|
spv::StorageClass::Output) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const uint32_t pointee = VariablePointeeType(ctx, &inst);
|
||||||
|
for (const auto& [structId, member] : memberTargets) {
|
||||||
|
if (pointee != structId) continue;
|
||||||
|
Instruction* structType = defUse->GetDef(structId);
|
||||||
|
if (structType == nullptr || member >= structType->NumInOperands()) continue;
|
||||||
|
OutputTarget target;
|
||||||
|
target.var = &inst;
|
||||||
|
target.isBlockMember = true;
|
||||||
|
target.memberIndex = member;
|
||||||
|
target.arrayTypeId = structType->GetSingleWordInOperand(member);
|
||||||
|
target.arrayLen = ArrayLength(ctx, target.arrayTypeId);
|
||||||
|
targets.push_back(target);
|
||||||
|
}
|
||||||
|
for (const uint32_t varId : plainTargets) {
|
||||||
|
if (inst.result_id() != varId) continue;
|
||||||
|
OutputTarget target;
|
||||||
|
target.var = &inst;
|
||||||
|
target.isBlockMember = false;
|
||||||
|
target.arrayTypeId = pointee;
|
||||||
|
target.arrayLen = ArrayLength(ctx, target.arrayTypeId);
|
||||||
|
targets.push_back(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
for (OutputTarget& target : targets) {
|
||||||
|
if (target.arrayLen == 0) continue;
|
||||||
|
Instruction* arrayType = defUse->GetDef(target.arrayTypeId);
|
||||||
|
target.elemTypeId = arrayType->GetSingleWordInOperand(0);
|
||||||
|
|
||||||
|
// Collect the accesses to redirect. For the block form only chains whose
|
||||||
|
// leading index selects the ClipDistance member count; for the standalone
|
||||||
|
// form every chain plus whole-variable loads/stores.
|
||||||
|
std::vector<Instruction*> chains;
|
||||||
|
std::vector<Instruction*> directAccesses;
|
||||||
|
bool unsupportedUse = false;
|
||||||
|
defUse->ForEachUser(target.var, [&](Instruction* user) {
|
||||||
|
if (IsAccessChain(user) &&
|
||||||
|
user->GetSingleWordInOperand(0) == target.var->result_id()) {
|
||||||
|
if (target.isBlockMember) {
|
||||||
|
if (user->NumInOperands() >= 2 &&
|
||||||
|
IsConstantWithValue(ctx, user->GetSingleWordInOperand(1),
|
||||||
|
target.memberIndex)) {
|
||||||
|
chains.push_back(user);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chains.push_back(user);
|
||||||
|
}
|
||||||
|
} else if (!target.isBlockMember) {
|
||||||
|
if (user->opcode() == spv::Op::OpLoad ||
|
||||||
|
(user->opcode() == spv::Op::OpStore &&
|
||||||
|
user->GetSingleWordInOperand(0) == target.var->result_id())) {
|
||||||
|
directAccesses.push_back(user);
|
||||||
|
} else if (user->opcode() == spv::Op::OpCopyMemory) {
|
||||||
|
unsupportedUse = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (unsupportedUse || (chains.empty() && directAccesses.empty())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Instruction* mgVar = AddPrivateVariable(ctx, target.arrayTypeId, "mg_ClipDistance");
|
||||||
|
const uint32_t mgVarId = mgVar->result_id();
|
||||||
|
|
||||||
|
for (Instruction* chain : chains) {
|
||||||
|
const uint32_t dropCount = target.isBlockMember ? 1u : 0u;
|
||||||
|
if (chain->NumInOperands() == 1 + dropCount) {
|
||||||
|
// Pointer to the whole float[N]: reuse the private variable itself.
|
||||||
|
ctx->ReplaceAllUsesWith(chain->result_id(), mgVarId);
|
||||||
|
ctx->KillInst(chain);
|
||||||
|
} else {
|
||||||
|
RetargetChainToPrivate(ctx, chain, mgVarId, dropCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Instruction* access : directAccesses) {
|
||||||
|
ctx->ForgetUses(access);
|
||||||
|
access->SetInOperand(0, {mgVarId});
|
||||||
|
ctx->AnalyzeUses(access);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush the shadow into the real builtin: geometry right before every
|
||||||
|
// EmitVertex, vertex before every return of the entry point. The flush is
|
||||||
|
// also what keeps the builtin statically used for cross-stage IO matching.
|
||||||
|
std::vector<Instruction*> flushSites;
|
||||||
|
if (isGeometry) {
|
||||||
|
for (Function& function : *ctx->module()) {
|
||||||
|
function.ForEachInst([&](Instruction* inst) {
|
||||||
|
if (inst->opcode() == spv::Op::OpEmitVertex) {
|
||||||
|
flushSites.push_back(inst);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const uint32_t entryFuncId = EntryFunctionId(ctx);
|
||||||
|
for (Function& function : *ctx->module()) {
|
||||||
|
if (function.result_id() != entryFuncId) continue;
|
||||||
|
function.ForEachInst([&](Instruction* inst) {
|
||||||
|
if (inst->opcode() == spv::Op::OpReturn ||
|
||||||
|
inst->opcode() == spv::Op::OpReturnValue) {
|
||||||
|
flushSites.push_back(inst);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Instruction* site : flushSites) {
|
||||||
|
InsertFlushBefore(ctx, site, target, mgVarId);
|
||||||
|
}
|
||||||
|
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Input side (geometry gl_in) ----------------------------------------------
|
||||||
|
|
||||||
|
bool LowerInputClipDistance(IRContext* ctx) {
|
||||||
|
auto* defUse = ctx->get_def_use_mgr();
|
||||||
|
auto* typeMgr = ctx->get_type_mgr();
|
||||||
|
|
||||||
|
// Locate the gl_in block member decorated ClipDistance.
|
||||||
|
Instruction* glInVar = nullptr;
|
||||||
|
uint32_t memberIndex = 0;
|
||||||
|
uint32_t arrayTypeId = 0; // float[N]
|
||||||
|
for (Instruction& ann : ctx->annotations()) {
|
||||||
|
if (ann.opcode() != spv::Op::OpMemberDecorate || ann.NumInOperands() < 4 ||
|
||||||
|
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) !=
|
||||||
|
spv::Decoration::BuiltIn ||
|
||||||
|
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(3)) !=
|
||||||
|
spv::BuiltIn::ClipDistance) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const uint32_t structId = ann.GetSingleWordInOperand(0);
|
||||||
|
const uint32_t member = ann.GetSingleWordInOperand(1);
|
||||||
|
for (Instruction& inst : ctx->module()->types_values()) {
|
||||||
|
if (inst.opcode() != spv::Op::OpVariable ||
|
||||||
|
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) !=
|
||||||
|
spv::StorageClass::Input) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const uint32_t pointee = VariablePointeeType(ctx, &inst);
|
||||||
|
Instruction* pointeeType = defUse->GetDef(pointee);
|
||||||
|
if (pointeeType == nullptr || pointeeType->opcode() != spv::Op::OpTypeArray ||
|
||||||
|
pointeeType->GetSingleWordInOperand(0) != structId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Instruction* structType = defUse->GetDef(structId);
|
||||||
|
if (structType == nullptr || member >= structType->NumInOperands()) continue;
|
||||||
|
glInVar = &inst;
|
||||||
|
memberIndex = member;
|
||||||
|
arrayTypeId = structType->GetSingleWordInOperand(member);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (glInVar != nullptr) break;
|
||||||
|
}
|
||||||
|
if (glInVar == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t clipCount = ArrayLength(ctx, arrayTypeId);
|
||||||
|
const uint32_t vertexCount = ArrayLength(ctx, VariablePointeeType(ctx, glInVar));
|
||||||
|
if (clipCount == 0 || vertexCount == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every gl_in chain that selects the ClipDistance member:
|
||||||
|
// (vertex, member) yields a whole float[N], (vertex, member, k) an element.
|
||||||
|
std::vector<Instruction*> chains;
|
||||||
|
defUse->ForEachUser(glInVar, [&](Instruction* user) {
|
||||||
|
if (IsAccessChain(user) && user->GetSingleWordInOperand(0) == glInVar->result_id() &&
|
||||||
|
user->NumInOperands() >= 3 &&
|
||||||
|
IsConstantWithValue(ctx, user->GetSingleWordInOperand(2), memberIndex)) {
|
||||||
|
chains.push_back(user);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (chains.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Instruction* arrayTypeInst = defUse->GetDef(arrayTypeId);
|
||||||
|
const uint32_t elemTypeId = arrayTypeInst->GetSingleWordInOperand(0);
|
||||||
|
|
||||||
|
// Private mg_ClipDistanceIn = float[vertexCount][clipCount].
|
||||||
|
const uint32_t vertexCountConst = IntConstant(ctx, false, vertexCount);
|
||||||
|
analysis::Type* innerType = typeMgr->GetType(arrayTypeId);
|
||||||
|
analysis::Array outerArray(
|
||||||
|
innerType, analysis::Array::LengthInfo{
|
||||||
|
vertexCountConst,
|
||||||
|
{analysis::Array::LengthInfo::kConstant, vertexCount}});
|
||||||
|
const uint32_t outerArrayTypeId = typeMgr->GetTypeInstruction(&outerArray);
|
||||||
|
Instruction* mgInVar = AddPrivateVariable(ctx, outerArrayTypeId, "mg_ClipDistanceIn");
|
||||||
|
const uint32_t mgInVarId = mgInVar->result_id();
|
||||||
|
|
||||||
|
// Copy loop at the top of the entry point:
|
||||||
|
// for (uint t = 0; t < vertexCount * clipCount; ++t)
|
||||||
|
// mg_ClipDistanceIn[t / clipCount][t % clipCount] =
|
||||||
|
// gl_in[t / clipCount].gl_ClipDistance[t % clipCount];
|
||||||
|
// Both gl_in indices are loop-derived (dynamic): constant-index element reads
|
||||||
|
// miscompile and whole-array reads crash the Adreno compiler.
|
||||||
|
const uint32_t entryFuncId = EntryFunctionId(ctx);
|
||||||
|
Function* entryFn = nullptr;
|
||||||
|
for (Function& function : *ctx->module()) {
|
||||||
|
if (function.result_id() == entryFuncId) {
|
||||||
|
entryFn = &function;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (entryFn == nullptr || entryFn->begin() == entryFn->end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t uintTypeId = UintType(ctx);
|
||||||
|
const uint32_t boolTypeId = BoolType(ctx);
|
||||||
|
const uint32_t ptrFnUint = PointerTypeTo(ctx, uintTypeId, spv::StorageClass::Function);
|
||||||
|
const uint32_t ptrInElem = PointerTypeTo(ctx, elemTypeId, spv::StorageClass::Input);
|
||||||
|
const uint32_t ptrPrivElem = PointerTypeTo(ctx, elemTypeId, spv::StorageClass::Private);
|
||||||
|
const uint32_t uint0 = IntConstant(ctx, false, 0);
|
||||||
|
const uint32_t uint1 = IntConstant(ctx, false, 1);
|
||||||
|
const uint32_t uintN = IntConstant(ctx, false, clipCount);
|
||||||
|
const uint32_t uintTotal = IntConstant(ctx, false, vertexCount * clipCount);
|
||||||
|
const uint32_t memberConst = IntConstant(ctx, true, memberIndex);
|
||||||
|
|
||||||
|
BasicBlock* entryBlock = &*entryFn->begin();
|
||||||
|
auto splitPoint = entryBlock->begin();
|
||||||
|
while (splitPoint != entryBlock->end() &&
|
||||||
|
splitPoint->opcode() == spv::Op::OpVariable) {
|
||||||
|
++splitPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop counter lives with the other function-local variables.
|
||||||
|
const uint32_t counterVarId = ctx->TakeNextId();
|
||||||
|
splitPoint->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpVariable, ptrFnUint, counterVarId,
|
||||||
|
std::initializer_list<Operand>{
|
||||||
|
{SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||||
|
{static_cast<uint32_t>(spv::StorageClass::Function)}}}));
|
||||||
|
|
||||||
|
const uint32_t restLabelId = ctx->TakeNextId();
|
||||||
|
BasicBlock* restBlock = entryBlock->SplitBasicBlock(ctx, restLabelId, splitPoint);
|
||||||
|
|
||||||
|
const uint32_t headerLabelId = ctx->TakeNextId();
|
||||||
|
const uint32_t checkLabelId = ctx->TakeNextId();
|
||||||
|
const uint32_t bodyLabelId = ctx->TakeNextId();
|
||||||
|
const uint32_t continueLabelId = ctx->TakeNextId();
|
||||||
|
|
||||||
|
auto makeBlock = [&](uint32_t labelId) {
|
||||||
|
return spvtools::MakeUnique<BasicBlock>(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, spv::Op::OpLabel, 0, labelId, std::initializer_list<Operand>{}));
|
||||||
|
};
|
||||||
|
auto addInst = [&](BasicBlock* block, spv::Op opcode, uint32_t typeId,
|
||||||
|
uint32_t resultId, std::vector<Operand> operands) {
|
||||||
|
block->AddInstruction(spvtools::MakeUnique<Instruction>(
|
||||||
|
ctx, opcode, typeId, resultId, std::move(operands)));
|
||||||
|
};
|
||||||
|
|
||||||
|
// entry: t = 0; branch header
|
||||||
|
addInst(entryBlock, spv::Op::OpStore, 0, 0,
|
||||||
|
{{SPV_OPERAND_TYPE_ID, {counterVarId}}, {SPV_OPERAND_TYPE_ID, {uint0}}});
|
||||||
|
addInst(entryBlock, spv::Op::OpBranch, 0, 0, {{SPV_OPERAND_TYPE_ID, {headerLabelId}}});
|
||||||
|
|
||||||
|
// header: structured loop header
|
||||||
|
auto headerBlock = makeBlock(headerLabelId);
|
||||||
|
addInst(headerBlock.get(), spv::Op::OpLoopMerge, 0, 0,
|
||||||
|
{{SPV_OPERAND_TYPE_ID, {restLabelId}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {continueLabelId}},
|
||||||
|
{SPV_OPERAND_TYPE_LOOP_CONTROL,
|
||||||
|
{static_cast<uint32_t>(spv::LoopControlMask::MaskNone)}}});
|
||||||
|
addInst(headerBlock.get(), spv::Op::OpBranch, 0, 0,
|
||||||
|
{{SPV_OPERAND_TYPE_ID, {checkLabelId}}});
|
||||||
|
|
||||||
|
// check: t < vertexCount * clipCount ?
|
||||||
|
auto checkBlock = makeBlock(checkLabelId);
|
||||||
|
const uint32_t tCheckId = ctx->TakeNextId();
|
||||||
|
addInst(checkBlock.get(), spv::Op::OpLoad, uintTypeId, tCheckId,
|
||||||
|
{{SPV_OPERAND_TYPE_ID, {counterVarId}}});
|
||||||
|
const uint32_t condId = ctx->TakeNextId();
|
||||||
|
addInst(checkBlock.get(), spv::Op::OpULessThan, boolTypeId, condId,
|
||||||
|
{{SPV_OPERAND_TYPE_ID, {tCheckId}}, {SPV_OPERAND_TYPE_ID, {uintTotal}}});
|
||||||
|
addInst(checkBlock.get(), spv::Op::OpBranchConditional, 0, 0,
|
||||||
|
{{SPV_OPERAND_TYPE_ID, {condId}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {bodyLabelId}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {restLabelId}}});
|
||||||
|
|
||||||
|
// body: mg_ClipDistanceIn[t / N][t % N] = gl_in[t / N].gl_ClipDistance[t % N]
|
||||||
|
auto bodyBlock = makeBlock(bodyLabelId);
|
||||||
|
const uint32_t tBodyId = ctx->TakeNextId();
|
||||||
|
addInst(bodyBlock.get(), spv::Op::OpLoad, uintTypeId, tBodyId,
|
||||||
|
{{SPV_OPERAND_TYPE_ID, {counterVarId}}});
|
||||||
|
const uint32_t vertexIdxId = ctx->TakeNextId();
|
||||||
|
addInst(bodyBlock.get(), spv::Op::OpUDiv, uintTypeId, vertexIdxId,
|
||||||
|
{{SPV_OPERAND_TYPE_ID, {tBodyId}}, {SPV_OPERAND_TYPE_ID, {uintN}}});
|
||||||
|
const uint32_t clipIdxId = ctx->TakeNextId();
|
||||||
|
addInst(bodyBlock.get(), spv::Op::OpUMod, uintTypeId, clipIdxId,
|
||||||
|
{{SPV_OPERAND_TYPE_ID, {tBodyId}}, {SPV_OPERAND_TYPE_ID, {uintN}}});
|
||||||
|
const uint32_t srcChainId = ctx->TakeNextId();
|
||||||
|
addInst(bodyBlock.get(), spv::Op::OpAccessChain, ptrInElem, srcChainId,
|
||||||
|
{{SPV_OPERAND_TYPE_ID, {glInVar->result_id()}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {vertexIdxId}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {memberConst}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {clipIdxId}}});
|
||||||
|
const uint32_t valId = ctx->TakeNextId();
|
||||||
|
addInst(bodyBlock.get(), spv::Op::OpLoad, elemTypeId, valId,
|
||||||
|
{{SPV_OPERAND_TYPE_ID, {srcChainId}}});
|
||||||
|
const uint32_t dstChainId = ctx->TakeNextId();
|
||||||
|
addInst(bodyBlock.get(), spv::Op::OpAccessChain, ptrPrivElem, dstChainId,
|
||||||
|
{{SPV_OPERAND_TYPE_ID, {mgInVarId}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {vertexIdxId}},
|
||||||
|
{SPV_OPERAND_TYPE_ID, {clipIdxId}}});
|
||||||
|
addInst(bodyBlock.get(), spv::Op::OpStore, 0, 0,
|
||||||
|
{{SPV_OPERAND_TYPE_ID, {dstChainId}}, {SPV_OPERAND_TYPE_ID, {valId}}});
|
||||||
|
addInst(bodyBlock.get(), spv::Op::OpBranch, 0, 0,
|
||||||
|
{{SPV_OPERAND_TYPE_ID, {continueLabelId}}});
|
||||||
|
|
||||||
|
// continue: ++t
|
||||||
|
auto continueBlock = makeBlock(continueLabelId);
|
||||||
|
const uint32_t tContinueId = ctx->TakeNextId();
|
||||||
|
addInst(continueBlock.get(), spv::Op::OpLoad, uintTypeId, tContinueId,
|
||||||
|
{{SPV_OPERAND_TYPE_ID, {counterVarId}}});
|
||||||
|
const uint32_t tIncId = ctx->TakeNextId();
|
||||||
|
addInst(continueBlock.get(), spv::Op::OpIAdd, uintTypeId, tIncId,
|
||||||
|
{{SPV_OPERAND_TYPE_ID, {tContinueId}}, {SPV_OPERAND_TYPE_ID, {uint1}}});
|
||||||
|
addInst(continueBlock.get(), spv::Op::OpStore, 0, 0,
|
||||||
|
{{SPV_OPERAND_TYPE_ID, {counterVarId}}, {SPV_OPERAND_TYPE_ID, {tIncId}}});
|
||||||
|
addInst(continueBlock.get(), spv::Op::OpBranch, 0, 0,
|
||||||
|
{{SPV_OPERAND_TYPE_ID, {headerLabelId}}});
|
||||||
|
|
||||||
|
BasicBlock* headerPtr = entryFn->InsertBasicBlockBefore(std::move(headerBlock), restBlock);
|
||||||
|
BasicBlock* checkPtr = entryFn->InsertBasicBlockAfter(std::move(checkBlock), headerPtr);
|
||||||
|
BasicBlock* bodyPtr = entryFn->InsertBasicBlockAfter(std::move(bodyBlock), checkPtr);
|
||||||
|
entryFn->InsertBasicBlockAfter(std::move(continueBlock), bodyPtr);
|
||||||
|
|
||||||
|
// Redirect the pre-existing accesses to the shadow copy.
|
||||||
|
for (Instruction* chain : chains) {
|
||||||
|
if (chain->NumInOperands() == 3) {
|
||||||
|
// (vertex, member): whole float[N] of one vertex.
|
||||||
|
Instruction* chainPtrType = defUse->GetDef(chain->type_id());
|
||||||
|
const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1);
|
||||||
|
const uint32_t newPtrType =
|
||||||
|
PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private);
|
||||||
|
ctx->ForgetUses(chain);
|
||||||
|
std::vector<Operand> newOperands;
|
||||||
|
newOperands.push_back({SPV_OPERAND_TYPE_ID, {mgInVarId}});
|
||||||
|
newOperands.push_back(chain->GetInOperand(1));
|
||||||
|
chain->SetResultType(newPtrType);
|
||||||
|
chain->SetInOperands(std::move(newOperands));
|
||||||
|
ctx->AnalyzeUses(chain);
|
||||||
|
} else {
|
||||||
|
// (vertex, member, k, ...): drop the member index.
|
||||||
|
Instruction* chainPtrType = defUse->GetDef(chain->type_id());
|
||||||
|
const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1);
|
||||||
|
const uint32_t newPtrType =
|
||||||
|
PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private);
|
||||||
|
ctx->ForgetUses(chain);
|
||||||
|
std::vector<Operand> newOperands;
|
||||||
|
newOperands.push_back({SPV_OPERAND_TYPE_ID, {mgInVarId}});
|
||||||
|
newOperands.push_back(chain->GetInOperand(1));
|
||||||
|
for (uint32_t i = 3; i < chain->NumInOperands(); ++i) {
|
||||||
|
newOperands.push_back(chain->GetInOperand(i));
|
||||||
|
}
|
||||||
|
chain->SetResultType(newPtrType);
|
||||||
|
chain->SetInOperands(std::move(newOperands));
|
||||||
|
ctx->AnalyzeUses(chain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
spvtools::opt::Pass::Status LowerClipDistanceForEsslPass::Process() {
|
||||||
|
auto* ctx = context();
|
||||||
|
const spv::ExecutionModel model = EntryExecutionModel(ctx);
|
||||||
|
const bool isVertex = model == spv::ExecutionModel::Vertex;
|
||||||
|
const bool isGeometry = model == spv::ExecutionModel::Geometry;
|
||||||
|
if (!isVertex && !isGeometry) {
|
||||||
|
return Status::SuccessWithoutChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = LowerOutputClipDistance(ctx, isGeometry);
|
||||||
|
if (isGeometry) {
|
||||||
|
changed |= LowerInputClipDistance(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) {
|
||||||
|
return Status::SuccessWithoutChange;
|
||||||
|
}
|
||||||
|
ctx->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
|
||||||
|
return Status::SuccessWithChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
spvtools::Optimizer::PassToken
|
||||||
|
LowerClipDistanceForEsslPass::CreateLowerClipDistanceForEsslPass() {
|
||||||
|
return spvtools::Optimizer::PassToken(MakeUnique<LowerClipDistanceForEsslPass>());
|
||||||
|
}
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "source/opt/pass.h"
|
||||||
|
#include "spirv-tools/optimizer.hpp"
|
||||||
|
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
// Adreno's ESSL compiler mishandles gl_ClipDistance (device-verified on Adreno 750):
|
||||||
|
// - writes through non-constant indices silently fail to link,
|
||||||
|
// - reads of gl_in[i].gl_ClipDistance[k] with a CONSTANT k >= 1 fail to compile
|
||||||
|
// ("array indexing out of boundary") while dynamic-index reads work,
|
||||||
|
// - compiling a whole-array read of gl_in[i].gl_ClipDistance segfaults the
|
||||||
|
// compiler backend (libllvm-qgl.so).
|
||||||
|
// This pass shadows the builtin so the decompiled ESSL only ever touches it in the
|
||||||
|
// shapes Adreno accepts. Output side (vertex + geometry): all accesses to the
|
||||||
|
// Output ClipDistance (gl_PerVertex member or standalone variable) are redirected
|
||||||
|
// to a Private mg_ClipDistance array, and a flush writing the real builtin with
|
||||||
|
// literal constant indices is inserted before every OpEmitVertex (geometry) or
|
||||||
|
// every return of the entry point (vertex). Input side (geometry): accesses to
|
||||||
|
// gl_in[...].gl_ClipDistance are redirected to a Private mg_ClipDistanceIn
|
||||||
|
// array-of-arrays filled once at the top of the entry point by a structured loop
|
||||||
|
// whose gl_in reads use dynamic (loop-variable) indices. The builtin members stay
|
||||||
|
// statically referenced by the flush/copy so cross-stage IO matching is intact.
|
||||||
|
// Only meant for the DirectGLES transpile path on Qualcomm devices.
|
||||||
|
class LowerClipDistanceForEsslPass : public spvtools::opt::Pass {
|
||||||
|
public:
|
||||||
|
const char* name() const override { return "lower-clip-distance-for-essl"; }
|
||||||
|
Status Process() override;
|
||||||
|
|
||||||
|
static spvtools::Optimizer::PassToken CreateLowerClipDistanceForEsslPass();
|
||||||
|
};
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -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.1–3.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.
|
||||||
@@ -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);
|
||||||
@@ -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.1–3.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."
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user