// MobileGL - MobileGL/MG_State/GLState/TextureState/TextureObject.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 "TextureEnum.h" #include "MipmapUploadTargetArray.h" #include "MG_Util/Types.h" #include "../SamplerState/SamplerObject.h" #include #include #include namespace MobileGL::MG_State::GLState { // Texture objects are always SharedPtr-owned (TextureState creates every instance via // MakeShared, including the per-target default objects). enable_shared_from_this lets // backends that only receive a reference (e.g. syncing a name-deleted texture kept // alive by an FBO attachment) still register a weak liveness reference for GC. class ITextureObject : public std::enable_shared_from_this { public: using TargetEnum = TextureTarget; virtual ~ITextureObject() = default; virtual TextureStorageType GetStorageType() const = 0; virtual TextureInternalFormat GetFormat() const = 0; virtual TextureTarget GetTarget() const = 0; virtual const Vector& GetUploadTargets() const = 0; virtual IntVec3 GetBaseSize() const = 0; virtual const SharedPtr& GetSamplerObject() const = 0; virtual void SetInternalFormat(TextureInternalFormat format) = 0; virtual Bool IsComplete() const = 0; virtual Uint GetExternalIndex() const = 0; virtual const FloatVec4& GetBorderColor() const = 0; virtual void SetBorderColor(const FloatVec4& color) = 0; virtual const IntVec4& GetBorderColorI() const = 0; virtual void SetBorderColorI(const IntVec4& color) = 0; virtual const UintVec4& GetBorderColorUI() const = 0; virtual void SetBorderColorUI(const UintVec4& color) = 0; // Which of the three setters above last ran; see SamplerParameters::borderColorForm. virtual BorderColorForm GetBorderColorForm() const = 0; virtual TextureSwizzleParam GetSwizzleParam(TextureSwizzleParam param) const = 0; virtual void SetSwizzleParam(TextureSwizzleParam param, TextureSwizzleParam value) = 0; virtual void SetSwizzleParamRGBA(const Vec4& values) = 0; virtual const Vec4& GetAllSwizzleParams() const = 0; virtual const UintVec2& GetLevelRange() const = 0; virtual void SetBaseLevel(Uint baseLevel) = 0; virtual void SetMaxLevel(Uint maxLevel) = 0; virtual Bool IsImmutable() const = 0; virtual Uint GetImmutableLevels() const = 0; // How many levels THIS object can address, i.e. the bound a level argument has to // stay under. The same number as GetImmutableLevels() for an ordinary immutable // texture, but NOT for a view: GL 4.6 core 8.18 defines TEXTURE_IMMUTABLE_LEVELS on a // view as the ORIGINAL texture's value, which says nothing about what the view itself // can reach, and bounding by it lets a level the view does not have through. virtual Uint GetAddressableLevelCount() const = 0; virtual void SetImmutableLevels(Uint levels) = 0; virtual Uint16 GetTextureParamsVersion() const = 0; // Monotonic counter bumped on every CPU-side pixel mutation (see MarkStorageDirty). // 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; // Monotonic counter bumped on every SHAPE mutation - level sizes, the stored level // set, the internal format, the level range (see BumpShapeVersion). Disjoint from the // content version on purpose: glTexImage2D(..., nullptr) re-specifies a level's size // without dirtying a single texel, so a backend that keys its "nothing changed since // the last sync" skip on content alone keeps a resource of the OLD size alive. virtual Uint64 GetShapeVersion() 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; virtual void SetFixedSampleLocations(Bool fixedSampleLocations) = 0; virtual Uint64 GetLifetimeId() const = 0; // Which aspect of a packed depth/stencil texture a sampler reads (GL 4.6 core 8.10). // DEPTH_COMPONENT until set, and meaningless for every other format. virtual GLenum GetDepthStencilTextureMode() const = 0; virtual void SetDepthStencilTextureMode(GLenum mode) = 0; // ---- Texture views (ARB_texture_view / GL 4.6 core 8.18) ---- // The texture object whose immutable storage this one's texels actually live in, or // nullptr when this texture owns its storage. It is itself NEVER a view: glTextureView // composes a view-of-a-view onto the ROOT at creation, which is exactly what the spec's // additive " plus the value of TEXTURE_VIEW_MIN_LEVEL from the original // texture" rule describes, so one hop always reaches the storage. // // Holding it as a SharedPtr is what gives GL's name-deletion semantics for free: after // glDeleteTextures(origtexture) the name is gone and TextureState has dropped its entry, // but the object - and therefore the storage and every backend resource keyed on it - // stays alive as long as some view still references it (GL 4.6 core 5.1.2). virtual const SharedPtr& GetViewStorageOwner() const = 0; Bool IsTextureView() const { return GetViewStorageOwner() != nullptr; } // GL 4.6 core table 23.17, expressed in the storage owner's level/layer coordinates // (see above - composition makes the two the same number). All four are 0 on a mutable // texture; TexStorage* seeds them with (0, levels, 0, layers) because the spec makes an // immutable texture a full-extent view of itself, and glTextureView composes onto those. virtual Uint GetViewMinLevel() const = 0; virtual Uint GetViewNumLevels() const = 0; virtual Uint GetViewMinLayer() const = 0; virtual Uint GetViewNumLayers() const = 0; virtual void SetViewLevelLayerRange(Uint minLevel, Uint numLevels, Uint minLayer, Uint numLayers) = 0; protected: virtual Uint GetIndexOfTextureUploadTarget(TextureUploadTarget target) const = 0; }; class TextureObjectBase : public ITextureObject { public: TextureObjectBase(TextureTarget target, Uint externalIndex); #if MOBILEGL_PIPE_PUSH // P2 step e2. Out of line, and declared only where there is a notice to raise: in a // pull build this stays the implicit `= default` the pre-P2 tree had, which is what // keeps the pull build's symbol set byte-for-byte the pre-P2 one (G1). Declared on the // BASE, so every concrete texture class - 2D, 3D, cube, buffer, view - announces once. virtual ~TextureObjectBase(); #else virtual ~TextureObjectBase() = default; #endif TextureInternalFormat GetFormat() const override; TextureTarget GetTarget() const override; IntVec3 GetBaseSize() const override; const SharedPtr& GetSamplerObject() const override; void SetInternalFormat(TextureInternalFormat format) override; Bool IsComplete() const override; Uint GetExternalIndex() const override; const FloatVec4& GetBorderColor() const override; void SetBorderColor(const FloatVec4& color) override; const IntVec4& GetBorderColorI() const override; void SetBorderColorI(const IntVec4& color) override; const UintVec4& GetBorderColorUI() const override; void SetBorderColorUI(const UintVec4& color) override; BorderColorForm GetBorderColorForm() const override; TextureSwizzleParam GetSwizzleParam(TextureSwizzleParam param) const override; const Vec4& GetAllSwizzleParams() const override; void SetSwizzleParam(TextureSwizzleParam param, TextureSwizzleParam value) override; void SetSwizzleParamRGBA(const Vec4& values) override; const UintVec2& GetLevelRange() const override; void SetBaseLevel(Uint baseLevel) override; void SetMaxLevel(Uint maxLevel) override; Bool IsImmutable() const override; Uint GetImmutableLevels() const override; // m_immutableLevels is already the VIEW-relative count for a view (its constructor // stores there so the level-range clamp works in view coordinates), so // this one accessor is correct for both and needs no override. Uint GetAddressableLevelCount() const override { return m_immutableLevels; } void SetImmutableLevels(Uint levels) override; Uint16 GetTextureParamsVersion() const override; Uint64 GetContentVersion() const override; Uint64 GetShapeVersion() 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. void BumpContentVersion(); Int GetSamples() const override; void SetSamples(Int samples) override; Bool HasFixedSampleLocations() const override; void SetFixedSampleLocations(Bool fixedSampleLocations) override; Uint64 GetLifetimeId() const override; // A plain texture owns its storage; TextureObjectView overrides this. const SharedPtr& GetViewStorageOwner() const override; Uint GetViewMinLevel() const override { return m_viewMinLevel; } Uint GetViewNumLevels() const override { return m_viewNumLevels; } Uint GetViewMinLayer() const override { return m_viewMinLayer; } Uint GetViewNumLayers() const override { return m_viewNumLayers; } void SetViewLevelLayerRange(Uint minLevel, Uint numLevels, Uint minLayer, Uint numLayers) override { m_viewMinLevel = minLevel; m_viewNumLevels = numLevels; m_viewMinLayer = minLayer; m_viewNumLayers = numLayers; } GLenum GetDepthStencilTextureMode() const override { return m_depthStencilTextureMode; } // Bumps the params version like every other backend-visible texture parameter: the mode // decides which ASPECT of a packed depth/stencil image a sampler reads, which DirectGLES // forwards as a texture parameter and DirectVulkan bakes into the sampled image view. A // silent write here would leave both backends showing the aspect they last built. void SetDepthStencilTextureMode(GLenum mode) override { if (m_depthStencilTextureMode == mode) return; 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: static Uint64 AllocateLifetimeId(); // The ONLY way m_shapeVersion may move. Besides invalidating this object's own // completeness memo it bumps the context-wide sampling-resolution generation, which is // what a backend memo of the resolved per-unit bindings watches: completeness decides // whether a bound texture reaches its native target at all, and a shape change is // 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 calls the contract's own hooks // in MG_Pipe/PipeMutation.h - the same door BufferObject.cpp uses, and no MG_State // translation unit sees MG_Impl/Pipe/TextureEmit.h at all (c0b, ID-13). They stay // members rather than free calls so the cube's, the view's and the buffer texture's // translation units keep calling an inherited helper. // // resource_respecify, WHOLE-RESOURCE scope: the format setter and the three parameter // setters that move a DESCRIPTOR field without moving the shape (immutable levels, // sample count, fixed sample locations), and a view's creation. The emitter dedupes // this form on the built descriptor, so an over-call costs one 88-byte compare and // never an extra record. void PipePublishDescriptor(); // The PER-LEVEL and the CHAIN-CUT forms of the same call (P4a final review C-1). The // applier keeps a pending-upload set per (uploadTarget, level) and drops the entries // against the storage a respecify REPLACES - and the descriptor cannot tell it which: // AllocateStorage is per level and TruncateMipmapLevels removes a tail, while the // descriptor carries the base extent and the level count only. So the storage entry // points state the scope themselves; the whole-resource form above is for the calls // that really redefine the whole store. A per-level form is NOT deduped on the // descriptor: a non-base level redefined at a new size moves no descriptor field, and // the applier's box against the old level has to go regardless. void PipePublishLevelDescriptor(TextureUploadTarget uploadTarget, Uint mipmapLevel); void PipePublishTruncatedDescriptor(TextureUploadTarget uploadTarget, Uint levelCount); // set_texture_params, from every mutator that bumps m_textureParamsVersion. void PipePublishParams(); // The sub-data DRAIN LIST's append, on a level's first dirty mark. There is no clean // arm: the contract's hook (MG_Pipe/PipeMutation.h) carries no `dirty` flag, and a // level that goes clean is collected at the next drain, where !IsStorageDirty is the // first test EmitOneLevel makes. void PipeNoteLevelDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel); #endif const Uint m_externalIndex; const Uint64 m_lifetimeId; const TextureTarget m_target = TextureTarget::Unknown; TextureInternalFormat m_internalFormat = TextureInternalFormat::Unknown; SharedPtr m_sampler = nullptr; Vec4 m_swizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green, TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha}; 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; GLenum m_depthStencilTextureMode = GL_DEPTH_COMPONENT; // GL 4.6 core table 23.17: all four are 0 until immutable storage exists, which is what // makes glGetTexParameteriv(GL_TEXTURE_VIEW_NUM_LEVELS) answer 0 on a mutable texture. Uint m_viewMinLevel = 0; Uint m_viewNumLevels = 0; Uint m_viewMinLayer = 0; Uint m_viewNumLayers = 0; Int m_samples = 0; Bool m_fixedSampleLocations = true; }; class TextureObjectMipmap : public TextureObjectBase { public: TextureObjectMipmap(TextureTarget target, Uint externalIndex) : TextureObjectBase(target, externalIndex) {} TextureStorageType GetStorageType() const override { return TextureStorageType::Mipmap; } virtual Uint GetMipmapLevelCount() const = 0; virtual const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const = 0; virtual const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const = 0; virtual void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) = 0; // AllocateStorage only ever grows the chain. Callers that define the complete level set - // glTexStorage*, mip regeneration, or a level-0 respecification at a new size - drop the // leftovers explicitly, so a stale tail can never make the texture silently incomplete. virtual void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) = 0; virtual void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) = 0; 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)}}; } // Scatter detail behind GetStorageDirtyRegion: up to maxRects disjoint rects // that together cover every dirty texel, so ~100 sprite writes in a big atlas // need not be uploaded as one atlas-sized box. Returns how many rects were // written to outRects; 0 means "no list, upload the union box" and is always a // safe answer - this base fallback keeps whole-level semantics for storage // classes that do not track rects, and backends OPT IN by calling this. virtual SizeT GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapDirtyRegion* outRects, SizeT maxRects) const { (void)uploadTarget; (void)mipmapLevel; (void)outRects; (void)maxRects; return 0; } // 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 // stays uncompressed and correctly sized, so nothing in the backend upload path has to know // these exist; only glGetCompressedTexImage, glGetCompressedTextureImage and the // GL_TEXTURE_COMPRESSED* level queries read them. virtual void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat, const void* data, SizeT size) = 0; // GL_NONE when this level is not stored compressed. virtual GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; virtual SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; virtual const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; // The compressed internalformat the level was REQUESTED with, recorded even when MobileGL // answered it with uncompressed storage (the six generic GL_COMPRESSED_* enums) - see // MipmapStorage. Only the entry points GL forbids on a compressed image read it. virtual void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat) = 0; // GL_NONE when the level was not requested with a compressed internalformat. virtual GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; }; // Cheap replacement for dynamic_cast on the hot path: TextureObjectMipmap is the // only hierarchy branch whose storage type reports Mipmap, so the tag check makes // the static_cast safe. inline TextureObjectMipmap* AsMipmapTexture(ITextureObject* texture) { return (texture && texture->GetStorageType() == TextureStorageType::Mipmap) ? static_cast(texture) : nullptr; } // Whether the texture satisfies the mipmap-completeness rules a minification filter // that samples the mip chain imposes (GL 4.6 core 8.17): every level from the base to // the effective max must exist at exactly half the previous one's size. `mipmapped` is // the effective sampler's answer to "does this filter read more than the base level" - // when it is false only base-level completeness matters, which the ordinary // IsComplete() already covers. Sampling an incomplete texture returns (0, 0, 0, 1). Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped); // The rule above asked as the backends need it: does a lookup on this texture read // (0, 0, 0, 1) instead of its contents? `effectiveSampler` is the sampler object bound // to the unit when there is one, otherwise the texture's own. A backend answers yes by // routing the texture to whatever it already uses for "nothing is bound there". Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler); inline const TextureObjectMipmap* AsMipmapTexture(const ITextureObject* texture) { return (texture && texture->GetStorageType() == TextureStorageType::Mipmap) ? static_cast(texture) : nullptr; } // The per-target default texture objects (name 0) sit permanently in every texture unit's // binding slots, so "nothing useful bound" is no longer a null slot. While a default texture // has never been given an image (its internal format is still Unknown) it can contribute // nothing to sampling; backends treat such a binding exactly like the old empty slot and // skip per-draw sync/bind work for it. Once an application defines an image on a default // texture it loses this shortcut and is synced like any other texture. inline Bool IsUndefinedDefaultTexture(const ITextureObject* texture) { return texture != nullptr && texture->GetExternalIndex() == 0 && texture->GetFormat() == TextureInternalFormat::Unknown; } class TextureObjectWithOneMipmap : public TextureObjectMipmap { public: TextureObjectWithOneMipmap(TextureTarget target, Uint externalIndex) : TextureObjectMipmap(target, externalIndex) {} virtual ~TextureObjectWithOneMipmap() = default; Uint GetMipmapLevelCount() const override; const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const override; const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const override; void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) override; void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) override; void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) override; 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; SizeT GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapDirtyRegion* outRects, SizeT maxRects) const override; void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat, const void* data, SizeT size) override; GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat) override; GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; IntVec3 GetBaseSize() const override; Bool IsComplete() const override; protected: MipmapUploadTargetArray<1> m_textureStorage; }; } // namespace MobileGL::MG_State::GLState