mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 13:48:30 +09:00
Compare commits
6
Commits
d39a706d57
...
62dea3bea4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62dea3bea4 | ||
|
|
57aeeec053 | ||
|
|
9c0144d24a | ||
|
|
1e45958e01 | ||
|
|
6e6f5268fb | ||
|
|
08f98ad9ce |
@@ -206,6 +206,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
|
||||
|
||||
MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp
|
||||
MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp
|
||||
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
|
||||
|
||||
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
|
||||
@@ -303,6 +304,7 @@ endif()
|
||||
if (ANDROID)
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Util/SelfTest/DriverPostJni.cpp
|
||||
MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -1518,7 +1518,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
backendVAOIt->second->Bind();
|
||||
}
|
||||
} else {
|
||||
g_GLESFuncs.glBindVertexArray(0);
|
||||
VertexArrayImpl::BindBackendVAOId(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3001,7 +3001,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const Float uvTransform[4] = {mirrorX ? -uvScaleX : uvScaleX, mirrorY ? -uvScaleY : uvScaleY,
|
||||
mirrorX ? uvScaleX : 0.0f, mirrorY ? uvScaleY : 0.0f};
|
||||
|
||||
g_GLESFuncs.glBindVertexArray(s_vertexArray);
|
||||
VertexArrayImpl::BindBackendVAOId(s_vertexArray);
|
||||
g_GLESFuncs.glActiveTexture(GL_TEXTURE0);
|
||||
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, s_texture);
|
||||
g_GLESFuncs.glViewport(dstLeft, dstBottom, dstWidth, dstHeight);
|
||||
@@ -3060,7 +3060,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glUseProgram(static_cast<GLuint>(previousProgram));
|
||||
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previousTexture));
|
||||
g_GLESFuncs.glActiveTexture(static_cast<GLenum>(previousActiveTexture));
|
||||
g_GLESFuncs.glBindVertexArray(static_cast<GLuint>(previousVertexArray));
|
||||
VertexArrayImpl::BindBackendVAOId(static_cast<GLuint>(previousVertexArray));
|
||||
g_GLESFuncs.glViewport(previousViewport[0], previousViewport[1], previousViewport[2], previousViewport[3]);
|
||||
g_GLESFuncs.glScissor(previousScissorBox[0], previousScissorBox[1], previousScissorBox[2],
|
||||
previousScissorBox[3]);
|
||||
@@ -5678,19 +5678,44 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
configs.resize(static_cast<SizeT>(numConfigs));
|
||||
|
||||
if (surfaceBit == EGL_WINDOW_BIT) {
|
||||
// X11 drivers can reserve every alpha-8 config for 32-bit ARGB
|
||||
// visuals (NVIDIA), so a default-visual (depth 24) window only
|
||||
// matches an alpha-0 config; keep those as a second candidate tier
|
||||
// for the visual match or eglCreateWindowSurface hits BAD_CONFIG.
|
||||
Vector<EGLConfig> alphaFreeConfigs;
|
||||
const EGLint alphaFreeAttribs[] = {EGL_SURFACE_TYPE, surfaceBit, EGL_RENDERABLE_TYPE,
|
||||
EGL_OPENGL_ES3_BIT,
|
||||
EGL_RED_SIZE, 8, EGL_GREEN_SIZE,
|
||||
8,
|
||||
EGL_BLUE_SIZE, 8, EGL_DEPTH_SIZE,
|
||||
24,
|
||||
EGL_STENCIL_SIZE, 8, EGL_NONE};
|
||||
EGLint numAlphaFree = 0;
|
||||
if (g_EGLFuncs.eglChooseConfig(g_Display, alphaFreeAttribs, nullptr, 0, &numAlphaFree) &&
|
||||
numAlphaFree > 0) {
|
||||
alphaFreeConfigs.resize(static_cast<SizeT>(numAlphaFree));
|
||||
if (!g_EGLFuncs.eglChooseConfig(g_Display, alphaFreeAttribs, alphaFreeConfigs.data(),
|
||||
numAlphaFree, &numAlphaFree)) {
|
||||
numAlphaFree = 0;
|
||||
}
|
||||
alphaFreeConfigs.resize(static_cast<SizeT>(numAlphaFree));
|
||||
}
|
||||
|
||||
const EGLint windowVisualId = QueryX11WindowVisualId(window);
|
||||
const EGLint visualIds[] = {windowVisualId, QueryDefaultX11VisualId()};
|
||||
for (const auto visualId : visualIds) {
|
||||
if (visualId == 0) {
|
||||
continue;
|
||||
}
|
||||
for (const auto config : configs) {
|
||||
EGLint nativeVisualId = 0;
|
||||
if (ConfigSupports(config, surfaceBit) &&
|
||||
GetConfigAttrib(config, EGL_NATIVE_VISUAL_ID, nativeVisualId) &&
|
||||
nativeVisualId == visualId) {
|
||||
outConfig = config;
|
||||
return true;
|
||||
for (const auto* candidates : {&configs, &alphaFreeConfigs}) {
|
||||
for (const auto config : *candidates) {
|
||||
EGLint nativeVisualId = 0;
|
||||
if (ConfigSupports(config, surfaceBit) &&
|
||||
GetConfigAttrib(config, EGL_NATIVE_VISUAL_ID, nativeVisualId) &&
|
||||
nativeVisualId == visualId) {
|
||||
outConfig = config;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5894,6 +5919,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// EGL ground-truth verification stamp, per thread. glvnd's
|
||||
// eglGetCurrentContext performs fork detection with a real getpid()
|
||||
// syscall on every call, and this predicate sits 2-3 deep in every
|
||||
// draw - measured at 16% of the render thread on a live workload.
|
||||
thread_local Uint64 t_eglVerifiedFrameSerial = ~0ull;
|
||||
thread_local Uint t_eglVerifiedContextGeneration = 0;
|
||||
} // namespace
|
||||
|
||||
Bool IsBackendContextCurrentOnThisThread() {
|
||||
if (g_Context == EGL_NO_CONTEXT) {
|
||||
return false;
|
||||
@@ -5904,10 +5938,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Belt and braces: EGL itself is the ground truth. A migration that bypassed
|
||||
// MakeCurrent()/ReleaseCurrent() must not leave a stale ownership claim
|
||||
// standing, or GL calls would silently no-op while shadow bookkeeping (bind
|
||||
// cache, synced serials) still advances.
|
||||
// cache, synced serials) still advances. Re-verify once per (thread, frame,
|
||||
// context generation) rather than per call: an external migration is caught
|
||||
// at the next frame boundary instead of the next call, which recovers the
|
||||
// bookkeeping just the same, without paying a syscall on every draw.
|
||||
const Uint64 frameSerial = g_currentFrameSerial.load(std::memory_order_relaxed);
|
||||
if (t_eglVerifiedFrameSerial == frameSerial &&
|
||||
t_eglVerifiedContextGeneration == g_syncContextGeneration) {
|
||||
return true;
|
||||
}
|
||||
if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() != g_Context) {
|
||||
return false;
|
||||
}
|
||||
t_eglVerifiedFrameSerial = frameSerial;
|
||||
t_eglVerifiedContextGeneration = g_syncContextGeneration;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -6202,6 +6246,33 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint64 CurrentFrameSerial() { return g_currentFrameSerial.load(std::memory_order_relaxed); }
|
||||
Uint64 CompletedFrameSerial() { return g_completedFrameSerial.load(std::memory_order_relaxed); }
|
||||
|
||||
Bool WaitForFrameSerialCompleted(Uint64 serial, Uint64 timeoutNs) {
|
||||
if (CompletedFrameSerial() >= serial) return true;
|
||||
if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glClientWaitSync) return false;
|
||||
// Fences signal in submission order, so the live fence with the SMALLEST
|
||||
// serial at or past the target is the earliest event that proves the
|
||||
// target frame retired. A recycled slot (GPU more than ring-depth frames
|
||||
// behind) leaves no usable fence; report failure and let the caller pick
|
||||
// its own fallback rather than draining the whole queue here.
|
||||
FrameFence* best = nullptr;
|
||||
for (FrameFence& slot : g_frameFenceRing) {
|
||||
if (!slot.sync || slot.contextGeneration != g_syncContextGeneration) continue;
|
||||
if (slot.serial < serial) continue;
|
||||
if (!best || slot.serial < best->serial) best = &slot;
|
||||
}
|
||||
if (!best) return false;
|
||||
const GLenum status =
|
||||
g_GLESFuncs.glClientWaitSync(best->sync, GL_SYNC_FLUSH_COMMANDS_BIT, timeoutNs);
|
||||
if (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED) return false;
|
||||
Uint64 completed = g_completedFrameSerial.load(std::memory_order_relaxed);
|
||||
if (best->serial > completed) {
|
||||
g_completedFrameSerial.store(best->serial, std::memory_order_relaxed);
|
||||
}
|
||||
if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(best->sync);
|
||||
best->sync = nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Present() {
|
||||
// Insert one fence per frame BEFORE the swap (eglSwapBuffers' implicit flush
|
||||
// makes it reachable), then non-blocking-poll prior frames' fences AFTER to
|
||||
@@ -6247,6 +6318,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
XfbImpl::OnBackendContextDestroyed();
|
||||
ScratchFBOImpl::OnBackendContextDestroyed();
|
||||
FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||
VertexArrayImpl::InvalidateVAOBindingCache();
|
||||
PixelStoreImpl::InvalidatePackStateCache();
|
||||
// Texture ids belong to the dying context; wrappers destroyed later must
|
||||
// not glDeleteTextures a recycled name in a successor context.
|
||||
|
||||
@@ -160,6 +160,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// A buffer retired during frame N is safe to recycle once CompletedFrameSerial() >= N.
|
||||
Uint64 CurrentFrameSerial();
|
||||
Uint64 CompletedFrameSerial();
|
||||
// Block (up to timeoutNs) until the given frame serial provably retired on the
|
||||
// GPU, using the per-frame fence ring. False when no usable fence covers the
|
||||
// serial (fence-less context, foreign thread, or the slot was recycled);
|
||||
// completion state is untouched in that case.
|
||||
Bool WaitForFrameSerialCompleted(Uint64 serial, Uint64 timeoutNs);
|
||||
// Applies (or defers until the window surface exists) the app-requested
|
||||
// eglSwapInterval on the native EGL surface.
|
||||
void SetSwapInterval(Int interval);
|
||||
|
||||
@@ -292,6 +292,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// sync point with a current ES context.
|
||||
Vector<SharedPtr<BackendBufferResource>> g_deferredBufferReleases;
|
||||
std::mutex g_deferredBufferReleasesMutex;
|
||||
// Cheap emptiness probe so the per-draw drain can skip the mutex and
|
||||
// context check when nothing was enqueued (the overwhelmingly common
|
||||
// case). Written only under the mutex; read lock-free.
|
||||
std::atomic<Bool> g_hasDeferredBufferReleases{false};
|
||||
|
||||
// --- Buffer-storage pool (Mesa-style BO recycle) -------------------------
|
||||
// Recycle idle GL buffer ids of an EXACT byte size instead of glDeleteBuffers
|
||||
@@ -457,8 +461,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const SizeT size = bufferObject.GetSize();
|
||||
const GLenum usage = MG_Util::ConvertBufferUsageToGLEnum(bufferObject.GetUsage());
|
||||
BindBufferId(TempBufferTarget, resource.id);
|
||||
g_GLESFuncs.glBufferData(TempBufferTarget, (GLsizeiptr)size,
|
||||
size > 0 ? bufferObject.MappedData() : nullptr, usage);
|
||||
// An orphaning respecify (glBufferData with NULL, content never
|
||||
// written since) stays a pure NULL reallocation: the driver renames
|
||||
// the store without a stall and nothing is transferred. Uploading
|
||||
// the stale shadow here turned Minecraft-style orphaning into a
|
||||
// full-size synchronized upload.
|
||||
const void* initialData =
|
||||
(size > 0 && bufferObject.HasDefinedContent()) ? bufferObject.MappedData() : nullptr;
|
||||
g_GLESFuncs.glBufferData(TempBufferTarget, (GLsizeiptr)size, initialData, usage);
|
||||
resource.storageSize = size;
|
||||
resource.storageInitialized = true;
|
||||
resource.pendingRespecify = false;
|
||||
@@ -680,6 +690,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
const std::lock_guard<std::mutex> lock(g_deferredBufferReleasesMutex);
|
||||
g_deferredBufferReleases.push_back(std::move(resource));
|
||||
g_hasDeferredBufferReleases.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
const BufferBackendOps g_glesBufferBackendOps = {
|
||||
@@ -706,6 +717,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const std::lock_guard<std::mutex> lock(g_deferredBufferReleasesMutex);
|
||||
// The ES context owning these ids is going away; just drop the handles.
|
||||
g_deferredBufferReleases.clear();
|
||||
g_hasDeferredBufferReleases.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
void OnBackendContextDestroyed() {
|
||||
@@ -720,11 +732,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
void ProcessDeferredBufferReleases() {
|
||||
// Runs on every draw; skip the context check, mutex and vector churn
|
||||
// outright when nothing was enqueued since the last drain.
|
||||
if (!g_hasDeferredBufferReleases.load(std::memory_order_acquire)) return;
|
||||
if (!CanTouchGLNow()) return;
|
||||
Vector<SharedPtr<BackendBufferResource>> releases;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(g_deferredBufferReleasesMutex);
|
||||
releases.swap(g_deferredBufferReleases);
|
||||
g_hasDeferredBufferReleases.store(false, std::memory_order_release);
|
||||
}
|
||||
for (auto& resource : releases) {
|
||||
auto* glesResource = static_cast<GLESBufferResource*>(resource.get());
|
||||
@@ -1115,8 +1131,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
} else if (g_uboRing.creationFailed) {
|
||||
return false; // store lost; callers fall back to glBufferSubData
|
||||
} else {
|
||||
// At the size cap (>kUboRingMaxBytes of uniforms in flight — not a
|
||||
// real workload): drain the GPU once rather than corrupt live slots.
|
||||
// At the size cap (>kUboRingMaxBytes of uniforms in flight). First
|
||||
// try to free room by waiting for the OLDEST in-flight frames to
|
||||
// retire - a bounded wait that ends as soon as enough tail space
|
||||
// exists, instead of draining the entire queue.
|
||||
constexpr Uint64 kFrameWaitNs = 50ull * 1000 * 1000; // 50ms per frame
|
||||
while (!g_uboRingFrameMarks.empty() &&
|
||||
g_uboRing.head + alignedSize - g_uboRing.tail > g_uboRing.size) {
|
||||
const auto& oldest = g_uboRingFrameMarks.front();
|
||||
if (!DirectGLES::WaitForFrameSerialCompleted(oldest.frameSerial, kFrameWaitNs)) {
|
||||
break;
|
||||
}
|
||||
if (oldest.headAtPresent > g_uboRing.tail) g_uboRing.tail = oldest.headAtPresent;
|
||||
g_uboRingFrameMarks.erase(g_uboRingFrameMarks.begin());
|
||||
}
|
||||
if (g_uboRing.head + alignedSize - g_uboRing.tail <= g_uboRing.size) {
|
||||
offset = static_cast<SizeT>(g_uboRing.head % g_uboRing.size);
|
||||
if (offset + alignedSize > g_uboRing.size) {
|
||||
g_uboRing.head += g_uboRing.size - offset;
|
||||
offset = 0;
|
||||
}
|
||||
g_uboRing.head += alignedSize;
|
||||
outOffset = offset;
|
||||
return true;
|
||||
}
|
||||
// No usable fence covers the oldest frames: drain once rather than
|
||||
// corrupt live slots.
|
||||
if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish();
|
||||
g_uboRing.tail = g_uboRing.head;
|
||||
g_uboRingFrameMarks.clear();
|
||||
@@ -1234,6 +1274,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
BackendVertexArrayObject::~BackendVertexArrayObject() {
|
||||
if (m_backendVAOId != 0) {
|
||||
NoteVAOIdDeleted(m_backendVAOId);
|
||||
g_GLESFuncs.glDeleteVertexArrays(1, &m_backendVAOId);
|
||||
m_backendVAOId = 0;
|
||||
}
|
||||
@@ -1246,11 +1287,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
Uint g_boundBackendVAOId = 0;
|
||||
Bool g_boundBackendVAOKnown = false;
|
||||
} // namespace
|
||||
|
||||
void BindBackendVAOId(Uint id) {
|
||||
if (g_boundBackendVAOKnown && g_boundBackendVAOId == id) {
|
||||
return;
|
||||
}
|
||||
g_GLESFuncs.glBindVertexArray(id);
|
||||
g_boundBackendVAOId = id;
|
||||
g_boundBackendVAOKnown = true;
|
||||
}
|
||||
|
||||
void InvalidateVAOBindingCache() {
|
||||
g_boundBackendVAOKnown = false;
|
||||
}
|
||||
|
||||
void NoteVAOIdDeleted(Uint id) {
|
||||
if (g_boundBackendVAOKnown && g_boundBackendVAOId == id) {
|
||||
g_boundBackendVAOId = 0; // glDeleteVertexArrays reverts a bound VAO to 0
|
||||
}
|
||||
}
|
||||
|
||||
void BackendVertexArrayObject::Bind() const {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
g_GLESFuncs.glBindVertexArray(m_backendVAOId);
|
||||
BindBackendVAOId(m_backendVAOId);
|
||||
}
|
||||
|
||||
inline Bool BindAttributeBuffer(const MG_State::GLState::VertexAttribute& attrib) {
|
||||
@@ -1792,7 +1857,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// textures thrash, forcing a real glBindTexture per texture per draw. When
|
||||
// nothing needs uploading, skip the bind + upload machinery entirely;
|
||||
// BindCurrentTextures() re-establishes the real sampling bindings regardless.
|
||||
if (m_isInitialized && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) {
|
||||
// The content-version stamp short-circuits before any shape probing: it
|
||||
// bumps on every CPU-side pixel mutation, so an unchanged stamp plus an
|
||||
// unchanged shape means no level can be dirty. Shape stays a separate
|
||||
// compare because a NULL-data glTexImage changes it without touching the
|
||||
// content version.
|
||||
if (m_isInitialized && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap &&
|
||||
m_syncedContentVersion != 0 &&
|
||||
m_syncedContentVersion == stateTextureObject->GetContentVersion()) {
|
||||
auto* mipmapObject =
|
||||
static_cast<MG_State::GLState::TextureObjectMipmap*>(stateTextureObject.get());
|
||||
const auto probeBaseSize = stateTextureObject->GetBaseSize();
|
||||
@@ -1804,25 +1876,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
0,
|
||||
stateTextureObject->GetSamples(),
|
||||
stateTextureObject->HasFixedSampleLocations()};
|
||||
// Equal info => needsRegeneration is false, and canAppendMipmaps is
|
||||
// false too (it requires strictly more mip levels than the last sync).
|
||||
// So the only remaining work would be re-uploading dirty levels.
|
||||
if (probe == m_prevTextureInfo) {
|
||||
Bool anyDirty = false;
|
||||
for (const auto& uploadTarget : mipmapObject->GetUploadTargets()) {
|
||||
for (SizeT level = 0; level < probe.mipmapLevels; ++level) {
|
||||
if (mipmapObject->IsStorageDirty(uploadTarget, level)) {
|
||||
anyDirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (anyDirty) break;
|
||||
}
|
||||
if (!anyDirty) {
|
||||
MGLOG_D("Texture ID %u already fully synced, skipping scratch bind + upload.",
|
||||
m_backendTextureId);
|
||||
return;
|
||||
}
|
||||
MGLOG_D("Texture ID %u already fully synced, skipping scratch bind + upload.",
|
||||
m_backendTextureId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2199,24 +2256,75 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
uploadData, byteSize, &glType, packedUploadData);
|
||||
const IntVec3 uploadSize =
|
||||
GetBackendUploadSize(stateTextureObject->GetTarget(), texelSize);
|
||||
// Sub-rect upload: when only a region of the level changed (a
|
||||
// 16x16 sprite in a 1024x512 atlas, the per-frame lightmap) and
|
||||
// the shadow bytes go to the driver unconverted, upload just that
|
||||
// region with UNPACK_ROW_LENGTH striding into the level shadow.
|
||||
// Conversion fallbacks rewrite the whole level into a fresh
|
||||
// buffer, so they stay on the full-level path, as do targets
|
||||
// whose backend upload size differs from the shadow's texel size.
|
||||
const auto dirtyRegion = textureMipmapObject->GetStorageDirtyRegion(uploadTarget, level);
|
||||
const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
|
||||
static_cast<SizeT>(texelSize.y()) *
|
||||
static_cast<SizeT>(std::max(texelSize.z(), 1));
|
||||
const Bool subRectEligible =
|
||||
uploadData == mipData && !dirtyRegion.Empty() &&
|
||||
!dirtyRegion.CoversWholeLevel(texelSize) && texelCount > 0 &&
|
||||
byteSize % texelCount == 0 && uploadSize.x() == texelSize.x() &&
|
||||
uploadSize.y() == texelSize.y() &&
|
||||
std::max(uploadSize.z(), 1) == std::max(texelSize.z(), 1);
|
||||
const SizeT bpp = subRectEligible ? byteSize / texelCount : 0;
|
||||
const IntVec3 regionSize = {dirtyRegion.hi.x() - dirtyRegion.lo.x(),
|
||||
dirtyRegion.hi.y() - dirtyRegion.lo.y(),
|
||||
dirtyRegion.hi.z() - dirtyRegion.lo.z()};
|
||||
const SizeT levelRowBytes = static_cast<SizeT>(texelSize.x()) * bpp;
|
||||
const SizeT levelSliceBytes = static_cast<SizeT>(texelSize.y()) * levelRowBytes;
|
||||
const Uint8* regionPtr =
|
||||
static_cast<const Uint8*>(uploadData) +
|
||||
static_cast<SizeT>(dirtyRegion.lo.z()) * levelSliceBytes +
|
||||
static_cast<SizeT>(dirtyRegion.lo.y()) * levelRowBytes +
|
||||
static_cast<SizeT>(dirtyRegion.lo.x()) * bpp;
|
||||
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
|
||||
case TextureTarget::Texture2D:
|
||||
case TextureTarget::TextureCubeMap:
|
||||
g_GLESFuncs.glTexSubImage2D(glUploadTarget, static_cast<GLint>(level), 0, 0,
|
||||
static_cast<GLsizei>(uploadSize.x()),
|
||||
static_cast<GLsizei>(uploadSize.y()), glFormat, glType,
|
||||
uploadData);
|
||||
if (subRectEligible) {
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x());
|
||||
g_GLESFuncs.glTexSubImage2D(
|
||||
glUploadTarget, static_cast<GLint>(level), dirtyRegion.lo.x(),
|
||||
dirtyRegion.lo.y(), static_cast<GLsizei>(regionSize.x()),
|
||||
static_cast<GLsizei>(regionSize.y()), glFormat, glType, regionPtr);
|
||||
// The surrounding ScopedDefaultUnpackState shadow says 0.
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
} else {
|
||||
g_GLESFuncs.glTexSubImage2D(glUploadTarget, static_cast<GLint>(level), 0, 0,
|
||||
static_cast<GLsizei>(uploadSize.x()),
|
||||
static_cast<GLsizei>(uploadSize.y()), glFormat,
|
||||
glType, uploadData);
|
||||
}
|
||||
break;
|
||||
case TextureTarget::Texture3D:
|
||||
case TextureTarget::Texture2DArray:
|
||||
// ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly
|
||||
// like a 2D array whose depth is 6 * the cube count.
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
g_GLESFuncs.glTexSubImage3D(glUploadTarget, static_cast<GLint>(level), 0, 0, 0,
|
||||
static_cast<GLsizei>(uploadSize.x()),
|
||||
static_cast<GLsizei>(uploadSize.y()),
|
||||
static_cast<GLsizei>(uploadSize.z()), glFormat, glType,
|
||||
uploadData);
|
||||
if (subRectEligible) {
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x());
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, texelSize.y());
|
||||
g_GLESFuncs.glTexSubImage3D(
|
||||
glUploadTarget, static_cast<GLint>(level), dirtyRegion.lo.x(),
|
||||
dirtyRegion.lo.y(), dirtyRegion.lo.z(),
|
||||
static_cast<GLsizei>(regionSize.x()),
|
||||
static_cast<GLsizei>(regionSize.y()),
|
||||
static_cast<GLsizei>(regionSize.z()), glFormat, glType, regionPtr);
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
|
||||
} else {
|
||||
g_GLESFuncs.glTexSubImage3D(glUploadTarget, static_cast<GLint>(level), 0, 0, 0,
|
||||
static_cast<GLsizei>(uploadSize.x()),
|
||||
static_cast<GLsizei>(uploadSize.y()),
|
||||
static_cast<GLsizei>(uploadSize.z()), glFormat,
|
||||
glType, uploadData);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
MGLOG_E("Unhandled texture target %s",
|
||||
@@ -2300,6 +2408,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
});
|
||||
|
||||
m_prevTextureInfo = currentTextureInfo;
|
||||
// Everything dirty at entry is uploaded (or provably has no bytes to
|
||||
// upload); stamp the version so per-draw re-syncs short-circuit until
|
||||
// the next CPU-side mutation.
|
||||
m_syncedContentVersion = stateTextureObject->GetContentVersion();
|
||||
}
|
||||
|
||||
void BackendTextureObject::SyncBuiltinSamplerToBackend(
|
||||
|
||||
@@ -266,6 +266,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
|
||||
g_backendVertexArrayObjects;
|
||||
|
||||
// Shadowed glBindVertexArray: every backend VAO bind goes through here so a
|
||||
// draw's second bind of the same VAO (SyncToBackend, then PrepareForDraw's
|
||||
// re-bind) reaches the driver once. Invalidate whenever the ES context is
|
||||
// replaced - ids restart and the resting binding is 0 again.
|
||||
void BindBackendVAOId(Uint id);
|
||||
void InvalidateVAOBindingCache();
|
||||
// ES resets the binding to 0 when the currently bound VAO is deleted.
|
||||
void NoteVAOIdDeleted(Uint id);
|
||||
} // namespace VertexArrayImpl
|
||||
|
||||
namespace TextureImpl {
|
||||
@@ -375,6 +384,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool m_imageBindableStorageRequired = false;
|
||||
Bool m_backendStorageImmutable = false;
|
||||
StateTextureBasicInfo m_prevTextureInfo;
|
||||
// Frontend content version at the last completed mipmap sync. The per-draw
|
||||
// clean probe compares this before rebuilding shape info and scanning
|
||||
// per-level dirty flags; 0 never matches a real version (they start at 1).
|
||||
Uint64 m_syncedContentVersion = 0;
|
||||
SamplerParameters m_cacheSamplerParameters;
|
||||
UintVec2 m_cacheLodRange = {0, 1000};
|
||||
FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
|
||||
@@ -202,9 +202,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
for (auto& cacheEntryPair : frame.descriptorSetCacheByLayout) {
|
||||
cacheEntryPair.second.cursor = 0;
|
||||
}
|
||||
// The frame's descriptor sets are recycled above, so last frame's reuse target
|
||||
// is gone: start the per-draw descriptor-reuse cache fresh this frame.
|
||||
m_hasLastDescriptor = false;
|
||||
// The frame's descriptor sets are recycled above, so last frame's reuse targets
|
||||
// are gone: start the per-draw descriptor-reuse cache fresh this frame.
|
||||
for (auto& entry : m_descriptorReuseMemo) {
|
||||
entry.valid = false;
|
||||
}
|
||||
m_lastBindValid = false;
|
||||
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
|
||||
// reuse cannot outlive a single frame (see SamplerResolveMemo).
|
||||
@@ -241,8 +243,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
if (purgedSets > 0) {
|
||||
// The per-draw reuse memo folds the layout handle into its signature; drop
|
||||
// it so a recycled handle value cannot revive a purged set mid-frame.
|
||||
m_hasLastDescriptor = false;
|
||||
// every entry so a recycled handle value cannot revive a purged set mid-frame.
|
||||
for (auto& entry : m_descriptorReuseMemo) {
|
||||
entry.valid = false;
|
||||
}
|
||||
MGLOG_D("UniformDescriptorBinder: freed %zu descriptor sets for destroyed layout", purgedSets);
|
||||
}
|
||||
}
|
||||
@@ -1358,13 +1362,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
// Reuse the previous draw's descriptor set when the resolved content is
|
||||
// Reuse a recent draw's descriptor set when the resolved content is
|
||||
// byte-identical (only the bind-time dynamic offsets differ). The signature
|
||||
// covers the descriptor-set layout + every write's binding/type/count + the
|
||||
// pointed-to buffer/image/texel-buffer infos (all value-initialized, so no
|
||||
// padding noise). Correctness: bindings are re-resolved every draw, so the
|
||||
// signature always reflects the current state and reuse happens only on an
|
||||
// exact match; the reused set is never re-acquired within a frame (the acquire
|
||||
// exact match; a reused set is never re-acquired within a frame (the acquire
|
||||
// cursor only advances), so its written contents survive; the layout is part of
|
||||
// the signature so reuse never crosses programs. Sampler overrides (blits)
|
||||
// bypass and invalidate the cache.
|
||||
@@ -1396,8 +1400,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
mixWords(texelBufferViews.data(), texelBufferViews.size() * sizeof(VkBufferView));
|
||||
}
|
||||
|
||||
if (cacheable && m_hasLastDescriptor && signature == m_lastDescriptorSignature) {
|
||||
descriptorSet = m_lastBoundDescriptorSet;
|
||||
VkDescriptorSet reusedSet = VK_NULL_HANDLE;
|
||||
if (cacheable) {
|
||||
for (const auto& entry : m_descriptorReuseMemo) {
|
||||
if (entry.valid && entry.signature == signature) {
|
||||
reusedSet = entry.set;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reusedSet != VK_NULL_HANDLE) {
|
||||
descriptorSet = reusedSet;
|
||||
} else {
|
||||
VkResult allocResult = AcquireDescriptorSet(frameIndex, programObj, descriptorSet);
|
||||
if (allocResult != VK_SUCCESS || descriptorSet == VK_NULL_HANDLE) {
|
||||
@@ -1411,9 +1424,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!writes.empty()) {
|
||||
vkUpdateDescriptorSets(m_device, static_cast<Uint32>(writes.size()), writes.data(), 0, nullptr);
|
||||
}
|
||||
m_lastBoundDescriptorSet = descriptorSet;
|
||||
m_lastDescriptorSignature = signature;
|
||||
m_hasLastDescriptor = cacheable;
|
||||
if (cacheable) {
|
||||
m_descriptorReuseMemo[m_descriptorReuseMemoNext] =
|
||||
DescriptorReuseEntry{signature, descriptorSet, true};
|
||||
m_descriptorReuseMemoNext = (m_descriptorReuseMemoNext + 1) % kDescriptorReuseMemoSize;
|
||||
} else {
|
||||
for (auto& entry : m_descriptorReuseMemo) {
|
||||
entry.valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip the driver call when this exact binding is already live on the
|
||||
|
||||
@@ -179,14 +179,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<VkBufferView> m_texelBufferViewsScratch;
|
||||
Vector<Uint32> m_dynamicOffsetsScratch;
|
||||
|
||||
// Descriptor-set reuse across consecutive draws (see BindProgramUniformBuffers).
|
||||
// When a draw's resolved descriptor content is byte-identical to the previous
|
||||
// draw's, reuse the same VkDescriptorSet and skip AcquireDescriptorSet +
|
||||
// vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Reset each
|
||||
// frame in BeginFrame because the frame's descriptor sets are recycled there.
|
||||
VkDescriptorSet m_lastBoundDescriptorSet = VK_NULL_HANDLE;
|
||||
Uint64 m_lastDescriptorSignature = 0;
|
||||
Bool m_hasLastDescriptor = false;
|
||||
// Descriptor-set reuse across recent draws (see BindProgramUniformBuffers).
|
||||
// When a draw's resolved descriptor content is byte-identical to one memoized
|
||||
// earlier, reuse that VkDescriptorSet and skip AcquireDescriptorSet +
|
||||
// vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Four
|
||||
// entries with round-robin replacement rather than one: draws alternating
|
||||
// between two programs (MC's chunk<->entity ping-pong) would thrash a single
|
||||
// slot into a full re-allocate+write every draw. Reset each frame in BeginFrame
|
||||
// because the frame's descriptor sets are recycled there.
|
||||
struct DescriptorReuseEntry {
|
||||
Uint64 signature = 0;
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
Bool valid = false;
|
||||
};
|
||||
static constexpr Uint32 kDescriptorReuseMemoSize = 4;
|
||||
DescriptorReuseEntry m_descriptorReuseMemo[kDescriptorReuseMemoSize];
|
||||
Uint32 m_descriptorReuseMemoNext = 0;
|
||||
|
||||
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
|
||||
// block resolve to the same set AND the same dynamic offsets, so the
|
||||
|
||||
@@ -591,6 +591,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Idle-content promotion: see the field comments in VkBufferResource. The
|
||||
// streak counts frame BOUNDARIES survived unchanged (the same-frame memo
|
||||
// above swallows repeat draws), so a promotion needs the content stable
|
||||
// for kStreamedPromotionStreak whole frames - one no-op frame does not
|
||||
// trigger the resident round-trip, whose creation upload is itself a
|
||||
// staged copy worth avoiding for content that is about to change again.
|
||||
constexpr Uint32 kStreamedPromotionStreak = 2;
|
||||
if (resource->promotedResident) {
|
||||
if (resource->promotedChangeSerial == changeSerial &&
|
||||
static_cast<VkDeviceSize>(bufferObject->GetSize()) == size) {
|
||||
return AcquireResidentSlice(kind, bufferObject, outSlice);
|
||||
}
|
||||
resource->promotedResident = false;
|
||||
resource->unchangedStreak = 0;
|
||||
} else if (resource->transientChangeSerial == changeSerial && resource->transientSize == size &&
|
||||
resource->transientFrameSerial != 0) {
|
||||
if (++resource->unchangedStreak >= kStreamedPromotionStreak) {
|
||||
resource->promotedResident = true;
|
||||
resource->promotedChangeSerial = changeSerial;
|
||||
if (AcquireResidentSlice(kind, bufferObject, outSlice)) {
|
||||
return true;
|
||||
}
|
||||
resource->promotedResident = false; // resident creation failed: stream as before
|
||||
}
|
||||
} else {
|
||||
resource->unchangedStreak = 0;
|
||||
}
|
||||
|
||||
if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject->MappedData(), size, 16,
|
||||
outSlice)) {
|
||||
return false;
|
||||
|
||||
@@ -62,6 +62,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 transientFrameSerial = 0;
|
||||
Uint64 transientChangeSerial = 0;
|
||||
VkDeviceSize transientSize = 0;
|
||||
|
||||
// Streaming re-copies the whole store into the per-frame arena on every
|
||||
// frame, which is right for genuinely per-frame data but pure waste for a
|
||||
// Dynamic-hinted buffer the app stopped touching. After the content
|
||||
// survives kStreamedPromotionStreak frame boundaries unchanged it is
|
||||
// promoted to resident storage (one final upload, then zero per-frame
|
||||
// cost); the first content change demotes it back to streaming, and the
|
||||
// streaming path's existing downgrade releases the resident store.
|
||||
Uint32 unchangedStreak = 0;
|
||||
Bool promotedResident = false;
|
||||
Uint64 promotedChangeSerial = 0;
|
||||
};
|
||||
|
||||
// Supplies a command buffer that is recording and outside any render pass,
|
||||
|
||||
@@ -25,9 +25,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Compute shaders may legally sample framebuffer-attached textures (the GL feedback-loop rule
|
||||
// only covers rendering commands; e.g. Flywheel's Hi-Z depth pyramid downsample samples the
|
||||
// depth attachment of the bound draw framebuffer), so sampled-read barriers must cover the
|
||||
// compute stage in addition to the graphics stages.
|
||||
static constexpr VkPipelineStageFlags kSampledReadStages =
|
||||
VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
||||
// compute stage in addition to the graphics stages. Set at Initialize from the renderer's
|
||||
// device-feature-derived mask: geometry/tessellation stage bits are invalid in a barrier when
|
||||
// their feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and ALL_GRAPHICS
|
||||
// would also serialize against non-shader stages. The default only matters before a device
|
||||
// exists, when nothing records barriers.
|
||||
static VkPipelineStageFlags s_sampledReadStages =
|
||||
VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
||||
|
||||
static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) {
|
||||
Int maxDimension = std::max<Int>(baseTexelSize.x(),
|
||||
@@ -193,7 +198,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
|
||||
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
|
||||
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
|
||||
outSrcStageMask = kSampledReadStages;
|
||||
outSrcStageMask = s_sampledReadStages;
|
||||
outSrcAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
return;
|
||||
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
|
||||
@@ -241,7 +246,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
|
||||
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
|
||||
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
|
||||
outDstStageMask = kSampledReadStages;
|
||||
outDstStageMask = s_sampledReadStages;
|
||||
outDstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
return;
|
||||
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
|
||||
@@ -599,6 +604,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_commandPool = initInfo.commandPool;
|
||||
m_graphicsQueue = initInfo.graphicsQueue;
|
||||
m_imageFormatListSupported = initInfo.imageFormatListSupported;
|
||||
s_sampledReadStages = initInfo.sampledReadStageMask;
|
||||
m_currentFrameIndex = 0;
|
||||
m_deferredReleases.clear();
|
||||
m_deferredReleases.resize(initInfo.frameCount);
|
||||
@@ -1226,7 +1232,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
|
||||
kSampledReadStages, srcAccessMask,
|
||||
s_sampledReadStages, srcAccessMask,
|
||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
|
||||
resource->arrayLayers);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
||||
@@ -2055,6 +2061,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const void* source = nullptr;
|
||||
Vector<Uint8> expandedData;
|
||||
VkDeviceSize offset = 0;
|
||||
// Sub-region upload (a small sprite in a big atlas): only the dirty box
|
||||
// is staged and copied. texelSize keeps the LEVEL extent - the staging
|
||||
// row copy needs it for the shadow's stride. Plain color formats only;
|
||||
// the RGB-expand and depth(+stencil) conversion passes rewrite whole
|
||||
// levels and stay full-size.
|
||||
Bool subRegion = false;
|
||||
IntVec3 regionLo = {0, 0, 0};
|
||||
IntVec3 regionSize = {0, 0, 0};
|
||||
SizeT texelBytes = 0;
|
||||
};
|
||||
|
||||
Vector<UploadItem> uploadItems;
|
||||
@@ -2101,6 +2116,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
uploadItem.source = source;
|
||||
uploadItem.offset = stagingSize;
|
||||
uploadItem.uploadByteSize = byteSize;
|
||||
if (!formatInfo.expandRgbToRgba &&
|
||||
GetAspectMaskForFormat(outResource.format) == VK_IMAGE_ASPECT_COLOR_BIT) {
|
||||
const auto region = mipmapTexture.GetStorageDirtyRegion(target, level);
|
||||
const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
|
||||
static_cast<SizeT>(texelSize.y()) *
|
||||
static_cast<SizeT>(std::max(texelSize.z(), 1));
|
||||
if (!region.Empty() && !region.CoversWholeLevel(texelSize) && texelCount > 0 &&
|
||||
byteSize % texelCount == 0) {
|
||||
uploadItem.subRegion = true;
|
||||
uploadItem.regionLo = region.lo;
|
||||
uploadItem.regionSize = {region.hi.x() - region.lo.x(), region.hi.y() - region.lo.y(),
|
||||
region.hi.z() - region.lo.z()};
|
||||
uploadItem.texelBytes = byteSize / texelCount;
|
||||
uploadItem.uploadByteSize = static_cast<SizeT>(uploadItem.regionSize.x()) *
|
||||
static_cast<SizeT>(uploadItem.regionSize.y()) *
|
||||
static_cast<SizeT>(uploadItem.regionSize.z()) *
|
||||
uploadItem.texelBytes;
|
||||
}
|
||||
}
|
||||
if (formatInfo.expandRgbToRgba) {
|
||||
const Bool expanded = ExpandRgbSourceToRgba(source, byteSize, texelSize, formatInfo,
|
||||
uploadItem.expandedData);
|
||||
@@ -2255,7 +2289,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void* mapped = nullptr;
|
||||
VK_VERIFY(vmaMapMemory(m_allocator, stagingAllocation, &mapped), "vmaMapMemory(staging texture)");
|
||||
for (const auto& item : uploadItems) {
|
||||
std::memcpy(static_cast<Uint8*>(mapped) + item.offset, item.source, item.uploadByteSize);
|
||||
Uint8* dst = static_cast<Uint8*>(mapped) + item.offset;
|
||||
if (!item.subRegion) {
|
||||
std::memcpy(dst, item.source, item.uploadByteSize);
|
||||
continue;
|
||||
}
|
||||
// Tight-pack the dirty box: the shadow keeps whole-level rows, the
|
||||
// staging slice holds only the region (bufferRowLength stays 0).
|
||||
const SizeT levelRowBytes = static_cast<SizeT>(item.texelSize.x()) * item.texelBytes;
|
||||
const SizeT levelSliceBytes = static_cast<SizeT>(item.texelSize.y()) * levelRowBytes;
|
||||
const SizeT regionRowBytes = static_cast<SizeT>(item.regionSize.x()) * item.texelBytes;
|
||||
const Uint8* src = static_cast<const Uint8*>(item.source);
|
||||
for (Int z = 0; z < item.regionSize.z(); ++z) {
|
||||
for (Int y = 0; y < item.regionSize.y(); ++y) {
|
||||
const Uint8* srcRow = src +
|
||||
static_cast<SizeT>(item.regionLo.z() + z) * levelSliceBytes +
|
||||
static_cast<SizeT>(item.regionLo.y() + y) * levelRowBytes +
|
||||
static_cast<SizeT>(item.regionLo.x()) * item.texelBytes;
|
||||
std::memcpy(dst + (static_cast<SizeT>(z) * item.regionSize.y() + y) * regionRowBytes,
|
||||
srcRow, regionRowBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
vmaUnmapMemory(m_allocator, stagingAllocation);
|
||||
|
||||
@@ -2306,6 +2360,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
copy.imageOffset = {0, 0, 0};
|
||||
copy.imageExtent = {static_cast<Uint32>(item.texelSize.x()), static_cast<Uint32>(item.texelSize.y()),
|
||||
depthSelectsArrayLayer ? 1u : depthOrLayers};
|
||||
if (item.subRegion) {
|
||||
const Uint32 regionDepth = static_cast<Uint32>(std::max(item.regionSize.z(), 1));
|
||||
copy.imageOffset = {item.regionLo.x(), item.regionLo.y(),
|
||||
depthSelectsArrayLayer ? 0 : item.regionLo.z()};
|
||||
copy.imageExtent = {static_cast<Uint32>(item.regionSize.x()),
|
||||
static_cast<Uint32>(item.regionSize.y()),
|
||||
depthSelectsArrayLayer ? 1u : regionDepth};
|
||||
if (depthSelectsArrayLayer) {
|
||||
// The GL "depth" axis addresses array layers here, so a partial
|
||||
// z-range narrows the layer span rather than the extent.
|
||||
copy.imageSubresource.baseArrayLayer =
|
||||
item.baseArrayLayer + static_cast<Uint32>(item.regionLo.z());
|
||||
copy.imageSubresource.layerCount = regionDepth;
|
||||
}
|
||||
}
|
||||
if (isCombinedDepthStencil) {
|
||||
const SizeT texelCount = static_cast<SizeT>(item.texelSize.x()) *
|
||||
static_cast<SizeT>(item.texelSize.y()) *
|
||||
@@ -2330,7 +2399,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
uploadLayout,
|
||||
finalLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
kSampledReadStages,
|
||||
s_sampledReadStages,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
VK_ACCESS_SHADER_READ_BIT,
|
||||
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
||||
|
||||
@@ -59,6 +59,12 @@ public:
|
||||
// VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of
|
||||
// formats they will be viewed as, which is what lets a tiler keep them compressed.
|
||||
Bool imageFormatListSupported = false;
|
||||
// Union of shader stages sampled-read barriers may name on this device; the renderer
|
||||
// builds it from the enabled features because geometry/tessellation stage bits are
|
||||
// invalid in a barrier when their feature is off.
|
||||
VkPipelineStageFlags sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
|
||||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
||||
};
|
||||
|
||||
struct TextureResource {
|
||||
|
||||
@@ -2680,7 +2680,8 @@ void main() {
|
||||
MOBILEGL_ASSERT(m_textureManager != nullptr, "VkTextureManager creation failed.");
|
||||
succeeded = m_textureManager->Initialize(
|
||||
{m_device, m_physicalDevice.handle, m_allocator, m_commandPool, m_graphicsQueue,
|
||||
m_frameContext.GetFrameCount(), m_imageFormatListExtensionEnabled});
|
||||
m_frameContext.GetFrameCount(), m_imageFormatListExtensionEnabled,
|
||||
m_sampledReadStageMask});
|
||||
MOBILEGL_ASSERT(succeeded, "VkTextureManager initialization failed.");
|
||||
m_clearManager = MakeUnique<VkClearManager>();
|
||||
MOBILEGL_ASSERT(m_clearManager != nullptr, "VkClearManager creation failed.");
|
||||
@@ -2920,6 +2921,23 @@ void main() {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(VK_USE_PLATFORM_ANDROID_KHR)
|
||||
// The AImageReader owns the ANativeWindow the pbuffer fallback handed to the
|
||||
// WSI, so it outlives the surface and is released only here.
|
||||
if (m_fallbackImageReader != nullptr && m_platformLibrary != nullptr) {
|
||||
using AImageReaderDeleteFn = void (*)(void*);
|
||||
auto* imageReaderDelete =
|
||||
reinterpret_cast<AImageReaderDeleteFn>(dlsym(m_platformLibrary, "AImageReader_delete"));
|
||||
if (imageReaderDelete) {
|
||||
imageReaderDelete(m_fallbackImageReader);
|
||||
}
|
||||
m_fallbackImageReader = nullptr;
|
||||
m_window = 0;
|
||||
dlclose(m_platformLibrary);
|
||||
m_platformLibrary = nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (m_debugMessenger != VK_NULL_HANDLE) {
|
||||
DestroyDebugMessenger();
|
||||
m_debugMessenger = VK_NULL_HANDLE;
|
||||
@@ -9809,6 +9827,20 @@ void main() {
|
||||
if (!m_window) {
|
||||
#ifdef VK_USE_PLATFORM_METAL_EXT
|
||||
exts.push_back(VK_EXT_METAL_SURFACE_EXTENSION_NAME);
|
||||
#elif defined VK_USE_PLATFORM_ANDROID_KHR
|
||||
m_headlessSurfaceSupported = IsExtensionSupported(m_extensions, VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
|
||||
if (m_headlessSurfaceSupported) {
|
||||
exts.push_back(VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
|
||||
} else {
|
||||
// No mobile ICD seen so far implements VK_EXT_headless_surface
|
||||
// (Mali r32p1 does not), and this used to abort the process the
|
||||
// moment an application asked for a pbuffer context. CreateSurface()
|
||||
// gives the WSI an AImageReader window instead, so request the
|
||||
// Android surface extension for it.
|
||||
MGLOG_I("%s not available; falling back to an AImageReader %s surface for the pbuffer context.",
|
||||
VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME);
|
||||
exts.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME);
|
||||
}
|
||||
#elif defined VK_USE_PLATFORM_XLIB_KHR
|
||||
m_headlessSurfaceSupported = IsExtensionSupported(m_extensions, VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
|
||||
if (m_headlessSurfaceSupported) {
|
||||
@@ -10134,6 +10166,19 @@ void main() {
|
||||
: supportedDeviceFeatures.robustBufferAccess;
|
||||
deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader;
|
||||
deviceFeatures.tessellationShader = supportedDeviceFeatures.tessellationShader;
|
||||
// Sampled-read barriers may only name the shader stages whose device feature is
|
||||
// actually enabled (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), so the
|
||||
// mask is assembled here, next to the feature decision, and handed to consumers.
|
||||
m_sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
|
||||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
||||
if (deviceFeatures.geometryShader == VK_TRUE) {
|
||||
m_sampledReadStageMask |= VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT;
|
||||
}
|
||||
if (deviceFeatures.tessellationShader == VK_TRUE) {
|
||||
m_sampledReadStageMask |= VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT |
|
||||
VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT;
|
||||
}
|
||||
deviceFeatures.independentBlend = supportedDeviceFeatures.independentBlend;
|
||||
m_independentBlendFeatureEnabled = deviceFeatures.independentBlend == VK_TRUE;
|
||||
deviceFeatures.fillModeNonSolid = supportedDeviceFeatures.fillModeNonSolid;
|
||||
@@ -10646,6 +10691,54 @@ void main() {
|
||||
m_window = reinterpret_cast<NativeWindowType>(
|
||||
CreateInternalMetalLayer(m_config.SurfaceWidth, m_config.SurfaceHeight, &m_platformDisplay));
|
||||
m_platformLibrary = reinterpret_cast<void*>(m_window);
|
||||
#elif defined VK_USE_PLATFORM_ANDROID_KHR
|
||||
if (m_headlessSurfaceSupported) {
|
||||
auto* createHeadlessSurface = reinterpret_cast<PFN_vkCreateHeadlessSurfaceEXT>(
|
||||
vkGetInstanceProcAddr(m_instance, "vkCreateHeadlessSurfaceEXT"));
|
||||
MOBILEGL_ASSERT(createHeadlessSurface != nullptr,
|
||||
"VK_EXT_headless_surface is not available for DirectVulkan pbuffer surface");
|
||||
VkHeadlessSurfaceCreateInfoEXT sci{VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT};
|
||||
VK_VERIFY(createHeadlessSurface(m_instance, &sci, nullptr, &m_surface),
|
||||
"vkCreateHeadlessSurfaceEXT failed");
|
||||
return;
|
||||
}
|
||||
// Windowless context on a driver without VK_EXT_headless_surface: give
|
||||
// the WSI an AImageReader's ANativeWindow. It is a real, valid producer
|
||||
// surface that is attached to no display and whose images this code never
|
||||
// acquires, which is exactly the "drawable nobody sees" the Xlib fallback
|
||||
// below builds out of an unmapped window. libmediandk is dlopen'd rather
|
||||
// than linked so a device without it degrades to the old error instead of
|
||||
// failing to load the library at all.
|
||||
{
|
||||
void* mediaLib = dlopen("libmediandk.so", RTLD_NOW | RTLD_LOCAL);
|
||||
MOBILEGL_ASSERT(mediaLib != nullptr,
|
||||
"VK_EXT_headless_surface is unavailable and libmediandk.so could not be loaded "
|
||||
"for the pbuffer surface fallback");
|
||||
using AImageReaderNewFn = int (*)(int32_t, int32_t, int32_t, int32_t, void**);
|
||||
using AImageReaderGetWindowFn = int (*)(void*, void**);
|
||||
auto* imageReaderNew = reinterpret_cast<AImageReaderNewFn>(dlsym(mediaLib, "AImageReader_new"));
|
||||
auto* imageReaderGetWindow =
|
||||
reinterpret_cast<AImageReaderGetWindowFn>(dlsym(mediaLib, "AImageReader_getWindow"));
|
||||
MOBILEGL_ASSERT(imageReaderNew != nullptr && imageReaderGetWindow != nullptr,
|
||||
"libmediandk.so is missing AImageReader_new/AImageReader_getWindow");
|
||||
|
||||
constexpr int32_t kAndroidFormatRgba8888 = 0x1; // AIMAGE_FORMAT_RGBA_8888
|
||||
const int32_t width = static_cast<int32_t>(std::max<Uint32>(m_config.SurfaceWidth, 1));
|
||||
const int32_t height = static_cast<int32_t>(std::max<Uint32>(m_config.SurfaceHeight, 1));
|
||||
void* reader = nullptr;
|
||||
// maxImages must cover the swapchain's images; the reader never
|
||||
// acquires any, so this only sizes its buffer queue.
|
||||
const int status = imageReaderNew(width, height, kAndroidFormatRgba8888, 8, &reader);
|
||||
MOBILEGL_ASSERT(status == 0 && reader != nullptr,
|
||||
"AImageReader_new failed (%d) for the pbuffer surface fallback", status);
|
||||
void* nativeWindow = nullptr;
|
||||
const int windowStatus = imageReaderGetWindow(reader, &nativeWindow);
|
||||
MOBILEGL_ASSERT(windowStatus == 0 && nativeWindow != nullptr,
|
||||
"AImageReader_getWindow failed (%d) for the pbuffer surface fallback", windowStatus);
|
||||
m_fallbackImageReader = reader;
|
||||
m_platformLibrary = mediaLib;
|
||||
m_window = reinterpret_cast<NativeWindowType>(nativeWindow);
|
||||
}
|
||||
#elif defined VK_USE_PLATFORM_XLIB_KHR
|
||||
if (m_headlessSurfaceSupported) {
|
||||
auto* createHeadlessSurface =
|
||||
|
||||
@@ -444,6 +444,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// above (rather than being handed one by the caller), so Shutdown() knows it
|
||||
// owns that window and must destroy it.
|
||||
Bool m_ownsFallbackXlibWindow = false;
|
||||
// Android has the same shortfall: no Mali/Adreno driver seen so far exposes
|
||||
// VK_EXT_headless_surface, so a windowless (EGL pbuffer) context gets an
|
||||
// AImageReader's ANativeWindow to hand the WSI instead. Nothing is ever
|
||||
// displayed - the reader's images are simply never acquired. Owned here, so
|
||||
// Shutdown() deletes it.
|
||||
void* m_fallbackImageReader = nullptr;
|
||||
VulkanRendererConfig m_config;
|
||||
Bool m_swapchainResizeRequested = false;
|
||||
// Presentation is suspended while the window is zero-area (minimized): the
|
||||
@@ -485,6 +491,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
|
||||
Bool m_dualSrcBlendFeatureEnabled = false;
|
||||
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
|
||||
// Union of shader stages sampled-read barriers may name; built at device creation
|
||||
// because geometry/tessellation stage bits are invalid in a barrier when their
|
||||
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
|
||||
// ALL_GRAPHICS would also serialize against non-shader stages.
|
||||
VkPipelineStageFlags m_sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
|
||||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
||||
// Cached at device creation from the graphics queue family properties
|
||||
// and device limits; drives timer-query support.
|
||||
Uint32 m_timestampValidBits = 0;
|
||||
|
||||
@@ -41,4 +41,5 @@ add_test(NAME SanityBench COMMAND SanityBench --benchmark_counters_tabular=true)
|
||||
set_tests_properties(SanityBench PROPERTIES LABELS benchmark)
|
||||
|
||||
add_subdirectory(Program)
|
||||
add_subdirectory(Buffer)
|
||||
add_subdirectory(Buffer)
|
||||
add_subdirectory(Driver)
|
||||
@@ -0,0 +1,15 @@
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
|
||||
# A real, headless EGL client, deliberately NOT linked against MobileGL: it
|
||||
# dlopens one EGL provider at runtime ($DRIVERBENCH_EGL_LIB - the system
|
||||
# libEGL.so.1 for the native driver, or a libMobileGL.so path for either
|
||||
# MobileGL backend), so the same binary measures all three stacks.
|
||||
if (NOT UNIX OR APPLE OR ANDROID)
|
||||
return()
|
||||
endif()
|
||||
|
||||
add_executable(DriverBench DriverBench.c)
|
||||
target_link_libraries(DriverBench PRIVATE dl)
|
||||
|
||||
add_test(NAME DriverBench COMMAND DriverBench draw_tiny)
|
||||
set_tests_properties(DriverBench PROPERTIES LABELS benchmark)
|
||||
@@ -0,0 +1,454 @@
|
||||
/* MobileGL - MobileGL/MG_Benchmark/Driver/DriverBench.c
|
||||
* 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
|
||||
*
|
||||
* Headless, EGL-based driver benchmark shaped like Minecraft's GL usage.
|
||||
* Unlike the MobileGL_s microbenches next door this exercises a full GL
|
||||
* stack: it dlopens ONE EGL provider ($DRIVERBENCH_EGL_LIB - the system
|
||||
* libEGL.so.1 for the native driver, or a libMobileGL.so path for either
|
||||
* MobileGL backend selected with MOBILEGL_BACKEND_TYPE), creates a desktop-GL
|
||||
* context on a small pbuffer, renders into its own FBO and paces frames with
|
||||
* glFinish. No window system is required beyond what the provider itself
|
||||
* needs - see run_driver_bench.sh.
|
||||
*
|
||||
* Every case models one hot pattern from captured Minecraft traces:
|
||||
* draw_tiny back-to-back glDrawElements, shared state (chunk batch)
|
||||
* draw_uniform per-draw vec3 offset uniform + draw (chunk sections)
|
||||
* draw_multi_vao per-draw VAO/VBO switch + draw (per-section buffers)
|
||||
* tex_pingpong per-draw texture bind churn on one unit
|
||||
* program_pingpong alternate two programs + mat4 upload (chunk<->entity)
|
||||
* chunk_upload glBufferData(NULL) orphan + glBufferSubData + draw
|
||||
* atlas_sprite N 16x16 glTexSubImage2D into a 1024x512 atlas + draw
|
||||
* lightmap full 16x16 lightmap respecify per frame + draw
|
||||
* scene_mix composite frame built from the knobs below
|
||||
*
|
||||
* Output: one CSV line per case:
|
||||
* case,frames,ops_per_frame,median_frame_ms,ns_per_op,fps
|
||||
*/
|
||||
#include <dlfcn.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
/* ---- EGL constants ---- */
|
||||
typedef void* EGLDisplay;
|
||||
typedef void* EGLConfig;
|
||||
typedef void* EGLContext;
|
||||
typedef void* EGLSurface;
|
||||
typedef int EGLint;
|
||||
typedef unsigned int EGLBoolean;
|
||||
typedef unsigned int EGLenum;
|
||||
#define EGL_DEFAULT_DISPLAY ((void*)0)
|
||||
#define EGL_NO_CONTEXT ((EGLContext)0)
|
||||
#define EGL_NO_SURFACE ((EGLSurface)0)
|
||||
#define EGL_FALSE 0
|
||||
#define EGL_SURFACE_TYPE 0x3033
|
||||
#define EGL_PBUFFER_BIT 0x0001
|
||||
#define EGL_RENDERABLE_TYPE 0x3040
|
||||
#define EGL_OPENGL_BIT 0x0008
|
||||
#define EGL_RED_SIZE 0x3024
|
||||
#define EGL_GREEN_SIZE 0x3023
|
||||
#define EGL_BLUE_SIZE 0x3022
|
||||
#define EGL_DEPTH_SIZE 0x3025
|
||||
#define EGL_WIDTH 0x3057
|
||||
#define EGL_HEIGHT 0x3056
|
||||
#define EGL_NONE 0x3038
|
||||
#define EGL_OPENGL_API 0x30A2
|
||||
#define EGL_OPENGL_ES_API 0x30A0
|
||||
#define EGL_OPENGL_ES3_BIT 0x0040
|
||||
#define EGL_CONTEXT_CLIENT_VERSION 0x3098
|
||||
#define EGL_CONTEXT_MAJOR_VERSION 0x3098
|
||||
#define EGL_CONTEXT_MINOR_VERSION 0x30FB
|
||||
#define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD
|
||||
#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001
|
||||
|
||||
/* ---- GL constants ---- */
|
||||
#define GL_COLOR_BUFFER_BIT 0x00004000
|
||||
#define GL_DEPTH_BUFFER_BIT 0x00000100
|
||||
#define GL_TRIANGLES 0x0004
|
||||
#define GL_UNSIGNED_INT 0x1405
|
||||
#define GL_SHORT 0x1402
|
||||
#define GL_FLOAT 0x1406
|
||||
#define GL_UNSIGNED_BYTE 0x1401
|
||||
#define GL_ARRAY_BUFFER 0x8892
|
||||
#define GL_ELEMENT_ARRAY_BUFFER 0x8893
|
||||
#define GL_STATIC_DRAW 0x88E4
|
||||
#define GL_TEXTURE_2D 0x0DE1
|
||||
#define GL_TEXTURE0 0x84C0
|
||||
#define GL_RGBA 0x1908
|
||||
#define GL_RGBA8 0x8058
|
||||
#define GL_DEPTH_COMPONENT24 0x81A6
|
||||
#define GL_TEXTURE_MIN_FILTER 0x2801
|
||||
#define GL_TEXTURE_MAG_FILTER 0x2800
|
||||
#define GL_NEAREST 0x2600
|
||||
#define GL_NEAREST_MIPMAP_LINEAR 0x2702
|
||||
#define GL_DEPTH_TEST 0x0B71
|
||||
#define GL_VERTEX_SHADER 0x8B31
|
||||
#define GL_FRAGMENT_SHADER 0x8B30
|
||||
#define GL_COMPILE_STATUS 0x8B81
|
||||
#define GL_LINK_STATUS 0x8B82
|
||||
#define GL_VERSION 0x1F02
|
||||
#define GL_RENDERER 0x1F01
|
||||
#define GL_NO_ERROR 0
|
||||
#define GL_FRAMEBUFFER 0x8D40
|
||||
#define GL_RENDERBUFFER 0x8D41
|
||||
#define GL_COLOR_ATTACHMENT0 0x8CE0
|
||||
#define GL_DEPTH_ATTACHMENT 0x8D00
|
||||
#define GL_FRAMEBUFFER_COMPLETE 0x8CD5
|
||||
#define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
|
||||
#define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001
|
||||
#define GL_UNIFORM_BUFFER 0x8A11
|
||||
#define GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT 0x8A34
|
||||
#define GL_DYNAMIC_DRAW 0x88E8
|
||||
#define GL_STREAM_DRAW 0x88E0
|
||||
#define GL_UNPACK_ALIGNMENT 0x0CF5
|
||||
#define GL_UNPACK_ROW_LENGTH 0x0CF2
|
||||
#define GL_UNPACK_SKIP_ROWS 0x0CF3
|
||||
#define GL_UNPACK_SKIP_PIXELS 0x0CF4
|
||||
#define GL_TEXTURE_WRAP_S 0x2802
|
||||
#define GL_TEXTURE_WRAP_T 0x2803
|
||||
#define GL_CLAMP_TO_EDGE 0x812F
|
||||
#define GL_REPEAT 0x2901
|
||||
|
||||
typedef unsigned int GLuint;
|
||||
typedef int GLint;
|
||||
typedef int GLsizei;
|
||||
typedef unsigned int GLenum;
|
||||
typedef char GLchar;
|
||||
typedef unsigned char GLboolean;
|
||||
typedef long GLsizeiptr;
|
||||
typedef long GLintptr;
|
||||
|
||||
/* ---- resolved entry points ---- */
|
||||
static void* (*g_eglGetProcAddress)(const char*);
|
||||
static void* g_provider;
|
||||
|
||||
#define GLF(ret, name, args) static ret(*name) args;
|
||||
GLF(void, glClear, (unsigned))
|
||||
GLF(void, glClearColor, (float, float, float, float))
|
||||
GLF(void, glEnable, (GLenum))
|
||||
GLF(void, glViewport, (GLint, GLint, GLsizei, GLsizei))
|
||||
GLF(const unsigned char*, glGetString, (GLenum))
|
||||
GLF(GLenum, glGetError, (void))
|
||||
GLF(void, glFinish, (void))
|
||||
GLF(void, glFlush, (void))
|
||||
GLF(void, glGenBuffers, (GLsizei, GLuint*))
|
||||
GLF(void, glBindBuffer, (GLenum, GLuint))
|
||||
GLF(void, glBufferData, (GLenum, GLsizeiptr, const void*, GLenum))
|
||||
GLF(void, glBufferSubData, (GLenum, GLintptr, GLsizeiptr, const void*))
|
||||
GLF(void, glGenVertexArrays, (GLsizei, GLuint*))
|
||||
GLF(void, glBindVertexArray, (GLuint))
|
||||
GLF(void, glEnableVertexAttribArray, (GLuint))
|
||||
GLF(void, glVertexAttribPointer, (GLuint, GLint, GLenum, GLboolean, GLsizei, const void*))
|
||||
GLF(void, glGenTextures, (GLsizei, GLuint*))
|
||||
GLF(void, glBindTexture, (GLenum, GLuint))
|
||||
GLF(void, glActiveTexture, (GLenum))
|
||||
GLF(void, glTexImage2D, (GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, const void*))
|
||||
GLF(void, glTexSubImage2D, (GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, const void*))
|
||||
GLF(void, glTexParameteri, (GLenum, GLenum, GLint))
|
||||
GLF(void, glPixelStorei, (GLenum, GLint))
|
||||
GLF(void, glGetIntegerv, (GLenum, GLint*))
|
||||
GLF(void, glGenerateMipmap, (GLenum))
|
||||
GLF(GLuint, glCreateShader, (GLenum))
|
||||
GLF(void, glShaderSource, (GLuint, GLsizei, const GLchar* const*, const GLint*))
|
||||
GLF(void, glCompileShader, (GLuint))
|
||||
GLF(void, glGetShaderiv, (GLuint, GLenum, GLint*))
|
||||
GLF(void, glGetShaderInfoLog, (GLuint, GLsizei, GLsizei*, GLchar*))
|
||||
GLF(GLuint, glCreateProgram, (void))
|
||||
GLF(void, glAttachShader, (GLuint, GLuint))
|
||||
GLF(void, glLinkProgram, (GLuint))
|
||||
GLF(void, glGetProgramiv, (GLuint, GLenum, GLint*))
|
||||
GLF(void, glUseProgram, (GLuint))
|
||||
GLF(GLint, glGetUniformLocation, (GLuint, const GLchar*))
|
||||
GLF(void, glUniform1i, (GLint, GLint))
|
||||
GLF(void, glUniform3f, (GLint, float, float, float))
|
||||
GLF(void, glUniformMatrix4fv, (GLint, GLsizei, GLboolean, const float*))
|
||||
GLF(void, glDrawElements, (GLenum, GLsizei, GLenum, const void*))
|
||||
GLF(void, glBindAttribLocation, (GLuint, GLuint, const GLchar*))
|
||||
GLF(void, glUniform3fv, (GLint, GLsizei, const float*))
|
||||
GLF(void, glDrawArrays, (GLenum, GLint, GLsizei))
|
||||
GLF(void, glDrawElementsBaseVertex, (GLenum, GLsizei, GLenum, const void*, GLint))
|
||||
GLF(void, glMultiDrawElementsBaseVertex,
|
||||
(GLenum, const GLsizei*, GLenum, const void* const*, GLsizei, const GLint*))
|
||||
GLF(void, glBindBufferRange, (GLenum, GLuint, GLuint, GLintptr, GLsizeiptr))
|
||||
GLF(void, glBindBufferBase, (GLenum, GLuint, GLuint))
|
||||
GLF(GLuint, glGetUniformBlockIndex, (GLuint, const GLchar*))
|
||||
GLF(void, glUniformBlockBinding, (GLuint, GLuint, GLuint))
|
||||
GLF(void, glGenSamplers, (GLsizei, GLuint*))
|
||||
GLF(void, glBindSampler, (GLuint, GLuint))
|
||||
GLF(void, glSamplerParameteri, (GLuint, GLenum, GLint))
|
||||
GLF(void, glGenFramebuffers, (GLsizei, GLuint*))
|
||||
GLF(void, glBindFramebuffer, (GLenum, GLuint))
|
||||
GLF(void, glGenRenderbuffers, (GLsizei, GLuint*))
|
||||
GLF(void, glBindRenderbuffer, (GLenum, GLuint))
|
||||
GLF(void, glRenderbufferStorage, (GLenum, GLenum, GLsizei, GLsizei))
|
||||
GLF(void, glFramebufferRenderbuffer, (GLenum, GLenum, GLenum, GLuint))
|
||||
GLF(GLenum, glCheckFramebufferStatus, (GLenum))
|
||||
GLF(void*, glFenceSync, (GLenum, unsigned))
|
||||
GLF(GLenum, glClientWaitSync, (void*, unsigned, unsigned long long))
|
||||
GLF(void, glDeleteSync, (void*))
|
||||
|
||||
static uint64_t now_ns(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
|
||||
}
|
||||
|
||||
static int cmp_u64(const void* a, const void* b) {
|
||||
uint64_t x = *(const uint64_t*)a, y = *(const uint64_t*)b;
|
||||
return x < y ? -1 : x > y;
|
||||
}
|
||||
|
||||
|
||||
/* Scene, cases and the case table live next door so the Android plugin's
|
||||
* in-process benchmark runs byte-identical bodies. */
|
||||
static void bench_gl_failed(const char* what, const char* detail) {
|
||||
fprintf(stderr, "FAIL: %s %s\n", what, detail ? detail : "");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/* GLES has glDrawElementsBaseVertex (3.2 core) but no multi-draw form of it, so
|
||||
* against a native mobile driver the multi-draw case issues the same sub-draws
|
||||
* one at a time - which is what the extension folds up, and what an application
|
||||
* without it would have to write. Desktop GL and MobileGL take the real call. */
|
||||
static void bench_multi_draw_elements_base_vertex(GLenum mode, const GLsizei* counts, GLenum type,
|
||||
const void* const* offsets, GLsizei drawCount,
|
||||
const GLint* baseVertices) {
|
||||
if (glMultiDrawElementsBaseVertex) {
|
||||
glMultiDrawElementsBaseVertex(mode, counts, type, offsets, drawCount, baseVertices);
|
||||
return;
|
||||
}
|
||||
for (GLsizei i = 0; i < drawCount; ++i) {
|
||||
glDrawElementsBaseVertex(mode, counts[i], type, offsets[i], baseVertices[i]);
|
||||
}
|
||||
}
|
||||
|
||||
#include "DriverBenchCases.inc"
|
||||
|
||||
/* ---- bench driver: fence-paced frames on the offscreen FBO ----------------
|
||||
* Frames are closed with a real fence wait, not glFinish: MobileGL implements
|
||||
* glFinish and glFlush as no-ops (MG_Impl/GLImpl/Exporting/Definitions.cpp),
|
||||
* so a glFinish-paced loop would time only the CPU-side submit on a MobileGL
|
||||
* backend while timing submit-plus-GPU on the native driver - the two numbers
|
||||
* would not describe the same work. A sync object is honoured by every stack
|
||||
* measured here.
|
||||
*/
|
||||
typedef void (*case_fn)(int frame, long a, long b);
|
||||
static int g_warmup = 30, g_frames = 120;
|
||||
|
||||
static void end_frame_wait(void) {
|
||||
if (glFenceSync && glClientWaitSync && glDeleteSync) {
|
||||
void* sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
if (sync) {
|
||||
glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 1000000000ull);
|
||||
glDeleteSync(sync);
|
||||
return;
|
||||
}
|
||||
}
|
||||
glFinish();
|
||||
}
|
||||
|
||||
static void run_case(const char* name, case_fn body, long a, long b, long opsPerFrame) {
|
||||
static uint64_t samples[4096];
|
||||
if (g_frames > 4096) g_frames = 4096;
|
||||
end_frame_wait();
|
||||
for (int i = 0; i < g_warmup; ++i) {
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
body(i, a, b);
|
||||
end_frame_wait();
|
||||
}
|
||||
for (int i = 0; i < g_frames; ++i) {
|
||||
uint64_t t0 = now_ns();
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
body(i, a, b);
|
||||
end_frame_wait();
|
||||
samples[i] = now_ns() - t0;
|
||||
}
|
||||
qsort(samples, g_frames, sizeof(uint64_t), cmp_u64);
|
||||
uint64_t med = samples[g_frames / 2];
|
||||
double frameMs = med / 1e6;
|
||||
double nsPerOp = opsPerFrame > 0 ? (double)med / (double)opsPerFrame : 0.0;
|
||||
printf("%s,%d,%ld,%.3f,%.1f,%.1f\n", name, g_frames, opsPerFrame, frameMs, nsPerOp,
|
||||
1e9 / (double)med);
|
||||
fflush(stdout);
|
||||
if (glGetError() != GL_NO_ERROR) fprintf(stderr, "WARN: GL error after %s\n", name);
|
||||
}
|
||||
|
||||
/* a = draws per frame */
|
||||
/* ---- EGL bootstrap: one provider library, pbuffer, desktop-GL context ---- */
|
||||
static int boot_egl(void) {
|
||||
const char* libpath = getenv("DRIVERBENCH_EGL_LIB");
|
||||
if (!libpath) libpath = "libEGL.so.1";
|
||||
g_provider = dlopen(libpath, RTLD_LAZY | RTLD_LOCAL);
|
||||
if (!g_provider) {
|
||||
fprintf(stderr, "FAIL: dlopen %s: %s\n", libpath, dlerror());
|
||||
return 1;
|
||||
}
|
||||
#define ESYM(name) \
|
||||
void* p_##name = dlsym(g_provider, #name); \
|
||||
if (!p_##name) { fprintf(stderr, "FAIL: dlsym %s\n", #name); return 1; }
|
||||
ESYM(eglGetDisplay)
|
||||
ESYM(eglInitialize)
|
||||
ESYM(eglChooseConfig)
|
||||
ESYM(eglBindAPI)
|
||||
ESYM(eglCreateContext)
|
||||
ESYM(eglCreatePbufferSurface)
|
||||
ESYM(eglMakeCurrent)
|
||||
ESYM(eglGetProcAddress)
|
||||
ESYM(eglGetError)
|
||||
g_eglGetProcAddress = (void* (*)(const char*))p_eglGetProcAddress;
|
||||
|
||||
EGLDisplay dpy = ((EGLDisplay(*)(void*))p_eglGetDisplay)(EGL_DEFAULT_DISPLAY);
|
||||
if (!dpy) { fprintf(stderr, "FAIL: eglGetDisplay\n"); return 1; }
|
||||
EGLint maj = 0, min = 0;
|
||||
if (!((EGLBoolean(*)(EGLDisplay, EGLint*, EGLint*))p_eglInitialize)(dpy, &maj, &min)) {
|
||||
fprintf(stderr, "FAIL: eglInitialize (0x%x)\n", ((EGLint(*)(void))p_eglGetError)());
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, "EGL %d.%d via %s\n", maj, min, libpath);
|
||||
|
||||
// Desktop GL first (that is what MobileGL exposes and what the cases are
|
||||
// written against), GLES 3 second so the same binary can measure a device's
|
||||
// native driver as the baseline. The .inc picks ESSL shader sources when the
|
||||
// context turns out to be ES.
|
||||
EGLBoolean (*chooseConfig)(EGLDisplay, const EGLint*, EGLConfig*, EGLint, EGLint*) =
|
||||
(EGLBoolean(*)(EGLDisplay, const EGLint*, EGLConfig*, EGLint, EGLint*))p_eglChooseConfig;
|
||||
EGLContext (*createContext)(EGLDisplay, EGLConfig, EGLContext, const EGLint*) =
|
||||
(EGLContext(*)(EGLDisplay, EGLConfig, EGLContext, const EGLint*))p_eglCreateContext;
|
||||
EGLBoolean (*bindApi)(EGLenum) = (EGLBoolean(*)(EGLenum))p_eglBindAPI;
|
||||
|
||||
EGLConfig cfg = NULL;
|
||||
EGLint ncfg = 0;
|
||||
EGLContext ctx = EGL_NO_CONTEXT;
|
||||
|
||||
if (bindApi(EGL_OPENGL_API)) {
|
||||
const EGLint cfgAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RED_SIZE, 8,
|
||||
EGL_DEPTH_SIZE, 24, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE};
|
||||
if (chooseConfig(dpy, cfgAttribs, &cfg, 1, &ncfg) && ncfg >= 1) {
|
||||
const EGLint ctxAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 2,
|
||||
EGL_CONTEXT_OPENGL_PROFILE_MASK,
|
||||
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, EGL_NONE};
|
||||
ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, ctxAttribs);
|
||||
if (ctx == EGL_NO_CONTEXT) ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, NULL);
|
||||
}
|
||||
}
|
||||
if (ctx == EGL_NO_CONTEXT) {
|
||||
if (!bindApi(EGL_OPENGL_ES_API)) {
|
||||
fprintf(stderr, "FAIL: neither OpenGL nor OpenGL ES is bindable on this provider\n");
|
||||
return 1;
|
||||
}
|
||||
const EGLint esCfgAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RED_SIZE, 8,
|
||||
EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_DEPTH_SIZE, 24,
|
||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_NONE};
|
||||
ncfg = 0;
|
||||
if (!chooseConfig(dpy, esCfgAttribs, &cfg, 1, &ncfg) || ncfg < 1) {
|
||||
const EGLint relaxed[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RED_SIZE, 8, EGL_NONE};
|
||||
if (!chooseConfig(dpy, relaxed, &cfg, 1, &ncfg) || ncfg < 1) {
|
||||
fprintf(stderr, "FAIL: eglChooseConfig\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
const EGLint esCtxAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
|
||||
ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, esCtxAttribs);
|
||||
}
|
||||
if (ctx == EGL_NO_CONTEXT) {
|
||||
fprintf(stderr, "FAIL: eglCreateContext (0x%x)\n", ((EGLint(*)(void))p_eglGetError)());
|
||||
return 1;
|
||||
}
|
||||
|
||||
const EGLint pbAttribs[] = {EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE};
|
||||
EGLSurface surf = ((EGLSurface(*)(EGLDisplay, EGLConfig, const EGLint*))p_eglCreatePbufferSurface)(
|
||||
dpy, cfg, pbAttribs);
|
||||
if (surf == EGL_NO_SURFACE) {
|
||||
fprintf(stderr, "FAIL: eglCreatePbufferSurface (0x%x)\n", ((EGLint(*)(void))p_eglGetError)());
|
||||
return 1;
|
||||
}
|
||||
if (!((EGLBoolean(*)(EGLDisplay, EGLSurface, EGLSurface, EGLContext))p_eglMakeCurrent)(dpy, surf,
|
||||
surf, ctx)) {
|
||||
fprintf(stderr, "FAIL: eglMakeCurrent (0x%x)\n", ((EGLint(*)(void))p_eglGetError)());
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Core GL entry points: eglGetProcAddress first (EGL 1.5 serves core
|
||||
* functions), provider dlsym as fallback (both glvnd and MobileGL export
|
||||
* the gl* symbols directly). */
|
||||
#define RESOLVE(name) \
|
||||
do { \
|
||||
*(void**)&name = g_eglGetProcAddress(#name); \
|
||||
if (!name) *(void**)&name = dlsym(g_provider, #name); \
|
||||
if (!name) { fprintf(stderr, "FAIL: resolve %s\n", #name); return 1; } \
|
||||
} while (0)
|
||||
RESOLVE(glClear); RESOLVE(glClearColor); RESOLVE(glEnable); RESOLVE(glViewport);
|
||||
RESOLVE(glGetString); RESOLVE(glGetError); RESOLVE(glFinish); RESOLVE(glFlush);
|
||||
RESOLVE(glGenBuffers); RESOLVE(glBindBuffer); RESOLVE(glBufferData); RESOLVE(glBufferSubData);
|
||||
RESOLVE(glGenVertexArrays); RESOLVE(glBindVertexArray); RESOLVE(glEnableVertexAttribArray);
|
||||
RESOLVE(glVertexAttribPointer); RESOLVE(glGenTextures); RESOLVE(glBindTexture);
|
||||
RESOLVE(glActiveTexture); RESOLVE(glTexImage2D); RESOLVE(glTexSubImage2D);
|
||||
RESOLVE(glTexParameteri); RESOLVE(glGenerateMipmap); RESOLVE(glCreateShader);
|
||||
RESOLVE(glPixelStorei); RESOLVE(glGetIntegerv);
|
||||
RESOLVE(glShaderSource); RESOLVE(glCompileShader); RESOLVE(glGetShaderiv);
|
||||
RESOLVE(glGetShaderInfoLog); RESOLVE(glCreateProgram); RESOLVE(glAttachShader);
|
||||
RESOLVE(glLinkProgram); RESOLVE(glGetProgramiv); RESOLVE(glUseProgram);
|
||||
RESOLVE(glGetUniformLocation); RESOLVE(glUniform1i); RESOLVE(glUniform3f);
|
||||
RESOLVE(glUniformMatrix4fv); RESOLVE(glDrawElements); RESOLVE(glBindAttribLocation);
|
||||
RESOLVE(glUniform3fv); RESOLVE(glDrawArrays); RESOLVE(glDrawElementsBaseVertex);
|
||||
RESOLVE(glBindBufferRange); RESOLVE(glBindBufferBase);
|
||||
RESOLVE(glGetUniformBlockIndex); RESOLVE(glUniformBlockBinding);
|
||||
RESOLVE(glGenSamplers); RESOLVE(glBindSampler); RESOLVE(glSamplerParameteri);
|
||||
RESOLVE(glGenFramebuffers); RESOLVE(glBindFramebuffer); RESOLVE(glGenRenderbuffers);
|
||||
RESOLVE(glBindRenderbuffer); RESOLVE(glRenderbufferStorage); RESOLVE(glFramebufferRenderbuffer);
|
||||
RESOLVE(glCheckFramebufferStatus);
|
||||
// Optional: end_frame_wait() falls back to glFinish when a stack has no
|
||||
// sync objects, so resolve without failing the run.
|
||||
*(void**)&glFenceSync = g_eglGetProcAddress("glFenceSync");
|
||||
if (!glFenceSync) *(void**)&glFenceSync = dlsym(g_provider, "glFenceSync");
|
||||
*(void**)&glClientWaitSync = g_eglGetProcAddress("glClientWaitSync");
|
||||
if (!glClientWaitSync) *(void**)&glClientWaitSync = dlsym(g_provider, "glClientWaitSync");
|
||||
*(void**)&glDeleteSync = g_eglGetProcAddress("glDeleteSync");
|
||||
if (!glDeleteSync) *(void**)&glDeleteSync = dlsym(g_provider, "glDeleteSync");
|
||||
// Desktop-only: GLES 3.2 has DrawElementsBaseVertex but no multi-draw form,
|
||||
// so bench_multi_draw_elements_base_vertex() emulates it when this is null.
|
||||
*(void**)&glMultiDrawElementsBaseVertex = g_eglGetProcAddress("glMultiDrawElementsBaseVertex");
|
||||
if (!glMultiDrawElementsBaseVertex)
|
||||
*(void**)&glMultiDrawElementsBaseVertex = dlsym(g_provider, "glMultiDrawElementsBaseVertex");
|
||||
|
||||
fprintf(stderr, "renderer: %s\n", glGetString(GL_RENDERER));
|
||||
fprintf(stderr, "version: %s\n", glGetString(GL_VERSION));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
long draws = 2048;
|
||||
if (getenv("DRIVERBENCH_DRAWS")) draws = atol(getenv("DRIVERBENCH_DRAWS"));
|
||||
if (getenv("DRIVERBENCH_FRAMES")) g_frames = atoi(getenv("DRIVERBENCH_FRAMES"));
|
||||
if (getenv("DRIVERBENCH_SPRITES")) g_mixSprites = atol(getenv("DRIVERBENCH_SPRITES"));
|
||||
|
||||
if (boot_egl()) return 1;
|
||||
build_resources();
|
||||
|
||||
printf("case,frames,ops_per_frame,median_frame_ms,ns_per_op,fps\n");
|
||||
for (int i = 0; i < kBenchCaseCount; ++i) {
|
||||
const BenchCaseDesc* c = &kBenchCases[i];
|
||||
if (argc > 1) {
|
||||
int wanted = 0;
|
||||
for (int j = 1; j < argc; ++j)
|
||||
if (strcmp(argv[j], c->name) == 0) wanted = 1;
|
||||
if (!wanted) continue;
|
||||
}
|
||||
// The generic cases scale with DRIVERBENCH_DRAWS; the mc_* rates are
|
||||
// measured and must not move, or the numbers stop being comparable.
|
||||
long a = c->a, ops = c->opsPerFrame;
|
||||
if (strncmp(c->name, "mc_", 3) != 0 && a > 100) {
|
||||
a = draws * a / 2048;
|
||||
ops = c->opsPerFrame * draws / 2048;
|
||||
}
|
||||
run_case(c->name, c->fn, a, c->b, ops);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
/* MobileGL - MobileGL/MG_Benchmark/Driver/DriverBenchCases.inc
|
||||
* 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
|
||||
*
|
||||
* The benchmark scene and its cases, with no harness and no GL loader: the
|
||||
* includer supplies both. DriverBench.c drives it through function pointers
|
||||
* resolved from one EGL provider; MG_Util/SelfTest/DriverBenchJni.cpp drives
|
||||
* it through MobileGL's own frontend entry points inside the Android plugin.
|
||||
* Sharing the bodies is the point - a number from the phone and a number from
|
||||
* the desktop have to describe the same work.
|
||||
*
|
||||
* The includer must have declared, before including this file: the GL types
|
||||
* and enums used below, and callable gl* entry points with the standard
|
||||
* signatures. bench_gl_failed() is called (and must be defined) when shader
|
||||
* compilation or linking fails, so a caller can report the failure instead of
|
||||
* dying inside a benchmark.
|
||||
*/
|
||||
|
||||
/* ---- shared scene resources (Minecraft-shaped) ---- */
|
||||
#define MAX_SECTIONS 512
|
||||
static GLuint g_progChunk, g_progEntity;
|
||||
static GLint g_uOffsetChunk, g_uMvpChunk, g_uMvpEntity;
|
||||
static GLuint g_vao[MAX_SECTIONS], g_vbo[MAX_SECTIONS];
|
||||
static GLuint g_sharedIbo;
|
||||
static GLuint g_texAtlas, g_texLight, g_texEntity;
|
||||
static int g_quadsPerSection = 128; /* 128 quads = 512 verts, 768 indices */
|
||||
static unsigned char* g_scratch;
|
||||
/* Uniform ring + sampler for the 26.2-shaped cases (see the case block below). */
|
||||
static GLuint g_uboRing;
|
||||
static GLint g_uboAlign = 256;
|
||||
static size_t g_uboSlot = 256;
|
||||
static GLuint g_sampler;
|
||||
static float g_mvp[16] = {0.002f, 0, 0, 0, 0, 0.002f, 0, 0, 0, 0, -0.001f, 0, -1.f, -1.f, 0.f, 1.f};
|
||||
|
||||
/* Minecraft chunk vertex: pos 3f, color 4ub, uv 2f, packed light 2s -> 32 B */
|
||||
#define VERT_STRIDE 32
|
||||
static void fill_section_vertices(unsigned char* dst, int quads, unsigned seed) {
|
||||
for (int q = 0; q < quads * 4; ++q) {
|
||||
float* f = (float*)(dst + q * VERT_STRIDE);
|
||||
unsigned r = seed = seed * 1664525u + 1013904223u;
|
||||
f[0] = (float)(q & 31) * 8.0f + (float)(r & 7);
|
||||
f[1] = (float)((q >> 5) & 31) * 8.0f;
|
||||
f[2] = (float)(q % 7) * 0.1f;
|
||||
dst[q * VERT_STRIDE + 12] = (unsigned char)r;
|
||||
dst[q * VERT_STRIDE + 13] = (unsigned char)(r >> 8);
|
||||
dst[q * VERT_STRIDE + 14] = (unsigned char)(r >> 16);
|
||||
dst[q * VERT_STRIDE + 15] = 255;
|
||||
f[4] = (float)(r & 1023) / 1024.0f;
|
||||
f[5] = (float)((r >> 10) & 511) / 512.0f;
|
||||
((short*)(dst + q * VERT_STRIDE + 24))[0] = 15 << 4;
|
||||
((short*)(dst + q * VERT_STRIDE + 24))[1] = 15 << 4;
|
||||
}
|
||||
}
|
||||
|
||||
static GLuint make_shader(GLenum kind, const char* src) {
|
||||
GLuint sh = glCreateShader(kind);
|
||||
glShaderSource(sh, 1, &src, NULL);
|
||||
glCompileShader(sh);
|
||||
GLint ok = 0;
|
||||
glGetShaderiv(sh, GL_COMPILE_STATUS, &ok);
|
||||
if (!ok) {
|
||||
char log[1024];
|
||||
glGetShaderInfoLog(sh, sizeof log, NULL, log);
|
||||
bench_gl_failed("shader compile", log);
|
||||
return 0;
|
||||
}
|
||||
return sh;
|
||||
}
|
||||
|
||||
static GLuint make_program(const char* vs_src, const char* fs_src) {
|
||||
GLuint prog = glCreateProgram();
|
||||
glAttachShader(prog, make_shader(GL_VERTEX_SHADER, vs_src));
|
||||
glAttachShader(prog, make_shader(GL_FRAGMENT_SHADER, fs_src));
|
||||
glBindAttribLocation(prog, 0, "aPos");
|
||||
glBindAttribLocation(prog, 1, "aColor");
|
||||
glBindAttribLocation(prog, 2, "aUv");
|
||||
glBindAttribLocation(prog, 3, "aLight");
|
||||
glLinkProgram(prog);
|
||||
GLint ok = 0;
|
||||
glGetProgramiv(prog, GL_LINK_STATUS, &ok);
|
||||
if (!ok) {
|
||||
bench_gl_failed("program link", "");
|
||||
return 0;
|
||||
}
|
||||
return prog;
|
||||
}
|
||||
|
||||
static const char* kChunkVs =
|
||||
"#version 150 core\n"
|
||||
"in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n"
|
||||
"uniform mat4 uMvp; uniform vec3 uOffset;\n"
|
||||
"out vec4 vColor; out vec2 vUv; out vec2 vLight;\n"
|
||||
"void main(){ gl_Position = uMvp * vec4(aPos + uOffset, 1.0);\n"
|
||||
" vColor = aColor; vUv = aUv; vLight = aLight * (1.0/256.0); }\n";
|
||||
static const char* kChunkFs =
|
||||
"#version 150 core\n"
|
||||
"in vec4 vColor; in vec2 vUv; in vec2 vLight; out vec4 o;\n"
|
||||
"uniform sampler2D uAtlas; uniform sampler2D uLight;\n"
|
||||
"void main(){ o = texture(uAtlas, vUv) * vColor * texture(uLight, vLight); }\n";
|
||||
static const char* kEntityVs =
|
||||
"#version 150 core\n"
|
||||
"in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n"
|
||||
"uniform mat4 uMvp; uniform mat4 uModel;\n"
|
||||
"out vec4 vColor; out vec2 vUv;\n"
|
||||
"void main(){ gl_Position = uMvp * uModel * vec4(aPos, 1.0); vColor = aColor; vUv = aUv; }\n";
|
||||
static const char* kEntityFs =
|
||||
"#version 150 core\n"
|
||||
"in vec4 vColor; in vec2 vUv; out vec4 o; uniform sampler2D uTex;\n"
|
||||
"void main(){ o = texture(uTex, vUv) * vColor; }\n";
|
||||
|
||||
// ESSL 3.20 twins of the four shaders above. The bodies are identical; only the
|
||||
// version line and the precision qualifiers differ, so the two paths compile the
|
||||
// same work. Needed because this bench also runs against a device's native GLES
|
||||
// driver as the baseline MobileGL is measured against, and that driver rejects
|
||||
// desktop GLSL - while MobileGL is fed desktop GLSL on purpose, since translating
|
||||
// it is the thing under test.
|
||||
static const char* kChunkVsEs =
|
||||
"#version 320 es\n"
|
||||
"precision highp float;\n"
|
||||
"in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n"
|
||||
"uniform mat4 uMvp; uniform vec3 uOffset;\n"
|
||||
"out vec4 vColor; out vec2 vUv; out vec2 vLight;\n"
|
||||
"void main(){ gl_Position = uMvp * vec4(aPos + uOffset, 1.0);\n"
|
||||
" vColor = aColor; vUv = aUv; vLight = aLight * (1.0/256.0); }\n";
|
||||
static const char* kChunkFsEs =
|
||||
"#version 320 es\n"
|
||||
"precision mediump float;\n"
|
||||
"in vec4 vColor; in vec2 vUv; in vec2 vLight; out vec4 o;\n"
|
||||
"uniform sampler2D uAtlas; uniform sampler2D uLight;\n"
|
||||
"void main(){ o = texture(uAtlas, vUv) * vColor * texture(uLight, vLight); }\n";
|
||||
static const char* kEntityVsEs =
|
||||
"#version 320 es\n"
|
||||
"precision highp float;\n"
|
||||
"in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n"
|
||||
"uniform mat4 uMvp; uniform mat4 uModel;\n"
|
||||
"out vec4 vColor; out vec2 vUv;\n"
|
||||
"void main(){ gl_Position = uMvp * uModel * vec4(aPos, 1.0); vColor = aColor; vUv = aUv; }\n";
|
||||
static const char* kEntityFsEs =
|
||||
"#version 320 es\n"
|
||||
"precision mediump float;\n"
|
||||
"in vec4 vColor; in vec2 vUv; out vec4 o; uniform sampler2D uTex;\n"
|
||||
"void main(){ o = texture(uTex, vUv) * vColor; }\n";
|
||||
|
||||
// True once build_resources() has seen a GL_VERSION beginning with "OpenGL ES".
|
||||
static int g_isGlesContext = 0;
|
||||
|
||||
static void setup_vao(GLuint vao, GLuint vbo, GLuint ibo) {
|
||||
glBindVertexArray(vao);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glEnableVertexAttribArray(0);
|
||||
glEnableVertexAttribArray(1);
|
||||
glEnableVertexAttribArray(2);
|
||||
glEnableVertexAttribArray(3);
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, 0, VERT_STRIDE, (void*)0);
|
||||
glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, 1, VERT_STRIDE, (void*)12);
|
||||
glVertexAttribPointer(2, 2, GL_FLOAT, 0, VERT_STRIDE, (void*)16);
|
||||
glVertexAttribPointer(3, 2, GL_SHORT, 0, VERT_STRIDE, (void*)24);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
|
||||
}
|
||||
|
||||
static void build_resources(void) {
|
||||
/* offscreen render target: 1280x720 RBO FBO, like CTS fbo surface mode */
|
||||
GLuint fbo, rboColor, rboDepth;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glGenRenderbuffers(1, &rboColor);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, rboColor);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1280, 720);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rboColor);
|
||||
glGenRenderbuffers(1, &rboDepth);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, rboDepth);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 1280, 720);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
bench_gl_failed("FBO incomplete", "");
|
||||
return;
|
||||
}
|
||||
|
||||
const char* versionString = (const char*)glGetString(GL_VERSION);
|
||||
g_isGlesContext = versionString != NULL && strncmp(versionString, "OpenGL ES", 9) == 0;
|
||||
g_progChunk = g_isGlesContext ? make_program(kChunkVsEs, kChunkFsEs) : make_program(kChunkVs, kChunkFs);
|
||||
g_progEntity = g_isGlesContext ? make_program(kEntityVsEs, kEntityFsEs) : make_program(kEntityVs, kEntityFs);
|
||||
glUseProgram(g_progChunk);
|
||||
g_uMvpChunk = glGetUniformLocation(g_progChunk, "uMvp");
|
||||
g_uOffsetChunk = glGetUniformLocation(g_progChunk, "uOffset");
|
||||
glUniform1i(glGetUniformLocation(g_progChunk, "uAtlas"), 0);
|
||||
glUniform1i(glGetUniformLocation(g_progChunk, "uLight"), 2);
|
||||
glUniformMatrix4fv(g_uMvpChunk, 1, 0, g_mvp);
|
||||
glUseProgram(g_progEntity);
|
||||
g_uMvpEntity = glGetUniformLocation(g_progEntity, "uMvp");
|
||||
glUniform1i(glGetUniformLocation(g_progEntity, "uTex"), 0);
|
||||
glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp);
|
||||
glUseProgram(g_progChunk);
|
||||
|
||||
/* shared quad index buffer, like Blaze3D's RenderSystem shared sequences */
|
||||
int maxQuads = 4096;
|
||||
unsigned* idx = (unsigned*)malloc((size_t)maxQuads * 6 * 4);
|
||||
for (int q = 0; q < maxQuads; ++q) {
|
||||
unsigned base = q * 4;
|
||||
unsigned* p = idx + q * 6;
|
||||
p[0] = base; p[1] = base + 1; p[2] = base + 2;
|
||||
p[3] = base + 2; p[4] = base + 3; p[5] = base;
|
||||
}
|
||||
glGenBuffers(1, &g_sharedIbo);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_sharedIbo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, maxQuads * 6 * 4, idx, GL_STATIC_DRAW);
|
||||
free(idx);
|
||||
|
||||
g_scratch = (unsigned char*)malloc(4 * 1024 * 1024);
|
||||
memset(g_scratch, 0x5a, 4 * 1024 * 1024);
|
||||
|
||||
glGenVertexArrays(MAX_SECTIONS, g_vao);
|
||||
glGenBuffers(MAX_SECTIONS, g_vbo);
|
||||
int bytes = g_quadsPerSection * 4 * VERT_STRIDE;
|
||||
for (int i = 0; i < MAX_SECTIONS; ++i) {
|
||||
fill_section_vertices(g_scratch, g_quadsPerSection, i * 7919u + 1);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, g_vbo[i]);
|
||||
glBufferData(GL_ARRAY_BUFFER, bytes, g_scratch, GL_STATIC_DRAW);
|
||||
setup_vao(g_vao[i], g_vbo[i], g_sharedIbo);
|
||||
}
|
||||
|
||||
glGenTextures(1, &g_texAtlas);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1024, 512, 0, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
|
||||
glGenTextures(1, &g_texLight);
|
||||
glActiveTexture(GL_TEXTURE0 + 2);
|
||||
glBindTexture(GL_TEXTURE_2D, g_texLight);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
|
||||
glGenTextures(1, &g_texEntity);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, g_texEntity);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||
|
||||
// Uniform ring the 26.2-style case sub-ranges into, sized like a real
|
||||
// frame's worth of per-draw uniform slots.
|
||||
GLint align = 256;
|
||||
glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &align);
|
||||
g_uboAlign = align > 0 ? align : 256;
|
||||
g_uboSlot = (size_t)g_uboAlign;
|
||||
glGenBuffers(1, &g_uboRing);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, g_uboRing);
|
||||
glBufferData(GL_UNIFORM_BUFFER, 4 * 1024 * 1024, g_scratch, GL_DYNAMIC_DRAW);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
|
||||
glGenSamplers(1, &g_sampler);
|
||||
glSamplerParameteri(g_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glSamplerParameteri(g_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glClearColor(0.3f, 0.5f, 0.9f, 1.0f);
|
||||
glViewport(0, 0, 1280, 720);
|
||||
const GLenum setupError = glGetError();
|
||||
if (setupError != GL_NO_ERROR) {
|
||||
char message[64];
|
||||
snprintf(message, sizeof message, "0x%04x", setupError);
|
||||
bench_gl_failed("GL error during resource setup", message);
|
||||
}
|
||||
}
|
||||
|
||||
static void case_draw_tiny(int frame, long a, long b) {
|
||||
(void)frame; (void)b;
|
||||
glBindVertexArray(g_vao[0]);
|
||||
for (long i = 0; i < a; ++i) glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
|
||||
static void case_draw_uniform(int frame, long a, long b) {
|
||||
(void)frame; (void)b;
|
||||
glBindVertexArray(g_vao[0]);
|
||||
for (long i = 0; i < a; ++i) {
|
||||
glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f);
|
||||
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void case_draw_multi_vao(int frame, long a, long b) {
|
||||
(void)frame; (void)b;
|
||||
for (long i = 0; i < a; ++i) {
|
||||
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
|
||||
glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f);
|
||||
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void case_tex_pingpong(int frame, long a, long b) {
|
||||
(void)frame; (void)b;
|
||||
glBindVertexArray(g_vao[0]);
|
||||
for (long i = 0; i < a; ++i) {
|
||||
glBindTexture(GL_TEXTURE_2D, (i & 1) ? g_texEntity : g_texAtlas);
|
||||
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||
}
|
||||
|
||||
static void case_program_pingpong(int frame, long a, long b) {
|
||||
(void)frame; (void)b;
|
||||
glBindVertexArray(g_vao[0]);
|
||||
for (long i = 0; i < a; ++i) {
|
||||
if (i & 1) {
|
||||
glUseProgram(g_progEntity);
|
||||
glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp);
|
||||
} else {
|
||||
glUseProgram(g_progChunk);
|
||||
glUniform3f(g_uOffsetChunk, (float)(i & 15), 0.0f, 0.0f);
|
||||
}
|
||||
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
glUseProgram(g_progChunk);
|
||||
}
|
||||
|
||||
/* a = uploads per frame, b = bytes per upload (0 => section size) */
|
||||
static void case_chunk_upload(int frame, long a, long b) {
|
||||
if (b <= 0) b = g_quadsPerSection * 4 * VERT_STRIDE;
|
||||
if (b > 4 * 1024 * 1024) b = 4 * 1024 * 1024;
|
||||
for (long i = 0; i < a; ++i) {
|
||||
int slot = (int)(((long)frame * a + i) % MAX_SECTIONS);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, g_vbo[slot]);
|
||||
glBufferData(GL_ARRAY_BUFFER, b, NULL, GL_STATIC_DRAW); /* orphan */
|
||||
glBufferSubData(GL_ARRAY_BUFFER, 0, b, g_scratch);
|
||||
glBindVertexArray(g_vao[slot]);
|
||||
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* a = sprite updates per frame */
|
||||
static void case_atlas_sprite(int frame, long a, long b) {
|
||||
(void)b;
|
||||
glBindVertexArray(g_vao[0]);
|
||||
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||
for (long i = 0; i < a; ++i) {
|
||||
int x = (int)((frame * 13 + i * 17) % (1024 - 16));
|
||||
int y = (int)((frame * 7 + i * 29) % (512 - 16));
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||
}
|
||||
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
|
||||
/* a = lightmap updates (+draw) per frame */
|
||||
static void case_lightmap(int frame, long a, long b) {
|
||||
(void)frame; (void)b;
|
||||
glBindVertexArray(g_vao[0]);
|
||||
for (long i = 0; i < a; ++i) {
|
||||
glActiveTexture(GL_TEXTURE0 + 2);
|
||||
glBindTexture(GL_TEXTURE_2D, g_texLight);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Composite: a = total draws, b = uploads per frame. Mix modeled on trace
|
||||
* analysis: chunk draws with per-draw offset uniform across sections, 10%
|
||||
* entity-style program flips, per-frame lightmap + sprite updates, b chunk
|
||||
* re-uploads. */
|
||||
static long g_mixSprites = 8;
|
||||
static void case_scene_mix(int frame, long a, long b) {
|
||||
glActiveTexture(GL_TEXTURE0 + 2);
|
||||
glBindTexture(GL_TEXTURE_2D, g_texLight);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||
for (long i = 0; i < g_mixSprites; ++i) {
|
||||
int x = (int)((frame * 13 + i * 17) % (1024 - 16));
|
||||
int y = (int)((frame * 7 + i * 29) % (512 - 16));
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||
}
|
||||
for (long i = 0; i < b; ++i) {
|
||||
int slot = (int)(((long)frame * b + i) % MAX_SECTIONS);
|
||||
long bytes = g_quadsPerSection * 4 * VERT_STRIDE;
|
||||
glBindBuffer(GL_ARRAY_BUFFER, g_vbo[slot]);
|
||||
glBufferData(GL_ARRAY_BUFFER, bytes, NULL, GL_STATIC_DRAW);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, 0, bytes, g_scratch);
|
||||
}
|
||||
long entityEvery = 10;
|
||||
for (long i = 0; i < a; ++i) {
|
||||
if (i % entityEvery == entityEvery - 1) {
|
||||
glUseProgram(g_progEntity);
|
||||
glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp);
|
||||
glBindTexture(GL_TEXTURE_2D, g_texEntity);
|
||||
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
|
||||
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||
glUseProgram(g_progChunk);
|
||||
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||
} else {
|
||||
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
|
||||
glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f);
|
||||
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Trace-derived cases -------------------------------------------------
|
||||
* Per-frame call mixes measured from the three captured Minecraft traces
|
||||
* (render distance 32, 1280x720, hovering in-world). Each case reproduces one
|
||||
* renderer's dominant per-draw sequence at its measured rate, so the number a
|
||||
* backend posts here is directly comparable to what that game version asks of
|
||||
* the driver every frame.
|
||||
*
|
||||
* vanilla 1.21.1 : 5495 glDrawElements, 5490 glBindVertexArray,
|
||||
* 5487 glUniform3fv, 95 glTexSubImage2D (+382 glPixelStorei,
|
||||
* 247 glTexParameteri), 23 glBufferData per frame
|
||||
* fabric+sodium : 132 glMultiDrawElementsBaseVertex, 279 glBindVertexArray,
|
||||
* 132 glUniform3f, 32 glBufferData per frame
|
||||
* 26.2 snapshot : 3401 glDrawElementsBaseVertex, each preceded by
|
||||
* glBindBufferRange + glBindBuffer (3639/3412 per frame)
|
||||
*/
|
||||
/* vanilla: bind VAO, push the chunk offset, draw. a = draws per frame. */
|
||||
static void case_mc_vanilla_draw(int frame, long a, long b) {
|
||||
(void)frame; (void)b;
|
||||
float offset[3];
|
||||
for (long i = 0; i < a; ++i) {
|
||||
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
|
||||
offset[0] = (float)(i & 15);
|
||||
offset[1] = (float)((i >> 4) & 15);
|
||||
offset[2] = 0.0f;
|
||||
glUniform3fv(g_uOffsetChunk, 1, offset);
|
||||
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* sodium: one multi-draw covers many chunk sections out of a shared buffer.
|
||||
* a = multi-draws per frame, b = sub-draws inside each. */
|
||||
static void case_mc_sodium_multidraw(int frame, long a, long b) {
|
||||
(void)frame;
|
||||
enum { kMaxSub = 64 };
|
||||
if (b <= 0 || b > kMaxSub) b = 32;
|
||||
GLsizei counts[kMaxSub];
|
||||
const void* offsets[kMaxSub];
|
||||
GLint baseVertices[kMaxSub];
|
||||
for (long s = 0; s < b; ++s) {
|
||||
counts[s] = (GLsizei)(g_quadsPerSection * 6 / b);
|
||||
offsets[s] = (const void*)(uintptr_t)(s * (g_quadsPerSection * 6 / b) * 4);
|
||||
baseVertices[s] = 0;
|
||||
}
|
||||
for (long i = 0; i < a; ++i) {
|
||||
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
|
||||
glBindVertexArray(g_vao[i % MAX_SECTIONS]); /* sodium rebinds ~2x per draw */
|
||||
glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f);
|
||||
// Routed through the includer: GLES has no multi-draw-with-base-vertex, so
|
||||
// a native-driver harness emulates it with the loop the extension folds up.
|
||||
bench_multi_draw_elements_base_vertex(GL_TRIANGLES, counts, GL_UNSIGNED_INT, offsets,
|
||||
(GLsizei)b, baseVertices);
|
||||
}
|
||||
}
|
||||
|
||||
/* 26.2: every draw rebinds a fresh uniform-buffer range out of a ring.
|
||||
* a = draws per frame. */
|
||||
static void case_mc_ubo_range(int frame, long a, long b) {
|
||||
(void)b;
|
||||
const size_t slots = (4u * 1024u * 1024u) / g_uboSlot;
|
||||
for (long i = 0; i < a; ++i) {
|
||||
const size_t slot = (size_t)(((long)frame * a + i) % (long)slots);
|
||||
glBindBufferRange(GL_UNIFORM_BUFFER, 0, g_uboRing, (GLintptr)(slot * g_uboSlot),
|
||||
(GLsizeiptr)g_uboSlot);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, g_uboRing);
|
||||
glDrawElementsBaseVertex(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* vanilla's animated-sprite path: every upload is wrapped in the pixel-store
|
||||
* and filter state Blaze3D re-sets around it. a = uploads per frame. */
|
||||
static void case_mc_tex_stream(int frame, long a, long b) {
|
||||
(void)b;
|
||||
glBindVertexArray(g_vao[0]);
|
||||
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||
for (long i = 0; i < a; ++i) {
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
|
||||
glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
int x = (int)((frame * 13 + i * 17) % (1024 - 16));
|
||||
int y = (int)((frame * 7 + i * 29) % (512 - 16));
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||
}
|
||||
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
|
||||
/* Blaze3D re-resolves uniform locations by name every frame. a = lookups. */
|
||||
static void case_mc_uniform_lookup(int frame, long a, long b) {
|
||||
(void)frame; (void)b;
|
||||
static const char* names[4] = {"uMvp", "uOffset", "uAtlas", "uLight"};
|
||||
volatile GLint sink = 0;
|
||||
for (long i = 0; i < a; ++i) sink += glGetUniformLocation(g_progChunk, names[i & 3]);
|
||||
(void)sink;
|
||||
glBindVertexArray(g_vao[0]);
|
||||
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
|
||||
/* 26.2 rebinds a sampler object per texture unit switch. a = switches. */
|
||||
static void case_mc_sampler_churn(int frame, long a, long b) {
|
||||
(void)frame; (void)b;
|
||||
glBindVertexArray(g_vao[0]);
|
||||
for (long i = 0; i < a; ++i) {
|
||||
glActiveTexture(GL_TEXTURE0 + (GLenum)(i & 3));
|
||||
glBindTexture(GL_TEXTURE_2D, (i & 1) ? g_texEntity : g_texAtlas);
|
||||
glBindSampler((GLuint)(i & 3), g_sampler);
|
||||
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
}
|
||||
|
||||
|
||||
/* ---- the case table both harnesses iterate --------------------------------
|
||||
* a/b are the case's own knobs; opsPerFrame is what one bench frame is
|
||||
* normalised by, so ns_per_op compares across renderers. The mc_* rates are
|
||||
* the per-frame call counts measured from the captured traces.
|
||||
*/
|
||||
typedef void (*bench_case_fn)(int frame, long a, long b);
|
||||
|
||||
typedef struct {
|
||||
const char* name;
|
||||
bench_case_fn fn;
|
||||
long a, b, opsPerFrame;
|
||||
} BenchCaseDesc;
|
||||
|
||||
static const BenchCaseDesc kBenchCases[] = {
|
||||
{"mc_vanilla_draw", case_mc_vanilla_draw, 5495, 0, 5495},
|
||||
{"mc_sodium_multidraw", case_mc_sodium_multidraw, 132, 32, 132},
|
||||
{"mc_ubo_range", case_mc_ubo_range, 3401, 0, 3401},
|
||||
{"mc_tex_stream", case_mc_tex_stream, 95, 0, 95},
|
||||
{"mc_uniform_lookup", case_mc_uniform_lookup, 41, 0, 41},
|
||||
{"mc_sampler_churn", case_mc_sampler_churn, 306, 0, 306},
|
||||
{"draw_tiny", case_draw_tiny, 2048, 0, 2048},
|
||||
{"draw_uniform", case_draw_uniform, 2048, 0, 2048},
|
||||
{"draw_multi_vao", case_draw_multi_vao, 2048, 0, 2048},
|
||||
{"tex_pingpong", case_tex_pingpong, 1024, 0, 1024},
|
||||
{"program_pingpong", case_program_pingpong, 512, 0, 512},
|
||||
{"chunk_upload", case_chunk_upload, 24, 0, 24},
|
||||
{"atlas_sprite", case_atlas_sprite, 32, 0, 32},
|
||||
{"lightmap", case_lightmap, 4, 0, 4},
|
||||
{"scene_mix", case_scene_mix, 2048, 12, 2048},
|
||||
};
|
||||
static const int kBenchCaseCount = (int)(sizeof kBenchCases / sizeof kBenchCases[0]);
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# Run the headless EGL DriverBench on one renderer:
|
||||
# ./run_driver_bench.sh native [bench args...]
|
||||
# ./run_driver_bench.sh espryt <libMobileGL.so> [bench args...]
|
||||
# ./run_driver_bench.sh magma <libMobileGL.so> [bench args...]
|
||||
# The bench dlopens exactly one EGL provider (DRIVERBENCH_EGL_LIB): the system
|
||||
# libEGL.so.1 for native, or the given libMobileGL.so for a MobileGL backend -
|
||||
# no LD_LIBRARY_PATH shadowing, so MobileGL's own loader still finds the real
|
||||
# driver underneath.
|
||||
#
|
||||
# Pin the vendor libraries explicitly. A bare libEGL.so.1 on a glvnd system
|
||||
# picks whatever vendor eglGetDisplay(EGL_DEFAULT_DISPLAY) resolves first,
|
||||
# which is Mesa/llvmpipe here - a software rasteriser silently replacing the
|
||||
# GPU under a benchmark. Override MGL_EGL_VENDOR / MGL_VK_ICD to test another
|
||||
# driver.
|
||||
set -eu
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
BENCH=${DRIVERBENCH_BIN:-$HERE/DriverBench}
|
||||
EGL_VENDOR=${MGL_EGL_VENDOR:-/usr/share/glvnd/egl_vendor.d/10_nvidia.json}
|
||||
VK_ICD=${MGL_VK_ICD:-/usr/share/vulkan/icd.d/nvidia_icd.x86_64.json}
|
||||
MODE=$1; shift
|
||||
|
||||
export __EGL_VENDOR_LIBRARY_FILENAMES=$EGL_VENDOR
|
||||
export EGL_PLATFORM=${EGL_PLATFORM:-x11}
|
||||
|
||||
case "$MODE" in
|
||||
native)
|
||||
export DRIVERBENCH_EGL_LIB=${DRIVERBENCH_EGL_LIB:-libEGL.so.1}
|
||||
;;
|
||||
espryt)
|
||||
export DRIVERBENCH_EGL_LIB=$(readlink -f "$1"); shift
|
||||
export MOBILEGL_BACKEND_TYPE=DirectGLES
|
||||
;;
|
||||
magma)
|
||||
export DRIVERBENCH_EGL_LIB=$(readlink -f "$1"); shift
|
||||
export MOBILEGL_BACKEND_TYPE=DirectVulkan
|
||||
export VK_ICD_FILENAMES=$VK_ICD
|
||||
;;
|
||||
*) echo "unknown mode: $MODE (native|espryt|magma)"; exit 1 ;;
|
||||
esac
|
||||
exec "$BENCH" "$@"
|
||||
@@ -1073,6 +1073,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
MGLOG_D("%s: program = %d, location = %d, byteOffset = %d", __func__, programObject.GetExternalIndex(),
|
||||
location, offset + byteOffsetInsideUniform);
|
||||
// Apps re-set identical uniform values constantly (Minecraft re-uploads the same
|
||||
// matrices and sampler indices every frame), and any content-version move makes both
|
||||
// backends re-upload the whole UBO on the next draw. Every glUniform entry point
|
||||
// funnels its final bytes through here - after any transpose/stride conversion, with
|
||||
// the exact destination range known - and the scratch is zero-filled at link (matching
|
||||
// the GL zero defaults), so a bytes-equal write can be dropped without moving the
|
||||
// version.
|
||||
if (std::memcmp(pUBO + offset + byteOffsetInsideUniform, value, writeSize) == 0) return;
|
||||
Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize);
|
||||
programObject.MarkUBOContentDirty();
|
||||
} else {
|
||||
|
||||
@@ -1406,7 +1406,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
free(processedPixels);
|
||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
|
||||
textureMipmapObject->MarkStorageDirtyRegion(textureUploadTarget, level, {xoffset, yoffset, zoffset},
|
||||
{width, height, depth});
|
||||
MaybeAutoGenerateMipmap(target, textureObject, false, level);
|
||||
}
|
||||
|
||||
@@ -1525,7 +1526,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
free(processedPixels);
|
||||
|
||||
MGLOG_D("%s: mark mip %d as dirty", __func__, level);
|
||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
|
||||
textureMipmapObject->MarkStorageDirtyRegion(textureUploadTarget, level, {xoffset, yoffset, 0},
|
||||
{width, height, 1});
|
||||
MaybeAutoGenerateMipmap(target, textureObject, false, level);
|
||||
}
|
||||
|
||||
@@ -1591,7 +1593,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
free(processedPixels);
|
||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
|
||||
textureMipmapObject->MarkStorageDirtyRegion(textureUploadTarget, level, {xoffset, 0, 0}, {width, 1, 1});
|
||||
MaybeAutoGenerateMipmap(target, textureObject, false, level);
|
||||
}
|
||||
|
||||
@@ -3536,8 +3538,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (texture == 0) {
|
||||
auto& currentUnit = MG_State::pGLContext->GetTextureUnitObject(activeUnit);
|
||||
auto& bindingSlot = currentUnit.GetBindingSlot(textureTarget);
|
||||
bindingSlot.Bind(MG_State::pGLContext->GetDefaultTextureObject(textureTarget));
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(activeUnit);
|
||||
const Bool changed = bindingSlot.Bind(MG_State::pGLContext->GetDefaultTextureObject(textureTarget));
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(activeUnit, changed);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3571,8 +3573,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// ======================= Processing ================================
|
||||
auto& currentUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||
auto& bindingSlot = currentUnit.GetBindingSlot(textureTarget);
|
||||
bindingSlot.Bind(textureObject);
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(MG_State::pGLContext->GetActiveTextureUnit());
|
||||
const Bool changed = bindingSlot.Bind(textureObject);
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(MG_State::pGLContext->GetActiveTextureUnit(), changed);
|
||||
}
|
||||
|
||||
void ActiveTexture_State(GLenum texture) {
|
||||
@@ -4410,13 +4412,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(static_cast<Int>(unit));
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit));
|
||||
if (texture == 0) {
|
||||
// GL 4.5 8.1: texture zero unbinds every target of the unit, i.e. rebinds each
|
||||
// target's default texture object (the unit's initial state).
|
||||
Bool changed = false;
|
||||
for (auto& slot : textureUnit.GetAllBindingSlots()) {
|
||||
slot.Bind(MG_State::pGLContext->GetDefaultTextureObject(slot.GetTarget()));
|
||||
if (slot.Bind(MG_State::pGLContext->GetDefaultTextureObject(slot.GetTarget()))) changed = true;
|
||||
}
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit), changed);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4427,7 +4430,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture object does not exist."));
|
||||
return;
|
||||
}
|
||||
textureUnit.GetBindingSlot(textureObject->GetTarget()).Bind(textureObject);
|
||||
const Bool changed = textureUnit.GetBindingSlot(textureObject->GetTarget()).Bind(textureObject);
|
||||
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit), changed);
|
||||
}
|
||||
|
||||
void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) {
|
||||
|
||||
@@ -353,7 +353,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateTextureObject(SharedPtr<MG_State::GLState::ITextureObject> textureObject) {
|
||||
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
|
||||
if (!textureObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -376,7 +376,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject,
|
||||
Bool ValidateTextureTargetUniformity(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureTarget target) {
|
||||
if (!textureObject) return true; // should be created later
|
||||
TextureTarget prevTarget = textureObject->GetTarget();
|
||||
@@ -390,7 +390,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateTextureSubImageOffsets(SharedPtr<MG_State::GLState::ITextureObject> textureObject, Int xoffset,
|
||||
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
|
||||
Int width, Int yoffset, Int height, Int zoffset, Int depth) {
|
||||
auto baseSize = textureObject->GetBaseSize();
|
||||
if (xoffset < 0 || (xoffset + width) > baseSize.x()) {
|
||||
|
||||
@@ -30,15 +30,15 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
TextureInternalFormat internalFormat,
|
||||
TexturePixelDataType type);
|
||||
Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level);
|
||||
Bool ValidateTextureObject(SharedPtr<MG_State::GLState::ITextureObject> textureObject);
|
||||
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject);
|
||||
// Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry
|
||||
// points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION
|
||||
// error is generated if zero is bound to target", ARB_texture_storage).
|
||||
Bool ValidateTextureNotDefault(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
const char* caller);
|
||||
Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject,
|
||||
Bool ValidateTextureTargetUniformity(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureTarget target);
|
||||
Bool ValidateTextureSubImageOffsets(SharedPtr<MG_State::GLState::ITextureObject> textureObject, Int xoffset,
|
||||
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
|
||||
Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0);
|
||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2);
|
||||
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
||||
|
||||
@@ -15,4 +15,257 @@ MOBILEGL_GLX_API void* glXGetProcAddress(const char* name) {
|
||||
|
||||
MOBILEGL_GLX_API void* glXGetProcAddressARB(const char* name) {
|
||||
return MG_Impl::GLXImpl::GetProcAddressARB(name);
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
#include "../GLXImpl.h"
|
||||
|
||||
namespace GLXImpl = MobileGL::MG_Impl::GLXImpl;
|
||||
|
||||
// GLX handle/type spellings from GL/glx.h, expressed without including it:
|
||||
// GLXContext/GLXFBConfig are opaque pointers, drawables are XIDs, Bool is int,
|
||||
// and XVisualInfo* crosses as void*.
|
||||
|
||||
MOBILEGL_GLX_API int glXQueryExtension(Display* dpy, int* errorBase, int* eventBase) {
|
||||
return GLXImpl::QueryExtension(dpy, errorBase, eventBase);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API int glXQueryVersion(Display* dpy, int* major, int* minor) {
|
||||
return GLXImpl::QueryVersion(dpy, major, minor);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API const char* glXQueryExtensionsString(Display* dpy, int screen) {
|
||||
return GLXImpl::QueryExtensionsString(dpy, screen);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API const char* glXGetClientString(Display* dpy, int name) {
|
||||
return GLXImpl::GetClientString(dpy, name);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API const char* glXQueryServerString(Display* dpy, int screen, int name) {
|
||||
return GLXImpl::QueryServerString(dpy, screen, name);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void** glXGetFBConfigs(Display* dpy, int screen, int* nelements) {
|
||||
return GLXImpl::GetFBConfigs(dpy, screen, nelements);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void** glXChooseFBConfig(Display* dpy, int screen, const int* attribList,
|
||||
int* nelements) {
|
||||
return GLXImpl::ChooseFBConfig(dpy, screen, attribList, nelements);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API int glXGetFBConfigAttrib(Display* dpy, void* config, int attribute, int* value) {
|
||||
return GLXImpl::GetFBConfigAttrib(dpy, config, attribute, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void* glXGetVisualFromFBConfig(Display* dpy, void* config) {
|
||||
return GLXImpl::GetVisualFromFBConfig(dpy, config);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void* glXChooseVisual(Display* dpy, int screen, int* attribList) {
|
||||
return GLXImpl::ChooseVisual(dpy, screen, attribList);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API int glXGetConfig(Display* dpy, void* visualInfo, int attribute, int* value) {
|
||||
return GLXImpl::GetConfig(dpy, visualInfo, attribute, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void* glXCreateContext(Display* dpy, void* visualInfo, void* shareList, int direct) {
|
||||
return GLXImpl::CreateContext(dpy, visualInfo, shareList, direct);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void* glXCreateNewContext(Display* dpy, void* config, int renderType,
|
||||
void* shareList, int direct) {
|
||||
return GLXImpl::CreateNewContext(dpy, config, renderType, shareList, direct);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void* glXCreateContextAttribsARB(Display* dpy, void* config, void* shareContext,
|
||||
int direct, const int* attribList) {
|
||||
return GLXImpl::CreateContextAttribsARB(dpy, config, shareContext, direct, attribList);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXDestroyContext(Display* dpy, void* context) {
|
||||
GLXImpl::DestroyContext(dpy, context);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API int glXMakeCurrent(Display* dpy, unsigned long drawable, void* context) {
|
||||
return GLXImpl::MakeCurrent(dpy, drawable, context);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API int glXMakeContextCurrent(Display* dpy, unsigned long draw, unsigned long read,
|
||||
void* context) {
|
||||
return GLXImpl::MakeContextCurrent(dpy, draw, read, context);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXSwapBuffers(Display* dpy, unsigned long drawable) {
|
||||
GLXImpl::SwapBuffers(dpy, drawable);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API unsigned long glXCreateWindow(Display* dpy, void* config, unsigned long window,
|
||||
const int* attribList) {
|
||||
return GLXImpl::CreateWindow(dpy, config, window, attribList);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXDestroyWindow(Display* dpy, unsigned long window) {
|
||||
GLXImpl::DestroyWindow(dpy, window);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void* glXGetCurrentContext() {
|
||||
return GLXImpl::GetCurrentContext();
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API unsigned long glXGetCurrentDrawable() {
|
||||
return GLXImpl::GetCurrentDrawable();
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API unsigned long glXGetCurrentReadDrawable() {
|
||||
return GLXImpl::GetCurrentReadDrawable();
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API Display* glXGetCurrentDisplay() {
|
||||
return GLXImpl::GetCurrentDisplay();
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API int glXIsDirect(Display* dpy, void* context) {
|
||||
return GLXImpl::IsDirect(dpy, context);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXWaitGL() {
|
||||
GLXImpl::WaitGL();
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXWaitX() {
|
||||
GLXImpl::WaitX();
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API int glXQueryContext(Display* dpy, void* context, int attribute, int* value) {
|
||||
return GLXImpl::QueryContext(dpy, context, attribute, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXQueryDrawable(Display* dpy, unsigned long drawable, int attribute,
|
||||
unsigned int* value) {
|
||||
GLXImpl::QueryDrawable(dpy, drawable, attribute, value);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXSwapIntervalEXT(Display* dpy, unsigned long drawable, int interval) {
|
||||
GLXImpl::SwapIntervalEXT(dpy, drawable, interval);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API int glXSwapIntervalMESA(unsigned int interval) {
|
||||
return GLXImpl::SwapIntervalMESA(interval);
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API int glXGetSwapIntervalMESA() {
|
||||
return GLXImpl::GetSwapIntervalMESA();
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API int glXSwapIntervalSGI(int interval) {
|
||||
return GLXImpl::SwapIntervalSGI(interval);
|
||||
}
|
||||
|
||||
// Legacy entry points some loaders probe for; harmless no-op stubs.
|
||||
MOBILEGL_GLX_API void glXCopyContext(Display*, void*, void*, unsigned long) {
|
||||
MGLOG_W("glx: glXCopyContext is not supported");
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API unsigned long glXCreateGLXPixmap(Display*, void*, unsigned long) {
|
||||
MGLOG_W("glx: glXCreateGLXPixmap is not supported");
|
||||
return 0;
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXDestroyGLXPixmap(Display*, unsigned long) {}
|
||||
|
||||
MOBILEGL_GLX_API unsigned long glXCreatePixmap(Display*, void*, unsigned long, const int*) {
|
||||
MGLOG_W("glx: glXCreatePixmap is not supported");
|
||||
return 0;
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXDestroyPixmap(Display*, unsigned long) {}
|
||||
|
||||
MOBILEGL_GLX_API unsigned long glXCreatePbuffer(Display*, void*, const int*) {
|
||||
MGLOG_W("glx: glXCreatePbuffer is not supported");
|
||||
return 0;
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXDestroyPbuffer(Display*, unsigned long) {}
|
||||
|
||||
MOBILEGL_GLX_API void glXUseXFont(unsigned long, int, int, int) {
|
||||
MGLOG_W("glx: glXUseXFont is not supported");
|
||||
}
|
||||
|
||||
MOBILEGL_GLX_API void glXSelectEvent(Display*, unsigned long, unsigned long) {}
|
||||
|
||||
MOBILEGL_GLX_API void glXGetSelectedEvent(Display*, unsigned long, unsigned long* eventMask) {
|
||||
if (eventMask) {
|
||||
*eventMask = 0;
|
||||
}
|
||||
}
|
||||
|
||||
namespace MobileGL::MG_Impl::GLXImpl {
|
||||
namespace {
|
||||
struct GLXEntryPoint {
|
||||
const char* Name;
|
||||
void* Proc;
|
||||
};
|
||||
|
||||
const GLXEntryPoint kGLXEntryPoints[] = {
|
||||
{"glXChooseFBConfig", reinterpret_cast<void*>(glXChooseFBConfig)},
|
||||
{"glXChooseVisual", reinterpret_cast<void*>(glXChooseVisual)},
|
||||
{"glXCopyContext", reinterpret_cast<void*>(glXCopyContext)},
|
||||
{"glXCreateContext", reinterpret_cast<void*>(glXCreateContext)},
|
||||
{"glXCreateContextAttribsARB", reinterpret_cast<void*>(glXCreateContextAttribsARB)},
|
||||
{"glXCreateGLXPixmap", reinterpret_cast<void*>(glXCreateGLXPixmap)},
|
||||
{"glXCreateNewContext", reinterpret_cast<void*>(glXCreateNewContext)},
|
||||
{"glXCreatePbuffer", reinterpret_cast<void*>(glXCreatePbuffer)},
|
||||
{"glXCreatePixmap", reinterpret_cast<void*>(glXCreatePixmap)},
|
||||
{"glXCreateWindow", reinterpret_cast<void*>(glXCreateWindow)},
|
||||
{"glXDestroyContext", reinterpret_cast<void*>(glXDestroyContext)},
|
||||
{"glXDestroyGLXPixmap", reinterpret_cast<void*>(glXDestroyGLXPixmap)},
|
||||
{"glXDestroyPbuffer", reinterpret_cast<void*>(glXDestroyPbuffer)},
|
||||
{"glXDestroyPixmap", reinterpret_cast<void*>(glXDestroyPixmap)},
|
||||
{"glXDestroyWindow", reinterpret_cast<void*>(glXDestroyWindow)},
|
||||
{"glXGetClientString", reinterpret_cast<void*>(glXGetClientString)},
|
||||
{"glXGetConfig", reinterpret_cast<void*>(glXGetConfig)},
|
||||
{"glXGetCurrentContext", reinterpret_cast<void*>(glXGetCurrentContext)},
|
||||
{"glXGetCurrentDisplay", reinterpret_cast<void*>(glXGetCurrentDisplay)},
|
||||
{"glXGetCurrentDrawable", reinterpret_cast<void*>(glXGetCurrentDrawable)},
|
||||
{"glXGetCurrentReadDrawable", reinterpret_cast<void*>(glXGetCurrentReadDrawable)},
|
||||
{"glXGetFBConfigAttrib", reinterpret_cast<void*>(glXGetFBConfigAttrib)},
|
||||
{"glXGetFBConfigs", reinterpret_cast<void*>(glXGetFBConfigs)},
|
||||
{"glXGetProcAddress", reinterpret_cast<void*>(glXGetProcAddress)},
|
||||
{"glXGetProcAddressARB", reinterpret_cast<void*>(glXGetProcAddressARB)},
|
||||
{"glXGetSelectedEvent", reinterpret_cast<void*>(glXGetSelectedEvent)},
|
||||
{"glXGetSwapIntervalMESA", reinterpret_cast<void*>(glXGetSwapIntervalMESA)},
|
||||
{"glXGetVisualFromFBConfig", reinterpret_cast<void*>(glXGetVisualFromFBConfig)},
|
||||
{"glXIsDirect", reinterpret_cast<void*>(glXIsDirect)},
|
||||
{"glXMakeContextCurrent", reinterpret_cast<void*>(glXMakeContextCurrent)},
|
||||
{"glXMakeCurrent", reinterpret_cast<void*>(glXMakeCurrent)},
|
||||
{"glXQueryContext", reinterpret_cast<void*>(glXQueryContext)},
|
||||
{"glXQueryDrawable", reinterpret_cast<void*>(glXQueryDrawable)},
|
||||
{"glXQueryExtension", reinterpret_cast<void*>(glXQueryExtension)},
|
||||
{"glXQueryExtensionsString", reinterpret_cast<void*>(glXQueryExtensionsString)},
|
||||
{"glXQueryServerString", reinterpret_cast<void*>(glXQueryServerString)},
|
||||
{"glXQueryVersion", reinterpret_cast<void*>(glXQueryVersion)},
|
||||
{"glXSelectEvent", reinterpret_cast<void*>(glXSelectEvent)},
|
||||
{"glXSwapBuffers", reinterpret_cast<void*>(glXSwapBuffers)},
|
||||
{"glXSwapIntervalEXT", reinterpret_cast<void*>(glXSwapIntervalEXT)},
|
||||
{"glXSwapIntervalMESA", reinterpret_cast<void*>(glXSwapIntervalMESA)},
|
||||
{"glXSwapIntervalSGI", reinterpret_cast<void*>(glXSwapIntervalSGI)},
|
||||
{"glXUseXFont", reinterpret_cast<void*>(glXUseXFont)},
|
||||
{"glXWaitGL", reinterpret_cast<void*>(glXWaitGL)},
|
||||
{"glXWaitX", reinterpret_cast<void*>(glXWaitX)},
|
||||
};
|
||||
} // namespace
|
||||
|
||||
void* GetGLXEntryPoint(const char* name) {
|
||||
for (const auto& entry : kGLXEntryPoints) {
|
||||
if (std::strcmp(entry.Name, name) == 0) {
|
||||
return entry.Proc;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::GLXImpl
|
||||
|
||||
#endif // __linux__ && !__ANDROID__
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
||||
// MobileGL - MobileGL/MG_Impl/GLXImpl/GLXImpl.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
|
||||
namespace MobileGL::MG_Impl::GLXImpl {
|
||||
// GLX layered on MobileGL's own EGL, mirroring WGLImpl/CGLImpl. Handles are
|
||||
// opaque to callers; XVisualInfo crosses the ABI as void* so this header
|
||||
// needs no Xlib includes (Includes.h forward-declares Display/XID/Window).
|
||||
using GLXFBConfigHandle = void*;
|
||||
using GLXContextHandle = void*;
|
||||
using GLXDrawableHandle = unsigned long; // XID
|
||||
|
||||
int QueryExtension(Display* dpy, int* errorBase, int* eventBase);
|
||||
int QueryVersion(Display* dpy, int* major, int* minor);
|
||||
const char* QueryExtensionsString(Display* dpy, int screen);
|
||||
const char* GetClientString(Display* dpy, int name);
|
||||
const char* QueryServerString(Display* dpy, int screen, int name);
|
||||
|
||||
GLXFBConfigHandle* GetFBConfigs(Display* dpy, int screen, int* nelements);
|
||||
GLXFBConfigHandle* ChooseFBConfig(Display* dpy, int screen, const int* attribList, int* nelements);
|
||||
int GetFBConfigAttrib(Display* dpy, GLXFBConfigHandle config, int attribute, int* value);
|
||||
void* GetVisualFromFBConfig(Display* dpy, GLXFBConfigHandle config);
|
||||
void* ChooseVisual(Display* dpy, int screen, int* attribList);
|
||||
int GetConfig(Display* dpy, void* visualInfo, int attribute, int* value);
|
||||
|
||||
GLXContextHandle CreateContext(Display* dpy, void* visualInfo, GLXContextHandle share, int direct);
|
||||
GLXContextHandle CreateNewContext(Display* dpy, GLXFBConfigHandle config, int renderType,
|
||||
GLXContextHandle share, int direct);
|
||||
GLXContextHandle CreateContextAttribsARB(Display* dpy, GLXFBConfigHandle config, GLXContextHandle share,
|
||||
int direct, const int* attribList);
|
||||
void DestroyContext(Display* dpy, GLXContextHandle context);
|
||||
int MakeCurrent(Display* dpy, GLXDrawableHandle drawable, GLXContextHandle context);
|
||||
int MakeContextCurrent(Display* dpy, GLXDrawableHandle draw, GLXDrawableHandle read,
|
||||
GLXContextHandle context);
|
||||
void SwapBuffers(Display* dpy, GLXDrawableHandle drawable);
|
||||
|
||||
GLXDrawableHandle CreateWindow(Display* dpy, GLXFBConfigHandle config, GLXDrawableHandle window,
|
||||
const int* attribList);
|
||||
void DestroyWindow(Display* dpy, GLXDrawableHandle window);
|
||||
|
||||
GLXContextHandle GetCurrentContext();
|
||||
GLXDrawableHandle GetCurrentDrawable();
|
||||
GLXDrawableHandle GetCurrentReadDrawable();
|
||||
Display* GetCurrentDisplay();
|
||||
int IsDirect(Display* dpy, GLXContextHandle context);
|
||||
void WaitGL();
|
||||
void WaitX();
|
||||
int QueryContext(Display* dpy, GLXContextHandle context, int attribute, int* value);
|
||||
void QueryDrawable(Display* dpy, GLXDrawableHandle drawable, int attribute, unsigned int* value);
|
||||
|
||||
void SwapIntervalEXT(Display* dpy, GLXDrawableHandle drawable, int interval);
|
||||
int SwapIntervalMESA(unsigned int interval);
|
||||
int GetSwapIntervalMESA();
|
||||
int SwapIntervalSGI(int interval);
|
||||
|
||||
// Name -> exported glX entry point (table lives with the exports).
|
||||
void* GetGLXEntryPoint(const char* name);
|
||||
} // namespace MobileGL::MG_Impl::GLXImpl
|
||||
|
||||
#endif // __linux__ && !__ANDROID__
|
||||
@@ -8,11 +8,27 @@
|
||||
|
||||
#include "LookUp.h"
|
||||
|
||||
namespace MG_Impl::GLXImpl {
|
||||
// TODO: implement complete GLX functionality
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
#include "../GLXImpl.h"
|
||||
#endif
|
||||
|
||||
namespace MG_Impl::GLXImpl {
|
||||
void* GetProcAddress(const char* name) {
|
||||
if (!name) {
|
||||
return nullptr;
|
||||
}
|
||||
MGLOG_D("glXGetProcAddress(\"%s\")", name);
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
if (name[0] == 'g' && name[1] == 'l' && name[2] == 'X') {
|
||||
// glX entry points resolve from the GLX layer's own table; GL/EGL
|
||||
// names fall through to the shared resolver below.
|
||||
void* proc = MobileGL::MG_Impl::GLXImpl::GetGLXEntryPoint(name);
|
||||
if (!proc) {
|
||||
MGLOG_D("glXGetProcAddress: unknown glX entry point %s", name);
|
||||
}
|
||||
return proc;
|
||||
}
|
||||
#endif
|
||||
void* proc = MobileGL::MG_Impl::GetProcAddress(name);
|
||||
if (!proc) {
|
||||
MGLOG_W("Failed to get function: %s", (const char*)name);
|
||||
@@ -25,4 +41,4 @@ namespace MG_Impl::GLXImpl {
|
||||
void* GetProcAddressARB(const char* name) {
|
||||
return GetProcAddress(name);
|
||||
}
|
||||
} // namespace MG_Impl::GLXImpl
|
||||
} // namespace MG_Impl::GLXImpl
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
void BufferObject::NotifySubData(SizeT offset, SizeT size) {
|
||||
++m_changeSerial;
|
||||
if (size == 0) return;
|
||||
m_hasDefinedContent = true;
|
||||
if (g_bufferBackendOps && g_bufferBackendOps->SubData) {
|
||||
g_bufferBackendOps->SubData(*this, offset, size);
|
||||
}
|
||||
@@ -49,12 +50,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
void BufferObject::NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess) {
|
||||
++m_changeSerial;
|
||||
if (range.start >= range.end) return;
|
||||
m_hasDefinedContent = true;
|
||||
if (g_bufferBackendOps && g_bufferBackendOps->FlushMappedRange) {
|
||||
g_bufferBackendOps->FlushMappedRange(*this, range, appAccess);
|
||||
}
|
||||
}
|
||||
|
||||
void BufferObject::NotifyContentWrite(SizeT offset, SizeT size) {
|
||||
m_hasDefinedContent = true;
|
||||
if (m_resource.IsGpuResident()) {
|
||||
// The write already landed in coherent GPU memory; the backend has no separate
|
||||
// copy to sync. Only bump the serial so cached transient slices invalidate.
|
||||
@@ -71,6 +74,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
if (data && size > 0) {
|
||||
Memcpy(m_resource.Bytes(), data, size);
|
||||
}
|
||||
// A NULL-data respecify (the orphaning idiom) leaves the store undefined;
|
||||
// record that so backends skip uploading the stale shadow bytes.
|
||||
m_hasDefinedContent = (data != nullptr) || size == 0;
|
||||
m_isImmutableStorage = false;
|
||||
m_storageFlags = 0;
|
||||
NotifyRespecify();
|
||||
@@ -89,6 +95,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
} else if (size > 0) {
|
||||
Memset(m_resource.Bytes(), 0, size);
|
||||
}
|
||||
m_hasDefinedContent = true;
|
||||
m_isImmutableStorage = true;
|
||||
m_storageFlags = storageFlags;
|
||||
NotifyRespecify();
|
||||
@@ -175,6 +182,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
void BufferObject::MarkGpuWritten() {
|
||||
m_hasDefinedContent = true;
|
||||
m_gpuWritePending = true;
|
||||
}
|
||||
|
||||
@@ -332,6 +340,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
return m_changeSerial;
|
||||
}
|
||||
|
||||
Bool BufferObject::HasDefinedContent() const {
|
||||
return m_hasDefinedContent;
|
||||
}
|
||||
|
||||
const SharedPtr<BackendBufferResource>& BufferObject::GetBackendResource() const {
|
||||
return m_resource.Backend();
|
||||
}
|
||||
|
||||
@@ -188,6 +188,11 @@ namespace MobileGL {
|
||||
// Monotonic counter bumped on every shadow mutation; backends use it to
|
||||
// validate cached transient slices.
|
||||
Uint64 GetChangeSerial() const;
|
||||
// False after a NULL-data (re)specification until the first content
|
||||
// write: the app's orphaning idiom (glBufferData with nullptr) leaves
|
||||
// the store undefined, so backends may (re)allocate GPU storage without
|
||||
// uploading the stale CPU shadow.
|
||||
Bool HasDefinedContent() const;
|
||||
|
||||
const SharedPtr<BackendBufferResource>& GetBackendResource() const;
|
||||
void SetBackendResource(SharedPtr<BackendBufferResource> resource);
|
||||
@@ -213,6 +218,8 @@ namespace MobileGL {
|
||||
Bool m_isImmutableStorage = false;
|
||||
GLbitfield m_storageFlags = 0;
|
||||
Uint64 m_changeSerial = 0;
|
||||
// See HasDefinedContent().
|
||||
Bool m_hasDefinedContent = true;
|
||||
// Set by MarkGpuWritten, cleared by SyncGpuWrites once the shadow is refreshed.
|
||||
Bool m_gpuWritePending = false;
|
||||
Range1D m_mappedRange;
|
||||
|
||||
@@ -111,7 +111,9 @@ namespace MobileGL {
|
||||
TextureUnit& GetTextureUnitObject(Int unit);
|
||||
ImageTextureBinding& GetImageTextureBinding(Int unit);
|
||||
const ImageTextureBinding& GetImageTextureBinding(Int unit) const;
|
||||
void NoteTextureUnitTouched(Int unit) { m_textureState.NoteUnitTouched(unit); }
|
||||
void NoteTextureUnitTouched(Int unit, Bool bindingChanged = true) {
|
||||
m_textureState.NoteUnitTouched(unit, bindingChanged);
|
||||
}
|
||||
Int GetMaxTouchedTextureUnit() const { return m_textureState.GetMaxTouchedUnit(); }
|
||||
// Monotonic counter bumped whenever a texture bind/unbind/delete changes which
|
||||
// texture is bound at a unit; lets a backend skip re-resolving an unchanged
|
||||
|
||||
@@ -27,11 +27,23 @@ namespace MobileGL {
|
||||
m_texelSizes.reserve(std::bit_ceil(requiredLevelCount));
|
||||
m_texelSizes.resize(requiredLevelCount);
|
||||
m_isDirty.resize(requiredLevelCount, false);
|
||||
m_dirtyRegions.resize(requiredLevelCount);
|
||||
m_compressedData.resize(requiredLevelCount);
|
||||
m_compressedFormats.resize(requiredLevelCount, GL_NONE);
|
||||
}
|
||||
|
||||
m_texelSizes[level] = input.texelSize;
|
||||
// A respecified level invalidates any pending sub-region: its extents were
|
||||
// measured against the old size. If the level is still flagged dirty the
|
||||
// pending upload widens to the whole (new) level.
|
||||
if (level < m_dirtyRegions.size()) {
|
||||
m_dirtyRegions[level] =
|
||||
m_isDirty[level]
|
||||
? MipmapDirtyRegion{IntVec3{0, 0, 0},
|
||||
IntVec3{input.texelSize.x(), input.texelSize.y(),
|
||||
std::max(input.texelSize.z(), 1)}}
|
||||
: MipmapDirtyRegion{};
|
||||
}
|
||||
auto& data = m_data[level];
|
||||
data.resize(input.byteSize, 0);
|
||||
|
||||
@@ -81,6 +93,7 @@ namespace MobileGL {
|
||||
m_data.resize(levelCount);
|
||||
m_texelSizes.resize(levelCount);
|
||||
m_isDirty.resize(levelCount);
|
||||
m_dirtyRegions.resize(levelCount);
|
||||
m_compressedData.resize(levelCount);
|
||||
m_compressedFormats.resize(levelCount);
|
||||
}
|
||||
@@ -95,7 +108,7 @@ namespace MobileGL {
|
||||
const Uint8* src = static_cast<const Uint8*>(input.data);
|
||||
// Clamp so a size mismatch can never write past the allocation.
|
||||
Memcpy(levelData.data(), src, std::min(input.size, levelData.size()));
|
||||
m_isDirty[level] = true;
|
||||
MarkDirty(level, true); // whole-level write: dirty region covers everything
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,12 +133,51 @@ namespace MobileGL {
|
||||
void MipmapStorage::MarkDirty(Uint level, bool dirty) {
|
||||
MOBILEGL_ASSERT(level < m_isDirty.size(), "MarkDirty: level out of range");
|
||||
m_isDirty[level] = dirty;
|
||||
if (level < m_dirtyRegions.size()) {
|
||||
if (dirty) {
|
||||
const IntVec3 size = level < m_texelSizes.size() ? m_texelSizes[level] : IntVec3{0, 0, 0};
|
||||
m_dirtyRegions[level] = {IntVec3{0, 0, 0},
|
||||
IntVec3{size.x(), size.y(), std::max(size.z(), 1)}};
|
||||
} else {
|
||||
m_dirtyRegions[level] = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MipmapStorage::IsDirty(Uint level) const {
|
||||
MOBILEGL_ASSERT(level < m_isDirty.size(), "IsDirty: level out of range");
|
||||
return m_isDirty[level];
|
||||
}
|
||||
|
||||
void MipmapStorage::MarkDirtyRegion(Uint level, IntVec3 offset, IntVec3 size) {
|
||||
MOBILEGL_ASSERT(level < m_isDirty.size(), "MarkDirtyRegion: level out of range");
|
||||
const IntVec3 levelSize = level < m_texelSizes.size() ? m_texelSizes[level] : IntVec3{0, 0, 0};
|
||||
MipmapDirtyRegion incoming;
|
||||
incoming.lo = {std::max(offset.x(), 0), std::max(offset.y(), 0), std::max(offset.z(), 0)};
|
||||
incoming.hi = {std::min(offset.x() + size.x(), levelSize.x()),
|
||||
std::min(offset.y() + size.y(), levelSize.y()),
|
||||
std::min(offset.z() + std::max(size.z(), 1), std::max(levelSize.z(), 1))};
|
||||
if (incoming.Empty()) return;
|
||||
if (level < m_dirtyRegions.size()) {
|
||||
MipmapDirtyRegion& region = m_dirtyRegions[level];
|
||||
if (m_isDirty[level] && !region.Empty()) {
|
||||
region.lo = {std::min(region.lo.x(), incoming.lo.x()),
|
||||
std::min(region.lo.y(), incoming.lo.y()),
|
||||
std::min(region.lo.z(), incoming.lo.z())};
|
||||
region.hi = {std::max(region.hi.x(), incoming.hi.x()),
|
||||
std::max(region.hi.y(), incoming.hi.y()),
|
||||
std::max(region.hi.z(), incoming.hi.z())};
|
||||
} else {
|
||||
region = incoming;
|
||||
}
|
||||
}
|
||||
m_isDirty[level] = true;
|
||||
}
|
||||
|
||||
MipmapDirtyRegion MipmapStorage::GetDirtyRegion(Uint level) const {
|
||||
if (level >= m_dirtyRegions.size()) return {};
|
||||
return m_dirtyRegions[level];
|
||||
}
|
||||
} // namespace GLState
|
||||
} // namespace MG_State
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
|
||||
#include "TextureEnum.h"
|
||||
#include "MG_Util/Types.h"
|
||||
#include "MG_Util/Math/VectorTypes.h"
|
||||
@@ -15,6 +17,22 @@
|
||||
namespace MobileGL {
|
||||
namespace MG_State {
|
||||
namespace GLState {
|
||||
// Texel-space bounding box of the shadow bytes a backend has not uploaded
|
||||
// yet, [lo, hi) per axis. Cleared (all zero) while the level is clean. A
|
||||
// box, not a range list: repeated sub-image writes union into one region,
|
||||
// which stays exact for the per-frame "small sub-rect of a big atlas"
|
||||
// pattern this exists for, and degrades to the old full-level upload as
|
||||
// the union grows.
|
||||
struct MipmapDirtyRegion {
|
||||
IntVec3 lo{0, 0, 0};
|
||||
IntVec3 hi{0, 0, 0};
|
||||
Bool Empty() const { return hi.x() <= lo.x() || hi.y() <= lo.y() || hi.z() <= lo.z(); }
|
||||
Bool CoversWholeLevel(const IntVec3& levelSize) const {
|
||||
return lo.x() <= 0 && lo.y() <= 0 && lo.z() <= 0 && hi.x() >= levelSize.x() &&
|
||||
hi.y() >= levelSize.y() && hi.z() >= std::max(levelSize.z(), 1);
|
||||
}
|
||||
};
|
||||
|
||||
class MipmapStorage {
|
||||
public:
|
||||
SizeT GetLevelCount() const;
|
||||
@@ -29,6 +47,12 @@ namespace MobileGL {
|
||||
SizeT GetByteSize(Uint level) const;
|
||||
void MarkDirty(Uint level, bool dirty);
|
||||
bool IsDirty(Uint level) const;
|
||||
// Union a sub-image write's box into the level's pending region and set the
|
||||
// dirty flag. MarkDirty keeps its meaning: true covers the whole level,
|
||||
// false clears the region along with the flag.
|
||||
void MarkDirtyRegion(Uint level, IntVec3 offset, IntVec3 size);
|
||||
// Meaningful only while IsDirty(level).
|
||||
MipmapDirtyRegion GetDirtyRegion(Uint level) const;
|
||||
|
||||
// The bytes an application handed to glCompressedTexImage*, kept verbatim beside the
|
||||
// (uncompressed) texel shadow rather than in place of it. GL 4.6 core 8.11 requires
|
||||
@@ -49,6 +73,7 @@ namespace MobileGL {
|
||||
Vector<IntVec3> m_texelSizes;
|
||||
Vector<Vector<Uint8>> m_data;
|
||||
Vector<bool> m_isDirty;
|
||||
Vector<MipmapDirtyRegion> m_dirtyRegions;
|
||||
Vector<Vector<Uint8>> m_compressedData;
|
||||
Vector<GLenum> m_compressedFormats;
|
||||
};
|
||||
|
||||
@@ -74,6 +74,16 @@ namespace MobileGL {
|
||||
return m_storage[targetIndex].IsDirty(level);
|
||||
}
|
||||
|
||||
void MarkDirtyRegion(Uint targetIndex, Uint level, IntVec3 offset, IntVec3 size) {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "MarkDirtyRegion: target invalid");
|
||||
m_storage[targetIndex].MarkDirtyRegion(level, offset, size);
|
||||
}
|
||||
|
||||
MipmapDirtyRegion GetDirtyRegion(Uint targetIndex, Uint level) const {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "GetDirtyRegion: target invalid");
|
||||
return m_storage[targetIndex].GetDirtyRegion(level);
|
||||
}
|
||||
|
||||
void SetCompressedImage(Uint targetIndex, Uint level, GLenum internalFormat, const void* data,
|
||||
SizeT size) {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "SetCompressedImage: target invalid");
|
||||
|
||||
@@ -16,6 +16,10 @@ namespace MobileGL {
|
||||
namespace GLState {
|
||||
static std::atomic<Uint64> s_nextTextureLifetimeId = 1;
|
||||
|
||||
// Defined further down next to the other sampling-completeness rules; the
|
||||
// memo in TextureObjectBase is its only caller.
|
||||
static Bool ComputeMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped);
|
||||
|
||||
// TextureObjectBase implementations
|
||||
Uint64 TextureObjectBase::AllocateLifetimeId() {
|
||||
return s_nextTextureLifetimeId.fetch_add(1, std::memory_order_relaxed);
|
||||
@@ -81,6 +85,7 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
m_internalFormat = format;
|
||||
++m_shapeVersion;
|
||||
++m_textureParamsVersion;
|
||||
}
|
||||
|
||||
@@ -192,6 +197,7 @@ namespace MobileGL {
|
||||
m_levelRange.y() = m_levelRange.x();
|
||||
}
|
||||
++m_textureParamsVersion;
|
||||
++m_shapeVersion;
|
||||
}
|
||||
|
||||
void TextureObjectBase::SetMaxLevel(Uint maxLevel) {
|
||||
@@ -202,6 +208,7 @@ namespace MobileGL {
|
||||
|
||||
m_levelRange.y() = maxLevel;
|
||||
++m_textureParamsVersion;
|
||||
++m_shapeVersion;
|
||||
}
|
||||
|
||||
Bool TextureObjectBase::IsImmutable() const {
|
||||
@@ -231,6 +238,17 @@ namespace MobileGL {
|
||||
return m_contentVersion;
|
||||
}
|
||||
|
||||
Bool TextureObjectBase::IsMipmapCompleteForFilterCached(Bool mipmapped) const {
|
||||
const int slot = mipmapped ? 1 : 0;
|
||||
if (m_completeMemoShapeVersion[slot] == m_shapeVersion) {
|
||||
return m_completeMemoValue[slot];
|
||||
}
|
||||
const Bool value = ComputeMipmapCompleteForFilter(this, mipmapped);
|
||||
m_completeMemoShapeVersion[slot] = m_shapeVersion;
|
||||
m_completeMemoValue[slot] = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
void TextureObjectBase::BumpContentVersion() {
|
||||
++m_contentVersion;
|
||||
}
|
||||
@@ -273,10 +291,12 @@ namespace MobileGL {
|
||||
|
||||
void TextureObjectWithOneMipmap::AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
MipmapInput input) {
|
||||
++m_shapeVersion;
|
||||
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||
}
|
||||
|
||||
void TextureObjectWithOneMipmap::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
|
||||
++m_shapeVersion;
|
||||
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
|
||||
}
|
||||
|
||||
@@ -301,6 +321,18 @@ namespace MobileGL {
|
||||
return m_textureStorage.IsDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
void TextureObjectWithOneMipmap::MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
IntVec3 offset, IntVec3 size) {
|
||||
++m_contentVersion;
|
||||
m_textureStorage.MarkDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, offset,
|
||||
size);
|
||||
}
|
||||
|
||||
MipmapDirtyRegion TextureObjectWithOneMipmap::GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const {
|
||||
return m_textureStorage.GetDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
void TextureObjectWithOneMipmap::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat, const void* data, SizeT size) {
|
||||
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
||||
@@ -361,13 +393,18 @@ namespace MobileGL {
|
||||
|
||||
// TODO: add other texture types as needed
|
||||
|
||||
Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped) {
|
||||
if (texture == nullptr) return true;
|
||||
return texture->IsMipmapCompleteForFilterCached(mipmapped);
|
||||
}
|
||||
|
||||
Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler) {
|
||||
const Bool mipmapped =
|
||||
effectiveSampler != nullptr && effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None;
|
||||
return !IsMipmapCompleteForFilter(texture, mipmapped);
|
||||
}
|
||||
|
||||
Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped) {
|
||||
static Bool ComputeMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped) {
|
||||
if (texture == nullptr) return true;
|
||||
if (!texture->IsComplete()) return false;
|
||||
if (!mipmapped) return true;
|
||||
|
||||
@@ -55,6 +55,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
// Backends compare it against a per-resource snapshot to skip re-syncing unchanged
|
||||
// textures across draws (e.g. the block atlas bound across a whole terrain batch).
|
||||
virtual Uint64 GetContentVersion() const = 0;
|
||||
// Answers IsMipmapCompleteForFilter() from a memo. Sampling completeness is a
|
||||
// property of the texture's SHAPE - level sizes, level count, level range,
|
||||
// internal format - and never of its texel content, but every draw asks about
|
||||
// every bound texture, which made recomputing it one of the hottest things both
|
||||
// backends did (the walk plus its GetTexelSize calls measured ~8% of the render
|
||||
// thread). Shape mutations invalidate the memo; uploads do not.
|
||||
virtual Bool IsMipmapCompleteForFilterCached(Bool mipmapped) const = 0;
|
||||
virtual Int GetSamples() const = 0;
|
||||
virtual void SetSamples(Int samples) = 0;
|
||||
virtual Bool HasFixedSampleLocations() const = 0;
|
||||
@@ -99,6 +106,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
void SetImmutableLevels(Uint levels) override;
|
||||
Uint16 GetTextureParamsVersion() const override;
|
||||
Uint64 GetContentVersion() const override;
|
||||
Bool IsMipmapCompleteForFilterCached(Bool mipmapped) const override;
|
||||
// Bumps the content version without touching per-level storage-dirty flags. Used when the
|
||||
// set of defined mip levels grows via GPU-side mip generation (glGenerateMipmap): the level
|
||||
// set changed (so a cached sampled view's level range is stale) but no CPU data is dirty.
|
||||
@@ -124,6 +132,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
UintVec2 m_levelRange = {0, 1000};
|
||||
Uint m_immutableLevels = 0;
|
||||
Uint16 m_textureParamsVersion = 0;
|
||||
// Bumped by every mutation the completeness answer depends on - internal
|
||||
// format, level range, and the stored level set - and by nothing else, so a
|
||||
// texel upload leaves the memo below valid.
|
||||
Uint64 m_shapeVersion = 1;
|
||||
// [0] = the non-mipmapped answer, [1] = the mipmapped one. Mutable because
|
||||
// completeness is a query; a zero version means "never computed".
|
||||
mutable Uint64 m_completeMemoShapeVersion[2] = {0, 0};
|
||||
mutable Bool m_completeMemoValue[2] = {false, false};
|
||||
// Starts at 1 so a freshly-created backend resource (snapshot 0) never spuriously
|
||||
// matches before its first sync. Bumped only on dirty=true in MarkStorageDirty.
|
||||
Uint64 m_contentVersion = 1;
|
||||
@@ -150,6 +166,20 @@ namespace MobileGL::MG_State::GLState {
|
||||
virtual void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) = 0;
|
||||
virtual void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty = true) = 0;
|
||||
virtual Bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
|
||||
// Sub-image variant of MarkStorageDirty(..., true): backends may then upload
|
||||
// only the accumulated region instead of the whole level. The base fallback
|
||||
// keeps whole-level semantics for storage classes that do not track regions.
|
||||
virtual void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
|
||||
IntVec3 size) {
|
||||
(void)offset;
|
||||
(void)size;
|
||||
MarkStorageDirty(uploadTarget, mipmapLevel, true);
|
||||
}
|
||||
// Meaningful only while IsStorageDirty(uploadTarget, mipmapLevel).
|
||||
virtual MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel) const {
|
||||
const IntVec3 size = GetMipmapTexelSize(uploadTarget, mipmapLevel);
|
||||
return {IntVec3{0, 0, 0}, IntVec3{size.x(), size.y(), std::max(size.z(), 1)}};
|
||||
}
|
||||
|
||||
// The compressed image a glCompressedTexImage* call shadowed for this level, kept verbatim
|
||||
// next to the texel data rather than instead of it - see MipmapStorage. The texel shadow
|
||||
@@ -218,6 +248,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
|
||||
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) override;
|
||||
bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
|
||||
IntVec3 size) override;
|
||||
MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat,
|
||||
const void* data, SizeT size) override;
|
||||
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
|
||||
@@ -28,10 +28,12 @@ namespace MobileGL {
|
||||
|
||||
void TextureObject2DCube::AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
MipmapInput input) {
|
||||
++m_shapeVersion;
|
||||
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||
}
|
||||
|
||||
void TextureObject2DCube::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
|
||||
++m_shapeVersion;
|
||||
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
|
||||
}
|
||||
|
||||
@@ -55,6 +57,18 @@ namespace MobileGL {
|
||||
return m_textureStorage.IsDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
void TextureObject2DCube::MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
IntVec3 offset, IntVec3 size) {
|
||||
++m_contentVersion;
|
||||
m_textureStorage.MarkDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, offset,
|
||||
size);
|
||||
}
|
||||
|
||||
MipmapDirtyRegion TextureObject2DCube::GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const {
|
||||
return m_textureStorage.GetDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
void TextureObject2DCube::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat, const void* data, SizeT size) {
|
||||
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
||||
|
||||
@@ -27,6 +27,10 @@ namespace MobileGL {
|
||||
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
|
||||
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, bool dirty) override;
|
||||
bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
|
||||
IntVec3 size) override;
|
||||
MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const override;
|
||||
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat, const void* data, SizeT size) override;
|
||||
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
// High-water mark of texture units ever touched by a texture or sampler bind.
|
||||
// Units above it have provably-empty binding slots, so per-draw backend scans
|
||||
// can stop there instead of walking all MAX_TEXTURE_IMAGE_UNITS units.
|
||||
void NoteUnitTouched(Int unit) {
|
||||
void NoteUnitTouched(Int unit, Bool bindingChanged = true) {
|
||||
if (unit > m_maxTouchedUnit && unit < MAX_TEXTURE_IMAGE_UNITS) m_maxTouchedUnit = unit;
|
||||
// Every texture/sampler bind entry point (glBindTexture / glBindTextureUnit /
|
||||
// glBindTextures / glBindSampler) routes through here, so bumping the generation here
|
||||
@@ -75,7 +75,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
// which texture is bound at which unit. A backend that has cached the per-draw
|
||||
// sampled-texture set can compare this against a snapshot to skip re-resolving it when
|
||||
// no bind changed (the block atlas + lightmap stay bound across a whole terrain batch).
|
||||
++m_textureBindGeneration;
|
||||
// Re-binding the object a slot already holds changes nothing that the generation
|
||||
// guards; such callers pass bindingChanged=false so only the high-water mark advances
|
||||
// and the backend fast path survives the redundant re-binds apps issue every frame.
|
||||
if (bindingChanged) ++m_textureBindGeneration;
|
||||
}
|
||||
Int GetMaxTouchedUnit() const { return m_maxTouchedUnit; }
|
||||
Uint64 GetTextureBindGeneration() const { return m_textureBindGeneration; }
|
||||
|
||||
@@ -34,7 +34,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
void VertexArrayState::Bind(Uint index) {
|
||||
m_boundVertexArray = GetVertexArrayObject(index);
|
||||
const auto& vertexArray = GetVertexArrayObject(index);
|
||||
// Re-binding the already-current VAO is a per-batch habit of Blaze3D-style renderers;
|
||||
// skip the shared_ptr store (two atomic refcount ops) when nothing changes.
|
||||
if (vertexArray == m_boundVertexArray) return;
|
||||
m_boundVertexArray = vertexArray;
|
||||
}
|
||||
|
||||
const SharedPtr<VertexArrayObject>& VertexArrayState::CreateVertexArrayObject(Uint index) {
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/DriverBenchJni.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
|
||||
|
||||
// In-process driver benchmark for the plugin APK's POST screen: runs the
|
||||
// shared MG_Benchmark case bodies THROUGH MobileGL's full translation stack
|
||||
// on the backend the caller names, and returns per-case timings as JSON.
|
||||
//
|
||||
// Process contract (enforced by BenchService, which hosts this in its own
|
||||
// android:process and exits afterwards): the backend is latched by
|
||||
// MobileGL::Initialize() from MOBILEGL_BACKEND_TYPE, and Espryt's teardown
|
||||
// terminates the process-default EGL display - both make a bench run
|
||||
// unrepeatable and unsafe next to a live UI. One process, one backend, one
|
||||
// run.
|
||||
#ifdef __ANDROID__
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
// Includes.h first: it pins GL_GLES_PROTOTYPES=0 so the GLES headers declare
|
||||
// types and enums without also declaring the entry points this library defines.
|
||||
#include <Includes.h>
|
||||
|
||||
#include <EGL/egl.h>
|
||||
#include <GLES3/gl32.h>
|
||||
|
||||
#include <MG_Impl/EGLImpl/EGLImpl.h>
|
||||
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
||||
#include <MG_Impl/GLImpl/Drawing/GL_Drawing.h>
|
||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_Impl/GLImpl/Program/GL_Program.h>
|
||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||
#include <MG_Impl/GLImpl/Sampler/GL_Sampler.h>
|
||||
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||
#include <MG_Impl/GLImpl/VertexArray/GL_VertexArray.h>
|
||||
|
||||
// Bind every gl*/egl* name the shared cases use to MobileGL's own frontend,
|
||||
// by name and not by linkage. Writing the bare gl* symbols here would leave the
|
||||
// binding to the dynamic linker, and this library legitimately has the platform
|
||||
// libEGL/libGLESv3 in its own lookup scope - a bench that quietly measured the
|
||||
// device driver instead of the translation layer would look like very good news.
|
||||
#define glActiveTexture MobileGL::MG_Impl::GLImpl::ActiveTexture
|
||||
#define glAttachShader MobileGL::MG_Impl::GLImpl::AttachShader
|
||||
#define glBindAttribLocation MobileGL::MG_Impl::GLImpl::BindAttribLocation
|
||||
#define glBindBuffer MobileGL::MG_Impl::GLImpl::BindBuffer
|
||||
#define glBindBufferRange MobileGL::MG_Impl::GLImpl::BindBufferRange
|
||||
#define glBindFramebuffer MobileGL::MG_Impl::GLImpl::BindFramebuffer
|
||||
#define glBindRenderbuffer MobileGL::MG_Impl::GLImpl::BindRenderbuffer
|
||||
#define glBindSampler MobileGL::MG_Impl::GLImpl::BindSampler
|
||||
#define glBindTexture MobileGL::MG_Impl::GLImpl::BindTexture
|
||||
#define glBindVertexArray MobileGL::MG_Impl::GLImpl::BindVertexArray
|
||||
#define glBufferData MobileGL::MG_Impl::GLImpl::BufferData
|
||||
#define glBufferSubData MobileGL::MG_Impl::GLImpl::BufferSubData
|
||||
#define glCheckFramebufferStatus MobileGL::MG_Impl::GLImpl::CheckFramebufferStatus
|
||||
#define glClear MobileGL::MG_Impl::GLImpl::Clear
|
||||
#define glClearColor MobileGL::MG_Impl::GLImpl::ClearColor
|
||||
#define glClientWaitSync MobileGL::MG_Impl::GLImpl::ClientWaitSync
|
||||
#define glCompileShader MobileGL::MG_Impl::GLImpl::CompileShader
|
||||
#define glCreateProgram MobileGL::MG_Impl::GLImpl::CreateProgram
|
||||
#define glCreateShader MobileGL::MG_Impl::GLImpl::CreateShader
|
||||
#define glDeleteSync MobileGL::MG_Impl::GLImpl::DeleteSync
|
||||
#define glDrawArrays MobileGL::MG_Impl::GLImpl::DrawArrays
|
||||
#define glDrawElements MobileGL::MG_Impl::GLImpl::DrawElements
|
||||
#define glDrawElementsBaseVertex MobileGL::MG_Impl::GLImpl::DrawElementsBaseVertex
|
||||
#define glEnable MobileGL::MG_Impl::GLImpl::Enable
|
||||
#define glEnableVertexAttribArray MobileGL::MG_Impl::GLImpl::EnableVertexAttribArray
|
||||
#define glFenceSync MobileGL::MG_Impl::GLImpl::FenceSync
|
||||
#define glFramebufferRenderbuffer MobileGL::MG_Impl::GLImpl::FramebufferRenderbuffer
|
||||
#define glGenBuffers MobileGL::MG_Impl::GLImpl::GenBuffers
|
||||
#define glGenFramebuffers MobileGL::MG_Impl::GLImpl::GenFramebuffers
|
||||
#define glGenRenderbuffers MobileGL::MG_Impl::GLImpl::GenRenderbuffers
|
||||
#define glGenSamplers MobileGL::MG_Impl::GLImpl::GenSamplers
|
||||
#define glGenTextures MobileGL::MG_Impl::GLImpl::GenTextures
|
||||
#define glGenVertexArrays MobileGL::MG_Impl::GLImpl::GenVertexArrays
|
||||
#define glGenerateMipmap MobileGL::MG_Impl::GLImpl::GenerateMipmap
|
||||
#define glGetError MobileGL::MG_Impl::GLImpl::GetError
|
||||
#define glGetIntegerv MobileGL::MG_Impl::GLImpl::GetIntegerv
|
||||
#define glGetProgramiv MobileGL::MG_Impl::GLImpl::GetProgramiv
|
||||
#define glGetShaderInfoLog MobileGL::MG_Impl::GLImpl::GetShaderInfoLog
|
||||
#define glGetShaderiv MobileGL::MG_Impl::GLImpl::GetShaderiv
|
||||
#define glGetString MobileGL::MG_Impl::GLImpl::GetString
|
||||
#define glGetUniformLocation MobileGL::MG_Impl::GLImpl::GetUniformLocation
|
||||
#define glLinkProgram MobileGL::MG_Impl::GLImpl::LinkProgram
|
||||
#define glMultiDrawElementsBaseVertex MobileGL::MG_Impl::GLImpl::MultiDrawElementsBaseVertex
|
||||
#define glPixelStorei MobileGL::MG_Impl::GLImpl::PixelStorei
|
||||
#define glRenderbufferStorage MobileGL::MG_Impl::GLImpl::RenderbufferStorage
|
||||
#define glSamplerParameteri MobileGL::MG_Impl::GLImpl::SamplerParameteri
|
||||
#define glShaderSource MobileGL::MG_Impl::GLImpl::ShaderSource
|
||||
#define glTexImage2D MobileGL::MG_Impl::GLImpl::TexImage2D
|
||||
#define glTexParameteri MobileGL::MG_Impl::GLImpl::TexParameteri
|
||||
#define glTexSubImage2D MobileGL::MG_Impl::GLImpl::TexSubImage2D
|
||||
#define glUniform1i MobileGL::MG_Impl::GLImpl::Uniform1i
|
||||
#define glUniform3f MobileGL::MG_Impl::GLImpl::Uniform3f
|
||||
#define glUniform3fv MobileGL::MG_Impl::GLImpl::Uniform3fv
|
||||
#define glUniformMatrix4fv MobileGL::MG_Impl::GLImpl::UniformMatrix4fv
|
||||
#define glUseProgram MobileGL::MG_Impl::GLImpl::UseProgram
|
||||
#define glVertexAttribPointer MobileGL::MG_Impl::GLImpl::VertexAttribPointer
|
||||
#define glViewport MobileGL::MG_Impl::GLImpl::Viewport
|
||||
#define glFinish() ((void)0) // MobileGL's own glFinish is a no-op; never paced on
|
||||
|
||||
#define eglChooseConfig MobileGL::MG_Impl::EGLImpl::ChooseConfig
|
||||
#define eglCreateContext MobileGL::MG_Impl::EGLImpl::CreateContext
|
||||
#define eglCreatePbufferSurface MobileGL::MG_Impl::EGLImpl::CreatePbufferSurface
|
||||
#define eglGetDisplay MobileGL::MG_Impl::EGLImpl::GetDisplay
|
||||
#define eglGetError MobileGL::MG_Impl::EGLImpl::GetError
|
||||
#define eglInitialize MobileGL::MG_Impl::EGLImpl::Initialize
|
||||
#define eglMakeCurrent MobileGL::MG_Impl::EGLImpl::MakeCurrent
|
||||
|
||||
namespace {
|
||||
|
||||
uint64_t NowNs() {
|
||||
timespec ts{};
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return static_cast<uint64_t>(ts.tv_sec) * 1000000000ull + static_cast<uint64_t>(ts.tv_nsec);
|
||||
}
|
||||
|
||||
int CompareU64(const void* a, const void* b) {
|
||||
const uint64_t x = *static_cast<const uint64_t*>(a);
|
||||
const uint64_t y = *static_cast<const uint64_t*>(b);
|
||||
return x < y ? -1 : x > y;
|
||||
}
|
||||
#define now_ns NowNs
|
||||
#define cmp_u64 CompareU64
|
||||
|
||||
// Failure funnel for the shared scene builder: remember the first failure,
|
||||
// let the run finish reporting it instead of aborting the service process.
|
||||
std::string g_benchFailure;
|
||||
void bench_gl_failed(const char* what, const char* detail) {
|
||||
if (!g_benchFailure.empty()) return;
|
||||
g_benchFailure = std::string(what) + (detail && detail[0] ? std::string(": ") + detail : "");
|
||||
}
|
||||
|
||||
int g_frames = 120;
|
||||
int g_warmup = 30;
|
||||
|
||||
// MobileGL exports the desktop multi-draw entry point on every platform, so
|
||||
// the shared cases always get the real call here (see the .inc for why this
|
||||
// is routed through a hook at all).
|
||||
void bench_multi_draw_elements_base_vertex(GLenum mode, const GLsizei* counts, GLenum type,
|
||||
const void* const* offsets, GLsizei drawCount,
|
||||
const GLint* baseVertices) {
|
||||
glMultiDrawElementsBaseVertex(mode, counts, type, offsets, drawCount, baseVertices);
|
||||
}
|
||||
|
||||
#include "../../MG_Benchmark/Driver/DriverBenchCases.inc"
|
||||
|
||||
// Frame boundary: a real fence wait. MobileGL's glFinish/glFlush are
|
||||
// deliberate no-ops, so a glFinish-paced loop would time only CPU submit.
|
||||
void EndFrameWait() {
|
||||
GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
if (sync != nullptr) {
|
||||
glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 1000000000ull);
|
||||
glDeleteSync(sync);
|
||||
return;
|
||||
}
|
||||
glFinish();
|
||||
}
|
||||
|
||||
std::string EscapeJson(const std::string& value) {
|
||||
std::string out;
|
||||
out.reserve(value.size() + 8);
|
||||
for (const char c : value) {
|
||||
switch (c) {
|
||||
case '"': out += "\\\""; break;
|
||||
case '\\': out += "\\\\"; break;
|
||||
case '\n': out += "\\n"; break;
|
||||
case '\r': out += "\\r"; break;
|
||||
case '\t': out += "\\t"; break;
|
||||
default:
|
||||
if (static_cast<unsigned char>(c) < 0x20 || static_cast<unsigned char>(c) >= 0x7F) {
|
||||
char buffer[8];
|
||||
snprintf(buffer, sizeof buffer, "\\u%04x", static_cast<unsigned char>(c));
|
||||
out += buffer;
|
||||
} else {
|
||||
out += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
struct EglBench {
|
||||
EGLDisplay display = EGL_NO_DISPLAY;
|
||||
EGLSurface surface = EGL_NO_SURFACE;
|
||||
EGLContext context = EGL_NO_CONTEXT;
|
||||
};
|
||||
|
||||
// MobileGL's own EGL, driven exactly like a launcher drives it. No
|
||||
// eglTerminate here: the service process exits right after the run, and
|
||||
// Espryt's display teardown is precisely the hazard being avoided.
|
||||
bool BootEgl(EglBench& out, std::string& error) {
|
||||
out.display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
if (out.display == EGL_NO_DISPLAY) { error = "eglGetDisplay failed"; return false; }
|
||||
EGLint major = 0, minor = 0;
|
||||
if (eglInitialize(out.display, &major, &minor) != EGL_TRUE) {
|
||||
char buffer[48];
|
||||
snprintf(buffer, sizeof buffer, "eglInitialize failed (0x%04x)", eglGetError());
|
||||
error = buffer;
|
||||
return false;
|
||||
}
|
||||
const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RED_SIZE, 8,
|
||||
EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_DEPTH_SIZE, 24,
|
||||
EGL_NONE};
|
||||
EGLConfig config = nullptr;
|
||||
EGLint numConfigs = 0;
|
||||
if (eglChooseConfig(out.display, configAttribs, &config, 1, &numConfigs) != EGL_TRUE ||
|
||||
numConfigs < 1) {
|
||||
error = "eglChooseConfig found no pbuffer config";
|
||||
return false;
|
||||
}
|
||||
const EGLint pbufferAttribs[] = {EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE};
|
||||
out.surface = eglCreatePbufferSurface(out.display, config, pbufferAttribs);
|
||||
if (out.surface == EGL_NO_SURFACE) {
|
||||
char buffer[56];
|
||||
snprintf(buffer, sizeof buffer, "eglCreatePbufferSurface failed (0x%04x)", eglGetError());
|
||||
error = buffer;
|
||||
return false;
|
||||
}
|
||||
const EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
|
||||
out.context = eglCreateContext(out.display, config, EGL_NO_CONTEXT, contextAttribs);
|
||||
if (out.context == EGL_NO_CONTEXT) {
|
||||
char buffer[48];
|
||||
snprintf(buffer, sizeof buffer, "eglCreateContext failed (0x%04x)", eglGetError());
|
||||
error = buffer;
|
||||
return false;
|
||||
}
|
||||
if (eglMakeCurrent(out.display, out.surface, out.surface, out.context) != EGL_TRUE) {
|
||||
char buffer[48];
|
||||
snprintf(buffer, sizeof buffer, "eglMakeCurrent failed (0x%04x)", eglGetError());
|
||||
error = buffer;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string RunBench(const std::string& backendType, int frames, int warmup) {
|
||||
// Must land before the first EGL call: Initialize() reads the
|
||||
// environment exactly once per process.
|
||||
setenv("MOBILEGL_BACKEND_TYPE", backendType.c_str(), 1);
|
||||
|
||||
EglBench egl;
|
||||
std::string error;
|
||||
if (!BootEgl(egl, error)) {
|
||||
return std::string("{\"error\":\"") + EscapeJson(error) + "\"}";
|
||||
}
|
||||
|
||||
const char* renderer = reinterpret_cast<const char*>(glGetString(GL_RENDERER));
|
||||
const char* version = reinterpret_cast<const char*>(glGetString(GL_VERSION));
|
||||
|
||||
g_benchFailure.clear();
|
||||
build_resources();
|
||||
if (!g_benchFailure.empty()) {
|
||||
return std::string("{\"error\":\"") + EscapeJson(g_benchFailure) + "\"}";
|
||||
}
|
||||
|
||||
if (frames < 8) frames = 8;
|
||||
if (frames > 512) frames = 512;
|
||||
if (warmup < 2) warmup = 2;
|
||||
if (warmup > 128) warmup = 128;
|
||||
g_frames = frames;
|
||||
g_warmup = warmup;
|
||||
|
||||
std::string rows;
|
||||
static uint64_t samples[512];
|
||||
for (int c = 0; c < kBenchCaseCount; ++c) {
|
||||
const BenchCaseDesc& benchCase = kBenchCases[c];
|
||||
EndFrameWait();
|
||||
for (int i = 0; i < g_warmup; ++i) {
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
benchCase.fn(i, benchCase.a, benchCase.b);
|
||||
EndFrameWait();
|
||||
}
|
||||
for (int i = 0; i < g_frames; ++i) {
|
||||
const uint64_t t0 = NowNs();
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
benchCase.fn(i, benchCase.a, benchCase.b);
|
||||
EndFrameWait();
|
||||
samples[i] = NowNs() - t0;
|
||||
}
|
||||
qsort(samples, static_cast<size_t>(g_frames), sizeof(uint64_t), CompareU64);
|
||||
const uint64_t median = samples[g_frames / 2];
|
||||
const double frameMs = static_cast<double>(median) / 1e6;
|
||||
const double nsPerOp =
|
||||
benchCase.opsPerFrame > 0 ? static_cast<double>(median) / static_cast<double>(benchCase.opsPerFrame) : 0.0;
|
||||
const GLenum caseError = glGetError();
|
||||
|
||||
char row[256];
|
||||
snprintf(row, sizeof row,
|
||||
"%s{\"case\":\"%s\",\"frames\":%d,\"opsPerFrame\":%ld,\"medianFrameMs\":%.3f,"
|
||||
"\"nsPerOp\":%.1f,\"fps\":%.1f,\"glError\":%u}",
|
||||
rows.empty() ? "" : ",", benchCase.name, g_frames, benchCase.opsPerFrame, frameMs,
|
||||
nsPerOp, 1e9 / static_cast<double>(median), caseError);
|
||||
rows += row;
|
||||
}
|
||||
|
||||
std::string json = "{\"backend\":\"" + EscapeJson(backendType) + "\"";
|
||||
json += ",\"renderer\":\"" + EscapeJson(renderer ? renderer : "unknown") + "\"";
|
||||
json += ",\"version\":\"" + EscapeJson(version ? version : "unknown") + "\"";
|
||||
json += ",\"frames\":" + std::to_string(g_frames);
|
||||
json += ",\"cases\":[" + rows + "]}";
|
||||
|
||||
// Minimal teardown: unbind so the driver flushes, then let the
|
||||
// process exit reclaim everything. eglTerminate stays un-called on
|
||||
// purpose (Espryt would take the process-default display down).
|
||||
eglMakeCurrent(egl.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
return json;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL Java_top_mobilegl_plugin_BenchService_nativeRunDriverBench(
|
||||
JNIEnv* env, jclass, jstring backendType, jint frames, jint warmupFrames) {
|
||||
const char* backendChars = backendType ? env->GetStringUTFChars(backendType, nullptr) : nullptr;
|
||||
const std::string backend = backendChars ? backendChars : "DirectGLES";
|
||||
if (backendChars) env->ReleaseStringUTFChars(backendType, backendChars);
|
||||
|
||||
std::string result;
|
||||
try {
|
||||
result = RunBench(backend, frames, warmupFrames);
|
||||
} catch (const std::exception& e) {
|
||||
result = std::string("{\"error\":\"") + EscapeJson(e.what()) + "\"}";
|
||||
} catch (...) {
|
||||
result = "{\"error\":\"unknown native exception\"}";
|
||||
}
|
||||
return env->NewStringUTF(result.c_str());
|
||||
}
|
||||
|
||||
#endif // __ANDROID__
|
||||
@@ -1218,6 +1218,22 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
}
|
||||
}
|
||||
|
||||
// Windowless (EGL pbuffer) contexts want a headless surface. Almost no mobile
|
||||
// ICD provides one - Mali r32p1 does not - so its absence is not fatal: the
|
||||
// renderer hands the WSI an AImageReader window instead. Reported because the
|
||||
// fallback costs a buffer queue the headless path does not need, and because
|
||||
// this used to abort the process instead.
|
||||
if (HasVkExtension(instanceExtensions, VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME)) {
|
||||
builder.Pass("Headless surface",
|
||||
format("{} present; windowless contexts get a real headless surface",
|
||||
VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME));
|
||||
} else {
|
||||
builder.Warn("Headless surface",
|
||||
format("{} absent; a windowless (pbuffer) context falls back to an AImageReader "
|
||||
"ANativeWindow, which needs libmediandk.so and an extra buffer queue",
|
||||
VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME));
|
||||
}
|
||||
|
||||
// The probe never creates a surface, so the instance is created without extensions.
|
||||
VkApplicationInfo appInfo{};
|
||||
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
|
||||
|
||||
@@ -156,11 +156,14 @@ namespace MobileGL {
|
||||
|
||||
BindingSlot() : m_target((TargetEnum)0) {}
|
||||
explicit BindingSlot(TargetEnum target) : m_target(target) {}
|
||||
void Bind(SharedPtr<ObjectType> object) {
|
||||
if (m_boundObject == object) return;
|
||||
// Reports whether the binding actually changed, so callers can keep change-driven
|
||||
// bookkeeping (e.g. the texture bind generation) quiet on redundant re-binds.
|
||||
Bool Bind(SharedPtr<ObjectType> object) {
|
||||
if (m_boundObject == object) return false;
|
||||
|
||||
m_boundObject = Move(object);
|
||||
++m_version;
|
||||
return true;
|
||||
}
|
||||
SharedPtr<ObjectType> const& GetBoundObject() const noexcept { return m_boundObject; }
|
||||
TargetEnum GetTarget() const { return m_target; }
|
||||
|
||||
@@ -33,6 +33,13 @@ fun Project.mobileGlAbiFilters(): List<String> {
|
||||
fun Project.mobileGlCmakeCompilerLauncher(): String =
|
||||
(findProperty("mobilegl.cmakeCompilerLauncher") ?: System.getenv("MOBILEGL_CMAKE_COMPILER_LAUNCHER") ?: "").toString().trim()
|
||||
|
||||
// Optional application-id suffix, so a development build can sit next to an
|
||||
// already-installed plugin instead of having to replace it - a differently
|
||||
// signed APK cannot upgrade one in place, and uninstalling costs the user their
|
||||
// plugin settings and the launcher's binding to it.
|
||||
fun Project.mobileGlApplicationIdSuffix(): String =
|
||||
(findProperty("mobilegl.applicationIdSuffix") ?: "").toString().trim()
|
||||
|
||||
fun Project.runGit(vararg arguments: String): String? = runCatching {
|
||||
ProcessBuilder("git", *arguments)
|
||||
.directory(rootDir)
|
||||
@@ -102,6 +109,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "top.mobilegl.plugin"
|
||||
mobileGlApplicationIdSuffix().takeIf { it.isNotEmpty() }?.let { applicationIdSuffix = it }
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = mobileGlVersionMajor * 1_000_000 + mobileGlVersionMinor * 10_000 + mobileGlMonthlyRevision
|
||||
|
||||
@@ -22,6 +22,15 @@
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.MobileGLPlugin.NoDisplay" />
|
||||
|
||||
<!-- One benchmark run per process: the backend is latched at MobileGL
|
||||
init and Espryt teardown kills the process-default EGL display, so
|
||||
the bench must never share a process with the POST UI. The service
|
||||
exits after each run. -->
|
||||
<service
|
||||
android:name=".BenchService"
|
||||
android:exported="false"
|
||||
android:process=":bench" />
|
||||
|
||||
<meta-data
|
||||
android:name="fclPlugin_V2"
|
||||
android:resource="@string/config" />
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package top.mobilegl.plugin;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Hosts one driver-benchmark run in its own process ({@code android:process=":bench"})
|
||||
* and exits when done.
|
||||
*
|
||||
* The isolation is load-bearing, not defensive: MobileGL latches its backend from
|
||||
* {@code MOBILEGL_BACKEND_TYPE} on first initialization, so Espryt and Magma can never
|
||||
* share a process, and Espryt's teardown terminates the process-default EGL display,
|
||||
* which would take the POST activity's HWUI context down with it. A fresh process per
|
||||
* tap sidesteps both, and {@link System#exit} at the end guarantees the next tap gets
|
||||
* one.
|
||||
*/
|
||||
public final class BenchService extends Service {
|
||||
private static final String TAG = "MobileGLBench";
|
||||
|
||||
public static final String ACTION_RESULT = "top.mobilegl.plugin.BENCH_RESULT";
|
||||
public static final String EXTRA_BACKEND = "backend";
|
||||
public static final String EXTRA_RESULT_JSON = "resultJson";
|
||||
public static final String EXTRA_FRAMES = "frames";
|
||||
public static final String EXTRA_WARMUP = "warmupFrames";
|
||||
|
||||
private static native String nativeRunDriverBench(String backendType, int frames, int warmupFrames);
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
final String backend = intent != null ? intent.getStringExtra(EXTRA_BACKEND) : null;
|
||||
final int frames = intent != null ? intent.getIntExtra(EXTRA_FRAMES, 120) : 120;
|
||||
final int warmup = intent != null ? intent.getIntExtra(EXTRA_WARMUP, 30) : 30;
|
||||
if (backend == null) {
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
new Thread(() -> {
|
||||
String result;
|
||||
try {
|
||||
System.loadLibrary("MobileGL");
|
||||
result = nativeRunDriverBench(backend, frames, warmup);
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "bench failed", t);
|
||||
result = "{\"error\":\"" + t.getClass().getSimpleName() + ": "
|
||||
+ String.valueOf(t.getMessage()).replace("\\", "\\\\").replace("\"", "\\\"")
|
||||
+ "\"}";
|
||||
}
|
||||
|
||||
Intent reply = new Intent(ACTION_RESULT);
|
||||
reply.setPackage(getPackageName());
|
||||
reply.putExtra(EXTRA_BACKEND, backend);
|
||||
reply.putExtra(EXTRA_RESULT_JSON, result);
|
||||
sendBroadcast(reply);
|
||||
Log.i(TAG, "bench done for " + backend + " (" + result.length() + " bytes)");
|
||||
|
||||
// The broadcast is already handed to system_server; give binder a
|
||||
// moment, then take the whole process down so the next run starts
|
||||
// from an uninitialized MobileGL.
|
||||
try {
|
||||
Thread.sleep(250);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
stopSelf();
|
||||
System.exit(0);
|
||||
}, "MobileGLDriverBench").start();
|
||||
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,26 @@
|
||||
package top.mobilegl.plugin;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -61,6 +70,16 @@ public final class PostActivity extends Activity {
|
||||
private LinearLayout contentLayout;
|
||||
private TextView statusView;
|
||||
|
||||
/**
|
||||
* Per-backend bench UI state. Unlike the POST itself (single-flight, latched),
|
||||
* a bench may be re-run freely: every tap starts a fresh {@link BenchService}
|
||||
* process, so the only state to manage here is the button and the results
|
||||
* container the reply renders into.
|
||||
*/
|
||||
private final Map<String, Button> benchButtons = new HashMap<>();
|
||||
private final Map<String, LinearLayout> benchResultContainers = new HashMap<>();
|
||||
private BroadcastReceiver benchReceiver;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -108,9 +127,100 @@ public final class PostActivity extends Activity {
|
||||
deliveryTarget = new WeakReference<>(null);
|
||||
}
|
||||
}
|
||||
if (benchReceiver != null) {
|
||||
unregisterReceiver(benchReceiver);
|
||||
benchReceiver = null;
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
/** Registered lazily on the first Run Bench tap; delivers results to the UI thread. */
|
||||
private void ensureBenchReceiver() {
|
||||
if (benchReceiver != null) {
|
||||
return;
|
||||
}
|
||||
benchReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String backend = intent.getStringExtra(BenchService.EXTRA_BACKEND);
|
||||
String json = intent.getStringExtra(BenchService.EXTRA_RESULT_JSON);
|
||||
if (backend != null && json != null) {
|
||||
renderBenchResult(backend, json);
|
||||
}
|
||||
}
|
||||
};
|
||||
IntentFilter filter = new IntentFilter(BenchService.ACTION_RESULT);
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
registerReceiver(benchReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||
} else {
|
||||
registerReceiver(benchReceiver, filter);
|
||||
}
|
||||
}
|
||||
|
||||
/** Kicks one bench run for a backend in its own service process. */
|
||||
private void startBench(String backendType) {
|
||||
ensureBenchReceiver();
|
||||
Button button = benchButtons.get(backendType);
|
||||
if (button != null) {
|
||||
button.setEnabled(false);
|
||||
button.setText("Bench running... (can take a minute)");
|
||||
}
|
||||
LinearLayout container = benchResultContainers.get(backendType);
|
||||
if (container != null) {
|
||||
container.removeAllViews();
|
||||
}
|
||||
Intent intent = new Intent(this, BenchService.class);
|
||||
intent.putExtra(BenchService.EXTRA_BACKEND, backendType);
|
||||
intent.putExtra(BenchService.EXTRA_FRAMES, 120);
|
||||
intent.putExtra(BenchService.EXTRA_WARMUP, 30);
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
private void renderBenchResult(String backendType, String json) {
|
||||
Button button = benchButtons.get(backendType);
|
||||
if (button != null) {
|
||||
button.setEnabled(true);
|
||||
button.setText("Run Bench");
|
||||
}
|
||||
LinearLayout container = benchResultContainers.get(backendType);
|
||||
if (container == null) {
|
||||
return;
|
||||
}
|
||||
container.removeAllViews();
|
||||
try {
|
||||
JSONObject root = new JSONObject(json);
|
||||
String error = root.optString("error", "");
|
||||
if (!error.isEmpty()) {
|
||||
container.addView(makeText("Bench failed: " + error, 12, COLOR_FAIL, false));
|
||||
return;
|
||||
}
|
||||
container.addView(makeText(root.optString("renderer", ""), 11, COLOR_INFO, false));
|
||||
String header = String.format(Locale.ROOT, "%-22s %10s %10s %9s",
|
||||
"case", "ns/op", "frame ms", "fps");
|
||||
container.addView(makeText(header, 11, COLOR_DETAIL, true));
|
||||
JSONArray cases = root.optJSONArray("cases");
|
||||
if (cases == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < cases.length(); ++i) {
|
||||
JSONObject row = cases.optJSONObject(i);
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
String line = String.format(Locale.ROOT, "%-22s %10.1f %10.3f %9.1f",
|
||||
row.optString("case", "?"),
|
||||
row.optDouble("nsPerOp", 0),
|
||||
row.optDouble("medianFrameMs", 0),
|
||||
row.optDouble("fps", 0));
|
||||
TextView view = makeText(line, 11,
|
||||
row.optInt("glError", 0) != 0 ? COLOR_WARN : COLOR_TEXT, false);
|
||||
container.addView(view);
|
||||
}
|
||||
} catch (JSONException error) {
|
||||
container.addView(makeText("Bench result unparsable: " + error, 12, COLOR_FAIL, false));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads libMobileGL.so on first use, off the UI thread, so the first frame
|
||||
* renders immediately. Any failure (including errors thrown by static
|
||||
@@ -253,6 +363,8 @@ public final class PostActivity extends Activity {
|
||||
addText(renderer, 12, COLOR_INFO, false, dp(2));
|
||||
}
|
||||
|
||||
addBenchControls(name);
|
||||
|
||||
JSONArray checks = backend.optJSONArray("checks");
|
||||
if (checks != null) {
|
||||
LinearLayout table = new LinearLayout(this);
|
||||
@@ -276,6 +388,54 @@ public final class PostActivity extends Activity {
|
||||
renderFormatCapabilities(backend.optJSONObject("formatCapabilities"));
|
||||
}
|
||||
|
||||
/** The MOBILEGL_BACKEND_TYPE value a POST section name stands for, or null. */
|
||||
private static String backendTypeForSection(String sectionName) {
|
||||
switch (sectionName.toLowerCase(Locale.ROOT)) {
|
||||
case "gles":
|
||||
case "directgles":
|
||||
return "DirectGLES";
|
||||
case "vulkan":
|
||||
case "directvulkan":
|
||||
return "DirectVulkan";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One Run Bench button plus the container its results render into. The bench
|
||||
* runs the Minecraft-shaped MG_Benchmark cases through the full MobileGL stack
|
||||
* on this backend, in a throwaway service process (see BenchService).
|
||||
*/
|
||||
private void addBenchControls(String sectionName) {
|
||||
final String backendType = backendTypeForSection(sectionName);
|
||||
if (backendType == null) {
|
||||
return;
|
||||
}
|
||||
Button button = new Button(this);
|
||||
button.setText("Run Bench");
|
||||
button.setAllCaps(false);
|
||||
button.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
||||
button.setOnClickListener(v -> startBench(backendType));
|
||||
LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
buttonParams.topMargin = dp(6);
|
||||
contentLayout.addView(button, buttonParams);
|
||||
benchButtons.put(backendType, button);
|
||||
|
||||
LinearLayout results = new LinearLayout(this);
|
||||
results.setOrientation(LinearLayout.VERTICAL);
|
||||
LinearLayout.LayoutParams resultParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
resultParams.topMargin = dp(4);
|
||||
contentLayout.addView(results, resultParams);
|
||||
benchResultContainers.put(backendType, results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds one two-column check row (name | status chip) to the table. Rows with
|
||||
* a non-empty detail get a collapse indicator and toggle the detail text on tap;
|
||||
|
||||
Reference in New Issue
Block a user