mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 22:28:32 +09:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e676b338b | ||
|
|
db01bfa3e8 | ||
|
|
cc3dcfd80e | ||
|
|
d9556ff041 | ||
|
|
39f21e52ea | ||
|
|
0e933b8f2f |
+1
-42
@@ -303,13 +303,6 @@ 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
|
||||||
@@ -338,18 +331,10 @@ set(MOBILEGL_INCLUDE_DIR
|
|||||||
${SPIRV-Headers_SOURCE_DIR}/include
|
${SPIRV-Headers_SOURCE_DIR}/include
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
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
|
||||||
@@ -392,18 +377,6 @@ 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}
|
||||||
@@ -455,21 +428,8 @@ if (ANDROID)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (APPLE AND NOT MOBILEGL_IOS)
|
if (APPLE AND NOT MOBILEGL_IOS)
|
||||||
# MobileGL statically embeds glslang, SPIRV-Tools, and SPIRV-Cross. When
|
|
||||||
# this dylib is injected with DYLD_INSERT_LIBRARIES, exporting those C++
|
|
||||||
# symbols interposes incompatible copies embedded by host libraries such
|
|
||||||
# as shaderc. Keep only the public GL/EGL/CGL loader surface globally
|
|
||||||
# visible; GetProcAddress can still return pointers to hidden internals.
|
|
||||||
set(MOBILEGL_MACOS_EXPORTED_SYMBOLS
|
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/MobileGL/MG_Impl/DyldInterpose/ExportedSymbols.txt")
|
|
||||||
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
|
|
||||||
"LINKER:-exported_symbols_list,${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
|
|
||||||
set_property(TARGET ${CMAKE_PROJECT_NAME} APPEND PROPERTY
|
|
||||||
LINK_DEPENDS "${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
|
|
||||||
|
|
||||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
|
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
|
||||||
"-framework Cocoa"
|
"-framework Cocoa"
|
||||||
"-framework CoreVideo"
|
|
||||||
"-framework QuartzCore"
|
"-framework QuartzCore"
|
||||||
"-framework Foundation"
|
"-framework Foundation"
|
||||||
"-framework OpenGL"
|
"-framework OpenGL"
|
||||||
@@ -477,7 +437,6 @@ if (APPLE AND NOT MOBILEGL_IOS)
|
|||||||
if(TARGET ${CMAKE_PROJECT_NAME}_s)
|
if(TARGET ${CMAKE_PROJECT_NAME}_s)
|
||||||
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC
|
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC
|
||||||
"-framework Cocoa"
|
"-framework Cocoa"
|
||||||
"-framework CoreVideo"
|
|
||||||
"-framework QuartzCore"
|
"-framework QuartzCore"
|
||||||
"-framework Foundation"
|
"-framework Foundation"
|
||||||
"-framework OpenGL"
|
"-framework OpenGL"
|
||||||
|
|||||||
@@ -34,7 +34,6 @@
|
|||||||
#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,13 +14,7 @@ namespace MobileGL {
|
|||||||
} // namespace MG_Config
|
} // namespace MG_Config
|
||||||
|
|
||||||
namespace MG_Backend {
|
namespace MG_Backend {
|
||||||
// Leak-at-exit storage: the UniquePtr itself lives on the heap and is
|
UniquePtr<BackendObject> pActiveBackendObject;
|
||||||
// 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
|
||||||
|
|||||||
+33
-47
@@ -9,25 +9,14 @@
|
|||||||
#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 <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <mutex>
|
|
||||||
|
|
||||||
namespace MobileGL {
|
namespace MobileGL {
|
||||||
namespace {
|
namespace {
|
||||||
std::atomic<Bool> g_isInitialized = false;
|
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) {
|
||||||
@@ -38,12 +27,6 @@ namespace MobileGL {
|
|||||||
MGLOG_I("MobileGL closing...");
|
MGLOG_I("MobileGL closing...");
|
||||||
}
|
}
|
||||||
glslang::FinalizeProcess();
|
glslang::FinalizeProcess();
|
||||||
// GL syncs die with their contexts, and every context is gone by the
|
|
||||||
// time full teardown runs: drain the live-sync registry while the
|
|
||||||
// backend function table can still release the backend handles (and
|
|
||||||
// before a re-initialized library could pair them with the wrong
|
|
||||||
// backend's DeleteSync).
|
|
||||||
MG_Impl::GLImpl::DestroyAllSyncObjects();
|
|
||||||
MG_Backend::pActiveBackendObject.reset();
|
MG_Backend::pActiveBackendObject.reset();
|
||||||
MG_State::pGLContext.reset();
|
MG_State::pGLContext.reset();
|
||||||
MG_State::pEGLContext.reset();
|
MG_State::pEGLContext.reset();
|
||||||
@@ -81,37 +64,40 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// MobileGL's lifecycle is owned entirely by the host-API layers
|
#if defined(__linux__) || defined(__APPLE__)
|
||||||
// (EGL/WGL/CGL): initialization happens lazily on the first entry point
|
__attribute__((constructor)) static void AutoInit() {
|
||||||
// via EnsureInitialized(), and full teardown happens deterministically
|
Initialize();
|
||||||
// when the last EGL display is terminated with nothing current (EGLImpl
|
}
|
||||||
// calls Destroy()). There is intentionally no backend-initializing static
|
|
||||||
// constructor, no static destructor, and no DllMain: the global singletons
|
__attribute__((destructor)) static void AutoDestroy() {
|
||||||
// use leak-at-exit storage (see GlobalObjects.cpp), so a process that exits
|
if (MG_Config::Features.TraceSkipAutodestroy) {
|
||||||
// without eglTerminate simply leaks them to the OS instead of running
|
return;
|
||||||
// backend destructors during static teardown. macOS has a lightweight
|
}
|
||||||
// dyld constructor that installs NSOpenGL dispatch hooks only; full backend
|
#if defined(__APPLE__)
|
||||||
// initialization still enters here from the first hooked CGL context.
|
// 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,13 +11,6 @@
|
|||||||
|
|
||||||
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
|
|
||||||
// full backend initialization never depends on ELF/DLL static constructors,
|
|
||||||
// and so a fresh init can follow a full Destroy() (e.g. after the last
|
|
||||||
// eglTerminate). The macOS dyld bootstrap installs only lightweight
|
|
||||||
// NSOpenGL method hooks.
|
|
||||||
void EnsureInitialized();
|
|
||||||
void Destroy();
|
void Destroy();
|
||||||
|
|
||||||
namespace MG_Util::Debug {
|
namespace MG_Util::Debug {
|
||||||
|
|||||||
@@ -313,8 +313,7 @@ namespace MobileGL {
|
|||||||
Android,
|
Android,
|
||||||
X11,
|
X11,
|
||||||
MetalLayer,
|
MetalLayer,
|
||||||
Win32, // Handle is an HWND
|
// TODO: Wayland, Windows, etc.
|
||||||
// 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,10 +701,9 @@ 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, CAMetalLayer, and Win32 native windows");
|
MGLOG_E("DirectGLES backend only supports Android, X11, and CAMetalLayer native windows");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4223,23 +4223,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
if (!ChooseConfigForSurface(surfaceBit, g_Config, window)) return false;
|
if (!ChooseConfigForSurface(surfaceBit, g_Config, window)) return false;
|
||||||
|
|
||||||
// Negotiate the highest ES 3.x context. Version-strict EGL implementations
|
const EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
|
||||||
// (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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const EGLint legacyContextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
|
g_Context = g_EGLFuncs.eglCreateContext(g_Display, g_Config, EGL_NO_CONTEXT, contextAttribs);
|
||||||
g_Context = g_EGLFuncs.eglCreateContext(g_Display, g_Config, EGL_NO_CONTEXT, legacyContextAttribs);
|
|
||||||
return g_Context != EGL_NO_CONTEXT;
|
return g_Context != EGL_NO_CONTEXT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -411,9 +411,8 @@ 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)) {
|
||||||
handle.Backend != WindowBackend::Win32)) {
|
MGLOG_E("DirectVulkan backend only supports Android, X11, and CAMetalLayer native windows");
|
||||||
MGLOG_E("DirectVulkan backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,9 +468,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// treat them as signaled/available with zero results from here on.
|
// treat them as signaled/available with zero results from here on.
|
||||||
BumpRendererGeneration();
|
BumpRendererGeneration();
|
||||||
pVulkanRenderer.reset();
|
pVulkanRenderer.reset();
|
||||||
// The reflection cache is file-scope, not renderer-owned; without this the
|
|
||||||
// deleted programs' reflection strings survive full context teardown.
|
|
||||||
ClearProgramResourceCaches();
|
|
||||||
BackendObject::ReleaseEGLResources();
|
BackendObject::ReleaseEGLResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,9 +477,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// treat them as signaled/available with zero results from here on.
|
// treat them as signaled/available with zero results from here on.
|
||||||
BumpRendererGeneration();
|
BumpRendererGeneration();
|
||||||
pVulkanRenderer.reset();
|
pVulkanRenderer.reset();
|
||||||
// The reflection cache is file-scope, not renderer-owned; without this the
|
|
||||||
// deleted programs' reflection strings survive full context teardown.
|
|
||||||
ClearProgramResourceCaches();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const RendererInfo& BackendObject_DirectVulkan::GetRendererInfo() const {
|
const RendererInfo& BackendObject_DirectVulkan::GetRendererInfo() const {
|
||||||
|
|||||||
@@ -20,8 +20,7 @@
|
|||||||
#include <spirv_reflect.h>
|
#include <spirv_reflect.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
// Leak-at-exit storage; see GlobalObjects.cpp.
|
UniquePtr<VulkanRenderer> pVulkanRenderer = nullptr;
|
||||||
UniquePtr<VulkanRenderer>& pVulkanRenderer = *new UniquePtr<VulkanRenderer>();
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Generation of the live VulkanRenderer instance, mirroring
|
// Generation of the live VulkanRenderer instance, mirroring
|
||||||
@@ -61,12 +60,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct ProgramResourceCache {
|
struct ProgramResourceCache {
|
||||||
// Lifetime id of the program the cached reflection belongs to. GL names are
|
|
||||||
// recycled (IndexGenerator hands freed indices straight back), and a
|
|
||||||
// recreated program's backendStateVersion restarts at the same small values,
|
|
||||||
// so the version alone can collide; the never-reused lifetime id makes the
|
|
||||||
// slot's ownership unambiguous.
|
|
||||||
Uint64 programLifetimeId = 0;
|
|
||||||
Uint32 backendStateVersion = 0;
|
Uint32 backendStateVersion = 0;
|
||||||
Vector<StorageBlockResource> storageBlocks;
|
Vector<StorageBlockResource> storageBlocks;
|
||||||
Vector<BufferVariableResource> bufferVariables;
|
Vector<BufferVariableResource> bufferVariables;
|
||||||
@@ -88,11 +81,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 baseInstance = 0;
|
Uint32 baseInstance = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keyed by GL program name so the freed-name reuse in IndexGenerator bounds the
|
|
||||||
// map at the peak-simultaneous-program high-water mark; each slot's ownership is
|
|
||||||
// checked against the program's lifetime id before it is served (see
|
|
||||||
// GetProgramResourceCache). Cleared wholesale at EGL teardown via
|
|
||||||
// ClearProgramResourceCaches.
|
|
||||||
UnorderedMap<GLuint, ProgramResourceCache> g_programResourceCaches;
|
UnorderedMap<GLuint, ProgramResourceCache> g_programResourceCaches;
|
||||||
|
|
||||||
void ClearReadPixelsOutput(GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
void ClearReadPixelsOutput(GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||||
@@ -153,19 +141,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
ProgramResourceCache& GetProgramResourceCache(const MG_State::GLState::ProgramObject& program) {
|
ProgramResourceCache& GetProgramResourceCache(const MG_State::GLState::ProgramObject& program) {
|
||||||
auto& cache = g_programResourceCaches[program.GetExternalIndex()];
|
auto& cache = g_programResourceCaches[program.GetExternalIndex()];
|
||||||
const Uint64 programLifetimeId = program.GetLifetimeId();
|
|
||||||
const Uint32 backendStateVersion = program.GetBackendStateVersion();
|
const Uint32 backendStateVersion = program.GetBackendStateVersion();
|
||||||
// The lifetime id must match too: a new program that reuses a deleted
|
if (cache.backendStateVersion == backendStateVersion &&
|
||||||
// program's name and happens to land on the same backendStateVersion (both
|
|
||||||
// count from zero) would otherwise be served the dead program's reflection.
|
|
||||||
if (cache.programLifetimeId == programLifetimeId &&
|
|
||||||
cache.backendStateVersion == backendStateVersion &&
|
|
||||||
(!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) {
|
(!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) {
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
cache = {};
|
cache = {};
|
||||||
cache.programLifetimeId = programLifetimeId;
|
|
||||||
cache.backendStateVersion = backendStateVersion;
|
cache.backendStateVersion = backendStateVersion;
|
||||||
|
|
||||||
Vector<SpvReflectShaderModule> modules;
|
Vector<SpvReflectShaderModule> modules;
|
||||||
@@ -383,15 +365,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void ClearProgramResourceCaches() {
|
|
||||||
// Called from EGL teardown while the backend's m_eglStateMutex is held; GL
|
|
||||||
// calls are serialized in this codebase (contexts migrate threads but never
|
|
||||||
// run concurrently), so no other thread can be inside the unsynchronized map.
|
|
||||||
// Live programs in another context self-heal: their entry rebuilds from the
|
|
||||||
// retained generated SPIR-V on the next resource query.
|
|
||||||
g_programResourceCaches.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint GetShaderStorageBlockIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
|
GLuint GetShaderStorageBlockIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
|
||||||
auto& cache = GetProgramResourceCache(program);
|
auto& cache = GetProgramResourceCache(program);
|
||||||
const auto it = std::find_if(cache.storageBlocks.begin(), cache.storageBlocks.end(),
|
const auto it = std::find_if(cache.storageBlocks.begin(), cache.storageBlocks.end(),
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -23,12 +23,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 GetRendererGeneration();
|
Uint64 GetRendererGeneration();
|
||||||
void BumpRendererGeneration();
|
void BumpRendererGeneration();
|
||||||
|
|
||||||
// Drops every cached program-resource reflection entry (CPU-side strings/vectors
|
|
||||||
// only, no Vulkan handles). Called at EGL teardown next to the renderer reset;
|
|
||||||
// safe because GL calls are serialized in this codebase, and any still-live
|
|
||||||
// program rebuilds its entry from the retained generated SPIR-V on demand.
|
|
||||||
void ClearProgramResourceCaches();
|
|
||||||
|
|
||||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||||
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||||
|
|||||||
@@ -150,30 +150,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
Bool FrameContext::TransitionToPresent(VkImage image, VkImageLayout oldLayout, VkImageLayout presentLayout) {
|
Bool FrameContext::TransitionToPresent(VkImage image, VkImageLayout oldLayout, VkImageLayout presentLayout) {
|
||||||
auto& frame = GetCurrent();
|
auto& frame = GetCurrent();
|
||||||
if (oldLayout == presentLayout || oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
|
if (frame.hasCommandBufferRecorded || frame.isCommandRecording || oldLayout == presentLayout ||
|
||||||
|
oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The barrier belongs in the frame's own recording. Bailing out because
|
auto& commandBuffer = BeginCommandRecording();
|
||||||
// something was already recorded (the previous behaviour) dropped the
|
|
||||||
// transition entirely for every frame that never ran a default-framebuffer
|
|
||||||
// render pass - the only other thing that carries the image to
|
|
||||||
// PRESENT_SRC_KHR, via that pass's finalLayout - so the swapchain image was
|
|
||||||
// handed to the WSI still in the layout it was acquired in.
|
|
||||||
// A closed-but-unsubmitted buffer can only come from a submit that already
|
|
||||||
// failed (SubmitPendingCommandBuffer leaves the flag set on error), and
|
|
||||||
// appending to it is illegal while reopening would reset the frame's own
|
|
||||||
// commands away. The device is gone on that path anyway - stay silent-safe
|
|
||||||
// rather than trade a lost device for a barrier into a closed buffer.
|
|
||||||
if (frame.hasCommandBufferRecorded) {
|
|
||||||
MGLOG_E("TransitionToPresent: command buffer already closed; skipping the present barrier");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reopening a recording here would vkResetCommandBuffer this frame's own
|
|
||||||
// commands away, so append to the open one and let the caller close it.
|
|
||||||
const Bool openedRecording = !frame.isCommandRecording;
|
|
||||||
VkCommandBuffer commandBuffer = openedRecording ? BeginCommandRecording() : frame.commandBuffer;
|
|
||||||
|
|
||||||
VkImageMemoryBarrier presentBarrier{};
|
VkImageMemoryBarrier presentBarrier{};
|
||||||
presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
||||||
@@ -192,9 +174,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
|
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
|
||||||
nullptr, 0, nullptr, 1, &presentBarrier);
|
nullptr, 0, nullptr, 1, &presentBarrier);
|
||||||
|
|
||||||
if (openedRecording) {
|
EndCommandRecording();
|
||||||
EndCommandRecording();
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,21 +227,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
result = vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence,
|
result = vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence,
|
||||||
&outImageIndex);
|
&outImageIndex);
|
||||||
// VK_SUBOPTIMAL_KHR is a success code: an image *was* acquired and
|
if (result != VK_SUCCESS) {
|
||||||
// imageAvailableSemaphore *will* be signaled. Bailing out on it skipped both
|
|
||||||
// the consumed-flag reset (leaving a stale "already consumed", so the next
|
|
||||||
// submit never waited on the pending signal) and the fence reset (leaving
|
|
||||||
// the slot's fence signaled for the next submit to reuse). Only a genuine
|
|
||||||
// failure - VK_ERROR_OUT_OF_DATE_KHR and friends, where nothing is acquired
|
|
||||||
// and nothing is signaled - skips the bookkeeping.
|
|
||||||
if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
frame.imageAvailableSemaphoreConsumed = false;
|
frame.imageAvailableSemaphoreConsumed = false;
|
||||||
const VkResult resetResult = vkResetFences(device, 1, &frame.imageInFlightFence);
|
return vkResetFences(device, 1, &frame.imageInFlightFence);
|
||||||
// Hand the acquire's own code back so the caller can schedule a rebuild.
|
|
||||||
return resetResult == VK_SUCCESS ? result : resetResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint32 FrameContext::GetCurrentFrameIndex() const {
|
Uint32 FrameContext::GetCurrentFrameIndex() const {
|
||||||
@@ -293,9 +264,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (result != VK_SUCCESS) {
|
if (result != VK_SUCCESS) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
// lastSubmitIndex was just written by the renderer for the submission
|
frame.retiredCommandBuffers.push_back(frame.commandBuffer);
|
||||||
// that carried this command buffer.
|
|
||||||
frame.retiredCommandBuffers.push_back({frame.commandBuffer, frame.lastSubmitIndex});
|
|
||||||
frame.commandBuffer = replacement;
|
frame.commandBuffer = replacement;
|
||||||
return VK_SUCCESS;
|
return VK_SUCCESS;
|
||||||
}
|
}
|
||||||
@@ -305,40 +274,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE) {
|
if (m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE) {
|
||||||
for (const auto& retired : frame.retiredCommandBuffers) {
|
vkFreeCommandBuffers(m_device, m_commandPool, static_cast<Uint32>(frame.retiredCommandBuffers.size()),
|
||||||
vkFreeCommandBuffers(m_device, m_commandPool, 1, &retired.commandBuffer);
|
frame.retiredCommandBuffers.data());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
frame.retiredCommandBuffers.clear();
|
frame.retiredCommandBuffers.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void FrameContext::FreeRetiredCommandBuffersCompletedUpTo(Uint64 completedSubmitIndex) {
|
|
||||||
if (m_device == VK_NULL_HANDLE || m_commandPool == VK_NULL_HANDLE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (auto& frame : m_frames) {
|
|
||||||
// Retired buffers are appended in submit order, so the completed
|
|
||||||
// ones form a prefix.
|
|
||||||
SizeT completedCount = 0;
|
|
||||||
while (completedCount < frame.retiredCommandBuffers.size() &&
|
|
||||||
frame.retiredCommandBuffers[completedCount].submitIndex <= completedSubmitIndex) {
|
|
||||||
vkFreeCommandBuffers(m_device, m_commandPool, 1,
|
|
||||||
&frame.retiredCommandBuffers[completedCount].commandBuffer);
|
|
||||||
++completedCount;
|
|
||||||
}
|
|
||||||
if (completedCount > 0) {
|
|
||||||
frame.retiredCommandBuffers.erase(frame.retiredCommandBuffers.begin(),
|
|
||||||
frame.retiredCommandBuffers.begin() + completedCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FrameContext::FreeAllRetiredCommandBuffers() {
|
|
||||||
for (auto& frame : m_frames) {
|
|
||||||
FreeRetiredCommandBuffers(frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FrameContext::AssertValidFrameIndex(Uint32 frameIndex) const {
|
void FrameContext::AssertValidFrameIndex(Uint32 frameIndex) const {
|
||||||
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "FrameContext index out of range");
|
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "FrameContext index out of range");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,16 +40,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkPresentInfoKHR presentInfo{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
|
VkPresentInfoKHR presentInfo{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
|
||||||
};
|
};
|
||||||
|
|
||||||
// A command buffer submitted mid-frame (FlushPendingCommands), tagged
|
|
||||||
// with the submit-tracker index it was submitted under so it can be
|
|
||||||
// freed as soon as that submission is observed complete - without
|
|
||||||
// waiting for the slot's fence to be waited again (present-less flush
|
|
||||||
// loops never wait it).
|
|
||||||
struct RetiredCommandBuffer {
|
|
||||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
|
||||||
Uint64 submitIndex = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct FrameData {
|
struct FrameData {
|
||||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||||
VkSemaphore imageAvailableSemaphore = VK_NULL_HANDLE;
|
VkSemaphore imageAvailableSemaphore = VK_NULL_HANDLE;
|
||||||
@@ -57,10 +47,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool isCommandRecording = false;
|
Bool isCommandRecording = false;
|
||||||
Bool hasCommandBufferRecorded = false;
|
Bool hasCommandBufferRecorded = false;
|
||||||
Bool imageAvailableSemaphoreConsumed = false;
|
Bool imageAvailableSemaphoreConsumed = false;
|
||||||
// Command buffers submitted mid-frame (FlushPendingCommands),
|
// Command buffers submitted mid-frame (FlushPendingCommands) whose
|
||||||
// appended in submit order; freed once their submission is known
|
// execution is only known complete once this slot's fence has been
|
||||||
// complete (fence wait or completion poll).
|
// waited again; freed at that point.
|
||||||
Vector<RetiredCommandBuffer> retiredCommandBuffers;
|
Vector<VkCommandBuffer> retiredCommandBuffers;
|
||||||
// Submit-tracker index of this slot's most recent queue submission
|
// Submit-tracker index of this slot's most recent queue submission
|
||||||
// (written by the renderer at submit time).
|
// (written by the renderer at submit time).
|
||||||
Uint64 lastSubmitIndex = 0;
|
Uint64 lastSubmitIndex = 0;
|
||||||
@@ -89,19 +79,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Parks the current (already ended and submitted) command buffer on the
|
// Parks the current (already ended and submitted) command buffer on the
|
||||||
// slot's retired list and installs a freshly allocated one, so recording
|
// slot's retired list and installs a freshly allocated one, so recording
|
||||||
// can restart while the submitted buffer is still executing. Retired
|
// can restart while the submitted buffer is still executing. Retired
|
||||||
// buffers are freed after the slot's fence is next waited, or as soon
|
// buffers are freed after the slot's fence is next waited.
|
||||||
// as their submission is observed complete.
|
|
||||||
VkResult RetireCurrentCommandBuffer();
|
VkResult RetireCurrentCommandBuffer();
|
||||||
|
|
||||||
// Frees every retired command buffer whose tagged submission index is
|
|
||||||
// known complete. Driven by the renderer's submit tracker on completion
|
|
||||||
// events (fence waits and non-blocking polls), so present-less flush
|
|
||||||
// loops reclaim their buffers without any extra wait.
|
|
||||||
void FreeRetiredCommandBuffersCompletedUpTo(Uint64 completedSubmitIndex);
|
|
||||||
// Frees every slot's retired command buffers. Only valid when the
|
|
||||||
// caller has proven every queue submission complete.
|
|
||||||
void FreeAllRetiredCommandBuffers();
|
|
||||||
|
|
||||||
Uint32 GetCurrentFrameIndex() const;
|
Uint32 GetCurrentFrameIndex() const;
|
||||||
Uint32 GetFrameCount() const;
|
Uint32 GetFrameCount() const;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
#include "PipelineFactory.h"
|
#include "PipelineFactory.h"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
|
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
|
||||||
@@ -244,108 +243,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const HashType hash = ComputeHash(payload);
|
const HashType hash = ComputeHash(payload);
|
||||||
auto it = m_cache.find(hash);
|
auto it = m_cache.find(hash);
|
||||||
if (it != m_cache.end()) {
|
if (it != m_cache.end()) {
|
||||||
it->second.lastUsedFrame = m_frameCounter;
|
return it->second;
|
||||||
return it->second.pipeline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VkPipeline pipeline = CreatePipeline(payload);
|
VkPipeline pipeline = CreatePipeline(payload);
|
||||||
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass,
|
m_cache.emplace(hash, pipeline);
|
||||||
m_frameCounter});
|
|
||||||
return pipeline;
|
return pipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PipelineFactory::DestroyAll() {
|
void PipelineFactory::DestroyAll() {
|
||||||
for (auto& pair : m_cache) {
|
for (auto& pair : m_cache) {
|
||||||
if (pair.second.pipeline != VK_NULL_HANDLE) {
|
if (pair.second != VK_NULL_HANDLE) {
|
||||||
vkDestroyPipeline(m_device, pair.second.pipeline, nullptr);
|
vkDestroyPipeline(m_device, pair.second, nullptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_cache.clear();
|
m_cache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint32 PipelineFactory::OnFrameBoundary() {
|
|
||||||
++m_frameCounter;
|
|
||||||
|
|
||||||
// Sweep cadence and retire age mirror VkRenderPassManager::OnPresent: an entry
|
|
||||||
// idle for more than kRetireAgeFrames frame boundaries cannot be referenced by
|
|
||||||
// any in-flight command buffer (frames-in-flight <= MOBILEGL_MAGMA_FRAMESINFLIGHT),
|
|
||||||
// so immediate vkDestroyPipeline is safe. The caller must drop its "last
|
|
||||||
// pipeline" memo when this returns non-zero: the memo can return a cached
|
|
||||||
// handle without touching this cache, so an evicted pipeline may still be
|
|
||||||
// memoized (present-less flush loops never reset the memo per frame).
|
|
||||||
constexpr Uint64 kSweepInterval = 256;
|
|
||||||
constexpr Uint64 kRetireAgeFrames = 1024;
|
|
||||||
if ((m_frameCounter % kSweepInterval) != 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint32 evicted = 0;
|
|
||||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
|
||||||
if (m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
|
|
||||||
if (it->second.pipeline != VK_NULL_HANDLE) {
|
|
||||||
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
|
|
||||||
}
|
|
||||||
it = m_cache.erase(it);
|
|
||||||
++evicted;
|
|
||||||
} else {
|
|
||||||
++it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (evicted > 0) {
|
|
||||||
MGLOG_D("PipelineFactory::OnFrameBoundary: evicted %u idle pipelines (%zu remain)", evicted,
|
|
||||||
m_cache.size());
|
|
||||||
}
|
|
||||||
return evicted;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint32 PipelineFactory::EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses) {
|
|
||||||
if (renderPasses.empty() || m_cache.empty()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
// Sorted-batch membership test keeps a mass eviction (shader-pack switch,
|
|
||||||
// dimension exit) at one O(cache * log batch) scan instead of one full scan
|
|
||||||
// per dying pass.
|
|
||||||
Vector<VkRenderPass> sortedPasses = renderPasses;
|
|
||||||
std::sort(sortedPasses.begin(), sortedPasses.end());
|
|
||||||
Uint32 evicted = 0;
|
|
||||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
|
||||||
if (std::binary_search(sortedPasses.begin(), sortedPasses.end(), it->second.renderPass)) {
|
|
||||||
if (it->second.pipeline != VK_NULL_HANDLE) {
|
|
||||||
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
|
|
||||||
}
|
|
||||||
it = m_cache.erase(it);
|
|
||||||
++evicted;
|
|
||||||
} else {
|
|
||||||
++it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (evicted > 0) {
|
|
||||||
MGLOG_D("PipelineFactory::EvictByRenderPasses: evicted %u pipelines for %zu destroyed render passes",
|
|
||||||
evicted, sortedPasses.size());
|
|
||||||
}
|
|
||||||
return evicted;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint32 PipelineFactory::EvictByProgramHash(HashType programHash) {
|
|
||||||
Uint32 evicted = 0;
|
|
||||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
|
||||||
if (it->second.programHash == programHash) {
|
|
||||||
if (it->second.pipeline != VK_NULL_HANDLE) {
|
|
||||||
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
|
|
||||||
}
|
|
||||||
it = m_cache.erase(it);
|
|
||||||
++evicted;
|
|
||||||
} else {
|
|
||||||
++it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (evicted > 0) {
|
|
||||||
MGLOG_D("PipelineFactory::EvictByProgramHash: evicted %u pipelines for program hash 0x%llx",
|
|
||||||
evicted, static_cast<unsigned long long>(programHash));
|
|
||||||
}
|
|
||||||
return evicted;
|
|
||||||
}
|
|
||||||
|
|
||||||
VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const {
|
VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const {
|
||||||
MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty");
|
MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty");
|
||||||
MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
|
MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
|
||||||
|
|||||||
@@ -65,26 +65,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
|
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
|
||||||
void DestroyAll();
|
void DestroyAll();
|
||||||
|
|
||||||
// Frame boundary hook: ages the pipeline cache and destroys long-unused entries
|
|
||||||
// (their command buffers retired many frames ago), mirroring
|
|
||||||
// VkRenderPassManager::OnPresent's sweep. Returns the number of pipelines
|
|
||||||
// destroyed so the caller can drop any memoized VkPipeline handle.
|
|
||||||
Uint32 OnFrameBoundary();
|
|
||||||
// Destroys every cached pipeline hashed on one of `renderPasses`. Only safe
|
|
||||||
// when the caller guarantees GPU idleness for them - the render-pass manager
|
|
||||||
// calls this (via the renderer) for passes its own >1024-boundary-idle sweep
|
|
||||||
// just evicted, and a pipeline hashed on those handles is only ever bound by
|
|
||||||
// draws that also hit the render-pass entries. Also closes the handle-recycling
|
|
||||||
// hazard: a recycled VkRenderPass value must never serve a stale pipeline.
|
|
||||||
// Batched: one cache scan regardless of how many passes died in the sweep.
|
|
||||||
// Returns the number destroyed (callers invalidate memos when non-zero).
|
|
||||||
Uint32 EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses);
|
|
||||||
// Destroys every cached pipeline built from the program with content hash
|
|
||||||
// `programHash`. Called from the ProgramFactory eviction path, which proves the
|
|
||||||
// same >1024-boundary idleness (the program's pipelines are only bound by draws
|
|
||||||
// that stamp its factory entry). Returns the number destroyed.
|
|
||||||
Uint32 EvictByProgramHash(HashType programHash);
|
|
||||||
|
|
||||||
// Driver quirk: suppress depth writes on accumulation-blended pipelines. Multi-pass
|
// Driver quirk: suppress depth writes on accumulation-blended pipelines. Multi-pass
|
||||||
// depth-equality rendering (a blended prepass writes depth that later passes re-test
|
// depth-equality rendering (a blended prepass writes depth that later passes re-test
|
||||||
// with an equality-inclusive compare on the re-rasterized geometry) requires
|
// with an equality-inclusive compare on the re-rasterized geometry) requires
|
||||||
@@ -107,26 +87,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
static Bool ShouldSuppressDepthWrite(const PipelineCreatePayload& payload);
|
static Bool ShouldSuppressDepthWrite(const PipelineCreatePayload& payload);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct PipelineCacheEntry {
|
|
||||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
|
||||||
// The hashed inputs the eviction paths key on: programHash ties the entry to
|
|
||||||
// its ProgramFactory entry, renderPass records the exact handle the hash
|
|
||||||
// folded in (the hash is one-way, so targeted eviction needs them verbatim).
|
|
||||||
HashType programHash = 0;
|
|
||||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
|
||||||
// Frame-boundary counter value of the last GetOrCreatePipeline hit; drives
|
|
||||||
// cache eviction (see OnFrameBoundary).
|
|
||||||
Uint64 lastUsedFrame = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
|
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
|
||||||
|
|
||||||
VkDevice m_device = VK_NULL_HANDLE;
|
VkDevice m_device = VK_NULL_HANDLE;
|
||||||
const VulkanRendererConfig& m_config;
|
const VulkanRendererConfig& m_config;
|
||||||
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
|
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
|
||||||
UnorderedMap<HashType, PipelineCacheEntry> m_cache;
|
UnorderedMap<HashType, VkPipeline> m_cache;
|
||||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
|
||||||
Uint64 m_frameCounter = 0;
|
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
static inline Bool s_suppressBlendedDepthWrite = false;
|
static inline Bool s_suppressBlendedDepthWrite = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,10 +12,7 @@
|
|||||||
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
||||||
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
|
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
|
||||||
#include "MG_Util/ShaderTranspiler/Types.h"
|
#include "MG_Util/ShaderTranspiler/Types.h"
|
||||||
#include <cmath>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <unordered_set>
|
|
||||||
#include <spirv-tools/libspirv.h>
|
#include <spirv-tools/libspirv.h>
|
||||||
#include <spirv-tools/optimizer.hpp>
|
#include <spirv-tools/optimizer.hpp>
|
||||||
#include <source/opt/build_module.h>
|
#include <source/opt/build_module.h>
|
||||||
@@ -926,610 +923,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ProgramFactory::CompileOptionFlags m_transformFlags;
|
ProgramFactory::CompileOptionFlags m_transformFlags;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Adreno 650 (driver 512.502) faults the GPU on an implicit-LOD sample of a full-screen
|
|
||||||
// colour render target: the texture unit's derivative path reads outside the image's
|
|
||||||
// allocation even though the sampler clamps LOD to 0 and the mapping is 1:1. MobileGL's
|
|
||||||
// own default-framebuffer blit shader works around it with textureLod, but an
|
|
||||||
// application's shader (Minecraft's blit.fsh is `texture(InSampler, texCoord)`) cannot be
|
|
||||||
// edited - so rewrite the sample at the SPIR-V level instead.
|
|
||||||
//
|
|
||||||
// The rewrite is only requested for draws whose every sampler binding is clamped to one
|
|
||||||
// mip level, where explicit LOD 0 is exactly what the implicit form must already produce:
|
|
||||||
// lambda' = clamp(lambda + bias, minLod, maxLod) with minLod = maxLod = 0. Bias and MinLod
|
|
||||||
// operands are therefore dropped rather than translated.
|
|
||||||
class ForceExplicitLod0SamplePass final : public spvtools::opt::Pass {
|
|
||||||
public:
|
|
||||||
const char* name() const override { return "force-explicit-lod0-sample"; }
|
|
||||||
|
|
||||||
Status Process() override {
|
|
||||||
Bool isFragment = false;
|
|
||||||
for (auto& entryPoint : get_module()->entry_points()) {
|
|
||||||
if (entryPoint.opcode() != spv::Op::OpEntryPoint) continue;
|
|
||||||
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) ==
|
|
||||||
spv::ExecutionModel::Fragment) {
|
|
||||||
isFragment = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isFragment) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
// Plan first, mutate second. Materializing the LOD constant is itself a module
|
|
||||||
// change, so it must not happen unless at least one rewrite is going to follow -
|
|
||||||
// otherwise the pass would grow the binary while reporting SuccessWithoutChange.
|
|
||||||
Vector<RewritePlan> plans;
|
|
||||||
for (auto& function : *get_module()) {
|
|
||||||
for (auto& block : function) {
|
|
||||||
for (auto& inst : block) {
|
|
||||||
RewritePlan plan{};
|
|
||||||
if (PlanRewrite(&inst, plan)) plans.push_back(Move(plan));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (plans.empty()) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
const Uint32 zeroId = GetFloatZeroId();
|
|
||||||
if (zeroId == 0) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
for (auto& plan : plans) {
|
|
||||||
plan.operands.push_back({SPV_OPERAND_TYPE_ID, {zeroId}});
|
|
||||||
for (auto& operand : plan.trailingOperands) {
|
|
||||||
plan.operands.push_back(operand);
|
|
||||||
}
|
|
||||||
plan.instruction->SetOpcode(plan.opcode);
|
|
||||||
plan.instruction->SetInOperands(Move(plan.operands));
|
|
||||||
}
|
|
||||||
// Opcodes and operand lists changed underneath every cached analysis.
|
|
||||||
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
|
|
||||||
return Status::SuccessWithChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct RewritePlan {
|
|
||||||
spvtools::opt::Instruction* instruction = nullptr;
|
|
||||||
spv::Op opcode = spv::Op::OpNop;
|
|
||||||
// Everything up to and including the Image Operands mask; the Lod id and the
|
|
||||||
// trailing operand values are appended once the constant exists.
|
|
||||||
Vector<spvtools::opt::Operand> operands;
|
|
||||||
Vector<spvtools::opt::Operand> trailingOperands;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Image Operands bits that may accompany an implicit-LOD sample, in the canonical
|
|
||||||
// ascending order SPIR-V requires the operand values to appear in.
|
|
||||||
static constexpr Uint32 kBias = 0x1;
|
|
||||||
static constexpr Uint32 kLod = 0x2;
|
|
||||||
static constexpr Uint32 kGrad = 0x4;
|
|
||||||
static constexpr Uint32 kConstOffset = 0x8;
|
|
||||||
static constexpr Uint32 kOffset = 0x10;
|
|
||||||
static constexpr Uint32 kConstOffsets = 0x20;
|
|
||||||
static constexpr Uint32 kSample = 0x40;
|
|
||||||
static constexpr Uint32 kMinLod = 0x80;
|
|
||||||
static constexpr Uint32 kKnownMask = 0xFF;
|
|
||||||
|
|
||||||
Uint32 GetFloatZeroId() {
|
|
||||||
// Reuse a 32-bit float type already in the module; a shader that samples always has
|
|
||||||
// one, and looking it up avoids depending on type-creation API details.
|
|
||||||
Uint32 floatTypeId = 0;
|
|
||||||
for (auto& inst : get_module()->types_values()) {
|
|
||||||
if (inst.opcode() == spv::Op::OpTypeFloat && inst.NumInOperands() >= 1 &&
|
|
||||||
inst.GetSingleWordInOperand(0) == 32) {
|
|
||||||
floatTypeId = inst.result_id();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (floatTypeId == 0) return 0;
|
|
||||||
|
|
||||||
const auto* floatType = context()->get_type_mgr()->GetType(floatTypeId);
|
|
||||||
if (floatType == nullptr) return 0;
|
|
||||||
const auto zeroBits = std::bit_cast<Uint32>(0.0f);
|
|
||||||
const auto* zeroConst = context()->get_constant_mgr()->GetConstant(floatType, {zeroBits});
|
|
||||||
if (zeroConst == nullptr) return 0;
|
|
||||||
auto* zeroInst = context()->get_constant_mgr()->GetDefiningInstruction(zeroConst);
|
|
||||||
return zeroInst != nullptr ? zeroInst->result_id() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Bool MapOpcode(spv::Op op, spv::Op& outOpcode, Uint32& outFixedOperandCount) {
|
|
||||||
switch (op) {
|
|
||||||
case spv::Op::OpImageSampleImplicitLod:
|
|
||||||
outOpcode = spv::Op::OpImageSampleExplicitLod;
|
|
||||||
outFixedOperandCount = 2; // sampled image, coordinate
|
|
||||||
return true;
|
|
||||||
case spv::Op::OpImageSampleProjImplicitLod:
|
|
||||||
outOpcode = spv::Op::OpImageSampleProjExplicitLod;
|
|
||||||
outFixedOperandCount = 2;
|
|
||||||
return true;
|
|
||||||
case spv::Op::OpImageSampleDrefImplicitLod:
|
|
||||||
outOpcode = spv::Op::OpImageSampleDrefExplicitLod;
|
|
||||||
outFixedOperandCount = 3; // sampled image, coordinate, Dref
|
|
||||||
return true;
|
|
||||||
case spv::Op::OpImageSampleProjDrefImplicitLod:
|
|
||||||
outOpcode = spv::Op::OpImageSampleProjDrefExplicitLod;
|
|
||||||
outFixedOperandCount = 3;
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Bool PlanRewrite(spvtools::opt::Instruction* inst, RewritePlan& outPlan) {
|
|
||||||
spv::Op newOpcode = spv::Op::OpNop;
|
|
||||||
Uint32 fixedCount = 0;
|
|
||||||
if (!MapOpcode(inst->opcode(), newOpcode, fixedCount)) return false;
|
|
||||||
if (inst->NumInOperands() < fixedCount) return false;
|
|
||||||
|
|
||||||
Uint32 mask = 0;
|
|
||||||
Uint32 next = fixedCount;
|
|
||||||
if (inst->NumInOperands() > fixedCount) {
|
|
||||||
mask = inst->GetSingleWordInOperand(fixedCount);
|
|
||||||
next = fixedCount + 1;
|
|
||||||
}
|
|
||||||
// An operand this pass does not model would be silently reordered or dropped, and
|
|
||||||
// Grad cannot legally accompany an implicit-LOD sample: leave such an instruction be.
|
|
||||||
if ((mask & ~kKnownMask) != 0 || (mask & kGrad) != 0) return false;
|
|
||||||
|
|
||||||
Vector<spvtools::opt::Operand> fixedOperands;
|
|
||||||
fixedOperands.reserve(fixedCount + 1);
|
|
||||||
for (Uint32 i = 0; i < fixedCount; ++i) {
|
|
||||||
fixedOperands.push_back(inst->GetInOperand(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect the surviving operand values in the same ascending-bit order they were
|
|
||||||
// encoded in, so the rebuilt list stays canonical.
|
|
||||||
Uint32 keptMask = kLod;
|
|
||||||
Vector<spvtools::opt::Operand> keptOperands;
|
|
||||||
static constexpr Uint32 kOrderedBits[] = {kBias, kLod, kGrad, kConstOffset,
|
|
||||||
kOffset, kConstOffsets, kSample, kMinLod};
|
|
||||||
for (const Uint32 bit : kOrderedBits) {
|
|
||||||
if ((mask & bit) == 0) continue;
|
|
||||||
if (next >= inst->NumInOperands()) return false;
|
|
||||||
const spvtools::opt::Operand value = inst->GetInOperand(next++);
|
|
||||||
// Bias and MinLod only shift a lambda that is already clamped to 0, and any
|
|
||||||
// original Lod is replaced by the constant the caller appends.
|
|
||||||
if (bit == kBias || bit == kMinLod || bit == kLod) continue;
|
|
||||||
keptMask |= bit;
|
|
||||||
keptOperands.push_back(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
fixedOperands.push_back({SPV_OPERAND_TYPE_IMAGE, {keptMask}});
|
|
||||||
outPlan.instruction = inst;
|
|
||||||
outPlan.opcode = newOpcode;
|
|
||||||
outPlan.operands = Move(fixedOperands);
|
|
||||||
outPlan.trailingOperands = Move(keptOperands);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
spvtools::Optimizer::PassToken CreateForceExplicitLod0SamplePass() {
|
|
||||||
return spvtools::Optimizer::PassToken(MakeUnique<ForceExplicitLod0SamplePass>());
|
|
||||||
}
|
|
||||||
|
|
||||||
// TEMP-PERFDIAG: measure what fragment-stage fp32 costs on this GPU. Desktop GLSL carries
|
|
||||||
// no precision qualifiers, so everything reaches the driver as full fp32 while Adreno runs
|
|
||||||
// fp16 at twice the rate. Decorating every float-typed result in a fragment entry point
|
|
||||||
// with RelaxedPrecision is the blunt "all mediump" upper bound - it changes results, so it
|
|
||||||
// is a probe, not a shipping transform. Toggled by /sdcard/MG/exp_relaxed_precision.
|
|
||||||
class RelaxedPrecisionProbePass final : public spvtools::opt::Pass {
|
|
||||||
public:
|
|
||||||
const char* name() const override { return "relaxed-precision-probe"; }
|
|
||||||
|
|
||||||
Status Process() override {
|
|
||||||
Bool isFragment = false;
|
|
||||||
for (auto& entryPoint : get_module()->entry_points()) {
|
|
||||||
if (entryPoint.opcode() != spv::Op::OpEntryPoint) continue;
|
|
||||||
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) ==
|
|
||||||
spv::ExecutionModel::Fragment) {
|
|
||||||
isFragment = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isFragment) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
// Every 32-bit-float scalar/vector/matrix type in the module. Anything wider (f64)
|
|
||||||
// or narrower is left alone: RelaxedPrecision only has meaning for 32-bit floats.
|
|
||||||
std::unordered_set<Uint32> relaxableTypes;
|
|
||||||
for (auto& type : get_module()->types_values()) {
|
|
||||||
const Uint32 typeId = type.result_id();
|
|
||||||
if (typeId == 0) continue;
|
|
||||||
switch (type.opcode()) {
|
|
||||||
case spv::Op::OpTypeFloat:
|
|
||||||
if (type.GetSingleWordInOperand(0) == 32) relaxableTypes.insert(typeId);
|
|
||||||
break;
|
|
||||||
case spv::Op::OpTypeVector:
|
|
||||||
case spv::Op::OpTypeMatrix:
|
|
||||||
if (relaxableTypes.count(type.GetSingleWordInOperand(0)) != 0) {
|
|
||||||
relaxableTypes.insert(typeId);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (relaxableTypes.empty()) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
Vector<Uint32> targets;
|
|
||||||
for (auto& function : *get_module()) {
|
|
||||||
for (auto& block : function) {
|
|
||||||
for (auto& inst : block) {
|
|
||||||
const Uint32 resultId = inst.result_id();
|
|
||||||
if (resultId == 0) continue;
|
|
||||||
if (relaxableTypes.count(inst.type_id()) == 0) continue;
|
|
||||||
targets.push_back(resultId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (targets.empty()) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
for (const Uint32 id : targets) {
|
|
||||||
context()->get_decoration_mgr()->AddDecoration(
|
|
||||||
id, static_cast<Uint32>(spv::Decoration::RelaxedPrecision));
|
|
||||||
}
|
|
||||||
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
|
|
||||||
return Status::SuccessWithChange;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Relax fragment-stage arithmetic that provably came out of a texture read. Desktop GLSL
|
|
||||||
// has no precision qualifiers, so every fragment value reaches the driver as fp32 while
|
|
||||||
// Adreno runs fp16 at twice the rate - and a texel is at most 8 bits per channel, which
|
|
||||||
// fp16's 11-bit mantissa carries exactly. Seeding at image reads and propagating only
|
|
||||||
// through operations whose every input is already relaxed keeps everything the shader
|
|
||||||
// computes from other sources (screen coordinates, depth, wide-range uniforms) at full
|
|
||||||
// precision, which is where fp16 would actually go wrong: fp16 cannot even represent a
|
|
||||||
// 3044-pixel gl_FragCoord.x exactly.
|
|
||||||
class RelaxTextureDerivedPrecisionPass final : public spvtools::opt::Pass {
|
|
||||||
public:
|
|
||||||
const char* name() const override { return "relax-texture-derived-precision"; }
|
|
||||||
|
|
||||||
Status Process() override {
|
|
||||||
if (!IsFragmentEntryPoint()) return Status::SuccessWithoutChange;
|
|
||||||
// A shader that drives depth or coverage itself is out of scope: those values must
|
|
||||||
// stay exact, and proving which computations feed them is not worth it here.
|
|
||||||
if (WritesDepthOrSampleMask()) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
CollectRelaxableFloatTypes();
|
|
||||||
if (m_relaxableTypes.empty()) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
// Whitelisting from texture reads captures nothing in practice: MC's fragment
|
|
||||||
// shaders multiply every texel by an interpolated colour and a UBO value, so one
|
|
||||||
// un-relaxed operand vetoes the whole expression (measured: no fps change).
|
|
||||||
// Taint the few genuinely precision-critical sources instead and relax the rest.
|
|
||||||
std::unordered_set<Uint32> tainted;
|
|
||||||
CollectPrecisionCriticalSeeds(tainted);
|
|
||||||
Bool grew = true;
|
|
||||||
while (grew) {
|
|
||||||
grew = false;
|
|
||||||
for (auto& function : *get_module()) {
|
|
||||||
for (auto& block : function) {
|
|
||||||
for (auto& inst : block) {
|
|
||||||
const Uint32 resultId = inst.result_id();
|
|
||||||
if (resultId == 0 || tainted.count(resultId) != 0) continue;
|
|
||||||
if (!AnyOperandTainted(inst, tainted)) continue;
|
|
||||||
tainted.insert(resultId);
|
|
||||||
grew = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unordered_set<Uint32> relaxed;
|
|
||||||
for (auto& function : *get_module()) {
|
|
||||||
for (auto& block : function) {
|
|
||||||
for (auto& inst : block) {
|
|
||||||
const Uint32 resultId = inst.result_id();
|
|
||||||
if (resultId == 0 || tainted.count(resultId) != 0) continue;
|
|
||||||
if (m_relaxableTypes.count(inst.type_id()) == 0) continue;
|
|
||||||
relaxed.insert(resultId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (relaxed.empty()) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
for (const Uint32 id : relaxed) {
|
|
||||||
context()->get_decoration_mgr()->AddDecoration(
|
|
||||||
id, static_cast<Uint32>(spv::Decoration::RelaxedPrecision));
|
|
||||||
}
|
|
||||||
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
|
|
||||||
return Status::SuccessWithChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::unordered_set<Uint32> m_relaxableTypes;
|
|
||||||
|
|
||||||
Bool IsFragmentEntryPoint() const {
|
|
||||||
for (auto& entryPoint : get_module()->entry_points()) {
|
|
||||||
if (entryPoint.opcode() != spv::Op::OpEntryPoint) continue;
|
|
||||||
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) ==
|
|
||||||
spv::ExecutionModel::Fragment) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool WritesDepthOrSampleMask() const {
|
|
||||||
for (auto& annotation : get_module()->annotations()) {
|
|
||||||
if (annotation.opcode() != spv::Op::OpDecorate) continue;
|
|
||||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
|
|
||||||
spv::Decoration::BuiltIn) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const auto builtIn = static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2));
|
|
||||||
if (builtIn == spv::BuiltIn::FragDepth || builtIn == spv::BuiltIn::SampleMask) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectRelaxableFloatTypes() {
|
|
||||||
m_relaxableTypes.clear();
|
|
||||||
for (auto& type : get_module()->types_values()) {
|
|
||||||
const Uint32 typeId = type.result_id();
|
|
||||||
if (typeId == 0) continue;
|
|
||||||
switch (type.opcode()) {
|
|
||||||
case spv::Op::OpTypeFloat:
|
|
||||||
if (type.GetSingleWordInOperand(0) == 32) m_relaxableTypes.insert(typeId);
|
|
||||||
break;
|
|
||||||
case spv::Op::OpTypeVector:
|
|
||||||
if (m_relaxableTypes.count(type.GetSingleWordInOperand(0)) != 0) {
|
|
||||||
m_relaxableTypes.insert(typeId);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectImageReadSeeds(std::unordered_set<Uint32>& relaxed) const {
|
|
||||||
for (auto& function : *get_module()) {
|
|
||||||
for (auto& block : function) {
|
|
||||||
for (auto& inst : block) {
|
|
||||||
const Uint32 resultId = inst.result_id();
|
|
||||||
if (resultId == 0 || m_relaxableTypes.count(inst.type_id()) == 0) continue;
|
|
||||||
// Interpolated user varyings seed too, or propagation dies at the
|
|
||||||
// first `texel * vertexColour`: the load of an Input can never be
|
|
||||||
// relaxed by the rule below (its operand is a pointer), so a single
|
|
||||||
// varying vetoes every downstream operation. This is what ESSL's
|
|
||||||
// mediump varyings already mean. Built-ins are excluded - gl_FragCoord
|
|
||||||
// carries pixel coordinates that fp16 cannot represent exactly.
|
|
||||||
if (inst.opcode() == spv::Op::OpLoad && IsNonBuiltInFragmentInput(inst)) {
|
|
||||||
relaxed.insert(resultId);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
switch (inst.opcode()) {
|
|
||||||
case spv::Op::OpImageSampleImplicitLod:
|
|
||||||
case spv::Op::OpImageSampleExplicitLod:
|
|
||||||
case spv::Op::OpImageSampleProjImplicitLod:
|
|
||||||
case spv::Op::OpImageSampleProjExplicitLod:
|
|
||||||
case spv::Op::OpImageSampleDrefImplicitLod:
|
|
||||||
case spv::Op::OpImageSampleDrefExplicitLod:
|
|
||||||
case spv::Op::OpImageFetch:
|
|
||||||
case spv::Op::OpImageRead:
|
|
||||||
case spv::Op::OpImageGather:
|
|
||||||
relaxed.insert(resultId);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpLoad straight out of a fragment Input variable that carries no BuiltIn decoration.
|
|
||||||
// Only a direct load counts: a load through an access chain could be indexing a
|
|
||||||
// structure whose other members are not interpolated colour data.
|
|
||||||
Bool IsNonBuiltInFragmentInput(const spvtools::opt::Instruction& load) const {
|
|
||||||
const Uint32 pointerId = load.GetSingleWordInOperand(0);
|
|
||||||
const auto* pointer = context()->get_def_use_mgr()->GetDef(pointerId);
|
|
||||||
if (pointer == nullptr || pointer->opcode() != spv::Op::OpVariable) return false;
|
|
||||||
if (static_cast<spv::StorageClass>(pointer->GetSingleWordInOperand(0)) !=
|
|
||||||
spv::StorageClass::Input) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Bool isBuiltIn = false;
|
|
||||||
context()->get_decoration_mgr()->ForEachDecoration(
|
|
||||||
pointerId, static_cast<Uint32>(spv::Decoration::BuiltIn),
|
|
||||||
[&isBuiltIn](const spvtools::opt::Instruction&) { isBuiltIn = true; });
|
|
||||||
return !isBuiltIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A float constant small enough that fp16 represents it without surprise. Colour math
|
|
||||||
// constants (0, 1, 0.5, 255, gamma exponents) all live here; anything larger is
|
|
||||||
// treated as unknown so it stops propagation.
|
|
||||||
Bool IsBoundedFloatConstant(Uint32 id) const {
|
|
||||||
const auto* constant = context()->get_constant_mgr()->FindDeclaredConstant(id);
|
|
||||||
if (constant == nullptr) return false;
|
|
||||||
if (const auto* scalar = constant->AsFloatConstant()) {
|
|
||||||
const float value = scalar->GetFloat();
|
|
||||||
return std::isfinite(value) && std::fabs(value) <= 1024.0f;
|
|
||||||
}
|
|
||||||
if (const auto* composite = constant->AsVectorConstant()) {
|
|
||||||
for (const auto* component : composite->GetComponents()) {
|
|
||||||
const auto* scalar = component->AsFloatConstant();
|
|
||||||
if (scalar == nullptr) return false;
|
|
||||||
const float value = scalar->GetFloat();
|
|
||||||
if (!std::isfinite(value) || std::fabs(value) > 1024.0f) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Precision-critical sources: a built-in fragment input. gl_FragCoord is the one that
|
|
||||||
// matters - fp16 cannot represent a 3044-pixel x coordinate exactly, and anything
|
|
||||||
// derived from it (screen-space effects, manual depth reconstruction) would visibly
|
|
||||||
// quantise. Everything else a fragment shader reads is colour-range data.
|
|
||||||
void CollectPrecisionCriticalSeeds(std::unordered_set<Uint32>& tainted) const {
|
|
||||||
for (auto& function : *get_module()) {
|
|
||||||
for (auto& block : function) {
|
|
||||||
for (auto& inst : block) {
|
|
||||||
if (inst.opcode() != spv::Op::OpLoad || inst.result_id() == 0) continue;
|
|
||||||
if (IsBuiltInInputLoad(inst)) tainted.insert(inst.result_id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool IsBuiltInInputLoad(const spvtools::opt::Instruction& load) const {
|
|
||||||
const Uint32 pointerId = load.GetSingleWordInOperand(0);
|
|
||||||
const auto* pointer = context()->get_def_use_mgr()->GetDef(pointerId);
|
|
||||||
if (pointer == nullptr || pointer->opcode() != spv::Op::OpVariable) return false;
|
|
||||||
if (static_cast<spv::StorageClass>(pointer->GetSingleWordInOperand(0)) !=
|
|
||||||
spv::StorageClass::Input) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Bool isBuiltIn = false;
|
|
||||||
context()->get_decoration_mgr()->ForEachDecoration(
|
|
||||||
pointerId, static_cast<Uint32>(spv::Decoration::BuiltIn),
|
|
||||||
[&isBuiltIn](const spvtools::opt::Instruction&) { isBuiltIn = true; });
|
|
||||||
return isBuiltIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool AnyOperandTainted(const spvtools::opt::Instruction& inst,
|
|
||||||
const std::unordered_set<Uint32>& tainted) const {
|
|
||||||
const Uint32 operandCount = inst.NumInOperands();
|
|
||||||
for (Uint32 i = 0; i < operandCount; ++i) {
|
|
||||||
const auto& operand = inst.GetInOperand(i);
|
|
||||||
if (!spvIsIdType(operand.type)) continue;
|
|
||||||
if (IsNonNumericOperand(inst, i)) continue;
|
|
||||||
if (tainted.count(operand.words[0]) != 0) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool AllValueOperandsRelaxed(const spvtools::opt::Instruction& inst,
|
|
||||||
const std::unordered_set<Uint32>& relaxed) const {
|
|
||||||
switch (inst.opcode()) {
|
|
||||||
// Pointer-typed plumbing: relaxing the loaded value would say nothing about the
|
|
||||||
// memory it came from, and the pointer operand can never be in the set.
|
|
||||||
case spv::Op::OpLoad:
|
|
||||||
case spv::Op::OpStore:
|
|
||||||
case spv::Op::OpAccessChain:
|
|
||||||
case spv::Op::OpInBoundsAccessChain:
|
|
||||||
case spv::Op::OpFunctionCall:
|
|
||||||
return false;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool sawValueOperand = false;
|
|
||||||
Bool allRelaxed = true;
|
|
||||||
const Uint32 operandCount = inst.NumInOperands();
|
|
||||||
for (Uint32 i = 0; i < operandCount; ++i) {
|
|
||||||
const auto& operand = inst.GetInOperand(i);
|
|
||||||
if (!spvIsIdType(operand.type)) continue; // literals: selectors, swizzle indices
|
|
||||||
const Uint32 id = operand.words[0];
|
|
||||||
// OpPhi's block labels, OpSelect's condition and OpExtInst's instruction-set id
|
|
||||||
// are ids that carry no numeric precision; skip them rather than let them veto.
|
|
||||||
if (IsNonNumericOperand(inst, i)) continue;
|
|
||||||
sawValueOperand = true;
|
|
||||||
if (relaxed.count(id) != 0) continue;
|
|
||||||
if (IsBoundedFloatConstant(id)) continue;
|
|
||||||
allRelaxed = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return sawValueOperand && allRelaxed;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Bool IsNonNumericOperand(const spvtools::opt::Instruction& inst, Uint32 index) {
|
|
||||||
switch (inst.opcode()) {
|
|
||||||
case spv::Op::OpPhi:
|
|
||||||
return (index % 2) == 1; // parent block labels
|
|
||||||
case spv::Op::OpSelect:
|
|
||||||
return index == 0; // condition
|
|
||||||
case spv::Op::OpExtInst:
|
|
||||||
return index == 0; // extended instruction set
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// TEMP-PERFDIAG: A/B switch between the scoped transform and the all-float upper bound.
|
|
||||||
Bool PerfDiagRelaxAllPrecision() {
|
|
||||||
static const Bool enabled = [] {
|
|
||||||
std::FILE* probe = std::fopen("/sdcard/MG/exp_relaxed_precision_all", "rb");
|
|
||||||
if (probe == nullptr) return false;
|
|
||||||
std::fclose(probe);
|
|
||||||
MGLOG_I("[PERFDIAG] fragment RelaxedPrecision: ALL floats (upper-bound probe)");
|
|
||||||
return true;
|
|
||||||
}();
|
|
||||||
return enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TEMP-PERFDIAG: lets a run turn the transform off entirely for an A/B baseline.
|
|
||||||
Bool PerfDiagRelaxedPrecisionEnabled() {
|
|
||||||
static const Bool disabled = [] {
|
|
||||||
std::FILE* probe = std::fopen("/sdcard/MG/exp_no_relaxed_precision", "rb");
|
|
||||||
if (probe == nullptr) return false;
|
|
||||||
std::fclose(probe);
|
|
||||||
MGLOG_I("[PERFDIAG] fragment RelaxedPrecision DISABLED");
|
|
||||||
return true;
|
|
||||||
}();
|
|
||||||
return !disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool TransformSpirvForExplicitLod0Sampling(const Vector<Uint>& input, Vector<Uint>& output) {
|
|
||||||
if (input.empty()) {
|
|
||||||
output.clear();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
|
||||||
spvtools::OptimizerOptions options;
|
|
||||||
// Matches the position-fix pass: this build of spirv-tools asserts rather than
|
|
||||||
// reporting, so validation stays off in the shipping path.
|
|
||||||
options.set_run_validator(false);
|
|
||||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
|
||||||
const char* message) {
|
|
||||||
MGLOG_E("Vulkan: explicit-LOD0 pass: %s", message != nullptr ? message : "");
|
|
||||||
});
|
|
||||||
optimizer.RegisterPass(CreateForceExplicitLod0SamplePass());
|
|
||||||
|
|
||||||
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
|
||||||
if (!success) {
|
|
||||||
MGLOG_E("Vulkan: explicit-LOD0 sampling pass failed; keeping the original module");
|
|
||||||
output = input;
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
spvtools::Optimizer::PassToken CreateGlToVulkanPositionFixPass(
|
spvtools::Optimizer::PassToken CreateGlToVulkanPositionFixPass(
|
||||||
ProgramFactory::CompileOptionFlags transformFlags) {
|
ProgramFactory::CompileOptionFlags transformFlags) {
|
||||||
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
|
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TEMP-PERFDIAG
|
|
||||||
Bool TransformSpirvForRelaxedPrecisionProbe(const Vector<Uint>& input, Vector<Uint>& output) {
|
|
||||||
if (input.empty()) {
|
|
||||||
output.clear();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
|
||||||
spvtools::OptimizerOptions options;
|
|
||||||
options.set_run_validator(false);
|
|
||||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
|
||||||
const char* message) {
|
|
||||||
MGLOG_E("Vulkan: relaxed-precision probe: %s", message != nullptr ? message : "");
|
|
||||||
});
|
|
||||||
// SSA promotion first: glslang emits function-local variables with stores and loads,
|
|
||||||
// and a load can never be relaxed (its operand is a pointer), so without this the
|
|
||||||
// propagation below dies at the first temporary.
|
|
||||||
optimizer.RegisterPass(spvtools::CreateLocalMultiStoreElimPass());
|
|
||||||
if (PerfDiagRelaxAllPrecision()) {
|
|
||||||
optimizer.RegisterPass(spvtools::Optimizer::PassToken(MakeUnique<RelaxedPrecisionProbePass>()));
|
|
||||||
} else {
|
|
||||||
optimizer.RegisterPass(
|
|
||||||
spvtools::Optimizer::PassToken(MakeUnique<RelaxTextureDerivedPrecisionPass>()));
|
|
||||||
}
|
|
||||||
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
|
||||||
if (!success) {
|
|
||||||
MGLOG_E("Vulkan: relaxed-precision probe failed; keeping the original module");
|
|
||||||
output = input;
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool TransformSpirvForVulkanPositionFix(const Vector<Uint>& input, Vector<Uint>& output,
|
Bool TransformSpirvForVulkanPositionFix(const Vector<Uint>& input, Vector<Uint>& output,
|
||||||
ProgramFactory::CompileOptionFlags transformFlags) {
|
ProgramFactory::CompileOptionFlags transformFlags) {
|
||||||
if (input.empty()) {
|
if (input.empty()) {
|
||||||
@@ -2552,17 +1950,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
auto it = m_cache.find(hash);
|
auto it = m_cache.find(hash);
|
||||||
if (it != m_cache.end()) {
|
if (it != m_cache.end()) {
|
||||||
// Every draw/dispatch funnels through this lookup (the renderer memos only
|
|
||||||
// skip re-hashing, never the factory lookup), so an actively-used entry is
|
|
||||||
// stamped at least once per frame boundary and can never be aged out while
|
|
||||||
// any in-flight command buffer still references it.
|
|
||||||
it->second.lastUsedFrame = m_frameCounter;
|
|
||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto& entry = m_cache[hash];
|
auto& entry = m_cache[hash];
|
||||||
entry.hash = hash;
|
entry.hash = hash;
|
||||||
entry.lastUsedFrame = m_frameCounter;
|
|
||||||
auto& shaders = program.GetAttachedShaders();
|
auto& shaders = program.GetAttachedShaders();
|
||||||
auto& spirv = program.GetGeneratedSpirv();
|
auto& spirv = program.GetGeneratedSpirv();
|
||||||
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
|
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
|
||||||
@@ -2580,23 +1972,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
moduleSpirvs[i] = spv;
|
moduleSpirvs[i] = spv;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((flags & ProgramFactory::CompileOptionBit::ExplicitLod0Sampling) && shaders[i] &&
|
|
||||||
shaders[i]->GetShaderStage() == ShaderStage::Fragment) {
|
|
||||||
Vector<Uint> explicitLodSpirv;
|
|
||||||
if (TransformSpirvForExplicitLod0Sampling(moduleSpirvs[i], explicitLodSpirv)) {
|
|
||||||
moduleSpirvs[i] = Move(explicitLodSpirv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((flags & ProgramFactory::CompileOptionBit::RelaxedFragmentPrecision) &&
|
|
||||||
PerfDiagRelaxedPrecisionEnabled() && shaders[i] &&
|
|
||||||
shaders[i]->GetShaderStage() == ShaderStage::Fragment) {
|
|
||||||
Vector<Uint> relaxedSpirv;
|
|
||||||
if (TransformSpirvForRelaxedPrecisionProbe(moduleSpirvs[i], relaxedSpirv)) {
|
|
||||||
moduleSpirvs[i] = Move(relaxedSpirv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GL apps depend on cross-program position invariance for multi-pass equality
|
// GL apps depend on cross-program position invariance for multi-pass equality
|
||||||
// depth tests (MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the
|
// depth tests (MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the
|
||||||
// depth its own first pass wrote); decorate Position outputs Invariant so
|
// depth its own first pass wrote); decorate Position outputs Invariant so
|
||||||
@@ -2693,42 +2068,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramFactory::OnFrameBoundary() {
|
|
||||||
++m_frameCounter;
|
|
||||||
|
|
||||||
// Sweep cadence and retire age mirror VkRenderPassManager::OnPresent: an entry
|
|
||||||
// idle for more than kRetireAgeFrames frame boundaries cannot be referenced by
|
|
||||||
// any in-flight command buffer (frames-in-flight <= MOBILEGL_MAGMA_FRAMESINFLIGHT),
|
|
||||||
// so its shader modules and layouts are destroyed immediately - no deferred-
|
|
||||||
// destroy machinery needed. Eviction is content-based, never tied to
|
|
||||||
// glDeleteProgram: the cache is content-hash-shared across GL programs, so a
|
|
||||||
// delete-driven erase could free an entry another live program still resolves.
|
|
||||||
// An evicted entry self-heals - the frontend program keeps its generated
|
|
||||||
// SPIR-V, so the next GetOrCreateProgram rebuilds it (this also covers the
|
|
||||||
// renderer's internal blit/depth-mipmap programs).
|
|
||||||
constexpr Uint64 kSweepInterval = 256;
|
|
||||||
constexpr Uint64 kRetireAgeFrames = 1024;
|
|
||||||
if ((m_frameCounter % kSweepInterval) != 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
|
||||||
if (m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
|
|
||||||
const HashType hash = it->first;
|
|
||||||
const VkDescriptorSetLayout descriptorSetLayout = it->second.descriptorSetLayout;
|
|
||||||
MGLOG_D("ProgramFactory::OnFrameBoundary: evicting idle program entry hash=0x%llx",
|
|
||||||
static_cast<unsigned long long>(hash));
|
|
||||||
// erase runs ~VkProgramObject (modules/layouts destroyed); notify after
|
|
||||||
// so an observer never observes a half-destroyed entry through a lookup.
|
|
||||||
// Observers only need the handle values to purge their keyed caches.
|
|
||||||
it = m_cache.erase(it);
|
|
||||||
if (m_evictionObserver != nullptr) {
|
|
||||||
m_evictionObserver->OnProgramEvicted(hash, descriptorSetLayout);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
++it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -42,16 +42,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
SurfaceRotate90 = 1 << 2,
|
SurfaceRotate90 = 1 << 2,
|
||||||
SurfaceRotate180 = 1 << 3,
|
SurfaceRotate180 = 1 << 3,
|
||||||
SurfaceRotate270 = 1 << 4,
|
SurfaceRotate270 = 1 << 4,
|
||||||
// Rewrites the fragment stage's implicit-LOD image samples to explicit LOD 0.
|
|
||||||
// Only ever set for a draw whose every sampler binding is clamped to a single mip
|
|
||||||
// level, which makes the two forms produce identical texels (the implicit lambda is
|
|
||||||
// clamped into [minLod, maxLod] = [0, 0] regardless of derivatives or bias).
|
|
||||||
ExplicitLod0Sampling = 1 << 5,
|
|
||||||
// Fragment arithmetic may run at relaxed (fp16) precision. Only requested for draws
|
|
||||||
// where every sampled texture and every colour attachment is an 8-bit-or-less
|
|
||||||
// normalized format, so nothing the shader reads or writes carries more precision
|
|
||||||
// than fp16 already represents exactly.
|
|
||||||
RelaxedFragmentPrecision = 1 << 6,
|
|
||||||
};
|
};
|
||||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||||
using HashType = Uint64;
|
using HashType = Uint64;
|
||||||
@@ -98,9 +88,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
|
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
|
||||||
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
|
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
|
||||||
Bool fragmentReplacesDepth = false;
|
Bool fragmentReplacesDepth = false;
|
||||||
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
|
||||||
// cache eviction (see OnFrameBoundary).
|
|
||||||
Uint64 lastUsedFrame = 0;
|
|
||||||
|
|
||||||
static inline VkDevice s_device = VK_NULL_HANDLE;
|
static inline VkDevice s_device = VK_NULL_HANDLE;
|
||||||
|
|
||||||
@@ -137,7 +124,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||||
lastUsedFrame = other.lastUsedFrame;
|
|
||||||
other.hash = 0;
|
other.hash = 0;
|
||||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
other.pipelineLayout = VK_NULL_HANDLE;
|
other.pipelineLayout = VK_NULL_HANDLE;
|
||||||
@@ -149,7 +135,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
other.producerOutputComponentCount = 0;
|
other.producerOutputComponentCount = 0;
|
||||||
other.fragmentInputComponentCount = 0;
|
other.fragmentInputComponentCount = 0;
|
||||||
other.fragmentReplacesDepth = false;
|
other.fragmentReplacesDepth = false;
|
||||||
other.lastUsedFrame = 0;
|
|
||||||
}
|
}
|
||||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||||
if (this == &other) {
|
if (this == &other) {
|
||||||
@@ -185,7 +170,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||||
lastUsedFrame = other.lastUsedFrame;
|
|
||||||
other.hash = 0;
|
other.hash = 0;
|
||||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
other.pipelineLayout = VK_NULL_HANDLE;
|
other.pipelineLayout = VK_NULL_HANDLE;
|
||||||
@@ -197,7 +181,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
other.producerOutputComponentCount = 0;
|
other.producerOutputComponentCount = 0;
|
||||||
other.fragmentInputComponentCount = 0;
|
other.fragmentInputComponentCount = 0;
|
||||||
other.fragmentReplacesDepth = false;
|
other.fragmentReplacesDepth = false;
|
||||||
other.lastUsedFrame = 0;
|
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,18 +210,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Notified when the OnFrameBoundary sweep destroys an aged-out cache entry,
|
|
||||||
// carrying the entry's content hash and the VkDescriptorSetLayout it owned.
|
|
||||||
// Dependent caches (compute pipelines, PipelineFactory entries, UniformManager's
|
|
||||||
// per-layout descriptor sets) must purge in the same step: after vkDestroy the
|
|
||||||
// layout handle value may be recycled for an unrelated layout, and the program
|
|
||||||
// hash may be re-inserted by a later rebuild of the same content.
|
|
||||||
class IEvictionObserver {
|
|
||||||
public:
|
|
||||||
virtual ~IEvictionObserver() = default;
|
|
||||||
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
|
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
|
||||||
Bool shaderDrawParametersEnabled = false,
|
Bool shaderDrawParametersEnabled = false,
|
||||||
Bool unformattedFloatStorageImagesEnabled = false)
|
Bool unformattedFloatStorageImagesEnabled = false)
|
||||||
@@ -254,13 +225,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const VkProgramObject& GetOrCreateProgram(
|
const VkProgramObject& GetOrCreateProgram(
|
||||||
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
|
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
|
||||||
|
|
||||||
// Observer may be null (no notifications). Not owned.
|
|
||||||
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
|
|
||||||
// Frame boundary hook: ages the program cache and evicts long-unused entries
|
|
||||||
// (their command buffers retired many frames ago), mirroring
|
|
||||||
// VkRenderPassManager::OnPresent's sweep.
|
|
||||||
void OnFrameBoundary();
|
|
||||||
|
|
||||||
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
||||||
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
||||||
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
|
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
|
||||||
@@ -302,9 +266,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
||||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||||
mutable ProgramLookupCache m_lastLookup;
|
mutable ProgramLookupCache m_lastLookup;
|
||||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
|
||||||
Uint64 m_frameCounter = 0;
|
|
||||||
IEvictionObserver* m_evictionObserver = nullptr;
|
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -247,11 +247,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
m_surfaceFormat = {createInfo.imageFormat, createInfo.imageColorSpace};
|
m_surfaceFormat = {createInfo.imageFormat, createInfo.imageColorSpace};
|
||||||
m_extent = createInfo.imageExtent;
|
m_extent = createInfo.imageExtent;
|
||||||
// The surface-space extent this swapchain was built from, i.e. before the
|
|
||||||
// quarter-turn swap above. Out-of-date checks must compare in THIS space: comparing a
|
|
||||||
// freshly queried currentExtent against the swapped m_extent flips axes every rotation
|
|
||||||
// and makes the comparison alternate forever.
|
|
||||||
m_surfaceExtent = defaultFramebufferExtent;
|
|
||||||
m_preTransform = createInfo.preTransform;
|
m_preTransform = createInfo.preTransform;
|
||||||
|
|
||||||
VK_VERIFY(vkCreateSwapchainKHR(device, &createInfo, nullptr, &m_swapchain));
|
VK_VERIFY(vkCreateSwapchainKHR(device, &createInfo, nullptr, &m_swapchain));
|
||||||
|
|||||||
@@ -35,9 +35,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkSwapchainKHR GetHandle() const { return m_swapchain; }
|
VkSwapchainKHR GetHandle() const { return m_swapchain; }
|
||||||
const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; }
|
const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; }
|
||||||
VkExtent2D GetExtent() const { return m_extent; }
|
VkExtent2D GetExtent() const { return m_extent; }
|
||||||
// Surface-space extent (before the pre-rotation quarter-turn swap) this swapchain was
|
|
||||||
// created from - the value to compare a freshly queried currentExtent against.
|
|
||||||
VkExtent2D GetSurfaceExtent() const { return m_surfaceExtent; }
|
|
||||||
VkSurfaceTransformFlagBitsKHR GetPreTransform() const { return m_preTransform; }
|
VkSurfaceTransformFlagBitsKHR GetPreTransform() const { return m_preTransform; }
|
||||||
const Vector<VkImage>& GetImages() const { return m_images; }
|
const Vector<VkImage>& GetImages() const { return m_images; }
|
||||||
const Vector<VkImageView>& GetImageViews() const { return m_imageViews; }
|
const Vector<VkImageView>& GetImageViews() const { return m_imageViews; }
|
||||||
@@ -66,7 +63,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkSwapchainKHR m_swapchain = VK_NULL_HANDLE;
|
VkSwapchainKHR m_swapchain = VK_NULL_HANDLE;
|
||||||
VkSurfaceFormatKHR m_surfaceFormat{};
|
VkSurfaceFormatKHR m_surfaceFormat{};
|
||||||
VkExtent2D m_extent{};
|
VkExtent2D m_extent{};
|
||||||
VkExtent2D m_surfaceExtent{};
|
|
||||||
VkSurfaceTransformFlagBitsKHR m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
VkSurfaceTransformFlagBitsKHR m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
||||||
Vector<VkImage> m_images;
|
Vector<VkImage> m_images;
|
||||||
Vector<VkImageView> m_imageViews;
|
Vector<VkImageView> m_imageViews;
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
||||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||||
#include <vulkan/utility/vk_format_utils.h>
|
|
||||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -212,40 +211,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UniformManager::OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout) {
|
|
||||||
SizeT purgedSets = 0;
|
|
||||||
for (auto& frame : m_frames) {
|
|
||||||
const auto it = frame.descriptorSetCacheByLayout.find(descriptorSetLayout);
|
|
||||||
if (it == frame.descriptorSetCacheByLayout.end()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Free the sets back to their pools and credit the bucket accounting, so
|
|
||||||
// program churn recycles pool capacity instead of abandoning the slots.
|
|
||||||
// GPU-safe: the layout only dies after >1024 idle frame boundaries, so no
|
|
||||||
// in-flight command buffer references these sets.
|
|
||||||
for (const auto& cached : it->second.sets) {
|
|
||||||
if (cached.set == VK_NULL_HANDLE) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
vkFreeDescriptorSets(m_device, cached.pool, 1, &cached.set);
|
|
||||||
const auto bucket = std::find_if(
|
|
||||||
frame.descriptorPools.begin(), frame.descriptorPools.end(),
|
|
||||||
[&cached](const DescriptorPoolBucket& candidate) { return candidate.handle == cached.pool; });
|
|
||||||
if (bucket != frame.descriptorPools.end() && bucket->allocatedSets > 0) {
|
|
||||||
--bucket->allocatedSets;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
purgedSets += it->second.sets.size();
|
|
||||||
frame.descriptorSetCacheByLayout.erase(it);
|
|
||||||
}
|
|
||||||
if (purgedSets > 0) {
|
|
||||||
// The per-draw reuse memo folds the layout handle into its signature; drop
|
|
||||||
// it so a recycled handle value cannot revive a purged set mid-frame.
|
|
||||||
m_hasLastDescriptor = false;
|
|
||||||
MGLOG_D("UniformDescriptorBinder: freed %zu descriptor sets for destroyed layout", purgedSets);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer,
|
Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer,
|
||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
@@ -385,28 +350,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Uint16 samplerVersion = samplerToUse->GetVersion();
|
const Uint16 samplerVersion = samplerToUse->GetVersion();
|
||||||
const Uint64 textureLifetimeId = texture->GetLifetimeId();
|
const Uint64 textureLifetimeId = texture->GetLifetimeId();
|
||||||
const Uint16 textureParamsVersion = texture->GetTextureParamsVersion();
|
const Uint16 textureParamsVersion = texture->GetTextureParamsVersion();
|
||||||
// The sampler's LOD clamp depends on how many levels the sampled view exposes, and that
|
|
||||||
// follows uploads as well as GL parameters - so it belongs in the memo key too.
|
|
||||||
const Uint32 viewLevelCount = resource->sampledLevelCount;
|
|
||||||
if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion &&
|
if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion &&
|
||||||
memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion &&
|
memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion &&
|
||||||
memo.forceNearestFiltering == forceNearestFiltering && memo.viewLevelCount == viewLevelCount) {
|
memo.forceNearestFiltering == forceNearestFiltering) {
|
||||||
resolvedSampler = memo.sampler;
|
resolvedSampler = memo.sampler;
|
||||||
} else {
|
} else {
|
||||||
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture,
|
resolvedSampler =
|
||||||
forceNearestFiltering, viewLevelCount);
|
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering);
|
||||||
memo.samplerLifetimeId = samplerLifetimeId;
|
memo.samplerLifetimeId = samplerLifetimeId;
|
||||||
memo.samplerVersion = samplerVersion;
|
memo.samplerVersion = samplerVersion;
|
||||||
memo.textureLifetimeId = textureLifetimeId;
|
memo.textureLifetimeId = textureLifetimeId;
|
||||||
memo.textureParamsVersion = textureParamsVersion;
|
memo.textureParamsVersion = textureParamsVersion;
|
||||||
memo.forceNearestFiltering = forceNearestFiltering;
|
memo.forceNearestFiltering = forceNearestFiltering;
|
||||||
memo.viewLevelCount = viewLevelCount;
|
|
||||||
memo.sampler = resolvedSampler;
|
memo.sampler = resolvedSampler;
|
||||||
memo.valid = true;
|
memo.valid = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering,
|
resolvedSampler =
|
||||||
resource->sampledLevelCount);
|
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering);
|
||||||
}
|
}
|
||||||
outImageInfo = {
|
outImageInfo = {
|
||||||
.sampler = resolvedSampler,
|
.sampler = resolvedSampler,
|
||||||
@@ -447,106 +408,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return outImageInfo.sampler != VK_NULL_HANDLE;
|
return outImageInfo.sampler != VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
|
||||||
// fp16 carries an 11-bit mantissa, so an 8-bit normalized channel round-trips exactly.
|
|
||||||
// Anything wider - 16-bit normalized, half float, full float, and every packed HDR
|
|
||||||
// encoding - holds precision or range that relaxing the arithmetic would throw away.
|
|
||||||
Bool IsLowPrecisionNormalizedFormat(VkFormat format) {
|
|
||||||
if (format == VK_FORMAT_UNDEFINED) return false;
|
|
||||||
if (!vkuFormatIsUNORM(format) && !vkuFormatIsSNORM(format) && !vkuFormatIsSRGB(format)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const struct VKU_FORMAT_INFO info = vkuGetFormatInfo(format);
|
|
||||||
for (Uint32 i = 0; i < info.component_count; ++i) {
|
|
||||||
if (info.components[i].size > 8) return false;
|
|
||||||
}
|
|
||||||
return info.component_count > 0;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
Bool UniformManager::DrawTargetIsLowPrecision(const MG_State::GLState::FramebufferObject* drawFramebuffer) {
|
|
||||||
// Default framebuffer: the swapchain is an 8-bit normalized surface.
|
|
||||||
if (drawFramebuffer == nullptr) return true;
|
|
||||||
|
|
||||||
Bool sawColour = false;
|
|
||||||
for (Int i = static_cast<Int>(FramebufferAttachmentType::Color0);
|
|
||||||
i < static_cast<Int>(FramebufferAttachmentType::FramebufferAttachmentTypeCount);
|
|
||||||
++i) {
|
|
||||||
const auto& attachment =
|
|
||||||
drawFramebuffer->GetAttachment(static_cast<FramebufferAttachmentType>(i));
|
|
||||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
|
||||||
if (const auto& texture = attachment.GetTexture()) {
|
|
||||||
format = MG_Util::ConvertTextureInternalFormatToVkEnum(texture->GetFormat());
|
|
||||||
} else if (const auto& renderbuffer = attachment.GetRenderbuffer()) {
|
|
||||||
format = MG_Util::ConvertTextureInternalFormatToVkEnum(
|
|
||||||
renderbuffer->GetInternalFormat());
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!IsLowPrecisionNormalizedFormat(format)) return false;
|
|
||||||
sawColour = true;
|
|
||||||
}
|
|
||||||
return sawColour;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool UniformManager::ProgramSamplesOnlyLowPrecisionTextures(
|
|
||||||
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj) {
|
|
||||||
for (Uint32 binding = 0; binding < programObj.bindingKinds.size(); ++binding) {
|
|
||||||
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const auto* texture = ResolveSamplerTextureRaw(program, programObj, binding);
|
|
||||||
// An unresolvable binding is unknown territory, not licence to relax.
|
|
||||||
if (texture == nullptr) return false;
|
|
||||||
const VkFormat format =
|
|
||||||
MG_Util::ConvertTextureInternalFormatToVkEnum(texture->GetFormat());
|
|
||||||
if (!IsLowPrecisionNormalizedFormat(format)) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool UniformManager::ProgramSamplesOnlySingleLevelTextures(
|
|
||||||
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj) {
|
|
||||||
Bool sawSampler = false;
|
|
||||||
for (Uint32 binding = 0; binding < programObj.bindingKinds.size(); ++binding) {
|
|
||||||
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const auto* texture = ResolveSamplerTextureRaw(program, programObj, binding);
|
|
||||||
if (texture == nullptr) return false;
|
|
||||||
const auto& levelRange = texture->GetLevelRange();
|
|
||||||
if (levelRange.x() != levelRange.y()) return false;
|
|
||||||
|
|
||||||
// An explicit-LOD sample is a single filtered tap, so it also gives up anisotropic
|
|
||||||
// filtering - which a single-level view can still have. Resolve the sampler exactly
|
|
||||||
// the way ResolveSamplerDescriptor does and bail if anisotropy would apply.
|
|
||||||
const Int location = programObj.samplerUniformLocationByBinding[binding];
|
|
||||||
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
|
|
||||||
const auto& samplerOverride = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();
|
|
||||||
const auto* effectiveSampler =
|
|
||||||
samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get();
|
|
||||||
if (effectiveSampler == nullptr) return false;
|
|
||||||
if (effectiveSampler->GetMaxAnisotropy() > 1.0f &&
|
|
||||||
effectiveSampler->GetMinFilter() == SamplerFilterMode::Linear &&
|
|
||||||
effectiveSampler->GetMagFilter() == SamplerFilterMode::Linear) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// An explicit LOD 0 makes lambda exactly 0, which is the magnification side of the
|
|
||||||
// min/mag decision. That only matches the implicit form when lambda could not have been
|
|
||||||
// positive anyway (the LOD clamp already pins it at or below 0), or when the two
|
|
||||||
// filters are the same and the choice cannot be observed.
|
|
||||||
const Float effectiveMaxLod = effectiveSampler->GetMipmapMode() == SamplerMipmapMode::None
|
|
||||||
? 0.0f
|
|
||||||
: effectiveSampler->GetMaxLod();
|
|
||||||
if (effectiveMaxLod > 0.0f && effectiveSampler->GetMinFilter() != effectiveSampler->GetMagFilter()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
sawSampler = true;
|
|
||||||
}
|
|
||||||
return sawSampler;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool UniformManager::ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
|
Bool UniformManager::ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
|
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
|
||||||
@@ -1050,11 +911,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
VkDescriptorPoolCreateInfo poolInfo{};
|
VkDescriptorPoolCreateInfo poolInfo{};
|
||||||
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||||
// FREE_DESCRIPTOR_SET_BIT lets a destroyed layout's cached sets be freed back
|
|
||||||
// (OnDescriptorSetLayoutDestroyed) so program churn recycles pool capacity.
|
|
||||||
// The cost is on set allocation only, which happens when a layout's per-frame
|
|
||||||
// cache grows - never on the per-draw reuse path.
|
|
||||||
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
|
|
||||||
poolInfo.maxSets = maxSets;
|
poolInfo.maxSets = maxSets;
|
||||||
poolInfo.poolSizeCount = static_cast<Uint32>(std::size(poolSizes));
|
poolInfo.poolSizeCount = static_cast<Uint32>(std::size(poolSizes));
|
||||||
poolInfo.pPoolSizes = poolSizes;
|
poolInfo.pPoolSizes = poolSizes;
|
||||||
@@ -1134,7 +990,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
auto& frame = m_frames[frameIndex];
|
auto& frame = m_frames[frameIndex];
|
||||||
auto& cache = frame.descriptorSetCacheByLayout[programObj.descriptorSetLayout];
|
auto& cache = frame.descriptorSetCacheByLayout[programObj.descriptorSetLayout];
|
||||||
if (cache.cursor < cache.sets.size()) {
|
if (cache.cursor < cache.sets.size()) {
|
||||||
outDescriptorSet = cache.sets[cache.cursor++].set;
|
outDescriptorSet = cache.sets[cache.cursor++];
|
||||||
} else {
|
} else {
|
||||||
VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
|
VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
|
||||||
if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) {
|
if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) {
|
||||||
@@ -1148,9 +1004,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return allocResult;
|
return allocResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The successful allocation came from the bucket the alloc helper left
|
cache.sets.push_back(outDescriptorSet);
|
||||||
// active; record it so a layout-destroyed purge can free the set back.
|
|
||||||
cache.sets.push_back({outDescriptorSet, frame.descriptorPools[frame.activeDescriptorPoolIndex].handle});
|
|
||||||
++cache.cursor;
|
++cache.cursor;
|
||||||
MGLOG_D("UniformDescriptorBinder: cached descriptor set count for frame=%u grew to %zu", frameIndex,
|
MGLOG_D("UniformDescriptorBinder: cached descriptor set count for frame=%u grew to %zu", frameIndex,
|
||||||
cache.sets.size());
|
cache.sets.size());
|
||||||
|
|||||||
@@ -39,17 +39,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void Shutdown();
|
void Shutdown();
|
||||||
|
|
||||||
void BeginFrame(Uint32 frameIndex);
|
void BeginFrame(Uint32 frameIndex);
|
||||||
// A ProgramFactory eviction just destroyed this layout: purge every frame
|
|
||||||
// slot's cached descriptor sets for it, so a recycled handle value can never
|
|
||||||
// stale-hit sets written for the dead layout's bindings. The sets are
|
|
||||||
// vkFreeDescriptorSets'd back to their pools (created with
|
|
||||||
// FREE_DESCRIPTOR_SET_BIT) and the pool accounting is credited, so program
|
|
||||||
// churn recycles pool capacity instead of abandoning it. GPU-safe: the layout
|
|
||||||
// only dies after >1024 idle frame boundaries, so no in-flight command buffer
|
|
||||||
// references its sets. This is the only eviction path for the per-layout
|
|
||||||
// caches - a live layout's entry must never be purged (its sets would be
|
|
||||||
// unreachable pool slots), so there is deliberately no age-based sweep here.
|
|
||||||
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
|
|
||||||
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
Vector<MG_State::GLState::ITextureObject*>& outTextures);
|
Vector<MG_State::GLState::ITextureObject*>& outTextures);
|
||||||
@@ -69,25 +58,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
|
static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
|
||||||
VkFormat resourceFormat, Bool useBindingFormat);
|
VkFormat resourceFormat, Bool useBindingFormat);
|
||||||
|
|
||||||
// True when the program reads at least one sampler and every one of them is bound to a
|
|
||||||
// texture whose GL level range is a single level. Such a sampler resolves to
|
|
||||||
// minLod = maxLod = 0 (see VkSamplerManager::GetOrCreateSampler), so an implicit-LOD sample
|
|
||||||
// and an explicit LOD 0 sample must read the same texel - which is what makes the
|
|
||||||
// ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads
|
|
||||||
// only GL state, so a texture that ends up single-level for another reason (one uploaded
|
|
||||||
// level under a wide level range) merely misses the rewrite.
|
|
||||||
// True when every texture this program samples is an 8-bit-or-less normalized format, so
|
|
||||||
// relaxing the fragment stage to fp16 cannot lose a bit the texel ever carried. Says
|
|
||||||
// nothing about the render target - the caller must check that too.
|
|
||||||
static Bool ProgramSamplesOnlyLowPrecisionTextures(const MG_State::GLState::ProgramObject& program,
|
|
||||||
const ProgramFactory::VkProgramObject& programObj);
|
|
||||||
// True when every colour attachment the draw writes is an 8-bit-or-less normalized
|
|
||||||
// format (nullptr = default framebuffer, which is). Blending happens at attachment
|
|
||||||
// precision, so a wider target must keep the fragment stage at full precision.
|
|
||||||
static Bool DrawTargetIsLowPrecision(const MG_State::GLState::FramebufferObject* drawFramebuffer);
|
|
||||||
static Bool ProgramSamplesOnlySingleLevelTextures(const MG_State::GLState::ProgramObject& program,
|
|
||||||
const ProgramFactory::VkProgramObject& programObj);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct DescriptorPoolBucket {
|
struct DescriptorPoolBucket {
|
||||||
VkDescriptorPool handle = VK_NULL_HANDLE;
|
VkDescriptorPool handle = VK_NULL_HANDLE;
|
||||||
@@ -95,16 +65,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 allocatedSets = 0;
|
Uint32 allocatedSets = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// A cached descriptor set together with the pool it was allocated from, so a
|
|
||||||
// layout-destroyed purge can vkFreeDescriptorSets it back and credit the
|
|
||||||
// owning bucket's accounting.
|
|
||||||
struct CachedDescriptorSet {
|
|
||||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
|
||||||
VkDescriptorPool pool = VK_NULL_HANDLE;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct DescriptorSetCacheEntry {
|
struct DescriptorSetCacheEntry {
|
||||||
Vector<CachedDescriptorSet> sets;
|
Vector<VkDescriptorSet> sets;
|
||||||
Uint32 cursor = 0;
|
Uint32 cursor = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -210,7 +172,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 samplerLifetimeId = 0;
|
Uint64 samplerLifetimeId = 0;
|
||||||
Uint64 textureLifetimeId = 0;
|
Uint64 textureLifetimeId = 0;
|
||||||
VkSampler sampler = VK_NULL_HANDLE;
|
VkSampler sampler = VK_NULL_HANDLE;
|
||||||
Uint32 viewLevelCount = 0;
|
|
||||||
Uint16 samplerVersion = 0;
|
Uint16 samplerVersion = 0;
|
||||||
Uint16 textureParamsVersion = 0;
|
Uint16 textureParamsVersion = 0;
|
||||||
Bool forceNearestFiltering = false;
|
Bool forceNearestFiltering = false;
|
||||||
|
|||||||
@@ -32,13 +32,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
|
||||||
|
|
||||||
// The buffer's heap address is an identity component of the key: a freed
|
|
||||||
// buffer's reused address can alias an old cache entry, but only under a
|
|
||||||
// byte-identical attribute layout - and the entry payload is a pure function
|
|
||||||
// of the hashed inputs, with the draw path re-resolving bindingBufferKeys
|
|
||||||
// against the live VAO attribute pointers, so an aliased hit returns exactly
|
|
||||||
// what a rebuild would. Address drift only grows the map; the OnFrameBoundary
|
|
||||||
// aging sweep bounds that.
|
|
||||||
const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get());
|
const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get());
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
||||||
}
|
}
|
||||||
@@ -65,7 +58,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
|
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
|
||||||
auto it = m_cache.find(hash);
|
auto it = m_cache.find(hash);
|
||||||
if (it != m_cache.end()) {
|
if (it != m_cache.end()) {
|
||||||
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
|
||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +166,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
auto& entry = m_cache[hash];
|
auto& entry = m_cache[hash];
|
||||||
entry.hash = hash;
|
entry.hash = hash;
|
||||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
|
||||||
entry.bindings = builder.GetBindings();
|
entry.bindings = builder.GetBindings();
|
||||||
entry.attributes = builder.GetAttributes();
|
entry.attributes = builder.GetAttributes();
|
||||||
entry.bindingBufferKeys = std::move(bindingBufferKeys);
|
entry.bindingBufferKeys = std::move(bindingBufferKeys);
|
||||||
@@ -189,30 +180,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
void VertexInputStateFactory::OnFrameBoundary() {
|
|
||||||
++m_frameBoundaryCounter;
|
|
||||||
|
|
||||||
// Sweep occasionally; evict entries whose last hit is far in the past.
|
|
||||||
// Erasure happens only here, never mid-frame: the draw path holds a
|
|
||||||
// reference into the current entry across its setup, and unordered_map
|
|
||||||
// erase would invalidate it. Entries are CPU-side only, so no GPU-idle
|
|
||||||
// proof is needed; an evicted entry that is used again is simply rebuilt
|
|
||||||
// from the VAO state (same hash, same content).
|
|
||||||
constexpr Uint64 kSweepInterval = 256;
|
|
||||||
constexpr Uint64 kRetireAgeBoundaries = 1024;
|
|
||||||
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto it = m_cache.begin(); it != m_cache.end();) {
|
|
||||||
if (m_frameBoundaryCounter - it->second.lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
|
||||||
it = m_cache.erase(it);
|
|
||||||
} else {
|
|
||||||
++it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
|
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
|
||||||
Bool isBgra) {
|
Bool isBgra) {
|
||||||
if (isBgra) {
|
if (isBgra) {
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
struct BackendVertexInputState {
|
struct BackendVertexInputState {
|
||||||
HashType hash = 0;
|
HashType hash = 0;
|
||||||
// Frame boundary of the last cache hit; entries idle past the
|
|
||||||
// OnFrameBoundary retirement age are evicted (CPU heap only).
|
|
||||||
Uint64 lastUsedFrameBoundary = 0;
|
|
||||||
Vector<VkVertexInputBindingDescription> bindings;
|
Vector<VkVertexInputBindingDescription> bindings;
|
||||||
Vector<VkVertexInputAttributeDescription> attributes;
|
Vector<VkVertexInputAttributeDescription> attributes;
|
||||||
Vector<SizeT> bindingBufferKeys;
|
Vector<SizeT> bindingBufferKeys;
|
||||||
@@ -58,14 +55,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const BackendVertexInputState& GetOrCreateVertexInputState(
|
const BackendVertexInputState& GetOrCreateVertexInputState(
|
||||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
|
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
|
||||||
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
|
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
|
||||||
// Frame boundary hook: ages the cache and evicts entries not hit for many
|
|
||||||
// frames. The key mixes buffer heap addresses, so buffer/VAO churn keeps
|
|
||||||
// minting fresh keys; without eviction the map grows for the whole session.
|
|
||||||
// Entries hold no Vulkan handles (pipeline creation copies the descriptions)
|
|
||||||
// and the draw path's entry reference never spans a frame boundary, so
|
|
||||||
// eviction here needs no GPU-idle proof. Self-gated: one counter bump and
|
|
||||||
// compare except on sweep boundaries.
|
|
||||||
void OnFrameBoundary();
|
|
||||||
static SizeT GetComponentSize(DataType type);
|
static SizeT GetComponentSize(DataType type);
|
||||||
// Tightly-packed byte size of one vertex element for this attribute: componentSize * size for
|
// Tightly-packed byte size of one vertex element for this attribute: componentSize * size for
|
||||||
// normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for
|
// normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for
|
||||||
@@ -81,8 +70,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const VulkanRendererConfig& m_config;
|
const VulkanRendererConfig& m_config;
|
||||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||||
UnorderedMap<HashType, BackendVertexInputState> m_cache;
|
UnorderedMap<HashType, BackendVertexInputState> m_cache;
|
||||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
|
||||||
Uint64 m_frameBoundaryCounter = 0;
|
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -141,15 +141,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_transientUploadArena.BeginFrame(frameIndex);
|
m_transientUploadArena.BeginFrame(frameIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VkBufferManager::CollectAllDeferredReleases() {
|
|
||||||
for (Uint32 frameIndex = 0; frameIndex < m_deferredBufferReleases.size(); ++frameIndex) {
|
|
||||||
CollectDeferredReleases(frameIndex);
|
|
||||||
}
|
|
||||||
for (Uint32 frameIndex = 0; frameIndex < m_transientUploadArena.GetFrameCount(); ++frameIndex) {
|
|
||||||
m_transientUploadArena.CollectDeferredReleases(frameIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void VkBufferManager::NotifyDeviceIdle() {
|
void VkBufferManager::NotifyDeviceIdle() {
|
||||||
// Everything submitted so far has completed. Work recorded for the
|
// Everything submitted so far has completed. Work recorded for the
|
||||||
// current frame has not been submitted yet, so the current serial
|
// current frame has not been submitted yet, so the current serial
|
||||||
|
|||||||
@@ -77,11 +77,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Recreate all per-frame transient arenas
|
// Recreate all per-frame transient arenas
|
||||||
Bool RecreateTransientArenas(Uint32 frameCount);
|
Bool RecreateTransientArenas(Uint32 frameCount);
|
||||||
void BeginFrame(Uint32 frameIndex);
|
void BeginFrame(Uint32 frameIndex);
|
||||||
// Drains every frame slot's deferred buffer/resource releases (and the
|
|
||||||
// transient arena's parked superseded blocks). Only valid when the
|
|
||||||
// caller has proven every queue submission complete; used by the
|
|
||||||
// present-less frame-boundary drain.
|
|
||||||
void CollectAllDeferredReleases();
|
|
||||||
// All previously submitted GPU work has completed (vkDeviceWaitIdle).
|
// All previously submitted GPU work has completed (vkDeviceWaitIdle).
|
||||||
void NotifyDeviceIdle();
|
void NotifyDeviceIdle();
|
||||||
// A frame slot's submission fence has been waited: every serial up to
|
// A frame slot's submission fence has been waited: every serial up to
|
||||||
|
|||||||
@@ -180,7 +180,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||||
internalFormat = TextureInternalFormat::Unknown;
|
internalFormat = TextureInternalFormat::Unknown;
|
||||||
samples = 0;
|
samples = 0;
|
||||||
deadSinceFrame = kNeverObservedDead;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VkRenderPassManager::VkRenderPassManager(VkDevice device,
|
VkRenderPassManager::VkRenderPassManager(VkDevice device,
|
||||||
@@ -207,7 +206,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
resource.Destroy(m_device, m_allocator);
|
resource.Destroy(m_device, m_allocator);
|
||||||
}
|
}
|
||||||
m_renderbufferResources.clear();
|
m_renderbufferResources.clear();
|
||||||
CollectDeferredRenderbufferReleases(/*destroyAll=*/true); // caller guarantees device idle
|
|
||||||
m_pendingRenderbufferClears.clear();
|
m_pendingRenderbufferClears.clear();
|
||||||
RenderPassEntry::s_textureResourcesScratch.clear();
|
RenderPassEntry::s_textureResourcesScratch.clear();
|
||||||
s_activeRenderPass = {};
|
s_activeRenderPass = {};
|
||||||
@@ -215,75 +213,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_rpFastValid = false;
|
m_rpFastValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint64 VkRenderPassManager::RetireAgeFrames() const {
|
|
||||||
// MaxFramesInFlight + 2 covers the frame ring plus one boundary for the
|
|
||||||
// recording-to-submit gap and one because OnPresent runs ahead of Present's
|
|
||||||
// fence wait; the floor of 8 keeps a margin over the default ring of 3 while
|
|
||||||
// still releasing multi-MB attachment memory promptly (the render-pass cache's
|
|
||||||
// 1024-frame retirement would pin it for no additional safety).
|
|
||||||
return std::max<Uint64>(8, static_cast<Uint64>(m_config.MaxFramesInFlight) + 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
void VkRenderPassManager::DeferRenderbufferBackingRelease(RenderbufferResource& resource) {
|
|
||||||
// The superseded backing may still be referenced by in-flight command buffers
|
|
||||||
// (glRenderbufferStorage can respecify a renderbuffer drawn this very frame),
|
|
||||||
// so it is parked and destroyed only after RetireAgeFrames() boundaries.
|
|
||||||
if (resource.image == VK_NULL_HANDLE && resource.view == VK_NULL_HANDLE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_deferredRenderbufferReleases.push_back({resource.image, resource.allocation, resource.view, m_frameCounter});
|
|
||||||
resource.image = VK_NULL_HANDLE;
|
|
||||||
resource.allocation = nullptr;
|
|
||||||
resource.view = VK_NULL_HANDLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
void VkRenderPassManager::CollectDeferredRenderbufferReleases(Bool destroyAll) {
|
|
||||||
if (m_deferredRenderbufferReleases.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const Uint64 retireAgeFrames = RetireAgeFrames();
|
|
||||||
std::erase_if(m_deferredRenderbufferReleases, [&](DeferredRenderbufferRelease& release) {
|
|
||||||
if (!destroyAll && m_frameCounter - release.deferredAtFrame < retireAgeFrames) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (release.view != VK_NULL_HANDLE) {
|
|
||||||
vkDestroyImageView(m_device, release.view, nullptr);
|
|
||||||
}
|
|
||||||
if (release.image != VK_NULL_HANDLE) {
|
|
||||||
vmaDestroyImage(m_allocator, release.image, release.allocation);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void VkRenderPassManager::CollectRenderbufferGarbage() {
|
void VkRenderPassManager::CollectRenderbufferGarbage() {
|
||||||
// Two-phase reclamation: a dead renderbuffer's VkImage may still be referenced by
|
Vector<MG_State::GLState::RenderbufferObject*> deadRenderbuffers;
|
||||||
// command buffers submitted up to frames-in-flight frames ago (it was legally
|
deadRenderbuffers.reserve(m_renderbufferResources.size());
|
||||||
// attached and drawn right up to its deletion), so the first observation of an
|
for (auto& [renderbuffer, resource] : m_renderbufferResources) {
|
||||||
// expired weak reference only stamps the current frame counter; Destroy runs once
|
|
||||||
// enough frame boundaries have passed that the stamping frame's submission fence
|
|
||||||
// has provably been waited (see RetireAgeFrames).
|
|
||||||
const Uint64 retireAgeFrames = RetireAgeFrames();
|
|
||||||
for (auto it = m_renderbufferResources.begin(); it != m_renderbufferResources.end();) {
|
|
||||||
auto& resource = it->second;
|
|
||||||
const auto liveRenderbuffer = resource.renderbuffer.lock();
|
const auto liveRenderbuffer = resource.renderbuffer.lock();
|
||||||
if (liveRenderbuffer && liveRenderbuffer.get() == it->first) {
|
if (!liveRenderbuffer || liveRenderbuffer.get() != renderbuffer) {
|
||||||
resource.deadSinceFrame = RenderbufferResource::kNeverObservedDead;
|
deadRenderbuffers.emplace_back(renderbuffer);
|
||||||
++it;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
if (resource.deadSinceFrame == RenderbufferResource::kNeverObservedDead) {
|
}
|
||||||
resource.deadSinceFrame = m_frameCounter;
|
for (auto* renderbuffer : deadRenderbuffers) {
|
||||||
++it;
|
auto resourceIt = m_renderbufferResources.find(renderbuffer);
|
||||||
continue;
|
if (resourceIt != m_renderbufferResources.end()) {
|
||||||
|
resourceIt->second.Destroy(m_device, m_allocator);
|
||||||
|
m_renderbufferResources.erase(resourceIt);
|
||||||
}
|
}
|
||||||
if (m_frameCounter - resource.deadSinceFrame < retireAgeFrames) {
|
m_pendingRenderbufferClears.erase(renderbuffer);
|
||||||
++it;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
m_pendingRenderbufferClears.erase(it->first);
|
|
||||||
resource.Destroy(m_device, m_allocator);
|
|
||||||
it = m_renderbufferResources.erase(it);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,15 +270,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
resource.samples != renderbuffer->GetSamples();
|
resource.samples != renderbuffer->GetSamples();
|
||||||
if (!needsCreate) {
|
if (!needsCreate) {
|
||||||
resource.renderbuffer = renderbuffer;
|
resource.renderbuffer = renderbuffer;
|
||||||
// A new renderbuffer at a recycled address may adopt a compatible entry that
|
|
||||||
// was already stamped dead; it is alive again, so cancel the aging.
|
|
||||||
resource.deadSinceFrame = RenderbufferResource::kNeverObservedDead;
|
|
||||||
return &resource;
|
return &resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Respecify: park the old backing for aged destruction instead of destroying
|
|
||||||
// inline - it may still be referenced by in-flight command buffers.
|
|
||||||
DeferRenderbufferBackingRelease(resource);
|
|
||||||
resource.Destroy(m_device, m_allocator);
|
resource.Destroy(m_device, m_allocator);
|
||||||
resource.renderbuffer = renderbuffer;
|
resource.renderbuffer = renderbuffer;
|
||||||
|
|
||||||
@@ -1239,14 +1178,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void VkRenderPassManager::OnPresent() {
|
void VkRenderPassManager::OnPresent() {
|
||||||
++m_frameCounter;
|
++m_frameCounter;
|
||||||
|
|
||||||
// Runs every frame boundary, ahead of the render-pass sweep gate below: the walk
|
|
||||||
// is O(#renderbuffer resources) — single digits in practice — and per-frame
|
|
||||||
// invocation keeps dead-resource reclaim latency at the aging bound instead of
|
|
||||||
// coupling it to renderbuffer *use* (the GetOrCreateRenderbufferResource call
|
|
||||||
// site never runs again once an app stops using renderbuffers).
|
|
||||||
CollectRenderbufferGarbage();
|
|
||||||
CollectDeferredRenderbufferReleases(/*destroyAll=*/false);
|
|
||||||
|
|
||||||
// Sweep occasionally; evict entries whose last use is far past every
|
// Sweep occasionally; evict entries whose last use is far past every
|
||||||
// in-flight frame so their VkRenderPass/VkFramebuffer can be destroyed
|
// in-flight frame so their VkRenderPass/VkFramebuffer can be destroyed
|
||||||
// safely (RenderPassEntry's destructor releases the handles).
|
// safely (RenderPassEntry's destructor releases the handles).
|
||||||
@@ -1256,12 +1187,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect the dying handles and notify once after the loop: pipelines hashed
|
|
||||||
// on them share the entries' >kRetireAgeFrames idleness (they are only bound
|
|
||||||
// by draws that hit those entries), so the observer may destroy them
|
|
||||||
// immediately - and a single batched notification costs one pipeline-cache
|
|
||||||
// scan instead of one per evicted pass.
|
|
||||||
Vector<VkRenderPass> destroyedRenderPasses;
|
|
||||||
const Uint64 activeHash = s_hasActiveRenderPass ? s_activeRenderPass.hash : 0;
|
const Uint64 activeHash = s_hasActiveRenderPass ? s_activeRenderPass.hash : 0;
|
||||||
for (auto it = m_renderPasses.begin(); it != m_renderPasses.end();) {
|
for (auto it = m_renderPasses.begin(); it != m_renderPasses.end();) {
|
||||||
const Bool isActive = s_hasActiveRenderPass && it->first == activeHash;
|
const Bool isActive = s_hasActiveRenderPass && it->first == activeHash;
|
||||||
@@ -1269,15 +1194,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (m_rpFastValid && m_rpFastRenderPassHash == it->first) {
|
if (m_rpFastValid && m_rpFastRenderPassHash == it->first) {
|
||||||
m_rpFastValid = false;
|
m_rpFastValid = false;
|
||||||
}
|
}
|
||||||
destroyedRenderPasses.push_back(it->second.renderPass);
|
|
||||||
it = m_renderPasses.erase(it);
|
it = m_renderPasses.erase(it);
|
||||||
} else {
|
} else {
|
||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!destroyedRenderPasses.empty() && m_evictionObserver != nullptr) {
|
|
||||||
m_evictionObserver->OnRenderPassesDestroyed(destroyedRenderPasses);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool VkRenderPassManager::BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry) {
|
Bool VkRenderPassManager::BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry) {
|
||||||
|
|||||||
@@ -157,31 +157,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
class VkRenderPassManager {
|
class VkRenderPassManager {
|
||||||
public:
|
public:
|
||||||
using HashType = Uint64;
|
using HashType = Uint64;
|
||||||
|
|
||||||
// Notified once per OnPresent sweep with every aged-out entry's VkRenderPass
|
|
||||||
// value: pipelines are hashed on the raw handle, and once destroyed the value
|
|
||||||
// may be recycled for an incompatible pass, so dependent caches must purge
|
|
||||||
// everything keyed on them before any new pass can be created (the sweep and
|
|
||||||
// the notification run back-to-back with no creation in between; observers
|
|
||||||
// compare the values, never dereference them). Batched so a mass-idle cohort
|
|
||||||
// (shader-pack switch, dimension exit) costs the observer one pipeline-cache
|
|
||||||
// scan, not one per dying pass. The wholesale paths
|
|
||||||
// (Shutdown/RecreateSwapchain) do not notify - their callers already drop
|
|
||||||
// every pipeline outright.
|
|
||||||
class IEvictionObserver {
|
|
||||||
public:
|
|
||||||
virtual ~IEvictionObserver() = default;
|
|
||||||
virtual void OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
VkRenderPassManager(VkDevice device,
|
VkRenderPassManager(VkDevice device,
|
||||||
VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config,
|
VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config,
|
||||||
VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject);
|
VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject);
|
||||||
~VkRenderPassManager();
|
~VkRenderPassManager();
|
||||||
|
|
||||||
// Observer may be null (no notifications). Not owned.
|
|
||||||
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
|
|
||||||
|
|
||||||
Bool Initialize();
|
Bool Initialize();
|
||||||
void Shutdown();
|
void Shutdown();
|
||||||
|
|
||||||
@@ -212,7 +192,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
UnorderedMap<Uint64, RenderPassEntry> m_renderPasses;
|
UnorderedMap<Uint64, RenderPassEntry> m_renderPasses;
|
||||||
// Monotonic frame counter (bumped in OnPresent) for render-pass cache aging.
|
// Monotonic frame counter (bumped in OnPresent) for render-pass cache aging.
|
||||||
Uint64 m_frameCounter = 0;
|
Uint64 m_frameCounter = 0;
|
||||||
IEvictionObserver* m_evictionObserver = nullptr;
|
|
||||||
|
|
||||||
// Bumped whenever a renderbuffer VkImage is (re)created; together with the texture
|
// Bumped whenever a renderbuffer VkImage is (re)created; together with the texture
|
||||||
// manager's image epoch this invalidates the render-pass fast path on any attachment
|
// manager's image epoch this invalidates the render-pass fast path on any attachment
|
||||||
@@ -234,12 +213,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
struct RenderbufferResource {
|
struct RenderbufferResource {
|
||||||
// deadSinceFrame sentinel: the owning weak reference has not been observed
|
|
||||||
// expired. Dead resources age past every in-flight frame before Destroy
|
|
||||||
// (see CollectRenderbufferGarbage); the GPU may still reference the image
|
|
||||||
// for frames-in-flight frames after the GL object dies.
|
|
||||||
static constexpr Uint64 kNeverObservedDead = UINT64_MAX;
|
|
||||||
|
|
||||||
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
|
||||||
VkImage image = VK_NULL_HANDLE;
|
VkImage image = VK_NULL_HANDLE;
|
||||||
VmaAllocation allocation = nullptr;
|
VmaAllocation allocation = nullptr;
|
||||||
@@ -251,8 +224,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||||
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
||||||
Int samples = 0;
|
Int samples = 0;
|
||||||
// m_frameCounter value at which the weak reference was first seen expired.
|
|
||||||
Uint64 deadSinceFrame = kNeverObservedDead;
|
|
||||||
|
|
||||||
void Destroy(VkDevice device, VmaAllocator allocator);
|
void Destroy(VkDevice device, VmaAllocator allocator);
|
||||||
};
|
};
|
||||||
@@ -270,28 +241,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ClearAttachmentPayload payload{};
|
ClearAttachmentPayload payload{};
|
||||||
};
|
};
|
||||||
|
|
||||||
// A superseded renderbuffer backing (glRenderbufferStorage respecify) parked
|
|
||||||
// until enough frame boundaries have passed that no in-flight command buffer
|
|
||||||
// can still reference it; destroyed in OnPresent (see RetireAgeFrames).
|
|
||||||
struct DeferredRenderbufferRelease {
|
|
||||||
VkImage image = VK_NULL_HANDLE;
|
|
||||||
VmaAllocation allocation = nullptr;
|
|
||||||
VkImageView view = VK_NULL_HANDLE;
|
|
||||||
Uint64 deferredAtFrame = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
||||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
||||||
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
|
||||||
|
|
||||||
Bool HasPendingRenderbufferClear(
|
Bool HasPendingRenderbufferClear(
|
||||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
|
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
|
||||||
void CollectRenderbufferGarbage();
|
void CollectRenderbufferGarbage();
|
||||||
// Frame-boundary margin after which a resource last referenced by a retired
|
|
||||||
// GL object (or superseded backing) is provably past every in-flight frame.
|
|
||||||
Uint64 RetireAgeFrames() const;
|
|
||||||
void DeferRenderbufferBackingRelease(RenderbufferResource& resource);
|
|
||||||
void CollectDeferredRenderbufferReleases(Bool destroyAll);
|
|
||||||
|
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
static inline ActiveRenderPassInfo s_activeRenderPass{};
|
static inline ActiveRenderPassInfo s_activeRenderPass{};
|
||||||
|
|||||||
@@ -51,18 +51,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) {
|
Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) {
|
||||||
return std::min(sampler.GetMinLod(), effectiveMaxLod);
|
return std::min(sampler.GetMinLod(), effectiveMaxLod);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A single-level view can only ever deliver the base level, but the LOD clamp must not be
|
|
||||||
// collapsed to exactly 0: both GL and Vulkan pick magFilter over minFilter from the
|
|
||||||
// *clamped* lambda, so maxLod = 0 would make every fragment magnify and quietly retire the
|
|
||||||
// min filter. 0.25 is the value VkSamplerCreateInfo's own note prescribes for emulating
|
|
||||||
// GL's non-mipmapped minification - large enough for lambda to stay positive, small enough
|
|
||||||
// that a NEAREST mip mode still rounds down to level 0. Clamped rather than assigned, so a
|
|
||||||
// texture whose GL_TEXTURE_MAX_LOD really is 0 keeps magnifying as GL says it must.
|
|
||||||
Float ResolveSingleLevelMaxLod(const MG_State::GLState::SamplerObject& sampler, Bool singleLevelView) {
|
|
||||||
const Float maxLod = ResolveEffectiveMaxLod(sampler);
|
|
||||||
return singleLevelView ? std::min(maxLod, 0.25f) : maxLod;
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Bool VkSamplerManager::Initialize(const InitInfo& initInfo) {
|
Bool VkSamplerManager::Initialize(const InitInfo& initInfo) {
|
||||||
@@ -101,43 +89,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
m_device = VK_NULL_HANDLE;
|
m_device = VK_NULL_HANDLE;
|
||||||
m_config = nullptr;
|
m_config = nullptr;
|
||||||
m_frameBoundaryCounter = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void VkSamplerManager::OnFrameBoundary() {
|
|
||||||
++m_frameBoundaryCounter;
|
|
||||||
|
|
||||||
// Sweep occasionally; destroy samplers whose last use is far past every
|
|
||||||
// in-flight frame. Destroy and erase must stay atomic, or Shutdown would
|
|
||||||
// double-free the handle; an evicted key that recurs simply re-creates
|
|
||||||
// its sampler on the next miss.
|
|
||||||
constexpr Uint64 kSweepInterval = 256;
|
|
||||||
constexpr Uint64 kRetireAgeBoundaries = 1024;
|
|
||||||
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto it = m_samplers.begin(); it != m_samplers.end();) {
|
|
||||||
auto& entry = it->second;
|
|
||||||
if (m_frameBoundaryCounter - entry.lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
|
||||||
if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) {
|
|
||||||
vkDestroySampler(m_device, entry.handle, nullptr);
|
|
||||||
}
|
|
||||||
it = m_samplers.erase(it);
|
|
||||||
} else {
|
|
||||||
++it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||||
const MG_State::GLState::ITextureObject& texture,
|
const MG_State::GLState::ITextureObject& texture,
|
||||||
Bool forceNearestFiltering, Bool singleLevelView) const {
|
Bool forceNearestFiltering) const {
|
||||||
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
|
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
|
||||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
|
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
|
||||||
|
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &singleLevelView, sizeof(singleLevelView)));
|
|
||||||
|
|
||||||
const auto minFilter = sampler.GetMinFilter();
|
const auto minFilter = sampler.GetMinFilter();
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
|
||||||
@@ -151,7 +111,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapT, sizeof(wrapT)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapT, sizeof(wrapT)));
|
||||||
const auto wrapR = sampler.GetWrapR();
|
const auto wrapR = sampler.GetWrapR();
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapR, sizeof(wrapR)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapR, sizeof(wrapR)));
|
||||||
const auto maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
|
const auto maxLod = ResolveEffectiveMaxLod(sampler);
|
||||||
const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
|
const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod)));
|
||||||
@@ -173,20 +133,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||||
const MG_State::GLState::ITextureObject& texture,
|
const MG_State::GLState::ITextureObject& texture,
|
||||||
Bool forceNearestFiltering, Uint32 viewLevelCount) {
|
Bool forceNearestFiltering) {
|
||||||
// A view that exposes a single mip level has no second level to blend with, so GL's
|
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering);
|
||||||
// *_MIPMAP_* minification filters degenerate to plain filtering on the base level -
|
|
||||||
// sampling is unchanged by pinning the Vulkan sampler to NEAREST mip mode at LOD 0.
|
|
||||||
// It is not cosmetic: MobileGL backs such a view with a fully allocated mip chain whose
|
|
||||||
// tail is never written, and a LINEAR mip mode lets the texture unit issue the level+1
|
|
||||||
// fetch anyway. On Adreno that fetch lands in uninitialized UBWC pages (or past the
|
|
||||||
// allocation for a genuinely single-level image) and faults the GPU - the same failure
|
|
||||||
// the default-framebuffer blit shader had to work around with an explicit-LOD sample.
|
|
||||||
const Bool singleLevelView = viewLevelCount == 1;
|
|
||||||
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering, singleLevelView);
|
|
||||||
auto it = m_samplers.find(key);
|
auto it = m_samplers.find(key);
|
||||||
if (it != m_samplers.end()) {
|
if (it != m_samplers.end()) {
|
||||||
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
|
||||||
return it->second.handle;
|
return it->second.handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,9 +144,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
||||||
samplerInfo.magFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMagFilter());
|
samplerInfo.magFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMagFilter());
|
||||||
samplerInfo.minFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMinFilter());
|
samplerInfo.minFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMinFilter());
|
||||||
samplerInfo.mipmapMode = (forceNearestFiltering || singleLevelView)
|
samplerInfo.mipmapMode = forceNearestFiltering ? VK_SAMPLER_MIPMAP_MODE_NEAREST
|
||||||
? VK_SAMPLER_MIPMAP_MODE_NEAREST
|
: ToVkMipmapMode(sampler.GetMipmapMode());
|
||||||
: ToVkMipmapMode(sampler.GetMipmapMode());
|
|
||||||
samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS());
|
samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS());
|
||||||
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
|
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
|
||||||
samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR());
|
samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR());
|
||||||
@@ -208,8 +157,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
samplerInfo.maxAnisotropy = maxAnisotropy;
|
samplerInfo.maxAnisotropy = maxAnisotropy;
|
||||||
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
|
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
|
||||||
samplerInfo.compareOp = ToVkCompareOp(ResolveCompareFunc(sampler, texture));
|
samplerInfo.compareOp = ToVkCompareOp(ResolveCompareFunc(sampler, texture));
|
||||||
// Must match BuildSamplerKey's resolution exactly.
|
samplerInfo.maxLod = ResolveEffectiveMaxLod(sampler);
|
||||||
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
|
|
||||||
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
|
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
|
||||||
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture);
|
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture);
|
||||||
samplerInfo.unnormalizedCoordinates = VK_FALSE;
|
samplerInfo.unnormalizedCoordinates = VK_FALSE;
|
||||||
@@ -221,7 +169,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
entry.handle = vkSampler;
|
entry.handle = vkSampler;
|
||||||
entry.externalIndex = sampler.GetExternalIndex();
|
entry.externalIndex = sampler.GetExternalIndex();
|
||||||
entry.version = sampler.GetVersion();
|
entry.version = sampler.GetVersion();
|
||||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
|
||||||
m_samplers[key] = entry;
|
m_samplers[key] = entry;
|
||||||
return vkSampler;
|
return vkSampler;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,38 +33,20 @@ public:
|
|||||||
Bool Initialize(const InitInfo& initInfo);
|
Bool Initialize(const InitInfo& initInfo);
|
||||||
void Shutdown();
|
void Shutdown();
|
||||||
|
|
||||||
// viewLevelCount is the mip-level count of the image view this sampler will be paired
|
|
||||||
// with; 0 means "unknown, do not narrow". See GetOrCreateSampler for why it matters.
|
|
||||||
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||||
const MG_State::GLState::ITextureObject& texture,
|
const MG_State::GLState::ITextureObject& texture,
|
||||||
Bool forceNearestFiltering = false,
|
Bool forceNearestFiltering = false);
|
||||||
Uint32 viewLevelCount = 0);
|
|
||||||
// Frame boundary hook: ages the sampler cache and destroys samplers not used
|
|
||||||
// for many frames. The key hashes continuous float state (lodBias, LOD clamps,
|
|
||||||
// anisotropy), so an app animating those would otherwise mint an unbounded
|
|
||||||
// stream of never-destroyed VkSamplers and eventually exhaust the device's
|
|
||||||
// maxSamplerAllocationCount. A sampler idle for over a thousand frame
|
|
||||||
// boundaries cannot be referenced by any in-flight command buffer (frames in
|
|
||||||
// flight are single digits), and every descriptor set the GPU consumes is
|
|
||||||
// written that same frame with live handles (the per-binding resolve memo and
|
|
||||||
// descriptor-set reuse are both frame-reset), so destruction here needs no
|
|
||||||
// fence wait. Self-gated: one counter bump and compare except on sweep
|
|
||||||
// boundaries.
|
|
||||||
void OnFrameBoundary();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct SamplerCacheEntry {
|
struct SamplerCacheEntry {
|
||||||
VkSampler handle = VK_NULL_HANDLE;
|
VkSampler handle = VK_NULL_HANDLE;
|
||||||
Uint externalIndex = 0;
|
Uint externalIndex = 0;
|
||||||
Uint16 version = 0;
|
Uint16 version = 0;
|
||||||
// Frame boundary of the last cache hit; entries idle past the
|
|
||||||
// OnFrameBoundary retirement age have their VkSampler destroyed.
|
|
||||||
Uint64 lastUsedFrameBoundary = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||||
const MG_State::GLState::ITextureObject& texture,
|
const MG_State::GLState::ITextureObject& texture,
|
||||||
Bool forceNearestFiltering, Bool singleLevelView) const;
|
Bool forceNearestFiltering) const;
|
||||||
static VkFilter ToVkFilter(SamplerFilterMode mode);
|
static VkFilter ToVkFilter(SamplerFilterMode mode);
|
||||||
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
|
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
|
||||||
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
|
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
|
||||||
@@ -85,8 +67,6 @@ private:
|
|||||||
Bool m_samplerAnisotropySupported = false;
|
Bool m_samplerAnisotropySupported = false;
|
||||||
Float m_maxSamplerAnisotropy = 1.0f;
|
Float m_maxSamplerAnisotropy = 1.0f;
|
||||||
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
|
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
|
||||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
|
||||||
Uint64 m_frameBoundaryCounter = 0;
|
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -587,7 +587,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_allocator = initInfo.allocator;
|
m_allocator = initInfo.allocator;
|
||||||
m_commandPool = initInfo.commandPool;
|
m_commandPool = initInfo.commandPool;
|
||||||
m_graphicsQueue = initInfo.graphicsQueue;
|
m_graphicsQueue = initInfo.graphicsQueue;
|
||||||
m_imageFormatListSupported = initInfo.imageFormatListSupported;
|
|
||||||
m_currentFrameIndex = 0;
|
m_currentFrameIndex = 0;
|
||||||
m_deferredReleases.clear();
|
m_deferredReleases.clear();
|
||||||
m_deferredReleases.resize(initInfo.frameCount);
|
m_deferredReleases.resize(initInfo.frameCount);
|
||||||
@@ -610,7 +609,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
DestroyDeferredReleases();
|
DestroyDeferredReleases();
|
||||||
m_textureResources.clear();
|
m_textureResources.clear();
|
||||||
m_aliveObjects.clear();
|
m_aliveObjects.clear();
|
||||||
m_storageImageTextures.clear();
|
|
||||||
|
|
||||||
m_device = VK_NULL_HANDLE;
|
m_device = VK_NULL_HANDLE;
|
||||||
m_physicalDevice = VK_NULL_HANDLE;
|
m_physicalDevice = VK_NULL_HANDLE;
|
||||||
@@ -629,26 +627,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
frameIndex, m_deferredViewReleases.size());
|
frameIndex, m_deferredViewReleases.size());
|
||||||
m_currentFrameIndex = frameIndex;
|
m_currentFrameIndex = frameIndex;
|
||||||
CollectDeferredReleases(frameIndex);
|
CollectDeferredReleases(frameIndex);
|
||||||
|
|
||||||
// Frame-boundary GC: every 64 frame boundaries (~1 s at 60 fps) bounds the reclaim
|
|
||||||
// latency for dead textures regardless of draw traffic — workloads that churn
|
|
||||||
// textures through clears/readbacks alone never reach the draw-gated
|
|
||||||
// CollectGarbage. Must run after CollectDeferredReleases above: the prune defers
|
|
||||||
// its releases into this frame's slot, which was just drained, so they are
|
|
||||||
// destroyed only after the slot's fence has been waited again one full frame-ring
|
|
||||||
// cycle from now (never while an in-flight frame may still reference them).
|
|
||||||
constexpr Uint32 kGcFrameInterval = 64;
|
|
||||||
++m_gcFrameCounter;
|
|
||||||
if (m_gcFrameCounter % kGcFrameInterval == 0) {
|
|
||||||
PruneDeadTextures();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void VkTextureManager::CollectAllDeferredReleases() {
|
|
||||||
const SizeT frameCount = std::min(m_deferredReleases.size(), m_deferredViewReleases.size());
|
|
||||||
for (SizeT frameIndex = 0; frameIndex < frameCount; ++frameIndex) {
|
|
||||||
CollectDeferredReleases(static_cast<Uint32>(frameIndex));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void VkTextureManager::EraseTrackedTexture(const TextureIdentity& identity) {
|
void VkTextureManager::EraseTrackedTexture(const TextureIdentity& identity) {
|
||||||
@@ -658,7 +636,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_textureResources.erase(resourceIt);
|
m_textureResources.erase(resourceIt);
|
||||||
}
|
}
|
||||||
m_aliveObjects.erase(identity);
|
m_aliveObjects.erase(identity);
|
||||||
m_storageImageTextures.erase(identity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void VkTextureManager::PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture) {
|
void VkTextureManager::PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture) {
|
||||||
@@ -725,22 +702,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// construction introduces a new identity. Doing this unconditionally made every
|
// construction introduces a new identity. Doing this unconditionally made every
|
||||||
// sampled-texture sync scan the entire alive-texture map per draw.
|
// sampled-texture sync scan the entire alive-texture map per draw.
|
||||||
if (aliveIt == m_aliveObjects.end()) {
|
if (aliveIt == m_aliveObjects.end()) {
|
||||||
WeakPtr<MG_State::GLState::ITextureObject> aliveTexture;
|
|
||||||
const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());
|
const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());
|
||||||
if (liveTexture && liveTexture.get() == &texture) {
|
if (liveTexture && liveTexture.get() == &texture) {
|
||||||
aliveTexture = liveTexture;
|
m_aliveObjects[identity] = WeakPtr<MG_State::GLState::ITextureObject>(liveTexture);
|
||||||
} else {
|
|
||||||
// The name lookup legally fails while the object is alive: the name was
|
|
||||||
// deleted with the texture still attached to an FBO (the attachment's
|
|
||||||
// SharedPtr keeps it alive), or the name was reused by a new texture, or
|
|
||||||
// this is a default texture object (name 0 lives outside the name map).
|
|
||||||
// Register through the object's own control block so the resource created
|
|
||||||
// below still participates in weak-expiry GC instead of becoming an
|
|
||||||
// orphan no reclamation path can reach until Shutdown.
|
|
||||||
aliveTexture = texture.weak_from_this();
|
|
||||||
}
|
|
||||||
if (!aliveTexture.expired()) {
|
|
||||||
m_aliveObjects[identity] = Move(aliveTexture);
|
|
||||||
PruneStaleTextureAliases(&texture);
|
PruneStaleTextureAliases(&texture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1192,25 +1156,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) {
|
|
||||||
m_storageImageTextures.insert(MakeTextureIdentity(&texture));
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool VkTextureManager::NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const {
|
|
||||||
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
|
||||||
if (m_storageImageTextures.find(identity) == m_storageImageTextures.end()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const auto it = m_textureResources.find(identity);
|
|
||||||
// No image yet: the first sync creates it with STORAGE straight away, so there is nothing
|
|
||||||
// to preserve and nothing to order against.
|
|
||||||
return it != m_textureResources.end() && it->second.image != VK_NULL_HANDLE &&
|
|
||||||
!it->second.storageUsageResolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool VkTextureManager::NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const {
|
Bool VkTextureManager::NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const {
|
||||||
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
const auto it = m_textureResources.find(MakeTextureIdentity(&texture));
|
||||||
const auto it = m_textureResources.find(identity);
|
|
||||||
if (it == m_textureResources.end()) {
|
if (it == m_textureResources.end()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1218,12 +1165,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (resource.image == VK_NULL_HANDLE || resource.layout != VK_IMAGE_LAYOUT_GENERAL) {
|
if (resource.image == VK_NULL_HANDLE || resource.layout != VK_IMAGE_LAYOUT_GENERAL) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// The image predates this texture's first image-unit binding, so it was created without
|
|
||||||
// STORAGE usage and has to be recreated - which is illegal inside a render pass.
|
|
||||||
if (!resource.storageUsageResolved &&
|
|
||||||
m_storageImageTextures.find(identity) != m_storageImageTextures.end()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Mirror SyncTexture's cross-draw skip condition: any version drift means the sync
|
// Mirror SyncTexture's cross-draw skip condition: any version drift means the sync
|
||||||
// path may upload or rebuild, both of which need the render pass ended first.
|
// path may upload or rebuild, both of which need the render pass ended first.
|
||||||
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
||||||
@@ -1271,22 +1212,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SizeT VkTextureManager::CollectGarbage() {
|
SizeT VkTextureManager::CollectGarbage() {
|
||||||
// Draw-gated stagger (1 in 256 calls): keeps the per-draw cost at one counter
|
|
||||||
// bump. The guaranteed reclaim path is the frame-boundary prune in BeginFrame;
|
|
||||||
// this remains as a cheap assist so draw-heavy workloads reclaim sooner.
|
|
||||||
m_gcCounter++;
|
m_gcCounter++;
|
||||||
if (m_gcCounter != 0) {
|
if (m_gcCounter != 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return PruneDeadTextures();
|
|
||||||
}
|
|
||||||
|
|
||||||
SizeT VkTextureManager::PruneDeadTextures() {
|
|
||||||
// Erasing entries would dangle the raw TextureResource pointers memoized for the
|
|
||||||
// current draw; every call path (BeginFrame, and CollectGarbage at the top of a
|
|
||||||
// freshly opened draw-sync scope) runs before any memo entry is recorded.
|
|
||||||
MOBILEGL_ASSERT(m_drawSyncedThisDraw.empty(),
|
|
||||||
"PruneDeadTextures: draw-sync memo holds raw resource pointers an erase would dangle");
|
|
||||||
|
|
||||||
Vector<MG_State::GLState::ITextureObject*> expiredTextures;
|
Vector<MG_State::GLState::ITextureObject*> expiredTextures;
|
||||||
expiredTextures.reserve(m_aliveObjects.size());
|
expiredTextures.reserve(m_aliveObjects.size());
|
||||||
@@ -1298,25 +1227,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
for (auto* texture : expiredTextures) {
|
for (auto* texture : expiredTextures) {
|
||||||
PruneStaleTextureAliases(texture);
|
PruneStaleTextureAliases(texture);
|
||||||
}
|
}
|
||||||
SizeT prunedCount = expiredTextures.size();
|
return expiredTextures.size();
|
||||||
|
|
||||||
// Orphan sweep: after the pass above, m_aliveObjects holds only live entries.
|
|
||||||
// Registration in SyncTextureAndGetDescriptor cannot fail for a SharedPtr-owned
|
|
||||||
// texture (weak_from_this fallback), so a resource whose identity has no alive
|
|
||||||
// entry has no trackable owner: its GL-side object is gone, or was never
|
|
||||||
// shared-owned, in which case recreation on a later sync is the safe fallback.
|
|
||||||
// Destruction goes through the per-frame deferred queues, never immediate.
|
|
||||||
Vector<TextureIdentity> orphanIdentities;
|
|
||||||
for (auto it = m_textureResources.begin(); it != m_textureResources.end(); ++it) {
|
|
||||||
if (m_aliveObjects.find(it->first) == m_aliveObjects.end()) {
|
|
||||||
orphanIdentities.emplace_back(it->first);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const auto& identity : orphanIdentities) {
|
|
||||||
EraseTrackedTexture(identity);
|
|
||||||
}
|
|
||||||
prunedCount += orphanIdentities.size();
|
|
||||||
return prunedCount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
|
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
|
||||||
@@ -1330,13 +1241,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
||||||
const Uint32 syncingMipLevelCount =
|
const Uint32 syncingMipLevelCount =
|
||||||
syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u;
|
syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u;
|
||||||
// A pending storage-usage upgrade also has to bust the skip: nothing about the texture's
|
if (outResource.image != VK_NULL_HANDLE &&
|
||||||
// content or params changed, but the image itself must be recreated with STORAGE usage
|
|
||||||
// before it can back an image-unit descriptor.
|
|
||||||
const Bool storageUpgradePending =
|
|
||||||
!outResource.storageUsageResolved &&
|
|
||||||
m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end();
|
|
||||||
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending &&
|
|
||||||
outResource.syncedContentVersion == syncingContentVersion &&
|
outResource.syncedContentVersion == syncingContentVersion &&
|
||||||
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
|
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
|
||||||
outResource.syncedMipLevelCount == syncingMipLevelCount) {
|
outResource.syncedMipLevelCount == syncingMipLevelCount) {
|
||||||
@@ -1447,44 +1352,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const VkImageAspectFlags aspect = GetAspectMaskForFormat(format);
|
const VkImageAspectFlags aspect = GetAspectMaskForFormat(format);
|
||||||
VkFormatProperties formatProperties{};
|
VkFormatProperties formatProperties{};
|
||||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||||
// Only textures that have actually been bound to a GL image unit get STORAGE usage (and
|
const Bool supportsStorageImage =
|
||||||
// the MUTABLE_FORMAT it drags in for format-reinterpreting image views). Requesting it
|
|
||||||
// for every storage-capable colour texture costs real bandwidth: Adreno cannot keep UBWC
|
|
||||||
// compression on an image that may be written through a storage descriptor, so the whole
|
|
||||||
// render target - MC's included - runs uncompressed. MarkStorageImageTexture upgrades a
|
|
||||||
// texture before its first image-unit draw, and the usage below feeds the compatibility
|
|
||||||
// check so the upgrade recreates the image.
|
|
||||||
const Bool markedAsStorageImage =
|
|
||||||
m_storageImageTextures.find(MakeTextureIdentity(
|
|
||||||
const_cast<MG_State::GLState::ITextureObject*>(&texture))) != m_storageImageTextures.end();
|
|
||||||
// Storage-image CAPABILITY (does the format allow it at all) is deliberately separate from
|
|
||||||
// whether this texture actually needs the usage. MUTABLE_FORMAT keys off capability, as
|
|
||||||
// before: format-reinterpreting views are not a storage-only concern - the SAMPLED path
|
|
||||||
// needs them too (GetOrCreateSampledImageView bails out without it, see ~line 892), so
|
|
||||||
// tying MUTABLE_FORMAT to the image-unit mark would break sampled format reinterpretation
|
|
||||||
// for every texture that never becomes a storage image.
|
|
||||||
const Bool storageImageCapable =
|
|
||||||
!isMultisampleTexture &&
|
!isMultisampleTexture &&
|
||||||
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
|
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
|
||||||
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
|
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
|
||||||
const Bool supportsStorageImage = storageImageCapable && markedAsStorageImage;
|
|
||||||
VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags;
|
VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags;
|
||||||
if (storageImageCapable && IsMutableStorageImageFormat(format) &&
|
if (supportsStorageImage && IsMutableStorageImageFormat(format) &&
|
||||||
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
|
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
|
||||||
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||||
}
|
}
|
||||||
|
|
||||||
VkImageUsageFlags desiredUsage =
|
|
||||||
VK_IMAGE_USAGE_SAMPLED_BIT |
|
|
||||||
(supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) |
|
|
||||||
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) |
|
|
||||||
(((aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || (aspect & VK_IMAGE_ASPECT_STENCIL_BIT)) ?
|
|
||||||
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT :
|
|
||||||
0);
|
|
||||||
if (!isMultisampleTexture) {
|
|
||||||
desiredUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
|
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
|
||||||
resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
|
resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
|
||||||
resource.extent.height == static_cast<Uint32>(texelSize.y()) &&
|
resource.extent.height == static_cast<Uint32>(texelSize.y()) &&
|
||||||
@@ -1493,7 +1370,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
resource.viewType == shapeInfo.viewType &&
|
resource.viewType == shapeInfo.viewType &&
|
||||||
resource.sampleCount == resolvedSampleCount &&
|
resource.sampleCount == resolvedSampleCount &&
|
||||||
resource.imageCreateFlags == imageCreateFlags &&
|
resource.imageCreateFlags == imageCreateFlags &&
|
||||||
resource.usageFlags == desiredUsage &&
|
|
||||||
resource.mipLevels == backingMipLevels;
|
resource.mipLevels == backingMipLevels;
|
||||||
if (compatible) {
|
if (compatible) {
|
||||||
if (resource.perMipViews.size() != backingMipLevels) {
|
if (resource.perMipViews.size() != backingMipLevels) {
|
||||||
@@ -1502,10 +1378,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (resource.perMipSampledViews.size() != backingMipLevels) {
|
if (resource.perMipSampledViews.size() != backingMipLevels) {
|
||||||
resource.perMipSampledViews.resize(backingMipLevels, VK_NULL_HANDLE);
|
resource.perMipSampledViews.resize(backingMipLevels, VK_NULL_HANDLE);
|
||||||
}
|
}
|
||||||
// Keeping the image is itself the answer to the mark: either it already carries
|
|
||||||
// STORAGE, or this format can never carry it. Either way there is nothing left to
|
|
||||||
// recreate, so stop reporting the texture as needing preparation.
|
|
||||||
resource.storageUsageResolved = markedAsStorageImage;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1520,10 +1392,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
resource.sampleCount == resolvedSampleCount &&
|
resource.sampleCount == resolvedSampleCount &&
|
||||||
resource.imageCreateFlags == imageCreateFlags &&
|
resource.imageCreateFlags == imageCreateFlags &&
|
||||||
resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT &&
|
resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT &&
|
||||||
// '<=' rather than '<': a storage-usage upgrade recreates the image with an
|
resource.mipLevels < backingMipLevels &&
|
||||||
// unchanged mip count, and its contents (a render target's pixels live only on the
|
|
||||||
// GPU) still have to survive. The vkCmdCopyImage below copies min(mipLevels).
|
|
||||||
resource.mipLevels <= backingMipLevels &&
|
|
||||||
resource.layout != VK_IMAGE_LAYOUT_UNDEFINED;
|
resource.layout != VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
|
||||||
std::unique_ptr<TextureResource> preservedResource;
|
std::unique_ptr<TextureResource> preservedResource;
|
||||||
@@ -1545,37 +1414,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
imageInfo.format = format;
|
imageInfo.format = format;
|
||||||
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||||
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
imageInfo.usage = desiredUsage;
|
imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT |
|
||||||
imageInfo.samples = resolvedSampleCount;
|
(supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) |
|
||||||
|
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) |
|
||||||
// Bound the mutability. A blindly-mutable image has to be laid out so that ANY format in
|
(((aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || (aspect & VK_IMAGE_ASPECT_STENCIL_BIT)) ?
|
||||||
// its compatibility class can be viewed, which costs bandwidth compression on tilers;
|
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT :
|
||||||
// naming the exact set instead lets the driver keep it. Only safe when that set really is
|
0);
|
||||||
// exhaustive, so it is restricted to textures that are not image-unit bound: sampled views
|
if (!isMultisampleTexture) {
|
||||||
// can only ever ask for ResolveSampledImageViewFormat's output, whereas glBindImageTexture
|
imageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||||
// may name any compatible format, which nothing here can enumerate ahead of time.
|
|
||||||
Vector<VkFormat> viewFormats;
|
|
||||||
VkImageFormatListCreateInfo formatListInfo{};
|
|
||||||
if (m_imageFormatListSupported && !supportsStorageImage &&
|
|
||||||
(imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
|
||||||
viewFormats.push_back(format);
|
|
||||||
for (const SamplerNumericDomain domain : {SamplerNumericDomain::Float,
|
|
||||||
SamplerNumericDomain::SignedInteger,
|
|
||||||
SamplerNumericDomain::UnsignedInteger}) {
|
|
||||||
const VkFormat viewFormat = ResolveSampledImageViewFormat(format, domain);
|
|
||||||
if (viewFormat == VK_FORMAT_UNDEFINED) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (std::find(viewFormats.begin(), viewFormats.end(), viewFormat) == viewFormats.end()) {
|
|
||||||
viewFormats.push_back(viewFormat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
|
|
||||||
formatListInfo.viewFormatCount = static_cast<Uint32>(viewFormats.size());
|
|
||||||
formatListInfo.pViewFormats = viewFormats.data();
|
|
||||||
imageInfo.pNext = &formatListInfo;
|
|
||||||
}
|
}
|
||||||
|
imageInfo.samples = resolvedSampleCount;
|
||||||
if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
||||||
VkImageFormatProperties imageFormatProperties{};
|
VkImageFormatProperties imageFormatProperties{};
|
||||||
VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
||||||
@@ -1632,8 +1480,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
resource.viewType = shapeInfo.viewType;
|
resource.viewType = shapeInfo.viewType;
|
||||||
resource.sampleCount = resolvedSampleCount;
|
resource.sampleCount = resolvedSampleCount;
|
||||||
resource.imageCreateFlags = imageCreateFlags;
|
resource.imageCreateFlags = imageCreateFlags;
|
||||||
resource.usageFlags = imageInfo.usage;
|
|
||||||
resource.storageUsageResolved = markedAsStorageImage;
|
|
||||||
resource.syncedTextureParamsVersion = 0;
|
resource.syncedTextureParamsVersion = 0;
|
||||||
|
|
||||||
if (preservedResource) {
|
if (preservedResource) {
|
||||||
|
|||||||
@@ -53,9 +53,6 @@ public:
|
|||||||
VkCommandPool commandPool = VK_NULL_HANDLE;
|
VkCommandPool commandPool = VK_NULL_HANDLE;
|
||||||
VkQueue graphicsQueue = VK_NULL_HANDLE;
|
VkQueue graphicsQueue = VK_NULL_HANDLE;
|
||||||
Uint32 frameCount = 0;
|
Uint32 frameCount = 0;
|
||||||
// VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of
|
|
||||||
// formats they will be viewed as, which is what lets a tiler keep them compressed.
|
|
||||||
Bool imageFormatListSupported = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct TextureResource {
|
struct TextureResource {
|
||||||
@@ -160,17 +157,6 @@ public:
|
|||||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||||
VkImageCreateFlags imageCreateFlags = 0;
|
VkImageCreateFlags imageCreateFlags = 0;
|
||||||
// Usage the live image was created with. STORAGE is only requested for textures that
|
|
||||||
// have actually been bound to a GL image unit, because on Adreno a storage-capable
|
|
||||||
// image loses UBWC bandwidth compression; a later image binding upgrades the usage
|
|
||||||
// and recreates the image, so the resolved usage has to be part of the compatibility
|
|
||||||
// check that decides whether the existing image can be kept.
|
|
||||||
VkImageUsageFlags usageFlags = 0;
|
|
||||||
// True once this image was (re)resolved while the texture was already marked as an
|
|
||||||
// image-unit texture. Distinguishes "not upgraded yet" from "cannot be upgraded"
|
|
||||||
// (a format whose optimalTilingFeatures lack STORAGE_IMAGE never gains the bit), so
|
|
||||||
// NeedsStorageImagePreparation cannot ask for a recreate that will never happen.
|
|
||||||
Bool storageUsageResolved = false;
|
|
||||||
Uint16 syncedTextureParamsVersion = 0;
|
Uint16 syncedTextureParamsVersion = 0;
|
||||||
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
|
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
|
||||||
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
|
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
|
||||||
@@ -204,8 +190,6 @@ public:
|
|||||||
std::swap(this->viewType, that.viewType);
|
std::swap(this->viewType, that.viewType);
|
||||||
std::swap(this->sampleCount, that.sampleCount);
|
std::swap(this->sampleCount, that.sampleCount);
|
||||||
std::swap(this->imageCreateFlags, that.imageCreateFlags);
|
std::swap(this->imageCreateFlags, that.imageCreateFlags);
|
||||||
std::swap(this->usageFlags, that.usageFlags);
|
|
||||||
std::swap(this->storageUsageResolved, that.storageUsageResolved);
|
|
||||||
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
|
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
|
||||||
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
||||||
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
||||||
@@ -267,8 +251,6 @@ public:
|
|||||||
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||||
sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||||
imageCreateFlags = 0;
|
imageCreateFlags = 0;
|
||||||
usageFlags = 0;
|
|
||||||
storageUsageResolved = false;
|
|
||||||
syncedTextureParamsVersion = 0;
|
syncedTextureParamsVersion = 0;
|
||||||
syncedContentVersion = 0;
|
syncedContentVersion = 0;
|
||||||
syncedMipLevelCount = 0;
|
syncedMipLevelCount = 0;
|
||||||
@@ -285,10 +267,6 @@ public:
|
|||||||
Bool Initialize(const InitInfo& initInfo);
|
Bool Initialize(const InitInfo& initInfo);
|
||||||
void Shutdown();
|
void Shutdown();
|
||||||
void BeginFrame(Uint32 frameIndex);
|
void BeginFrame(Uint32 frameIndex);
|
||||||
// Drains every frame slot's deferred image/view releases. Only valid when
|
|
||||||
// the caller has proven every queue submission complete; used by the
|
|
||||||
// present-less frame-boundary drain.
|
|
||||||
void CollectAllDeferredReleases();
|
|
||||||
|
|
||||||
TextureResource* SyncTextureAndGetDescriptor(
|
TextureResource* SyncTextureAndGetDescriptor(
|
||||||
MG_State::GLState::ITextureObject& texture);
|
MG_State::GLState::ITextureObject& texture);
|
||||||
@@ -307,17 +285,6 @@ public:
|
|||||||
VkImageLayout newLayout);
|
VkImageLayout newLayout);
|
||||||
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||||
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||||
// Records that this texture is bound to a GL image unit, so its image must carry
|
|
||||||
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
|
|
||||||
// therefore before the render pass is committed: an image that has to be upgraded is
|
|
||||||
// recreated, which is illegal inside a render pass. Sticky for the texture's lifetime -
|
|
||||||
// GL lets an image binding come and go, and re-creating the image every time it does
|
|
||||||
// would cost far more than the compression it wins back.
|
|
||||||
void MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture);
|
|
||||||
// True when this texture is marked but its live image predates the mark, i.e. the next sync
|
|
||||||
// will recreate it with STORAGE usage and copy the old contents forward. Callers use this to
|
|
||||||
// submit their pending recording first, so that copy cannot read pre-flush content.
|
|
||||||
Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const;
|
|
||||||
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
|
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
|
||||||
// texture as a storage image may need work that is illegal inside a render pass (resource
|
// texture as a storage image may need work that is illegal inside a render pass (resource
|
||||||
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
|
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
|
||||||
@@ -397,20 +364,15 @@ private:
|
|||||||
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
|
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
|
||||||
void EraseTrackedTexture(const TextureIdentity& identity);
|
void EraseTrackedTexture(const TextureIdentity& identity);
|
||||||
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
|
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
|
||||||
SizeT PruneDeadTextures();
|
|
||||||
|
|
||||||
VkDevice m_device = VK_NULL_HANDLE;
|
VkDevice m_device = VK_NULL_HANDLE;
|
||||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||||
VmaAllocator m_allocator = nullptr;
|
VmaAllocator m_allocator = nullptr;
|
||||||
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
||||||
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
|
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
|
||||||
Bool m_imageFormatListSupported = false;
|
|
||||||
Uint32 m_currentFrameIndex = 0;
|
Uint32 m_currentFrameIndex = 0;
|
||||||
|
|
||||||
Uint8 m_gcCounter = 0;
|
Uint8 m_gcCounter = 0;
|
||||||
// Frame-boundary GC gate: counts BeginFrame calls, not draws, so texture churn
|
|
||||||
// through non-draw paths (FBO clears, readbacks) still reaches the prune.
|
|
||||||
Uint32 m_gcFrameCounter = 0;
|
|
||||||
// Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of
|
// Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of
|
||||||
// textures already fully synced in the current draw (small N -> flat scan).
|
// textures already fully synced in the current draw (small N -> flat scan).
|
||||||
Bool m_drawSyncScopeActive = false;
|
Bool m_drawSyncScopeActive = false;
|
||||||
@@ -428,8 +390,6 @@ private:
|
|||||||
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
|
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
|
||||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||||
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
||||||
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
|
||||||
std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures;
|
|
||||||
Vector<Vector<TextureResource>> m_deferredReleases;
|
Vector<Vector<TextureResource>> m_deferredReleases;
|
||||||
Vector<Vector<VkImageView>> m_deferredViewReleases;
|
Vector<Vector<VkImageView>> m_deferredViewReleases;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1016,18 +1016,7 @@ layout(location = 0) in vec2 vTexCoord;
|
|||||||
layout(location = 0) out vec4 outColor;
|
layout(location = 0) out vec4 outColor;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// Explicit LOD, not texture(): a blit reads exactly the selected level, so
|
outColor = texture(uSource, vTexCoord);
|
||||||
// derivative-based mip selection has no business here. It is also load-bearing:
|
|
||||||
// on Adreno 650 (driver 512.502) an implicit-LOD sample of this single-mip
|
|
||||||
// UBWC render target through the pre-rotation (ROTATE_90) mapping reads past
|
|
||||||
// the image's allocation - despite the sampler's maxLod=0 and a nominal 1:1
|
|
||||||
// texel mapping whose LOD is 0, so the driver's implicit-LOD path itself is at
|
|
||||||
// fault - and page-faults the GPU once the neighbouring memory is returned to
|
|
||||||
// the kernel (frame 2 of Minecraft 26.2's resource reload; the kernel then
|
|
||||||
// invalidates the context and the next submit dies with EDEADLK ->
|
|
||||||
// VK_ERROR_DEVICE_LOST at Present). Verified on device: texture() faults on
|
|
||||||
// the second frame every run, textureLod survives with identical state.
|
|
||||||
outColor = textureLod(uSource, vTexCoord, 0.0);
|
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
|
|
||||||
@@ -1921,10 +1910,8 @@ void main() {
|
|||||||
case VK_FORMAT_R16G16_UNORM: out = {ReadbackSourceClass::Float, 2, 16}; return true;
|
case VK_FORMAT_R16G16_UNORM: out = {ReadbackSourceClass::Float, 2, 16}; return true;
|
||||||
case VK_FORMAT_R16G16B16A16_UNORM: out = {ReadbackSourceClass::Float, 4, 16}; return true;
|
case VK_FORMAT_R16G16B16A16_UNORM: out = {ReadbackSourceClass::Float, 4, 16}; return true;
|
||||||
// --- SRGB (decode to linear like GL readback of sRGB textures) ---
|
// --- SRGB (decode to linear like GL readback of sRGB textures) ---
|
||||||
// GL GetTexImage/ReadPixels of sRGB textures return the raw sRGB-encoded
|
case VK_FORMAT_R8G8B8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8, false, true}; return true;
|
||||||
// bytes (GL 3.3 has no FRAMEBUFFER_SRGB read decode) - do NOT linearize.
|
case VK_FORMAT_B8G8R8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8, false, true, true}; return true;
|
||||||
case VK_FORMAT_R8G8B8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8}; return true;
|
|
||||||
case VK_FORMAT_B8G8R8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8, false, false, true}; return true;
|
|
||||||
// --- SNORM ---
|
// --- SNORM ---
|
||||||
case VK_FORMAT_R8_SNORM: out = {ReadbackSourceClass::Float, 1, 8, true}; return true;
|
case VK_FORMAT_R8_SNORM: out = {ReadbackSourceClass::Float, 1, 8, true}; return true;
|
||||||
case VK_FORMAT_R8G8_SNORM: out = {ReadbackSourceClass::Float, 2, 8, true}; return true;
|
case VK_FORMAT_R8G8_SNORM: out = {ReadbackSourceClass::Float, 2, 8, true}; return true;
|
||||||
@@ -1962,8 +1949,6 @@ void main() {
|
|||||||
// --- packed / special ---
|
// --- packed / special ---
|
||||||
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
|
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
|
||||||
case VK_FORMAT_A2B10G10R10_UINT_PACK32:
|
case VK_FORMAT_A2B10G10R10_UINT_PACK32:
|
||||||
case VK_FORMAT_A2R10G10B10_UNORM_PACK32:
|
|
||||||
case VK_FORMAT_A2R10G10B10_UINT_PACK32:
|
|
||||||
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
|
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
|
||||||
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
|
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
|
||||||
case VK_FORMAT_R5G6B5_UNORM_PACK16:
|
case VK_FORMAT_R5G6B5_UNORM_PACK16:
|
||||||
@@ -1973,8 +1958,7 @@ void main() {
|
|||||||
case VK_FORMAT_B5G5R5A1_UNORM_PACK16:
|
case VK_FORMAT_B5G5R5A1_UNORM_PACK16:
|
||||||
case VK_FORMAT_R4G4B4A4_UNORM_PACK16:
|
case VK_FORMAT_R4G4B4A4_UNORM_PACK16:
|
||||||
case VK_FORMAT_B4G4R4A4_UNORM_PACK16:
|
case VK_FORMAT_B4G4R4A4_UNORM_PACK16:
|
||||||
out.sourceClass = (format == VK_FORMAT_A2B10G10R10_UINT_PACK32 ||
|
out.sourceClass = format == VK_FORMAT_A2B10G10R10_UINT_PACK32 ?
|
||||||
format == VK_FORMAT_A2R10G10B10_UINT_PACK32) ?
|
|
||||||
ReadbackSourceClass::UnsignedInt : ReadbackSourceClass::Float;
|
ReadbackSourceClass::UnsignedInt : ReadbackSourceClass::Float;
|
||||||
out.special = format;
|
out.special = format;
|
||||||
return true;
|
return true;
|
||||||
@@ -2050,15 +2034,6 @@ void main() {
|
|||||||
rgba[3] = static_cast<Float>((word >> 30) & 0x3u) / 3.0f;
|
rgba[3] = static_cast<Float>((word >> 30) & 0x3u) / 3.0f;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case VK_FORMAT_A2R10G10B10_UNORM_PACK32: {
|
|
||||||
Uint32 word = 0;
|
|
||||||
Memcpy(&word, source, sizeof(word));
|
|
||||||
rgba[2] = static_cast<Float>(word & 0x3FFu) / 1023.0f;
|
|
||||||
rgba[1] = static_cast<Float>((word >> 10) & 0x3FFu) / 1023.0f;
|
|
||||||
rgba[0] = static_cast<Float>((word >> 20) & 0x3FFu) / 1023.0f;
|
|
||||||
rgba[3] = static_cast<Float>((word >> 30) & 0x3u) / 3.0f;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case VK_FORMAT_B10G11R11_UFLOAT_PACK32: {
|
case VK_FORMAT_B10G11R11_UFLOAT_PACK32: {
|
||||||
Uint32 word = 0;
|
Uint32 word = 0;
|
||||||
Memcpy(&word, source, sizeof(word));
|
Memcpy(&word, source, sizeof(word));
|
||||||
@@ -2210,13 +2185,6 @@ void main() {
|
|||||||
rgba[1] = (word >> 10) & 0x3FFu;
|
rgba[1] = (word >> 10) & 0x3FFu;
|
||||||
rgba[2] = (word >> 20) & 0x3FFu;
|
rgba[2] = (word >> 20) & 0x3FFu;
|
||||||
rgba[3] = (word >> 30) & 0x3u;
|
rgba[3] = (word >> 30) & 0x3u;
|
||||||
} else if (srcFormat == VK_FORMAT_A2R10G10B10_UINT_PACK32) {
|
|
||||||
Uint32 word = 0;
|
|
||||||
Memcpy(&word, source, sizeof(word));
|
|
||||||
rgba[2] = word & 0x3FFu;
|
|
||||||
rgba[1] = (word >> 10) & 0x3FFu;
|
|
||||||
rgba[0] = (word >> 20) & 0x3FFu;
|
|
||||||
rgba[3] = (word >> 30) & 0x3u;
|
|
||||||
} else {
|
} else {
|
||||||
for (Int c = 0; c < desc.channels; ++c) {
|
for (Int c = 0; c < desc.channels; ++c) {
|
||||||
if (desc.componentBits == 8) {
|
if (desc.componentBits == 8) {
|
||||||
@@ -2248,9 +2216,8 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Bool PackReadbackToClientOrPbo(const Uint8* srcPixels, VkFormat srcFormat, GLsizei width,
|
static Bool PackReadbackToClientOrPbo(const Uint8* srcPixels, VkFormat srcFormat, GLsizei width,
|
||||||
GLsizei sliceHeight, GLsizei sliceCount, GLenum format, GLenum type,
|
GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||||
void* pixels, Bool applyPackImageParams) {
|
if (width <= 0 || height <= 0) {
|
||||||
if (width <= 0 || sliceHeight <= 0 || sliceCount <= 0) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2263,8 +2230,7 @@ void main() {
|
|||||||
|
|
||||||
Vector<Uint8> wide;
|
Vector<Uint8> wide;
|
||||||
GLenum wideType = GL_FLOAT;
|
GLenum wideType = GL_FLOAT;
|
||||||
if (!DecodeReadbackRowsToWide(srcPixels, srcFormat, width,
|
if (!DecodeReadbackRowsToWide(srcPixels, srcFormat, width, height, wide, wideType)) {
|
||||||
sliceHeight * sliceCount, wide, wideType)) {
|
|
||||||
MGLOG_E("DirectVulkan readback skipped: unsupported source format=%d",
|
MGLOG_E("DirectVulkan readback skipped: unsupported source format=%d",
|
||||||
static_cast<Int>(srcFormat));
|
static_cast<Int>(srcFormat));
|
||||||
return false;
|
return false;
|
||||||
@@ -2277,9 +2243,9 @@ void main() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return DirectGLES::ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, sliceHeight,
|
return DirectGLES::ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, height,
|
||||||
sliceCount, mapping, type, pixels,
|
/*sliceCount=*/1, mapping, type, pixels,
|
||||||
applyPackImageParams);
|
/*applyPackImageParams=*/false);
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -2491,7 +2457,7 @@ void main() {
|
|||||||
MOBILEGL_ASSERT(m_textureManager != nullptr, "VkTextureManager creation failed.");
|
MOBILEGL_ASSERT(m_textureManager != nullptr, "VkTextureManager creation failed.");
|
||||||
succeeded = m_textureManager->Initialize(
|
succeeded = m_textureManager->Initialize(
|
||||||
{m_device, m_physicalDevice.handle, m_allocator, m_commandPool, m_graphicsQueue,
|
{m_device, m_physicalDevice.handle, m_allocator, m_commandPool, m_graphicsQueue,
|
||||||
m_frameContext.GetFrameCount(), m_imageFormatListExtensionEnabled});
|
m_frameContext.GetFrameCount()});
|
||||||
MOBILEGL_ASSERT(succeeded, "VkTextureManager initialization failed.");
|
MOBILEGL_ASSERT(succeeded, "VkTextureManager initialization failed.");
|
||||||
m_clearManager = MakeUnique<VkClearManager>();
|
m_clearManager = MakeUnique<VkClearManager>();
|
||||||
MOBILEGL_ASSERT(m_clearManager != nullptr, "VkClearManager creation failed.");
|
MOBILEGL_ASSERT(m_clearManager != nullptr, "VkClearManager creation failed.");
|
||||||
@@ -2540,11 +2506,6 @@ void main() {
|
|||||||
m_shaderDrawParametersFeatureEnabled,
|
m_shaderDrawParametersFeatureEnabled,
|
||||||
m_unformattedFloatStorageImagesEnabled);
|
m_unformattedFloatStorageImagesEnabled);
|
||||||
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
|
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
|
||||||
// Aging evictions (render passes and program entries) must purge the dependent
|
|
||||||
// pipeline / compute-pipeline / descriptor-set caches in the same step; both
|
|
||||||
// sweeps only run from the frame-boundary seams, long after initialization.
|
|
||||||
m_renderPassManager->SetEvictionObserver(this);
|
|
||||||
m_programFactory->SetEvictionObserver(this);
|
|
||||||
|
|
||||||
m_samplerManager = MakeUnique<VkSamplerManager>();
|
m_samplerManager = MakeUnique<VkSamplerManager>();
|
||||||
MOBILEGL_ASSERT(m_samplerManager != nullptr, "VkSamplerManager creation failed.");
|
MOBILEGL_ASSERT(m_samplerManager != nullptr, "VkSamplerManager creation failed.");
|
||||||
@@ -2567,32 +2528,15 @@ void main() {
|
|||||||
MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed.");
|
MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed.");
|
||||||
|
|
||||||
// Prime the first frame so Render() always targets an acquired swapchain image.
|
// Prime the first frame so Render() always targets an acquired swapchain image.
|
||||||
// A zero-area window (GLFW's hidden helper window during the WGL bootstrap, or a
|
VkResult acquireResult =
|
||||||
// window that is already minimized) legitimately yields no swapchain here; defer
|
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
|
||||||
// the first acquire to Present in that case instead of acquiring from a null
|
if (acquireResult == VK_ERROR_OUT_OF_DATE_KHR || acquireResult == VK_SUBOPTIMAL_KHR) {
|
||||||
// swapchain handle.
|
MGLOG_D("Initialize, vkAcquireNextImageKHR got %d, recreating swapchain", acquireResult);
|
||||||
if (m_swapchainObject.GetHandle() != VK_NULL_HANDLE) {
|
RecreateSwapchain();
|
||||||
VkResult acquireResult =
|
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) {
|
|
||||||
// Nothing was acquired and no semaphore signal was armed, so
|
|
||||||
// rebuilding and re-acquiring on the same semaphore is safe.
|
|
||||||
MGLOG_D("Initialize, vkAcquireNextImageKHR got %d, recreating swapchain", acquireResult);
|
|
||||||
RecreateSwapchain();
|
|
||||||
acquireResult =
|
|
||||||
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
|
|
||||||
} else if (acquireResult == VK_SUBOPTIMAL_KHR) {
|
|
||||||
// The image is usable, and its acquire signal is already armed on
|
|
||||||
// imageAvailableSemaphore. Re-acquiring here would arm a second signal on a
|
|
||||||
// binary semaphore whose first one nobody has waited on yet; keep the image.
|
|
||||||
// Only a real surface change schedules a rebuild.
|
|
||||||
m_swapchainResizeRequested = m_swapchainResizeRequested || SwapchainIsOutOfDate();
|
|
||||||
acquireResult = VK_SUCCESS;
|
|
||||||
}
|
|
||||||
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();
|
||||||
@@ -2614,15 +2558,6 @@ void main() {
|
|||||||
DestroyDeferredDepthMipmapCleanup();
|
DestroyDeferredDepthMipmapCleanup();
|
||||||
DestroyComputePipelines();
|
DestroyComputePipelines();
|
||||||
|
|
||||||
// No sweep runs during teardown, but the observers point at this renderer
|
|
||||||
// and the factories die at different times below; disconnect them first.
|
|
||||||
if (m_renderPassManager) {
|
|
||||||
m_renderPassManager->SetEvictionObserver(nullptr);
|
|
||||||
}
|
|
||||||
if (m_programFactory) {
|
|
||||||
m_programFactory->SetEvictionObserver(nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_pipelineFactory.reset();
|
m_pipelineFactory.reset();
|
||||||
ShutdownBlitResources();
|
ShutdownBlitResources();
|
||||||
ShutdownDepthMipmapResources();
|
ShutdownDepthMipmapResources();
|
||||||
@@ -2713,7 +2648,6 @@ void main() {
|
|||||||
DestroyDebugMessenger();
|
DestroyDebugMessenger();
|
||||||
m_debugMessenger = VK_NULL_HANDLE;
|
m_debugMessenger = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
DestroyDebugReportCallback();
|
|
||||||
|
|
||||||
if (m_instance != VK_NULL_HANDLE) {
|
if (m_instance != VK_NULL_HANDLE) {
|
||||||
vkDestroyInstance(m_instance, nullptr);
|
vkDestroyInstance(m_instance, nullptr);
|
||||||
@@ -4193,7 +4127,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Bool VulkanRenderer::PrepareStorageImageTextures(
|
Bool VulkanRenderer::PrepareStorageImageTextures(
|
||||||
FrameContext::FrameData& frame,
|
VkCommandBuffer commandBuffer,
|
||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj) {
|
const ProgramFactory::VkProgramObject& programObj) {
|
||||||
if (!programObj.hasStorageImages) {
|
if (!programObj.hasStorageImages) {
|
||||||
@@ -4214,18 +4148,9 @@ void main() {
|
|||||||
// keep the render pass alive instead of splitting it on every storage-image draw (on
|
// keep the render pass alive instead of splitting it on every storage-image draw (on
|
||||||
// tiled GPUs each split is a full tile load/store). GL makes cross-draw image-store
|
// tiled GPUs each split is a full tile load/store). GL makes cross-draw image-store
|
||||||
// coherence the app's job (glMemoryBarrier), so no implicit barrier is owed here.
|
// coherence the app's job (glMemoryBarrier), so no implicit barrier is owed here.
|
||||||
// Record every image-unit binding before probing anything: a texture whose image was
|
|
||||||
// created without STORAGE usage (the default - it costs UBWC compression on Adreno)
|
|
||||||
// needs a recreate, and the probe below is what ends the render pass so that recreate
|
|
||||||
// lands here rather than mid-pass. This cannot be folded into the probe loop, which
|
|
||||||
// stops at the first texture that needs work and would leave the rest unmarked.
|
|
||||||
for (auto* texture : storageTextures) {
|
|
||||||
MOBILEGL_ASSERT(texture != nullptr, "%s: collected a null storage texture", __func__);
|
|
||||||
m_textureManager->MarkStorageImageTexture(*texture);
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool anyNeedsPreparation = false;
|
Bool anyNeedsPreparation = false;
|
||||||
for (auto* texture : storageTextures) {
|
for (auto* texture : storageTextures) {
|
||||||
|
MOBILEGL_ASSERT(texture != nullptr, "%s: collected a null storage texture", __func__);
|
||||||
if (m_textureManager->NeedsStorageImagePreparation(*texture) ||
|
if (m_textureManager->NeedsStorageImagePreparation(*texture) ||
|
||||||
m_clearManager->HasPendingClear(texture)) {
|
m_clearManager->HasPendingClear(texture)) {
|
||||||
anyNeedsPreparation = true;
|
anyNeedsPreparation = true;
|
||||||
@@ -4236,51 +4161,21 @@ void main() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A first-time storage-usage upgrade recreates the image and carries the old contents
|
|
||||||
// forward with an out-of-band, immediately-submitted copy (PreserveTextureContentsOnRecreate).
|
|
||||||
// Whatever this frame already recorded into the old image is still sitting unsubmitted in
|
|
||||||
// this command buffer, so that copy would read pre-frame content and this frame's rendering
|
|
||||||
// into the texture would be lost - precisely the render-target-then-image-unit case this
|
|
||||||
// whole path exists for. Submit what is recorded first; the copy then queues behind it.
|
|
||||||
Bool anyNeedsStorageUpgrade = false;
|
|
||||||
for (auto* texture : storageTextures) {
|
|
||||||
if (m_textureManager->NeedsStorageUsageUpgrade(*texture)) {
|
|
||||||
anyNeedsStorageUpgrade = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (anyNeedsStorageUpgrade && HasPendingRecordedWork()) {
|
|
||||||
if (FlushPendingCommands()) {
|
|
||||||
// Fresh command buffer: the sampled-descriptor-set memo describes bindings that
|
|
||||||
// only existed in the retired one. FlushPendingCommands drops the pipeline memo
|
|
||||||
// itself; this is the other command-buffer-scoped cache.
|
|
||||||
m_lastSampledSetValid = false;
|
|
||||||
} else {
|
|
||||||
// Best effort: the upgrade still produces a correct image, only its preserved
|
|
||||||
// contents may predate this frame's writes. Dropping the draw would be worse.
|
|
||||||
MGLOG_E("%s: flush before a storage-usage image upgrade failed; preserved contents "
|
|
||||||
"may be stale for one frame", __func__);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!frame.isCommandRecording) {
|
|
||||||
m_frameContext.BeginCommandRecording();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Image uploads, deferred-clear materialization, and layout barriers are illegal inside
|
// Image uploads, deferred-clear materialization, and layout barriers are illegal inside
|
||||||
// a classic render pass. Do this before sampler preparation as well: a texture used by
|
// a classic render pass. Do this before sampler preparation as well: a texture used by
|
||||||
// both a sampler and an image must stay in GENERAL, and both descriptors must name that
|
// both a sampler and an image must stay in GENERAL, and both descriptors must name that
|
||||||
// same layout independent of SPIR-V reflection/binding order.
|
// same layout independent of SPIR-V reflection/binding order.
|
||||||
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
|
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
|
||||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
VkRenderPassManager::EndRenderPass(commandBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto* texture : storageTextures) {
|
for (auto* texture : storageTextures) {
|
||||||
if (!MaterializePendingClearForTexture(frame.commandBuffer, *texture)) {
|
if (!MaterializePendingClearForTexture(commandBuffer, *texture)) {
|
||||||
MGLOG_E("%s: failed to materialize pending clear for storage textureId=%d",
|
MGLOG_E("%s: failed to materialize pending clear for storage textureId=%d",
|
||||||
__func__, texture->GetExternalIndex());
|
__func__, texture->GetExternalIndex());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!m_textureManager->TransitionTextureForStorageImage(frame.commandBuffer, *texture)) {
|
if (!m_textureManager->TransitionTextureForStorageImage(commandBuffer, *texture)) {
|
||||||
MGLOG_E("%s: failed to prepare storage textureId=%d",
|
MGLOG_E("%s: failed to prepare storage textureId=%d",
|
||||||
__func__, texture->GetExternalIndex());
|
__func__, texture->GetExternalIndex());
|
||||||
return false;
|
return false;
|
||||||
@@ -4306,25 +4201,7 @@ void main() {
|
|||||||
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||||
const auto& program = *MG_State::pGLContext->GetCurrentProgram();
|
const auto& program = *MG_State::pGLContext->GetCurrentProgram();
|
||||||
ProgramFactory::CompileOptionFlags transformFlags = GetShaderTransformFlags(m_swapchainObject.GetPreTransform());
|
ProgramFactory::CompileOptionFlags transformFlags = GetShaderTransformFlags(m_swapchainObject.GetPreTransform());
|
||||||
const auto* programObjPtr = &m_programFactory->GetOrCreateProgram(program, transformFlags);
|
const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags);
|
||||||
// Sampling a colour render target through the driver's implicit-LOD path faults the GPU on
|
|
||||||
// Adreno 650 (see ForceExplicitLod0SamplePass); ask for the explicit-LOD variant when doing
|
|
||||||
// so cannot change a texel, i.e. when every sampler this program reads is pinned to a
|
|
||||||
// single mip level.
|
|
||||||
if (UniformManager::ProgramSamplesOnlySingleLevelTextures(program, *programObjPtr)) {
|
|
||||||
transformFlags |= ProgramFactory::CompileOptionBit::ExplicitLod0Sampling;
|
|
||||||
programObjPtr = &m_programFactory->GetOrCreateProgram(program, transformFlags);
|
|
||||||
}
|
|
||||||
// fp16 fragment arithmetic is only sound when nothing this draw reads or writes carries
|
|
||||||
// more than 8 normalized bits per channel. A shaderpack's HDR gbuffer, or a data texture
|
|
||||||
// holding positions, must keep full precision - and SPIR-V cannot tell, since sampler2D
|
|
||||||
// yields vec4 whatever the bound format is, so the decision has to be made here.
|
|
||||||
if (UniformManager::ProgramSamplesOnlyLowPrecisionTextures(program, *programObjPtr) &&
|
|
||||||
UniformManager::DrawTargetIsLowPrecision(drawFbo.get())) {
|
|
||||||
transformFlags |= ProgramFactory::CompileOptionBit::RelaxedFragmentPrecision;
|
|
||||||
programObjPtr = &m_programFactory->GetOrCreateProgram(program, transformFlags);
|
|
||||||
}
|
|
||||||
const auto& programObj = *programObjPtr;
|
|
||||||
|
|
||||||
// Begin command recording if not yet
|
// Begin command recording if not yet
|
||||||
if (!frame.isCommandRecording) {
|
if (!frame.isCommandRecording) {
|
||||||
@@ -4334,7 +4211,7 @@ void main() {
|
|||||||
m_lastSampledSetValid = false;
|
m_lastSampledSetValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!PrepareStorageImageTextures(frame, program, programObj)) {
|
if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) {
|
||||||
MGLOG_E("SetupDraw skipped: storage image preparation failed");
|
MGLOG_E("SetupDraw skipped: storage image preparation failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -4559,7 +4436,7 @@ void main() {
|
|||||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!PrepareStorageImageTextures(frame, program, programObj)) {
|
if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) {
|
||||||
MGLOG_E("DispatchCompute skipped: storage image preparation failed");
|
MGLOG_E("DispatchCompute skipped: storage image preparation failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4599,7 +4476,7 @@ void main() {
|
|||||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!PrepareStorageImageTextures(frame, program, programObj)) {
|
if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) {
|
||||||
MGLOG_E("DispatchComputeIndirect skipped: storage image preparation failed");
|
MGLOG_E("DispatchComputeIndirect skipped: storage image preparation failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -6296,12 +6173,12 @@ void main() {
|
|||||||
|
|
||||||
frame.hasCommandBufferRecorded = false;
|
frame.hasCommandBufferRecorded = false;
|
||||||
frame.isCommandRecording = false;
|
frame.isCommandRecording = false;
|
||||||
// The wait proved every submission complete, so the full frame-boundary
|
// The wait proved every descriptor set this slot has in flight idle;
|
||||||
// drain applies: descriptor cursors, transient arenas, deferred
|
// rewind the reuse cursors so present-less readback loops stay
|
||||||
// texture/buffer releases, retired command buffers and the converted
|
// bounded (Present is the only other rewind point).
|
||||||
// vertex-stream cache all rewind here, keeping present-less readback
|
if (m_uniformManager) {
|
||||||
// loops bounded (Present is the only other drain point).
|
m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
|
||||||
TryDrainFrameTransients();
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6448,8 +6325,7 @@ void main() {
|
|||||||
if (RemapDefaultFboReadbackToGLOrientation(mapped, swapchainExtent, preTransform,
|
if (RemapDefaultFboReadbackToGLOrientation(mapped, swapchainExtent, preTransform,
|
||||||
sourceTexelSize,
|
sourceTexelSize,
|
||||||
remapped.data())) {
|
remapped.data())) {
|
||||||
PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, 1, format, type, pixels,
|
PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, format, type, pixels);
|
||||||
/*applyPackImageParams=*/false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6458,8 +6334,7 @@ void main() {
|
|||||||
width, height, swapchainExtent.width, swapchainExtent.height,
|
width, height, swapchainExtent.width, swapchainExtent.height,
|
||||||
static_cast<Int>(preTransform));
|
static_cast<Int>(preTransform));
|
||||||
}
|
}
|
||||||
PackReadbackToClientOrPbo(mapped, srcFormat, width, height, 1, format, type, pixels,
|
PackReadbackToClientOrPbo(mapped, srcFormat, width, height, format, type, pixels);
|
||||||
/*applyPackImageParams=*/false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void VulkanRenderer::GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
|
void VulkanRenderer::GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
|
||||||
@@ -6512,17 +6387,6 @@ void main() {
|
|||||||
if (width <= 0 || height <= 0) {
|
if (width <= 0 || height <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// GetTexImage returns every slice of a 3D level and every layer of an array
|
|
||||||
// level; GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply to the 3D/array
|
|
||||||
// destination layout (GL 3.3 section 6.1.4).
|
|
||||||
const auto imageTextureTarget = textureObject->GetTarget();
|
|
||||||
const Bool is3dImage = imageTextureTarget == TextureTarget::Texture3D;
|
|
||||||
const Bool isArrayImage = imageTextureTarget == TextureTarget::Texture1DArray ||
|
|
||||||
imageTextureTarget == TextureTarget::Texture2DArray ||
|
|
||||||
imageTextureTarget == TextureTarget::TextureCubeMapArray;
|
|
||||||
const GLsizei depthSlices = is3dImage ? std::max<GLsizei>(texelSize.z(), 1) : 1;
|
|
||||||
const GLsizei arrayLayers = isArrayImage ? static_cast<GLsizei>(resource->arrayLayers) : 1;
|
|
||||||
const GLsizei sliceCount = std::max<GLsizei>(depthSlices * arrayLayers, 1);
|
|
||||||
if (bufSize >= 0) {
|
if (bufSize >= 0) {
|
||||||
const Int dstChannels = GetReadbackChannelCount(format);
|
const Int dstChannels = GetReadbackChannelCount(format);
|
||||||
if ((type == GL_UNSIGNED_BYTE || type == GL_FLOAT) && dstChannels > 0) {
|
if ((type == GL_UNSIGNED_BYTE || type == GL_FLOAT) && dstChannels > 0) {
|
||||||
@@ -6543,8 +6407,7 @@ void main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const VkDeviceSize readbackSize = static_cast<VkDeviceSize>(width) *
|
const VkDeviceSize readbackSize = static_cast<VkDeviceSize>(width) *
|
||||||
static_cast<VkDeviceSize>(height) *
|
static_cast<VkDeviceSize>(height) * sourceTexelSize;
|
||||||
static_cast<VkDeviceSize>(sliceCount) * sourceTexelSize;
|
|
||||||
VkBufferObject readback;
|
VkBufferObject readback;
|
||||||
if (!readback.Create({
|
if (!readback.Create({
|
||||||
.allocator = m_allocator,
|
.allocator = m_allocator,
|
||||||
@@ -6572,9 +6435,8 @@ void main() {
|
|||||||
copyRegion.imageSubresource.aspectMask = resource->aspect;
|
copyRegion.imageSubresource.aspectMask = resource->aspect;
|
||||||
copyRegion.imageSubresource.mipLevel = static_cast<Uint32>(level);
|
copyRegion.imageSubresource.mipLevel = static_cast<Uint32>(level);
|
||||||
copyRegion.imageSubresource.baseArrayLayer = 0;
|
copyRegion.imageSubresource.baseArrayLayer = 0;
|
||||||
copyRegion.imageSubresource.layerCount = static_cast<Uint32>(arrayLayers);
|
copyRegion.imageSubresource.layerCount = 1;
|
||||||
copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height),
|
copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
||||||
static_cast<Uint32>(depthSlices)};
|
|
||||||
vkCmdCopyImageToBuffer(frame.commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
vkCmdCopyImageToBuffer(frame.commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
readback.GetHandle(), 1, ©Region);
|
readback.GetHandle(), 1, ©Region);
|
||||||
|
|
||||||
@@ -6600,8 +6462,7 @@ void main() {
|
|||||||
MGLOG_E("DirectVulkan::GetTextureImage skipped: failed to invalidate readback buffer");
|
MGLOG_E("DirectVulkan::GetTextureImage skipped: failed to invalidate readback buffer");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PackReadbackToClientOrPbo(mapped, resource->format, width, height, sliceCount, format, type, pixels,
|
PackReadbackToClientOrPbo(mapped, resource->format, width, height, format, type, pixels);
|
||||||
/*applyPackImageParams=*/is3dImage || isArrayImage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void VulkanRenderer::GenerateMipmap(GLenum target) {
|
void VulkanRenderer::GenerateMipmap(GLenum target) {
|
||||||
@@ -7215,9 +7076,6 @@ void main() {
|
|||||||
}
|
}
|
||||||
m_bufferManager.NotifyDeviceIdle();
|
m_bufferManager.NotifyDeviceIdle();
|
||||||
OnSubmitsCompletedUpTo(m_submitCounter);
|
OnSubmitsCompletedUpTo(m_submitCounter);
|
||||||
// The queue was just drained; take the free frame-boundary drain when
|
|
||||||
// nothing is recorded (present-less timer-query loops). No-op otherwise.
|
|
||||||
TryDrainFrameTransients();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7286,85 +7144,6 @@ void main() {
|
|||||||
vkDestroyFence(m_device, record.fence, nullptr);
|
vkDestroyFence(m_device, record.fence, nullptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Mid-frame-flushed command buffers whose submission just completed can
|
|
||||||
// be freed now; present-less flush loops have no other reclaim point.
|
|
||||||
m_frameContext.FreeRetiredCommandBuffersCompletedUpTo(m_completedSubmitCounter);
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool VulkanRenderer::TryDrainFrameTransients() {
|
|
||||||
if (m_device == VK_NULL_HANDLE || m_frameContext.GetFrameCount() == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (m_completedSubmitCounter != m_submitCounter) {
|
|
||||||
RefreshCompletedSubmits();
|
|
||||||
if (m_completedSubmitCounter != m_submitCounter) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (HasPendingRecordedWork()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Every submission is complete and nothing recorded references the
|
|
||||||
// per-frame transients. Pure-reclaim work runs on every drain: it only
|
|
||||||
// releases memory that is provably dead, never invalidates anything a
|
|
||||||
// later draw would have to rebuild. Raise the buffer manager's
|
|
||||||
// completed floor first so busy-tracking reflects the proven idleness.
|
|
||||||
m_bufferManager.NotifyDeviceIdle();
|
|
||||||
|
|
||||||
const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex();
|
|
||||||
m_frameContext.FreeAllRetiredCommandBuffers();
|
|
||||||
for (Uint32 slot = 0; slot < m_deferredDepthMipmapCleanup.size(); ++slot) {
|
|
||||||
CollectDeferredDepthMipmapCleanup(slot);
|
|
||||||
}
|
|
||||||
if (m_textureManager) {
|
|
||||||
m_textureManager->CollectAllDeferredReleases();
|
|
||||||
}
|
|
||||||
m_bufferManager.CollectAllDeferredReleases();
|
|
||||||
// Descriptor cursors rewind on every drain (the pre-drain readback path
|
|
||||||
// already did exactly this), keeping fence/readback loops' set usage bounded.
|
|
||||||
if (m_uniformManager) {
|
|
||||||
m_uniformManager->BeginFrame(frameIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Frame-boundary-equivalent work - transient arena rewind (which invalidates
|
|
||||||
// the conversion cache) and the cache-aging clocks - is gated to every 8th
|
|
||||||
// drain since the last Present: a presenting app's mid-frame readbacks/waits
|
|
||||||
// must neither force re-conversion/re-upload churn for the rest of the frame
|
|
||||||
// nor multiply the aging rate (which would shrink the 1024-boundary retire
|
|
||||||
// window and thrash periodically-used pipelines/programs), while present-less
|
|
||||||
// loops still rewind the arena and age their caches every 8 iterations -
|
|
||||||
// bounded by 8 iterations' transient usage.
|
|
||||||
++m_drainsSinceLastPresent;
|
|
||||||
if ((m_drainsSinceLastPresent % 8) != 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (m_textureManager) {
|
|
||||||
m_textureManager->BeginFrame(frameIndex);
|
|
||||||
}
|
|
||||||
m_bufferManager.BeginFrame(frameIndex);
|
|
||||||
// The cached conversion slices point into the transient arena the
|
|
||||||
// BeginFrame above just rewound; drop them together.
|
|
||||||
m_convertedVertexStreams.clear();
|
|
||||||
if (m_renderPassManager) {
|
|
||||||
m_renderPassManager->OnPresent();
|
|
||||||
}
|
|
||||||
// The pipeline memo can survive across these boundaries (no per-frame reset
|
|
||||||
// on this path), so it must drop whenever the sweep destroys anything.
|
|
||||||
if (m_programFactory) {
|
|
||||||
m_programFactory->OnFrameBoundary();
|
|
||||||
}
|
|
||||||
if (m_pipelineFactory && m_pipelineFactory->OnFrameBoundary() > 0) {
|
|
||||||
m_lastPipelineValid = false;
|
|
||||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
|
||||||
}
|
|
||||||
if (m_vertexInputStateFactory) {
|
|
||||||
m_vertexInputStateFactory->OnFrameBoundary();
|
|
||||||
}
|
|
||||||
if (m_samplerManager) {
|
|
||||||
m_samplerManager->OnFrameBoundary();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VkFence VulkanRenderer::AcquirePooledSubmitFence() {
|
VkFence VulkanRenderer::AcquirePooledSubmitFence() {
|
||||||
@@ -7427,10 +7206,6 @@ void main() {
|
|||||||
if (m_device == VK_NULL_HANDLE || m_graphicsQueue == VK_NULL_HANDLE || m_frameContext.GetFrameCount() == 0) {
|
if (m_device == VK_NULL_HANDLE || m_graphicsQueue == VK_NULL_HANDLE || m_frameContext.GetFrameCount() == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Non-blocking completion poll: gives flush-only workloads (no sync
|
|
||||||
// objects, no present) a point where finished submissions retire their
|
|
||||||
// pooled fences and mid-frame command buffers.
|
|
||||||
RefreshCompletedSubmits();
|
|
||||||
auto& frame = m_frameContext.GetCurrent();
|
auto& frame = m_frameContext.GetCurrent();
|
||||||
if (!frame.isCommandRecording && !frame.hasCommandBufferRecorded) {
|
if (!frame.isCommandRecording && !frame.hasCommandBufferRecorded) {
|
||||||
return false;
|
return false;
|
||||||
@@ -7455,14 +7230,6 @@ void main() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command-buffer boundary: the pipeline memo must not survive it, or a
|
|
||||||
// pipeline bound only through memo hits is never re-stamped in the factory
|
|
||||||
// cache and the aging sweep could destroy it while the flushed submission
|
|
||||||
// still references it. Mirrors the drops at the readback and Present
|
|
||||||
// boundaries; costs one full pipeline lookup on the next draw.
|
|
||||||
m_lastPipelineValid = false;
|
|
||||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
|
||||||
|
|
||||||
// The submitted command buffer may still be executing; recording must
|
// The submitted command buffer may still be executing; recording must
|
||||||
// restart on a fresh one. If none can be allocated, fall back to
|
// restart on a fresh one. If none can be allocated, fall back to
|
||||||
// draining this submission so reusing the buffer stays legal.
|
// draining this submission so reusing the buffer stays legal.
|
||||||
@@ -7514,10 +7281,6 @@ void main() {
|
|||||||
const VkResult result = vkWaitForFences(m_device, 1, &record.fence, VK_TRUE, timeoutNs);
|
const VkResult result = vkWaitForFences(m_device, 1, &record.fence, VK_TRUE, timeoutNs);
|
||||||
if (result == VK_SUCCESS) {
|
if (result == VK_SUCCESS) {
|
||||||
OnSubmitsCompletedUpTo(record.submitIndex);
|
OnSubmitsCompletedUpTo(record.submitIndex);
|
||||||
// The wait already stalled the pipeline; if it happens to
|
|
||||||
// have drained everything (present-less fence loops), take
|
|
||||||
// the free frame-boundary drain. No-op otherwise.
|
|
||||||
TryDrainFrameTransients();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (result != VK_TIMEOUT) {
|
if (result != VK_TIMEOUT) {
|
||||||
@@ -7590,86 +7353,24 @@ 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;
|
|
||||||
// The dropped recording is never submitted, so once the fence
|
|
||||||
// poll shows the pre-suspension submissions complete the frame
|
|
||||||
// transients (descriptor sets, transient arenas, deferred
|
|
||||||
// releases, conversion caches) can rewind; without this a
|
|
||||||
// minimized-window app accumulates them for the whole
|
|
||||||
// suspension.
|
|
||||||
TryDrainFrameTransients();
|
|
||||||
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) {
|
|
||||||
// Usable image with its acquire signal already armed; a rebuild is scheduled
|
|
||||||
// only if the surface genuinely no longer matches (see step 4 of Present).
|
|
||||||
m_swapchainResizeRequested = m_swapchainResizeRequested || SwapchainIsOutOfDate();
|
|
||||||
} else {
|
|
||||||
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();
|
||||||
// A real presented frame is the canonical aging cadence; mid-frame drains
|
|
||||||
// count against this and only age when presents stop coming.
|
|
||||||
m_drainsSinceLastPresent = 0;
|
|
||||||
// Age the content-addressed caches on the same frame-boundary cadence. Each
|
|
||||||
// keeps its own internal 256-sweep gate, so the per-frame cost is one counter
|
|
||||||
// increment and compare per cache; entries used by this frame's still-
|
|
||||||
// unsubmitted recording were stamped this boundary (every command-buffer
|
|
||||||
// boundary drops the pipeline memo, so the first draw of each recording
|
|
||||||
// performs a real, stamping lookup) and can never age out.
|
|
||||||
m_programFactory->OnFrameBoundary();
|
|
||||||
if (m_pipelineFactory->OnFrameBoundary() > 0) {
|
|
||||||
m_lastPipelineValid = false; // an aged-out pipeline may still be memoized
|
|
||||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
|
||||||
}
|
|
||||||
m_vertexInputStateFactory->OnFrameBoundary();
|
|
||||||
m_samplerManager->OnFrameBoundary();
|
|
||||||
auto& frame = m_frameContext.GetCurrent();
|
auto& frame = m_frameContext.GetCurrent();
|
||||||
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
||||||
if (activeRenderPass)
|
if (activeRenderPass)
|
||||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||||
|
|
||||||
// Transition while this frame's recording is still open. A frame that
|
|
||||||
// rendered only into FBOs has no default-framebuffer render pass, and that
|
|
||||||
// pass's finalLayout is the only other thing that carries the swapchain
|
|
||||||
// image to PRESENT_SRC_KHR - so closing the buffer first, which made
|
|
||||||
// TransitionToPresent refuse to record, handed the image to
|
|
||||||
// vkQueuePresentKHR in the layout it was acquired in (UNDEFINED on a fresh
|
|
||||||
// swapchain). The SetImageLayout below then made the tracker's
|
|
||||||
// disagreement with reality permanent for that image index.
|
|
||||||
const auto acquiredImageLayout = m_swapchainObject.GetImageLayout(m_imageIndexAcquired);
|
|
||||||
m_frameContext.TransitionToPresent(m_swapchainObject.GetImage(m_imageIndexAcquired), acquiredImageLayout);
|
|
||||||
|
|
||||||
if (frame.isCommandRecording) {
|
if (frame.isCommandRecording) {
|
||||||
m_frameContext.EndCommandRecording();
|
m_frameContext.EndCommandRecording();
|
||||||
frame.hasCommandBufferRecorded = true;
|
frame.hasCommandBufferRecorded = true;
|
||||||
m_lastPipelineValid = false; // command-buffer boundary: drop the pipeline memo
|
m_lastPipelineValid = false; // command-buffer boundary: drop the pipeline memo
|
||||||
}
|
}
|
||||||
|
|
||||||
const Bool shouldSubmitCommandBuffer = frame.hasCommandBufferRecorded;
|
const auto acquiredImageLayout = m_swapchainObject.GetImageLayout(m_imageIndexAcquired);
|
||||||
|
const Bool needsLayoutTransitionForPresent =
|
||||||
|
m_frameContext.TransitionToPresent(m_swapchainObject.GetImage(m_imageIndexAcquired), acquiredImageLayout);
|
||||||
|
const Bool shouldSubmitCommandBuffer = frame.hasCommandBufferRecorded || needsLayoutTransitionForPresent;
|
||||||
|
|
||||||
// 1) Submit current frame work.
|
// 1) Submit current frame work.
|
||||||
auto submitPacket = m_frameContext.GetSubmitInfo(shouldSubmitCommandBuffer, m_imageIndexAcquired);
|
auto submitPacket = m_frameContext.GetSubmitInfo(shouldSubmitCommandBuffer, m_imageIndexAcquired);
|
||||||
@@ -7683,43 +7384,16 @@ void main() {
|
|||||||
// 2) Present current frame.
|
// 2) Present current frame.
|
||||||
auto presentPacket = m_frameContext.GetPresentInfo(m_swapchainObject.GetHandle(), m_imageIndexAcquired);
|
auto presentPacket = m_frameContext.GetPresentInfo(m_swapchainObject.GetHandle(), m_imageIndexAcquired);
|
||||||
auto result = vkQueuePresentKHR(m_presentQueue, &presentPacket.presentInfo);
|
auto result = vkQueuePresentKHR(m_presentQueue, &presentPacket.presentInfo);
|
||||||
if (result == VK_SUBOPTIMAL_KHR) {
|
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
|
||||||
// Suboptimal is not a reason to rebuild on its own: a driver may report it for a
|
|
||||||
// surface whose size and orientation still match what we built from (Android does
|
|
||||||
// this routinely), and rebuilding on it alone destroys every pipeline and
|
|
||||||
// reallocates the default framebuffer once per frame - flicker, then garbage.
|
|
||||||
// Defer to the surface-capabilities comparison below.
|
|
||||||
result = VK_SUCCESS;
|
|
||||||
}
|
|
||||||
if (result == VK_ERROR_OUT_OF_DATE_KHR) {
|
|
||||||
MGLOG_D("Present, vkQueuePresentKHR got %d, recreating swapchain", result);
|
MGLOG_D("Present, vkQueuePresentKHR got %d, recreating swapchain", result);
|
||||||
if (!RecreateSwapchain()) {
|
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");
|
||||||
// The authoritative check, done here - after the frame is presented, before the next
|
|
||||||
// acquire. This is what makes a launcher-side resolution change take effect: shrinking
|
|
||||||
// the window's buffer (SurfaceHolder.setFixedSize) moves currentExtent, the swapchain
|
|
||||||
// follows, and the compositor scales the smaller image up to the view for free.
|
|
||||||
if (!m_swapchainResizeRequested && SwapchainIsOutOfDate()) {
|
|
||||||
m_swapchainResizeRequested = true;
|
|
||||||
}
|
|
||||||
if (m_swapchainResizeRequested) {
|
if (m_swapchainResizeRequested) {
|
||||||
MGLOG_D("Present, processing requested swapchain resize");
|
MGLOG_D("Present, processing requested swapchain resize");
|
||||||
if (!RecreateSwapchain()) {
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7728,23 +7402,9 @@ void main() {
|
|||||||
|
|
||||||
// 4) Wait/reset/acquire for next frame.
|
// 4) Wait/reset/acquire for next frame.
|
||||||
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_SUBOPTIMAL_KHR) {
|
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
|
||||||
// An image WAS acquired and its signal is armed on this slot's
|
|
||||||
// imageAvailableSemaphore, so the frame proceeds normally. Whether a rebuild is
|
|
||||||
// actually needed is decided by the surface-capabilities comparison at the next
|
|
||||||
// Present - suboptimal alone must not schedule one, or a driver that reports it
|
|
||||||
// every frame would rebuild every frame.
|
|
||||||
m_swapchainResizeRequested = m_swapchainResizeRequested || SwapchainIsOutOfDate();
|
|
||||||
result = VK_SUCCESS;
|
|
||||||
} else if (result == VK_ERROR_OUT_OF_DATE_KHR) {
|
|
||||||
// Nothing acquired, nothing signaled: safe to rebuild and re-acquire.
|
|
||||||
MGLOG_D("Present, vkAcquireNextImageKHR got %d, recreating swapchain", result);
|
MGLOG_D("Present, vkAcquireNextImageKHR got %d, recreating swapchain", result);
|
||||||
if (!RecreateSwapchain()) {
|
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);
|
||||||
@@ -7785,24 +7445,6 @@ void main() {
|
|||||||
|
|
||||||
m_validationLayersEnabled = m_config.EnableValidationLayers && validationLayerAvailable;
|
m_validationLayersEnabled = m_config.EnableValidationLayers && validationLayerAvailable;
|
||||||
|
|
||||||
// The debug messenger is a VK_EXT_debug_utils object, but a driver can ship
|
|
||||||
// the validation layers while exposing only the older VK_EXT_debug_report
|
|
||||||
// (Adreno 650 / Vulkan 1.1.128 does exactly that). Requesting the extension
|
|
||||||
// unconditionally tripped the required-extension assert below, aborting every
|
|
||||||
// validation-enabled build in CreateInstance. Keep the layers - they still
|
|
||||||
// validate, and on Android they report to logcat on their own - and drop only
|
|
||||||
// the messenger.
|
|
||||||
const Bool debugUtilsAvailable =
|
|
||||||
m_validationLayersEnabled && IsExtensionSupported(m_extensions, VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
|
|
||||||
// Without a reporting channel the layers validate but say nothing, so fall
|
|
||||||
// back to VK_EXT_debug_report when debug_utils is missing.
|
|
||||||
const Bool debugReportAvailable = m_validationLayersEnabled && !debugUtilsAvailable &&
|
|
||||||
IsExtensionSupported(m_extensions, VK_EXT_DEBUG_REPORT_EXTENSION_NAME);
|
|
||||||
if (m_validationLayersEnabled && !debugUtilsAvailable) {
|
|
||||||
MGLOG_I("%s not available; validation reports via %s instead.", VK_EXT_DEBUG_UTILS_EXTENSION_NAME,
|
|
||||||
debugReportAvailable ? VK_EXT_DEBUG_REPORT_EXTENSION_NAME : "(no channel)");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------- App info -------------------
|
// ---------------- App info -------------------
|
||||||
VkApplicationInfo appInfo = {};
|
VkApplicationInfo appInfo = {};
|
||||||
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
|
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
|
||||||
@@ -7853,10 +7495,8 @@ void main() {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (debugUtilsAvailable) {
|
if (m_validationLayersEnabled) {
|
||||||
exts.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
|
exts.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
|
||||||
} else if (debugReportAvailable) {
|
|
||||||
exts.push_back(VK_EXT_DEBUG_REPORT_EXTENSION_NAME);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MGLOG_I("Enabling %d Vulkan instance extensions:", exts.size());
|
MGLOG_I("Enabling %d Vulkan instance extensions:", exts.size());
|
||||||
@@ -7881,8 +7521,7 @@ void main() {
|
|||||||
MGLOG_I("Enabling validation layer...");
|
MGLOG_I("Enabling validation layer...");
|
||||||
instanceInfo.enabledLayerCount = static_cast<uint32_t>(std::size(s_validationLayerNames));
|
instanceInfo.enabledLayerCount = static_cast<uint32_t>(std::size(s_validationLayerNames));
|
||||||
instanceInfo.ppEnabledLayerNames = s_validationLayerNames;
|
instanceInfo.ppEnabledLayerNames = s_validationLayerNames;
|
||||||
// Chaining the messenger create-info is only legal with the extension on.
|
instanceInfo.pNext = &debugMessengerCreateInfo;
|
||||||
instanceInfo.pNext = debugUtilsAvailable ? &debugMessengerCreateInfo : nullptr;
|
|
||||||
} else {
|
} else {
|
||||||
instanceInfo.enabledLayerCount = 0;
|
instanceInfo.enabledLayerCount = 0;
|
||||||
instanceInfo.pNext = nullptr;
|
instanceInfo.pNext = nullptr;
|
||||||
@@ -7890,40 +7529,7 @@ void main() {
|
|||||||
|
|
||||||
VK_VERIFY(vkCreateInstance(&instanceInfo, nullptr, &m_instance), "vkCreateInstance failed");
|
VK_VERIFY(vkCreateInstance(&instanceInfo, nullptr, &m_instance), "vkCreateInstance failed");
|
||||||
|
|
||||||
if (debugUtilsAvailable) {
|
if (m_validationLayersEnabled) VK_VERIFY(SetupDebugMessenger());
|
||||||
VK_VERIFY(SetupDebugMessenger());
|
|
||||||
} else if (debugReportAvailable) {
|
|
||||||
VK_VERIFY(SetupDebugReportCallback());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static VKAPI_ATTR VkBool32 VKAPI_CALL DebugReportCallback(VkDebugReportFlagsEXT flags, VkDebugReportObjectTypeEXT,
|
|
||||||
Uint64, size_t, Int32 messageCode, const char* pLayerPrefix,
|
|
||||||
const char* pMessage, void*) {
|
|
||||||
if ((flags & (VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT |
|
|
||||||
VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT)) != 0) {
|
|
||||||
MGLOG_F("[Vulkan %s %d] %s", pLayerPrefix ? pLayerPrefix : "?", messageCode, pMessage ? pMessage : "");
|
|
||||||
}
|
|
||||||
return VK_FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
VkResult VulkanRenderer::SetupDebugReportCallback() {
|
|
||||||
auto vkCreateDebugReportCallbackEXT =
|
|
||||||
(PFN_vkCreateDebugReportCallbackEXT)vkGetInstanceProcAddr(m_instance, "vkCreateDebugReportCallbackEXT");
|
|
||||||
if (!vkCreateDebugReportCallbackEXT) return VK_ERROR_EXTENSION_NOT_PRESENT;
|
|
||||||
VkDebugReportCallbackCreateInfoEXT createInfo{VK_STRUCTURE_TYPE_DEBUG_REPORT_CALLBACK_CREATE_INFO_EXT};
|
|
||||||
createInfo.flags = VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT |
|
|
||||||
VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT;
|
|
||||||
createInfo.pfnCallback = &DebugReportCallback;
|
|
||||||
return vkCreateDebugReportCallbackEXT(m_instance, &createInfo, nullptr, &m_debugReportCallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
void VulkanRenderer::DestroyDebugReportCallback() {
|
|
||||||
if (m_debugReportCallback == VK_NULL_HANDLE) return;
|
|
||||||
auto func = (PFN_vkDestroyDebugReportCallbackEXT)vkGetInstanceProcAddr(m_instance,
|
|
||||||
"vkDestroyDebugReportCallbackEXT");
|
|
||||||
if (func != nullptr) func(m_instance, m_debugReportCallback, nullptr);
|
|
||||||
m_debugReportCallback = VK_NULL_HANDLE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VkResult VulkanRenderer::SetupDebugMessenger() {
|
VkResult VulkanRenderer::SetupDebugMessenger() {
|
||||||
@@ -8205,18 +7811,6 @@ void main() {
|
|||||||
|
|
||||||
const Vector<VkExtensionProperties> availableExtensions = EnumerateDeviceExtensions(m_physicalDevice.handle);
|
const Vector<VkExtensionProperties> availableExtensions = EnumerateDeviceExtensions(m_physicalDevice.handle);
|
||||||
ResolveOptionalDeviceExtensions(availableExtensions, enabledDeviceExtensions);
|
ResolveOptionalDeviceExtensions(availableExtensions, enabledDeviceExtensions);
|
||||||
|
|
||||||
// VK_KHR_image_format_list lets a MUTABLE_FORMAT image declare exactly which formats it
|
|
||||||
// may be viewed as. Adreno drops UBWC bandwidth compression on a blindly-mutable image
|
|
||||||
// (measured: 65 -> 80 fps in MC 26.2 once mutability is not requested); an explicit,
|
|
||||||
// compression-compatible format list is the portable way to keep both.
|
|
||||||
m_imageFormatListExtensionEnabled =
|
|
||||||
IsExtensionSupported(availableExtensions, VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME);
|
|
||||||
if (m_imageFormatListExtensionEnabled) {
|
|
||||||
enabledDeviceExtensions.push_back(VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME);
|
|
||||||
}
|
|
||||||
MGLOG_I("VK_KHR_image_format_list enabled: %s",
|
|
||||||
m_imageFormatListExtensionEnabled ? "true" : "false");
|
|
||||||
MGLOG_I("VK_KHR_draw_indirect_count enabled: %s", m_drawIndirectCountExtensionEnabled ? "true" : "false");
|
MGLOG_I("VK_KHR_draw_indirect_count enabled: %s", m_drawIndirectCountExtensionEnabled ? "true" : "false");
|
||||||
|
|
||||||
m_indexTypeUint8ExtensionEnabled = false;
|
m_indexTypeUint8ExtensionEnabled = false;
|
||||||
@@ -8462,7 +8056,6 @@ 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
|
||||||
@@ -8624,13 +8217,13 @@ void main() {
|
|||||||
m_swapchainObject.Shutdown(m_device);
|
m_swapchainObject.Shutdown(m_device);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool VulkanRenderer::RecreateSwapchain() {
|
void 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 false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
vkDeviceWaitIdle(m_device);
|
vkDeviceWaitIdle(m_device);
|
||||||
@@ -8674,45 +8267,12 @@ 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 {
|
||||||
return m_physicalDevice;
|
return m_physicalDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool VulkanRenderer::SwapchainIsOutOfDate() {
|
|
||||||
if (m_surface == VK_NULL_HANDLE || m_swapchainObject.GetHandle() == VK_NULL_HANDLE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
VkSurfaceCapabilitiesKHR surfaceCaps{};
|
|
||||||
if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_physicalDevice.handle, m_surface, &surfaceCaps) !=
|
|
||||||
VK_SUCCESS) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// A driver-defined currentExtent (UINT32_MAX) means the surface takes its size from the
|
|
||||||
// swapchain, so there is nothing to compare against - the app's requested size wins and
|
|
||||||
// only an explicit RequestSwapchainResize can change it.
|
|
||||||
if (surfaceCaps.currentExtent.width == UINT32_MAX || surfaceCaps.currentExtent.height == UINT32_MAX) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Compare in SURFACE space against the extent the live swapchain was created from. Using
|
|
||||||
// the swapchain's own (quarter-turn swapped) extent here would report a difference on
|
|
||||||
// every rotated frame and rebuild forever.
|
|
||||||
const VkExtent2D builtFrom = m_swapchainObject.GetSurfaceExtent();
|
|
||||||
const Bool extentChanged = surfaceCaps.currentExtent.width != builtFrom.width ||
|
|
||||||
surfaceCaps.currentExtent.height != builtFrom.height;
|
|
||||||
const Bool transformChanged = surfaceCaps.currentTransform != m_swapchainObject.GetPreTransform();
|
|
||||||
if (!extentChanged && !transformChanged) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
MGLOG_I("Swapchain out of date: surface %ux%u transform %u -> %ux%u transform %u",
|
|
||||||
builtFrom.width, builtFrom.height, static_cast<Uint32>(m_swapchainObject.GetPreTransform()),
|
|
||||||
surfaceCaps.currentExtent.width, surfaceCaps.currentExtent.height,
|
|
||||||
static_cast<Uint32>(surfaceCaps.currentTransform));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void VulkanRenderer::RequestSwapchainResize(Uint32 width, Uint32 height) {
|
void VulkanRenderer::RequestSwapchainResize(Uint32 width, Uint32 height) {
|
||||||
width = std::max<Uint32>(width, 1);
|
width = std::max<Uint32>(width, 1);
|
||||||
height = std::max<Uint32>(height, 1);
|
height = std::max<Uint32>(height, 1);
|
||||||
@@ -8809,43 +8369,6 @@ void main() {
|
|||||||
m_computePipelines.clear();
|
m_computePipelines.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void VulkanRenderer::OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) {
|
|
||||||
if (m_pipelineFactory == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// The render-pass sweep's >1024-boundary idle guarantee covers these pipelines
|
|
||||||
// too (they are only bound by draws that hit the dying entries), so the factory
|
|
||||||
// destroys them immediately. The memo must drop as well: it can hand out a
|
|
||||||
// cached handle without touching the factory.
|
|
||||||
if (m_pipelineFactory->EvictByRenderPasses(renderPasses) > 0) {
|
|
||||||
m_lastPipelineValid = false;
|
|
||||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void VulkanRenderer::OnProgramEvicted(ProgramFactory::HashType programHash,
|
|
||||||
VkDescriptorSetLayout descriptorSetLayout) {
|
|
||||||
// Same >1024-boundary idleness as the program entry: its compute pipeline is
|
|
||||||
// only dispatched, and its graphics pipelines only bound, through paths that
|
|
||||||
// stamp the entry, so immediate destruction is GPU-safe. (The graphics memo
|
|
||||||
// never holds compute pipelines; it only needs invalidating for the factory
|
|
||||||
// eviction below.)
|
|
||||||
const auto computeIt = m_computePipelines.find(programHash);
|
|
||||||
if (computeIt != m_computePipelines.end()) {
|
|
||||||
if (computeIt->second != VK_NULL_HANDLE && m_device != VK_NULL_HANDLE) {
|
|
||||||
vkDestroyPipeline(m_device, computeIt->second, nullptr);
|
|
||||||
}
|
|
||||||
m_computePipelines.erase(computeIt);
|
|
||||||
}
|
|
||||||
if (m_pipelineFactory != nullptr && m_pipelineFactory->EvictByProgramHash(programHash) > 0) {
|
|
||||||
m_lastPipelineValid = false;
|
|
||||||
m_lastPipelineResult = VK_NULL_HANDLE;
|
|
||||||
}
|
|
||||||
if (m_uniformManager != nullptr) {
|
|
||||||
m_uniformManager->OnDescriptorSetLayoutDestroyed(descriptorSetLayout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VkPipeline VulkanRenderer::GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj) {
|
VkPipeline VulkanRenderer::GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj) {
|
||||||
const auto it = m_computePipelines.find(programObj.hash);
|
const auto it = m_computePipelines.find(programObj.hash);
|
||||||
if (it != m_computePipelines.end()) {
|
if (it != m_computePipelines.end()) {
|
||||||
|
|||||||
@@ -114,10 +114,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class VulkanRenderer : public IBufferCopyCommandProvider,
|
class VulkanRenderer : public IBufferCopyCommandProvider, public FrameContext::IRecordingObserver {
|
||||||
public FrameContext::IRecordingObserver,
|
|
||||||
public VkRenderPassManager::IEvictionObserver,
|
|
||||||
public ProgramFactory::IEvictionObserver {
|
|
||||||
public:
|
public:
|
||||||
VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg = {});
|
VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg = {});
|
||||||
~VulkanRenderer();
|
~VulkanRenderer();
|
||||||
@@ -134,20 +131,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// recording, before any render pass.
|
// recording, before any render pass.
|
||||||
void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) override;
|
void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) override;
|
||||||
|
|
||||||
// VkRenderPassManager::IEvictionObserver: the render-pass aging sweep just
|
|
||||||
// destroyed these VkRenderPasses; evict every graphics pipeline hashed on a
|
|
||||||
// dying handle (they share its >1024-boundary idleness, so immediate
|
|
||||||
// destruction is safe) and drop the last-pipeline memo if any went.
|
|
||||||
void OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) override;
|
|
||||||
|
|
||||||
// ProgramFactory::IEvictionObserver: an aged-out program entry was
|
|
||||||
// destroyed; evict its compute pipeline and graphics pipelines (same
|
|
||||||
// idleness guarantee - they are only bound through draws/dispatches that
|
|
||||||
// stamp the program entry) and purge the descriptor-set cache entries
|
|
||||||
// keyed by its now-recyclable VkDescriptorSetLayout handle.
|
|
||||||
void OnProgramEvicted(ProgramFactory::HashType programHash,
|
|
||||||
VkDescriptorSetLayout descriptorSetLayout) override;
|
|
||||||
|
|
||||||
Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
||||||
const DrawCmdParam& drawParams,
|
const DrawCmdParam& drawParams,
|
||||||
const IndexBufferView* pIndexBufferView = nullptr);
|
const IndexBufferView* pIndexBufferView = nullptr);
|
||||||
@@ -274,13 +257,7 @@ 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);
|
||||||
// Re-query the surface and report whether the live swapchain no longer matches it
|
void RecreateSwapchain();
|
||||||
// (size or orientation). This - not a VK_SUBOPTIMAL_KHR result - is what decides a
|
|
||||||
// rebuild, so a surface the driver merely considers suboptimal cannot thrash.
|
|
||||||
Bool SwapchainIsOutOfDate();
|
|
||||||
// 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 {
|
||||||
@@ -366,26 +343,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkFence AcquirePooledSubmitFence();
|
VkFence AcquirePooledSubmitFence();
|
||||||
void DestroySubmitFencePool();
|
void DestroySubmitFencePool();
|
||||||
Bool HasPendingRecordedWork() const;
|
Bool HasPendingRecordedWork() const;
|
||||||
// Frame-boundary housekeeping for paths that never reach Present's
|
|
||||||
// tail (present-less readback loops, suspended presentation, blocking
|
|
||||||
// sync waits): runs the same per-frame drains Present performs, but
|
|
||||||
// only when every queue submission has been observed complete AND no
|
|
||||||
// recorded-but-unsubmitted commands exist - i.e. when CPU-GPU overlap
|
|
||||||
// is provably already zero. Never blocks (non-blocking fence poll
|
|
||||||
// only), so the presenting path's frames-in-flight pipelining is
|
|
||||||
// untouched. Returns true when the drain ran.
|
|
||||||
Bool TryDrainFrameTransients();
|
|
||||||
|
|
||||||
Vector<SubmitRecord> m_inFlightSubmits;
|
Vector<SubmitRecord> m_inFlightSubmits;
|
||||||
Vector<VkFence> m_freeSubmitFences;
|
Vector<VkFence> m_freeSubmitFences;
|
||||||
Uint64 m_submitCounter = 0;
|
Uint64 m_submitCounter = 0;
|
||||||
Uint64 m_completedSubmitCounter = 0;
|
Uint64 m_completedSubmitCounter = 0;
|
||||||
// Drains since the last Present, gating the drain's frame-boundary-equivalent
|
|
||||||
// work (arena rewind + cache aging): a presenting app's mid-frame
|
|
||||||
// readbacks/waits must neither churn the transient caches nor accelerate the
|
|
||||||
// aging clocks, while present-less loops still cross a boundary every few
|
|
||||||
// iterations. Reset in Present.
|
|
||||||
Uint32 m_drainsSinceLastPresent = 0;
|
|
||||||
|
|
||||||
NativeWindowType m_window = 0;
|
NativeWindowType m_window = 0;
|
||||||
void* m_platformDisplay = nullptr;
|
void* m_platformDisplay = nullptr;
|
||||||
@@ -393,19 +355,12 @@ 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;
|
||||||
Vector<VkExtensionProperties> m_extensions;
|
Vector<VkExtensionProperties> m_extensions;
|
||||||
VkInstance m_instance = VK_NULL_HANDLE;
|
VkInstance m_instance = VK_NULL_HANDLE;
|
||||||
VkDebugUtilsMessengerEXT m_debugMessenger = VK_NULL_HANDLE;
|
VkDebugUtilsMessengerEXT m_debugMessenger = VK_NULL_HANDLE;
|
||||||
// Fallback reporting channel for drivers that ship the validation layers but
|
|
||||||
// only expose the older VK_EXT_debug_report (Adreno 650 / Vulkan 1.1.128).
|
|
||||||
VkDebugReportCallbackEXT m_debugReportCallback = VK_NULL_HANDLE;
|
|
||||||
PhysicalDevice m_physicalDevice;
|
PhysicalDevice m_physicalDevice;
|
||||||
VkDevice m_device = VK_NULL_HANDLE;
|
VkDevice m_device = VK_NULL_HANDLE;
|
||||||
VmaAllocator m_allocator = nullptr;
|
VmaAllocator m_allocator = nullptr;
|
||||||
@@ -556,8 +511,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void CreateInstance();
|
void CreateInstance();
|
||||||
VkResult SetupDebugMessenger();
|
VkResult SetupDebugMessenger();
|
||||||
VkResult DestroyDebugMessenger();
|
VkResult DestroyDebugMessenger();
|
||||||
VkResult SetupDebugReportCallback();
|
|
||||||
void DestroyDebugReportCallback();
|
|
||||||
VkDebugUtilsMessengerCreateInfoEXT PopulateDebugMessengerCreateInfo();
|
VkDebugUtilsMessengerCreateInfoEXT PopulateDebugMessengerCreateInfo();
|
||||||
void CreateSurface();
|
void CreateSurface();
|
||||||
void PickPhysicalDevice();
|
void PickPhysicalDevice();
|
||||||
@@ -576,10 +529,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const RenderPassEntry& renderPassEntry);
|
const RenderPassEntry& renderPassEntry);
|
||||||
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
|
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
|
||||||
void DestroyComputePipelines();
|
void DestroyComputePipelines();
|
||||||
// Takes the frame rather than a command buffer: a first-time storage-usage upgrade has to
|
|
||||||
// flush the pending recording (see the body), which retires the current command buffer.
|
|
||||||
Bool PrepareStorageImageTextures(
|
Bool PrepareStorageImageTextures(
|
||||||
FrameContext::FrameData& frame,
|
VkCommandBuffer commandBuffer,
|
||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj);
|
const ProgramFactory::VkProgramObject& programObj);
|
||||||
|
|
||||||
@@ -641,10 +592,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const PhysicalDevice& compareWithDevice,
|
const PhysicalDevice& compareWithDevice,
|
||||||
PhysicalDevice& outBetterDevice);
|
PhysicalDevice& outBetterDevice);
|
||||||
static constexpr const char* s_validationLayerNames[] = {"VK_LAYER_KHRONOS_validation"};
|
static constexpr const char* s_validationLayerNames[] = {"VK_LAYER_KHRONOS_validation"};
|
||||||
// VK_KHR_image_format_list: lets MUTABLE_FORMAT images declare their exact view-format
|
|
||||||
// set so the driver can keep bandwidth compression (see CreateLogicalDeviceAndQueues).
|
|
||||||
Bool m_imageFormatListExtensionEnabled = false;
|
|
||||||
|
|
||||||
static constexpr const char* s_deviceExtensionNames[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
|
static constexpr const char* s_deviceExtensionNames[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
|
||||||
static Bool CheckValidationLayerSupport();
|
static Bool CheckValidationLayerSupport();
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ namespace MobileGL::MG_Impl::CGLImpl {
|
|||||||
GLint Samples = 0;
|
GLint Samples = 0;
|
||||||
GLint Profile = kCGLOGLPVersion_3_2_Core;
|
GLint Profile = kCGLOGLPVersion_3_2_Core;
|
||||||
GLint RendererId = 0x4d474c;
|
GLint RendererId = 0x4d474c;
|
||||||
GLint DisplayMask = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ContextObject {
|
struct ContextObject {
|
||||||
@@ -135,9 +134,6 @@ namespace MobileGL::MG_Impl::CGLImpl {
|
|||||||
case kCGLPFARendererID:
|
case kCGLPFARendererID:
|
||||||
pixelFormat.RendererId = value;
|
pixelFormat.RendererId = value;
|
||||||
break;
|
break;
|
||||||
case kCGLPFADisplayMask:
|
|
||||||
pixelFormat.DisplayMask = value;
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -347,9 +343,6 @@ namespace MobileGL::MG_Impl::CGLImpl {
|
|||||||
case kCGLPFARendererID:
|
case kCGLPFARendererID:
|
||||||
*value = pixelFormat->RendererId;
|
*value = pixelFormat->RendererId;
|
||||||
return kCGLNoError;
|
return kCGLNoError;
|
||||||
case kCGLPFADisplayMask:
|
|
||||||
*value = pixelFormat->DisplayMask;
|
|
||||||
return kCGLNoError;
|
|
||||||
case kCGLPFAOpenGLProfile:
|
case kCGLPFAOpenGLProfile:
|
||||||
*value = pixelFormat->Profile;
|
*value = pixelFormat->Profile;
|
||||||
return kCGLNoError;
|
return kCGLNoError;
|
||||||
@@ -488,32 +481,6 @@ namespace MobileGL::MG_Impl::CGLImpl {
|
|||||||
return it == currentContexts.end() ? nullptr : it->second;
|
return it == currentContexts.end() ? nullptr : it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
CGLError SetVirtualScreen(CGLContextObj ctx, GLint screen) {
|
|
||||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
|
||||||
auto* object = TryGetContext(ctx);
|
|
||||||
if (!object) {
|
|
||||||
return kCGLBadContext;
|
|
||||||
}
|
|
||||||
if (screen != 0) {
|
|
||||||
return kCGLBadValue;
|
|
||||||
}
|
|
||||||
object->VirtualScreen = screen;
|
|
||||||
return kCGLNoError;
|
|
||||||
}
|
|
||||||
|
|
||||||
CGLError GetVirtualScreen(CGLContextObj ctx, GLint* screen) {
|
|
||||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
|
||||||
auto* object = TryGetContext(ctx);
|
|
||||||
if (!object) {
|
|
||||||
return kCGLBadContext;
|
|
||||||
}
|
|
||||||
if (!screen) {
|
|
||||||
return kCGLBadAddress;
|
|
||||||
}
|
|
||||||
*screen = object->VirtualScreen;
|
|
||||||
return kCGLNoError;
|
|
||||||
}
|
|
||||||
|
|
||||||
CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) {
|
CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) {
|
||||||
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
||||||
auto* object = TryGetContext(ctx);
|
auto* object = TryGetContext(ctx);
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ namespace MobileGL::MG_Impl::CGLImpl {
|
|||||||
|
|
||||||
CGLError SetCurrentContext(CGLContextObj ctx);
|
CGLError SetCurrentContext(CGLContextObj ctx);
|
||||||
CGLContextObj GetCurrentContext();
|
CGLContextObj GetCurrentContext();
|
||||||
CGLError SetVirtualScreen(CGLContextObj ctx, GLint screen);
|
|
||||||
CGLError GetVirtualScreen(CGLContextObj ctx, GLint* screen);
|
|
||||||
CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params);
|
CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params);
|
||||||
CGLError GetParameter(CGLContextObj ctx, CGLContextParameter pname, GLint* params);
|
CGLError GetParameter(CGLContextObj ctx, CGLContextParameter pname, GLint* params);
|
||||||
CGLError UpdateContext(CGLContextObj ctx);
|
CGLError UpdateContext(CGLContextObj ctx);
|
||||||
|
|||||||
@@ -71,14 +71,6 @@ MOBILEGL_CGL_API CGLContextObj CGLGetCurrentContext(void) {
|
|||||||
return MobileGL::MG_Impl::CGLImpl::GetCurrentContext();
|
return MobileGL::MG_Impl::CGLImpl::GetCurrentContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
MOBILEGL_CGL_API CGLError CGLSetVirtualScreen(CGLContextObj ctx, GLint screen) {
|
|
||||||
return MobileGL::MG_Impl::CGLImpl::SetVirtualScreen(ctx, screen);
|
|
||||||
}
|
|
||||||
|
|
||||||
MOBILEGL_CGL_API CGLError CGLGetVirtualScreen(CGLContextObj ctx, GLint* screen) {
|
|
||||||
return MobileGL::MG_Impl::CGLImpl::GetVirtualScreen(ctx, screen);
|
|
||||||
}
|
|
||||||
|
|
||||||
MOBILEGL_CGL_API CGLError CGLSetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) {
|
MOBILEGL_CGL_API CGLError CGLSetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) {
|
||||||
return MobileGL::MG_Impl::CGLImpl::SetParameter(ctx, pname, params);
|
return MobileGL::MG_Impl::CGLImpl::SetParameter(ctx, pname, params);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,8 @@
|
|||||||
|
|
||||||
#if defined(__APPLE__)
|
#if defined(__APPLE__)
|
||||||
|
|
||||||
#include "MG_Impl/CGLImpl/CGLImpl.h"
|
|
||||||
#include "MG_Impl/GetProcAddress.h"
|
#include "MG_Impl/GetProcAddress.h"
|
||||||
|
|
||||||
#include <CoreGraphics/CoreGraphics.h>
|
|
||||||
#include <CoreVideo/CVDisplayLink.h>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -51,52 +47,10 @@ namespace {
|
|||||||
return dlsym(handle, symbol);
|
return dlsym(handle, symbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
CGDirectDisplayID DisplayForMask(GLint displayMask) {
|
|
||||||
constexpr std::uint32_t MaxDisplays = sizeof(CGOpenGLDisplayMask) * 8;
|
|
||||||
CGDirectDisplayID displays[MaxDisplays] = {};
|
|
||||||
std::uint32_t displayCount = 0;
|
|
||||||
if (displayMask != 0 &&
|
|
||||||
CGGetActiveDisplayList(MaxDisplays, displays, &displayCount) == kCGErrorSuccess) {
|
|
||||||
const auto mask = static_cast<CGOpenGLDisplayMask>(displayMask);
|
|
||||||
for (std::uint32_t i = 0; i < displayCount; ++i) {
|
|
||||||
if ((CGDisplayIDToOpenGLDisplayMask(displays[i]) & mask) != 0) {
|
|
||||||
return displays[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return CGMainDisplayID();
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma clang diagnostic push
|
|
||||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
||||||
CVReturn MobileGLCVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(
|
|
||||||
CVDisplayLinkRef displayLink,
|
|
||||||
CGLContextObj context,
|
|
||||||
CGLPixelFormatObj pixelFormat) {
|
|
||||||
GLint virtualScreen = 0;
|
|
||||||
if (MobileGL::MG_Impl::CGLImpl::GetVirtualScreen(context, &virtualScreen) == kCGLNoError) {
|
|
||||||
GLint displayMask = 0;
|
|
||||||
if (!displayLink ||
|
|
||||||
MobileGL::MG_Impl::CGLImpl::DescribePixelFormat(
|
|
||||||
pixelFormat, virtualScreen, kCGLPFADisplayMask, &displayMask) != kCGLNoError) {
|
|
||||||
return kCVReturnInvalidArgument;
|
|
||||||
}
|
|
||||||
return CVDisplayLinkSetCurrentCGDisplay(displayLink, DisplayForMask(displayMask));
|
|
||||||
}
|
|
||||||
|
|
||||||
using OriginalFunction = CVReturn (*)(CVDisplayLinkRef, CGLContextObj, CGLPixelFormatObj);
|
|
||||||
static const auto original = reinterpret_cast<OriginalFunction>(
|
|
||||||
dlsym(RTLD_NEXT, "CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext"));
|
|
||||||
return original ? original(displayLink, context, pixelFormat) : kCVReturnError;
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((used)) static const DyldInterposeEntry kMobileGLDyldInterpose[]
|
__attribute__((used)) static const DyldInterposeEntry kMobileGLDyldInterpose[]
|
||||||
__attribute__((section("__DATA,__interpose"))) = {
|
__attribute__((section("__DATA,__interpose"))) = {
|
||||||
{reinterpret_cast<const void*>(MobileGLDlsym), reinterpret_cast<const void*>(dlsym)},
|
{reinterpret_cast<const void*>(MobileGLDlsym), reinterpret_cast<const void*>(dlsym)},
|
||||||
{reinterpret_cast<const void*>(MobileGLCVDisplayLinkSetCurrentCGDisplayFromOpenGLContext),
|
|
||||||
reinterpret_cast<const void*>(CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext)},
|
|
||||||
};
|
};
|
||||||
#pragma clang diagnostic pop
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
# Public CGL entry points.
|
|
||||||
_CGL*
|
|
||||||
|
|
||||||
# Public EGL entry points.
|
|
||||||
_egl*
|
|
||||||
|
|
||||||
# Public OpenGL and GLX entry points. OpenGL function names always use an
|
|
||||||
# uppercase letter or digit after the "gl" prefix; excluding lowercase here
|
|
||||||
# deliberately prevents glslang_* from matching this pattern.
|
|
||||||
_gl[A-Z0-9]*
|
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
#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>
|
||||||
@@ -26,17 +25,6 @@ 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) {
|
||||||
@@ -61,8 +49,6 @@ 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
|
||||||
@@ -201,7 +187,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) {
|
EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) {
|
||||||
auto* state = GetStateEnsureInitialized();
|
auto* state = GetState();
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return EGL_FALSE;
|
return EGL_FALSE;
|
||||||
}
|
}
|
||||||
@@ -222,7 +208,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EGLDisplay GetDisplay(NativeDisplayType display) {
|
EGLDisplay GetDisplay(NativeDisplayType display) {
|
||||||
auto* state = GetStateEnsureInitialized();
|
auto* state = GetState();
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return EGL_NO_DISPLAY;
|
return EGL_NO_DISPLAY;
|
||||||
}
|
}
|
||||||
@@ -327,14 +313,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +345,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EGLBoolean BindAPI(EGLenum api) {
|
EGLBoolean BindAPI(EGLenum api) {
|
||||||
auto* state = GetStateEnsureInitialized();
|
auto* state = GetState();
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return EGL_FALSE;
|
return EGL_FALSE;
|
||||||
}
|
}
|
||||||
@@ -400,7 +378,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
char const* QueryString(EGLDisplay display, EGLint name) {
|
char const* QueryString(EGLDisplay display, EGLint name) {
|
||||||
auto* state = GetStateEnsureInitialized();
|
auto* state = GetState();
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -663,7 +641,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 = GetStateEnsureInitialized();
|
auto* state = GetState();
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return EGL_NO_DISPLAY;
|
return EGL_NO_DISPLAY;
|
||||||
}
|
}
|
||||||
@@ -759,7 +737,6 @@ 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,7 +2144,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace FramebufferImpl {
|
namespace FramebufferImpl {
|
||||||
// Leak-at-exit storage; see GlobalObjects.cpp.
|
UniquePtr<DefaultFramebufferInfo> pDefaultFramebufferInfo;
|
||||||
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
|
||||||
|
|||||||
@@ -133,33 +133,4 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
values[0] = value;
|
values[0] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DestroyAllSyncObjects() {
|
|
||||||
// Detach the registry under the lock, release outside it. Entries the app
|
|
||||||
// already deleted were erased by DeleteSync, so nothing here double-frees;
|
|
||||||
// a DeleteSync racing this sweep finds an empty registry and returns. A
|
|
||||||
// thread still blocked inside ClientWaitSync/GetSynciv during teardown
|
|
||||||
// holds a raw SyncObject* these deletes invalidate - the same undefined
|
|
||||||
// race an app-driven DeleteSync already has.
|
|
||||||
UnorderedMap<GLsync, SyncObject*> orphans;
|
|
||||||
{
|
|
||||||
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
|
|
||||||
orphans.swap(g_liveSyncObjects);
|
|
||||||
}
|
|
||||||
if (orphans.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Both backends' DeleteSync only free the heap wrapper once their GL
|
|
||||||
// context/renderer is gone (generation/current-thread guards), so this is
|
|
||||||
// safe after the backend has released its EGL resources - but not after
|
|
||||||
// the function table itself is cleared.
|
|
||||||
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
|
|
||||||
for (const auto& [_, syncObject] : orphans) {
|
|
||||||
if (backendDeleteSync && syncObject->backendHandle) {
|
|
||||||
backendDeleteSync(syncObject->backendHandle);
|
|
||||||
}
|
|
||||||
delete syncObject;
|
|
||||||
}
|
|
||||||
MGLOG_D("DestroyAllSyncObjects: reclaimed %zu sync object(s) the app left undeleted", orphans.size());
|
|
||||||
}
|
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -16,12 +16,4 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout);
|
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout);
|
||||||
void DeleteSync(GLsync sync);
|
void DeleteSync(GLsync sync);
|
||||||
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values);
|
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values);
|
||||||
// Destroys every still-registered sync object exactly as DeleteSync would.
|
|
||||||
// GL requires syncs to die with their context; called only from full library
|
|
||||||
// teardown (DestroyImpl), where no context survives on any thread, so the
|
|
||||||
// process-global registry can be drained wholesale. Must run while the
|
|
||||||
// backend function table is still populated: each backend handle has to be
|
|
||||||
// released by the backend that created it, never by a later re-initialized
|
|
||||||
// one.
|
|
||||||
void DestroyAllSyncObjects();
|
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -14,8 +14,7 @@
|
|||||||
#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 {
|
||||||
// Leak-at-exit storage; see GlobalObjects.cpp.
|
UniquePtr<ProxyTextureManager> pProxyTextureManager;
|
||||||
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
|
||||||
|
|||||||
@@ -85,8 +85,6 @@ namespace MobileGL::MG_Impl {
|
|||||||
GETPROC(CGLGetPixelFormat, name);
|
GETPROC(CGLGetPixelFormat, name);
|
||||||
GETPROC(CGLSetCurrentContext, name);
|
GETPROC(CGLSetCurrentContext, name);
|
||||||
GETPROC(CGLGetCurrentContext, name);
|
GETPROC(CGLGetCurrentContext, name);
|
||||||
GETPROC(CGLSetVirtualScreen, name);
|
|
||||||
GETPROC(CGLGetVirtualScreen, name);
|
|
||||||
GETPROC(CGLSetParameter, name);
|
GETPROC(CGLSetParameter, name);
|
||||||
GETPROC(CGLGetParameter, name);
|
GETPROC(CGLGetParameter, name);
|
||||||
GETPROC(CGLUpdateContext, name);
|
GETPROC(CGLUpdateContext, name);
|
||||||
|
|||||||
@@ -29,19 +29,10 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
|||||||
char kContextViewKey;
|
char kContextViewKey;
|
||||||
char kContextLayerKey;
|
char kContextLayerKey;
|
||||||
|
|
||||||
|
std::once_flag g_installOnce;
|
||||||
IMP g_pixelFormatDealloc = nullptr;
|
IMP g_pixelFormatDealloc = nullptr;
|
||||||
IMP g_contextDealloc = nullptr;
|
IMP g_contextDealloc = nullptr;
|
||||||
|
|
||||||
std::mutex& HookInstallMutex() {
|
|
||||||
static auto* mutex = new std::mutex();
|
|
||||||
return *mutex;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool& HooksInstalled() {
|
|
||||||
static auto* installed = new Bool(false);
|
|
||||||
return *installed;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename Fn>
|
template <typename Fn>
|
||||||
Fn ObjcMsgSend() {
|
Fn ObjcMsgSend() {
|
||||||
return reinterpret_cast<Fn>(objc_msgSend);
|
return reinterpret_cast<Fn>(objc_msgSend);
|
||||||
@@ -440,12 +431,12 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
|||||||
method_setImplementation(method, replacement);
|
method_setImplementation(method, replacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool InstallHooksOnce() {
|
void InstallHooksOnce() {
|
||||||
Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat");
|
Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat");
|
||||||
Class contextClass = objc_getClass("NSOpenGLContext");
|
Class contextClass = objc_getClass("NSOpenGLContext");
|
||||||
if (!pixelFormatClass || !contextClass) {
|
if (!pixelFormatClass || !contextClass) {
|
||||||
MGLOG_W("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
|
MGLOG_W("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ReplaceInstanceMethod(pixelFormatClass, "initWithAttributes:",
|
ReplaceInstanceMethod(pixelFormatClass, "initWithAttributes:",
|
||||||
@@ -480,34 +471,11 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
|||||||
ReplaceInstanceMethod(contextClass, "dealloc", reinterpret_cast<IMP>(ContextDealloc), &g_contextDealloc);
|
ReplaceInstanceMethod(contextClass, "dealloc", reinterpret_cast<IMP>(ContextDealloc), &g_contextDealloc);
|
||||||
|
|
||||||
MGLOG_I("NSOpenGLImpl hooks installed");
|
MGLOG_I("NSOpenGLImpl hooks installed");
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void InstallHooks() {
|
void InstallHooks() {
|
||||||
const std::lock_guard<std::mutex> lock(HookInstallMutex());
|
std::call_once(g_installOnce, InstallHooksOnce);
|
||||||
if (!HooksInstalled()) {
|
|
||||||
// Do not permanently consume the install attempt when the OpenGL
|
|
||||||
// framework has not registered its Objective-C classes yet. The
|
|
||||||
// dyld bootstrap normally runs after framework dependencies, but
|
|
||||||
// an explicitly loaded/static-linked MobileGL can arrive earlier.
|
|
||||||
HooksInstalled() = InstallHooksOnce();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} // namespace MobileGL::MG_Impl::NSOpenGLImpl
|
} // namespace MobileGL::MG_Impl::NSOpenGLImpl
|
||||||
|
|
||||||
namespace {
|
|
||||||
// SDL's Cocoa backend creates NSOpenGLPixelFormat/NSOpenGLContext before
|
|
||||||
// its first dlsym("glGetString") or other MobileGL host-API call. Install
|
|
||||||
// only the lightweight Objective-C dispatch hooks while the injected dylib
|
|
||||||
// is loading so those first Cocoa objects are routed through CGLImpl. The
|
|
||||||
// hooked context constructor reaches EGLImpl::GetDisplay(), which performs
|
|
||||||
// the full, thread-safe MobileGL initialization outside this bootstrap.
|
|
||||||
//
|
|
||||||
// There is intentionally no matching destructor: backend teardown remains
|
|
||||||
// owned by the EGL lifecycle and process-exit globals remain leak-at-exit.
|
|
||||||
__attribute__((constructor)) void BootstrapNSOpenGLHooks() {
|
|
||||||
MobileGL::MG_Impl::NSOpenGLImpl::InstallHooks();
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
// 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
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
; 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
|
|
||||||
@@ -1,732 +0,0 @@
|
|||||||
// 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
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
// 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,26 +368,6 @@ 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);
|
||||||
@@ -1435,7 +1415,6 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
} // namespace EGLState
|
} // namespace EGLState
|
||||||
|
|
||||||
// Leak-at-exit storage; see GlobalObjects.cpp.
|
UniquePtr<EGLState::EGLContext> pEGLContext;
|
||||||
UniquePtr<EGLState::EGLContext>& pEGLContext = *new UniquePtr<EGLState::EGLContext>();
|
|
||||||
} // namespace MG_State
|
} // namespace MG_State
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
@@ -39,10 +39,6 @@ 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,
|
||||||
@@ -266,6 +262,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,6 +721,5 @@ namespace MobileGL::MG_State {
|
|||||||
}
|
}
|
||||||
} // namespace GLState
|
} // namespace GLState
|
||||||
|
|
||||||
// Leak-at-exit storage; see GlobalObjects.cpp.
|
UniquePtr<GLState::GLContext> pGLContext;
|
||||||
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
|
||||||
|
|||||||
@@ -334,32 +334,16 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// draw. The memo is keyed by (backendStateVersion, flags); ResetLinkArtifacts and
|
// draw. The memo is keyed by (backendStateVersion, flags); ResetLinkArtifacts and
|
||||||
// the binding setters below invalidate it by bumping m_backendStateVersion.
|
// the binding setters below invalidate it by bumping m_backendStateVersion.
|
||||||
Bool GetBackendHashMemo(Uint flags, Uint64& outHash) const {
|
Bool GetBackendHashMemo(Uint flags, Uint64& outHash) const {
|
||||||
if (m_backendHashMemoVersion != m_backendStateVersion) return false;
|
if (m_backendHashMemoVersion != m_backendStateVersion || m_backendHashMemoFlags != flags) {
|
||||||
for (const auto& slot : m_backendHashMemoSlots) {
|
return false;
|
||||||
if (slot.valid && slot.flags == flags) {
|
|
||||||
outHash = slot.hash;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
outHash = m_backendHashMemo;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
void SetBackendHashMemo(Uint flags, Uint64 hash) const {
|
void SetBackendHashMemo(Uint flags, Uint64 hash) const {
|
||||||
if (m_backendHashMemoVersion != m_backendStateVersion) {
|
m_backendHashMemo = hash;
|
||||||
for (auto& slot : m_backendHashMemoSlots) slot.valid = false;
|
m_backendHashMemoVersion = m_backendStateVersion;
|
||||||
m_backendHashMemoVersion = m_backendStateVersion;
|
m_backendHashMemoFlags = flags;
|
||||||
m_backendHashMemoNextSlot = 0;
|
|
||||||
}
|
|
||||||
for (auto& slot : m_backendHashMemoSlots) {
|
|
||||||
if (slot.valid && slot.flags == flags) {
|
|
||||||
slot.hash = hash;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
auto& slot = m_backendHashMemoSlots[m_backendHashMemoNextSlot];
|
|
||||||
slot.flags = flags;
|
|
||||||
slot.hash = hash;
|
|
||||||
slot.valid = true;
|
|
||||||
m_backendHashMemoNextSlot = (m_backendHashMemoNextSlot + 1) % kBackendHashMemoSlotCount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetUniformSamplerOrImageUnitIndex(Uint location, Int unit) {
|
void SetUniformSamplerOrImageUnitIndex(Uint location, Int unit) {
|
||||||
@@ -543,19 +527,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
Uint32 m_backendStateVersion = 0;
|
Uint32 m_backendStateVersion = 0;
|
||||||
|
|
||||||
// Backend-owned content-hash memo (see GetBackendHashMemo): valid only while
|
// Backend-owned content-hash memo (see GetBackendHashMemo): valid only while
|
||||||
// m_backendStateVersion matches. Several slots, not one: a backend may resolve the same
|
// m_backendStateVersion and the compile flags match the recorded values.
|
||||||
// program under more than one compile-flag set within a frame (surface rotation, and the
|
mutable Uint64 m_backendHashMemo = 0;
|
||||||
// explicit-LOD sampling variant), and a single slot would then miss on every lookup and
|
|
||||||
// re-hash the program's whole SPIR-V once per draw.
|
|
||||||
static constexpr SizeT kBackendHashMemoSlotCount = 4;
|
|
||||||
struct BackendHashMemoSlot {
|
|
||||||
Uint64 hash = 0;
|
|
||||||
Uint flags = 0;
|
|
||||||
Bool valid = false;
|
|
||||||
};
|
|
||||||
mutable Array<BackendHashMemoSlot, kBackendHashMemoSlotCount> m_backendHashMemoSlots{};
|
|
||||||
mutable SizeT m_backendHashMemoNextSlot = 0;
|
|
||||||
mutable Uint32 m_backendHashMemoVersion = ~0u;
|
mutable Uint32 m_backendHashMemoVersion = ~0u;
|
||||||
|
mutable Uint m_backendHashMemoFlags = 0;
|
||||||
Uint32 m_uboContentVersion = 0;
|
Uint32 m_uboContentVersion = 0;
|
||||||
Uint32 m_linkVersion = 0;
|
Uint32 m_linkVersion = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,11 +15,7 @@
|
|||||||
#include <MG_Util/Math/VectorTypes.h>
|
#include <MG_Util/Math/VectorTypes.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
// Texture objects are always SharedPtr-owned (TextureState creates every instance via
|
class ITextureObject {
|
||||||
// MakeShared, including the per-target default objects). enable_shared_from_this lets
|
|
||||||
// backends that only receive a reference (e.g. syncing a name-deleted texture kept
|
|
||||||
// alive by an FBO attachment) still register a weak liveness reference for GC.
|
|
||||||
class ITextureObject : public std::enable_shared_from_this<ITextureObject> {
|
|
||||||
public:
|
public:
|
||||||
using TargetEnum = TextureTarget;
|
using TargetEnum = TextureTarget;
|
||||||
virtual ~ITextureObject() = default;
|
virtual ~ITextureObject() = default;
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
||||||
#include <MG_Util/Debug/Log.h>
|
#include <MG_Util/Debug/Log.h>
|
||||||
#include <FastSTL/UnorderedMap.h>
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
class DynamicParameterBackend final : public MobileGL::MG_Backend::BackendObject {
|
class DynamicParameterBackend final : public MobileGL::MG_Backend::BackendObject {
|
||||||
@@ -1737,61 +1736,3 @@ TEST(DirectGLESStateGuards, DefaultFramebufferBindGoesThroughShadow) {
|
|||||||
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, 7); // must reach the driver again
|
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, 7); // must reach the driver again
|
||||||
EXPECT_EQ(mocks.log.Count("BindFramebuffer:"), 3u);
|
EXPECT_EQ(mocks.log.Count("BindFramebuffer:"), 3u);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FastSTL::unordered_map::erase(iterator) regression coverage. The open-addressing
|
|
||||||
// iterator constructor snaps forward from a tombstoned slot to the successor, so
|
|
||||||
// erase must NOT advance the rebuilt iterator again: the old double-advance skipped
|
|
||||||
// one live element per erase, and erasing the element in the highest occupied
|
|
||||||
// bucket pushed the returned index past bucket_count where it never compared equal
|
|
||||||
// to end() again - erase-while-iterating sweeps (pipeline/program cache eviction)
|
|
||||||
// then ran off the bucket array and fed garbage handles to vkDestroyPipeline
|
|
||||||
// (device crash on first mass eviction during world load).
|
|
||||||
TEST(FastSTLSanity, EraseWhileIteratingVisitsEveryElementExactlyOnce) {
|
|
||||||
FastSTL::unordered_map<MobileGL::Uint64, MobileGL::Uint64> map;
|
|
||||||
constexpr MobileGL::Uint64 kCount = 1000;
|
|
||||||
for (MobileGL::Uint64 key = 0; key < kCount; ++key) {
|
|
||||||
map.emplace(key * 0x9e3779b97f4a7c15ull, key);
|
|
||||||
}
|
|
||||||
ASSERT_EQ(map.size(), kCount);
|
|
||||||
|
|
||||||
MobileGL::SizeT visited = 0;
|
|
||||||
for (auto it = map.begin(); it != map.end();) {
|
|
||||||
it = map.erase(it);
|
|
||||||
++visited;
|
|
||||||
ASSERT_LE(visited, kCount); // old code: runaway past end / skipped entries
|
|
||||||
}
|
|
||||||
EXPECT_EQ(visited, kCount);
|
|
||||||
EXPECT_EQ(map.size(), 0u);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(FastSTLSanity, EraseReturnsTheSuccessorElement) {
|
|
||||||
FastSTL::unordered_map<MobileGL::Uint32, MobileGL::Uint32> map;
|
|
||||||
for (MobileGL::Uint32 key = 1; key <= 64; ++key) {
|
|
||||||
map.emplace(key, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Erasing every other visited element must still visit all 64 exactly once:
|
|
||||||
// the iterator returned by erase names the very next element, not one past it.
|
|
||||||
MobileGL::SizeT visited = 0;
|
|
||||||
MobileGL::SizeT erased = 0;
|
|
||||||
for (auto it = map.begin(); it != map.end();) {
|
|
||||||
++visited;
|
|
||||||
if ((visited & 1) != 0) {
|
|
||||||
it = map.erase(it);
|
|
||||||
++erased;
|
|
||||||
} else {
|
|
||||||
++it;
|
|
||||||
}
|
|
||||||
ASSERT_LE(visited, 64u);
|
|
||||||
}
|
|
||||||
EXPECT_EQ(visited, 64u);
|
|
||||||
EXPECT_EQ(map.size(), 64u - erased);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(FastSTLSanity, ErasingTheOnlyElementReturnsEnd) {
|
|
||||||
FastSTL::unordered_map<MobileGL::Uint32, MobileGL::Uint32> map;
|
|
||||||
map.emplace(42u, 1u);
|
|
||||||
auto next = map.erase(map.begin());
|
|
||||||
EXPECT_EQ(next, map.end());
|
|
||||||
EXPECT_TRUE(map.empty());
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,12 +9,7 @@
|
|||||||
#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)
|
#if !defined(__WIN32) && !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
|
||||||
|
|
||||||
@@ -65,14 +60,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
static void* OpenLib(const Vector<String>& names) {
|
static void* OpenLib(const Vector<String>& names) {
|
||||||
#if defined(_WIN32)
|
#if !defined(__WIN32) && !defined(_WIN32) && (!defined(__APPLE__) || defined(MOBILEGL_IOS))
|
||||||
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/",
|
||||||
@@ -116,9 +104,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline void* ProcAddress(void* lib, const char* name) {
|
inline void* ProcAddress(void* lib, const char* name) {
|
||||||
#if defined(_WIN32)
|
#if !defined(__WIN32) && !defined(_WIN32) && (!defined(__APPLE__) || defined(MOBILEGL_IOS))
|
||||||
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;
|
||||||
@@ -534,15 +520,6 @@ 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) {
|
||||||
@@ -564,7 +541,6 @@ 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");
|
||||||
|
|||||||
@@ -133,11 +133,9 @@ namespace MobileGL {
|
|||||||
case TextureInternalFormat::RGBA8Snorm:
|
case TextureInternalFormat::RGBA8Snorm:
|
||||||
return VK_FORMAT_R8G8B8A8_SNORM;
|
return VK_FORMAT_R8G8B8A8_SNORM;
|
||||||
case TextureInternalFormat::RGB10A2:
|
case TextureInternalFormat::RGB10A2:
|
||||||
// GL_UNSIGNED_INT_2_10_10_10_REV puts R in bits 0-9, which is Vulkan's
|
return VK_FORMAT_A2R10G10B10_UNORM_PACK32;
|
||||||
// A2B10G10R10 layout - A2R10G10B10 silently swaps R and B on upload.
|
|
||||||
return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
|
|
||||||
case TextureInternalFormat::RGB10A2UI:
|
case TextureInternalFormat::RGB10A2UI:
|
||||||
return VK_FORMAT_A2B10G10R10_UINT_PACK32;
|
return VK_FORMAT_A2R10G10B10_UINT_PACK32;
|
||||||
case TextureInternalFormat::RGBA16:
|
case TextureInternalFormat::RGBA16:
|
||||||
return VK_FORMAT_R16G16B16A16_UNORM;
|
return VK_FORMAT_R16G16B16A16_UNORM;
|
||||||
case TextureInternalFormat::RGBA16Snorm:
|
case TextureInternalFormat::RGBA16Snorm:
|
||||||
|
|||||||
+1
-1
Submodule include/FastSTL updated: 022211c998...34f55f9df2
@@ -87,7 +87,7 @@ def main():
|
|||||||
ap.add_argument("--device-dir", default="/data/local/tmp/mgcts")
|
ap.add_argument("--device-dir", default="/data/local/tmp/mgcts")
|
||||||
ap.add_argument("--surface", default="fbo", help="--deqp-surface-type value")
|
ap.add_argument("--surface", default="fbo", help="--deqp-surface-type value")
|
||||||
ap.add_argument("--max-rounds", type=int, default=4000)
|
ap.add_argument("--max-rounds", type=int, default=4000)
|
||||||
ap.add_argument("--max-empty-streak", type=int, default=64,
|
ap.add_argument("--max-empty-streak", type=int, default=8,
|
||||||
help="abort after this many consecutive chunks that produce no log at all")
|
help="abort after this many consecutive chunks that produce no log at all")
|
||||||
ap.add_argument("--min-mem-kb", type=int, default=400000,
|
ap.add_argument("--min-mem-kb", type=int, default=400000,
|
||||||
help="pause when the device drops below this much available memory")
|
help="pause when the device drops below this much available memory")
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
; 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'
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,445 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
#!/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())
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
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."
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
@@ -1,472 +0,0 @@
|
|||||||
// 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