Files
MobileGL/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h
T
swung0x48 1f8de61be1 [MG_Remote, MG_Pipe, MG_Impl, MG_State, MG_Backend, MG_Test, MG_IntegrationTest] (Disaggregated): P5d round 3 - the profile-directed cuts on both roles
- The round-2 report guessed the apply thread's ~48% mutex time was the backend
  pendingMutex; the symbolized call graph at 56a77348 says it is the idle poll in
  ServerLoop::ApplyThreadMain taking m_controlMutex twice per iteration
  (ControlIsPending in the ready lambda, PumpControlRequest), with DrainRing at
  29% of the thread. Every package below is cut from that graph, not from self%.
- T (transport): the control mailbox gets an atomic shadow bit m_controlPosted so
  the idle poll takes no lock (handshake, C2 critical section and the
  NOT_INITIALIZED answer unchanged); Doorbell::Wait spins on a once-calibrated
  iteration budget and reads no clock in steady state (the park phase alone
  computes deadlines); client/server wait and park counts ride the stats line
  as wait[srv= srvpark= cli= clipark=] and the wire ledger as cliwait=/clipark=.
- D (role guards): OnApplyThread() is inline - one relaxed atomic key against the
  caller's thread pointer (aarch64 __builtin_thread_pointer, else
  std::this_thread::get_id) - and m_applyThreadId, the function-static guard and
  the pthread_equal call are gone; the three scope depth counters stop being
  thread_local (their only reader is the apply-thread guard, so they count only
  when OnApplyThread() is true; a GL-thread scope no longer exempts the apply
  thread, pinned by a new case); the guard chains are ordered cheapest-first.
- B (persistent maps): the shadow allocator rounds the SIZE to a page multiple, so
  TrackWriteMap aligns OUTWARD when the shadow is page-granular - a sub-page map
  is one protected page, edges and their per-draw XXH3 disappear; the inward arm
  stays as the fallback. Review found a hole the edge hashes had hidden since
  round 1: a map that faulted but was not consumed by the walk that advanced the
  epoch was never pushed. The walk now drains every marked member before the
  binding walk, registration bumps the epoch so a fresh map ships once on the
  draw path, and a non-4 KB kernel page refuses the mprotect arm by name.
- C (client per verb): an image-unit high-water mark (push build only, fed by
  glBindImageTexture) replaces the 192-unit sweep per draw; the residual fill's
  7-term supplied conjunction is a memo keyed on its four inputs; the server's
  63-field stamp per verb is a constexpr per-class mask table.
- generate_mipmap no longer skips its wait under MOBILEGL_IPC_BATCH_WAITS: its sink
  is the backend GenerateMipmap, which reads MGB_CTX->GetActiveTextureUnit(), and
  a batched publish let the next validate move the serial under it -
  Fatal{UnmigratedPipeInput, "GetActiveTextureUnit@ReadPixels"} on the gate tree.
  The rule is written at the site: a record may skip its wait only if its apply
  reads nothing the residual fill writes. The texture-death scenario gets the
  framebuffer case's fence before it reads the object_death tally.
- Redmi 2f7cbe2e, FCL + Minecraft 26.3-rc-3, VD12, CPU pinned: inproc 64.3 fps
  (client 15.0 ms/frame, apply 12.4) -> 103-114 fps (client 8.7-9.2 ms/frame on a
  mid core, apply 7.0-7.4); monolith 115 (120 Hz vsync-capped runs) / 206 (one
  uncapped run, 5.0 ms/frame on the big core). Gates: unit 2203/2203,
  integration-split 111/111 (the two formerly flaky cases 6/6 on repeat),
  generators, include closure, dirty surface and doc citations green.
2026-09-18 04:28:25 -04:00

171 lines
11 KiB
C++

// MobileGL - MobileGL/MG_State/GLState/TextureState/MipmapStorage.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 <algorithm>
#include "TextureEnum.h"
#include "MG_Util/Types.h"
#include "MG_Util/Math/VectorTypes.h"
#include "TextureTypes.h"
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);
}
SizeT TexelCount() const {
if (Empty()) return 0;
return static_cast<SizeT>(hi.x() - lo.x()) * static_cast<SizeT>(hi.y() - lo.y()) *
static_cast<SizeT>(hi.z() - lo.z());
}
};
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (gt, CONTRACT-P5C §6 layer 1): the NAMED EXEMPTION to the texture legacy-arm
// guard, the texture analogue of hd's MGPipeFrontendKeyedRegistryScope
// (MG_Impl/Pipe/SlotAllocator.h). Magma's texture sync (VkTextureManager::SyncTexture
// and the UploadDirtyMipLevels it drives) still reads and clears the CLIENT's mip
// shadow - the one texture family tx did not migrate (CONTRACT-P5C §2 names Espryt's
// sync reads and Magma's T5 writes; Magma's upload read path is absent from it), and
// inproc was green on it because the barrier and the shared address space held.
// Inside this scope the guard's surfaces stay legal - read-only-or-clean, barrier-
// held, and greppable as exactly this name; retiring the scope is P7's server-side
// Magma texture sync. Every OTHER apply-thread touch of a guarded surface is still
// Fatal{RoleViolation, "texture-legacy-arm"}.
//
// The depth is counted ONLY on the apply thread (P5d round 3, package D), exactly
// as hd's two scopes in MG_Impl/Pipe/SlotAllocator.h are: the guard above is the
// counter's only reader and it returns before it looks unless
// ServerLoop::OnApplyThread() is true, so a depth kept on any other thread could
// never change an answer. m_counted remembers the constructor's decision so the
// destructor undoes exactly what the constructor did. The query is spelled
// ActiveOnApplyThread() rather than Active() because the precondition belongs in
// the name: off the apply thread it now answers false however many scopes are
// open, and a future second reader must be made to see that rather than trust a
// bare Active().
class MGPipeTextureLegacyArmScope {
public:
MGPipeTextureLegacyArmScope();
~MGPipeTextureLegacyArmScope();
MGPipeTextureLegacyArmScope(const MGPipeTextureLegacyArmScope&) = delete;
MGPipeTextureLegacyArmScope& operator=(const MGPipeTextureLegacyArmScope&) = delete;
static Bool ActiveOnApplyThread();
private:
Bool m_counted;
};
#endif
class MipmapStorage {
public:
SizeT GetLevelCount() const;
void AllocateLevel(Uint level, MipmapInput input);
// Discard every level at or above levelCount. AllocateLevel never shrinks, so this
// is the only way a chain gets shorter - use it where the caller defines the whole
// level set (glTexStorage*, mip regeneration, atlas respecification).
void TruncateToLevelCount(SizeT levelCount);
void UpdateSubData(Uint level, DataPtr input);
void* MapData(Uint level);
IntVec3 GetTexelSize(Uint level) const;
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;
// Behind the union box, the level keeps up to kMaxDirtyRects pairwise
// disjoint rects recording WHERE the writes actually landed. A frame of
// ~100 scattered sprite updates in a big atlas has a union box that
// covers nearly the whole level while the touched texels are ~5% of it;
// the union box stays the source of truth (every write funnels through
// MarkDirty/MarkDirtyRegion into BOTH representations), backends OPT IN
// to the list purely as an upload-size refinement. 96 slots because the
// pattern this exists for is Minecraft's ~100 sprites/frame: a 16-slot
// list forced into far-apart merges was measured at >90% of the union
// box's area on exactly that pattern, i.e. worthless. Inserts merge any
// touching/overlapping rect (cascading, so the list stays disjoint);
// when full, the incoming rect folds into the neighbour whose box grows
// least and the list degrades gracefully toward the union box.
static constexpr SizeT kMaxDirtyRects = 96;
// Copies the level's dirty rects into outRects and returns how many were
// written. 0 means "upload the union box instead" and covers every
// reason at once: tracking unavailable, a single rect (identical to the
// union box by construction), more rects than maxRects, or a summed
// area so close to the union box's that one big upload beats many small
// ones (fewer driver calls wins when the bytes are nearly equal).
SizeT GetDirtyRects(Uint level, MipmapDirtyRegion* outRects, SizeT maxRects) 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
// glGetCompressedTexImage to return the image *as stored*, and no backend here has a
// BC/ETC codec, so a re-encode could never be byte-exact; at the same time m_data has
// to keep the "width * height * bytes-per-texel" layout that the backend upload
// sizing, glGenerateMipmap's bytes-per-texel division and the pixel-store packer all
// divide by. Two parallel vectors, one invariant preserved. Call order is
// AllocateLevel then SetCompressedImage - AllocateLevel clears the tag, so a plain
// glTexImage2D over the level un-compresses it.
void SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size);
// GL_NONE when the level is not stored compressed.
GLenum GetCompressedFormat(Uint level) const;
SizeT GetCompressedByteSize(Uint level) const;
const void* MapCompressedData(Uint level) const;
// The compressed internalformat the application ASKED for, which is not the same
// question as the one above: the six generic GL_COMPRESSED_* enums let the
// implementation choose, MobileGL chooses uncompressed storage, and the level is
// deliberately left untagged so GL_TEXTURE_COMPRESSED keeps answering false and
// glGetCompressedTexImage is not handed a blob nothing ever compressed. The entry
// points that must refuse a compressed image outright (glClearTexImage /
// glClearTexSubImage, GL 4.6 core 8.19) still need to know, so the request is
// recorded separately. Set right after AllocateLevel, which clears it.
void SetRequestedCompressedFormat(Uint level, GLenum internalFormat);
// GL_NONE when the level was not requested with a compressed internalformat.
GLenum GetRequestedCompressedFormat(Uint level) const;
protected:
// Insert one clamped, non-empty write box, keeping the list disjoint
// and bounded (see kMaxDirtyRects).
void InsertDirtyRect(Uint level, MipmapDirtyRegion incoming);
Vector<IntVec3> m_texelSizes;
Vector<Vector<Uint8>> m_data;
Vector<bool> m_isDirty;
Vector<MipmapDirtyRegion> m_dirtyRegions;
// Per level, the disjoint rect list behind m_dirtyRegions' union box.
// An EMPTY list is the common resting state and always means "the union
// box is the whole story" - clean levels, whole-level dirties and
// respecifies all just clear it, so plain full-level uploads never pay
// a heap allocation; the first scattered MarkDirtyRegion on an
// already-dirty level seeds the list from the union box accumulated so
// far and refines from there.
Vector<Vector<MipmapDirtyRegion>> m_dirtyRects;
Vector<Vector<Uint8>> m_compressedData;
Vector<GLenum> m_compressedFormats;
Vector<GLenum> m_requestedCompressedFormats;
};
} // namespace GLState
} // namespace MG_State
} // namespace MobileGL