mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +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:
@@ -15,12 +15,35 @@
|
||||
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
#include "MG_Util/Miscellany/IndexGenerator.h"
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <spirv_reflect.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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 {
|
||||
struct BufferVariableResource {
|
||||
String name;
|
||||
@@ -1349,6 +1372,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// horizon used to recycle buffer resources).
|
||||
struct VulkanSyncObject {
|
||||
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
|
||||
|
||||
@@ -1356,7 +1385,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!pVulkanRenderer) {
|
||||
return nullptr;
|
||||
}
|
||||
return new VulkanSyncObject{pVulkanRenderer->GetCurrentFrameSerial()};
|
||||
return new VulkanSyncObject{pVulkanRenderer->GetCurrentFrameSerial(), GetRendererGeneration()};
|
||||
}
|
||||
|
||||
GLenum ClientWaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) {
|
||||
@@ -1365,7 +1394,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// waiting can succeed at all.
|
||||
(void)flags;
|
||||
const auto* sync = static_cast<VulkanSyncObject*>(handle);
|
||||
if (sync == nullptr || !pVulkanRenderer) {
|
||||
if (sync == nullptr || !pVulkanRenderer || sync->rendererGeneration != GetRendererGeneration()) {
|
||||
return GL_ALREADY_SIGNALED;
|
||||
}
|
||||
if (pVulkanRenderer->IsFrameSerialComplete(sync->frameSerial)) {
|
||||
@@ -1393,12 +1422,143 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
Bool GetSyncStatus(BackendSyncHandle handle) {
|
||||
const auto* sync = static_cast<VulkanSyncObject*>(handle);
|
||||
if (sync == nullptr || !pVulkanRenderer) {
|
||||
if (sync == nullptr || !pVulkanRenderer || sync->rendererGeneration != GetRendererGeneration()) {
|
||||
return true;
|
||||
}
|
||||
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() {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Present called with null VulkanRenderer");
|
||||
pVulkanRenderer->Present();
|
||||
|
||||
Reference in New Issue
Block a user