Files
MobileGL/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp
T

540 lines
26 KiB
C++

// MobileGL - MobileGL/MG_State/GLState/TextureState/TextureObject.cpp
// 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
#include "TextureObject.h"
#include "MG_State/GLState/Core.h"
#include "MG_Util/Types.h"
#include <MG_Util/Metrics/TextureMetrics.h>
namespace MobileGL {
namespace MG_State {
namespace GLState {
static std::atomic<Uint64> s_nextTextureLifetimeId = 1;
// Defined further down next to the other sampling-completeness rules; the
// memo in TextureObjectBase is its only caller.
static Bool ComputeMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped);
// TextureObjectBase implementations
Uint64 TextureObjectBase::AllocateLifetimeId() {
return s_nextTextureLifetimeId.fetch_add(1, std::memory_order_relaxed);
}
void TextureObjectBase::BumpShapeVersion() {
++m_shapeVersion;
// Shape is what mipmap-completeness is computed from, and completeness decides
// whether a backend binds this texture on its unit at all. Nothing else tells a
// backend memo of the resolved per-unit bindings that the answer moved - no bind
// changed and the texel content may be untouched. Proxy textures (used only to
// answer PROXY queries) are never bound, so their shape churn costs a memo
// invalidation for nothing; that is accepted rather than filtered, because a
// missed bump renders wrong pixels while a spare bump only costs one re-resolve.
if (pGLContext) pGLContext->BumpSamplingResolutionGeneration();
}
TextureObjectBase::TextureObjectBase(TextureTarget target, Uint externalIndex)
: m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()), m_target(target) {
m_sampler = MakeShared<SamplerObject>(0);
if (target == TextureTarget::TextureRectangle) {
// A rectangle texture has no mip chain, so its initial sampler state is not
// the shared one: TEXTURE_MIN_FILTER is LINEAR and TEXTURE_WRAP_S/T are
// CLAMP_TO_EDGE (GL 4.6 core table 23.15). Leaving the 2D default of
// NEAREST_MIPMAP_LINEAR in place makes the texture mipmap-incomplete from
// birth, and every lookup that the application never re-filtered reads
// (0, 0, 0, 1) instead of its contents.
m_sampler->SetMinFilter(SamplerFilterMode::Linear);
m_sampler->SetMipmapMode(SamplerMipmapMode::None);
m_sampler->SetWrapS(SamplerWrapMode::ClampToEdge);
m_sampler->SetWrapT(SamplerWrapMode::ClampToEdge);
m_sampler->SetWrapR(SamplerWrapMode::ClampToEdge);
}
}
TextureInternalFormat TextureObjectBase::GetFormat() const {
return m_internalFormat;
}
TextureTarget TextureObjectBase::GetTarget() const {
return m_target;
}
IntVec3 TextureObjectBase::GetBaseSize() const {
return {0, 0, 0};
}
const SharedPtr<SamplerObject>& TextureObjectBase::GetSamplerObject() const {
return m_sampler;
}
Bool TextureObjectBase::IsComplete() const {
if (m_internalFormat == TextureInternalFormat::Unknown) {
return false;
}
return true;
}
void TextureObjectBase::SetInternalFormat(TextureInternalFormat format) {
if (format == m_internalFormat) return;
// A default texture (name 0) changes IsUndefinedDefaultTexture on the
// Unknown<->defined transition, which changes per-draw sampled-set membership
// without any bind happening; bump the bind generation so cached sampled sets
// re-resolve instead of replaying the stale membership. The identity check
// excludes the other externalIndex-0 objects (proxy textures, default-FBO
// attachments) whose definedness never feeds sampled-set membership, so e.g.
// proxy probes cannot churn the cache.
if (m_externalIndex == 0 && pGLContext &&
(m_internalFormat == TextureInternalFormat::Unknown) !=
(format == TextureInternalFormat::Unknown) &&
pGLContext->GetDefaultTextureObject(GetTarget()).get() == this) {
pGLContext->BumpTextureBindGeneration();
}
m_internalFormat = format;
BumpShapeVersion();
++m_textureParamsVersion;
}
Uint TextureObjectBase::GetExternalIndex() const {
return m_externalIndex;
}
// TEXTURE_BORDER_COLOR is sampler state, so it lives on the SamplerObject this texture
// owns rather than being duplicated here - a sampler object bound over the texture then
// supplies its own, exactly as GL says it should. The texture params version still moves
// on a write, because the DirectGLES texture sync memoises on it.
const FloatVec4& TextureObjectBase::GetBorderColor() const {
return m_sampler->GetBorderColor();
}
// The redundancy filters test the FORM as well as the value: the derived representations
// make a float (0,0,0,1) and an integer (0,0,0,1) numerically identical, but they are
// different GL state and the DirectGLES sync memoises on m_textureParamsVersion.
void TextureObjectBase::SetBorderColor(const FloatVec4& color) {
if (color == m_sampler->GetBorderColor() &&
m_sampler->GetBorderColorForm() == BorderColorForm::Float) {
return;
}
m_sampler->SetBorderColor(color);
++m_textureParamsVersion;
}
const IntVec4& TextureObjectBase::GetBorderColorI() const {
return m_sampler->GetBorderColorI();
}
void TextureObjectBase::SetBorderColorI(const IntVec4& color) {
if (color == m_sampler->GetBorderColorI() &&
m_sampler->GetBorderColorForm() == BorderColorForm::Int) {
return;
}
m_sampler->SetBorderColorI(color);
++m_textureParamsVersion;
}
const UintVec4& TextureObjectBase::GetBorderColorUI() const {
return m_sampler->GetBorderColorUI();
}
void TextureObjectBase::SetBorderColorUI(const UintVec4& color) {
if (color == m_sampler->GetBorderColorUI() &&
m_sampler->GetBorderColorForm() == BorderColorForm::Uint) {
return;
}
m_sampler->SetBorderColorUI(color);
++m_textureParamsVersion;
}
BorderColorForm TextureObjectBase::GetBorderColorForm() const {
return m_sampler->GetBorderColorForm();
}
TextureSwizzleParam TextureObjectBase::GetSwizzleParam(TextureSwizzleParam param) const {
switch (param) {
case TextureSwizzleParam::Red:
return m_swizzleParams.r();
case TextureSwizzleParam::Green:
return m_swizzleParams.g();
case TextureSwizzleParam::Blue:
return m_swizzleParams.b();
case TextureSwizzleParam::Alpha:
return m_swizzleParams.a();
default:
MOBILEGL_ASSERT(false, "TextureObjectBase::GetSwizzleParam: Invalid TextureSwizzleParam: %d",
static_cast<Int>(param));
return TextureSwizzleParam::Red;
}
}
const Vec4<TextureSwizzleParam>& TextureObjectBase::GetAllSwizzleParams() const {
return m_swizzleParams;
}
void TextureObjectBase::SetSwizzleParam(TextureSwizzleParam param, TextureSwizzleParam value) {
if (GetSwizzleParam(param) == value) return;
switch (param) {
case TextureSwizzleParam::Red:
m_swizzleParams.r() = value;
break;
case TextureSwizzleParam::Green:
m_swizzleParams.g() = value;
break;
case TextureSwizzleParam::Blue:
m_swizzleParams.b() = value;
break;
case TextureSwizzleParam::Alpha:
m_swizzleParams.a() = value;
break;
default:
MOBILEGL_ASSERT(false, "TextureObjectBase::SetSwizzleParam: Invalid TextureSwizzleParam: %d",
static_cast<Int>(param));
break;
}
++m_textureParamsVersion;
}
void TextureObjectBase::SetSwizzleParamRGBA(const Vec4<TextureSwizzleParam>& values) {
if (values == m_swizzleParams) return;
m_swizzleParams = values;
++m_textureParamsVersion;
}
const UintVec2& TextureObjectBase::GetLevelRange() const {
return m_levelRange;
}
void TextureObjectBase::SetBaseLevel(Uint baseLevel) {
if (IsImmutable() && m_immutableLevels > 0) {
baseLevel = std::min(baseLevel, m_immutableLevels - 1);
}
if (baseLevel == m_levelRange.x()) return;
m_levelRange.x() = baseLevel;
if (IsImmutable() && m_levelRange.y() < m_levelRange.x()) {
m_levelRange.y() = m_levelRange.x();
}
++m_textureParamsVersion;
BumpShapeVersion();
}
void TextureObjectBase::SetMaxLevel(Uint maxLevel) {
if (IsImmutable() && m_immutableLevels > 0) {
maxLevel = std::min(std::max(maxLevel, m_levelRange.x()), m_immutableLevels - 1);
}
if (maxLevel == m_levelRange.y()) return;
m_levelRange.y() = maxLevel;
++m_textureParamsVersion;
BumpShapeVersion();
}
Bool TextureObjectBase::IsImmutable() const {
return m_immutableLevels > 0;
}
Uint TextureObjectBase::GetImmutableLevels() const {
return m_immutableLevels;
}
void TextureObjectBase::SetImmutableLevels(Uint levels) {
if (m_immutableLevels == levels) return;
m_immutableLevels = levels;
if (m_immutableLevels > 0) {
m_levelRange.x() = std::min(m_levelRange.x(), m_immutableLevels - 1);
m_levelRange.y() = std::min(std::max(m_levelRange.y(), m_levelRange.x()), m_immutableLevels - 1);
}
++m_textureParamsVersion;
}
Uint16 TextureObjectBase::GetTextureParamsVersion() const {
return m_textureParamsVersion;
}
Uint64 TextureObjectBase::GetContentVersion() const {
return m_contentVersion;
}
Uint64 TextureObjectBase::GetShapeVersion() const {
return m_shapeVersion;
}
Bool TextureObjectBase::IsMipmapCompleteForFilterCached(Bool mipmapped) const {
const int slot = mipmapped ? 1 : 0;
if (m_completeMemoShapeVersion[slot] == m_shapeVersion) {
return m_completeMemoValue[slot];
}
const Bool value = ComputeMipmapCompleteForFilter(this, mipmapped);
m_completeMemoShapeVersion[slot] = m_shapeVersion;
m_completeMemoValue[slot] = value;
return value;
}
void TextureObjectBase::BumpContentVersion() {
++m_contentVersion;
}
Int TextureObjectBase::GetSamples() const {
return m_samples;
}
void TextureObjectBase::SetSamples(Int samples) {
m_samples = samples;
++m_textureParamsVersion;
}
Bool TextureObjectBase::HasFixedSampleLocations() const {
return m_fixedSampleLocations;
}
void TextureObjectBase::SetFixedSampleLocations(Bool fixedSampleLocations) {
m_fixedSampleLocations = fixedSampleLocations;
++m_textureParamsVersion;
}
Uint64 TextureObjectBase::GetLifetimeId() const {
return m_lifetimeId;
}
const SharedPtr<ITextureObject>& TextureObjectBase::GetViewStorageOwner() const {
// A plain texture owns its own storage. Only TextureObjectView overrides this,
// which is what IsTextureView() keys on everywhere else.
static const SharedPtr<ITextureObject> noStorageOwner = nullptr;
return noStorageOwner;
}
Uint TextureObjectWithOneMipmap::GetMipmapLevelCount() const {
return m_textureStorage.GetLevelCount();
}
const IntVec3 TextureObjectWithOneMipmap::GetMipmapTexelSize(TextureUploadTarget target,
Uint mipmapLevel) const {
return m_textureStorage.GetTexelSize(GetIndexOfTextureUploadTarget(target), mipmapLevel);
}
const SizeT TextureObjectWithOneMipmap::GetMipmapByteSize(TextureUploadTarget target,
Uint mipmapLevel) const {
return m_textureStorage.GetByteSize(GetIndexOfTextureUploadTarget(target), mipmapLevel);
}
void TextureObjectWithOneMipmap::AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
MipmapInput input) {
BumpShapeVersion();
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
}
void TextureObjectWithOneMipmap::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
BumpShapeVersion();
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
}
void TextureObjectWithOneMipmap::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel,
DataPtr input) {
m_textureStorage.UpdateSubData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
}
void* TextureObjectWithOneMipmap::MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) {
return m_textureStorage.MapData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
void TextureObjectWithOneMipmap::MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel,
Bool dirty) {
if (dirty) {
++m_contentVersion;
}
m_textureStorage.MarkDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, dirty);
}
Bool TextureObjectWithOneMipmap::IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const {
return m_textureStorage.IsDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
void TextureObjectWithOneMipmap::MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel,
IntVec3 offset, IntVec3 size) {
++m_contentVersion;
m_textureStorage.MarkDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, offset,
size);
}
MipmapDirtyRegion TextureObjectWithOneMipmap::GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
SizeT TextureObjectWithOneMipmap::GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel,
MipmapDirtyRegion* outRects,
SizeT maxRects) const {
return m_textureStorage.GetDirtyRects(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
outRects, maxRects);
}
void TextureObjectWithOneMipmap::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat, const void* data, SizeT size) {
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
internalFormat, data, size);
}
GLenum TextureObjectWithOneMipmap::GetMipmapCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
SizeT TextureObjectWithOneMipmap::GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetCompressedByteSize(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
const void* TextureObjectWithOneMipmap::MapMipmapCompressedImage(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
void TextureObjectWithOneMipmap::SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel, GLenum internalFormat) {
m_textureStorage.SetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
internalFormat);
}
GLenum TextureObjectWithOneMipmap::GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget),
mipmapLevel);
}
IntVec3 TextureObjectWithOneMipmap::GetBaseSize() const {
if (m_textureStorage.GetLevelCount() == 0) {
return {0, 0, 0};
}
return m_textureStorage.GetTexelSize(0, 0);
}
Bool TextureObjectWithOneMipmap::IsComplete() const {
if (!TextureObjectBase::IsComplete()) return false;
SizeT levelCount = m_textureStorage.GetLevelCount();
if (levelCount == 0) {
MGLOG_D("%s: not complete because levelCount == 0", __func__);
return false;
}
// For some reason mojang decided to have 0x0 in last level mipmap
// Relaxing checks for that
Bool hadZero = false;
for (SizeT i = 0; i < levelCount; ++i) {
const auto& levelSize = m_textureStorage.GetTexelSize(0, i);
if (levelSize.x() <= 0 || levelSize.y() <= 0 || levelSize.z() <= 0) {
hadZero = true;
} else {
if (hadZero) {
// We're checking for "zero - not zero - zero" here
// "not zero - zero - zero" should pass this test
MGLOG_D("%s: not complete because 0x0 occurred, and is not last level mipmap", __func__);
return false;
}
}
}
// TODO: add more completeness checks based on texture type and mipmap levels
return true;
}
// TODO: add other texture types as needed
Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped) {
if (texture == nullptr) return true;
return texture->IsMipmapCompleteForFilterCached(mipmapped);
}
Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler) {
// A multisample texture is fetched, never filtered. GL 4.6 core 8.17 gives it exactly
// one level and says its sampler state is not used at all - texelFetch is the only way
// a shader can read it - so 8.14's filter-completeness rules, which is what the
// `mipmapped` branch below asks about, never apply to it.
//
// Deriving `mipmapped` from that unused sampler is what made EVERY multisample texture
// look incomplete: MIN_FILTER's initial value is NEAREST_MIPMAP_LINEAR, and a texture
// that can only ever have one level never satisfies the mip-chain check. Both backends
// treat "samples as incomplete" as "do not bind it" (DirectGLES's per-unit walk in
// ResolveAndBindUnitTextures, DirectVulkan's UniformManager), so the sampler2DMS the
// shader declared was left pointing at nothing and every texelFetch read zero. That is
// the sampler2DMS/sampler2DMSArray half of KHR-GL43.compute_shader.resource-texture,
// which fails at the first data7 element with the multisample texture correctly
// cleared and simply never bound.
//
// IsCopyImageEndpointComplete already spells the same guard as
// CopyImageTargetHasMipmapChain; this was the one place that asked without it.
const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown;
const Bool filtered = target != TextureTarget::Texture2DMultisample &&
target != TextureTarget::Texture2DMultisampleArray;
const Bool mipmapped = filtered && effectiveSampler != nullptr &&
effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None;
return !IsMipmapCompleteForFilter(texture, mipmapped);
}
static Bool ComputeMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped) {
if (texture == nullptr) return true;
if (!texture->IsComplete()) return false;
if (!mipmapped) return true;
const auto* mipmapTexture = AsMipmapTexture(texture);
if (mipmapTexture == nullptr) return true; // no mip chain to be incomplete about
const UintVec2& levelRange = texture->GetLevelRange();
const Uint baseLevel = levelRange.x();
const Uint storedLevels = mipmapTexture->GetMipmapLevelCount();
if (baseLevel >= storedLevels) return false;
// An array texture's layer count is not a dimension of the image: it stays put all
// the way down the chain (GL 4.6 core 8.14.3). GetMipmapTexelSize reports it in the
// slot after the image's own dimensions.
const TextureTarget target = texture->GetTarget();
Int shrinkingComponents = 3;
if (target == TextureTarget::Texture1DArray) {
shrinkingComponents = 1;
} else if (target == TextureTarget::Texture2DArray || target == TextureTarget::TextureCubeMapArray) {
shrinkingComponents = 2;
}
for (const auto uploadTarget : texture->GetUploadTargets()) {
const IntVec3 baseSize = mipmapTexture->GetMipmapTexelSize(uploadTarget, baseLevel);
Int largest = 0;
for (Int component = 0; component < shrinkingComponents; ++component) {
largest = std::max(largest, baseSize[component]);
}
if (largest <= 0) return false;
// p = log2 of the largest base dimension: the last level the chain needs
// before every dimension has reached 1. TEXTURE_MAX_LEVEL can cut it short.
Uint p = 0;
for (Int extent = largest; extent > 1; extent >>= 1) ++p;
const Uint lastLevel = std::min(baseLevel + p, levelRange.y());
for (Uint level = baseLevel; level <= lastLevel; ++level) {
if (level >= storedLevels) return false;
const IntVec3 actual = mipmapTexture->GetMipmapTexelSize(uploadTarget, level);
for (Int component = 0; component < 3; ++component) {
const Int expected = component < shrinkingComponents
? std::max(1, baseSize[component] >> (level - baseLevel))
: baseSize[component];
if (actual[component] != expected) return false;
}
}
}
return true;
}
} // namespace GLState
} // namespace MG_State
} // namespace MobileGL