mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
[Feat] (MG_Impl, MG_Backend): GL_ARB_timer_query on both backends
Implements GL timer queries end to end: a frontend query registry (modeled on the sync module - mutex-guarded objects wrapping opaque backend handles behind optional function pointers) serving glGenQueries/glBeginQuery/glEndQuery(GL_TIME_ELAPSED)/glQueryCounter (GL_TIMESTAMP)/glGetQueryObject*/glGetQueryiv with GL 3.3 error semantics and a graceful zero-result fallback when a backend cannot time. DirectGLES backs spans with GL_EXT_disjoint_timer_query (context- generation-stamped handles, bounded result waits). DirectVulkan gets a VkTimerQueryManager: per-frame-in-flight timestamp query pools reset at command-buffer begin (outside render passes), records harvested by frame serial before their pool recycles, elapsed = masked tick delta x timestampPeriod; handles are stamped with a renderer generation that also now guards fence syncs across renderer recreation. GL_QUERY_ COUNTER_BITS reports 0 unless the live backend can actually time (dynamic IsTimerQuerySupported hook), and a failed blocking read keeps the handle alive so the real value stays reachable once the frame submits. GL_ARB_timer_query is advertised only when the device supports timing and MOBILEGL_DISABLE_TIMERQUERY is unset - LWJGL keys Minecraft's F3 'GPU: x%' line off exactly that extension string; verified on device (Adreno 830) on both backends. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -222,6 +222,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp
|
MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp
|
||||||
MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp
|
MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp
|
||||||
MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp
|
MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp
|
||||||
|
MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp
|
||||||
|
|
||||||
MobileGL/MG_Impl/Init.cpp
|
MobileGL/MG_Impl/Init.cpp
|
||||||
MobileGL/MG_Impl/GetProcAddress.cpp
|
MobileGL/MG_Impl/GetProcAddress.cpp
|
||||||
@@ -249,6 +250,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp
|
MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp
|
||||||
MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.cpp
|
MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.cpp
|
||||||
MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp
|
MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp
|
||||||
|
MobileGL/MG_Backend/DirectVulkan/Renderer/VkTimerQueryManager.cpp
|
||||||
MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp
|
MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp
|
||||||
MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp
|
MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp
|
||||||
MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp
|
MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ namespace MobileGL {
|
|||||||
// and released by GLFunctionsTable::DeleteSync.
|
// and released by GLFunctionsTable::DeleteSync.
|
||||||
using BackendSyncHandle = void*;
|
using BackendSyncHandle = void*;
|
||||||
|
|
||||||
|
// Opaque backend timer-query handle, created by
|
||||||
|
// GLFunctionsTable::BeginTimeElapsedQuery / QueryCounterTimestamp and
|
||||||
|
// released by GLFunctionsTable::DeleteBackendQuery.
|
||||||
|
using BackendQueryHandle = void*;
|
||||||
|
|
||||||
struct GLFunctionsTable {
|
struct GLFunctionsTable {
|
||||||
void (*DrawArrays)(GLenum mode, GLint first, GLsizei count);
|
void (*DrawArrays)(GLenum mode, GLint first, GLsizei count);
|
||||||
void (*DrawElements)(GLenum mode, GLsizei count, GLenum type, const void* indices);
|
void (*DrawElements)(GLenum mode, GLsizei count, GLenum type, const void* indices);
|
||||||
@@ -171,6 +176,31 @@ namespace MobileGL {
|
|||||||
void (*WaitSync)(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
|
void (*WaitSync)(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
|
||||||
void (*DeleteSync)(BackendSyncHandle sync);
|
void (*DeleteSync)(BackendSyncHandle sync);
|
||||||
Bool (*GetSyncStatus)(BackendSyncHandle sync); // true = signaled
|
Bool (*GetSyncStatus)(BackendSyncHandle sync); // true = signaled
|
||||||
|
// GL timer-query objects (GL_ARB_timer_query). All entries are
|
||||||
|
// optional (may be null); the frontend then falls back to zero
|
||||||
|
// results and reports GL_QUERY_COUNTER_BITS == 0.
|
||||||
|
// BeginTimeElapsedQuery / QueryCounterTimestamp may themselves
|
||||||
|
// return null when the backend cannot create a query right now;
|
||||||
|
// the frontend treats such a query as immediately available with
|
||||||
|
// a zero result.
|
||||||
|
// Dynamic support check: true only when the live backend can
|
||||||
|
// actually time at the moment of the call (extension / entry
|
||||||
|
// points / timestamp valid bits are known then, not at table
|
||||||
|
// init). Gates the advertised GL_QUERY_COUNTER_BITS.
|
||||||
|
Bool (*IsTimerQuerySupported)();
|
||||||
|
BackendQueryHandle (*BeginTimeElapsedQuery)(); // starts a TIME_ELAPSED span
|
||||||
|
void (*EndTimeElapsedQuery)(BackendQueryHandle query); // ends the span
|
||||||
|
BackendQueryHandle (*QueryCounterTimestamp)(); // glQueryCounter(GL_TIMESTAMP) one-shot
|
||||||
|
Bool (*IsQueryResultAvailable)(BackendQueryHandle query); // non-blocking
|
||||||
|
// Returns true when a final value was produced (*outNanoseconds
|
||||||
|
// written; the frontend may cache it and release the handle).
|
||||||
|
// Returns false when the result could not be obtained YET - e.g.
|
||||||
|
// a Vulkan wait that refuses to block on a not-yet-submitted
|
||||||
|
// frame serial - in which case the frontend must keep the handle
|
||||||
|
// and leave the query readable later.
|
||||||
|
Bool (*GetQueryResult64)(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
|
||||||
|
void (*DeleteBackendQuery)(BackendQueryHandle query);
|
||||||
|
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
|
||||||
};
|
};
|
||||||
struct GlobalBackendFunctionsTable {
|
struct GlobalBackendFunctionsTable {
|
||||||
GLFunctionsTable GL;
|
GLFunctionsTable GL;
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
|
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
|
||||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||||
|
#include <Config.h>
|
||||||
|
#include <algorithm>
|
||||||
#include <format>
|
#include <format>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectGLES {
|
namespace MobileGL::MG_Backend::DirectGLES {
|
||||||
@@ -176,7 +178,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
Flags<PixelFormatNormalizeOptionBit> GetForcedPixelFormatNormalizeOptions() {
|
Flags<PixelFormatNormalizeOptionBit> GetForcedPixelFormatNormalizeOptions() {
|
||||||
Flags<PixelFormatNormalizeOptionBit> options;
|
Flags<PixelFormatNormalizeOptionBit> options;
|
||||||
if (g_GLESCapabilities.GLESRendererString.find("ANGLE") != String::npos) {
|
if (g_GLESCapabilities.IsAngleRenderer) {
|
||||||
options |= PixelFormatNormalizeOptionBit::NoRgb16;
|
options |= PixelFormatNormalizeOptionBit::NoRgb16;
|
||||||
options |= PixelFormatNormalizeOptionBit::NoSnorm16;
|
options |= PixelFormatNormalizeOptionBit::NoSnorm16;
|
||||||
options |= PixelFormatNormalizeOptionBit::NoSnorm8;
|
options |= PixelFormatNormalizeOptionBit::NoSnorm8;
|
||||||
@@ -612,6 +614,57 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The advertised renderer info must be mutable after its first use:
|
||||||
|
// E_GL_ARB_timer_query can only be decided once the ES capabilities
|
||||||
|
// are known, long after the list is first read (see
|
||||||
|
// UpdateAdvertisedTimerQueryExtension below).
|
||||||
|
RendererInfo& MutableRendererInfo() {
|
||||||
|
static RendererInfo rendererInfo = {
|
||||||
|
.RendererName = "Espryt", // Renderer Name
|
||||||
|
.BackendName = "Direct (OpenGL ES)", // Backend Name
|
||||||
|
.ExtraVendor = Nullopt, // Extra vendor
|
||||||
|
.RendererGLInfo =
|
||||||
|
{
|
||||||
|
.TargetGLVersion = {3, 3, 0}, // Target OpenGL Version
|
||||||
|
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
||||||
|
.Extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32, // OpenGL Extensions
|
||||||
|
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
|
||||||
|
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||||
|
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object,
|
||||||
|
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
||||||
|
E_GL_ARB_texture_storage, E_GL_ARB_direct_state_access,
|
||||||
|
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
|
||||||
|
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
|
||||||
|
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding,
|
||||||
|
E_GL_ARB_shader_image_size},
|
||||||
|
.IsCompatibilityProfile = false // Is Compatibility Profile
|
||||||
|
},
|
||||||
|
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
|
||||||
|
};
|
||||||
|
return rendererInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension
|
||||||
|
// string via glGetStringi plus non-null glQueryCounter and
|
||||||
|
// glGetQueryObject(u)i64v entries). GetRendererInfo is first invoked
|
||||||
|
// from LogBackendInfo() during MG_Backend::Init, BEFORE any ES
|
||||||
|
// context or capabilities exist, so the advertisement cannot be baked
|
||||||
|
// into the static initializer above; it is reconciled here at the end
|
||||||
|
// of InitCapabilities instead (mirroring DirectVulkan's mutable
|
||||||
|
// m_rendererInfo + UpdateAdvertisedExtensions). InitCapabilities
|
||||||
|
// completes inside the first MakeEGLCurrent on a context, so an app
|
||||||
|
// thread can only observe the extension string after the
|
||||||
|
// advertisement for its context has settled; erase-then-append keeps
|
||||||
|
// the re-run after a context recreation idempotent.
|
||||||
|
void UpdateAdvertisedTimerQueryExtension() {
|
||||||
|
auto& extensions = MutableRendererInfo().RendererGLInfo.Extensions;
|
||||||
|
extensions.erase(std::remove(extensions.begin(), extensions.end(), E_GL_ARB_timer_query),
|
||||||
|
extensions.end());
|
||||||
|
if (AreTimerQueriesSupported() && !MG_Config::Features.DisableTimerQuery) {
|
||||||
|
extensions.push_back(E_GL_ARB_timer_query);
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
BackendObject_DirectGLES::~BackendObject_DirectGLES() {
|
BackendObject_DirectGLES::~BackendObject_DirectGLES() {
|
||||||
@@ -648,6 +701,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
|
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
|
||||||
|
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query,
|
||||||
|
// reconcile the E_GL_ARB_timer_query advertisement (see the comment on
|
||||||
|
// UpdateAdvertisedTimerQueryExtension for why it cannot happen when
|
||||||
|
// the extension list is first built).
|
||||||
|
UpdateAdvertisedTimerQueryExtension();
|
||||||
UpdateDynamicBackendParameters();
|
UpdateDynamicBackendParameters();
|
||||||
ProbeGLESFormatCapabilities(m_GLESFunctions, MutableFormatCapabilities(), m_dynamicParameters);
|
ProbeGLESFormatCapabilities(m_GLESFunctions, MutableFormatCapabilities(), m_dynamicParameters);
|
||||||
PrintFormatCapabilities(GetFormatCapabilities());
|
PrintFormatCapabilities(GetFormatCapabilities());
|
||||||
@@ -773,29 +831,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RendererInfo& BackendObject_DirectGLES::GetRendererInfo() const {
|
const RendererInfo& BackendObject_DirectGLES::GetRendererInfo() const {
|
||||||
static RendererInfo RendererInfo = {
|
return MutableRendererInfo();
|
||||||
.RendererName = "Espryt", // Renderer Name
|
|
||||||
.BackendName = "Direct (OpenGL ES)", // Backend Name
|
|
||||||
.ExtraVendor = Nullopt, // Extra vendor
|
|
||||||
.RendererGLInfo =
|
|
||||||
{
|
|
||||||
.TargetGLVersion = {3, 3, 0}, // Target OpenGL Version
|
|
||||||
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
|
||||||
.Extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32, // OpenGL Extensions
|
|
||||||
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
|
|
||||||
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
|
||||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object,
|
|
||||||
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
|
||||||
E_GL_ARB_texture_storage, E_GL_ARB_direct_state_access,
|
|
||||||
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
|
|
||||||
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
|
|
||||||
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding,
|
|
||||||
E_GL_ARB_shader_image_size},
|
|
||||||
.IsCompatibilityProfile = false // Is Compatibility Profile
|
|
||||||
},
|
|
||||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
|
|
||||||
};
|
|
||||||
return RendererInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String BackendObject_DirectGLES::GetBackendAPIVersionString() const {
|
String BackendObject_DirectGLES::GetBackendAPIVersionString() const {
|
||||||
@@ -872,6 +908,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
funcsTable.GL.WaitSync = WaitSync;
|
funcsTable.GL.WaitSync = WaitSync;
|
||||||
funcsTable.GL.DeleteSync = DeleteSync;
|
funcsTable.GL.DeleteSync = DeleteSync;
|
||||||
funcsTable.GL.GetSyncStatus = GetSyncStatus;
|
funcsTable.GL.GetSyncStatus = GetSyncStatus;
|
||||||
|
// Optional timer-query group: left null (the frontend then falls
|
||||||
|
// back) when disabled via MOBILEGL_DISABLE_TIMERQUERY. The hooks
|
||||||
|
// themselves additionally degrade to null handles / zero results
|
||||||
|
// when GL_EXT_disjoint_timer_query or its entry points are
|
||||||
|
// missing, or when the calling thread does not own the ES
|
||||||
|
// context.
|
||||||
|
if (!MG_Config::Features.DisableTimerQuery) {
|
||||||
|
// AreTimerQueriesSupported is a pure capability read (no
|
||||||
|
// current ES context required, false until the caps are
|
||||||
|
// filled in), which is exactly the dynamic support check
|
||||||
|
// the frontend wants from IsTimerQuerySupported.
|
||||||
|
funcsTable.GL.IsTimerQuerySupported = AreTimerQueriesSupported;
|
||||||
|
funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
|
||||||
|
funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
|
||||||
|
funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp;
|
||||||
|
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
||||||
|
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
||||||
|
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||||
|
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
|
||||||
|
}
|
||||||
funcsTableInitialized = true;
|
funcsTableInitialized = true;
|
||||||
}
|
}
|
||||||
return funcsTable;
|
return funcsTable;
|
||||||
|
|||||||
@@ -25,7 +25,9 @@
|
|||||||
#include <MG_Util/Converters/MGToGL/RenderStateEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/RenderStateEnumConverter.h>
|
||||||
#include <MG_Util/Metrics/BufferMetrics.h>
|
#include <MG_Util/Metrics/BufferMetrics.h>
|
||||||
#include <MG_Util/Texture/PixelStoreProcessor.h>
|
#include <MG_Util/Texture/PixelStoreProcessor.h>
|
||||||
|
#include <Config.h>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -2445,7 +2447,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
void MemoryBarrier(GLbitfield barriers) {
|
void MemoryBarrier(GLbitfield barriers) {
|
||||||
g_GLESFuncs.glMemoryBarrier(barriers);
|
g_GLESFuncs.glMemoryBarrier(barriers);
|
||||||
if (g_GLESCapabilities.GLESRendererString.find("ANGLE") != String::npos) {
|
if (g_GLESCapabilities.IsAngleRenderer) {
|
||||||
g_GLESFuncs.glFlush();
|
g_GLESFuncs.glFlush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3334,8 +3336,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Bool PresentStatsEnabled() {
|
static Bool PresentStatsEnabled() {
|
||||||
const char* value = std::getenv("MOBILEGL_GLES_PRESENT_STATS");
|
// MOBILEGL_GLES_PRESENT_STATS, parsed once in MG_ConfigLoader::Init.
|
||||||
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
return MG_Config::Features.GlesPresentStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void DumpDefaultFramebufferStats() {
|
static void DumpDefaultFramebufferStats() {
|
||||||
@@ -3627,9 +3629,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// desynchronizing backend buffer state.
|
// desynchronizing backend buffer state.
|
||||||
std::atomic<std::thread::id> g_backendContextOwnerThread{};
|
std::atomic<std::thread::id> g_backendContextOwnerThread{};
|
||||||
|
|
||||||
// Bumped whenever the backend ES context is destroyed; fence handles
|
// Bumped whenever the backend ES context is destroyed; fence and
|
||||||
// created under an older generation belong to a dead context and must
|
// timer-query handles created under an older generation belong to a
|
||||||
// never be passed back to GL (mirrors BufferImpl's context tracking).
|
// dead context and must never be passed back to GL (mirrors
|
||||||
|
// BufferImpl's context tracking).
|
||||||
Uint g_syncContextGeneration = 1;
|
Uint g_syncContextGeneration = 1;
|
||||||
|
|
||||||
// Backend fence handle: a native ES sync plus the ES context
|
// Backend fence handle: a native ES sync plus the ES context
|
||||||
@@ -3638,6 +3641,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
GLsync esSync = nullptr;
|
GLsync esSync = nullptr;
|
||||||
Uint contextGeneration = 0;
|
Uint contextGeneration = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Backend timer-query handle: a native GL query object name plus the
|
||||||
|
// ES context generation it was created under. Stale-generation
|
||||||
|
// handles read as available with a zero result, and deleting them
|
||||||
|
// only frees the wrapper (the dead ES context already reclaimed the
|
||||||
|
// query object).
|
||||||
|
struct GLESQueryObject {
|
||||||
|
GLuint queryId = 0;
|
||||||
|
Uint contextGeneration = 0;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool MakeCurrent() {
|
Bool MakeCurrent() {
|
||||||
@@ -3771,6 +3784,156 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return status == GL_SIGNALED;
|
return status == GL_SIGNALED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL timer queries, backed by GL_EXT_disjoint_timer_query. The desktop
|
||||||
|
// tokens from glext.h are used throughout: GL_TIME_ELAPSED (0x88BF),
|
||||||
|
// GL_TIMESTAMP (0x8E28), GL_QUERY_RESULT (0x8866) and
|
||||||
|
// GL_QUERY_RESULT_AVAILABLE (0x8867) are numerically identical to their
|
||||||
|
// _EXT counterparts.
|
||||||
|
|
||||||
|
Bool AreTimerQueriesSupported() {
|
||||||
|
return g_GLESCapabilities.SupportsDisjointTimerQuery && g_GLESFuncs.glGenQueries &&
|
||||||
|
g_GLESFuncs.glDeleteQueries && g_GLESFuncs.glBeginQuery && g_GLESFuncs.glEndQuery &&
|
||||||
|
g_GLESFuncs.glGetQueryObjectuiv && g_GLESFuncs.glQueryCounterEXT &&
|
||||||
|
g_GLESFuncs.glGetQueryObjectui64vEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
BackendQueryHandle BeginTimeElapsedQuery() {
|
||||||
|
// Query objects can only be created on the thread that owns the ES
|
||||||
|
// context (MC's F3 profiler queries on the render thread, which
|
||||||
|
// does). Returning null makes the frontend fall back to an
|
||||||
|
// immediately available zero result.
|
||||||
|
if (!IsBackendContextCurrentOnThisThread() || !AreTimerQueriesSupported()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
GLuint queryId = 0;
|
||||||
|
g_GLESFuncs.glGenQueries(1, &queryId);
|
||||||
|
if (queryId == 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
g_GLESFuncs.glBeginQuery(GL_TIME_ELAPSED, queryId);
|
||||||
|
return new GLESQueryObject{queryId, g_syncContextGeneration};
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndTimeElapsedQuery(BackendQueryHandle handle) {
|
||||||
|
const auto* query = static_cast<GLESQueryObject*>(handle);
|
||||||
|
if (query == nullptr || query->contextGeneration != g_syncContextGeneration ||
|
||||||
|
!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glEndQuery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ES tracks the active query per target, not per object, so the
|
||||||
|
// handle only guards the degraded paths above.
|
||||||
|
g_GLESFuncs.glEndQuery(GL_TIME_ELAPSED);
|
||||||
|
}
|
||||||
|
|
||||||
|
BackendQueryHandle QueryCounterTimestamp() {
|
||||||
|
if (!IsBackendContextCurrentOnThisThread() || !AreTimerQueriesSupported()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
GLuint queryId = 0;
|
||||||
|
g_GLESFuncs.glGenQueries(1, &queryId);
|
||||||
|
if (queryId == 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
g_GLESFuncs.glQueryCounterEXT(queryId, GL_TIMESTAMP);
|
||||||
|
return new GLESQueryObject{queryId, g_syncContextGeneration};
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool IsQueryResultAvailable(BackendQueryHandle handle) {
|
||||||
|
const auto* query = static_cast<GLESQueryObject*>(handle);
|
||||||
|
// Null/stale handles report available so the frontend proceeds to
|
||||||
|
// GetQueryResult64, which finalizes them as zero. A thread that does
|
||||||
|
// not own the ES context also reports available: GetQueryResult64
|
||||||
|
// then returns false and the frontend keeps the handle for a later
|
||||||
|
// read from the owning thread.
|
||||||
|
if (query == nullptr || query->contextGeneration != g_syncContextGeneration ||
|
||||||
|
!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glGetQueryObjectuiv) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
GLuint available = GL_FALSE;
|
||||||
|
g_GLESFuncs.glGetQueryObjectuiv(query->queryId, GL_QUERY_RESULT_AVAILABLE, &available);
|
||||||
|
return available != GL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool GetQueryResult64(BackendQueryHandle handle, Bool wait, Uint64* outNanoseconds) {
|
||||||
|
*outNanoseconds = 0;
|
||||||
|
const auto* query = static_cast<GLESQueryObject*>(handle);
|
||||||
|
// Null handles never had a GL query object, handles from a
|
||||||
|
// since-destroyed ES context lost theirs, and missing entry points
|
||||||
|
// can never produce a reading (belt and braces: the creators already
|
||||||
|
// require them): zero is the FINAL result in all three cases, so
|
||||||
|
// report it as produced and let the frontend cache it and release
|
||||||
|
// the handle.
|
||||||
|
if (query == nullptr || query->contextGeneration != g_syncContextGeneration ||
|
||||||
|
!g_GLESFuncs.glGetQueryObjectuiv || !g_GLESFuncs.glGetQueryObjectui64vEXT) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// A thread that does not own the ES context cannot issue GL calls,
|
||||||
|
// but the result still lands on the owning context eventually: report
|
||||||
|
// "not obtainable yet" so the frontend keeps the handle and a later
|
||||||
|
// availability poll / result read from the owning thread can still
|
||||||
|
// produce the real value.
|
||||||
|
if (!IsBackendContextCurrentOnThisThread()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (wait) {
|
||||||
|
// Reading GL_QUERY_RESULT blocks in the driver until the result
|
||||||
|
// lands, but only after the commands were flushed; flush once,
|
||||||
|
// then poll availability for a bounded ~100ms before dropping to
|
||||||
|
// a glFinish as the last resort (ClientWaitSync has no polling
|
||||||
|
// loop to mirror - it delegates its timeout to the driver, which
|
||||||
|
// a query-object read cannot do).
|
||||||
|
if (g_GLESFuncs.glFlush) {
|
||||||
|
g_GLESFuncs.glFlush();
|
||||||
|
}
|
||||||
|
constexpr Int kMaxAvailabilityPolls = 1000; // ~100ms at 100us per poll
|
||||||
|
GLuint available = GL_FALSE;
|
||||||
|
for (Int i = 0; i < kMaxAvailabilityPolls && available == GL_FALSE; ++i) {
|
||||||
|
g_GLESFuncs.glGetQueryObjectuiv(query->queryId, GL_QUERY_RESULT_AVAILABLE, &available);
|
||||||
|
if (available == GL_FALSE) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::microseconds(100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (available == GL_FALSE && g_GLESFuncs.glFinish) {
|
||||||
|
g_GLESFuncs.glFinish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// GL_EXT_disjoint_timer_query's GPU_DISJOINT_EXT signal is
|
||||||
|
// deliberately ignored: after a disjoint event (power state change,
|
||||||
|
// context switch) the result may be garbage, which is tolerable for
|
||||||
|
// an F3 GPU% readout, and consuming the latched flag here could hide
|
||||||
|
// the event from another observer.
|
||||||
|
GLuint64 result = 0;
|
||||||
|
g_GLESFuncs.glGetQueryObjectui64vEXT(query->queryId, GL_QUERY_RESULT, &result);
|
||||||
|
*outNanoseconds = static_cast<Uint64>(result);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeleteBackendQuery(BackendQueryHandle handle) {
|
||||||
|
auto* query = static_cast<GLESQueryObject*>(handle);
|
||||||
|
if (query == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (query->contextGeneration == g_syncContextGeneration && IsBackendContextCurrentOnThisThread() &&
|
||||||
|
g_GLESFuncs.glDeleteQueries) {
|
||||||
|
g_GLESFuncs.glDeleteQueries(1, &query->queryId);
|
||||||
|
}
|
||||||
|
// Otherwise the GL query object is abandoned; the ES context reclaims
|
||||||
|
// all of its query objects when it is destroyed.
|
||||||
|
delete query;
|
||||||
|
}
|
||||||
|
|
||||||
|
Int64 GetGpuTimestampNs() {
|
||||||
|
// Synchronous GPU clock sample; 0 tells the frontend GL_TIMESTAMP
|
||||||
|
// getter to fall back.
|
||||||
|
if (!IsBackendContextCurrentOnThisThread() || !AreTimerQueriesSupported() ||
|
||||||
|
!g_GLESFuncs.glGetInteger64v) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
GLint64 timestamp = 0;
|
||||||
|
g_GLESFuncs.glGetInteger64v(GL_TIMESTAMP, ×tamp);
|
||||||
|
return static_cast<Int64>(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
void Present() {
|
void Present() {
|
||||||
g_EGLFuncs.eglSwapBuffers(g_Display, g_Surface);
|
g_EGLFuncs.eglSwapBuffers(g_Display, g_Surface);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void WaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
|
void WaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
|
||||||
void DeleteSync(BackendSyncHandle sync);
|
void DeleteSync(BackendSyncHandle sync);
|
||||||
Bool GetSyncStatus(BackendSyncHandle sync);
|
Bool GetSyncStatus(BackendSyncHandle sync);
|
||||||
|
// True when GL_EXT_disjoint_timer_query and every entry point the timer
|
||||||
|
// hooks below need are present. Also gates the E_GL_ARB_timer_query
|
||||||
|
// advertisement in BackendObject_DirectGLES::InitCapabilities, and is
|
||||||
|
// registered as the GLFunctionsTable::IsTimerQuerySupported hook: a pure
|
||||||
|
// capability read needs no current ES context, and it stays false until
|
||||||
|
// the ES capabilities have been filled in.
|
||||||
|
Bool AreTimerQueriesSupported();
|
||||||
|
// GL timer-query objects, backed by GL_EXT_disjoint_timer_query. The
|
||||||
|
// creators return null (the frontend then falls back to an immediately
|
||||||
|
// available zero result) when the calling thread does not own the ES
|
||||||
|
// context or the extension/entry points are missing, and handles created
|
||||||
|
// under a since-destroyed ES context are always treated as complete with
|
||||||
|
// a zero result (mirrors the fence-sync handles above).
|
||||||
|
BackendQueryHandle BeginTimeElapsedQuery();
|
||||||
|
void EndTimeElapsedQuery(BackendQueryHandle query);
|
||||||
|
BackendQueryHandle QueryCounterTimestamp();
|
||||||
|
Bool IsQueryResultAvailable(BackendQueryHandle query);
|
||||||
|
// Returns true when a final value landed in *outNanoseconds (a zero for
|
||||||
|
// null or stale-generation handles IS final: the frontend may cache it
|
||||||
|
// and release the handle). Returns false only when the calling thread
|
||||||
|
// does not own the ES context, so the value is genuinely unobtainable
|
||||||
|
// right now; the handle stays alive and readable later.
|
||||||
|
Bool GetQueryResult64(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
|
||||||
|
void DeleteBackendQuery(BackendQueryHandle query);
|
||||||
|
Int64 GetGpuTimestampNs();
|
||||||
void Present();
|
void Present();
|
||||||
// Applies (or defers until the window surface exists) the app-requested
|
// Applies (or defers until the window surface exists) the app-requested
|
||||||
// eglSwapInterval on the native EGL surface.
|
// eglSwapInterval on the native EGL surface.
|
||||||
|
|||||||
@@ -17,17 +17,14 @@
|
|||||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||||
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
||||||
|
|
||||||
|
#include <Config.h>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
namespace {
|
namespace {
|
||||||
Bool IsR11G11B10FFallbackEnabled() {
|
Bool IsR11G11B10FFallbackEnabled() {
|
||||||
static const Bool enabled = [] {
|
return MG_Config::Features.VulkanR11G11B10FFallback;
|
||||||
const char* value = std::getenv("MOBILEGL_VULKAN_R11G11B10F_FALLBACK");
|
|
||||||
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
|
||||||
}();
|
|
||||||
return enabled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool IsReleaseCurrentRequest(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
Bool IsReleaseCurrentRequest(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
||||||
@@ -374,6 +371,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
auto nativeWindow = reinterpret_cast<NativeWindowType>(m_windowHandle.Handle);
|
auto nativeWindow = reinterpret_cast<NativeWindowType>(m_windowHandle.Handle);
|
||||||
|
|
||||||
|
// Any renderer instance this assignment replaces is destroyed here;
|
||||||
|
// fence/timer-query handles stamped with the old generation go stale.
|
||||||
|
BumpRendererGeneration();
|
||||||
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow);
|
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow);
|
||||||
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitWindowSurface: VulkanRenderer creation failed");
|
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitWindowSurface: VulkanRenderer creation failed");
|
||||||
pVulkanRenderer->Initialize();
|
pVulkanRenderer->Initialize();
|
||||||
@@ -384,6 +384,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VulkanRendererConfig config;
|
VulkanRendererConfig config;
|
||||||
config.SurfaceWidth = static_cast<Uint32>(std::max<EGLint>(width, 1));
|
config.SurfaceWidth = static_cast<Uint32>(std::max<EGLint>(width, 1));
|
||||||
config.SurfaceHeight = static_cast<Uint32>(std::max<EGLint>(height, 1));
|
config.SurfaceHeight = static_cast<Uint32>(std::max<EGLint>(height, 1));
|
||||||
|
// Any renderer instance this assignment replaces is destroyed here;
|
||||||
|
// fence/timer-query handles stamped with the old generation go stale.
|
||||||
|
BumpRendererGeneration();
|
||||||
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(NativeWindowType{}, config);
|
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(NativeWindowType{}, config);
|
||||||
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitPbufferSurface: VulkanRenderer creation failed");
|
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitPbufferSurface: VulkanRenderer creation failed");
|
||||||
pVulkanRenderer->Initialize();
|
pVulkanRenderer->Initialize();
|
||||||
@@ -486,12 +489,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
void BackendObject_DirectVulkan::ReleaseEGLResources() {
|
void BackendObject_DirectVulkan::ReleaseEGLResources() {
|
||||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||||
|
// Outstanding fence/timer-query handles now refer to a dead renderer;
|
||||||
|
// treat them as signaled/available with zero results from here on.
|
||||||
|
BumpRendererGeneration();
|
||||||
pVulkanRenderer.reset();
|
pVulkanRenderer.reset();
|
||||||
BackendObject::ReleaseEGLResources();
|
BackendObject::ReleaseEGLResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
void BackendObject_DirectVulkan::OnEGLSurfaceReleased(EGLSurface surface) {
|
void BackendObject_DirectVulkan::OnEGLSurfaceReleased(EGLSurface surface) {
|
||||||
(void)surface;
|
(void)surface;
|
||||||
|
// Outstanding fence/timer-query handles now refer to a dead renderer;
|
||||||
|
// treat them as signaled/available with zero results from here on.
|
||||||
|
BumpRendererGeneration();
|
||||||
pVulkanRenderer.reset();
|
pVulkanRenderer.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,6 +583,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
funcsTable.GL.WaitSync = WaitSync;
|
funcsTable.GL.WaitSync = WaitSync;
|
||||||
funcsTable.GL.DeleteSync = DeleteSync;
|
funcsTable.GL.DeleteSync = DeleteSync;
|
||||||
funcsTable.GL.GetSyncStatus = GetSyncStatus;
|
funcsTable.GL.GetSyncStatus = GetSyncStatus;
|
||||||
|
// Optional timer-query group: left null (the frontend then falls
|
||||||
|
// back) when disabled via MOBILEGL_DISABLE_TIMERQUERY. The hooks
|
||||||
|
// themselves additionally degrade to null handles when the device
|
||||||
|
// lacks timestamp support.
|
||||||
|
if (!MG_Config::Features.DisableTimerQuery) {
|
||||||
|
funcsTable.GL.IsTimerQuerySupported = IsTimerQuerySupported;
|
||||||
|
funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
|
||||||
|
funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
|
||||||
|
funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp;
|
||||||
|
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
||||||
|
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
||||||
|
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||||
|
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
|
||||||
|
}
|
||||||
funcsTableInitialized = true;
|
funcsTableInitialized = true;
|
||||||
}
|
}
|
||||||
return funcsTable;
|
return funcsTable;
|
||||||
@@ -599,6 +622,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (m_vulkanCaps.SupportsShaderSubgroup) {
|
if (m_vulkanCaps.SupportsShaderSubgroup) {
|
||||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension
|
||||||
|
// string). InitCapabilities runs after InitWindowSurface has created
|
||||||
|
// and initialized the renderer, so the advertisement can be gated on
|
||||||
|
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may
|
||||||
|
// run without a renderer; nothing is advertised then.
|
||||||
|
extensions.erase(std::remove(extensions.begin(), extensions.end(), E_GL_ARB_timer_query),
|
||||||
|
extensions.end());
|
||||||
|
if (pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported() &&
|
||||||
|
!MG_Config::Features.DisableTimerQuery) {
|
||||||
|
extensions.push_back(E_GL_ARB_timer_query);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
||||||
|
|||||||
@@ -15,12 +15,35 @@
|
|||||||
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
||||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||||
#include "MG_Util/Miscellany/IndexGenerator.h"
|
#include "MG_Util/Miscellany/IndexGenerator.h"
|
||||||
|
#include <atomic>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <spirv_reflect.h>
|
#include <spirv_reflect.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
UniquePtr<VulkanRenderer> pVulkanRenderer = nullptr;
|
UniquePtr<VulkanRenderer> pVulkanRenderer = nullptr;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Generation of the live VulkanRenderer instance, mirroring
|
||||||
|
// DirectGLES's g_syncContextGeneration. BackendObject_DirectVulkan
|
||||||
|
// bumps it (BumpRendererGeneration) wherever pVulkanRenderer is reset
|
||||||
|
// or recreated. Fence and timer-query handles are stamped with the
|
||||||
|
// generation they were created under: a stale stamp means the frame
|
||||||
|
// serials and query-pool slots the handle refers to belong to a
|
||||||
|
// destroyed renderer and must never be dereferenced against the
|
||||||
|
// current one (a new renderer restarts its frame-serial counter and
|
||||||
|
// reuses pool indices). Atomic because handles may be polled from a
|
||||||
|
// thread other than the EGL thread that recreates the renderer.
|
||||||
|
std::atomic<Uint64> g_rendererGeneration{1};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Uint64 GetRendererGeneration() {
|
||||||
|
return g_rendererGeneration.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BumpRendererGeneration() {
|
||||||
|
g_rendererGeneration.fetch_add(1, std::memory_order_acq_rel);
|
||||||
|
}
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
struct BufferVariableResource {
|
struct BufferVariableResource {
|
||||||
String name;
|
String name;
|
||||||
@@ -1349,6 +1372,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// horizon used to recycle buffer resources).
|
// horizon used to recycle buffer resources).
|
||||||
struct VulkanSyncObject {
|
struct VulkanSyncObject {
|
||||||
Uint64 frameSerial = 0;
|
Uint64 frameSerial = 0;
|
||||||
|
// Renderer generation the serial was issued under (see
|
||||||
|
// g_rendererGeneration). A stale generation reports the fence
|
||||||
|
// signaled: renderer destruction waits for device idle, so the
|
||||||
|
// old renderer's GPU work is long complete, and the serial must
|
||||||
|
// not be compared against the new renderer's restarted counter.
|
||||||
|
Uint64 rendererGeneration = 0;
|
||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -1356,7 +1385,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (!pVulkanRenderer) {
|
if (!pVulkanRenderer) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return new VulkanSyncObject{pVulkanRenderer->GetCurrentFrameSerial()};
|
return new VulkanSyncObject{pVulkanRenderer->GetCurrentFrameSerial(), GetRendererGeneration()};
|
||||||
}
|
}
|
||||||
|
|
||||||
GLenum ClientWaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) {
|
GLenum ClientWaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) {
|
||||||
@@ -1365,7 +1394,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// waiting can succeed at all.
|
// waiting can succeed at all.
|
||||||
(void)flags;
|
(void)flags;
|
||||||
const auto* sync = static_cast<VulkanSyncObject*>(handle);
|
const auto* sync = static_cast<VulkanSyncObject*>(handle);
|
||||||
if (sync == nullptr || !pVulkanRenderer) {
|
if (sync == nullptr || !pVulkanRenderer || sync->rendererGeneration != GetRendererGeneration()) {
|
||||||
return GL_ALREADY_SIGNALED;
|
return GL_ALREADY_SIGNALED;
|
||||||
}
|
}
|
||||||
if (pVulkanRenderer->IsFrameSerialComplete(sync->frameSerial)) {
|
if (pVulkanRenderer->IsFrameSerialComplete(sync->frameSerial)) {
|
||||||
@@ -1393,12 +1422,143 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
Bool GetSyncStatus(BackendSyncHandle handle) {
|
Bool GetSyncStatus(BackendSyncHandle handle) {
|
||||||
const auto* sync = static_cast<VulkanSyncObject*>(handle);
|
const auto* sync = static_cast<VulkanSyncObject*>(handle);
|
||||||
if (sync == nullptr || !pVulkanRenderer) {
|
if (sync == nullptr || !pVulkanRenderer || sync->rendererGeneration != GetRendererGeneration()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return pVulkanRenderer->IsFrameSerialComplete(sync->frameSerial);
|
return pVulkanRenderer->IsFrameSerialComplete(sync->frameSerial);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Backend timer-query handle: a TIME_ELAPSED span holds a begin and an
|
||||||
|
// end timestamp record; a GL_TIMESTAMP one-shot holds only `end`. The
|
||||||
|
// records are shared (SharedPtr) with the owning pool's pending list,
|
||||||
|
// so deleting the query while results are still in flight is safe.
|
||||||
|
struct VulkanTimerQuery {
|
||||||
|
SharedPtr<VkTimerQueryManager::TimestampRecord> begin;
|
||||||
|
SharedPtr<VkTimerQueryManager::TimestampRecord> end;
|
||||||
|
// Renderer generation the records were written under (see
|
||||||
|
// g_rendererGeneration). A stale generation resolves as available
|
||||||
|
// with a final zero result: the records' pool indices and frame
|
||||||
|
// serials refer to a destroyed renderer and must never be handed
|
||||||
|
// to the current one. DeleteBackendQuery only frees the wrapper
|
||||||
|
// (and, via the SharedPtrs, the records), never pool slots, so
|
||||||
|
// stale queries are always safe to delete.
|
||||||
|
Uint64 rendererGeneration = 0;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Bool IsTimerQuerySupported() {
|
||||||
|
return pVulkanRenderer != nullptr && pVulkanRenderer->IsTimerQuerySupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
BackendQueryHandle BeginTimeElapsedQuery() {
|
||||||
|
if (!pVulkanRenderer || !pVulkanRenderer->IsTimerQuerySupported()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto begin = pVulkanRenderer->WriteTimerQueryTimestamp();
|
||||||
|
if (!begin) {
|
||||||
|
// Pool exhausted this frame; the frontend falls back on a null handle.
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto* query = new VulkanTimerQuery{};
|
||||||
|
query->begin = std::move(begin);
|
||||||
|
query->rendererGeneration = GetRendererGeneration();
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndTimeElapsedQuery(BackendQueryHandle handle) {
|
||||||
|
auto* query = static_cast<VulkanTimerQuery*>(handle);
|
||||||
|
if (query == nullptr || !pVulkanRenderer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (query->rendererGeneration != GetRendererGeneration()) {
|
||||||
|
// The span began under a renderer that has since been destroyed;
|
||||||
|
// never write into the new renderer's pools on its behalf. The
|
||||||
|
// query resolves as available with a zero result.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// May be null on pool exhaustion; the query then reads back as 0.
|
||||||
|
query->end = pVulkanRenderer->WriteTimerQueryTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
BackendQueryHandle QueryCounterTimestamp() {
|
||||||
|
if (!pVulkanRenderer || !pVulkanRenderer->IsTimerQuerySupported()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto record = pVulkanRenderer->WriteTimerQueryTimestamp();
|
||||||
|
if (!record) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto* query = new VulkanTimerQuery{};
|
||||||
|
query->end = std::move(record);
|
||||||
|
query->rendererGeneration = GetRendererGeneration();
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool IsQueryResultAvailable(BackendQueryHandle handle) {
|
||||||
|
auto* query = static_cast<VulkanTimerQuery*>(handle);
|
||||||
|
// Degraded/stale handles report available; GetQueryResult64 then
|
||||||
|
// resolves them with a final zero result.
|
||||||
|
if (query == nullptr || !pVulkanRenderer || query->rendererGeneration != GetRendererGeneration()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (query->begin && !pVulkanRenderer->IsTimerQueryResultReady(*query->begin)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (query->end && !pVulkanRenderer->IsTimerQueryResultReady(*query->end)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool GetQueryResult64(BackendQueryHandle handle, Bool wait, Uint64* outNanoseconds) {
|
||||||
|
*outNanoseconds = 0;
|
||||||
|
auto* query = static_cast<VulkanTimerQuery*>(handle);
|
||||||
|
if (query == nullptr || !pVulkanRenderer || query->rendererGeneration != GetRendererGeneration()) {
|
||||||
|
// No renderer, or the records belong to a destroyed renderer: no
|
||||||
|
// real value can ever be produced, so resolve with a final 0.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// With wait, mirrors ClientWaitSync: a query ended this frame cannot
|
||||||
|
// complete until Present submits the commands, so the wait refuses to
|
||||||
|
// block on the current unsubmitted serial. Returning false keeps the
|
||||||
|
// handle alive in the frontend; the query stays readable once a later
|
||||||
|
// Present submits the frame.
|
||||||
|
const auto ensureReady = [&](VkTimerQueryManager::TimestampRecord& record) {
|
||||||
|
return wait ? pVulkanRenderer->WaitForTimerQueryResult(record)
|
||||||
|
: pVulkanRenderer->IsTimerQueryResultReady(record);
|
||||||
|
};
|
||||||
|
if (query->begin && query->end) {
|
||||||
|
if (!ensureReady(*query->begin) || !ensureReady(*query->end)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*outNanoseconds = pVulkanRenderer->GetTimerQueryElapsedNs(*query->begin, *query->end);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (query->end) {
|
||||||
|
if (!ensureReady(*query->end)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*outNanoseconds = pVulkanRenderer->GetTimerQueryTimestampNs(*query->end);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// TIME_ELAPSED span that never got its end timestamp (pool
|
||||||
|
// exhaustion): nothing further can arrive, resolve with a final 0.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeleteBackendQuery(BackendQueryHandle handle) {
|
||||||
|
delete static_cast<VulkanTimerQuery*>(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
Int64 GetGpuTimestampNs() {
|
||||||
|
// Vulkan cannot synchronously sample the GPU clock: timestamps only
|
||||||
|
// exist as vkCmdWriteTimestamp results read back later, and
|
||||||
|
// VK_EXT_calibrated_timestamps is not wired up. Returning 0 tells the
|
||||||
|
// frontend GL_TIMESTAMP getter to fall back.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
void Present() {
|
void Present() {
|
||||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Present called with null VulkanRenderer");
|
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Present called with null VulkanRenderer");
|
||||||
pVulkanRenderer->Present();
|
pVulkanRenderer->Present();
|
||||||
|
|||||||
@@ -14,6 +14,15 @@
|
|||||||
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
|
||||||
|
// g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever
|
||||||
|
// pVulkanRenderer is reset or recreated; fence and timer-query handles
|
||||||
|
// stamped with an older generation are stale and resolve as signaled /
|
||||||
|
// available with zero results instead of dereferencing the destroyed
|
||||||
|
// renderer's frame serials and query-pool slots.
|
||||||
|
Uint64 GetRendererGeneration();
|
||||||
|
void BumpRendererGeneration();
|
||||||
|
|
||||||
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);
|
||||||
@@ -99,5 +108,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void WaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
|
void WaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout);
|
||||||
void DeleteSync(BackendSyncHandle sync);
|
void DeleteSync(BackendSyncHandle sync);
|
||||||
Bool GetSyncStatus(BackendSyncHandle sync);
|
Bool GetSyncStatus(BackendSyncHandle sync);
|
||||||
|
// GPU timer queries (GL_TIME_ELAPSED spans and GL_TIMESTAMP one-shots),
|
||||||
|
// backed by per-frame VkQueryPool timestamp slots. All hooks degrade
|
||||||
|
// gracefully: null handles when the renderer is absent, the device lacks
|
||||||
|
// timestamp support, or the frame's pool is exhausted.
|
||||||
|
// Dynamic support check (GLFunctionsTable::IsTimerQuerySupported): true
|
||||||
|
// only while a live renderer exists whose device can actually time.
|
||||||
|
Bool IsTimerQuerySupported();
|
||||||
|
BackendQueryHandle BeginTimeElapsedQuery();
|
||||||
|
void EndTimeElapsedQuery(BackendQueryHandle query);
|
||||||
|
BackendQueryHandle QueryCounterTimestamp();
|
||||||
|
Bool IsQueryResultAvailable(BackendQueryHandle query);
|
||||||
|
// Returns true when a final value was produced (outNanoseconds set; the
|
||||||
|
// frontend may cache it and release the handle), false when the result
|
||||||
|
// cannot be obtained yet (e.g. a wait refused because the records' frame
|
||||||
|
// serial is the current unsubmitted frame) - the handle then stays
|
||||||
|
// readable later.
|
||||||
|
Bool GetQueryResult64(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
|
||||||
|
void DeleteBackendQuery(BackendQueryHandle query);
|
||||||
|
// Always 0: Vulkan cannot synchronously sample the GPU clock (timestamps
|
||||||
|
// only exist as vkCmdWriteTimestamp results); the frontend falls back.
|
||||||
|
Int64 GetGpuTimestampNs();
|
||||||
void Present();
|
void Present();
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VK_VERIFY(vkBeginCommandBuffer(frame.commandBuffer, &beginInfo), "BeginCommandRecording, vkBeginCommandBuffer");
|
VK_VERIFY(vkBeginCommandBuffer(frame.commandBuffer, &beginInfo), "BeginCommandRecording, vkBeginCommandBuffer");
|
||||||
|
|
||||||
frame.isCommandRecording = true;
|
frame.isCommandRecording = true;
|
||||||
|
if (m_recordingObserver != nullptr) {
|
||||||
|
m_recordingObserver->OnFrameCommandRecordingBegan(frame.commandBuffer);
|
||||||
|
}
|
||||||
return frame.commandBuffer;
|
return frame.commandBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,6 +233,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return static_cast<Uint32>(m_frames.size());
|
return static_cast<Uint32>(m_frames.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FrameContext::SetRecordingObserver(IRecordingObserver* observer) {
|
||||||
|
m_recordingObserver = observer;
|
||||||
|
}
|
||||||
|
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,17 @@
|
|||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
class FrameContext {
|
class FrameContext {
|
||||||
public:
|
public:
|
||||||
|
// Notified immediately after a frame command buffer begins recording
|
||||||
|
// (before any render pass has been begun); every BeginCommandRecording
|
||||||
|
// caller funnels through this single seam. Implemented by the renderer
|
||||||
|
// to prepare per-frame timer-query pools (vkCmdResetQueryPool must be
|
||||||
|
// recorded outside a render pass).
|
||||||
|
class IRecordingObserver {
|
||||||
|
public:
|
||||||
|
virtual ~IRecordingObserver() = default;
|
||||||
|
virtual void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
struct SubmitInfoPacket {
|
struct SubmitInfoPacket {
|
||||||
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||||
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
|
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
|
||||||
@@ -61,6 +72,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 GetCurrentFrameIndex() const;
|
Uint32 GetCurrentFrameIndex() const;
|
||||||
Uint32 GetFrameCount() const;
|
Uint32 GetFrameCount() const;
|
||||||
|
|
||||||
|
// Observer may be null (no notifications). Not owned.
|
||||||
|
void SetRecordingObserver(IRecordingObserver* observer);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void AssertValidFrameIndex(Uint32 frameIndex) const;
|
void AssertValidFrameIndex(Uint32 frameIndex) const;
|
||||||
void AssertValidSwapchainImageIndex(Uint32 imageIndex) const;
|
void AssertValidSwapchainImageIndex(Uint32 imageIndex) const;
|
||||||
@@ -73,5 +87,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<FrameData> m_frames;
|
Vector<FrameData> m_frames;
|
||||||
Vector<VkSemaphore> m_swapchainImageRenderFinishedSemaphores;
|
Vector<VkSemaphore> m_swapchainImageRenderFinishedSemaphores;
|
||||||
Uint32 currentFrameIndex = 0;
|
Uint32 currentFrameIndex = 0;
|
||||||
|
IRecordingObserver* m_recordingObserver = nullptr;
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#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 "MG_Util/Metrics/TextureMetrics.h"
|
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||||
|
#include <Config.h>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -78,11 +79,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Bool ShouldDumpDescriptorStats() {
|
static Bool ShouldDumpDescriptorStats() {
|
||||||
static const Bool enabled = [] {
|
return MG_Config::Features.DescriptorStats;
|
||||||
const char* value = std::getenv("MOBILEGL_DESCRIPTOR_STATS");
|
|
||||||
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
|
||||||
}();
|
|
||||||
return enabled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool UniformManager::Initialize(VkDevice device, VkBufferManager* bufferManager,
|
Bool UniformManager::Initialize(VkDevice device, VkBufferManager* bufferManager,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||||
|
|
||||||
|
#include <Config.h>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -52,11 +53,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static Bool IsR11G11B10FFallbackEnabled() {
|
static Bool IsR11G11B10FFallbackEnabled() {
|
||||||
static const Bool enabled = [] {
|
return MG_Config::Features.VulkanR11G11B10FFallback;
|
||||||
const char* value = std::getenv("MOBILEGL_VULKAN_R11G11B10F_FALLBACK");
|
|
||||||
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
|
||||||
}();
|
|
||||||
return enabled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Bool IsMultisampleTextureUploadTarget(TextureUploadTarget target) {
|
static Bool IsMultisampleTextureUploadTarget(TextureUploadTarget target) {
|
||||||
@@ -387,11 +384,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Bool ShouldDumpTextureUploadStats() {
|
static Bool ShouldDumpTextureUploadStats() {
|
||||||
static const Bool enabled = [] {
|
return MG_Config::Features.TextureUploadStats;
|
||||||
const char* value = std::getenv("MOBILEGL_TEXTURE_UPLOAD_STATS");
|
|
||||||
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
|
||||||
}();
|
|
||||||
return enabled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void DumpTextureSyncStats(Int textureId, TextureInternalFormat format, TextureUploadTarget uploadTarget,
|
static void DumpTextureSyncStats(Int textureId, TextureInternalFormat format, TextureUploadTarget uploadTarget,
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkTimerQueryManager.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 "VkTimerQueryManager.h"
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
|
Bool VkTimerQueryManager::Initialize(const InitInfo& initInfo) {
|
||||||
|
Shutdown();
|
||||||
|
|
||||||
|
MOBILEGL_ASSERT(initInfo.device != VK_NULL_HANDLE, "VkTimerQueryManager::Initialize requires valid VkDevice");
|
||||||
|
MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkTimerQueryManager::Initialize requires non-zero frame count");
|
||||||
|
if (initInfo.timestampValidBits == 0 || initInfo.timestampPeriodNs <= 0.0f || initInfo.slotsPerPool == 0) {
|
||||||
|
MGLOG_W("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
|
||||||
|
initInfo.timestampValidBits, initInfo.timestampPeriodNs, initInfo.slotsPerPool);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_device = initInfo.device;
|
||||||
|
m_timestampPeriodNs = initInfo.timestampPeriodNs;
|
||||||
|
m_validBitsMask = initInfo.timestampValidBits >= 64
|
||||||
|
? ~0ull
|
||||||
|
: ((1ull << initInfo.timestampValidBits) - 1ull);
|
||||||
|
m_slotsPerPool = initInfo.slotsPerPool;
|
||||||
|
m_pools.resize(initInfo.frameCount);
|
||||||
|
|
||||||
|
VkQueryPoolCreateInfo poolInfo{};
|
||||||
|
poolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
|
||||||
|
poolInfo.queryType = VK_QUERY_TYPE_TIMESTAMP;
|
||||||
|
poolInfo.queryCount = m_slotsPerPool;
|
||||||
|
for (auto& poolState : m_pools) {
|
||||||
|
const VkResult result = vkCreateQueryPool(m_device, &poolInfo, nullptr, &poolState.pool);
|
||||||
|
if (result != VK_SUCCESS) {
|
||||||
|
MGLOG_E("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
|
||||||
|
Shutdown();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VkTimerQueryManager::Shutdown() {
|
||||||
|
if (m_device != VK_NULL_HANDLE) {
|
||||||
|
for (auto& poolState : m_pools) {
|
||||||
|
if (poolState.pool != VK_NULL_HANDLE) {
|
||||||
|
vkDestroyQueryPool(m_device, poolState.pool, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Records the frontend still holds simply stay unharvested; their
|
||||||
|
// results read back as 0.
|
||||||
|
m_pools.clear();
|
||||||
|
m_device = VK_NULL_HANDLE;
|
||||||
|
m_timestampPeriodNs = 0.0f;
|
||||||
|
m_validBitsMask = 0;
|
||||||
|
m_slotsPerPool = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VkTimerQueryManager::OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer, Uint32 frameIndex,
|
||||||
|
Uint64 frameSerial) {
|
||||||
|
MOBILEGL_ASSERT(frameIndex < m_pools.size(), "VkTimerQueryManager frame index out of range");
|
||||||
|
auto& poolState = m_pools[frameIndex];
|
||||||
|
if (poolState.preparedFrameSerial == frameSerial) {
|
||||||
|
// Recording re-began within the same frame (mid-frame readback
|
||||||
|
// submit or the Present layout transition); the pool was already
|
||||||
|
// harvested and reset for this cycle, and resetting again would
|
||||||
|
// clobber timestamps written earlier in the frame.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Harvest what the pool's previous cycle left behind. The frame slot's
|
||||||
|
// fence was waited before re-recording, so every executed query is
|
||||||
|
// already available and the reads return immediately.
|
||||||
|
DrainPoolPending(poolState);
|
||||||
|
|
||||||
|
vkCmdResetQueryPool(commandBuffer, poolState.pool, 0, m_slotsPerPool);
|
||||||
|
poolState.cursor = 0;
|
||||||
|
poolState.exhaustionWarned = false;
|
||||||
|
poolState.preparedFrameSerial = frameSerial;
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedPtr<VkTimerQueryManager::TimestampRecord> VkTimerQueryManager::WriteTimestamp(VkCommandBuffer commandBuffer,
|
||||||
|
Uint32 frameIndex,
|
||||||
|
Uint64 frameSerial) {
|
||||||
|
MOBILEGL_ASSERT(frameIndex < m_pools.size(), "VkTimerQueryManager frame index out of range");
|
||||||
|
auto& poolState = m_pools[frameIndex];
|
||||||
|
if (poolState.cursor >= m_slotsPerPool) {
|
||||||
|
if (!poolState.exhaustionWarned) {
|
||||||
|
MGLOG_W("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
|
||||||
|
"this frame fall back to the frontend path",
|
||||||
|
frameIndex, m_slotsPerPool);
|
||||||
|
poolState.exhaustionWarned = true;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto record = MakeShared<TimestampRecord>();
|
||||||
|
record->poolIndex = frameIndex;
|
||||||
|
record->slot = poolState.cursor++;
|
||||||
|
record->frameSerial = frameSerial;
|
||||||
|
vkCmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, poolState.pool, record->slot);
|
||||||
|
poolState.pendingRecords.push_back(record);
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool VkTimerQueryManager::TryHarvest(TimestampRecord& record) {
|
||||||
|
if (record.harvested) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (m_device == VK_NULL_HANDLE || record.poolIndex >= m_pools.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 resultWithAvailability[2] = {0, 0};
|
||||||
|
const VkResult result = vkGetQueryPoolResults(
|
||||||
|
m_device, m_pools[record.poolIndex].pool, record.slot, 1, sizeof(resultWithAvailability),
|
||||||
|
resultWithAvailability, sizeof(Uint64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
|
||||||
|
if (result != VK_SUCCESS && result != VK_NOT_READY) {
|
||||||
|
MGLOG_E("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (resultWithAvailability[1] == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
record.rawTicks = resultWithAvailability[0];
|
||||||
|
record.harvested = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VkTimerQueryManager::InvalidatePendingRecords() {
|
||||||
|
for (auto& poolState : m_pools) {
|
||||||
|
DrainPoolPending(poolState);
|
||||||
|
// Force a harvest-free reset cycle the next time this pool's frame
|
||||||
|
// begins recording.
|
||||||
|
poolState.preparedFrameSerial = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VkTimerQueryManager::DrainPoolPending(PoolState& poolState) {
|
||||||
|
for (auto& record : poolState.pendingRecords) {
|
||||||
|
if (record->harvested) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!TryHarvest(*record)) {
|
||||||
|
// The commands carrying this timestamp never executed (they
|
||||||
|
// were dropped, e.g. by a swapchain recreation mid-frame).
|
||||||
|
// Mark the record resolved-as-invalid so waits on it cannot
|
||||||
|
// hang; its result reads back as 0.
|
||||||
|
record->harvested = true;
|
||||||
|
record->valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
poolState.pendingRecords.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 VkTimerQueryManager::MaskToValidBits(Uint64 ticks) const {
|
||||||
|
return ticks & m_validBitsMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 VkTimerQueryManager::ElapsedNs(const TimestampRecord& begin, const TimestampRecord& end) const {
|
||||||
|
if (!begin.valid || !end.valid) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const Uint64 deltaTicks = MaskToValidBits(end.rawTicks - begin.rawTicks);
|
||||||
|
return static_cast<Uint64>(static_cast<double>(deltaTicks) * static_cast<double>(m_timestampPeriodNs));
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 VkTimerQueryManager::TimestampNs(const TimestampRecord& record) const {
|
||||||
|
if (!record.valid) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return static_cast<Uint64>(static_cast<double>(MaskToValidBits(record.rawTicks)) *
|
||||||
|
static_cast<double>(m_timestampPeriodNs));
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkTimerQueryManager.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 "../VkIncludes.h"
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
|
// GPU timestamp storage backing the GL timer-query frontend (GL_TIME_ELAPSED
|
||||||
|
// spans and GL_TIMESTAMP one-shots): one VkQueryPool of timestamp slots per
|
||||||
|
// frame in flight.
|
||||||
|
//
|
||||||
|
// Per-frame lifecycle: right after a frame slot's command buffer begins
|
||||||
|
// recording (and before any render pass, since vkCmdResetQueryPool must be
|
||||||
|
// recorded outside one), OnFrameCommandRecordingBegan harvests every
|
||||||
|
// not-yet-read slot of the pool about to be reused (the slot's frame fence
|
||||||
|
// was waited before re-recording, so the results are already available),
|
||||||
|
// records a reset of the whole pool, and rewinds the allocation cursor.
|
||||||
|
class VkTimerQueryManager {
|
||||||
|
public:
|
||||||
|
// One vkCmdWriteTimestamp landing spot. Shared (via SharedPtr) between
|
||||||
|
// the frontend-held query object and the owning pool's pending list, so
|
||||||
|
// deleting a query while its result is still in flight never leaves the
|
||||||
|
// pool with a dangling record.
|
||||||
|
struct TimestampRecord {
|
||||||
|
Uint32 poolIndex = 0;
|
||||||
|
Uint32 slot = 0;
|
||||||
|
// VkBufferManager frame serial current when the timestamp was
|
||||||
|
// recorded; result availability is bounded by its completion.
|
||||||
|
Uint64 frameSerial = 0;
|
||||||
|
Bool harvested = false;
|
||||||
|
// Cleared when the recorded commands were dropped before they could
|
||||||
|
// execute (swapchain recreation abandons the in-progress command
|
||||||
|
// buffer); the result then reads back as 0.
|
||||||
|
Bool valid = true;
|
||||||
|
Uint64 rawTicks = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InitInfo {
|
||||||
|
VkDevice device = VK_NULL_HANDLE;
|
||||||
|
Uint32 frameCount = 0;
|
||||||
|
Uint32 timestampValidBits = 0;
|
||||||
|
Float timestampPeriodNs = 0.0f; // nanoseconds per timestamp tick
|
||||||
|
Uint32 slotsPerPool = 128;
|
||||||
|
};
|
||||||
|
|
||||||
|
Bool Initialize(const InitInfo& initInfo);
|
||||||
|
// The caller guarantees the device is idle (same contract as the other
|
||||||
|
// DirectVulkan managers' Shutdown paths).
|
||||||
|
void Shutdown();
|
||||||
|
|
||||||
|
// The per-frame hook described in the class comment. Re-begins within
|
||||||
|
// the same frame serial (mid-frame readback submits, the Present layout
|
||||||
|
// transition) are skipped so already-written slots survive.
|
||||||
|
void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer, Uint32 frameIndex, Uint64 frameSerial);
|
||||||
|
|
||||||
|
// Allocates a slot from the frame's pool and records a bottom-of-pipe
|
||||||
|
// vkCmdWriteTimestamp (valid both inside and outside a render pass).
|
||||||
|
// Returns null on pool exhaustion, with one warning per pool cycle; the
|
||||||
|
// frontend falls back gracefully on a null handle.
|
||||||
|
SharedPtr<TimestampRecord> WriteTimestamp(VkCommandBuffer commandBuffer, Uint32 frameIndex,
|
||||||
|
Uint64 frameSerial);
|
||||||
|
|
||||||
|
// Non-blocking single-slot read (WITH_AVAILABILITY, no WAIT). Returns
|
||||||
|
// true once the record holds its raw ticks. Callers gate this on the
|
||||||
|
// record's frame serial being complete.
|
||||||
|
Bool TryHarvest(TimestampRecord& record);
|
||||||
|
|
||||||
|
// Reads every pending result that is available (the caller guarantees
|
||||||
|
// the device is idle) and marks the rest invalid. Called when recorded
|
||||||
|
// but unsubmitted commands are dropped (swapchain recreation), which
|
||||||
|
// would otherwise leave slots that never become available. Each pool is
|
||||||
|
// reset lazily on its next OnFrameCommandRecordingBegan.
|
||||||
|
void InvalidatePendingRecords();
|
||||||
|
|
||||||
|
// end - begin using unsigned wrap arithmetic masked to the queue's
|
||||||
|
// timestampValidBits, converted to nanoseconds. 0 if either record was
|
||||||
|
// invalidated.
|
||||||
|
Uint64 ElapsedNs(const TimestampRecord& begin, const TimestampRecord& end) const;
|
||||||
|
// Raw GPU timestamp converted to nanoseconds. 0 if invalidated.
|
||||||
|
Uint64 TimestampNs(const TimestampRecord& record) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct PoolState {
|
||||||
|
VkQueryPool pool = VK_NULL_HANDLE;
|
||||||
|
Uint32 cursor = 0;
|
||||||
|
// Frame serial the pool was last harvested + reset for; guards
|
||||||
|
// against double resets when recording re-begins mid-frame.
|
||||||
|
Uint64 preparedFrameSerial = 0;
|
||||||
|
Bool exhaustionWarned = false;
|
||||||
|
Vector<SharedPtr<TimestampRecord>> pendingRecords;
|
||||||
|
};
|
||||||
|
|
||||||
|
Uint64 MaskToValidBits(Uint64 ticks) const;
|
||||||
|
// Harvest (or invalidate, when the result never became available)
|
||||||
|
// every pending record of a pool and clear its pending list.
|
||||||
|
void DrainPoolPending(PoolState& pool);
|
||||||
|
|
||||||
|
VkDevice m_device = VK_NULL_HANDLE;
|
||||||
|
Float m_timestampPeriodNs = 0.0f;
|
||||||
|
Uint64 m_validBitsMask = 0;
|
||||||
|
Uint32 m_slotsPerPool = 0;
|
||||||
|
Vector<PoolState> m_pools;
|
||||||
|
};
|
||||||
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
#include "MG_Util/Converters/MGToVk/RenderStateEnumConverter.h"
|
#include "MG_Util/Converters/MGToVk/RenderStateEnumConverter.h"
|
||||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||||
|
#include <Config.h>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -617,19 +618,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Bool ShouldDumpVertexInputStats() {
|
static Bool ShouldDumpVertexInputStats() {
|
||||||
static const Bool enabled = [] {
|
return MG_Config::Features.VertexInputStats;
|
||||||
const char* value = std::getenv("MOBILEGL_VERTEX_INPUT_STATS");
|
|
||||||
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
|
||||||
}();
|
|
||||||
return enabled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char* PresentDumpPath() {
|
static const char* PresentDumpPath() {
|
||||||
const char* value = std::getenv("MOBILEGL_PRESENT_DUMP_PATH");
|
const String& path = MG_Config::Features.PresentDumpPath;
|
||||||
return value != nullptr && value[0] != '\0' ? value : nullptr;
|
return path.empty() ? nullptr : path.c_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Bool PresentDumpMatchesTargetCall() {
|
static Bool PresentDumpMatchesTargetCall() {
|
||||||
|
// MOBILEGL_PRESENT_DUMP_CALL / MOBILEGL_PRESENT_CURRENT_CALL stay live
|
||||||
|
// getenv on purpose: the retrace harness mutates them at runtime via
|
||||||
|
// setenv to select which eglSwapBuffers call gets dumped, so they must
|
||||||
|
// not be snapshotted into MG_Config::Features at init time.
|
||||||
const char* target = std::getenv("MOBILEGL_PRESENT_DUMP_CALL");
|
const char* target = std::getenv("MOBILEGL_PRESENT_DUMP_CALL");
|
||||||
if (target == nullptr || target[0] == '\0') {
|
if (target == nullptr || target[0] == '\0') {
|
||||||
return true;
|
return true;
|
||||||
@@ -1573,8 +1574,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Bool PresentStatsEnabled() {
|
static Bool PresentStatsEnabled() {
|
||||||
const char* value = std::getenv("MOBILEGL_PRESENT_STATS");
|
return MG_Config::Features.PresentStats;
|
||||||
return value != nullptr && value[0] == '1' && value[1] == '\0';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Bool IsBgraVkFormat(VkFormat format) {
|
static Bool IsBgraVkFormat(VkFormat format) {
|
||||||
@@ -1894,6 +1894,19 @@ void main() {
|
|||||||
});
|
});
|
||||||
MOBILEGL_ASSERT(succeeded, "VkBufferManager initialization failed.");
|
MOBILEGL_ASSERT(succeeded, "VkBufferManager initialization failed.");
|
||||||
m_bufferManager.SetCopyCommandProvider(this);
|
m_bufferManager.SetCopyCommandProvider(this);
|
||||||
|
if (m_timerQuerySupported) {
|
||||||
|
m_timerQueryManager = MakeUnique<VkTimerQueryManager>();
|
||||||
|
if (m_timerQueryManager->Initialize({.device = m_device,
|
||||||
|
.frameCount = m_frameContext.GetFrameCount(),
|
||||||
|
.timestampValidBits = m_timestampValidBits,
|
||||||
|
.timestampPeriodNs = m_timestampPeriodNs})) {
|
||||||
|
m_frameContext.SetRecordingObserver(this);
|
||||||
|
} else {
|
||||||
|
MGLOG_W("VkTimerQueryManager initialization failed; timer queries disabled");
|
||||||
|
m_timerQueryManager.reset();
|
||||||
|
m_timerQuerySupported = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
m_textureManager = MakeUnique<VkTextureManager>();
|
m_textureManager = MakeUnique<VkTextureManager>();
|
||||||
MOBILEGL_ASSERT(m_textureManager != nullptr, "VkTextureManager creation failed.");
|
MOBILEGL_ASSERT(m_textureManager != nullptr, "VkTextureManager creation failed.");
|
||||||
succeeded = m_textureManager->Initialize(
|
succeeded = m_textureManager->Initialize(
|
||||||
@@ -1988,6 +2001,13 @@ void main() {
|
|||||||
m_vertexInputStateFactory.reset();
|
m_vertexInputStateFactory.reset();
|
||||||
m_bufferManager.Shutdown();
|
m_bufferManager.Shutdown();
|
||||||
|
|
||||||
|
// Device is idle (vkDeviceWaitIdle above); query pools can be destroyed.
|
||||||
|
m_frameContext.SetRecordingObserver(nullptr);
|
||||||
|
if (m_timerQueryManager) {
|
||||||
|
m_timerQueryManager->Shutdown();
|
||||||
|
m_timerQueryManager.reset();
|
||||||
|
}
|
||||||
|
|
||||||
if (m_device != VK_NULL_HANDLE) {
|
if (m_device != VK_NULL_HANDLE) {
|
||||||
m_frameContext.Destroy(m_device, m_commandPool);
|
m_frameContext.Destroy(m_device, m_commandPool);
|
||||||
}
|
}
|
||||||
@@ -5457,6 +5477,63 @@ void main() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VulkanRenderer::OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) {
|
||||||
|
if (m_timerQueryManager) {
|
||||||
|
m_timerQueryManager->OnFrameCommandRecordingBegan(commandBuffer, m_frameContext.GetCurrentFrameIndex(),
|
||||||
|
m_bufferManager.GetFrameSerial());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool VulkanRenderer::IsTimerQuerySupported() const {
|
||||||
|
return m_timerQuerySupported && m_timerQueryManager != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedPtr<VkTimerQueryManager::TimestampRecord> VulkanRenderer::WriteTimerQueryTimestamp() {
|
||||||
|
if (!IsTimerQuerySupported() || m_device == VK_NULL_HANDLE || m_frameContext.GetFrameCount() == 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto& frame = m_frameContext.GetCurrent();
|
||||||
|
if (!frame.isCommandRecording) {
|
||||||
|
m_frameContext.BeginCommandRecording();
|
||||||
|
}
|
||||||
|
// vkCmdWriteTimestamp is valid both inside and outside a render pass,
|
||||||
|
// so any active render pass is left untouched.
|
||||||
|
return m_timerQueryManager->WriteTimestamp(frame.commandBuffer, m_frameContext.GetCurrentFrameIndex(),
|
||||||
|
m_bufferManager.GetFrameSerial());
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool VulkanRenderer::IsTimerQueryResultReady(VkTimerQueryManager::TimestampRecord& record) {
|
||||||
|
if (record.harvested) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!m_timerQueryManager || !IsFrameSerialComplete(record.frameSerial)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return m_timerQueryManager->TryHarvest(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool VulkanRenderer::WaitForTimerQueryResult(VkTimerQueryManager::TimestampRecord& record) {
|
||||||
|
if (IsTimerQueryResultReady(record)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Mirrors ClientWaitSync: WaitForFrameSerial refuses serials that
|
||||||
|
// cannot complete without further submissions (a timestamp written
|
||||||
|
// this frame only executes once Present submits the command buffer).
|
||||||
|
if (!WaitForFrameSerial(record.frameSerial, UINT64_MAX)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return IsTimerQueryResultReady(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 VulkanRenderer::GetTimerQueryElapsedNs(const VkTimerQueryManager::TimestampRecord& begin,
|
||||||
|
const VkTimerQueryManager::TimestampRecord& end) const {
|
||||||
|
return m_timerQueryManager ? m_timerQueryManager->ElapsedNs(begin, end) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 VulkanRenderer::GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const {
|
||||||
|
return m_timerQueryManager ? m_timerQueryManager->TimestampNs(record) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
void VulkanRenderer::Present() {
|
void VulkanRenderer::Present() {
|
||||||
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");
|
||||||
@@ -5473,6 +5550,8 @@ void main() {
|
|||||||
const Bool collectPresentStats = (PresentStatsEnabled() || shouldDumpPresent) && frame.isCommandRecording &&
|
const Bool collectPresentStats = (PresentStatsEnabled() || shouldDumpPresent) && frame.isCommandRecording &&
|
||||||
presentStatsExtent.width > 0 && presentStatsExtent.height > 0;
|
presentStatsExtent.width > 0 && presentStatsExtent.height > 0;
|
||||||
if (PresentStatsEnabled() && presentDumpPath != nullptr) {
|
if (PresentStatsEnabled() && presentDumpPath != nullptr) {
|
||||||
|
// Live getenv on purpose (not MG_Config::Features): the retrace
|
||||||
|
// harness mutates these two variables at runtime via setenv.
|
||||||
const char* targetCall = std::getenv("MOBILEGL_PRESENT_DUMP_CALL");
|
const char* targetCall = std::getenv("MOBILEGL_PRESENT_DUMP_CALL");
|
||||||
const char* currentCall = std::getenv("MOBILEGL_PRESENT_CURRENT_CALL");
|
const char* currentCall = std::getenv("MOBILEGL_PRESENT_CURRENT_CALL");
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
@@ -6075,6 +6154,21 @@ void main() {
|
|||||||
vkGetDeviceQueue(m_device, m_physicalDevice.queueFamilies.graphicsFamily, 0, &m_graphicsQueue);
|
vkGetDeviceQueue(m_device, m_physicalDevice.queueFamilies.graphicsFamily, 0, &m_graphicsQueue);
|
||||||
vkGetDeviceQueue(m_device, m_physicalDevice.queueFamilies.presentFamily, 0, &m_presentQueue);
|
vkGetDeviceQueue(m_device, m_physicalDevice.queueFamilies.presentFamily, 0, &m_presentQueue);
|
||||||
MGLOG_I("Queues got successfully.");
|
MGLOG_I("Queues got successfully.");
|
||||||
|
|
||||||
|
// Timestamp (timer query) support: re-enumerate the graphics queue
|
||||||
|
// family's properties for its timestampValidBits (0 means the queue
|
||||||
|
// cannot write timestamps) and take timestampPeriod (ns per tick) from
|
||||||
|
// the device limits.
|
||||||
|
const auto timestampQueueFamilies = GetQueueFamilyFromPhysicalDevice(m_physicalDevice.handle);
|
||||||
|
m_timestampValidBits = 0;
|
||||||
|
const Int32 graphicsFamilyIndex = m_physicalDevice.queueFamilies.graphicsFamily;
|
||||||
|
if (graphicsFamilyIndex >= 0 && static_cast<SizeT>(graphicsFamilyIndex) < timestampQueueFamilies.size()) {
|
||||||
|
m_timestampValidBits = timestampQueueFamilies[graphicsFamilyIndex].timestampValidBits;
|
||||||
|
}
|
||||||
|
m_timestampPeriodNs = m_physicalDevice.properties.limits.timestampPeriod;
|
||||||
|
m_timerQuerySupported = m_timestampValidBits > 0 && m_timestampPeriodNs > 0.0f;
|
||||||
|
MGLOG_I("Timer queries %s (timestampValidBits=%u, timestampPeriod=%f ns/tick)",
|
||||||
|
m_timerQuerySupported ? "supported" : "not supported", m_timestampValidBits, m_timestampPeriodNs);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VulkanRenderer::CreateAllocator() {
|
void VulkanRenderer::CreateAllocator() {
|
||||||
@@ -6327,6 +6421,14 @@ void main() {
|
|||||||
|
|
||||||
vkDeviceWaitIdle(m_device);
|
vkDeviceWaitIdle(m_device);
|
||||||
|
|
||||||
|
if (m_timerQueryManager) {
|
||||||
|
// The in-progress command buffer is abandoned below (its recording
|
||||||
|
// flags are force-cleared), so timestamp writes recorded into it
|
||||||
|
// will never execute; resolve or invalidate all pending records now
|
||||||
|
// to keep later waits from hanging on never-available queries.
|
||||||
|
m_timerQueryManager->InvalidatePendingRecords();
|
||||||
|
}
|
||||||
|
|
||||||
DestroyDeferredDepthMipmapCleanup();
|
DestroyDeferredDepthMipmapCleanup();
|
||||||
m_deferredDepthMipmapCleanup.assign(m_frameContext.GetFrameCount(), {});
|
m_deferredDepthMipmapCleanup.assign(m_frameContext.GetFrameCount(), {});
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include "VkRenderPassManager.h"
|
#include "VkRenderPassManager.h"
|
||||||
#include "VkSamplerManager.h"
|
#include "VkSamplerManager.h"
|
||||||
#include "VkTextureManager.h"
|
#include "VkTextureManager.h"
|
||||||
|
#include "VkTimerQueryManager.h"
|
||||||
#include "MG_Util/Math/VectorTypes.h"
|
#include "MG_Util/Math/VectorTypes.h"
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
#include <vk_mem_alloc.h>
|
#include <vk_mem_alloc.h>
|
||||||
@@ -101,7 +102,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class VulkanRenderer : public IBufferCopyCommandProvider {
|
class VulkanRenderer : public IBufferCopyCommandProvider, public FrameContext::IRecordingObserver {
|
||||||
public:
|
public:
|
||||||
VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg = {});
|
VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg = {});
|
||||||
~VulkanRenderer();
|
~VulkanRenderer();
|
||||||
@@ -113,6 +114,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// render pass, for immediate staged buffer copies.
|
// render pass, for immediate staged buffer copies.
|
||||||
VkCommandBuffer AcquireBufferCopyCommandBuffer() override;
|
VkCommandBuffer AcquireBufferCopyCommandBuffer() override;
|
||||||
|
|
||||||
|
// FrameContext::IRecordingObserver: prepares the frame's timer-query
|
||||||
|
// pool (harvest + reset) right after the frame command buffer begins
|
||||||
|
// recording, before any render pass.
|
||||||
|
void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) 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);
|
||||||
@@ -178,6 +184,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// current, not-yet-presented frame) or when the wait failed.
|
// current, not-yet-presented frame) or when the wait failed.
|
||||||
Bool WaitForFrameSerial(Uint64 serial, Uint64 timeoutNs);
|
Bool WaitForFrameSerial(Uint64 serial, Uint64 timeoutNs);
|
||||||
|
|
||||||
|
// GPU timer queries, backing the GL_TIME_ELAPSED / GL_TIMESTAMP
|
||||||
|
// frontend. Timestamp support (queue timestampValidBits > 0 and a
|
||||||
|
// non-zero timestampPeriod) is cached at device creation.
|
||||||
|
Bool IsTimerQuerySupported() const;
|
||||||
|
// Ensures the frame command buffer is recording (same lazy pattern as
|
||||||
|
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
|
||||||
|
// frame's pool. Null when unsupported or the pool is exhausted.
|
||||||
|
SharedPtr<VkTimerQueryManager::TimestampRecord> WriteTimerQueryTimestamp();
|
||||||
|
// Non-blocking: true once the record's raw ticks are on the CPU
|
||||||
|
// (harvests the slot once its frame serial has completed).
|
||||||
|
Bool IsTimerQueryResultReady(VkTimerQueryManager::TimestampRecord& record);
|
||||||
|
// Blocking wait, mirroring ClientWaitSync's caveat: a record written
|
||||||
|
// this frame cannot complete until Present submits the commands, so
|
||||||
|
// this returns false (result reads as 0) instead of deadlocking.
|
||||||
|
Bool WaitForTimerQueryResult(VkTimerQueryManager::TimestampRecord& record);
|
||||||
|
Uint64 GetTimerQueryElapsedNs(const VkTimerQueryManager::TimestampRecord& begin,
|
||||||
|
const VkTimerQueryManager::TimestampRecord& end) const;
|
||||||
|
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;
|
||||||
|
|
||||||
void RequestSwapchainResize(Uint32 width, Uint32 height);
|
void RequestSwapchainResize(Uint32 width, Uint32 height);
|
||||||
void RecreateSwapchain();
|
void RecreateSwapchain();
|
||||||
|
|
||||||
@@ -246,6 +271,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool m_multiDrawIndirectFeatureEnabled = false;
|
Bool m_multiDrawIndirectFeatureEnabled = false;
|
||||||
Bool m_shaderDrawParametersExtensionEnabled = false;
|
Bool m_shaderDrawParametersExtensionEnabled = false;
|
||||||
Bool m_shaderDrawParametersFeatureEnabled = false;
|
Bool m_shaderDrawParametersFeatureEnabled = false;
|
||||||
|
// Cached at device creation from the graphics queue family properties
|
||||||
|
// and device limits; drives timer-query support.
|
||||||
|
Uint32 m_timestampValidBits = 0;
|
||||||
|
Float m_timestampPeriodNs = 0.0f;
|
||||||
|
Bool m_timerQuerySupported = false;
|
||||||
using PFNDrawIndexedIndirectCountFunc = void(VKAPI_PTR*)(VkCommandBuffer commandBuffer, VkBuffer buffer,
|
using PFNDrawIndexedIndirectCountFunc = void(VKAPI_PTR*)(VkCommandBuffer commandBuffer, VkBuffer buffer,
|
||||||
VkDeviceSize offset, VkBuffer countBuffer,
|
VkDeviceSize offset, VkBuffer countBuffer,
|
||||||
VkDeviceSize countBufferOffset, Uint32 maxDrawCount,
|
VkDeviceSize countBufferOffset, Uint32 maxDrawCount,
|
||||||
@@ -268,6 +298,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
UniquePtr<VkRenderPassManager> m_renderPassManager;
|
UniquePtr<VkRenderPassManager> m_renderPassManager;
|
||||||
UniquePtr<VkTextureManager> m_textureManager;
|
UniquePtr<VkTextureManager> m_textureManager;
|
||||||
UniquePtr<VkSamplerManager> m_samplerManager;
|
UniquePtr<VkSamplerManager> m_samplerManager;
|
||||||
|
UniquePtr<VkTimerQueryManager> m_timerQueryManager;
|
||||||
BlitResources m_blitResources;
|
BlitResources m_blitResources;
|
||||||
DepthMipmapResources m_depthMipmapResources;
|
DepthMipmapResources m_depthMipmapResources;
|
||||||
Vector<DeferredDepthMipmapCleanup> m_deferredDepthMipmapCleanup;
|
Vector<DeferredDepthMipmapCleanup> m_deferredDepthMipmapCleanup;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "../Getter/GL_Getter.h"
|
#include "../Getter/GL_Getter.h"
|
||||||
#include "../Sampler/GL_Sampler.h"
|
#include "../Sampler/GL_Sampler.h"
|
||||||
#include "../Sync/GL_Sync.h"
|
#include "../Sync/GL_Sync.h"
|
||||||
|
#include "../Query/GL_Query.h"
|
||||||
#include "../Texture/GL_Texture.h"
|
#include "../Texture/GL_Texture.h"
|
||||||
#include "../Drawing/GL_Drawing.h"
|
#include "../Drawing/GL_Drawing.h"
|
||||||
#include "../Program/GL_Program.h"
|
#include "../Program/GL_Program.h"
|
||||||
@@ -209,21 +210,13 @@ DECLARE_GL_FUNCTION_HEAD(void, TexSubImage3D, GLenum target, GLint level, GLint
|
|||||||
DECLARE_GL_FUNCTION_HEAD(void, CopyTexSubImage3D, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTexSubImage3D, target, level, xoffset, yoffset, zoffset, x, y, width, height)
|
DECLARE_GL_FUNCTION_HEAD(void, CopyTexSubImage3D, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTexSubImage3D, target, level, xoffset, yoffset, zoffset, x, y, width, height)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, CompressedTexImage3D, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTexImage3D, target, level, internalformat, width, height, depth, border, imageSize, data)
|
DECLARE_GL_FUNCTION_HEAD(void, CompressedTexImage3D, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTexImage3D, target, level, internalformat, width, height, depth, border, imageSize, data)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, CompressedTexSubImage3D, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTexSubImage3D, target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
|
DECLARE_GL_FUNCTION_HEAD(void, CompressedTexSubImage3D, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTexSubImage3D, target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenQueries, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenQueries, n, ids)
|
DECLARE_GL_FUNCTION_HEAD(void, GenQueries, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenQueries, n, ids)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteQueries, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteQueries, n, ids)
|
DECLARE_GL_FUNCTION_HEAD(void, DeleteQueries, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DeleteQueries, n, ids)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, IsQuery, GLuint id) DECLARE_GL_FUNCTION_STUB_END(GLboolean, IsQuery, id)
|
DECLARE_GL_FUNCTION_HEAD(GLboolean, IsQuery, GLuint id) DECLARE_GL_FUNCTION_END(GLboolean, IsQuery, id)
|
||||||
MOBILEGL_GL_API void glBeginQuery(GLenum target, GLuint id) {
|
DECLARE_GL_FUNCTION_HEAD(void, BeginQuery, GLenum target, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginQuery, target, id)
|
||||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
|
DECLARE_GL_FUNCTION_HEAD(void, EndQuery, GLenum target) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndQuery, target)
|
||||||
if (id != 0) {
|
DECLARE_GL_FUNCTION_HEAD(void, GetQueryiv, GLenum target, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryiv, target, pname, params)
|
||||||
MobileGL::MG_State::pGLContext->RecordError(
|
DECLARE_GL_FUNCTION_HEAD(void, GetQueryObjectuiv, GLuint id, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryObjectuiv, id, pname, params)
|
||||||
MobileGL::ErrorCode::InvalidOperation,
|
|
||||||
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>("MG_Impl/GLImpl", __FUNCTION__,
|
|
||||||
"Query object does not exist."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, EndQuery, GLenum target) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndQuery, target)
|
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryiv, GLenum target, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryiv, target, pname, params)
|
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryObjectuiv, GLuint id, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryObjectuiv, id, pname, params)
|
|
||||||
DECLARE_GL_FUNCTION_HEAD(GLboolean, UnmapBuffer, GLenum target) DECLARE_GL_FUNCTION_END(GLboolean, UnmapBuffer, target)
|
DECLARE_GL_FUNCTION_HEAD(GLboolean, UnmapBuffer, GLenum target) DECLARE_GL_FUNCTION_END(GLboolean, UnmapBuffer, target)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetBufferPointerv, GLenum target, GLenum pname, void** params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetBufferPointerv, target, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetBufferPointerv, GLenum target, GLenum pname, void** params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetBufferPointerv, target, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawBuffers, GLsizei n, const GLenum* bufs) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawBuffers, n, bufs)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawBuffers, GLsizei n, const GLenum* bufs) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawBuffers, n, bufs)
|
||||||
@@ -836,7 +829,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3i, GLint x, GLint y, GLint z) DECL
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3iv, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3iv, v)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3iv, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3iv, v)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3s, GLshort x, GLshort y, GLshort z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3s, x, y, z)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3s, GLshort x, GLshort y, GLshort z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3s, x, y, z)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3sv, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3sv, v)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3sv, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3sv, v)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryObjectiv, GLuint id, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryObjectiv, id, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetQueryObjectiv, GLuint id, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryObjectiv, id, pname, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetBufferSubData, GLenum target, GLintptr offset, GLsizeiptr size, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetBufferSubData, target, offset, size, data)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetBufferSubData, GLenum target, GLintptr offset, GLsizeiptr size, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetBufferSubData, target, offset, size, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexAttribdv, GLuint index, GLenum pname, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexAttribdv, index, pname, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexAttribdv, GLuint index, GLenum pname, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexAttribdv, index, pname, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib1d, GLuint index, GLdouble x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib1d, index, x)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib1d, GLuint index, GLdouble x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib1d, index, x)
|
||||||
@@ -875,9 +868,9 @@ DECLARE_GL_FUNCTION_HEAD(void, TexImage2DMultisample, GLenum target, GLsizei sam
|
|||||||
DECLARE_GL_FUNCTION_HEAD(void, TexImage3DMultisample, GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexImage3DMultisample, target, samples, internalformat, width, height, depth, fixedsamplelocations)
|
DECLARE_GL_FUNCTION_HEAD(void, TexImage3DMultisample, GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexImage3DMultisample, target, samples, internalformat, width, height, depth, fixedsamplelocations)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindFragDataLocationIndexed, GLuint program, GLuint colorNumber, GLuint index, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindFragDataLocationIndexed, program, colorNumber, index, name)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindFragDataLocationIndexed, GLuint program, GLuint colorNumber, GLuint index, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindFragDataLocationIndexed, program, colorNumber, index, name)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLint, GetFragDataIndex, GLuint program, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END(GLint, GetFragDataIndex, program, name)
|
DECLARE_GL_FUNCTION_STUB_HEAD(GLint, GetFragDataIndex, GLuint program, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END(GLint, GetFragDataIndex, program, name)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, QueryCounter, GLuint id, GLenum target) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, QueryCounter, id, target)
|
DECLARE_GL_FUNCTION_HEAD(void, QueryCounter, GLuint id, GLenum target) DECLARE_GL_FUNCTION_END_NO_RETURN(void, QueryCounter, id, target)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryObjecti64v, GLuint id, GLenum pname, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryObjecti64v, id, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetQueryObjecti64v, GLuint id, GLenum pname, GLint64* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryObjecti64v, id, pname, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryObjectui64v, GLuint id, GLenum pname, GLuint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryObjectui64v, id, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetQueryObjectui64v, GLuint id, GLenum pname, GLuint64* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryObjectui64v, id, pname, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribP1ui, GLuint index, GLenum type, GLboolean normalized, GLuint value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribP1ui, index, type, normalized, value)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribP1ui, GLuint index, GLenum type, GLboolean normalized, GLuint value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribP1ui, index, type, normalized, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribP1uiv, GLuint index, GLenum type, GLboolean normalized, const GLuint* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribP1uiv, index, type, normalized, value)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribP1uiv, GLuint index, GLenum type, GLboolean normalized, const GLuint* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribP1uiv, index, type, normalized, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribP2ui, GLuint index, GLenum type, GLboolean normalized, GLuint value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribP2ui, index, type, normalized, value)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribP2ui, GLuint index, GLenum type, GLboolean normalized, GLuint value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribP2ui, index, type, normalized, value)
|
||||||
@@ -2111,8 +2104,8 @@ DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, AreTexturesResidentEXT, GLsizei n, cons
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrioritizeTexturesEXT, GLsizei n, const GLuint* textures, const GLclampf* priorities) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PrioritizeTexturesEXT, n, textures, priorities)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrioritizeTexturesEXT, GLsizei n, const GLuint* textures, const GLclampf* priorities) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PrioritizeTexturesEXT, n, textures, priorities)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureNormalEXT, GLenum mode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureNormalEXT, mode)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureNormalEXT, GLenum mode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureNormalEXT, mode)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TexStorage1DEXT, GLenum target, GLsizei levels, GLenum internalformat, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage1D, target, levels, internalformat, width)
|
DECLARE_GL_FUNCTION_HEAD(void, TexStorage1DEXT, GLenum target, GLsizei levels, GLenum internalformat, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage1D, target, levels, internalformat, width)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryObjecti64vEXT, GLuint id, GLenum pname, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryObjecti64vEXT, id, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetQueryObjecti64vEXT, GLuint id, GLenum pname, GLint64* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryObjecti64v, id, pname, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryObjectui64vEXT, GLuint id, GLenum pname, GLuint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryObjectui64vEXT, id, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetQueryObjectui64vEXT, GLuint id, GLenum pname, GLuint64* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryObjectui64v, id, pname, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindBufferOffsetEXT, GLenum target, GLuint index, GLuint buffer, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindBufferOffsetEXT, target, index, buffer, offset)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindBufferOffsetEXT, GLenum target, GLuint index, GLuint buffer, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindBufferOffsetEXT, target, index, buffer, offset)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ArrayElementEXT, GLint i) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ArrayElementEXT, i)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, ArrayElementEXT, GLint i) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ArrayElementEXT, i)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ColorPointerEXT, GLint size, GLenum type, GLsizei stride, GLsizei count, const void* pointer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ColorPointerEXT, size, type, stride, count, pointer)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, ColorPointerEXT, GLint size, GLenum type, GLsizei stride, GLsizei count, const void* pointer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ColorPointerEXT, size, type, stride, count, pointer)
|
||||||
|
|||||||
@@ -793,6 +793,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
params[0] = static_cast<GLint64>(value);
|
params[0] = static_cast<GLint64>(value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case GL_TIMESTAMP: {
|
||||||
|
// Handled here (not via the 32-bit GetIntegerv fallback) so the
|
||||||
|
// full 64-bit GPU timestamp survives; LWJGL reads it this way.
|
||||||
|
Int64 timestamp = 0;
|
||||||
|
if (!MG_Config::Features.DisableTimerQuery) {
|
||||||
|
if (const auto getGpuTimestampNs = MG_Backend::gBackendFunctionsTable.GL.GetGpuTimestampNs) {
|
||||||
|
timestamp = getGpuTimestampNs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
params[0] = static_cast<GLint64>(timestamp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1563,9 +1575,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT:
|
case GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT:
|
||||||
*params = 0; // texture-buffer range entrypoints are stubbed
|
*params = 0; // texture-buffer range entrypoints are stubbed
|
||||||
return;
|
return;
|
||||||
case GL_TIMESTAMP:
|
case GL_TIMESTAMP: {
|
||||||
*params = 0; // timer-query entrypoints are stubbed
|
Int64 timestamp = 0;
|
||||||
|
if (!MG_Config::Features.DisableTimerQuery) {
|
||||||
|
if (const auto getGpuTimestampNs = MG_Backend::gBackendFunctionsTable.GL.GetGpuTimestampNs) {
|
||||||
|
timestamp = getGpuTimestampNs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 32-bit query: clamp per the GL state-query conversion rules.
|
||||||
|
*params = timestamp > static_cast<Int64>(INT_MAX) ? INT_MAX : static_cast<GLint>(timestamp);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
case GL_TRANSFORM_FEEDBACK_BUFFER_BINDING:
|
case GL_TRANSFORM_FEEDBACK_BUFFER_BINDING:
|
||||||
if (const auto& obj =
|
if (const auto& obj =
|
||||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::TransformFeedback).GetBoundObject()) {
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::TransformFeedback).GetBoundObject()) {
|
||||||
|
|||||||
@@ -0,0 +1,362 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/GLImpl/Query/GL_Query.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 "GL_Query.h"
|
||||||
|
#include <Config.h>
|
||||||
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
|
namespace {
|
||||||
|
// Frontend query object (GL_ARB_timer_query): wraps an optional backend
|
||||||
|
// timer-query handle. A null backend handle (backend has no timer-query
|
||||||
|
// support, timer queries are disabled by config, or the backend could
|
||||||
|
// not create a query at call time) keeps a graceful fallback: the query
|
||||||
|
// result is immediately available and reads as zero.
|
||||||
|
struct QueryObject {
|
||||||
|
GLuint id = 0;
|
||||||
|
GLenum target = 0; // 0 = gen'd but never used with BeginQuery/QueryCounter
|
||||||
|
MG_Backend::BackendQueryHandle backendHandle = nullptr;
|
||||||
|
Bool active = false;
|
||||||
|
Bool ended = false;
|
||||||
|
Bool resultCached = false;
|
||||||
|
Uint64 cachedResult = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Query calls may arrive from any thread (launchers migrate the context
|
||||||
|
// across JVM threads), so the live-object registry is mutex-guarded,
|
||||||
|
// like the sync-object registry in GL_Sync.cpp. Entries left at process
|
||||||
|
// shutdown are simply dropped; their backend handles die with the
|
||||||
|
// backend.
|
||||||
|
std::mutex g_queryObjectsMutex;
|
||||||
|
UnorderedMap<GLuint, QueryObject*> g_liveQueryObjects;
|
||||||
|
// Monotonically increasing id allocator; ids are valid query objects
|
||||||
|
// immediately after GenQueries.
|
||||||
|
GLuint g_nextQueryId = 1;
|
||||||
|
// Id of the query currently active on GL_TIME_ELAPSED (0 = none).
|
||||||
|
GLuint g_activeTimeElapsedQueryId = 0;
|
||||||
|
|
||||||
|
Bool TimerQueryDisabled() {
|
||||||
|
return MG_Config::Features.DisableTimerQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RecordQueryError(ErrorCode code, const char* function, const char* message) {
|
||||||
|
MG_State::pGLContext->RecordError(code,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callers must hold g_queryObjectsMutex.
|
||||||
|
QueryObject* FindQueryObjectLocked(GLuint id) {
|
||||||
|
const auto it = g_liveQueryObjects.find(id);
|
||||||
|
return it != g_liveQueryObjects.end() ? it->second : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callers must hold g_queryObjectsMutex. Releases the backend handle
|
||||||
|
// (if any) and clears any cached result, so the object can be reused.
|
||||||
|
void ResetQueryObjectLocked(QueryObject* queryObject) {
|
||||||
|
if (queryObject->backendHandle) {
|
||||||
|
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||||
|
deleteBackendQuery(queryObject->backendHandle);
|
||||||
|
}
|
||||||
|
queryObject->backendHandle = nullptr;
|
||||||
|
}
|
||||||
|
queryObject->active = false;
|
||||||
|
queryObject->ended = false;
|
||||||
|
queryObject->resultCached = false;
|
||||||
|
queryObject->cachedResult = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callers must hold g_queryObjectsMutex.
|
||||||
|
void EndTimeElapsedQueryLocked(QueryObject* queryObject) {
|
||||||
|
const auto endTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.EndTimeElapsedQuery;
|
||||||
|
if (endTimeElapsedQuery && queryObject->backendHandle) {
|
||||||
|
endTimeElapsedQuery(queryObject->backendHandle);
|
||||||
|
}
|
||||||
|
queryObject->active = false;
|
||||||
|
queryObject->ended = true;
|
||||||
|
g_activeTimeElapsedQueryId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared GetQueryObject* implementation. Returns false when an error
|
||||||
|
// was recorded and no value should be written back.
|
||||||
|
Bool GetQueryObjectValue(GLuint id, GLenum pname, const char* function, Uint64& outValue) {
|
||||||
|
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||||
|
auto* queryObject = FindQueryObjectLocked(id);
|
||||||
|
if (!queryObject) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, function, "Query object does not exist.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (queryObject->active) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, function, "Query object is still active.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (pname) {
|
||||||
|
case GL_QUERY_RESULT_AVAILABLE: {
|
||||||
|
if (queryObject->resultCached || !queryObject->backendHandle) {
|
||||||
|
outValue = 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const auto isQueryResultAvailable = MG_Backend::gBackendFunctionsTable.GL.IsQueryResultAvailable;
|
||||||
|
outValue = (!isQueryResultAvailable || isQueryResultAvailable(queryObject->backendHandle)) ? 1 : 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case GL_QUERY_RESULT: {
|
||||||
|
if (queryObject->resultCached) {
|
||||||
|
outValue = queryObject->cachedResult;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Uint64 result = 0;
|
||||||
|
if (queryObject->backendHandle) {
|
||||||
|
const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64;
|
||||||
|
if (getQueryResult64 &&
|
||||||
|
!getQueryResult64(queryObject->backendHandle, /*wait=*/true, &result)) {
|
||||||
|
// The backend could not produce the result YET (e.g. a
|
||||||
|
// Vulkan wait refusing to block on a not-yet-submitted
|
||||||
|
// frame serial). Per the documented no-stall tradeoff
|
||||||
|
// this call reads 0, but the value is NOT cached and
|
||||||
|
// the backend handle is kept, so a later AVAILABLE
|
||||||
|
// poll / RESULT read still produces the real value.
|
||||||
|
outValue = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Final value produced (or no GetQueryResult64 hook: the
|
||||||
|
// query degrades to a zero result); the backend handle is
|
||||||
|
// consumed and the value cached for later reads.
|
||||||
|
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||||
|
deleteBackendQuery(queryObject->backendHandle);
|
||||||
|
}
|
||||||
|
queryObject->backendHandle = nullptr;
|
||||||
|
}
|
||||||
|
queryObject->cachedResult = result;
|
||||||
|
queryObject->resultCached = true;
|
||||||
|
outValue = result;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
RecordQueryError(ErrorCode::InvalidEnum, function, "Unsupported query object parameter.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void GenQueries(GLsizei n, GLuint* ids) {
|
||||||
|
if (n < 0) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "n cannot be negative.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ids) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||||
|
for (GLsizei i = 0; i < n; ++i) {
|
||||||
|
const GLuint id = g_nextQueryId++;
|
||||||
|
auto* queryObject = new QueryObject;
|
||||||
|
queryObject->id = id;
|
||||||
|
g_liveQueryObjects[id] = queryObject;
|
||||||
|
ids[i] = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeleteQueries(GLsizei n, const GLuint* ids) {
|
||||||
|
if (n < 0) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "n cannot be negative.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ids) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||||
|
for (GLsizei i = 0; i < n; ++i) {
|
||||||
|
const auto it = g_liveQueryObjects.find(ids[i]);
|
||||||
|
if (it == g_liveQueryObjects.end()) {
|
||||||
|
continue; // unknown ids are silently ignored
|
||||||
|
}
|
||||||
|
QueryObject* queryObject = it->second;
|
||||||
|
if (queryObject->active) {
|
||||||
|
EndTimeElapsedQueryLocked(queryObject); // implicitly end before deletion
|
||||||
|
}
|
||||||
|
if (queryObject->backendHandle) {
|
||||||
|
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||||
|
deleteBackendQuery(queryObject->backendHandle);
|
||||||
|
}
|
||||||
|
queryObject->backendHandle = nullptr;
|
||||||
|
}
|
||||||
|
g_liveQueryObjects.erase(it);
|
||||||
|
delete queryObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GLboolean IsQuery(GLuint id) {
|
||||||
|
if (id == 0) {
|
||||||
|
return GL_FALSE;
|
||||||
|
}
|
||||||
|
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||||
|
// Gen'd ids count as query objects here: the registry creates live
|
||||||
|
// objects at GenQueries time.
|
||||||
|
return FindQueryObjectLocked(id) != nullptr ? GL_TRUE : GL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BeginQuery(GLenum target, GLuint id) {
|
||||||
|
if (target != GL_TIME_ELAPSED) {
|
||||||
|
// Only GL_TIME_ELAPSED timer queries are implemented (occlusion and
|
||||||
|
// primitive queries remain stubs); GL_TIMESTAMP is not a valid
|
||||||
|
// BeginQuery target either.
|
||||||
|
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (id == 0) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query id 0 cannot be used.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||||
|
auto* queryObject = FindQueryObjectLocked(id);
|
||||||
|
if (!queryObject) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (g_activeTimeElapsedQueryId != 0) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
|
||||||
|
"A query is already active on GL_TIME_ELAPSED.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (queryObject->active) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object is already active.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (queryObject->target != 0 && queryObject->target != target) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
|
||||||
|
"Query object was already used with a different target.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetQueryObjectLocked(queryObject); // discard any previous result
|
||||||
|
queryObject->target = target;
|
||||||
|
queryObject->active = true;
|
||||||
|
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
|
||||||
|
queryObject->backendHandle =
|
||||||
|
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
|
||||||
|
g_activeTimeElapsedQueryId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndQuery(GLenum target) {
|
||||||
|
if (target != GL_TIME_ELAPSED) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||||
|
if (g_activeTimeElapsedQueryId == 0) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on GL_TIME_ELAPSED.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto* queryObject = FindQueryObjectLocked(g_activeTimeElapsedQueryId);
|
||||||
|
if (!queryObject) {
|
||||||
|
g_activeTimeElapsedQueryId = 0; // should not happen; keep state consistent
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
EndTimeElapsedQueryLocked(queryObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
void QueryCounter(GLuint id, GLenum target) {
|
||||||
|
if (target != GL_TIMESTAMP) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "QueryCounter target must be GL_TIMESTAMP.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (id == 0) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query id 0 cannot be used.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||||
|
auto* queryObject = FindQueryObjectLocked(id);
|
||||||
|
if (!queryObject) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (queryObject->active) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object is currently active.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (queryObject->target != 0 && queryObject->target != target) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
|
||||||
|
"Query object was already used with a different target.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetQueryObjectLocked(queryObject); // discard any previous result
|
||||||
|
queryObject->target = target;
|
||||||
|
const auto queryCounterTimestamp = MG_Backend::gBackendFunctionsTable.GL.QueryCounterTimestamp;
|
||||||
|
queryObject->backendHandle =
|
||||||
|
(!TimerQueryDisabled() && queryCounterTimestamp) ? queryCounterTimestamp() : nullptr;
|
||||||
|
queryObject->ended = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetQueryiv(GLenum target, GLenum pname, GLint* params) {
|
||||||
|
if (!params) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (pname) {
|
||||||
|
case GL_CURRENT_QUERY: {
|
||||||
|
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||||
|
// Only GL_TIME_ELAPSED queries can be active; GL_TIMESTAMP queries
|
||||||
|
// never are, and other targets remain unimplemented.
|
||||||
|
*params = target == GL_TIME_ELAPSED ? static_cast<GLint>(g_activeTimeElapsedQueryId) : 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case GL_QUERY_COUNTER_BITS: {
|
||||||
|
// 64 bits are advertised only while the live backend can actually
|
||||||
|
// time: IsTimerQuerySupported is the dynamic truth (extension /
|
||||||
|
// entry points / timestamp valid bits at call time, not at table
|
||||||
|
// init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always
|
||||||
|
// wins. Non-timer targets remain unimplemented and report 0.
|
||||||
|
const Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP;
|
||||||
|
const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported;
|
||||||
|
const Bool supported =
|
||||||
|
timerTarget && !TimerQueryDisabled() && isTimerQuerySupported && isTimerQuerySupported();
|
||||||
|
*params = supported ? 64 : 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Unsupported query parameter.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetQueryObjectiv(GLuint id, GLenum pname, GLint* params) {
|
||||||
|
Uint64 value = 0;
|
||||||
|
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
constexpr Uint64 kMaxInt = static_cast<Uint64>(INT_MAX);
|
||||||
|
*params = value > kMaxInt ? INT_MAX : static_cast<GLint>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetQueryObjectuiv(GLuint id, GLenum pname, GLuint* params) {
|
||||||
|
Uint64 value = 0;
|
||||||
|
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*params = static_cast<GLuint>(value & 0xFFFFFFFFull);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetQueryObjecti64v(GLuint id, GLenum pname, GLint64* params) {
|
||||||
|
Uint64 value = 0;
|
||||||
|
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*params = static_cast<GLint64>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetQueryObjectui64v(GLuint id, GLenum pname, GLuint64* params) {
|
||||||
|
Uint64 value = 0;
|
||||||
|
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*params = static_cast<GLuint64>(value);
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/GLImpl/Query/GL_Query.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>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
|
void GenQueries(GLsizei n, GLuint* ids);
|
||||||
|
void DeleteQueries(GLsizei n, const GLuint* ids);
|
||||||
|
GLboolean IsQuery(GLuint id);
|
||||||
|
void BeginQuery(GLenum target, GLuint id);
|
||||||
|
void EndQuery(GLenum target);
|
||||||
|
void GetQueryiv(GLenum target, GLenum pname, GLint* params);
|
||||||
|
void GetQueryObjectiv(GLuint id, GLenum pname, GLint* params);
|
||||||
|
void GetQueryObjectuiv(GLuint id, GLenum pname, GLuint* params);
|
||||||
|
void GetQueryObjecti64v(GLuint id, GLenum pname, GLint64* params);
|
||||||
|
void GetQueryObjectui64v(GLuint id, GLenum pname, GLuint64* params);
|
||||||
|
void QueryCounter(GLuint id, GLenum target);
|
||||||
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
@@ -71,6 +71,7 @@ add_subdirectory(Framebuffer)
|
|||||||
add_subdirectory(Texture)
|
add_subdirectory(Texture)
|
||||||
add_subdirectory(VertexArray)
|
add_subdirectory(VertexArray)
|
||||||
add_subdirectory(Program)
|
add_subdirectory(Program)
|
||||||
|
add_subdirectory(Query)
|
||||||
if (ENABLE_INTEGRATION_TESTS)
|
if (ENABLE_INTEGRATION_TESTS)
|
||||||
add_subdirectory(Backend/DirectVulkan)
|
add_subdirectory(Backend/DirectVulkan)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
|
||||||
|
add_executable(
|
||||||
|
QueryTest
|
||||||
|
QueryTest.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(QueryTest PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
QueryTest PRIVATE
|
||||||
|
GTest::gtest_main
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
include(GoogleTest)
|
||||||
|
gtest_discover_tests(QueryTest DISCOVERY_TIMEOUT 30)
|
||||||
@@ -0,0 +1,439 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/Query/QueryTest.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 <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include "Init.h"
|
||||||
|
#include <Config.h>
|
||||||
|
|
||||||
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||||
|
#include <MG_Impl/GLImpl/Query/GL_Query.h>
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// On the ctest host no ES context is ever current, but the timer-query
|
||||||
|
// POINTERS of gBackendFunctionsTable.GL are NOT null: MG_Backend::Init
|
||||||
|
// populates the real DirectGLES hooks (see GetBackendFunctions in
|
||||||
|
// BackendObject_DirectGLES.cpp, which installs them whenever
|
||||||
|
// MOBILEGL_DISABLE_TIMERQUERY is unset). Without a current ES context
|
||||||
|
// those hooks degrade to returning null HANDLES, so the fallback these
|
||||||
|
// tests exercise is the frontend's null-handle path (results immediately
|
||||||
|
// available and zero), not a null-pointer path. Tests that need
|
||||||
|
// controllable backend behavior install stubs and restore the table
|
||||||
|
// afterwards.
|
||||||
|
|
||||||
|
// Snapshots MG_Config::Features on construction and restores it on
|
||||||
|
// destruction, so a test that flips DisableTimerQuery cannot leak the
|
||||||
|
// setting into later tests even if an assertion unwinds the test body
|
||||||
|
// early (same pattern as SanityTest's ScopedGLESCapabilitiesOverride).
|
||||||
|
struct ScopedFeaturesOverride {
|
||||||
|
ScopedFeaturesOverride(): m_snapshot(MG_Config::Features) {}
|
||||||
|
~ScopedFeaturesOverride() { MG_Config::Features = m_snapshot; }
|
||||||
|
ScopedFeaturesOverride(const ScopedFeaturesOverride&) = delete;
|
||||||
|
ScopedFeaturesOverride& operator=(const ScopedFeaturesOverride&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
MG_Config::FeaturesTable m_snapshot;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Snapshots the global backend function table on construction and restores
|
||||||
|
// it on destruction, so stub timer-query pointers cannot leak into later
|
||||||
|
// tests.
|
||||||
|
struct ScopedBackendFunctionsOverride {
|
||||||
|
ScopedBackendFunctionsOverride(): m_snapshot(MG_Backend::gBackendFunctionsTable) {}
|
||||||
|
~ScopedBackendFunctionsOverride() { MG_Backend::gBackendFunctionsTable = m_snapshot; }
|
||||||
|
ScopedBackendFunctionsOverride(const ScopedBackendFunctionsOverride&) = delete;
|
||||||
|
ScopedBackendFunctionsOverride& operator=(const ScopedBackendFunctionsOverride&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
MG_Backend::GlobalBackendFunctionsTable m_snapshot;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stub backend timer-query implementation (plain function pointers, so the
|
||||||
|
// observable state lives in file-scope globals).
|
||||||
|
Int g_stubBeginCount = 0;
|
||||||
|
Int g_stubEndCount = 0;
|
||||||
|
Int g_stubCounterCount = 0;
|
||||||
|
Int g_stubDeleteCount = 0;
|
||||||
|
Bool g_stubTimerQuerySupported = true;
|
||||||
|
Bool g_stubResultAvailable = true;
|
||||||
|
// false = GetQueryResult64 cannot produce the result YET (returns false
|
||||||
|
// without writing outNanoseconds), mirroring e.g. a Vulkan wait on a
|
||||||
|
// not-yet-submitted frame serial.
|
||||||
|
Bool g_stubResultObtainable = true;
|
||||||
|
Uint64 g_stubResultNs = 0;
|
||||||
|
|
||||||
|
Bool StubIsTimerQuerySupported() { return g_stubTimerQuerySupported; }
|
||||||
|
|
||||||
|
MG_Backend::BackendQueryHandle StubBeginTimeElapsedQuery() {
|
||||||
|
++g_stubBeginCount;
|
||||||
|
return reinterpret_cast<MG_Backend::BackendQueryHandle>(static_cast<uintptr_t>(0x51));
|
||||||
|
}
|
||||||
|
|
||||||
|
void StubEndTimeElapsedQuery(MG_Backend::BackendQueryHandle) { ++g_stubEndCount; }
|
||||||
|
|
||||||
|
MG_Backend::BackendQueryHandle StubQueryCounterTimestamp() {
|
||||||
|
++g_stubCounterCount;
|
||||||
|
return reinterpret_cast<MG_Backend::BackendQueryHandle>(static_cast<uintptr_t>(0x52));
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool StubIsQueryResultAvailable(MG_Backend::BackendQueryHandle) { return g_stubResultAvailable; }
|
||||||
|
|
||||||
|
Bool StubGetQueryResult64(MG_Backend::BackendQueryHandle, Bool, Uint64* outNanoseconds) {
|
||||||
|
if (!g_stubResultObtainable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*outNanoseconds = g_stubResultNs;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StubDeleteBackendQuery(MG_Backend::BackendQueryHandle) { ++g_stubDeleteCount; }
|
||||||
|
|
||||||
|
void InstallStubBackendTimerQueries() {
|
||||||
|
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
|
||||||
|
backendGL.IsTimerQuerySupported = StubIsTimerQuerySupported;
|
||||||
|
backendGL.BeginTimeElapsedQuery = StubBeginTimeElapsedQuery;
|
||||||
|
backendGL.EndTimeElapsedQuery = StubEndTimeElapsedQuery;
|
||||||
|
backendGL.QueryCounterTimestamp = StubQueryCounterTimestamp;
|
||||||
|
backendGL.IsQueryResultAvailable = StubIsQueryResultAvailable;
|
||||||
|
backendGL.GetQueryResult64 = StubGetQueryResult64;
|
||||||
|
backendGL.DeleteBackendQuery = StubDeleteBackendQuery;
|
||||||
|
g_stubBeginCount = 0;
|
||||||
|
g_stubEndCount = 0;
|
||||||
|
g_stubCounterCount = 0;
|
||||||
|
g_stubDeleteCount = 0;
|
||||||
|
g_stubTimerQuerySupported = true;
|
||||||
|
g_stubResultAvailable = true;
|
||||||
|
g_stubResultObtainable = true;
|
||||||
|
g_stubResultNs = 0;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class QueryTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
// Drain errors recorded by earlier tests so assertions here are
|
||||||
|
// attributable to this test alone (GetError pops one queued error
|
||||||
|
// per call).
|
||||||
|
while (MG_Impl::GLImpl::GetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(QueryTest, GenQueriesReturnsDistinctNonzeroIdsAndTracksLiveness) {
|
||||||
|
GLuint ids[3] = {0, 0, 0};
|
||||||
|
MG_Impl::GLImpl::GenQueries(3, ids);
|
||||||
|
|
||||||
|
EXPECT_NE(ids[0], 0u);
|
||||||
|
EXPECT_NE(ids[1], 0u);
|
||||||
|
EXPECT_NE(ids[2], 0u);
|
||||||
|
EXPECT_NE(ids[0], ids[1]);
|
||||||
|
EXPECT_NE(ids[0], ids[2]);
|
||||||
|
EXPECT_NE(ids[1], ids[2]);
|
||||||
|
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(0), GL_FALSE);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[0]), GL_TRUE);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[1]), GL_TRUE);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[2]), GL_TRUE);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::DeleteQueries(3, ids);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[0]), GL_FALSE);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[1]), GL_FALSE);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(ids[2]), GL_FALSE);
|
||||||
|
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QueryTest, TimeElapsedSpanFallsBackToImmediateZeroResult) {
|
||||||
|
GLuint id = 0;
|
||||||
|
MG_Impl::GLImpl::GenQueries(1, &id);
|
||||||
|
ASSERT_NE(id, 0u);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, id);
|
||||||
|
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
// With null backend timer-query pointers (no ES context on the test host)
|
||||||
|
// the result is immediately available and reads as zero.
|
||||||
|
GLint available = -1;
|
||||||
|
MG_Impl::GLImpl::GetQueryObjectiv(id, GL_QUERY_RESULT_AVAILABLE, &available);
|
||||||
|
EXPECT_EQ(available, 1);
|
||||||
|
|
||||||
|
GLint64 result = -1;
|
||||||
|
MG_Impl::GLImpl::GetQueryObjecti64v(id, GL_QUERY_RESULT, &result);
|
||||||
|
EXPECT_EQ(result, 0);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::DeleteQueries(1, &id);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QueryTest, NestedBeginQueryRecordsInvalidOperation) {
|
||||||
|
GLuint ids[2] = {0, 0};
|
||||||
|
MG_Impl::GLImpl::GenQueries(2, ids);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, ids[0]);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, ids[1]);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||||
|
|
||||||
|
// The failed nested begin must not have displaced the active query.
|
||||||
|
GLint currentQuery = -1;
|
||||||
|
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_CURRENT_QUERY, ¤tQuery);
|
||||||
|
EXPECT_EQ(currentQuery, static_cast<GLint>(ids[0]));
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::DeleteQueries(2, ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QueryTest, EndQueryWithoutActiveQueryRecordsInvalidOperation) {
|
||||||
|
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QueryTest, QueryCounterTimestampResultImmediatelyAvailable) {
|
||||||
|
GLuint id = 0;
|
||||||
|
MG_Impl::GLImpl::GenQueries(1, &id);
|
||||||
|
ASSERT_NE(id, 0u);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::QueryCounter(id, GL_TIMESTAMP);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
GLint available = -1;
|
||||||
|
MG_Impl::GLImpl::GetQueryObjectiv(id, GL_QUERY_RESULT_AVAILABLE, &available);
|
||||||
|
EXPECT_EQ(available, 1);
|
||||||
|
|
||||||
|
GLuint64 result = 123u;
|
||||||
|
MG_Impl::GLImpl::GetQueryObjectui64v(id, GL_QUERY_RESULT, &result);
|
||||||
|
EXPECT_EQ(result, 0u);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::DeleteQueries(1, &id);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QueryTest, GetQueryivCurrentQueryTracksActiveId) {
|
||||||
|
GLint currentQuery = -1;
|
||||||
|
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_CURRENT_QUERY, ¤tQuery);
|
||||||
|
EXPECT_EQ(currentQuery, 0);
|
||||||
|
|
||||||
|
GLuint id = 0;
|
||||||
|
MG_Impl::GLImpl::GenQueries(1, &id);
|
||||||
|
ASSERT_NE(id, 0u);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, id);
|
||||||
|
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_CURRENT_QUERY, ¤tQuery);
|
||||||
|
EXPECT_EQ(currentQuery, static_cast<GLint>(id));
|
||||||
|
|
||||||
|
// GL_TIMESTAMP queries are never active, so GL_CURRENT_QUERY stays 0 there.
|
||||||
|
MG_Impl::GLImpl::GetQueryiv(GL_TIMESTAMP, GL_CURRENT_QUERY, ¤tQuery);
|
||||||
|
EXPECT_EQ(currentQuery, 0);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
|
||||||
|
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_CURRENT_QUERY, ¤tQuery);
|
||||||
|
EXPECT_EQ(currentQuery, 0);
|
||||||
|
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
MG_Impl::GLImpl::DeleteQueries(1, &id);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QueryTest, QueryCounterBitsReportsZeroWhenTimerQueryDisabled) {
|
||||||
|
const ScopedFeaturesOverride featuresGuard;
|
||||||
|
const ScopedBackendFunctionsOverride backendGuard;
|
||||||
|
InstallStubBackendTimerQueries();
|
||||||
|
|
||||||
|
// With the stub backend reporting live timer-query support and the
|
||||||
|
// feature enabled, the frontend advertises 64-bit counters for both timer
|
||||||
|
// targets.
|
||||||
|
MG_Config::Features.DisableTimerQuery = false;
|
||||||
|
GLint counterBits = -1;
|
||||||
|
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_QUERY_COUNTER_BITS, &counterBits);
|
||||||
|
EXPECT_EQ(counterBits, 64);
|
||||||
|
MG_Impl::GLImpl::GetQueryiv(GL_TIMESTAMP, GL_QUERY_COUNTER_BITS, &counterBits);
|
||||||
|
EXPECT_EQ(counterBits, 64);
|
||||||
|
|
||||||
|
// MOBILEGL_DISABLE_TIMERQUERY zeroes the advertised counter bits even when
|
||||||
|
// the backend supports timer queries.
|
||||||
|
MG_Config::Features.DisableTimerQuery = true;
|
||||||
|
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_QUERY_COUNTER_BITS, &counterBits);
|
||||||
|
EXPECT_EQ(counterBits, 0);
|
||||||
|
MG_Impl::GLImpl::GetQueryiv(GL_TIMESTAMP, GL_QUERY_COUNTER_BITS, &counterBits);
|
||||||
|
EXPECT_EQ(counterBits, 0);
|
||||||
|
|
||||||
|
// A disabled span must not touch the backend and must read back as an
|
||||||
|
// immediately available zero result.
|
||||||
|
GLuint id = 0;
|
||||||
|
MG_Impl::GLImpl::GenQueries(1, &id);
|
||||||
|
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, id);
|
||||||
|
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
|
||||||
|
EXPECT_EQ(g_stubBeginCount, 0);
|
||||||
|
|
||||||
|
GLint available = -1;
|
||||||
|
MG_Impl::GLImpl::GetQueryObjectiv(id, GL_QUERY_RESULT_AVAILABLE, &available);
|
||||||
|
EXPECT_EQ(available, 1);
|
||||||
|
GLuint64 result = 123u;
|
||||||
|
MG_Impl::GLImpl::GetQueryObjectui64v(id, GL_QUERY_RESULT, &result);
|
||||||
|
EXPECT_EQ(result, 0u);
|
||||||
|
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
MG_Impl::GLImpl::DeleteQueries(1, &id);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QueryTest, QueryCounterBitsReportsZeroWhenBackendUnsupported) {
|
||||||
|
const ScopedFeaturesOverride featuresGuard;
|
||||||
|
const ScopedBackendFunctionsOverride backendGuard;
|
||||||
|
InstallStubBackendTimerQueries();
|
||||||
|
MG_Config::Features.DisableTimerQuery = false;
|
||||||
|
|
||||||
|
// All backend hooks are installed and the feature is enabled, but the
|
||||||
|
// dynamic support check says the live backend cannot time right now
|
||||||
|
// (e.g. missing extension or zero timestamp valid bits) - the advertised
|
||||||
|
// counter bits must read 0 for both timer targets.
|
||||||
|
g_stubTimerQuerySupported = false;
|
||||||
|
GLint counterBits = -1;
|
||||||
|
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_QUERY_COUNTER_BITS, &counterBits);
|
||||||
|
EXPECT_EQ(counterBits, 0);
|
||||||
|
counterBits = -1;
|
||||||
|
MG_Impl::GLImpl::GetQueryiv(GL_TIMESTAMP, GL_QUERY_COUNTER_BITS, &counterBits);
|
||||||
|
EXPECT_EQ(counterBits, 0);
|
||||||
|
|
||||||
|
// Support coming back (fresh caps after a context recreation) flips the
|
||||||
|
// advertisement back to 64 without reinstalling the table.
|
||||||
|
g_stubTimerQuerySupported = true;
|
||||||
|
MG_Impl::GLImpl::GetQueryiv(GL_TIME_ELAPSED, GL_QUERY_COUNTER_BITS, &counterBits);
|
||||||
|
EXPECT_EQ(counterBits, 64);
|
||||||
|
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QueryTest, FailedResultWaitKeepsHandleUntilResultLands) {
|
||||||
|
const ScopedFeaturesOverride featuresGuard;
|
||||||
|
const ScopedBackendFunctionsOverride backendGuard;
|
||||||
|
InstallStubBackendTimerQueries();
|
||||||
|
MG_Config::Features.DisableTimerQuery = false;
|
||||||
|
g_stubResultObtainable = false;
|
||||||
|
g_stubResultNs = 777;
|
||||||
|
|
||||||
|
GLuint id = 0;
|
||||||
|
MG_Impl::GLImpl::GenQueries(1, &id);
|
||||||
|
ASSERT_NE(id, 0u);
|
||||||
|
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, id);
|
||||||
|
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
|
||||||
|
|
||||||
|
// A wait the backend cannot satisfy yet (e.g. Vulkan refusing to block on
|
||||||
|
// the current unsubmitted frame serial) reads 0 for this call - without
|
||||||
|
// recording an error - and must NOT consume the backend handle or cache
|
||||||
|
// the zero.
|
||||||
|
GLuint64 result = 123u;
|
||||||
|
MG_Impl::GLImpl::GetQueryObjectui64v(id, GL_QUERY_RESULT, &result);
|
||||||
|
EXPECT_EQ(result, 0u);
|
||||||
|
EXPECT_EQ(g_stubDeleteCount, 0);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
// Repeated failed waits behave identically...
|
||||||
|
result = 123u;
|
||||||
|
MG_Impl::GLImpl::GetQueryObjectui64v(id, GL_QUERY_RESULT, &result);
|
||||||
|
EXPECT_EQ(result, 0u);
|
||||||
|
EXPECT_EQ(g_stubDeleteCount, 0);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
// ...and availability still polls the backend afterwards (the failed
|
||||||
|
// read did not force-complete the query).
|
||||||
|
GLint available = -1;
|
||||||
|
g_stubResultAvailable = false;
|
||||||
|
MG_Impl::GLImpl::GetQueryObjectiv(id, GL_QUERY_RESULT_AVAILABLE, &available);
|
||||||
|
EXPECT_EQ(available, 0);
|
||||||
|
g_stubResultAvailable = true;
|
||||||
|
MG_Impl::GLImpl::GetQueryObjectiv(id, GL_QUERY_RESULT_AVAILABLE, &available);
|
||||||
|
EXPECT_EQ(available, 1);
|
||||||
|
|
||||||
|
// Once the backend can produce the value, the same query returns the
|
||||||
|
// real result; only then is the backend handle released and the value
|
||||||
|
// cached for later reads.
|
||||||
|
g_stubResultObtainable = true;
|
||||||
|
result = 0;
|
||||||
|
MG_Impl::GLImpl::GetQueryObjectui64v(id, GL_QUERY_RESULT, &result);
|
||||||
|
EXPECT_EQ(result, 777u);
|
||||||
|
EXPECT_EQ(g_stubDeleteCount, 1);
|
||||||
|
result = 0;
|
||||||
|
MG_Impl::GLImpl::GetQueryObjectui64v(id, GL_QUERY_RESULT, &result);
|
||||||
|
EXPECT_EQ(result, 777u);
|
||||||
|
EXPECT_EQ(g_stubDeleteCount, 1);
|
||||||
|
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
MG_Impl::GLImpl::DeleteQueries(1, &id);
|
||||||
|
EXPECT_EQ(g_stubDeleteCount, 1); // handle already released by the result read
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QueryTest, BackendResultsPropagateThroughFrontend) {
|
||||||
|
const ScopedFeaturesOverride featuresGuard;
|
||||||
|
const ScopedBackendFunctionsOverride backendGuard;
|
||||||
|
InstallStubBackendTimerQueries();
|
||||||
|
MG_Config::Features.DisableTimerQuery = false;
|
||||||
|
g_stubResultAvailable = false;
|
||||||
|
g_stubResultNs = 42;
|
||||||
|
|
||||||
|
GLuint id = 0;
|
||||||
|
MG_Impl::GLImpl::GenQueries(1, &id);
|
||||||
|
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, id);
|
||||||
|
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
|
||||||
|
EXPECT_EQ(g_stubBeginCount, 1);
|
||||||
|
EXPECT_EQ(g_stubEndCount, 1);
|
||||||
|
|
||||||
|
// Availability follows the backend while the result has not been read.
|
||||||
|
GLint available = -1;
|
||||||
|
MG_Impl::GLImpl::GetQueryObjectiv(id, GL_QUERY_RESULT_AVAILABLE, &available);
|
||||||
|
EXPECT_EQ(available, 0);
|
||||||
|
g_stubResultAvailable = true;
|
||||||
|
MG_Impl::GLImpl::GetQueryObjectiv(id, GL_QUERY_RESULT_AVAILABLE, &available);
|
||||||
|
EXPECT_EQ(available, 1);
|
||||||
|
|
||||||
|
// Reading the result consumes the backend handle exactly once and caches
|
||||||
|
// the value for later reads.
|
||||||
|
GLuint64 result = 0;
|
||||||
|
MG_Impl::GLImpl::GetQueryObjectui64v(id, GL_QUERY_RESULT, &result);
|
||||||
|
EXPECT_EQ(result, 42u);
|
||||||
|
EXPECT_EQ(g_stubDeleteCount, 1);
|
||||||
|
result = 0;
|
||||||
|
MG_Impl::GLImpl::GetQueryObjectui64v(id, GL_QUERY_RESULT, &result);
|
||||||
|
EXPECT_EQ(result, 42u);
|
||||||
|
EXPECT_EQ(g_stubDeleteCount, 1);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::DeleteQueries(1, &id);
|
||||||
|
EXPECT_EQ(g_stubDeleteCount, 1); // handle already released by the result read
|
||||||
|
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment-agnostic property test for the env -> ConfigLoader -> Features
|
||||||
|
// chain: whatever MOBILEGL_DISABLE_TIMERQUERY is set to in the environment of
|
||||||
|
// this test process, MG_ConfigLoader::Init must have parsed it with the
|
||||||
|
// unified truthy rule (set, non-empty, not "0", case-insensitive not "false").
|
||||||
|
// Running the binary under MOBILEGL_DISABLE_TIMERQUERY=1 therefore exercises
|
||||||
|
// the real end-to-end path rather than the struct field alone.
|
||||||
|
TEST_F(QueryTest, DisableTimerQueryFeatureMatchesEnvironment) {
|
||||||
|
const char* raw = std::getenv("MOBILEGL_DISABLE_TIMERQUERY");
|
||||||
|
Bool expected = false;
|
||||||
|
if (raw != nullptr && raw[0] != '\0') {
|
||||||
|
String value = raw;
|
||||||
|
String lowered = value;
|
||||||
|
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
|
||||||
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
expected = value != "0" && lowered != "false";
|
||||||
|
}
|
||||||
|
EXPECT_EQ(MG_Config::Features.DisableTimerQuery, expected);
|
||||||
|
}
|
||||||
@@ -8,21 +8,21 @@
|
|||||||
|
|
||||||
#include "Loader.h"
|
#include "Loader.h"
|
||||||
#include "MG_Util/Types.h"
|
#include "MG_Util/Types.h"
|
||||||
|
#include <Config.h>
|
||||||
#if defined(MOBILEGL_IOS)
|
#if defined(MOBILEGL_IOS)
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace MobileGL::MG_Util::BackendLoader {
|
namespace MobileGL::MG_Util::BackendLoader {
|
||||||
static Bool UseRetraceAngle() {
|
static Bool UseRetraceAngle() {
|
||||||
const char* value = std::getenv("MOBILEGL_RETRACE_USE_ANGLE");
|
return MG_Config::Features.RetraceUseAngle;
|
||||||
return value != nullptr && std::strcmp(value, "1") == 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Vector<String> AngleLibNames(const char* name) {
|
static Vector<String> AngleLibNames(const char* name) {
|
||||||
const char* angleDir = std::getenv("MOBILEGL_RETRACE_ANGLE_DIR");
|
const String& angleDir = MG_Config::Features.RetraceAngleDir;
|
||||||
if (angleDir != nullptr && angleDir[0] != '\0') {
|
if (!angleDir.empty()) {
|
||||||
String path = angleDir;
|
String path = angleDir;
|
||||||
if (!path.empty() && path.back() != '/') {
|
if (path.back() != '/') {
|
||||||
path += "/";
|
path += "/";
|
||||||
}
|
}
|
||||||
path += name;
|
path += name;
|
||||||
@@ -450,6 +450,8 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
INIT_GLES_FUNC(glBufferStorageEXT)
|
INIT_GLES_FUNC(glBufferStorageEXT)
|
||||||
INIT_GLES_FUNC(glGetQueryObjectivEXT)
|
INIT_GLES_FUNC(glGetQueryObjectivEXT)
|
||||||
INIT_GLES_FUNC(glGetQueryObjecti64vEXT)
|
INIT_GLES_FUNC(glGetQueryObjecti64vEXT)
|
||||||
|
INIT_GLES_FUNC(glQueryCounterEXT)
|
||||||
|
INIT_GLES_FUNC(glGetQueryObjectui64vEXT)
|
||||||
INIT_GLES_FUNC(glBindFragDataLocationEXT)
|
INIT_GLES_FUNC(glBindFragDataLocationEXT)
|
||||||
INIT_GLES_FUNC(glMapBufferOES)
|
INIT_GLES_FUNC(glMapBufferOES)
|
||||||
INIT_GLES_FUNC(glMultiDrawArraysIndirectEXT)
|
INIT_GLES_FUNC(glMultiDrawArraysIndirectEXT)
|
||||||
@@ -726,6 +728,9 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
if (std::strcmp(extension, "GL_EXT_base_instance") == 0) {
|
if (std::strcmp(extension, "GL_EXT_base_instance") == 0) {
|
||||||
caps.SupportsBaseInstance = true;
|
caps.SupportsBaseInstance = true;
|
||||||
}
|
}
|
||||||
|
if (std::strcmp(extension, "GL_EXT_disjoint_timer_query") == 0) {
|
||||||
|
caps.SupportsDisjointTimerQuery = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -911,6 +916,18 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
MGLOG_I(" Indirect draw gl_InstanceID includes baseInstance: %s",
|
MGLOG_I(" Indirect draw gl_InstanceID includes baseInstance: %s",
|
||||||
caps.IndirectDrawInstanceIdIncludesBaseInstance ? "true" : "false");
|
caps.IndirectDrawInstanceIdIncludesBaseInstance ? "true" : "false");
|
||||||
|
|
||||||
|
caps.IsAngleRenderer = caps.GLESRendererString.find("ANGLE") != String::npos;
|
||||||
|
caps.IsAngleLlvmpipeRenderer =
|
||||||
|
caps.IsAngleRenderer && caps.GLESRendererString.find("llvmpipe") != String::npos;
|
||||||
|
caps.AvoidSamplerMipmapMinFilter =
|
||||||
|
caps.IsAngleLlvmpipeRenderer && MG_Config::Features.AvoidAngleLlvmpipeSamplerMipmapMinFilter;
|
||||||
|
MGLOG_I(" GL_EXT_disjoint_timer_query supported: %s",
|
||||||
|
caps.SupportsDisjointTimerQuery ? "true" : "false");
|
||||||
|
MGLOG_I(" ANGLE renderer: %s", caps.IsAngleRenderer ? "true" : "false");
|
||||||
|
MGLOG_I(" ANGLE llvmpipe renderer: %s", caps.IsAngleLlvmpipeRenderer ? "true" : "false");
|
||||||
|
MGLOG_I(" Avoid sampler mipmap min filter: %s",
|
||||||
|
caps.AvoidSamplerMipmapMinFilter ? "true" : "false");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} // namespace MobileGL::MG_Util::BackendLoader
|
} // namespace MobileGL::MG_Util::BackendLoader
|
||||||
|
|||||||
@@ -611,6 +611,8 @@ namespace MobileGL {
|
|||||||
GLbitfield flags)
|
GLbitfield flags)
|
||||||
GL_FUNC_TYPEDEF(void, glGetQueryObjectivEXT, GLuint id, GLenum pname, GLint* params)
|
GL_FUNC_TYPEDEF(void, glGetQueryObjectivEXT, GLuint id, GLenum pname, GLint* params)
|
||||||
GL_FUNC_TYPEDEF(void, glGetQueryObjecti64vEXT, GLuint id, GLenum pname, GLint64* params)
|
GL_FUNC_TYPEDEF(void, glGetQueryObjecti64vEXT, GLuint id, GLenum pname, GLint64* params)
|
||||||
|
GL_FUNC_TYPEDEF(void, glQueryCounterEXT, GLuint id, GLenum target)
|
||||||
|
GL_FUNC_TYPEDEF(void, glGetQueryObjectui64vEXT, GLuint id, GLenum pname, GLuint64* params)
|
||||||
GL_FUNC_TYPEDEF(void, glBindFragDataLocationEXT, GLuint program, GLuint colorNumber, const GLchar* name)
|
GL_FUNC_TYPEDEF(void, glBindFragDataLocationEXT, GLuint program, GLuint colorNumber, const GLchar* name)
|
||||||
GL_FUNC_TYPEDEF(void*, glMapBufferOES, GLenum target, GLenum access)
|
GL_FUNC_TYPEDEF(void*, glMapBufferOES, GLenum target, GLenum access)
|
||||||
|
|
||||||
@@ -1000,6 +1002,8 @@ namespace MobileGL {
|
|||||||
GL_FUNC_DECL(glBufferStorageEXT)
|
GL_FUNC_DECL(glBufferStorageEXT)
|
||||||
GL_FUNC_DECL(glGetQueryObjectivEXT)
|
GL_FUNC_DECL(glGetQueryObjectivEXT)
|
||||||
GL_FUNC_DECL(glGetQueryObjecti64vEXT)
|
GL_FUNC_DECL(glGetQueryObjecti64vEXT)
|
||||||
|
GL_FUNC_DECL(glQueryCounterEXT)
|
||||||
|
GL_FUNC_DECL(glGetQueryObjectui64vEXT)
|
||||||
GL_FUNC_DECL(glBindFragDataLocationEXT)
|
GL_FUNC_DECL(glBindFragDataLocationEXT)
|
||||||
GL_FUNC_DECL(glMapBufferOES)
|
GL_FUNC_DECL(glMapBufferOES)
|
||||||
|
|
||||||
@@ -1019,6 +1023,16 @@ namespace MobileGL {
|
|||||||
Bool SupportsPersistentMapping = false;
|
Bool SupportsPersistentMapping = false;
|
||||||
Bool SupportsNorm16Texture = false;
|
Bool SupportsNorm16Texture = false;
|
||||||
Bool SupportsBaseInstance = false;
|
Bool SupportsBaseInstance = false;
|
||||||
|
// GL_EXT_disjoint_timer_query is present in the extension string.
|
||||||
|
Bool SupportsDisjointTimerQuery = false;
|
||||||
|
// GL_RENDERER contains "ANGLE".
|
||||||
|
Bool IsAngleRenderer = false;
|
||||||
|
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
|
||||||
|
Bool IsAngleLlvmpipeRenderer = false;
|
||||||
|
// IsAngleLlvmpipeRenderer combined with the
|
||||||
|
// MOBILEGL_ANGLE_LLVMPIPE_AVOID_SAMPLER_MIPMAP_MIN_FILTER feature toggle:
|
||||||
|
// sampler min filters should drop their mipmap component.
|
||||||
|
Bool AvoidSamplerMipmapMinFilter = false;
|
||||||
// True when indirect draws leak the command's baseInstance word ("reserved,
|
// True when indirect draws leak the command's baseInstance word ("reserved,
|
||||||
// must be zero" in unextended ES) into gl_InstanceID. Conforming ES drivers
|
// must be zero" in unextended ES) into gl_InstanceID. Conforming ES drivers
|
||||||
// keep gl_InstanceID zero-based; ANGLE's Vulkan backend hands the command
|
// keep gl_InstanceID zero-based; ANGLE's Vulkan backend hands the command
|
||||||
|
|||||||
Reference in New Issue
Block a user