[Feat] (Pipe, State): mint a {slot, gen} handle for every texture and renderbuffer and publish its create, respecify, parameters and accumulated sub-data as pipe calls

This commit is contained in:
2026-09-08 16:52:10 -04:00
committed by rereview
parent 92dffb81b4
commit 612dbac44f
9 changed files with 1247 additions and 41 deletions
File diff suppressed because it is too large Load Diff
@@ -25,15 +25,22 @@ namespace MobileGL::MG_State::GLState {
#if MOBILEGL_PIPE_PUSH
FramebufferObject::~FramebufferObject() {
// P2 step e2: ANNOUNCE the death instead of leaving the backend to discover it in a
// garbage sweep. This is the last SharedPtr to this object dropping - not the
// glDelete* that only marks the name and leaves a still-bound object very much
// alive - so it is the exact moment the backend's twin, and the driver storage
// that twin owns, stop being reachable. The notice carries the lifetime id
// because the object no longer exists to be passed, and because the lifetime id
// is what the client slot allocator resolves the handle from. No-op unless a
// backend registered the ops (a pull build declares none at all).
NotifyStateObjectDestroyed(MG_Pipe::MGPipeKind::Framebuffer, m_lifetimeId);
// P4a D-I2: A FRAMEBUFFER HAS A HANDLE AND NO WIRE LIFETIME. PipeCalls.def carries
// resource_destroy and five delete_* rows and NO framebuffer delete, because a
// framebuffer is not a resource and is not a CSO - it is STATE, and
// set_framebuffer_state is the only call that names one - and the catalogue is closed,
// so P4a invents no row. The helper is therefore steps 2 and 3 only: the death notice,
// raised while the handle still resolves (this is the P2 step-e2 announcement that used
// to stand here alone - the last SharedPtr to this object dropping, not the glDelete*
// that only marks the name and leaves a still-bound object very much alive), and then
// the slot.
//
// What makes a dangling Fbo unreachable is the frontend's own
// MarkFramebufferObjectForDeletion path, which already rebinds any slot holding the
// victim to framebuffer 0; and a RECYCLED framebuffer handle can never be suppressed
// against its predecessor's record, because Fbo carries Gen and Gen is inside the
// record's ContentHash.
MG_Pipe::MGPipeEmitFramebufferDestroyAndFree(m_lifetimeId);
}
#endif
@@ -9,6 +9,13 @@
#include "RenderbufferObject.h"
#include <MG_Util/Metrics/TextureMetrics.h>
#include <MG_State/GLState/StateObjectDeathNotice.h>
#if MOBILEGL_PIPE_PUSH
// The second and last MG_State translation unit that sees the client's texture emitter. A
// renderbuffer has no base class to hang protected helpers on and exactly one .cpp, so the
// include is the whole coupling; see MG_Impl/Pipe/TextureEmit.h's header comment for why the
// declaration cannot live in MG_Pipe/PipeMutation.h this phase.
#include <MG_Impl/Pipe/TextureEmit.h>
#endif
#include <atomic>
@@ -25,19 +32,27 @@ namespace MobileGL {
return g_nextRenderbufferLifetimeId.fetch_add(1, std::memory_order_relaxed);
}
RenderbufferObject::RenderbufferObject(Uint externalIndex) : m_externalIndex(externalIndex) {}
RenderbufferObject::RenderbufferObject(Uint externalIndex) : m_externalIndex(externalIndex) {
#if MOBILEGL_PIPE_PUSH
// P4a D-D1: resource_create from the constructor, carrying no storage. A
// renderbuffer is an INDEPENDENT class on the wire - it shares MGPResourceDesc's
// shape with textures and buffers and nothing else - and its handle is minted
// whatever the subsystem bitmask says, because MGPSurface::Res names it out of the
// framebuffer subsystem.
MG_Pipe::MGPipeMintAndCreateRenderbuffer(*this);
#endif
}
#if MOBILEGL_PIPE_PUSH
RenderbufferObject::~RenderbufferObject() {
// P2 step e2: ANNOUNCE the death instead of leaving the backend to discover it in a
// garbage sweep. This is the last SharedPtr to this object dropping - not
// glDeleteRenderbuffers, which only marks the name and leaves a still-bound object very much
// alive - so it is the exact moment the backend's twin, and the driver storage that
// twin owns, stop being reachable. The notice carries the lifetime id because the
// object no longer exists to be passed, and because the lifetime id is what the client
// slot allocator resolves the handle from. No-op unless a backend registered the ops
// (a pull build declares none at all).
NotifyStateObjectDestroyed(MG_Pipe::MGPipeKind::Renderbuffer, m_lifetimeId);
// P4a D-I1, the fixed three-step order: the wire delete first (published-gated,
// because a slot is not evidence of a record), then the death notice - the P2
// step-e2 announcement that used to stand here alone, raised while the handle
// still resolves - and the slot last. Steps 2 and 3 are the contract's helper;
// step 1 is this package's, one statement earlier, because the helper's file
// belongs to the contract package for the whole phase.
MG_Pipe::MGPipeEmitRenderbufferResourceDestroy(m_lifetimeId);
MG_Pipe::MGPipeEmitRenderbufferDestroyAndFree(m_lifetimeId);
}
#endif
@@ -96,17 +111,43 @@ namespace MobileGL {
void RenderbufferObject::SetInternalFormat(TextureInternalFormat format) {
m_internalFormat = format;
m_componentSizes = MG_Util::GetComponentSizesForInternalFormat(format);
#if MOBILEGL_PIPE_PUSH
PipePublishDescriptor();
#endif
}
void RenderbufferObject::AllocateStorage(IntVec2 size) {
m_width = size.x();
m_height = size.y();
m_allocated = true;
#if MOBILEGL_PIPE_PUSH
PipePublishDescriptor();
#endif
}
void RenderbufferObject::SetSamples(Int samples) {
m_samples = samples;
#if MOBILEGL_PIPE_PUSH
PipePublishDescriptor();
#endif
}
#if MOBILEGL_PIPE_PUSH
// D-D2: THE RENDERBUFFER PUBLICATION HOLE, CLOSED BY EMISSION AND NOT BY A NEW
// VERSION. These three setters bump no version and raise no notice, and the
// framebuffer dirty bit's shutter does not move when an ALREADY-ATTACHED renderbuffer
// is re-storaged - so `glBindRenderbuffer; glRenderbufferStorage(newSize)` on an
// attached renderbuffer was invisible to everything downstream. Emitting from the
// storage entry point closes it; a version counter here would resize the pull build's
// object and break G1, and widening the shutter would fire the framebuffer emission on
// an unrelated renderbuffer write.
//
// The emitter dedupes on the built descriptor, so glRenderbufferStorage's three-setter
// sequence publishes once rather than three times.
void RenderbufferObject::PipePublishDescriptor() {
MG_Pipe::MGPipeEmitRenderbufferRespecify(*this);
}
#endif
} // namespace GLState
} // namespace MG_State
} // namespace MobileGL
@@ -59,6 +59,13 @@ namespace MobileGL {
private:
static Uint64 AllocateLifetimeId();
#if MOBILEGL_PIPE_PUSH
// P4a D-D2: resource_respecify, from every storage-defining setter. Non-virtual
// and push-only, so the pull build's object layout is untouched (P4a's
// admitted-resize set is EMPTY); defined in RenderbufferObject.cpp, which is the
// one translation unit that includes the client emitter.
void PipePublishDescriptor();
#endif
Uint m_externalIndex = 0;
const Uint64 m_lifetimeId = AllocateLifetimeId();
@@ -12,6 +12,15 @@
#include "MG_Util/Types.h"
#include <MG_Util/Metrics/TextureMetrics.h>
#include <MG_Pipe/PipeMutation.h>
#if MOBILEGL_PIPE_PUSH
// THE ONE MG_State TRANSLATION UNIT THAT SEES THE CLIENT'S TEXTURE EMITTER, on purpose: the
// three PipePublish* helpers below are declared on TextureObjectBase and defined here, so the
// cube's, the view's and the buffer texture's translation units call an inherited member and
// still see only a declaration. That is the layering MG_Pipe/PipeMutation.h gives the buffer
// family; P4a cannot use that header because it belongs to the contract package for the whole
// phase and carries no texture row (TextureEmit.h's header comment has the full argument).
#include <MG_Impl/Pipe/TextureEmit.h>
#endif
namespace MobileGL {
namespace MG_State {
@@ -29,15 +38,44 @@ namespace MobileGL {
#if MOBILEGL_PIPE_PUSH
TextureObjectBase::~TextureObjectBase() {
// P2 step e2: ANNOUNCE the death instead of leaving the backend to discover it in a
// garbage sweep. This is the last SharedPtr to this object dropping - not the
// glDelete* that only marks the name and leaves a still-bound object very much
// alive - so it is the exact moment the backend's twin, and the driver storage
// that twin owns, stop being reachable. The notice carries the lifetime id
// because the object no longer exists to be passed, and because the lifetime id
// is what the client slot allocator resolves the handle from. No-op unless a
// backend registered the ops (a pull build declares none at all).
NotifyStateObjectDestroyed(MG_Pipe::MGPipeKind::Texture, m_lifetimeId);
// P4a D-I1: BACKEND-NEUTRAL FROM DAY ONE, and the three-step order is fixed.
//
// 1. the wire delete FIRST - it drops the applier's record while the record
// still exists, so a recycled slot cannot inherit a field. Published-gated
// rather than slot-gated: a backend twin table mints a slot through
// MGPipeSlots().Acquire whether or not the subsystem ever asked this client
// to emit a create, and a resource_destroy on such a handle is a refused
// call the applier counts and asserts on;
// 2. the death notice SECOND - it resolves the handle through the allocator,
// and a backend told after the free could no longer find its twin. This is
// the P2 step-e2 announcement that used to stand here alone: the last
// SharedPtr to this object dropping, not the glDelete* that only marks the
// name and leaves a still-bound object very much alive;
// 3. the slot LAST, and a double free on a stale generation is a proven no-op.
//
// Steps 2 and 3 - plus the SamplerViewCso minted off this same lifetime id - are
// the contract's helper. The BUILT-IN SAMPLER is deliberately not released from
// here: it is a real SamplerObject with its own lifetime id and its own
// destructor, which runs immediately after this body and takes the same helper
// shape. Step 1 is the client package's, one statement earlier, because the
// helper's file belongs to the contract package for the whole phase.
MG_Pipe::MGPipeEmitTextureResourceDestroy(m_lifetimeId);
MG_Pipe::MGPipeEmitTextureDestroyAndFree(m_lifetimeId);
}
// ---- P4a's three client emission points (see TextureObject.h) ----
void TextureObjectBase::PipePublishDescriptor() {
MG_Pipe::MGPipeEmitTextureRespecify(*this);
}
void TextureObjectBase::PipePublishParams() {
MG_Pipe::MGPipeEmitTextureParams(*this);
}
void TextureObjectBase::PipeNoteLevelDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel,
Bool dirty) {
MG_Pipe::MGPipeNoteTextureLevelDirty(*this, uploadTarget, mipmapLevel, dirty);
}
#endif
@@ -69,6 +107,24 @@ namespace MobileGL {
m_sampler->SetWrapT(SamplerWrapMode::ClampToEdge);
m_sampler->SetWrapR(SamplerWrapMode::ClampToEdge);
}
#if MOBILEGL_PIPE_PUSH
// P4a D-D1: A RESOURCE EXISTS BEFORE ANYTHING CAN NAME IT, so resource_create is
// emitted from the constructor and carries no storage - the store is defined
// lazily by the first respecify and every backend already tolerates a resource
// with none. The handle itself is minted whatever the subsystem bitmask says,
// because set_framebuffer_state and set_sampler_views name this texture by handle
// out of two different subsystems.
//
// NOTHING VIRTUAL IS TOUCHED HERE and that is a correctness requirement rather
// than a style: the derived object does not exist yet, and
// ITextureObject::GetStorageType is PURE - calling it from a base constructor is
// undefined behaviour. The target and the GL name are the two facts a create
// carries and both are plain members by this point; the storage kind is derived
// from the target, which is exact (TextureObjectBuffer is the only class that
// reports Buffer and TextureBuffer is the only target it is constructed with).
MG_Pipe::MGPipeMintAndCreateTexture(static_cast<ITextureObject*>(this), m_lifetimeId, target,
externalIndex);
#endif
}
TextureInternalFormat TextureObjectBase::GetFormat() const {
@@ -116,6 +172,14 @@ namespace MobileGL {
BumpShapeVersion();
++m_textureParamsVersion;
MGP_NOTE_AGGREGATE(TextureParams);
#if MOBILEGL_PIPE_PUSH
// The format is BOTH halves: a descriptor field the backend allocates from and a
// parameter the swizzle / depth-stencil push reads, so both records move. It is
// also the last statement of both glTexBuffer entry points, which is what
// publishes a buffer texture's window without a call site in MG_Impl/GLImpl.
PipePublishDescriptor();
PipePublishParams();
#endif
}
Uint TextureObjectBase::GetExternalIndex() const {
@@ -142,6 +206,9 @@ namespace MobileGL {
m_sampler->SetBorderColor(color);
++m_textureParamsVersion;
MGP_NOTE_AGGREGATE(TextureParams);
#if MOBILEGL_PIPE_PUSH
PipePublishParams();
#endif
}
const IntVec4& TextureObjectBase::GetBorderColorI() const {
@@ -157,6 +224,9 @@ namespace MobileGL {
m_sampler->SetBorderColorI(color);
++m_textureParamsVersion;
MGP_NOTE_AGGREGATE(TextureParams);
#if MOBILEGL_PIPE_PUSH
PipePublishParams();
#endif
}
const UintVec4& TextureObjectBase::GetBorderColorUI() const {
@@ -172,6 +242,9 @@ namespace MobileGL {
m_sampler->SetBorderColorUI(color);
++m_textureParamsVersion;
MGP_NOTE_AGGREGATE(TextureParams);
#if MOBILEGL_PIPE_PUSH
PipePublishParams();
#endif
}
BorderColorForm TextureObjectBase::GetBorderColorForm() const {
@@ -222,6 +295,9 @@ namespace MobileGL {
}
++m_textureParamsVersion;
MGP_NOTE_AGGREGATE(TextureParams);
#if MOBILEGL_PIPE_PUSH
PipePublishParams();
#endif
}
void TextureObjectBase::SetSwizzleParamRGBA(const Vec4<TextureSwizzleParam>& values) {
@@ -230,6 +306,9 @@ namespace MobileGL {
m_swizzleParams = values;
++m_textureParamsVersion;
MGP_NOTE_AGGREGATE(TextureParams);
#if MOBILEGL_PIPE_PUSH
PipePublishParams();
#endif
}
const UintVec2& TextureObjectBase::GetLevelRange() const {
@@ -249,6 +328,12 @@ namespace MobileGL {
++m_textureParamsVersion;
MGP_NOTE_AGGREGATE(TextureParams);
BumpShapeVersion();
#if MOBILEGL_PIPE_PUSH
// The level range is PARAMS, not a descriptor field: the record carries
// BaseLevel / MaxLevel and the storage is untouched. The descriptor is deduped on
// its own bytes, so the shape bump above costs nothing here.
PipePublishParams();
#endif
}
void TextureObjectBase::SetMaxLevel(Uint maxLevel) {
@@ -261,6 +346,9 @@ namespace MobileGL {
++m_textureParamsVersion;
MGP_NOTE_AGGREGATE(TextureParams);
BumpShapeVersion();
#if MOBILEGL_PIPE_PUSH
PipePublishParams();
#endif
}
Bool TextureObjectBase::IsImmutable() const {
@@ -281,6 +369,14 @@ namespace MobileGL {
}
++m_textureParamsVersion;
MGP_NOTE_AGGREGATE(TextureParams);
#if MOBILEGL_PIPE_PUSH
// Immutable is a descriptor fact the backend reads - and NOT a request for an
// acknowledgement: MGPipeResourceRespecifyNeedsAck names the buffer target
// explicitly, because texture allocation is lazy in monolith and stays lazy in
// split. The level clamp above is params.
PipePublishDescriptor();
PipePublishParams();
#endif
}
Uint16 TextureObjectBase::GetTextureParamsVersion() const {
@@ -319,6 +415,9 @@ namespace MobileGL {
m_samples = samples;
++m_textureParamsVersion;
MGP_NOTE_AGGREGATE(TextureParams);
#if MOBILEGL_PIPE_PUSH
PipePublishDescriptor();
#endif
}
Bool TextureObjectBase::HasFixedSampleLocations() const {
@@ -329,6 +428,9 @@ namespace MobileGL {
m_fixedSampleLocations = fixedSampleLocations;
++m_textureParamsVersion;
MGP_NOTE_AGGREGATE(TextureParams);
#if MOBILEGL_PIPE_PUSH
PipePublishDescriptor();
#endif
}
Uint64 TextureObjectBase::GetLifetimeId() const {
@@ -360,11 +462,23 @@ namespace MobileGL {
MipmapInput input) {
BumpShapeVersion();
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
#if MOBILEGL_PIPE_PUSH
// AFTER the allocation, never before: BumpShapeVersion runs first and a descriptor
// built there would describe the level set this call is about to change. Every
// storage-defining GL entry point - glTexImage*, glCompressedTexImage*,
// glTexStorage*, glTextureView and the generated-mip storage grow - reaches
// storage through here, which is what makes the emission complete without one call
// site per entry point in MG_Impl/GLImpl.
PipePublishDescriptor();
#endif
}
void TextureObjectWithOneMipmap::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
BumpShapeVersion();
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
#if MOBILEGL_PIPE_PUSH
PipePublishDescriptor();
#endif
}
void TextureObjectWithOneMipmap::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel,
@@ -383,6 +497,9 @@ namespace MobileGL {
MGP_NOTE_AGGREGATE(TextureContent);
}
m_textureStorage.MarkDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, dirty);
#if MOBILEGL_PIPE_PUSH
PipeNoteLevelDirty(uploadTarget, mipmapLevel, dirty);
#endif
}
Bool TextureObjectWithOneMipmap::IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const {
@@ -395,6 +512,13 @@ namespace MobileGL {
MGP_NOTE_AGGREGATE(TextureContent);
m_textureStorage.MarkDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, offset,
size);
#if MOBILEGL_PIPE_PUSH
// THE DRAIN LIST IS KEYED ON THE STORAGE OWNER FOR FREE: TextureObjectView
// forwards this call to the OWNER's method after remapping the level and the
// region, so an upload through a view and an upload through the owner arrive here
// on the same object with the same owner-side coordinates.
PipeNoteLevelDirty(uploadTarget, mipmapLevel, true);
#endif
}
MipmapDirtyRegion TextureObjectWithOneMipmap::GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
@@ -190,6 +190,12 @@ namespace MobileGL::MG_State::GLState {
m_depthStencilTextureMode = mode;
++m_textureParamsVersion;
MGP_NOTE_AGGREGATE(TextureParams);
#if MOBILEGL_PIPE_PUSH
// D-E3's whole point, at the one site that proves it: the depth-stencil mode of a
// texture that is ONLY the READ framebuffer's attachment reaches the driver, because
// set_texture_params is addressed by resource and is independent of every binding.
PipePublishParams();
#endif
}
protected:
@@ -201,6 +207,34 @@ namespace MobileGL::MG_State::GLState {
// otherwise invisible to such a memo (no bind moved).
void BumpShapeVersion();
#if MOBILEGL_PIPE_PUSH
// ---- P4a's client emission points (brief D-D1, D-D3, D-E1, D-I1) ----
//
// NON-VIRTUAL AND PUSH-ONLY, both deliberately: a virtual would grow the vtable and a
// member would grow the object, and P4a's admitted-resize set is EMPTY - every edit
// that reaches the pull build is inside this guard, so the pull build's symbol set is
// byte-for-byte the one it had before the phase.
//
// DECLARED HERE AND DEFINED IN TextureObject.cpp, which is the ONE MG_State
// translation unit that includes the client's MG_Impl/Pipe/TextureEmit.h. Every other
// texture .cpp - the cube's, the view's, the buffer texture's - calls the inherited
// helper and still sees only a declaration, which is the same layering
// MG_Pipe/PipeMutation.h gives the buffer family (that header is the contract
// package's for the whole phase and carries no texture row, which is why the
// declaration lives here instead; see TextureEmit.h's header comment).
//
// resource_respecify. Called from BumpShapeVersion and from the three parameter
// setters that move a DESCRIPTOR field without moving the shape (immutable levels,
// sample count, fixed sample locations). The emitter dedupes on the built descriptor,
// so an over-call costs one 88-byte compare and never an extra record.
void PipePublishDescriptor();
// set_texture_params, from every mutator that bumps m_textureParamsVersion.
void PipePublishParams();
// The sub-data DRAIN LIST. `dirty` false is a level going clean - a respecify, a
// truncation, or the emitter's own clear after an accepted record.
void PipeNoteLevelDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty);
#endif
const Uint m_externalIndex;
const Uint64 m_lifetimeId;
const TextureTarget m_target = TextureTarget::Unknown;
@@ -31,11 +31,20 @@ namespace MobileGL {
MipmapInput input) {
BumpShapeVersion();
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
#if MOBILEGL_PIPE_PUSH
// AFTER the allocation, for TextureObjectWithOneMipmap's reason: BumpShapeVersion
// runs first and a descriptor built there would describe the level set this call
// is about to change.
PipePublishDescriptor();
#endif
}
void TextureObject2DCube::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
BumpShapeVersion();
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
#if MOBILEGL_PIPE_PUSH
PipePublishDescriptor();
#endif
}
void TextureObject2DCube::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel,
@@ -53,6 +62,11 @@ namespace MobileGL {
MGP_NOTE_AGGREGATE(TextureContent);
}
m_textureStorage.MarkDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, dirty);
#if MOBILEGL_PIPE_PUSH
// SIX FACES, SIX BLOBS, SIX DRAIN KEYS: the upload target is the face, and it is
// what the sub-data record's Target byte carries beside the resource target.
PipeNoteLevelDirty(uploadTarget, mipmapLevel, dirty);
#endif
}
bool TextureObject2DCube::IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const {
@@ -65,6 +79,9 @@ namespace MobileGL {
MGP_NOTE_AGGREGATE(TextureContent);
m_textureStorage.MarkDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, offset,
size);
#if MOBILEGL_PIPE_PUSH
PipeNoteLevelDirty(uploadTarget, mipmapLevel, true);
#endif
}
MipmapDirtyRegion TextureObject2DCube::GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
@@ -29,6 +29,15 @@ namespace MobileGL {
void SetBufferRange(SizeT offset, SizeT size) {
m_bufferRangeOffset = offset;
m_bufferRangeSize = size;
#if MOBILEGL_PIPE_PUSH
// Both glTexBuffer entry points bind the backing buffer and then set the
// window, so this is the first statement at which the descriptor's
// BufferForTexBuffer / BufOffset / BufSize trio is complete. SetInternalFormat
// publishes again one statement later and is deduped away when the format did
// not move - which is exactly the case a re-attach of a DIFFERENT buffer at
// the same format would otherwise fall through.
PipePublishDescriptor();
#endif
}
SizeT GetBufferRangeOffset() const { return m_bufferRangeOffset; }
// Resolved against the buffer's current size, so kWholeBuffer tracks it.
@@ -79,6 +79,18 @@ namespace MobileGL::MG_State::GLState {
// to the view. GetImmutableLevels() forwards to the owner for the actual GL query, which
// GL 4.6 core 8.18 defines as the ORIGINAL texture's value.
SetImmutableLevels(numLevels);
#if MOBILEGL_PIPE_PUSH
// ViewOf, and it is published from HERE rather than left to SetImmutableLevels above,
// which early-returns when the composed level count happens to be the base class's
// current value - a degenerate view (the spec's min() composition narrowed to zero
// levels) would otherwise never publish the one field that makes it a view. The
// descriptor is deduped on its own bytes, so the ordinary case pays one compare.
//
// ONE HOP ALWAYS REACHES STORAGE: glTextureView composes a view-of-a-view onto the ROOT
// at creation, which is what the spec's additive min-level rule describes and what the
// assertion above pins, so the owner named here is never itself a view.
PipePublishDescriptor();
#endif
}
Uint TextureObjectView::GetImmutableLevels() const {