[Perf] (MG_Backend, MG_State): stop re-resolving texture unit bindings on every draw

DirectGLES re-derived the whole texture binding state for every draw: for each
touched unit, two alias-resolution passes over all binding slots, then a third
walk to unbind native targets nothing claimed, then the sampler. With the
Minecraft-shaped bench that was 13.2% of the render thread in BindCurrentTextures
alone, plus 4.6% in SyncNeccessaryTextures deciding which textures to consider.
The answer is identical across a whole terrain batch.

The resolution is now memoised, and what makes replaying it as a no-op legitimate
is that the memo does not merely trust a key: it compares the backend's own bound
texture shadow against the one resolution left behind. Every path that binds a
texture behind this function's back already maintains that shadow - the scratch
bind an upload does on the temp unit, CopyTexSubImage2D and GenerateMipmap
binding on the active unit, the glBindTextures fast path, the scrub a backend
texture performs when it is destroyed or respecified - so a memcmp catches all of
them without having to enumerate them. On top of that the key covers the texture
bind generation, the program that arbitrates aliased targets (pointer, lifetime
id, backend state version, link status), and the ES context generation.

Two invalidation sources had no signal at all and needed one. Mipmap completeness
decides whether a texture is bound in the first place, and it moves with texture
shape and with the effective sampler's filter - so a sampling-resolution
generation now moves with both, routed through single choke points
(TextureObjectBase::BumpShapeVersion, SamplerObject::BumpVersion) so a future
bump site cannot forget it. A texture context id was needed because both
generations restart at zero in a new GLContext, which can land on the old heap
address.

This also closes a pre-existing hole rather than working around it:
glDeleteSamplers unbinds the sampler from every unit straight through
TextureUnit::SetSamplerObject, bypassing the touch bookkeeping, so that setter now
bumps the bind generation on a real change. The sampler bind step itself stays
outside the memo and runs every draw - the program's raw-depth-fetch substitution
rewrites unit samplers immediately afterwards, so a memo there could never hit.

ns per draw, DriverBench on a GTX 1660 SUPER (native / Espryt):
mc_vanilla_draw 253 / 2037->1315, mc_ubo_range 202 / 1684->955,
mc_sodium_multidraw 739 / 3939->3150. Espryt goes from 8.3x to 4.7x the native
driver on the per-draw uniform-range case. Magma is unaffected (the MG_State
additions are counter bumps), and no case regressed.

Unit tests 421/421.
This commit is contained in:
BZLZHH
2026-08-06 07:41:10 -04:00
parent 62dea3bea4
commit 49aab57f03
10 changed files with 294 additions and 45 deletions
@@ -28,12 +28,12 @@ namespace MobileGL {
void TextureObject2DCube::AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
MipmapInput input) {
++m_shapeVersion;
BumpShapeVersion();
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
}
void TextureObject2DCube::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
++m_shapeVersion;
BumpShapeVersion();
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
}