[Perf] (MG_Backend/DirectGLES, MG_Impl): cache link-time lookups, bound unit scans, honor eglSwapInterval

- BackendProgramObjectImpl::CacheResourceLocations resolves every
  glGetUniformBlockIndex / glGetUniformLocation string query once per
  link and establishes the block binding points there. Per draw,
  BindCurrentProgramWithResources now uses the cached indices, re-issues
  glUniform1i only when a sampler's unit actually changed (program state
  persists), uploads the global UBO only when its content version moved,
  and skips redundant glUseProgram binds (guard reset on program-name
  reuse, MakeCurrent, and every explicit glUseProgram(0)). The caches are
  invalidated through ProgramObject's link version, which also makes a
  relinked program finally re-sync its backend program.
- Track a texture-unit high-water mark (fed by glBindTexture /
  glBindTextureUnit / glBindSampler / glBindImageTexture) so the two
  per-draw unit scans (MAX_TEXTURE_IMAGE_UNITS is 192) and the
  texture-deletion unbind loop only walk units that were ever touched.
- Forward the app's eglSwapInterval to the native EGL surface through a
  new BackendObject::SetEGLSwapInterval hook (applied immediately when
  the surface exists, otherwise deferred to surface creation /
  MakeCurrent). "VSync off" finally reaches the hardware - DirectGLES
  was hard-locked to the display refresh before.

The driver-side cost of the per-draw string lookups was about half of a
30% Adreno driver hotspot; libMobileGL's share of the vanilla render
thread fell from 22% to 9% (simpleperf, Adreno 830).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 12:12:51 +00:00
co-authored by Claude Fable 5
parent bd208783d7
commit a6a5edf573
14 changed files with 255 additions and 84 deletions
@@ -52,7 +52,16 @@ namespace MobileGL::MG_State::GLState {
Bool ValidateName(Uint index) const;
Bool ValidateTextureObject(Uint index) const;
// 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) {
if (unit > m_maxTouchedUnit && unit < MAX_TEXTURE_IMAGE_UNITS) m_maxTouchedUnit = unit;
}
Int GetMaxTouchedUnit() const { return m_maxTouchedUnit; }
private:
Int m_maxTouchedUnit = -1;
Int m_activeTextureUnit = 0;
Array<TextureUnit, MAX_TEXTURE_IMAGE_UNITS> m_textureUnits;
Array<ImageTextureBinding, MAX_TEXTURE_IMAGE_UNITS> m_imageTextureBindings;