[Feature, Fix, Test] (GLState, GLImpl, DirectVulkan, DirectGLES): implement glTextureView over shared texture storage

This commit is contained in:
2026-08-22 10:41:52 -04:00
parent a5f36c8f8d
commit 6162603072
36 changed files with 2995 additions and 68 deletions
+1
View File
@@ -392,6 +392,7 @@ set(SOURCE_FILES
MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp
MobileGL/MG_State/GLState/TextureState/TextureObject3D.cpp
MobileGL/MG_State/GLState/TextureState/TextureObjectBuffer.cpp
MobileGL/MG_State/GLState/TextureState/TextureObjectView.cpp
MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
@@ -753,7 +753,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
// Baseline advertisement (no runtime capabilities yet); reconciled once
// the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
.Extensions = BuildAdvertisedExtensions(false, false, false, false, false),
.IsCompatibilityProfile = false // Is Compatibility Profile
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
@@ -777,7 +777,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(
AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
capabilities.SupportsDrawIndirect,
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance);
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance,
capabilities.SupportsTextureView);
}
} // namespace
@@ -990,7 +991,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
Bool drawIndirectSupported,
Bool nonZeroIndirectBaseInstanceSupported) {
Bool nonZeroIndirectBaseInstanceSupported,
Bool textureViewSupported) {
Vector<GLExtension> extensions = {
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
@@ -1065,6 +1067,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
extensions.push_back(E_GL_ARB_timer_query);
}
// Only advertised when the host ES driver has EXT/OES_texture_view. ES has no core
// texture views at any version and no honest emulation exists: a view is a SECOND NAME
// over the SAME storage, so that writes through either are visible through the other and
// the two carry independent per-texture parameters at the same time - which is exactly
// what applications use it for (Better Clouds samples one D24S8 through its own name with
// DEPTH_STENCIL_TEXTURE_MODE = STENCIL_INDEX and through a view with DEPTH_COMPONENT, in
// a single shading pass). A copy-based fallback satisfies neither half, and fails
// silently; withholding the string and answering glTextureView with INVALID_OPERATION is
// the only behaviour that cannot be mistaken for success.
if (textureViewSupported) {
extensions.push_back(E_GL_ARB_texture_view);
}
// Only advertised when the host ES driver actually filters anisotropically: the sampler
// state is accepted regardless, but forwarding it would be a no-op without the extension,
// and an app that trusts the string (LWJGL builds GLCapabilities from it) would silently
@@ -78,11 +78,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
// for a device whose timer queries / anisotropic filtering / native indirect draws /
// non-zero indirect baseInstance semantics are (or are not) usable.
// non-zero indirect baseInstance semantics / EXT-OES texture views are (or are not) usable.
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
Bool drawIndirectSupported,
Bool nonZeroIndirectBaseInstanceSupported);
Bool nonZeroIndirectBaseInstanceSupported,
Bool textureViewSupported);
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
// initialized backend returns from GetBackendAPIVersionString (and that ends up
+34 -5
View File
@@ -1082,19 +1082,48 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
auto* backendTextureSlot = g_backendTextureObjects.Find(textureObject.get());
auto& backendObj = backendTextureSlot ? *backendTextureSlot
: g_backendTextureObjects.GetOrCreate(textureObject);
if (!backendObj) {
backendObj = MakeShared<BackendTextureObject>();
auto& backendSlot = backendTextureSlot ? *backendTextureSlot
: g_backendTextureObjects.GetOrCreate(textureObject);
if (!backendSlot) {
backendSlot = MakeShared<BackendTextureObject>();
}
// A by-VALUE copy of the twin for the duration of the syncs below. `backendSlot` is a
// reference INTO the open-addressed registry, and syncing can RE-ENTER this function:
// a texture created by glTextureView has to sync the texture whose storage it views
// first (SyncTextureViewToBackend), and that nested call may insert, grow the map and
// relocate every entry - leaving the reference dangling. Holding the object itself
// keeps the calls below working on the right twin regardless; the slot is re-resolved
// at the end for the reference this function returns.
const SharedPtr<BackendTextureObject> backendObj = backendSlot;
if (imageBindableStorageRequired) {
backendObj->RequireImageBindableStorage(textureObject);
}
backendObj->SyncTextureParamsToBackend(textureObject);
backendObj->SyncBuiltinSamplerToBackend(textureObject);
backendObj->SyncMipmapsToBackend(textureObject);
// The storage sync may RE-MINT the driver texture - a fresh glTexStorage after a
// shape change, an image-bindable widening, or the glTextureView that an
// ARB_texture_view view is created on - which discards every parameter the two calls
// above just pushed. Re-push them here rather than leaving it to the next sync: the
// very next thing that happens is usually the draw this sync was run for, and until
// the filters land the new texture is at the ES defaults, which for a single-level or
// integer texture is not merely mis-filtered but INCOMPLETE, i.e. it samples zero.
if (backendObj->NeedsParameterResync()) {
backendObj->SyncTextureParamsToBackend(textureObject);
backendObj->SyncBuiltinSamplerToBackend(textureObject);
}
return backendObj;
auto* refreshedSlot = g_backendTextureObjects.Find(textureObject.get());
auto& refreshedBackendObj = refreshedSlot ? *refreshedSlot
: g_backendTextureObjects.GetOrCreate(textureObject);
if (!refreshedBackendObj) {
// A collection ran during the nested sync and took this slot with it; put the
// twin the caller is about to use back, rather than handing back an empty one.
refreshedBackendObj = backendObj;
}
return refreshedBackendObj;
}
// Identity snapshot of what one texture unit has bound: the object in every binding
+132 -1
View File
@@ -2495,6 +2495,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
TextureSwizzleParam::Alpha};
m_cacheDepthStencilTextureMode = GL_DEPTH_COMPONENT;
m_forceTextureParamsResync = true;
// The filter/wrap/LOD cache belongs to the name that just went away, and its gate is
// the frontend SAMPLER's version, which a backend re-mint does not move - so without
// this the new driver texture keeps the ES defaults for life. See m_forceSamplerResync.
m_cacheSamplerParameters = SamplerParameters{};
m_forceSamplerResync = true;
}
// Sets the backend GL unpack state to MobileGL's upload default for the scope,
@@ -3117,6 +3122,122 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
// The ES entry point for EXT/OES_texture_view, whichever spelling this driver brought.
// Callers must have checked g_GLESCapabilities.SupportsTextureView first - the capability
// is the extension AND the pointer, because eglGetProcAddress hands back live-looking
// stubs (see AcquireGLESFunctions).
static MG_External::GLES::glTextureViewEXT_PTR ResolveTextureViewEntryPoint() {
if (g_GLESFuncs.glTextureViewEXT != nullptr) {
return g_GLESFuncs.glTextureViewEXT;
}
return reinterpret_cast<MG_External::GLES::glTextureViewEXT_PTR>(g_GLESFuncs.glTextureViewOES);
}
// Stamps the same per-draw clean-gate keys a completed storage sync stamps, so a view
// that needs no work costs the same nothing per draw that any other synced texture does.
void BackendTextureObject::StampViewSyncKeys(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
if (MG_State::pGLContext) {
m_syncedShapeContextId = MG_State::pGLContext->GetTextureContextId();
m_syncedShapeGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion();
}
m_syncedContentVersion = stateTextureObject->GetContentVersion();
}
void BackendTextureObject::SyncTextureViewToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
const auto& storageObject = stateTextureObject->GetViewStorageOwner();
if (!storageObject) {
MGLOG_E_ONCE("Texture %u claims to be a view but names no storage owner.",
stateTextureObject->GetExternalIndex());
return;
}
if (!g_GLESCapabilities.SupportsTextureView) {
// Unreachable through the API: the frontend refuses glTextureView with
// GL_INVALID_OPERATION when the backend does not advertise GL_ARB_texture_view,
// and DirectGLES only advertises it when this capability is set.
MGLOG_E_ONCE("Texture view %u reached the backend on a driver without "
"EXT/OES_texture_view.",
stateTextureObject->GetExternalIndex());
return;
}
// Deliberately a by-VALUE copy of the SharedPtr: SyncTextureObjectToBackend hands back
// a reference INTO the open-addressed registry map, and the params/sampler syncs below
// (plus any nested growth) can rehash it out from under a reference.
const SharedPtr<BackendTextureObject> storageBackendObject =
SyncTextureObjectToBackend(storageObject, m_imageBindableStorageRequired);
if (!storageBackendObject) {
MGLOG_E_ONCE("Failed to sync the storage texture of view %u.",
stateTextureObject->GetExternalIndex());
return;
}
const Uint storageBackendTextureId = storageBackendObject->GetBackendTextureId();
if (storageBackendTextureId == 0) {
MGLOG_D("Storage texture of view %u has no ES name yet.",
stateTextureObject->GetExternalIndex());
return;
}
if (m_isInitialized && m_viewSourceBackendTextureId == storageBackendTextureId) {
StampViewSyncKeys(stateTextureObject);
return;
}
// Either the first sync, or the storage was re-minted underneath us. A name that has
// already been through glTextureView cannot be viewed again, so start from a fresh
// one (this also scrubs the binding caches and bumps the FBO attachment generation).
RecreateBackendTexture();
GLenum glInternalFormat = 0;
GLenum glFormat = 0;
GLenum glType = 0;
TextureImpl::GenerateTextureFormatInfo(stateTextureObject->GetFormat(), &glInternalFormat, &glFormat,
&glType, stateTextureObject->GetTarget());
const GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget());
DebugImpl::ErrorLopper::Clear();
ResolveTextureViewEntryPoint()(m_backendTextureId, target, storageBackendTextureId, glInternalFormat,
stateTextureObject->GetViewMinLevel(),
stateTextureObject->GetViewNumLevels(),
stateTextureObject->GetViewMinLayer(),
stateTextureObject->GetViewNumLayers());
const GLenum error = g_GLESFuncs.glGetError();
if (error != GL_NO_ERROR) {
MGLOG_E_ONCE("glTextureView(view=%u target=%s origtexture=%u internalformat=%s levels=[%u,%u) "
"layers=[%u,%u)) failed: %s",
m_backendTextureId, MG_Util::ConvertGLEnumToString(target).c_str(),
storageBackendTextureId, MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(),
stateTextureObject->GetViewMinLevel(),
stateTextureObject->GetViewMinLevel() + stateTextureObject->GetViewNumLevels(),
stateTextureObject->GetViewMinLayer(),
stateTextureObject->GetViewMinLayer() + stateTextureObject->GetViewNumLayers(),
MG_Util::ConvertGLEnumToString(error).c_str());
return;
}
m_viewSourceBackendTextureId = storageBackendTextureId;
m_isInitialized = true;
// A view's storage is immutable by construction (its origtexture had to be), which is
// what keeps the respecify paths away from this name.
m_backendStorageImmutable = true;
const auto baseSize = stateTextureObject->GetBaseSize();
m_prevTextureInfo = {stateTextureObject->GetFormat(),
static_cast<SizeT>(baseSize.x()),
static_cast<SizeT>(baseSize.y()),
static_cast<SizeT>(baseSize.z()),
static_cast<SizeT>(stateTextureObject->GetViewNumLevels()),
0,
stateTextureObject->GetSamples(),
stateTextureObject->HasFixedSampleLocations()};
MGLOG_D("Texture view %u (ES %u) now views storage texture %u (ES %u), levels [%u,%u) layers [%u,%u)",
stateTextureObject->GetExternalIndex(), m_backendTextureId, storageObject->GetExternalIndex(),
storageBackendTextureId, stateTextureObject->GetViewMinLevel(),
stateTextureObject->GetViewMinLevel() + stateTextureObject->GetViewNumLevels(),
stateTextureObject->GetViewMinLayer(),
stateTextureObject->GetViewMinLayer() + stateTextureObject->GetViewNumLayers());
StampViewSyncKeys(stateTextureObject);
}
void BackendTextureObject::SyncMipmapsToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
if (!stateTextureObject) {
@@ -3124,6 +3245,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
return;
}
// A texture created by glTextureView owns no storage: the levels, the format and
// every texel belong to the texture it views, and this name only has to be made to
// ALIAS them. Everything below - storage allocation, respecification, per-level
// uploads - would be re-doing the storage texture's work on the wrong name.
if (stateTextureObject->IsTextureView()) {
SyncTextureViewToBackend(stateTextureObject);
return;
}
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
@@ -3977,12 +4107,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto* samplerObject = stateTextureObject->GetSamplerObject().get();
Uint currentSamplerVersion = samplerObject->GetVersion();
if (m_syncedSamplerVersion == currentSamplerVersion) {
if (m_syncedSamplerVersion == currentSamplerVersion && !m_forceSamplerResync) {
MGLOG_D("Sampler parameters have not changed for texture ID: %u, skipping sync.", m_backendTextureId);
return;
}
m_syncedSamplerVersion = currentSamplerVersion;
m_forceSamplerResync = false;
MGLOG_D("Syncing texture built-in sampler with backend ID %u to backend for state ID %u",
m_backendTextureId, stateTextureObject->GetExternalIndex());
+36
View File
@@ -792,6 +792,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendTextureObject(const BackendTextureObject&) = delete;
BackendTextureObject& operator=(const BackendTextureObject&) = delete;
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
// The storage half of the sync for a texture created by glTextureView. Instead of
// allocating storage and replaying uploads, it makes this object's ES name BE a view
// of the storage texture's ES name (EXT/OES_texture_view), which is what gives the
// two names one image and independent per-texture parameters at the same time. The
// parameter and sampler halves are unchanged and run on this name as on any other.
void SyncTextureViewToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void StampViewSyncKeys(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
// The storage half of the sync for a texture created by glTextureView. Instead of
// allocating storage and replaying uploads, it makes this object's ES name BE a view
// of the storage texture's ES name (EXT/OES_texture_view), which is what gives the
// two names one image and independent per-texture parameters at the same time. The
// parameter and sampler halves are unchanged and run on this name as on any other.
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
// Marks the texture as one whose ES storage has to be image-bindable, which for a
@@ -824,6 +836,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// `contextId`/`samplingGeneration` are the frontend context's current
// values, hoisted by the caller so a per-draw list walk reads them once
// instead of per texture. `t` must be the live frontend texture.
// True while a driver-side re-mint has left the parameter caches describing a texture
// that no longer exists; SyncTextureObjectToBackend re-pushes them in the same sync.
Bool NeedsParameterResync() const { return m_forceTextureParamsResync || m_forceSamplerResync; }
Bool IsDrawSyncClean(const MG_State::GLState::ITextureObject* t, Uint64 contextId,
Uint64 samplingGeneration) const {
if (!m_isInitialized || m_syncedShapeContextId == 0 || m_syncedShapeContextId != contextId ||
@@ -867,6 +883,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
// it is only the IMAGE binding ES cannot spell - and the private name below carries
// the split the shader was rewritten against. 0 when this texture takes no split.
Uint m_bufferImageSplitViewId = 0;
// For a texture created by glTextureView: the ES name of the storage texture this
// one was last made a view OF. EXT_texture_view may be called only once per name, so
// a storage texture that got re-minted underneath (RecreateBackendTexture) has to be
// detected here and answered with a fresh name for the view as well - otherwise the
// view would keep aliasing storage that no longer exists.
Uint m_viewSourceBackendTextureId = 0;
// For a texture created by glTextureView: the ES name of the storage texture this
// one was last made a view OF. EXT_texture_view may be called only once per name, so
// a storage texture that got re-minted underneath (RecreateBackendTexture) has to be
// detected here and answered with a fresh name for the view as well - otherwise the
// view would keep aliasing storage that no longer exists.
// ES context generation the id was created under; a dtor running after
// that context died must not delete a foreign (recycled) name.
Uint m_contextGeneration = 0;
@@ -915,6 +942,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
// parameter already pushed onto it: the params-version early-out has to be overridden
// once, or an unchanged version would skip the re-push forever.
Bool m_forceTextureParamsResync = false;
// The same problem for the FILTER state, which lives in m_cacheSamplerParameters and
// is gated on the frontend sampler's version rather than on the params version. A
// re-mint leaves that cache describing values the new driver texture never received,
// and an unchanged sampler version would then skip re-pushing them forever. This
// matters more than mis-filtering: ES makes a texture INCOMPLETE when its filters do
// not suit its level set (any integer texture with a non-NEAREST filter, or a
// single-level texture with a mipmapping filter), and an incomplete texture samples
// (0, 0, 0, 1) rather than its contents.
Bool m_forceSamplerResync = false;
};
void ActivateTextureUnit(Uint unit);
@@ -533,6 +533,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3,
// so on a 4.0 context the string is the only way to reach it.
E_GL_ARB_stencil_texturing,
// Unconditional, unlike DirectGLES: a GL texture view is a second set of VkImageViews
// over the same VkImage with a sub-range and possibly a reinterpreted VkFormat, which
// is core Vulkan on every device MobileGL runs on. Format-reinterpreting views need
// VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT on the image, which SyncTextureResource sets for
// every immutable-storage texture (see the comment there).
E_GL_ARB_texture_view,
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
// extension explicitly permits. It is also the only thing that
// exposes glProgramParameteri before GL 4.1.
@@ -47,7 +47,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto attachedTexture = attachment.GetTexture();
if (attachedTexture && attachedTexture.get() == &texture) {
outAttachment = attachmentType;
outLevel = attachment.GetTextureLevel();
outLevel = static_cast<Int>(ToStorageMipLevel(attachment.GetTexture().get(),
attachment.GetTextureLevel()));
return true;
}
}
@@ -416,16 +417,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
numericDomain == SamplerNumericDomain::UnsignedInteger;
SamplerResolveMemo* viewFormatMemo =
binding < m_samplerResolveMemo.size() ? &m_samplerResolveMemo[binding] : nullptr;
// The format this GL texture presents to the shader. For a texture created by
// glTextureView that is the format the VIEW reinterpreted its storage as (GL 4.6 core
// 8.18), not the storage image's own - resolving the numeric domain against the latter
// would pick a sampled view for a format the shader never declared. The probe is behind
// IsTextureView() so nothing about the ordinary per-draw path changes.
const VkFormat sampledSourceFormat =
texture->IsTextureView()
? m_textureManager->ResolveTextureViewWindow(*texture, *resource).format
: resource->format;
VkFormat sampledViewFormat;
if (viewFormatMemo != nullptr && viewFormatMemo->viewFormatValid &&
viewFormatMemo->viewFormatSource == resource->format &&
viewFormatMemo->viewFormatSource == sampledSourceFormat &&
viewFormatMemo->viewFormatDomain == numericDomain) {
sampledViewFormat = viewFormatMemo->viewFormat;
} else {
sampledViewFormat =
VkTextureManager::ResolveSampledImageViewFormat(resource->format, numericDomain);
VkTextureManager::ResolveSampledImageViewFormat(sampledSourceFormat, numericDomain);
if (viewFormatMemo != nullptr) {
viewFormatMemo->viewFormatSource = resource->format;
viewFormatMemo->viewFormatSource = sampledSourceFormat;
viewFormatMemo->viewFormatDomain = numericDomain;
viewFormatMemo->viewFormat = sampledViewFormat;
viewFormatMemo->viewFormatValid = true;
@@ -440,9 +450,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
// No reinterpretation requested: bind the depth-or-color aspect view the sync above
// already produced instead of re-entering GetOrCreateSampledImageView's sync path.
// already produced instead of re-entering GetOrCreateSampledImageView's sync path. A GL
// texture view is excluded because resource->sampledView belongs to the texture it VIEWS
// - same image, but the storage texture's level range and depth/stencil aspect, which is
// exactly the state a view exists to differ on.
const VkImageView sampledImageView =
sampledViewFormat == resource->format
(!texture->IsTextureView() && sampledViewFormat == resource->format)
? resource->sampledView
: m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
if (sampledImageView == VK_NULL_HANDLE) {
@@ -547,7 +560,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource->sampledLevelCount),
.imageView = samplerBindingOverride.imageView != VK_NULL_HANDLE ?
samplerBindingOverride.imageView :
(resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView),
// Same reason as in ResolveSamplerDescriptor: the resource's own views describe
// the storage texture, so a view has to be asked for its own.
(samplerBindingOverride.texture->IsTextureView()
? m_textureManager->GetOrCreateSampledImageView(*samplerBindingOverride.texture,
VK_FORMAT_UNDEFINED)
: (resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView)),
.imageLayout = samplerBindingOverride.imageLayout != VK_IMAGE_LAYOUT_UNDEFINED ?
samplerBindingOverride.imageLayout : resource->layout,
};
@@ -1029,8 +1047,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
useBindingFormat ? "true" : "false");
return false;
}
// glBindImageTexture named a level and a layer of the bound texture; on a GL texture
// view both are relative to the view, and the storage image is what the descriptor
// actually points at (see ToStorageMipLevel).
const Int32 storageImageLayer =
imageBinding.Layered != GL_FALSE
? imageBinding.Layer
: static_cast<Int32>(ToStorageArrayLayer(imageBinding.Texture.get(), imageBinding.Layer));
const VkImageView view = m_textureManager->GetOrCreateStorageImageView(
*imageBinding.Texture, mipLevel, viewFormat, imageBinding.Layered != GL_FALSE, imageBinding.Layer);
*imageBinding.Texture, ToStorageMipLevel(imageBinding.Texture.get(), static_cast<Int>(mipLevel)),
viewFormat, imageBinding.Layered != GL_FALSE, storageImageLayer);
if (view == VK_NULL_HANDLE) {
MGLOG_E_ONCE("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u "
"bindingFormat=0x%x imageFormat=%d reflectedFormat=%d selectedFormat=%d bindingPolicy=%s",
@@ -122,8 +122,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return &attachment;
}
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel,
// The texture a pending clear is actually ABOUT. A clear issued through a GL texture view
// (ARB_texture_view) targets the storage it views, so it must queue against - and be found
// by - the storage texture; keying it on the view instead left the clear invisible to every
// materialisation done through the parent's name (and vice versa), so the image stayed in
// VK_IMAGE_LAYOUT_UNDEFINED and the readback was dropped as unreadable.
static MG_State::GLState::ITextureObject* ClearStorageTextureOf(MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) {
return nullptr;
}
const auto& storageOwner = texture->GetViewStorageOwner();
return storageOwner ? storageOwner.get() : texture;
}
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel,
Uint32 baseArrayLayer, Uint32 layerCount) {
MG_State::GLState::ITextureObject* texture = ClearStorageTextureOf(rawTexture);
if (rawTexture != nullptr && texture != rawTexture) {
// The caller named a level and a layer of the VIEW; the key describes the STORAGE, so
// both have to be shifted into its numbering (GL 4.6 core 8.18). Without this a clear
// of a view's level 0 would collide with a clear of the storage's level 0 even when
// the view opened onto level 1.
mipLevel += static_cast<Uint32>(rawTexture->GetViewMinLevel());
baseArrayLayer += static_cast<Uint32>(rawTexture->GetViewMinLayer());
}
return PendingClearKey {
.texture = texture,
.textureLifetimeId = texture ? texture->GetLifetimeId() : 0,
@@ -157,6 +179,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
// Same rule as VkTextureManager::MakeTextureIdentity: a GL texture view is identified by
// the storage it views. A clear posted against a view and one posted against its parent
// target the same image, so they have to coalesce rather than queue independently.
if (texture != nullptr) {
const auto& storageOwner = texture->GetViewStorageOwner();
if (storageOwner) {
texture = storageOwner.get();
}
}
return TextureIdentity {
.texture = texture,
.lifetimeId = texture ? texture->GetLifetimeId() : 0,
@@ -286,9 +317,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return;
}
const PendingClearKey key = MakePendingClearKey(texture.get());
const auto& storageOwner = texture->GetViewStorageOwner();
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
const PendingClearKey key = MakePendingClearKey(storageTexture.get());
const std::lock_guard<std::mutex> lock(m_mutex);
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
@@ -305,8 +338,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const PendingClearKey key = MakePendingClearKey(attachment);
// The alive entry must hold the STORAGE object, because the key names it:
// LockTextureIdentityLocked cross-checks the two, and registering a view here under its
// storage's identity made every lookup of this clear fail that check and silently report
// "nothing pending" - which is how a clear issued through a view's framebuffer vanished.
const auto& storageOwner = texture->GetViewStorageOwner();
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
const std::lock_guard<std::mutex> lock(m_mutex);
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
@@ -321,6 +360,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false; // per-draw hot path: nothing pending anywhere
}
texture = ClearStorageTextureOf(texture);
const Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex);
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
@@ -411,6 +451,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false; // per-draw hot path: nothing pending anywhere
}
texture = ClearStorageTextureOf(texture);
const Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex);
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
@@ -114,7 +114,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
class VkClearManager {
public:
static PendingClearKey MakePendingClearKey(const MG_State::GLState::FramebufferAttachmentObject& attachment);
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel = 0,
// Resolves a GL texture view to the storage it views before keying; see the definition.
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel = 0,
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
Bool Initialize();
@@ -72,7 +72,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const TextureUploadTarget uploadTarget = attachment.GetTextureUploadTarget();
if (!IsCubeMapFaceUploadTarget(uploadTarget)) {
return static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
return ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer());
}
return static_cast<Uint32>(uploadTarget) - static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX);
}
@@ -633,11 +633,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (att.IsTexture()) {
const Uint64 textureLifetimeId = att.GetTexture()->GetLifetimeId();
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLifetimeId, sizeof(textureLifetimeId)));
const Int textureLevel = att.GetTextureLevel();
const Int textureLevel = static_cast<Int>(ToStorageMipLevel(att.GetTexture().get(),
att.GetTextureLevel()));
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLevel, sizeof(textureLevel)));
const TextureUploadTarget textureUploadTarget = att.GetTextureUploadTarget();
XXHASH_VERIFY(XXH64_update(m_hashState, &textureUploadTarget, sizeof(textureUploadTarget)));
const Int textureLayer = att.GetTextureLayer();
const Int textureLayer = static_cast<Int>(ToStorageArrayLayer(att.GetTexture().get(),
att.GetTextureLayer()));
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayer, sizeof(textureLayer)));
const Bool textureLayered = att.IsLayered();
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayered, sizeof(textureLayered)));
@@ -1006,7 +1008,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue;
auto& att = fbo.GetAttachment(drawbuf);
const Uint32 attachmentMipLevel = static_cast<Uint32>(std::max(att.GetTextureLevel(), 0));
const Uint32 attachmentMipLevel = ToStorageMipLevel(att.GetTexture().get(), att.GetTextureLevel());
const auto textureTarget = texture->GetTarget();
const Uint32 attachmentIndex = static_cast<Uint32>(attachmentDescriptions.size());
attachmentDescriptions.emplace_back();
@@ -1144,7 +1146,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (a.IsTexture() && b.IsTexture()) {
return a.GetTexture().get() == b.GetTexture().get() &&
a.GetTextureUploadTarget() == b.GetTextureUploadTarget() &&
a.GetTextureLevel() == b.GetTextureLevel();
ToStorageMipLevel(a.GetTexture().get(), a.GetTextureLevel()) ==
ToStorageMipLevel(b.GetTexture().get(), b.GetTextureLevel());
}
if (a.IsRenderbuffer() && b.IsRenderbuffer()) {
return a.GetRenderbuffer().get() == b.GetRenderbuffer().get();
@@ -1254,7 +1257,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} else if (selectedDepthStencilAttachment->IsTexture()) {
auto& texture = *selectedDepthStencilAttachment->GetTexture();
const Uint32 attachmentMipLevel =
static_cast<Uint32>(std::max(selectedDepthStencilAttachment->GetTextureLevel(), 0));
ToStorageMipLevel(selectedDepthStencilAttachment->GetTexture().get(),
selectedDepthStencilAttachment->GetTextureLevel());
MOBILEGL_ASSERT(depthTextureResource->layout != VK_IMAGE_LAYOUT_UNDEFINED ||
depthAttachmentDescription.loadOp != VK_ATTACHMENT_LOAD_OP_LOAD,
"GetOrCreateRenderPass: depth attachment textureId=%d has undefined tracked layout with LOAD_OP_LOAD",
@@ -220,6 +220,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkTextureManager::TextureIdentity VkTextureManager::MakeTextureIdentity(
MG_State::GLState::ITextureObject* texture) {
// A GL texture view (ARB_texture_view) is identified by the texture whose STORAGE it
// views, not by itself. Everything this identity keys - the TextureResource, the tracked
// image layout, the alive-object weak reference, the storage-usage marks, the per-draw
// sync memos - is a property of the IMAGE, and a view shares that image exactly. Doing
// the resolution here rather than at each call site is what makes it impossible to miss
// one: a layout update posted against a view's own identity would have found no resource
// at all, which is precisely how an attached view came back blank.
//
// One hop suffices and cannot recurse: glTextureView composes a view-of-a-view onto the
// root at creation, so a storage owner is never itself a view.
if (texture != nullptr) {
const auto& storageOwner = texture->GetViewStorageOwner();
if (storageOwner) {
texture = storageOwner.get();
}
}
return TextureIdentity{
.texture = texture,
.lifetimeId = texture ? texture->GetLifetimeId() : 0,
@@ -694,6 +710,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void VkTextureManager::EraseTrackedTexture(const TextureIdentity& identity) {
m_viewRequestedImageFlags.erase(identity);
auto resourceIt = m_textureResources.find(identity);
if (resourceIt != m_textureResources.end()) {
DeferResourceRelease(Move(resourceIt->second));
@@ -737,9 +754,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_drawSyncedThisDraw.clear();
}
VkTextureManager::TextureResource* VkTextureManager::SyncTextureAndGetDescriptor(MG_State::GLState::ITextureObject& texture) {
VkTextureManager::TextureResource* VkTextureManager::SyncTextureAndGetDescriptor(MG_State::GLState::ITextureObject& textureOrView) {
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "SyncTextureAndGetDescriptor: m_device == VK_NULL_HANDLE");
// A GL texture view has no image of its own; it resolves to - and shares - the resource
// of the texture whose storage it views, so that there is exactly one VkImage, one
// tracked layout and one upload path per storage. Everything that makes the view a
// different texture (format, level/layer window, sampled aspect) is applied where the
// VkImageViews are built, keyed in alternateSampledViews / attachmentViews.
MG_State::GLState::ITextureObject& texture = StorageTextureOf(textureOrView);
if (&texture != &textureOrView) {
NoteTextureViewImageRequirements(textureOrView, texture);
}
const TextureIdentity identity = MakeTextureIdentity(&texture);
// Per-draw memo fast path (see BeginDrawSyncScope): a texture already fully
@@ -838,7 +865,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImageView VkTextureManager::GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
return VK_NULL_HANDLE;
}
// A GL texture view shares this resource with the texture it views, so it must not touch
// perMipViews: that vector is indexed by mip level alone and holds views built with the
// STORAGE texture's format and full layer range. Route it through the keyed attachment
// cache instead, where its own window is part of the key.
if (texture.IsTextureView()) {
const TextureViewWindow window = ResolveTextureViewWindow(texture, *resource);
return GetOrCreateAttachmentViewAtMipLevel(texture, mipLevel, window.baseArrayLayer, window.layerCount,
window.viewType);
}
if (mipLevel >= resource->mipLevels) {
return VK_NULL_HANDLE;
}
@@ -866,7 +905,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 layerCount,
VkImageViewType viewType) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
return VK_NULL_HANDLE;
}
// mipLevel and baseArrayLayer arrive in STORAGE space - every caller runs them through
// ToStorageMipLevel / ToStorageArrayLayer at the GL attachment boundary. What a GL texture
// view still contributes here is its own internal format, which may reinterpret the
// storage's (GL 4.6 core table 8.21) and is what the attachment must actually be written
// through.
VkFormat viewFormatOverride = VK_FORMAT_UNDEFINED;
if (texture.IsTextureView()) {
viewFormatOverride = ResolveTextureViewWindow(texture, *resource).format;
}
if (mipLevel >= resource->mipLevels) {
return VK_NULL_HANDLE;
}
// A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so a per-slice
@@ -894,10 +945,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool framebufferSrgbEnabled =
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
const VkFormat attachmentFormat = ResolveSrgbAttachmentWriteFormat(resource->format, framebufferSrgbEnabled);
const VkFormat baseAttachmentFormat =
viewFormatOverride != VK_FORMAT_UNDEFINED ? viewFormatOverride : resource->format;
const VkFormat attachmentFormat =
ResolveSrgbAttachmentWriteFormat(baseAttachmentFormat, framebufferSrgbEnabled);
if (attachmentFormat == resource->format && baseArrayLayer == 0 && layerCount == resource->arrayLayers &&
viewType == resource->viewType) {
// The shortcut back to the per-mip vector is only sound for the storage texture itself;
// for a view every field below is part of what distinguishes it from its parent.
if (viewFormatOverride == VK_FORMAT_UNDEFINED && attachmentFormat == resource->format &&
baseArrayLayer == 0 && layerCount == resource->arrayLayers && viewType == resource->viewType) {
return GetOrCreateViewAtMipLevel(texture, mipLevel);
}
@@ -932,7 +988,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImageView VkTextureManager::GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture,
Uint32 mipLevel) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
return VK_NULL_HANDLE;
}
// As in GetOrCreateViewAtMipLevel: perMipSampledViews belongs to the storage texture's
// own format and aspect, so a GL view has to go to the keyed cache.
if (texture.IsTextureView()) {
if (mipLevel >= resource->mipLevels) {
return VK_NULL_HANDLE;
}
TextureViewWindow window = ResolveTextureViewWindow(texture, *resource);
// Storage space already (see ToStorageMipLevel); only the level COUNT narrows.
window.baseMipLevel = mipLevel;
window.levelCount = 1;
return GetOrCreateWindowedSampledView(texture, *resource, window);
}
if (mipLevel >= resource->mipLevels) {
return VK_NULL_HANDLE;
}
@@ -960,14 +1031,77 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return perMipSampledView;
}
VkImageView VkTextureManager::GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture,
VkFormat format) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE ||
resource->sampledView == VK_NULL_HANDLE) {
// Builds (and caches) one sampled VkImageView over `resource`'s image for an arbitrary
// window - the shared back end of every GL-texture-view sampled path. Keyed by the whole
// window, which is what keeps a D24S8's depth-aspect view and its stencil-aspect view apart
// in the same cache while both name the same image, the same levels and the same layers.
VkImageView VkTextureManager::GetOrCreateWindowedSampledView(MG_State::GLState::ITextureObject& texture,
TextureResource& resource,
const TextureViewWindow& window) {
const TextureResource::SampledImageViewKey key{
.baseMipLevel = window.baseMipLevel,
.levelCount = window.levelCount,
.baseArrayLayer = window.baseArrayLayer,
.layerCount = window.layerCount,
.viewType = window.viewType,
.format = window.format,
.aspect = window.sampledAspect,
};
const auto existing = resource.alternateSampledViews.find(key);
if (existing != resource.alternateSampledViews.end()) {
return existing->second;
}
if (window.format != resource.format &&
(resource.imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
MGLOG_E_ONCE("%s: textureId=%d needs a mutable-format image to be viewed as format=%d "
"(image format=%d)",
__func__, texture.GetExternalIndex(), static_cast<Int>(window.format),
static_cast<Int>(resource.format));
return VK_NULL_HANDLE;
}
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
const VkImageView view =
CreateImageView(resource.image, window.format, window.sampledAspect, window.viewType,
window.baseMipLevel, window.levelCount, window.baseArrayLayer, window.layerCount,
&sampledComponents);
if (view == VK_NULL_HANDLE) {
MGLOG_E_ONCE("%s: failed to create sampled view for textureId=%d format=%d aspect=0x%x "
"mips=[%u,%u) layers=[%u,%u)",
__func__, texture.GetExternalIndex(), static_cast<Int>(window.format),
static_cast<Uint32>(window.sampledAspect), window.baseMipLevel,
window.baseMipLevel + window.levelCount, window.baseArrayLayer,
window.baseArrayLayer + window.layerCount);
return VK_NULL_HANDLE;
}
resource.alternateSampledViews.emplace(key, view);
return view;
}
VkImageView VkTextureManager::GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture,
VkFormat format) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
return VK_NULL_HANDLE;
}
// A GL texture view never has a sampledView of its own on this resource - that one
// belongs to the storage texture, with the storage texture's format, level range and
// depth/stencil aspect. The window is the view's whole identity, so it always goes to the
// keyed cache, even when the requested format happens to match the image's.
if (texture.IsTextureView()) {
TextureViewWindow window = ResolveTextureViewWindow(texture, *resource);
if (format != VK_FORMAT_UNDEFINED) {
window.format = format;
}
return GetOrCreateWindowedSampledView(texture, *resource, window);
}
if (resource->sampledView == VK_NULL_HANDLE) {
return VK_NULL_HANDLE;
}
if (format == VK_FORMAT_UNDEFINED || format == resource->format) {
return resource->sampledView;
}
@@ -987,8 +1121,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const TextureResource::SampledImageViewKey key{
.baseMipLevel = resource->sampledBaseMipLevel,
.levelCount = resource->sampledLevelCount,
.baseArrayLayer = 0,
.layerCount = resource->arrayLayers,
.viewType = resource->viewType,
.format = format,
.aspect = VK_IMAGE_ASPECT_COLOR_BIT,
};
const auto existing = resource->alternateSampledViews.find(key);
if (existing != resource->alternateSampledViews.end()) {
@@ -1029,6 +1166,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImageView VkTextureManager::GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture,
Uint32 mipLevel, VkFormat format,
Bool layered, Int32 layer) {
// mipLevel and layer arrive in STORAGE space; ResolveStorageImageDescriptor converts
// the glBindImageTexture values with ToStorageMipLevel / ToStorageArrayLayer.
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels ||
resource->sampleCount != VK_SAMPLE_COUNT_1_BIT ||
@@ -1613,7 +1752,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool storageUpgradePending =
!outResource.storageUsageResolved &&
m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end();
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending &&
// Same shape for a GL texture view's demands on the image (MUTABLE_FORMAT for a
// format-reinterpreting view, CUBE_COMPATIBLE for a cube view of an array texture):
// nothing about the texture itself changed, but the live image cannot carry the view.
const VkImageCreateFlags requestedViewFlags = GetViewRequestedImageFlags(texture);
const Bool viewFlagUpgradePending =
(outResource.imageCreateFlags & requestedViewFlags) != requestedViewFlags;
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending && !viewFlagUpgradePending &&
outResource.syncedContentVersion == syncingContentVersion &&
outResource.syncedShapeVersion == syncingShapeVersion &&
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
@@ -1807,6 +1952,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
}
// Flags a GL texture view over this storage asked for (see NoteTextureViewImageRequirements).
// MUTABLE_FORMAT is still withheld from formats the driver has already refused it for, so a
// reinterpreting view degrades to no view rather than to no texture.
const VkImageCreateFlags requestedViewFlags = GetViewRequestedImageFlags(texture);
if (requestedViewFlags != 0) {
imageCreateFlags |= requestedViewFlags;
if (m_mutableFormatUnsupported.find(format) != m_mutableFormatUnsupported.end()) {
imageCreateFlags &= ~VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
}
}
// sRGB color images attach through their UNORM twin while GL_FRAMEBUFFER_SRGB is
// disabled (see ResolveSrgbAttachmentWriteFormat), which needs format-reinterpreting
// views - multisample sRGB render targets included.
@@ -2392,6 +2547,126 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_deferredViewReleases[m_currentFrameIndex].push_back(view);
}
MG_State::GLState::ITextureObject& VkTextureManager::StorageTextureOf(
MG_State::GLState::ITextureObject& texture) {
const auto& storageOwner = texture.GetViewStorageOwner();
return storageOwner ? *storageOwner : texture;
}
// The VkImageViewType a GL texture view's own target asks for. Deliberately derived from the
// GL target rather than inherited from the storage image: a 2D view of a 2D-array texture is
// a VK_IMAGE_VIEW_TYPE_2D over one layer, and a cube view of the same image is a
// VK_IMAGE_VIEW_TYPE_CUBE over six - which is the whole reason table 8.20 lists those pairs.
static VkImageViewType ResolveTextureViewImageViewType(TextureTarget target,
VkImageViewType storageViewType) {
switch (target) {
case TextureTarget::Texture1D:
return VK_IMAGE_VIEW_TYPE_1D;
case TextureTarget::Texture1DArray:
return VK_IMAGE_VIEW_TYPE_1D_ARRAY;
case TextureTarget::Texture2D:
case TextureTarget::TextureRectangle:
case TextureTarget::Texture2DMultisample:
return VK_IMAGE_VIEW_TYPE_2D;
case TextureTarget::Texture2DArray:
case TextureTarget::Texture2DMultisampleArray:
return VK_IMAGE_VIEW_TYPE_2D_ARRAY;
case TextureTarget::TextureCubeMap:
return VK_IMAGE_VIEW_TYPE_CUBE;
case TextureTarget::TextureCubeMapArray:
return VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
default:
return storageViewType;
}
}
VkTextureManager::TextureViewWindow VkTextureManager::ResolveTextureViewWindow(
MG_State::GLState::ITextureObject& texture, const TextureResource& resource) const {
TextureViewWindow window{};
window.format = resource.format;
window.viewType = resource.viewType;
window.baseArrayLayer = 0;
window.layerCount = resource.arrayLayers;
window.sampledAspect =
ResolveSampledImageViewAspectMask(resource.aspect, texture.GetDepthStencilTextureMode());
ResolveViewMipRange(texture, resource.mipLevels, window.baseMipLevel, window.levelCount);
if (!texture.IsTextureView()) {
return window;
}
window.isTextureView = true;
// GL 4.6 core 8.18: the view's TEXTURE_BASE_LEVEL / TEXTURE_MAX_LEVEL are relative to the
// view, so ResolveViewMipRange above already clamped them against the view's own level
// count (TextureObjectView reports it); shifting by TEXTURE_VIEW_MIN_LEVEL puts them back
// into the storage image's numbering.
window.baseMipLevel += static_cast<Uint32>(texture.GetViewMinLevel());
window.baseArrayLayer = static_cast<Uint32>(texture.GetViewMinLayer());
window.layerCount = static_cast<Uint32>(texture.GetViewNumLayers());
window.viewType = ResolveTextureViewImageViewType(texture.GetTarget(), resource.viewType);
// The view's OWN internalformat, which may reinterpret the storage's (table 8.21).
const VkFormat viewFormat = ResolveTextureFormatInfo(texture.GetFormat()).format;
if (viewFormat != VK_FORMAT_UNDEFINED) {
window.format = viewFormat;
}
// Recomputed against the view's own format: a depth/stencil storage viewed as
// depth/stencil still has to honour the VIEW's DEPTH_STENCIL_TEXTURE_MODE, which is the
// one parameter Better Clouds deliberately sets differently on the two names.
window.sampledAspect =
ResolveSampledImageViewAspectMask(GetAspectMaskForFormat(window.format) != VK_IMAGE_ASPECT_NONE
? GetAspectMaskForFormat(window.format)
: resource.aspect,
texture.GetDepthStencilTextureMode());
// Clamp to what the image actually has; a malformed view must degrade to an empty range
// rather than reach vkCreateImageView with an out-of-bounds subresource.
if (window.baseMipLevel >= resource.mipLevels) {
window.baseMipLevel = resource.mipLevels - 1;
window.levelCount = 1;
} else {
window.levelCount = std::min(window.levelCount, resource.mipLevels - window.baseMipLevel);
}
if (window.levelCount == 0) window.levelCount = 1;
if (window.baseArrayLayer >= resource.arrayLayers) {
window.baseArrayLayer = resource.arrayLayers - 1;
window.layerCount = 1;
} else {
window.layerCount = std::min(window.layerCount, resource.arrayLayers - window.baseArrayLayer);
}
if (window.layerCount == 0) window.layerCount = 1;
return window;
}
// The extra VkImageCreateFlags a GL texture view needs on the image it views. Recorded
// BEFORE the storage texture is synced (see SyncTextureAndGetDescriptor) so the very first
// resolve of a view already creates - or recreates and copies forward - an image the view can
// legally be built over, instead of handing back VK_NULL_HANDLE for a frame.
void VkTextureManager::NoteTextureViewImageRequirements(MG_State::GLState::ITextureObject& viewTexture,
MG_State::GLState::ITextureObject& storageTexture) {
VkImageCreateFlags required = 0;
const VkFormat viewFormat = ResolveTextureFormatInfo(viewTexture.GetFormat()).format;
const VkFormat storageFormat = ResolveTextureFormatInfo(storageTexture.GetFormat()).format;
if (viewFormat != VK_FORMAT_UNDEFINED && storageFormat != VK_FORMAT_UNDEFINED &&
viewFormat != storageFormat) {
required |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
}
const TextureTarget viewTarget = viewTexture.GetTarget();
if (viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::TextureCubeMapArray) {
required |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
}
if (required == 0) {
return;
}
VkImageCreateFlags& stored = m_viewRequestedImageFlags[MakeTextureIdentity(&storageTexture)];
stored |= required;
}
VkImageCreateFlags VkTextureManager::GetViewRequestedImageFlags(
const MG_State::GLState::ITextureObject& storageTexture) const {
const auto it = m_viewRequestedImageFlags.find(
MakeTextureIdentity(const_cast<MG_State::GLState::ITextureObject*>(&storageTexture)));
return it == m_viewRequestedImageFlags.end() ? 0 : it->second;
}
Bool VkTextureManager::SyncTextureViews(const MG_State::GLState::ITextureObject& texture, TextureResource& resource) {
MOBILEGL_ASSERT(resource.image != VK_NULL_HANDLE, "SyncTextureViews: image == VK_NULL_HANDLE");
@@ -41,6 +41,27 @@ inline IntVec3 ToVulkanLevelExtent(TextureTarget stateTarget, const IntVec3& glT
return glTexelSize;
}
// A GL framebuffer attachment's level/layer, and a GL image unit's, are relative to the texture
// the application NAMED. When that texture was created by glTextureView (ARB_texture_view) they
// are relative to the VIEW, and have to be shifted into the storage image's numbering before they
// can index a Vulkan subresource - DirectVulkan gives a view no image of its own, it shares the
// storage texture's (VkTextureManager::StorageTextureOf).
//
// Apply EXACTLY ONCE, at the boundary where a GL level/layer becomes a subresource index. Every
// GetOrCreate*View entry point below expects values that have already been through here, and so
// does everything that reads or copies an attachment directly. Both are identity on a plain
// texture (TEXTURE_VIEW_MIN_LEVEL / MIN_LAYER are 0 there), so the conversion is unconditional
// and there is no second, view-only code path to keep in step.
inline Uint32 ToStorageMipLevel(const MG_State::GLState::ITextureObject* texture, Int glLevel) {
const Uint32 level = static_cast<Uint32>(glLevel > 0 ? glLevel : 0);
return texture != nullptr ? level + static_cast<Uint32>(texture->GetViewMinLevel()) : level;
}
inline Uint32 ToStorageArrayLayer(const MG_State::GLState::ITextureObject* texture, Int glLayer) {
const Uint32 layer = static_cast<Uint32>(glLayer > 0 ? glLayer : 0);
return texture != nullptr ? layer + static_cast<Uint32>(texture->GetViewMinLayer()) : layer;
}
class VkTextureManager {
public:
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
@@ -139,17 +160,28 @@ public:
}
};
// Layer range and aspect join the key because a GL texture view (ARB_texture_view) can
// differ from its storage on either: the Better Clouds shape samples ONE D24S8 image
// through two GL names in one draw, the parent with the stencil aspect and the view with
// the depth aspect, and a layer-sliced view of an array texture names a sub-range of the
// same image. Without these two fields those views would alias each other in the cache.
struct SampledImageViewKey {
Uint32 baseMipLevel = 0;
Uint32 levelCount = 1;
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT;
Bool operator==(const SampledImageViewKey& other) const {
return baseMipLevel == other.baseMipLevel &&
levelCount == other.levelCount &&
baseArrayLayer == other.baseArrayLayer &&
layerCount == other.layerCount &&
viewType == other.viewType &&
format == other.format;
format == other.format &&
aspect == other.aspect;
}
};
@@ -157,10 +189,14 @@ public:
SizeT operator()(const SampledImageViewKey& key) const {
SizeT hash = std::hash<Uint32>{}(key.baseMipLevel);
hash ^= std::hash<Uint32>{}(key.levelCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(key.baseArrayLayer) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.aspect)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
@@ -358,6 +394,45 @@ public:
// present-less frame-boundary drain.
void CollectAllDeferredReleases();
// ---- GL texture views (ARB_texture_view / GL 4.6 core 8.18) ----
// The GL texture whose STORAGE backs the given one: itself, or - for a texture created by
// glTextureView - the texture it views. Every image-scoped question (which VkImage, its
// LAYOUT, its uploads, its extent, its usage) must be asked of this object, because a view
// has none of its own; only the VkImageViews differ per GL texture object. Sharing one
// TextureResource is not an optimisation, it is the only correct arrangement: layout is a
// property of the image, and VulkanRenderer caches raw pointers straight to the resource's
// layout field, so a second resource aliasing the same image would desynchronise the moment
// either of them transitioned it.
static MG_State::GLState::ITextureObject& StorageTextureOf(MG_State::GLState::ITextureObject& texture);
// The window a GL texture object opens onto its storage image. For a plain texture this is
// the resource's own full extent; for a view it is the sub-range, format and aspect
// glTextureView gave it. Views built from a non-default window must live in the KEYED caches
// (attachmentViews / alternateSampledViews), never in the per-mip vectors, which belong to
// the storage texture's own defaults.
struct TextureViewWindow {
Uint32 baseMipLevel = 0;
Uint32 levelCount = 1;
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkImageAspectFlags sampledAspect = VK_IMAGE_ASPECT_COLOR_BIT;
Bool isTextureView = false;
};
TextureViewWindow ResolveTextureViewWindow(MG_State::GLState::ITextureObject& texture,
const TextureResource& resource) const;
// Records what a GL texture view needs of the image it views, so the next sync of the
// STORAGE texture creates (or recreates and copies forward) an image the view can be built
// over. See m_viewRequestedImageFlags for why this is lazy rather than unconditional.
void NoteTextureViewImageRequirements(MG_State::GLState::ITextureObject& viewTexture,
MG_State::GLState::ITextureObject& storageTexture);
VkImageCreateFlags GetViewRequestedImageFlags(const MG_State::GLState::ITextureObject& storageTexture) const;
// Builds (and caches, keyed by the whole window) one sampled VkImageView over a storage
// image. Shared back end of every GL-texture-view sampled path.
VkImageView GetOrCreateWindowedSampledView(MG_State::GLState::ITextureObject& texture,
TextureResource& resource, const TextureViewWindow& window);
TextureResource* SyncTextureAndGetDescriptor(
MG_State::GLState::ITextureObject& texture);
VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
@@ -572,6 +647,14 @@ private:
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures;
// Extra VkImageCreateFlags a GL texture view needs on the storage image it views, keyed by
// the STORAGE texture's identity. Requested lazily, exactly like STORAGE usage above and for
// the same reason: VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT costs bandwidth compression on tilers
// (it is what VK_KHR_image_format_list exists to claw back), so setting it on every
// immutable-storage texture would tax every glTexStorage2D render target in a game for a
// feature almost none of them use. A SAME-format view - which is the common case, and the
// Better Clouds case - needs no flag at all and therefore costs nothing.
std::unordered_map<TextureIdentity, VkImageCreateFlags, TextureIdentityHash> m_viewRequestedImageFlags;
// Supported multisample counts per format, so repeat texture syncs do not
// re-query vkGetPhysicalDeviceImageFormatProperties.
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
@@ -1258,7 +1258,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return depthAttachment.GetTexture().get() != stencilAttachment.GetTexture().get() ||
depthAttachment.GetTextureUploadTarget() != stencilAttachment.GetTextureUploadTarget() ||
depthAttachment.GetTextureLevel() != stencilAttachment.GetTextureLevel();
ToStorageMipLevel(depthAttachment.GetTexture().get(), depthAttachment.GetTextureLevel()) !=
ToStorageMipLevel(stencilAttachment.GetTexture().get(), stencilAttachment.GetTextureLevel());
}
static Bool IsColorAttachment(FramebufferAttachmentType attachmentType) {
@@ -1475,11 +1476,15 @@ void main() {
static Uint32 ResolveAttachmentBaseArrayLayer(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
const TextureUploadTarget uploadTarget = attachment.GetTextureUploadTarget();
if (IsCubeMapFaceUploadTarget(uploadTarget)) {
return static_cast<Uint32>(uploadTarget) - static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX);
// The face index IS the layer index, so it takes the same view shift as one that
// arrived through GetTextureLayer (see ToStorageArrayLayer).
const Int face = static_cast<Int>(uploadTarget) -
static_cast<Int>(TextureUploadTarget::CubeMapPositiveX);
return ToStorageArrayLayer(attachment.GetTexture().get(), face);
}
// Every other layered attachment names its layer directly. Returning 0 regardless made
// every blit, copy and ReadPixels against such an attachment read layer zero.
return static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
return ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer());
}
// A 3D image has arrayLayers == 1: its "layer" is a z slice, which has to travel as an
@@ -1755,10 +1760,10 @@ void main() {
outBinding.sampleCount = resource->sampleCount;
const auto attachmentExtent = attachment.GetSize();
outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()};
outBinding.mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0));
outBinding.mipLevel = ToStorageMipLevel(attachment.GetTexture().get(), attachment.GetTextureLevel());
outBinding.mipLevelCount = resource->mipLevels;
if (AttachmentIsDepthSlice(attachment)) {
outBinding.depthOffset = static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
outBinding.depthOffset = ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer());
outBinding.baseArrayLayer = 0;
} else {
outBinding.baseArrayLayer = ResolveAttachmentBaseArrayLayer(attachment);
@@ -1871,10 +1876,10 @@ void main() {
outBinding.sampleCount = resource->sampleCount;
const auto attachmentExtent = attachment.GetSize();
outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()};
outBinding.mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0));
outBinding.mipLevel = ToStorageMipLevel(attachment.GetTexture().get(), attachment.GetTextureLevel());
outBinding.mipLevelCount = resource->mipLevels;
if (AttachmentIsDepthSlice(attachment)) {
outBinding.depthOffset = static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
outBinding.depthOffset = ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer());
outBinding.baseArrayLayer = 0;
} else {
outBinding.baseArrayLayer = ResolveAttachmentBaseArrayLayer(attachment);
@@ -2020,10 +2025,10 @@ void main() {
outBinding.sampleCount = resource->sampleCount;
const auto attachmentExtent = attachment.GetSize();
outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()};
outBinding.mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0));
outBinding.mipLevel = ToStorageMipLevel(attachment.GetTexture().get(), attachment.GetTextureLevel());
outBinding.mipLevelCount = 1;
if (AttachmentIsDepthSlice(attachment)) {
outBinding.depthOffset = static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
outBinding.depthOffset = ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer());
outBinding.baseArrayLayer = 0;
} else {
outBinding.baseArrayLayer = ResolveAttachmentBaseArrayLayer(attachment);
@@ -9828,8 +9833,8 @@ void main() {
vkFormat = resource->format;
trackedLayout = &resource->layout;
imageAspect = resource->aspect;
mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0));
baseArrayLayer = static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
mipLevel = ToStorageMipLevel(attachment.GetTexture().get(), attachment.GetTextureLevel());
baseArrayLayer = ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer());
} else if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
const auto& renderbufferObject = attachment.GetRenderbuffer();
const Bool clearReady = MaterializePendingClearForRenderbuffer(frame.commandBuffer, renderbufferObject);
@@ -996,7 +996,7 @@ DECLARE_GL_FUNCTION_HEAD(void, MultiDrawArraysIndirect, GLenum mode, const void*
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirect, GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawElementsIndirect, mode, type, indirect, drawcount, stride)
DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocationIndex, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocationIndex, program, programInterface, name)
DECLARE_GL_FUNCTION_HEAD(void, ShaderStorageBlockBinding, GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderStorageBlockBinding, program, storageBlockIndex, storageBlockBinding)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
DECLARE_GL_FUNCTION_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset)
DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags)
DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data)
+293 -4
View File
@@ -186,6 +186,37 @@ namespace MobileGL::MG_Impl::GLImpl {
return TextureImpl::ValidateTextureInternalFormat(textureInternalFormat);
}
// How many LAYERS a texture of this target has, given its base level's state-side extent.
// GL keeps a 1D array's layer count in the height and every other layered target's in the
// depth; a cube map has exactly six and a 3D texture has one (its depth is spatial).
Uint LayerCountOfImmutableTexture(TextureTarget target, const IntVec3& baseSize) {
switch (target) {
case TextureTarget::Texture1DArray:
return static_cast<Uint>(std::max(baseSize.y(), 1));
case TextureTarget::Texture2DArray:
case TextureTarget::TextureCubeMapArray:
case TextureTarget::Texture2DMultisampleArray:
return static_cast<Uint>(std::max(baseSize.z(), 1));
case TextureTarget::TextureCubeMap:
return 6;
default:
return 1;
}
}
// GL 4.6 core 8.19: TexStorage* leaves the texture describing itself as a full-extent view
// of its own storage - TEXTURE_VIEW_MIN_LEVEL 0, TEXTURE_VIEW_NUM_LEVELS <levels>,
// TEXTURE_VIEW_MIN_LAYER 0, TEXTURE_VIEW_NUM_LAYERS the layer count. That is not just a
// query detail: glTextureView COMPOSES onto these ("<numlevels> and the value of
// TEXTURE_VIEW_NUM_LEVELS from the original texture minus <minlevel>", 8.18), so leaving
// them at the mutable-texture default of 0 would clamp every view to zero levels.
void SeedImmutableViewState(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Uint levels) {
if (!textureObject) return;
textureObject->SetViewLevelLayerRange(
0, levels, 0,
LayerCountOfImmutableTexture(textureObject->GetTarget(), textureObject->GetBaseSize()));
}
Bool ValidateTextureMutable(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
const char* caller) {
if (!textureObject || !textureObject->IsImmutable()) return true;
@@ -1373,15 +1404,20 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_IMAGE_FORMAT_COMPATIBILITY_TYPE:
*params = GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE;
break;
// Texture views are not implemented; a texture that is not a view reports the defaults
// GL 4.6 core table 23.17 gives (0 layers/levels of offset, and its own extent).
// GL 4.6 core table 23.17. All four start at 0 and stay there on a mutable texture;
// TexStorage* seeds them with the texture's full extent and glTextureView composes onto
// that (see SeedImmutableViewState and TextureView).
case GL_TEXTURE_VIEW_MIN_LEVEL:
*params = static_cast<GLint>(textureObject->GetViewMinLevel());
break;
case GL_TEXTURE_VIEW_MIN_LAYER:
*params = 0;
*params = static_cast<GLint>(textureObject->GetViewMinLayer());
break;
case GL_TEXTURE_VIEW_NUM_LEVELS:
*params = static_cast<GLint>(textureObject->GetViewNumLevels());
break;
case GL_TEXTURE_VIEW_NUM_LAYERS:
*params = 0;
*params = static_cast<GLint>(textureObject->GetViewNumLayers());
break;
default:
MG_State::pGLContext->RecordError(
@@ -2845,6 +2881,28 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = static_cast<GLint>(textureObject->GetImmutableLevels());
}
break;
// GL 4.6 core table 23.17. Zero on a mutable texture; TexStorage* seeds the full extent
// and glTextureView composes onto it (SeedImmutableViewState / TextureView).
case GL_TEXTURE_VIEW_MIN_LEVEL:
if (params) {
*params = static_cast<GLint>(textureObject->GetViewMinLevel());
}
break;
case GL_TEXTURE_VIEW_NUM_LEVELS:
if (params) {
*params = static_cast<GLint>(textureObject->GetViewNumLevels());
}
break;
case GL_TEXTURE_VIEW_MIN_LAYER:
if (params) {
*params = static_cast<GLint>(textureObject->GetViewMinLayer());
}
break;
case GL_TEXTURE_VIEW_NUM_LAYERS:
if (params) {
*params = static_cast<GLint>(textureObject->GetViewNumLayers());
}
break;
case GL_TEXTURE_BORDER_COLOR:
if (params) {
const auto& borderColor = textureObject->GetBorderColor();
@@ -3003,6 +3061,28 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = static_cast<GLfloat>(textureObject->GetImmutableLevels());
}
break;
// GL 4.6 core table 23.17; the float form answers the same state as the integer one
// (KHR-GL43.texture_view.gettexparameter queries both).
case GL_TEXTURE_VIEW_MIN_LEVEL:
if (params) {
*params = static_cast<GLfloat>(textureObject->GetViewMinLevel());
}
break;
case GL_TEXTURE_VIEW_NUM_LEVELS:
if (params) {
*params = static_cast<GLfloat>(textureObject->GetViewNumLevels());
}
break;
case GL_TEXTURE_VIEW_MIN_LAYER:
if (params) {
*params = static_cast<GLfloat>(textureObject->GetViewMinLayer());
}
break;
case GL_TEXTURE_VIEW_NUM_LAYERS:
if (params) {
*params = static_cast<GLfloat>(textureObject->GetViewNumLayers());
}
break;
case GL_TEXTURE_BORDER_COLOR:
if (params) {
const auto& borderColor = textureObject->GetBorderColor();
@@ -4779,6 +4859,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// longer pre-existing chain has to be dropped explicitly.
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
SeedImmutableViewState(textureObject, static_cast<Uint>(levels));
}
void TextureStorage2D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height) {
@@ -4853,6 +4934,7 @@ namespace MobileGL::MG_Impl::GLImpl {
textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels));
}
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
SeedImmutableViewState(textureObject, static_cast<Uint>(levels));
}
void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
@@ -4927,6 +5009,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// See TextureStorage1D.
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
SeedImmutableViewState(textureObject, static_cast<Uint>(levels));
}
// Shared front half of glTextureStorage2DMultisample/3DMultisample. The target forms are reached
@@ -5007,6 +5090,211 @@ namespace MobileGL::MG_Impl::GLImpl {
});
}
namespace {
void RecordTextureViewError(ErrorCode code, const String& message) {
MG_State::pGLContext->RecordError(code,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "TextureView", message));
}
// The internalformat the view-compatibility rule has to compare against, which is NOT
// always ConvertTextureInternalFormatToGLEnum(GetFormat()): MobileGL answers every
// compressed request with uncompressed storage and only remembers the requested enum on
// the side, so a BPTC parent would otherwise present itself as RGBA8 and admit an RGBA8
// view that table 8.21 forbids.
GLenum ResolveTextureViewSourceFormat(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
if (mipmapTexture != nullptr && !textureObject->GetUploadTargets().empty()) {
const TextureUploadTarget uploadTarget = textureObject->GetUploadTargets()[0];
const GLenum stored = mipmapTexture->GetMipmapCompressedFormat(uploadTarget, 0);
if (stored != GL_NONE) return stored;
const GLenum requested = mipmapTexture->GetMipmapRequestedCompressedFormat(uploadTarget, 0);
if (requested != GL_NONE) return requested;
}
return MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat());
}
Bool BackendSupportsTextureViews() {
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
if (!activeBackendObject) return false;
// Deliberately the ADVERTISED extension list rather than a separate capability bit:
// it makes "MobileGL claims GL_ARB_texture_view" and "glTextureView actually works"
// the same fact by construction. DirectVulkan always advertises it; DirectGLES only
// does when the driver has EXT/OES_texture_view, because ES cannot otherwise give two
// texture names one storage (see the no-EXT discussion in BackendObject_DirectGLES).
const auto& extensions = activeBackendObject->GetRendererInfo().RendererGLInfo.Extensions;
return std::find(extensions.begin(), extensions.end(), E_GL_ARB_texture_view) != extensions.end();
}
} // namespace
// glTextureView - ARB_texture_view, core since GL 4.3 (GL 4.6 core 8.18).
//
// Creates a texture whose STORAGE is another texture's, optionally reinterpreting the format
// and narrowing the level/layer range. The error list below is the spec's, in the order the
// conformance suite (KHR-GL43.texture_view.errors, cases a..s) walks it.
void TextureView(GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel,
GLuint numlevels, GLuint minlayer, GLuint numlayers) {
if (!BackendSupportsTextureViews()) {
// The honest answer when the backend cannot share one storage between two texture
// names. Raising an error - and withholding the GL_ARB_texture_view string - is the
// only alternative to a silent no-op that leaves the view with no storage at all,
// which is indistinguishable from success at the call site and renders garbage.
MGLOG_W_ONCE("glTextureView: the active backend has no texture-view support "
"(GL_EXT_texture_view / GL_OES_texture_view absent); raising GL_INVALID_OPERATION");
RecordTextureViewError(ErrorCode::InvalidOperation,
"The active backend does not support texture views.");
return;
}
const TextureTarget viewTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
if (!TextureImpl::ValidateTextureTarget(viewTarget)) return;
// a) <texture> is 0.
if (texture == 0) {
RecordTextureViewError(ErrorCode::InvalidValue, "texture must not be zero.");
return;
}
// b) <texture> is not a name returned by glGenTextures.
if (!MG_State::pGLContext->ValidateTextureName(texture)) {
RecordTextureViewError(ErrorCode::InvalidOperation,
std::format("texture {} is not a name returned by glGenTextures.", texture));
return;
}
// c) <texture> has already been bound and given a target. A name that any bind (or
// glCreateTextures, or an earlier glTextureView) has instantiated owns a texture object;
// only a still-uninstantiated reservation may become a view.
if (MG_State::pGLContext->ValidateTextureObject(texture)) {
RecordTextureViewError(ErrorCode::InvalidOperation,
std::format("texture {} has already been bound and given a target.", texture));
return;
}
// d) <origtexture> is not the name of a texture object. Note the error code differs from
// (b): INVALID_VALUE here, INVALID_OPERATION there.
auto origTextureObject = MG_State::pGLContext->GetTextureObject(origtexture);
if (origtexture == 0 || !origTextureObject) {
RecordTextureViewError(ErrorCode::InvalidValue,
std::format("origtexture {} is not the name of a texture object.", origtexture));
return;
}
// e) <origtexture> is a mutable texture object. A view aliases storage that can never be
// respecified underneath it, so only immutable storage qualifies.
if (!origTextureObject->IsImmutable()) {
RecordTextureViewError(ErrorCode::InvalidOperation,
std::format("origtexture {} does not have immutable storage.", origtexture));
return;
}
// f) target is incompatible with origtexture's target (table 8.20).
const TextureTarget origTarget = origTextureObject->GetTarget();
if (!TextureImpl::IsLegalTextureViewTargetPair(origTarget, viewTarget)) {
RecordTextureViewError(
ErrorCode::InvalidOperation,
std::format("target {} is not a legal texture-view target for an origtexture whose target is {}.",
MG_Util::ConvertGLEnumToString(target),
MG_Util::ConvertGLEnumToString(MG_Util::ConvertTextureTargetToGLEnum(origTarget))));
return;
}
// k)..q) the per-target <numlayers> constraints, all INVALID_VALUE.
const Uint requiredLayers = TextureImpl::RequiredTextureViewLayerCount(viewTarget);
if (requiredLayers != 0 && numlayers != requiredLayers) {
RecordTextureViewError(ErrorCode::InvalidValue,
std::format("target {} requires numlayers to be {}, but it is {}.",
MG_Util::ConvertGLEnumToString(target), requiredLayers, numlayers));
return;
}
if (viewTarget == TextureTarget::TextureCubeMapArray && (numlayers == 0 || numlayers % 6 != 0)) {
RecordTextureViewError(
ErrorCode::InvalidValue,
std::format("GL_TEXTURE_CUBE_MAP_ARRAY requires numlayers to be a multiple of 6, but it is {}.",
numlayers));
return;
}
// g)/h) the format-compatibility rule (table 8.21). A format WITH a view class may be
// reinterpreted as any other format in the same class; a format with NO entry in the
// table - every depth, stencil and depth/stencil format among them - may only ever be
// viewed as itself, which is why the Better Clouds D24S8 view must name
// GL_DEPTH24_STENCIL8 exactly.
const GLenum origFormat = ResolveTextureViewSourceFormat(origTextureObject);
const auto origViewClass = TextureImpl::GetTextureViewClass(origFormat);
if (origViewClass == TextureImpl::TextureViewClass::None) {
if (internalformat != origFormat) {
RecordTextureViewError(
ErrorCode::InvalidOperation,
std::format("origtexture's internal format {} has no view class, so internalformat must be "
"identical to it, but it is {}.",
MG_Util::ConvertGLEnumToString(origFormat),
MG_Util::ConvertGLEnumToString(internalformat)));
return;
}
} else if (TextureImpl::GetTextureViewClass(internalformat) != origViewClass) {
RecordTextureViewError(
ErrorCode::InvalidOperation,
std::format("internalformat {} is not in the same view class as origtexture's internal format {}.",
MG_Util::ConvertGLEnumToString(internalformat),
MG_Util::ConvertGLEnumToString(origFormat)));
return;
}
const TextureInternalFormat viewInternalFormat =
MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
if (!TextureImpl::ValidateTextureInternalFormat(viewInternalFormat)) return;
// i)/j) the range checks, both against the ORIGINAL's view state rather than its raw
// level/layer counts. On a plain immutable texture TexStorage* seeded those with the full
// extent, so the two agree; on a view-of-a-view they are what bounds the child to the
// parent's already-narrowed window.
const Uint origNumLevels = origTextureObject->GetViewNumLevels();
const Uint origNumLayers = origTextureObject->GetViewNumLayers();
if (minlevel >= origNumLevels) {
RecordTextureViewError(ErrorCode::InvalidValue,
std::format("minlevel {} is larger than origtexture's greatest level {}.", minlevel,
origNumLevels == 0 ? 0 : origNumLevels - 1));
return;
}
if (minlayer >= origNumLayers) {
RecordTextureViewError(ErrorCode::InvalidValue,
std::format("minlayer {} is larger than origtexture's greatest layer {}.", minlayer,
origNumLayers == 0 ? 0 : origNumLayers - 1));
return;
}
// r)/s) a cube-map or cube-map-array view demands square levels, because its faces are
// square by definition and the storage it borrows is not reshaped.
if (viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::TextureCubeMapArray) {
const IntVec3 baseSize = origTextureObject->GetBaseSize();
if (baseSize.x() != baseSize.y()) {
RecordTextureViewError(
ErrorCode::InvalidOperation,
std::format("a cube-map texture view requires origtexture's width and height to match, but "
"they are {}x{}.",
baseSize.x(), baseSize.y()));
return;
}
}
// GL 4.6 core 8.18, verbatim:
// TEXTURE_VIEW_MIN_LEVEL = <minlevel> + origtexture's TEXTURE_VIEW_MIN_LEVEL
// TEXTURE_VIEW_NUM_LEVELS = min(<numlevels>, origtexture's TEXTURE_VIEW_NUM_LEVELS - <minlevel>)
// TEXTURE_VIEW_MIN_LAYER = <minlayer> + origtexture's TEXTURE_VIEW_MIN_LAYER
// TEXTURE_VIEW_NUM_LAYERS = min(<numlayers>, origtexture's TEXTURE_VIEW_NUM_LAYERS - <minlayer>)
// Because the offsets ADD all the way down, the composed values are already expressed in
// the ROOT's coordinates - which is exactly what lets the view point straight at the root
// and skip the chain.
const auto& storageOwner =
origTextureObject->IsTextureView() ? origTextureObject->GetViewStorageOwner() : origTextureObject;
const Uint composedMinLevel = minlevel + origTextureObject->GetViewMinLevel();
const Uint composedNumLevels = std::min(numlevels, origNumLevels - minlevel);
const Uint composedMinLayer = minlayer + origTextureObject->GetViewMinLayer();
const Uint composedNumLayers = std::min(numlayers, origNumLayers - minlayer);
const auto& viewObject = MG_State::pGLContext->CreateTextureViewObject(
texture, viewTarget, storageOwner, composedMinLevel, composedNumLevels, composedMinLayer,
composedNumLayers);
if (!viewObject) {
RecordTextureViewError(ErrorCode::InvalidOperation, "Failed to create the texture view object.");
return;
}
viewObject->SetInternalFormat(viewInternalFormat);
viewObject->SetSamples(storageOwner->GetSamples());
viewObject->SetFixedSampleLocations(storageOwner->HasFixedSampleLocations());
}
void TexStorage1D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width) {
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
if (!TextureImpl::ValidateTextureTarget(textureTarget)) return;
@@ -5112,6 +5400,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject = activeUnit.GetBindingSlot(textureTarget).GetBoundObject();
if (!textureObject) return;
textureObject->SetImmutableLevels(1);
SeedImmutableViewState(textureObject, 1);
}
void TexStorage2DMultisample(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width,
@@ -60,6 +60,8 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetTextureParameteriv(GLuint texture, GLenum pname, GLint* params);
void GetTextureLevelParameterfv(GLuint texture, GLint level, GLenum pname, GLfloat* params);
void GetTextureLevelParameteriv(GLuint texture, GLint level, GLenum pname, GLint* params);
void TextureView(GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel,
GLuint numlevels, GLuint minlayer, GLuint numlayers);
void TexStorage1D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width);
void TexStorage2D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height);
void TexStorage3D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
@@ -623,4 +623,144 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
}
return true;
}
// GL 4.6 core table 8.21 ("Compatible internal formats for TextureView"), transcribed whole.
// Written against the raw GLenum rather than TextureInternalFormat on purpose: MobileGL's own
// enum collapses every compressed format onto uncompressed storage and drops formats it
// cannot carry, so classifying the converted value would silently widen the compatibility
// rule - GL_COMPRESSED_RG_RGTC2 and GL_RGBA8 would end up in the same class.
TextureViewClass GetTextureViewClass(GLenum internalformat) {
switch (internalformat) {
case GL_RGBA32F:
case GL_RGBA32UI:
case GL_RGBA32I:
return TextureViewClass::Bits128;
case GL_RGB32F:
case GL_RGB32UI:
case GL_RGB32I:
return TextureViewClass::Bits96;
case GL_RGBA16F:
case GL_RG32F:
case GL_RGBA16UI:
case GL_RG32UI:
case GL_RGBA16I:
case GL_RG32I:
case GL_RGBA16:
case GL_RGBA16_SNORM:
return TextureViewClass::Bits64;
case GL_RGB16:
case GL_RGB16_SNORM:
case GL_RGB16F:
case GL_RGB16UI:
case GL_RGB16I:
return TextureViewClass::Bits48;
case GL_RG16F:
case GL_R11F_G11F_B10F:
case GL_R32F:
case GL_RGB10_A2UI:
case GL_RGBA8UI:
case GL_RG16UI:
case GL_R32UI:
case GL_RGBA8I:
case GL_RG16I:
case GL_R32I:
case GL_RGB10_A2:
case GL_RGBA8:
case GL_RG16:
case GL_RGBA8_SNORM:
case GL_RG16_SNORM:
case GL_SRGB8_ALPHA8:
case GL_RGB9_E5:
return TextureViewClass::Bits32;
case GL_RGB8:
case GL_RGB8_SNORM:
case GL_SRGB8:
case GL_RGB8UI:
case GL_RGB8I:
return TextureViewClass::Bits24;
case GL_R16F:
case GL_RG8UI:
case GL_R16UI:
case GL_RG8I:
case GL_R16I:
case GL_RG8:
case GL_R16:
case GL_RG8_SNORM:
case GL_R16_SNORM:
return TextureViewClass::Bits16;
case GL_R8UI:
case GL_R8I:
case GL_R8:
case GL_R8_SNORM:
return TextureViewClass::Bits8;
case GL_COMPRESSED_RED_RGTC1:
case GL_COMPRESSED_SIGNED_RED_RGTC1:
return TextureViewClass::Rgtc1Red;
case GL_COMPRESSED_RG_RGTC2:
case GL_COMPRESSED_SIGNED_RG_RGTC2:
return TextureViewClass::Rgtc2Rg;
case GL_COMPRESSED_RGBA_BPTC_UNORM:
case GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM:
return TextureViewClass::BptcUnorm;
case GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT:
case GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT:
return TextureViewClass::BptcFloat;
default:
// Every depth/stencil format, every S3TC/ETC/ASTC format and every unsized format
// reaches here. The caller must then demand an EXACT format match.
return TextureViewClass::None;
}
}
// GL 4.6 core table 8.20 ("Legal texture targets for TextureView").
Bool IsLegalTextureViewTargetPair(TextureTarget origTarget, TextureTarget viewTarget) {
switch (origTarget) {
case TextureTarget::Texture1D:
return viewTarget == TextureTarget::Texture1D || viewTarget == TextureTarget::Texture1DArray;
case TextureTarget::Texture2D:
return viewTarget == TextureTarget::Texture2D || viewTarget == TextureTarget::Texture2DArray;
case TextureTarget::Texture3D:
return viewTarget == TextureTarget::Texture3D;
case TextureTarget::TextureCubeMap:
return viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::Texture2D ||
viewTarget == TextureTarget::Texture2DArray || viewTarget == TextureTarget::TextureCubeMapArray;
case TextureTarget::TextureRectangle:
return viewTarget == TextureTarget::TextureRectangle;
case TextureTarget::Texture1DArray:
return viewTarget == TextureTarget::Texture1DArray || viewTarget == TextureTarget::Texture1D;
case TextureTarget::Texture2DArray:
return viewTarget == TextureTarget::Texture2DArray || viewTarget == TextureTarget::Texture2D ||
viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::TextureCubeMapArray;
case TextureTarget::TextureCubeMapArray:
return viewTarget == TextureTarget::TextureCubeMapArray || viewTarget == TextureTarget::Texture2DArray ||
viewTarget == TextureTarget::Texture2D || viewTarget == TextureTarget::TextureCubeMap;
case TextureTarget::Texture2DMultisample:
case TextureTarget::Texture2DMultisampleArray:
return viewTarget == TextureTarget::Texture2DMultisample ||
viewTarget == TextureTarget::Texture2DMultisampleArray;
case TextureTarget::TextureBuffer:
// The table lists no legal target for a buffer texture: its storage is a buffer
// object, and there is nothing to make a view of.
return false;
default:
return false;
}
}
Uint RequiredTextureViewLayerCount(TextureTarget viewTarget) {
switch (viewTarget) {
case TextureTarget::TextureCubeMap:
return 6;
case TextureTarget::Texture1D:
case TextureTarget::Texture2D:
case TextureTarget::Texture3D:
case TextureTarget::TextureRectangle:
case TextureTarget::Texture2DMultisample:
return 1;
default:
// 1D/2D array, cube-map array, 2D multisample array: any count (the cube-map array's
// "multiple of 6" is checked by the caller).
return 0;
}
}
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
@@ -79,4 +79,33 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
// GL 4.6 SS 8.6 subset rule for glCopyTexImage*: the read buffer must supply every component
// the requested internalformat asks for, but may supply more.
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat);
// ---- glTextureView (ARB_texture_view / GL 4.6 core 8.18) ----
// Table 8.21's view classes. `None` is not a class - it means the format has NO entry in the
// table, which the spec turns into a much stricter rule than "same class": such a format can
// only ever be viewed as ITSELF. Every depth, stencil and depth/stencil format lands here,
// which is why the Better Clouds D24S8 view must name GL_DEPTH24_STENCIL8 exactly.
enum class TextureViewClass {
None = 0,
Bits128,
Bits96,
Bits64,
Bits48,
Bits32,
Bits24,
Bits16,
Bits8,
Rgtc1Red,
Rgtc2Rg,
BptcUnorm,
BptcFloat,
};
TextureViewClass GetTextureViewClass(GLenum internalformat);
// Table 8.20: which <target> values glTextureView accepts for a given origtexture target.
Bool IsLegalTextureViewTargetPair(TextureTarget origTarget, TextureTarget viewTarget);
// Table 8.20 again, read the other way: how many layers <target> requires. Returns 0 for the
// targets whose layer count is unconstrained (the array targets), 6 for GL_TEXTURE_CUBE_MAP,
// and 1 for every single-layer target. GL_TEXTURE_CUBE_MAP_ARRAY is special-cased by the
// caller because its constraint is "a multiple of 6", not an exact count.
Uint RequiredTextureViewLayerCount(TextureTarget viewTarget);
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
@@ -98,6 +98,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/VertexArrayEnableDisableScenario.cpp
Scenarios/CopyImageLevelRangeScenario.cpp
Scenarios/CopyImageLayeredScenario.cpp
Scenarios/TextureViewScenario.cpp
Scenarios/PackedWordReadbackScenario.cpp
Scenarios/LayeredAttachmentBarrierScenario.cpp
Scenarios/LayeredTextureReadbackScenario.cpp
@@ -0,0 +1,799 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TextureViewScenario.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
//
// glTextureView (ARB_texture_view / GL 4.6 core 8.18) end to end on both backends.
//
// THE DEFECT. glTextureView was a stub that logged once and returned. That is worse than not
// having the function: MobileGL advertises GL 4.6, so LWJGL resolves a non-null pointer, an
// application's capability check passes, it takes the texture-view path, and the view texture it
// then samples has no storage at all. Nothing errors; the picture is simply wrong. The Better
// Clouds Minecraft mod is exactly this shape - its GLCompat gates `supportsTextureView` on
// `caps.glTextureView != NULL`, which was already true, so it ran its FULL path against a view
// that aliased nothing.
//
// WHAT A VIEW IS, and why a copy cannot stand in for one. A view is a second texture NAME over
// the SAME storage. Two consequences the tests below pin, both of which a copy fails:
// * writes through either name are visible through the other (CoherencyIsBidirectional), and
// * the two names carry INDEPENDENT per-texture parameters at the same time - which is the
// entire point for Better Clouds: one D24S8 image, sampled in ONE shading pass through the
// parent with DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX and through the view with
// GL_DEPTH_COMPONENT (BetterCloudsCoveragePipeline below).
//
// MECHANISM PER BACKEND. DirectVulkan: the view resolves to the storage texture's ONE
// TextureResource - one VkImage, one tracked layout, one upload path - and its own VkImageViews
// (sub-range, reinterpreted VkFormat, its own aspect) are cached in alternateSampledViews /
// attachmentViews keyed by the whole window. DirectGLES: the view gets its own ES name minted by
// EXT/OES_texture_view over the storage texture's name, so the driver supplies the aliasing and
// per-name parameters come for free. Without that extension the frontend refuses glTextureView
// with GL_INVALID_OPERATION and withholds GL_ARB_texture_view rather than emulate by copying -
// see NoExtensionSupportIsRefusedRatherThanFaked.
//
// CONTROLS. Every case here would pass on a stub for at least one of its assertions, so each one
// also asserts something the stub cannot produce: a non-zero sampled value, a DIFFERENT value
// through the two names, or a value that changed after a write through the other name.
#include <array>
#include <cstdint>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kSize = 64;
// The lower strip no cloud quad covers, so coverage 0 / depth 0 is asserted too - a
// uniform image would otherwise pass a test that only ever looked at covered texels.
constexpr int kUncoveredTop = 16;
constexpr const char* kQuadVertexSource = R"(#version 330 core
in vec2 aPos;
uniform vec4 uRect; // x0, y0, x1, y1 in NDC
uniform float uDepth; // NDC z
void main() {
vec2 p = mix(uRect.xy, uRect.zw, aPos);
gl_Position = vec4(p, uDepth, 1.0);
}
)";
// Mirrors betterclouds_coverage.fsh's shape: a second fragment output at location 1 whose
// draw buffer is GL_NONE. The mod declares and writes it while glDrawBuffers names only
// COLOR_ATTACHMENT0, so a layer that mishandles a write to a NONE draw buffer would either
// error or clobber attachment 0.
constexpr const char* kCoverageFragmentSource = R"(#version 330 core
layout (location = 0) out vec4 outColor;
layout (location = 1) out float outUnused;
void main() {
outColor = vec4(1.0, 0.0, 0.0, 1.0);
outUnused = 1.0 / 255.0;
}
)";
// The Better Clouds shading pass, reduced to its sampling. Both fetches name the SAME
// D24S8 image through two GL texture names bound to two units in this one invocation.
// `ivec2(gl_FragCoord)` (a truncating vec4 -> ivec2 constructor) is the mod's own spelling
// at betterclouds_shading.fsh:56, kept verbatim because a strict GLSL front end can reject
// it; the depth fetch uses the conventional `.xy` form the mod uses at line 117.
constexpr const char* kShadingFragmentSource = R"(#version 330 core
uniform usampler2D uCoverage; // the PARENT, DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX
uniform sampler2D uDepthView; // the VIEW, DEPTH_STENCIL_TEXTURE_MODE = GL_DEPTH_COMPONENT
out vec4 outColor;
void main() {
uint coverage = texelFetch(uCoverage, ivec2(gl_FragCoord), 0).r;
float depth = texelFetch(uDepthView, ivec2(gl_FragCoord.xy), 0).r;
outColor = vec4(float(coverage) * 0.25, depth, 0.0, 1.0);
gl_FragDepth = depth;
}
)";
// Reads a reinterpreting view (GL_R32UI over GL_RGBA8 storage - both VIEW_CLASS_32_BITS)
// and unpacks the word back into the four bytes it was written as.
constexpr const char* kDecodeWordFragmentSource = R"(#version 330 core
uniform usampler2D uWords;
out vec4 outColor;
void main() {
uint word = texelFetch(uWords, ivec2(gl_FragCoord.xy), 0).r;
outColor = vec4(float((word ) & 0xFFu) / 255.0,
float((word >> 8) & 0xFFu) / 255.0,
float((word >> 16) & 0xFFu) / 255.0,
float((word >> 24) & 0xFFu) / 255.0);
}
)";
constexpr const char* kSampleFragmentSource = R"(#version 330 core
uniform sampler2D uTexture;
uniform float uLod;
out vec4 outColor;
void main() {
outColor = textureLod(uTexture, gl_FragCoord.xy / 64.0, uLod);
}
)";
std::string Describe(const Rgba8& c) {
return "rgba(" + std::to_string(c.r) + "," + std::to_string(c.g) + "," + std::to_string(c.b) + "," +
std::to_string(c.a) + ")";
}
class TextureViewScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
if (!TextureViewUsable()) {
GTEST_SKIP() << "glTextureView is unavailable on backend " << Gl().BackendName()
<< " (GL_ARB_texture_view not advertised)";
}
}
void TearDown() override {
if (!Ready()) return;
for (const GLuint texture : m_textures) {
glDeleteTextures(1, &texture);
}
m_textures.clear();
for (const GLuint fbo : m_fbos) {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &fbo);
}
m_fbos.clear();
for (const GLuint rbo : m_rbos) {
glDeleteRenderbuffers(1, &rbo);
}
m_rbos.clear();
for (const GLuint program : m_programs) {
glDeleteProgram(program);
}
m_programs.clear();
if (m_vao != 0) {
glBindVertexArray(0);
glDeleteVertexArrays(1, &m_vao);
m_vao = 0;
}
if (m_vbo != 0) {
glDeleteBuffers(1, &m_vbo);
m_vbo = 0;
}
}
// A trivial same-format full-range view. It exercises nothing the cases below test,
// so a backend that simply does not have the feature skips instead of failing every
// one of them - the same shape CopyImageLayeredScenario uses for glCopyImageSubData.
bool TextureViewUsable() {
GLuint storage = 0;
glGenTextures(1, &storage);
glBindTexture(GL_TEXTURE_2D, storage);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 1, 1);
glBindTexture(GL_TEXTURE_2D, 0);
GLuint view = 0;
glGenTextures(1, &view);
while (glGetError() != GL_NO_ERROR) {
}
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
const bool usable = glGetError() == GL_NO_ERROR;
glDeleteTextures(1, &view);
glDeleteTextures(1, &storage);
return usable;
}
GLuint MakeVao() {
if (m_vao != 0) return m_vao;
// A unit quad; the vertex shader maps it onto whatever NDC rect uRect names, so
// one buffer serves every draw here.
static constexpr float kQuad[] = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
return m_vao;
}
GLuint MakeProgram(const char* vertexSource, const char* fragmentSource) {
std::string error;
const GLuint program = CompileProgram(vertexSource, fragmentSource, &error);
EXPECT_NE(program, 0u) << "program failed to build: " << error;
if (program != 0) m_programs.push_back(program);
return program;
}
GLuint MakeTexture() {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
return texture;
}
GLuint MakeFbo() {
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
m_fbos.push_back(fbo);
return fbo;
}
// A 2D texture with immutable storage and NEAREST filtering, i.e. what every case
// here views. Levels beyond 1 stay undefined until a caller fills them.
GLuint MakeImmutable2D(GLenum internalFormat, int levels, int width, int height) {
const GLuint texture = MakeTexture();
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, levels, internalFormat, width, height);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
return texture;
}
void DrawQuad(GLuint program, float x0, float y0, float x1, float y1, float depth) {
glUseProgram(program);
glUniform4f(glGetUniformLocation(program, "uRect"), x0, y0, x1, y1);
const GLint depthLocation = glGetUniformLocation(program, "uDepth");
if (depthLocation >= 0) glUniform1f(depthLocation, depth);
glBindVertexArray(MakeVao());
glDrawArrays(GL_TRIANGLES, 0, 6);
}
// Reads the colour texture currently attached to `fbo` as COLOR_ATTACHMENT0.
Image ReadFbo(GLuint fbo, int width, int height) {
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glReadBuffer(GL_COLOR_ATTACHMENT0);
return ReadPixels(width, height);
}
// Every pixel of the inclusive region must match `expected` within `tolerance` per
// channel. Whole-region rather than a spot check, for the reason HeadlessGL.h gives:
// three of four vertices carrying stale data still paints a correct centre pixel.
void ExpectRegion(const Image& image, int x0, int x1, int y0, int y1, Rgba8 expected, int tolerance,
const char* what) {
int offenders = 0;
Rgba8 firstOffender{};
int firstX = -1;
int firstY = -1;
for (int y = y0; y <= y1; ++y) {
for (int x = x0; x <= x1; ++x) {
const Rgba8 actual = image.At(x, y);
const bool ok = std::abs(int(actual.r) - int(expected.r)) <= tolerance &&
std::abs(int(actual.g) - int(expected.g)) <= tolerance &&
std::abs(int(actual.b) - int(expected.b)) <= tolerance &&
std::abs(int(actual.a) - int(expected.a)) <= tolerance;
if (!ok) {
if (offenders == 0) {
firstOffender = actual;
firstX = x;
firstY = y;
}
++offenders;
}
}
}
EXPECT_EQ(offenders, 0) << what << ": " << offenders << " of "
<< (x1 - x0 + 1) * (y1 - y0 + 1) << " pixels disagree; first at (" << firstX
<< ", " << firstY << ") is " << Describe(firstOffender) << ", expected "
<< Describe(expected) << " +/- " << tolerance;
}
std::vector<GLuint> m_textures;
std::vector<GLuint> m_fbos;
std::vector<GLuint> m_rbos;
std::vector<GLuint> m_programs;
GLuint m_vao = 0;
GLuint m_vbo = 0;
};
// ------------------------------------------------------------------------------------
// The driving case: the Better Clouds full-mode pipeline, in its real order.
// ------------------------------------------------------------------------------------
TEST_F(TextureViewScenario, BetterCloudsCoveragePipeline) {
if (!Ready() || IsSkipped()) return;
// --- Resources.java:230-251, in order ---------------------------------------------
const GLuint coverageColor = MakeImmutable2D(GL_RGBA8, 1, kSize, kSize);
const GLuint coverage = MakeTexture();
glBindTexture(GL_TEXTURE_2D, coverage);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH24_STENCIL8, kSize, kSize);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
const GLuint coverageFbo = MakeFbo();
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, coverageFbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, coverageColor, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, coverage, 0);
const GLenum drawBuffers[] = {GL_COLOR_ATTACHMENT0};
glDrawBuffers(1, drawBuffers);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
<< "the coverage framebuffer is incomplete; the mod would silently demote to its "
"fallback configuration here (Resources.java:187-209)";
// The view is made from a name glGenTextures has only RESERVED - it has never been
// bound, so glTextureView has to instantiate the texture object itself.
const GLuint coverageDepthView = MakeTexture();
glTextureView(coverageDepthView, GL_TEXTURE_2D, coverage, GL_DEPTH24_STENCIL8, 0, 1, 0, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glTextureView raised an error";
glBindTexture(GL_TEXTURE_2D, coverageDepthView);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT);
glBindTexture(GL_TEXTURE_2D, 0);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "setting up the view raised an error";
// The two names must be distinguishable through the queries, or nothing below proves
// which one produced a sample.
GLint parentMode = 0;
GLint viewMode = 0;
glBindTexture(GL_TEXTURE_2D, coverage);
glGetTexParameteriv(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, &parentMode);
glBindTexture(GL_TEXTURE_2D, coverageDepthView);
glGetTexParameteriv(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, &viewMode);
glBindTexture(GL_TEXTURE_2D, 0);
EXPECT_EQ(parentMode, GL_STENCIL_INDEX) << "the parent must keep the stencil aspect";
EXPECT_EQ(viewMode, GL_DEPTH_COMPONENT)
<< "the view must carry its OWN depth-stencil mode; sharing one parameter set with "
"the parent is precisely what a texture view exists to avoid";
// --- OpenGLRenderer.java:244-344, the coverage pass -------------------------------
const GLuint coverageProgram = MakeProgram(kQuadVertexSource, kCoverageFragmentSource);
ASSERT_NE(coverageProgram, 0u);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, coverageFbo);
glViewport(0, 0, kSize, kSize);
glEnable(GL_DEPTH_TEST);
glDepthMask(GL_TRUE);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
// Reverse-Z, as the mod runs it (OpenGLRenderer.java:236/241).
glClearDepth(0.0);
glDepthFunc(GL_GEQUAL);
glDisable(GL_BLEND);
glEnable(GL_STENCIL_TEST);
glStencilMask(0xff);
glClearStencil(0);
// The coverage COUNT: one increment per depth-passing cloud fragment.
glStencilOp(GL_KEEP, GL_INCR, GL_INCR);
glStencilFunc(GL_ALWAYS, 0xff, 0xff);
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Quad 1 covers everything above the uncovered strip, at window depth 0.25.
const float stripTop = 2.0f * (float(kUncoveredTop) / float(kSize)) - 1.0f;
DrawQuad(coverageProgram, -1.0f, stripTop, 1.0f, 1.0f, -0.5f);
// Quad 2 covers the right half of that, at window depth 0.75 - nearer under GEQUAL,
// so it both passes the depth test and increments the stencil a second time.
DrawQuad(coverageProgram, 0.0f, stripTop, 1.0f, 1.0f, 0.5f);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the coverage pass raised an error";
// --- OpenGLRenderer.java:393-464, the shading pass --------------------------------
// A different draw framebuffer, exactly as the mod does (it hands the frame back to
// Blaze3D before shading). The coverage texture stays ATTACHED to coverageFbo while
// being sampled here, which is the shape a lazy/deferred FBO binding gets wrong.
ColorFbo destination = MakeColorFbo(kSize, kSize);
ASSERT_NE(destination.fbo, 0u);
GLuint destinationDepth = 0;
glGenRenderbuffers(1, &destinationDepth);
m_rbos.push_back(destinationDepth);
glBindRenderbuffer(GL_RENDERBUFFER, destinationDepth);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, kSize, kSize);
glBindFramebuffer(GL_FRAMEBUFFER, destination.fbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, destinationDepth);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
glViewport(0, 0, kSize, kSize);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClearDepth(0.0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glDepthFunc(GL_GEQUAL);
glDepthMask(GL_TRUE);
glEnable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
// The mod's own indexed/non-indexed colour-mask pair (OpenGLRenderer.java:411-412).
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glColorMaski(0, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
const GLuint shadingProgram = MakeProgram(kQuadVertexSource, kShadingFragmentSource);
ASSERT_NE(shadingProgram, 0u);
glUseProgram(shadingProgram);
// Unit 1 = the view (depth aspect), unit 3 = the parent (stencil aspect), the mod's
// own unit assignment (Resources.java:309/311).
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, coverageDepthView);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, coverage);
glUniform1i(glGetUniformLocation(shadingProgram, "uDepthView"), 1);
glUniform1i(glGetUniformLocation(shadingProgram, "uCoverage"), 3);
glActiveTexture(GL_TEXTURE0);
DrawQuad(shadingProgram, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the shading pass raised an error";
const Image shaded = ReadFbo(destination.fbo, kSize, kSize);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
// R = coverage * 0.25 (so 1 -> 64, 2 -> 128), G = the depth read THROUGH THE VIEW.
// A stub view samples (0,0,0,1), which fails the green channel of both covered
// regions; a view that inherited the parent's stencil aspect fails them too.
constexpr int kTolerance = 3;
ExpectRegion(shaded, 1, kSize - 2, 1, kUncoveredTop - 2, Rgba8{0, 0, 0, 255}, kTolerance,
"the uncovered strip must read coverage 0 and cleared depth 0");
ExpectRegion(shaded, 1, kSize / 2 - 2, kUncoveredTop + 1, kSize - 2, Rgba8{64, 64, 0, 255}, kTolerance,
"one cloud quad: stencil 1 through the parent, window depth 0.25 through the view");
ExpectRegion(shaded, kSize / 2 + 1, kSize - 2, kUncoveredTop + 1, kSize - 2, Rgba8{128, 191, 0, 255},
kTolerance,
"two overlapping cloud quads: stencil 2 through the parent, window depth 0.75 "
"through the view");
DestroyColorFbo(destination);
}
// ------------------------------------------------------------------------------------
// Storage sharing, in both directions. This is the assertion a copy-based emulation
// fails, and the reason the no-EXT path refuses rather than emulates.
// ------------------------------------------------------------------------------------
TEST_F(TextureViewScenario, CoherencyIsBidirectional) {
if (!Ready() || IsSkipped()) return;
const GLuint storage = MakeImmutable2D(GL_RGBA8, 1, kSize, kSize);
const GLuint view = MakeTexture();
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
// Render red through the PARENT's name...
const GLuint fbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
glViewport(0, 0, kSize, kSize);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// ...and read it back through the VIEW's.
const GLuint viewFbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
<< "a texture view must be attachable like any other texture";
Image throughView = ReadFbo(viewFbo, kSize, kSize);
ExpectRegion(throughView, 0, kSize - 1, 0, kSize - 1, Rgba8{255, 0, 0, 255}, 1,
"a write through the parent must be visible through the view");
// Now the other direction: write green through the VIEW, read through the PARENT.
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
const Image throughParent = ReadFbo(fbo, kSize, kSize);
ExpectRegion(throughParent, 0, kSize - 1, 0, kSize - 1, Rgba8{0, 255, 0, 255}, 1,
"a write through the view must be visible through the parent - they are one "
"storage, not two");
}
// ------------------------------------------------------------------------------------
// Format reinterpretation within a view class (GL 4.6 core table 8.21).
// ------------------------------------------------------------------------------------
TEST_F(TextureViewScenario, ReinterpretingViewReadsTheSameBitsThroughAnotherFormat) {
if (!Ready() || IsSkipped()) return;
// GL_RGBA8 and GL_R32UI are both VIEW_CLASS_32_BITS, so one may be viewed as the
// other. Filling the RGBA8 storage with a known byte pattern makes the R32UI view's
// answer a fact about the BITS rather than about the colour.
const GLuint storage = MakeImmutable2D(GL_RGBA8, 1, kSize, kSize);
std::vector<std::uint8_t> texels(static_cast<std::size_t>(kSize) * kSize * 4);
for (std::size_t i = 0; i < texels.size(); i += 4) {
texels[i + 0] = 0x40;
texels[i + 1] = 0x80;
texels[i + 2] = 0xC0;
texels[i + 3] = 0xFF;
}
glBindTexture(GL_TEXTURE_2D, storage);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kSize, kSize, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
glBindTexture(GL_TEXTURE_2D, 0);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the storage raised an error";
// NEGATIVE CONTROL. Everything below reads the storage through a REINTERPRETING view,
// so a test that only asserted the view's answer could not tell "the reinterpret is
// wrong" from "the seed never reached the GPU at all". Read the same texels through
// the parent's own format first.
const GLuint parentFbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, parentFbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
const Image seeded = ReadFbo(parentFbo, kSize, kSize);
ExpectRegion(seeded, 0, kSize - 1, 0, kSize - 1, Rgba8{0x40, 0x80, 0xC0, 0xFF}, 1,
"control: the storage must hold the seeded byte pattern before any view reads it");
const GLuint view = MakeTexture();
glTextureView(view, GL_TEXTURE_2D, storage, GL_R32UI, 0, 1, 0, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "an in-class reinterpret must be accepted";
glBindTexture(GL_TEXTURE_2D, view);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
GLint viewFormat = 0;
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &viewFormat);
glBindTexture(GL_TEXTURE_2D, 0);
EXPECT_EQ(viewFormat, GL_R32UI) << "the view must report its OWN internal format";
// A REAL GL_R32UI texture holding the very word the storage's bytes spell. The
// assertion below is that the view and this texture sample IDENTICALLY.
//
// Comparing against a reference texture rather than against a hard-coded colour is
// deliberate. Sampling a 32-bit integer texture is not itself what this scenario is
// about, and llvmpipe's ES driver does it inconsistently (verified outside MobileGL,
// with a raw-EGL program that reproduces the same wrong decode with NO view in play).
// Holding both sides to the same driver factors that out completely: whatever the
// driver makes of a usampler2D fetch, the view has to make the same thing of it, or
// it is not delivering the storage's bits. A view that samples zero, that lands on
// the wrong texels, or that lost its format still fails.
constexpr std::uint32_t kExpectedWord = 0xFFC08040u; // little-endian A,B,G,R
const GLuint reference = MakeImmutable2D(GL_R32UI, 1, kSize, kSize);
std::vector<std::uint32_t> words(static_cast<std::size_t>(kSize) * kSize, kExpectedWord);
glBindTexture(GL_TEXTURE_2D, reference);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kSize, kSize, GL_RED_INTEGER, GL_UNSIGNED_INT, words.data());
glBindTexture(GL_TEXTURE_2D, 0);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the reference texture failed";
const GLuint program = MakeProgram(kQuadVertexSource, kDecodeWordFragmentSource);
ASSERT_NE(program, 0u);
ColorFbo destination = MakeColorFbo(kSize, kSize);
ASSERT_NE(destination.fbo, 0u);
const auto decodeThrough = [&](GLuint texture) {
glBindFramebuffer(GL_FRAMEBUFFER, destination.fbo);
glViewport(0, 0, kSize, kSize);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glUniform1i(glGetUniformLocation(program, "uWords"), 0);
DrawQuad(program, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "sampling raised an error";
return ReadFbo(destination.fbo, kSize, kSize);
};
const Image throughReference = decodeThrough(reference);
const Image throughView = decodeThrough(view);
// Guard against the degenerate agreement of two black images: the reference must
// itself carry something, or "identical" would prove nothing.
const Rgba8 referenceTexel = throughReference.At(kSize / 2, kSize / 2);
ASSERT_FALSE(referenceTexel == (Rgba8{0, 0, 0, 0}))
<< "the reference GL_R32UI texture sampled as nothing, so the comparison below is vacuous";
std::size_t mismatches = 0;
for (int y = 0; y < kSize; ++y) {
for (int x = 0; x < kSize; ++x) {
if (!(throughView.At(x, y) == throughReference.At(x, y))) ++mismatches;
}
}
EXPECT_EQ(mismatches, 0u)
<< "the GL_R32UI view of GL_RGBA8 storage must sample exactly what a real GL_R32UI texture "
"holding the same word does; view centre is " << Describe(throughView.At(kSize / 2, kSize / 2))
<< ", reference centre is " << Describe(referenceTexel);
DestroyColorFbo(destination);
}
// ------------------------------------------------------------------------------------
// Sub-ranges: one mip level of two, and one layer of an array.
// ------------------------------------------------------------------------------------
TEST_F(TextureViewScenario, ViewOfOneMipLevelAddressesThatLevelAsItsOwnLevelZero) {
if (!Ready() || IsSkipped()) return;
const GLuint storage = MakeImmutable2D(GL_RGBA8, 2, kSize, kSize);
// Level 0 red, level 1 blue, so the view's answer names the level it opened onto.
const GLuint seedFbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, seedFbo);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
glViewport(0, 0, kSize, kSize);
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 1);
glViewport(0, 0, kSize / 2, kSize / 2);
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the mip chain raised an error";
const GLuint view = MakeTexture();
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 1, 1, 0, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
GLint minLevel = -1;
GLint numLevels = -1;
GLint immutableLevels = -1;
glBindTexture(GL_TEXTURE_2D, view);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL, &minLevel);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS, &numLevels);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_LEVELS, &immutableLevels);
glBindTexture(GL_TEXTURE_2D, 0);
EXPECT_EQ(minLevel, 1);
EXPECT_EQ(numLevels, 1);
// GL 4.6 core 8.18: inherited from the ORIGINAL, not set to <numlevels>.
EXPECT_EQ(immutableLevels, 2) << "TEXTURE_IMMUTABLE_LEVELS is the original texture's value";
// The view's level 0 IS the parent's level 1: attaching level 0 of the view must find
// the blue half-size image.
const GLuint viewFbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
const Image levelOne = ReadFbo(viewFbo, kSize / 2, kSize / 2);
ExpectRegion(levelOne, 0, kSize / 2 - 1, 0, kSize / 2 - 1, Rgba8{0, 0, 255, 255}, 1,
"the view's level 0 must be the parent's level 1 (blue), not its level 0 (red)");
}
TEST_F(TextureViewScenario, ViewOfOneArrayLayerAddressesThatLayer) {
if (!Ready() || IsSkipped()) return;
constexpr int kLayers = 4;
constexpr int kChosenLayer = 2;
const GLuint storage = MakeTexture();
glBindTexture(GL_TEXTURE_2D_ARRAY, storage);
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kSize, kSize, kLayers);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
// A different colour per layer, so a view that lost its layer offset reads the wrong
// one rather than merely reading nothing.
const GLuint seedFbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, seedFbo);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glViewport(0, 0, kSize, kSize);
for (int layer = 0; layer < kLayers; ++layer) {
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, storage, 0, layer);
glClearColor(float(layer) / 8.0f, 1.0f - float(layer) / 8.0f, 0.5f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the array layers raised an error";
const GLuint view = MakeTexture();
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, kChosenLayer, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "2D_ARRAY -> 2D is a legal view pair";
GLint minLayer = -1;
GLint numLayers = -1;
glBindTexture(GL_TEXTURE_2D, view);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LAYER, &minLayer);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LAYERS, &numLayers);
glBindTexture(GL_TEXTURE_2D, 0);
EXPECT_EQ(minLayer, kChosenLayer);
EXPECT_EQ(numLayers, 1);
const GLuint viewFbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
const Image sliced = ReadFbo(viewFbo, kSize, kSize);
const Rgba8 expected{static_cast<std::uint8_t>(kChosenLayer * 255 / 8),
static_cast<std::uint8_t>(255 - kChosenLayer * 255 / 8), 128, 255};
ExpectRegion(sliced, 0, kSize - 1, 0, kSize - 1, expected, 2,
"a single-layer 2D view of an array must address the layer it named");
}
// ------------------------------------------------------------------------------------
// Views of views compose; the composed view still reaches the ROOT storage.
// ------------------------------------------------------------------------------------
TEST_F(TextureViewScenario, ViewOfAViewComposesTheLevelRanges) {
if (!Ready() || IsSkipped()) return;
constexpr int kLevels = 3;
const GLuint storage = MakeImmutable2D(GL_RGBA8, kLevels, kSize, kSize);
const GLuint seedFbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, seedFbo);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
for (int level = 0; level < kLevels; ++level) {
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, level);
glViewport(0, 0, kSize >> level, kSize >> level);
glClearColor(0.0f, 0.0f, float(level + 1) / 4.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
// First view opens onto levels [1, 3); the second takes level 1 OF THAT, which is the
// root's level 2. GL 4.6 core 8.18 makes the offsets add.
const GLuint firstView = MakeTexture();
glTextureView(firstView, GL_TEXTURE_2D, storage, GL_RGBA8, 1, 2, 0, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
const GLuint secondView = MakeTexture();
glTextureView(secondView, GL_TEXTURE_2D, firstView, GL_RGBA8, 1, 1, 0, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "origtexture may itself be a view";
GLint minLevel = -1;
GLint numLevels = -1;
glBindTexture(GL_TEXTURE_2D, secondView);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL, &minLevel);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS, &numLevels);
glBindTexture(GL_TEXTURE_2D, 0);
EXPECT_EQ(minLevel, 2) << "TEXTURE_VIEW_MIN_LEVEL adds the original's";
EXPECT_EQ(numLevels, 1);
const GLuint viewFbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, secondView, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
const Image composed = ReadFbo(viewFbo, kSize >> 2, kSize >> 2);
ExpectRegion(composed, 0, (kSize >> 2) - 1, 0, (kSize >> 2) - 1, Rgba8{0, 0, 191, 255}, 2,
"the composed view must land on the root's level 2");
}
// ------------------------------------------------------------------------------------
// GL name-deletion semantics: the storage outlives the original's NAME.
// ------------------------------------------------------------------------------------
TEST_F(TextureViewScenario, DeletingTheOriginalKeepsTheViewUsable) {
if (!Ready() || IsSkipped()) return;
GLuint storage = 0;
glGenTextures(1, &storage);
glBindTexture(GL_TEXTURE_2D, storage);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSize, kSize);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
const GLuint view = MakeTexture();
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
const GLuint fbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
glViewport(0, 0, kSize, kSize);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// The NAME goes; the storage may not, because a view still references it
// (GL 4.6 core 5.1.2 - an object is not deleted while anything still refers to it).
glDeleteTextures(1, &storage);
EXPECT_EQ(glIsTexture(storage), static_cast<GLboolean>(GL_FALSE));
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
// Sample the view through a shader, so the answer comes from a live descriptor rather
// than from an attachment the frontend might have kept alive by other means.
ColorFbo destination = MakeColorFbo(kSize, kSize);
ASSERT_NE(destination.fbo, 0u);
glBindFramebuffer(GL_FRAMEBUFFER, destination.fbo);
glViewport(0, 0, kSize, kSize);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
const GLuint program = MakeProgram(kQuadVertexSource, kSampleFragmentSource);
ASSERT_NE(program, 0u);
glUseProgram(program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, view);
glUniform1i(glGetUniformLocation(program, "uTexture"), 0);
glUniform1f(glGetUniformLocation(program, "uLod"), 0.0f);
DrawQuad(program, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
const Image sampled = ReadFbo(destination.fbo, kSize, kSize);
ExpectRegion(sampled, 1, kSize - 2, 1, kSize - 2, Rgba8{0, 255, 255, 255}, 2,
"the view must still reach its storage after the original's name was deleted");
DestroyColorFbo(destination);
}
} // namespace
} // namespace MGITest
+7
View File
@@ -267,6 +267,13 @@ namespace MobileGL::MG_State {
return m_textureState.CreateTextureObject(index, target);
}
const SharedPtr<ITextureObject>& GLContext::CreateTextureViewObject(
Uint index, TextureTarget target, const SharedPtr<ITextureObject>& storageOwner, Uint minLevel,
Uint numLevels, Uint minLayer, Uint numLayers) {
return m_textureState.CreateTextureViewObject(index, target, storageOwner, minLevel, numLevels, minLayer,
numLayers);
}
void GLContext::MarkTextureObjectForDeletion(Uint index) {
// GL 3.3 core 4.4.2: deleting a texture whose image is attached to the framebuffer
// that is currently bound acts as if FramebufferTexture* had been called with texture
+5
View File
@@ -111,6 +111,11 @@ namespace MobileGL {
// Per-target default texture object (name 0); see TextureState::GetDefaultTextureObject.
const SharedPtr<ITextureObject>& GetDefaultTextureObject(TextureTarget target) const;
const SharedPtr<ITextureObject>& CreateTextureObject(Uint index, TextureTarget target);
// See TextureState::CreateTextureViewObject (glTextureView, GL 4.6 core 8.18).
const SharedPtr<ITextureObject>& CreateTextureViewObject(Uint index, TextureTarget target,
const SharedPtr<ITextureObject>& storageOwner,
Uint minLevel, Uint numLevels, Uint minLayer,
Uint numLayers);
void MarkTextureObjectForDeletion(Uint index);
TextureUnit& GetTextureUnitObject(Int unit);
ImageTextureBinding& GetImageTextureBinding(Int unit);
@@ -291,6 +291,13 @@ namespace MobileGL {
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();
}
@@ -78,6 +78,29 @@ namespace MobileGL::MG_State::GLState {
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 "<minlevel> 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<ITextureObject>& 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;
};
@@ -123,6 +146,18 @@ namespace MobileGL::MG_State::GLState {
Bool HasFixedSampleLocations() const override;
void SetFixedSampleLocations(Bool fixedSampleLocations) override;
Uint64 GetLifetimeId() const override;
// A plain texture owns its storage; TextureObjectView overrides this.
const SharedPtr<ITextureObject>& 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
@@ -165,6 +200,12 @@ namespace MobileGL::MG_State::GLState {
// 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;
};
@@ -0,0 +1,289 @@
// MobileGL - MobileGL/MG_State/GLState/TextureState/TextureObjectView.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 "TextureObjectView.h"
#include <algorithm>
namespace MobileGL::MG_State::GLState {
namespace {
// Where a target keeps its LAYER count. GL puts a 1D array's layers in the state-side
// height (that is what glTexImage2D(GL_TEXTURE_1D_ARRAY, width, layers) means, and what
// TextureObject.cpp's completeness walk assumes); every other layered target keeps them
// in z. GL_TEXTURE_3D is deliberately None: its depth is a spatial axis, not layers, and
// ARB_texture_view forbids anything but a full-depth 3D->3D view of it.
enum class LayerAxis { None, Y, Z };
LayerAxis LayerAxisOf(TextureTarget target) {
switch (target) {
case TextureTarget::Texture1DArray:
return LayerAxis::Y;
case TextureTarget::Texture2DArray:
case TextureTarget::TextureCubeMapArray:
case TextureTarget::Texture2DMultisampleArray:
return LayerAxis::Z;
default:
return LayerAxis::None;
}
}
Vector<TextureUploadTarget> UploadTargetsForViewTarget(TextureTarget target) {
switch (target) {
case TextureTarget::Texture1D:
return {TextureUploadTarget::Texture1D};
case TextureTarget::Texture2D:
return {TextureUploadTarget::Texture2D};
case TextureTarget::Texture3D:
return {TextureUploadTarget::Texture3D};
case TextureTarget::TextureRectangle:
return {TextureUploadTarget::TextureRectangle};
case TextureTarget::Texture1DArray:
return {TextureUploadTarget::Texture1DArray};
case TextureTarget::Texture2DArray:
return {TextureUploadTarget::Texture2DArray};
case TextureTarget::TextureCubeMapArray:
return {TextureUploadTarget::CubeMapArray};
case TextureTarget::Texture2DMultisample:
return {TextureUploadTarget::Texture2DMultisample};
case TextureTarget::Texture2DMultisampleArray:
return {TextureUploadTarget::Texture2DMultisampleArray};
case TextureTarget::TextureCubeMap:
return {TextureUploadTarget::CubeMapPositiveX, TextureUploadTarget::CubeMapNegativeX,
TextureUploadTarget::CubeMapPositiveY, TextureUploadTarget::CubeMapNegativeY,
TextureUploadTarget::CubeMapPositiveZ, TextureUploadTarget::CubeMapNegativeZ};
default:
MOBILEGL_ASSERT(false, "TextureObjectView: target %d cannot be a texture view", (int)target);
return {TextureUploadTarget::Texture2D};
}
}
} // namespace
TextureObjectView::TextureObjectView(Uint externalIndex, TextureTarget target,
SharedPtr<ITextureObject> storageOwner, Uint minLevel, Uint numLevels,
Uint minLayer, Uint numLayers)
: TextureObjectMipmap(target, externalIndex), m_storageOwner(Move(storageOwner)),
m_uploadTargets(UploadTargetsForViewTarget(target)) {
MOBILEGL_ASSERT(m_storageOwner != nullptr, "TextureObjectView: storage owner is null");
MOBILEGL_ASSERT(!m_storageOwner->IsTextureView(),
"TextureObjectView: storage owner must be a root texture, not another view");
m_ownerMipmap = AsMipmapTexture(m_storageOwner.get());
SetViewLevelLayerRange(minLevel, numLevels, minLayer, numLayers);
// Held rather than forwarded so the base class's level-range clamp works against the
// view's OWN level count - TEXTURE_BASE_LEVEL / TEXTURE_MAX_LEVEL on a view are relative
// 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);
}
Uint TextureObjectView::GetImmutableLevels() const {
return m_storageOwner->GetImmutableLevels();
}
Uint64 TextureObjectView::GetContentVersion() const {
return m_storageOwner->GetContentVersion();
}
Int TextureObjectView::GetSamples() const {
return m_storageOwner->GetSamples();
}
Bool TextureObjectView::HasFixedSampleLocations() const {
return m_storageOwner->HasFixedSampleLocations();
}
TextureUploadTarget TextureObjectView::ToOwnerUploadTarget(TextureUploadTarget viewTarget) const {
const auto& ownerTargets = m_storageOwner->GetUploadTargets();
MOBILEGL_ASSERT(!ownerTargets.empty(), "TextureObjectView: storage owner has no upload target");
if (ownerTargets.size() == 1) {
// The owner keeps every layer in one blob, so there is nothing to choose.
return ownerTargets[0];
}
// The owner is a cube map: six independent blobs, one per face, and the view's layer
// index selects among them. A cube-map view of a cube map maps face to face; any other
// view target addresses layers, which for a cube-map owner ARE its faces.
const Uint faceCount = static_cast<Uint>(ownerTargets.size());
Uint face = m_viewMinLayer;
if (GetTarget() == TextureTarget::TextureCubeMap) {
for (Uint i = 0; i < m_uploadTargets.size(); ++i) {
if (m_uploadTargets[i] == viewTarget) {
face = m_viewMinLayer + i;
break;
}
}
}
return ownerTargets[std::min(face, faceCount - 1)];
}
IntVec3 TextureObjectView::ToViewLevelSize(const IntVec3& ownerLevelSize) const {
IntVec3 size = ownerLevelSize;
// Collapse whichever axis the OWNER stored its layers in down to a single slice, then
// impose this view's own layer count on whichever axis THIS target stores layers in.
// Doing it in that order makes every legal target pair fall out: 2D_ARRAY->2D clears z,
// 2D->2D_ARRAY sets it, 2D_ARRAY->2D_ARRAY replaces it, and 3D->3D touches neither
// (LayerAxis::None on both sides), which is what keeps a 3D view's full depth intact.
switch (LayerAxisOf(m_storageOwner->GetTarget())) {
case LayerAxis::Y:
size.y() = 1;
break;
case LayerAxis::Z:
size.z() = 1;
break;
case LayerAxis::None:
break;
}
switch (LayerAxisOf(GetTarget())) {
case LayerAxis::Y:
size.y() = static_cast<Int>(m_viewNumLayers);
break;
case LayerAxis::Z:
size.z() = static_cast<Int>(m_viewNumLayers);
break;
case LayerAxis::None:
break;
}
return size;
}
Uint TextureObjectView::GetMipmapLevelCount() const {
if (m_ownerMipmap == nullptr) return 0;
const Uint ownerLevels = m_ownerMipmap->GetMipmapLevelCount();
if (m_viewMinLevel >= ownerLevels) return 0;
return std::min(m_viewNumLevels, ownerLevels - m_viewMinLevel);
}
const IntVec3 TextureObjectView::GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const {
if (m_ownerMipmap == nullptr) return {0, 0, 0};
return ToViewLevelSize(
m_ownerMipmap->GetMipmapTexelSize(ToOwnerUploadTarget(target), ToOwnerLevel(mipmapLevel)));
}
const SizeT TextureObjectView::GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const {
if (m_ownerMipmap == nullptr) return 0;
const TextureUploadTarget ownerTarget = ToOwnerUploadTarget(target);
const Uint ownerLevel = ToOwnerLevel(mipmapLevel);
const IntVec3 ownerSize = m_ownerMipmap->GetMipmapTexelSize(ownerTarget, ownerLevel);
const SizeT ownerBytes = m_ownerMipmap->GetMipmapByteSize(ownerTarget, ownerLevel);
const SizeT ownerTexels = static_cast<SizeT>(std::max(ownerSize.x(), 0)) *
static_cast<SizeT>(std::max(ownerSize.y(), 0)) *
static_cast<SizeT>(std::max(ownerSize.z(), 1));
if (ownerTexels == 0 || ownerBytes == 0) return 0;
// Scaled rather than recomputed from a format table: the view's internalformat is
// required to be in the same view class as the owner's (GL 4.6 core table 8.21), i.e. to
// have the identical texel size, so bytes-per-texel is shared by construction and the
// only difference is how many texels the view addresses.
const IntVec3 viewSize = ToViewLevelSize(ownerSize);
const SizeT viewTexels = static_cast<SizeT>(std::max(viewSize.x(), 0)) *
static_cast<SizeT>(std::max(viewSize.y(), 0)) *
static_cast<SizeT>(std::max(viewSize.z(), 1));
return (ownerBytes / ownerTexels) * viewTexels;
}
void TextureObjectView::AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) {
// Unreachable through the API: a view is immutable from birth (GL 4.6 core 8.18 sets its
// TEXTURE_IMMUTABLE_FORMAT), and every entry point that would allocate is gated on
// ValidateTextureMutable. Forwarded rather than asserted so an internal caller that
// re-specifies the storage still hits the one real allocation.
if (m_ownerMipmap == nullptr) return;
m_ownerMipmap->AllocateStorage(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel), input);
}
void TextureObjectView::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
if (m_ownerMipmap == nullptr) return;
m_ownerMipmap->TruncateMipmapLevels(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(levelCount));
}
void TextureObjectView::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) {
if (m_ownerMipmap == nullptr) return;
m_ownerMipmap->UpdateMipmapSubData(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel), input);
}
void* TextureObjectView::MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) {
if (m_ownerMipmap == nullptr) return nullptr;
return m_ownerMipmap->MapMipmapData(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel));
}
void TextureObjectView::MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) {
if (m_ownerMipmap == nullptr) return;
m_ownerMipmap->MarkStorageDirty(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel), dirty);
}
Bool TextureObjectView::IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const {
if (m_ownerMipmap == nullptr) return false;
return m_ownerMipmap->IsStorageDirty(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel));
}
void TextureObjectView::MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
IntVec3 size) {
if (m_ownerMipmap == nullptr) return;
m_ownerMipmap->MarkStorageDirtyRegion(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel), offset,
size);
}
MipmapDirtyRegion TextureObjectView::GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
if (m_ownerMipmap == nullptr) return {};
return m_ownerMipmap->GetStorageDirtyRegion(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel));
}
void TextureObjectView::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat, const void* data, SizeT size) {
if (m_ownerMipmap == nullptr) return;
m_ownerMipmap->SetMipmapCompressedImage(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel),
internalFormat, data, size);
}
GLenum TextureObjectView::GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const {
if (m_ownerMipmap == nullptr) return GL_NONE;
return m_ownerMipmap->GetMipmapCompressedFormat(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel));
}
SizeT TextureObjectView::GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const {
if (m_ownerMipmap == nullptr) return 0;
return m_ownerMipmap->GetMipmapCompressedByteSize(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel));
}
const void* TextureObjectView::MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const {
if (m_ownerMipmap == nullptr) return nullptr;
return m_ownerMipmap->MapMipmapCompressedImage(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel));
}
void TextureObjectView::SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat) {
if (m_ownerMipmap == nullptr) return;
m_ownerMipmap->SetMipmapRequestedCompressedFormat(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel),
internalFormat);
}
GLenum TextureObjectView::GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
if (m_ownerMipmap == nullptr) return GL_NONE;
return m_ownerMipmap->GetMipmapRequestedCompressedFormat(ToOwnerUploadTarget(uploadTarget),
ToOwnerLevel(mipmapLevel));
}
IntVec3 TextureObjectView::GetBaseSize() const {
if (GetMipmapLevelCount() == 0) return {0, 0, 0};
return GetMipmapTexelSize(m_uploadTargets[0], 0);
}
Bool TextureObjectView::IsComplete() const {
if (!TextureObjectBase::IsComplete()) return false;
// The view's own level set is what sampling walks, and it can be shorter than the
// owner's. Everything below it - that the owner has real storage at all - is the owner's
// answer, because these texels are its texels.
if (GetMipmapLevelCount() == 0) return false;
return m_storageOwner->IsComplete();
}
Uint TextureObjectView::GetIndexOfTextureUploadTarget(TextureUploadTarget target) const {
for (Uint i = 0; i < static_cast<Uint>(m_uploadTargets.size()); ++i) {
if (m_uploadTargets[i] == target) return i;
}
return 0;
}
} // namespace MobileGL::MG_State::GLState
@@ -0,0 +1,108 @@
// MobileGL - MobileGL/MG_State/GLState/TextureState/TextureObjectView.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 "TextureObject.h"
namespace MobileGL::MG_State::GLState {
// A texture created by glTextureView (ARB_texture_view / GL 4.6 core 8.18): a texture object
// in every respect - own name, own target, own internal format, own sampler and own
// per-texture parameters - whose TEXELS are somebody else's. That last part is the whole
// point of the extension, and the reason this cannot be a plain TextureObject2D with a copy:
// the application samples the view and the original SIMULTANEOUSLY, reading different aspects
// or different formats out of one storage, and writes through either name must be visible
// through the other.
//
// So this class owns no MipmapStorage at all. Every storage question is answered by
// m_storageOwner, shifted by the view's level offset; every parameter question is answered
// by this object's own TextureObjectBase state. The owner is held by SharedPtr, which is
// exactly GL's name-deletion rule (5.1.2): glDeleteTextures on the original frees the NAME
// immediately, but the storage - and every backend resource keyed on the owner object -
// lives until the last view referencing it is gone too.
//
// m_storageOwner is guaranteed never to be a view itself. glTextureView composes a
// view-of-a-view onto the root at creation time, which is what the spec's additive
// "<minlevel> plus the value of TEXTURE_VIEW_MIN_LEVEL from the original texture" rule
// means; one hop therefore always reaches real storage and no recursion is possible.
//
// LAYER offsets are deliberately NOT applied here. The TextureObjectMipmap interface
// addresses storage as (upload target, level) and a layer lives INSIDE a level's blob, so a
// layer offset is not expressible at this boundary. The entry points that move texels for a
// view (glTexSubImage*, glGetTexImage) therefore redirect to the owner themselves and add
// GetViewMinLayer() to the z coordinate there, where it can be said. What this class does
// apply is the view's layer COUNT, because the level extents it reports are what both
// backends size their images and views from.
class TextureObjectView : public TextureObjectMipmap {
public:
TextureObjectView(Uint externalIndex, TextureTarget target, SharedPtr<ITextureObject> storageOwner,
Uint minLevel, Uint numLevels, Uint minLayer, Uint numLayers);
const SharedPtr<ITextureObject>& GetViewStorageOwner() const override { return m_storageOwner; }
const Vector<TextureUploadTarget>& GetUploadTargets() const override { return m_uploadTargets; }
// GL 4.6 core 8.18: "TEXTURE_IMMUTABLE_LEVELS is set to the value of
// TEXTURE_IMMUTABLE_LEVELS from the original texture" - NOT to <numlevels>. Kept as a
// forward rather than in m_immutableLevels so that the base class's level-range clamp
// keeps using the view's own level count, which is what TEXTURE_BASE_LEVEL /
// TEXTURE_MAX_LEVEL on a view are relative to.
Uint GetImmutableLevels() const override;
// Both follow the storage, not this object: a backend that memoised on the view's own
// counter would keep serving stale texels after the owner was written through its own
// name (KHR-GL43.texture_view.coherency is exactly this test).
Uint64 GetContentVersion() const override;
Int GetSamples() const override;
Bool HasFixedSampleLocations() const override;
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;
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:
Uint GetIndexOfTextureUploadTarget(TextureUploadTarget target) const override;
private:
// The owner-side upload target a given view-side one addresses. Only GL_TEXTURE_CUBE_MAP
// stores its six faces as six separate blobs (MipmapUploadTargetArray<6>); every other
// target - arrays and cube-map arrays included - keeps all its layers in one blob, so
// the mapping is "the owner's only target" unless one of the two sides is a cube map.
TextureUploadTarget ToOwnerUploadTarget(TextureUploadTarget viewTarget) const;
Uint ToOwnerLevel(Uint viewLevel) const { return m_viewMinLevel + viewLevel; }
// The owner's level extent rewritten into this view's shape: the owner's layer axis is
// collapsed to one slice and the view's own layer count is imposed on the view's layer
// axis. A GL 1D array carries its layer count in the state-side HEIGHT while every other
// layered target carries it in z, so the axis is target-dependent.
IntVec3 ToViewLevelSize(const IntVec3& ownerLevelSize) const;
SharedPtr<ITextureObject> m_storageOwner;
// Non-owning; m_storageOwner keeps it alive and is never a view, so this is set once in
// the constructor and is null only for the (rejected at creation) buffer-texture case.
TextureObjectMipmap* m_ownerMipmap = nullptr;
Vector<TextureUploadTarget> m_uploadTargets;
};
} // namespace MobileGL::MG_State::GLState
@@ -18,6 +18,7 @@
#include "TextureObject2DCube.h"
#include "TextureObjectBuffer.h"
#include "TextureObjectStubs.h"
#include "TextureObjectView.h"
namespace MobileGL::MG_State::GLState {
static std::atomic<Uint64> s_nextTextureStateContextId = 1;
@@ -104,6 +105,16 @@ namespace MobileGL::MG_State::GLState {
return textureObject;
}
const SharedPtr<ITextureObject>& TextureState::CreateTextureViewObject(
Uint index, TextureTarget target, const SharedPtr<ITextureObject>& storageOwner, Uint minLevel,
Uint numLevels, Uint minLayer, Uint numLayers) {
MOBILEGL_ASSERT(storageOwner != nullptr, "CreateTextureViewObject: storage owner is null");
auto& textureObject = m_textureObjects[index];
textureObject = MakeShared<TextureObjectView>(index, target, storageOwner, minLevel, numLevels, minLayer,
numLayers);
return textureObject;
}
void TextureState::MarkTextureObjectForDeletion(Uint index, Bool keepUnboundReservation) {
if (m_indexGenerator.IsValid(index)) {
auto it = m_textureObjects.find(index);
@@ -48,6 +48,13 @@ namespace MobileGL::MG_State::GLState {
TextureState();
void GenerateNames(Uint number, Vector<Uint>& textures);
const SharedPtr<ITextureObject>& CreateTextureObject(Uint index, TextureTarget target);
// glTextureView (GL 4.6 core 8.18). `storageOwner` must already be a texture with
// immutable storage and must NOT itself be a view - the caller composes a view-of-a-view
// onto the root first, and passes the composed (root-relative) level/layer range here.
const SharedPtr<ITextureObject>& CreateTextureViewObject(Uint index, TextureTarget target,
const SharedPtr<ITextureObject>& storageOwner,
Uint minLevel, Uint numLevels, Uint minLayer,
Uint numLayers);
const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
// The context's default texture object (name 0) for `target`. GL 3.3 core 3.8: texture
// zero names a real, per-target texture object shared by every texture unit; binding 0
@@ -1087,11 +1087,11 @@ TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSu
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
};
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false);
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false);
EXPECT_FALSE(contains(without, MobileGL::E_GL_EXT_texture_filter_anisotropic));
EXPECT_FALSE(contains(without, MobileGL::E_GL_ARB_texture_filter_anisotropic));
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true, false, false);
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true, false, false, false);
EXPECT_TRUE(contains(with, MobileGL::E_GL_EXT_texture_filter_anisotropic));
EXPECT_TRUE(contains(with, MobileGL::E_GL_ARB_texture_filter_anisotropic));
@@ -1113,17 +1113,17 @@ TEST(IndirectDrawAdvertisement, MatchesEachBackendsUsableCommandSemantics) {
};
const auto esWithoutIndirect =
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false);
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false);
EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_draw_indirect));
EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_base_instance));
const auto esWithoutBaseInstance =
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, false);
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, false, false);
EXPECT_TRUE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_draw_indirect));
EXPECT_FALSE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_base_instance));
const auto esWithBoth =
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, true);
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, true, false);
EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_draw_indirect));
EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_base_instance));
@@ -516,14 +516,14 @@ TEST_F(ParallelShaderCompileTest, MaxShaderCompilerThreadsIgnoresTheCurrentBudge
TEST_F(ParallelShaderCompileTest, BothBackendsAdvertiseTheExtensionIffAsyncIsEnabled) {
{
const AsyncModeScope async(true);
EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false),
EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false),
E_GL_KHR_parallel_shader_compile));
EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
E_GL_KHR_parallel_shader_compile));
}
{
const AsyncModeScope async(false);
EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false),
EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false, false),
E_GL_KHR_parallel_shader_compile))
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
+18
View File
@@ -19,6 +19,24 @@ target_link_libraries(
include(GoogleTest)
gtest_discover_tests(TextureTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
add_executable(
TextureViewTest
TextureViewTest.cpp
)
target_include_directories(TextureViewTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
TextureViewTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
gtest_discover_tests(TextureViewTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
add_executable(
VkClearManagerTest
VkClearManagerTest.cpp
@@ -0,0 +1,477 @@
// MobileGL - MobileGL/MG_Test/Texture/TextureViewTest.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
//
// The frontend half of glTextureView (ARB_texture_view / GL 4.6 core 8.18): its error surface and
// the state it derives. The cases below are the conformance suite's own list
// (KHR-GL43.texture_view.errors, a..s) plus the composition rules 8.18 spells out, which
// KHR-GL43.texture_view.gettexparameter checks.
//
// No backend is involved: glTextureView creates a frontend texture object whose storage is
// another object's, and everything asserted here is decided before any driver sees it. The one
// backend fact that matters is whether the active backend can share storage between two texture
// names at all - MobileGL keys that on the advertised GL_ARB_texture_view string, so the fixture
// installs a backend that advertises it (and one test removes it again, to pin the refusal).
#include <gtest/gtest.h>
#include <algorithm>
#include "Includes.h"
#include "Init.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/TextureState/TextureObject.h>
#include <MG_Util/GLExtensions.h>
using namespace MobileGL;
namespace {
// A backend that claims exactly one thing: whether it can back a texture view. Everything
// else about it is inert, because nothing else in this file reaches a backend.
class TextureViewCapabilityBackend final : public MG_Backend::BackendObject {
public:
explicit TextureViewCapabilityBackend(Bool advertiseTextureView) {
m_info.RendererName = "TextureViewTest";
if (advertiseTextureView) {
m_info.RendererGLInfo.Extensions.push_back(E_GL_ARB_texture_view);
}
}
void Initialize() override {}
Bool InitCapabilities() override { return true; }
Bool InitWindowSurface() override { return true; }
const RendererInfo& GetRendererInfo() const override { return m_info; }
String GetBackendAPIVersionString() const override { return {}; }
const MG_Backend::GlobalBackendFunctionsTable& GetBackendFunctions() const override {
static MG_Backend::GlobalBackendFunctionsTable table = {};
return table;
}
const MG_Backend::DynamicBackendParameters& GetDynamicParameters() const override {
static MG_Backend::DynamicBackendParameters params = {};
return params;
}
BackendType GetBackendType() const override { return BackendType::Unknown; }
private:
RendererInfo m_info;
};
class ScopedBackendOverride {
public:
explicit ScopedBackendOverride(Bool advertiseTextureView)
: m_previous(Move(MG_Backend::pActiveBackendObject)) {
MG_Backend::pActiveBackendObject = MakeUnique<TextureViewCapabilityBackend>(advertiseTextureView);
}
~ScopedBackendOverride() { MG_Backend::pActiveBackendObject = Move(m_previous); }
private:
UniquePtr<MG_Backend::BackendObject> m_previous;
};
class TextureViewTest : public ::testing::Test {
protected:
// GL error flags are sticky per error code and the context outlives an individual test in
// this binary, so anything an earlier test left pending would be handed to the next
// GetError() call - which silently turns error-code assertions into reads of someone
// else's error. Bounded because there is one flag per code.
static void DrainPendingGlErrors() {
for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) {
}
}
// The call under test must raise exactly the expected error and nothing more: a second
// pending error means one entry point queued several, which GetError() would hand out at
// an unrelated call site later on.
static void ExpectSingleGlError(GLenum expected) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), expected);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the call recorded more than one error";
}
void SetUp() override {
MobileGL::Initialize();
DrainPendingGlErrors();
m_backend = MakeUnique<ScopedBackendOverride>(true);
}
void TearDown() override {
m_backend.reset();
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
}
static GLuint GenTexture() {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
return texture;
}
// A bound, immutable GL_TEXTURE_2D. `levels` levels of `size` x `size` RGBA8 unless a
// caller wants otherwise.
static GLuint MakeImmutable2D(GLsizei levels = 2, GLsizei width = 16, GLsizei height = 16,
GLenum internalFormat = GL_RGBA8) {
const GLuint texture = GenTexture();
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_2D, levels, internalFormat, width, height);
return texture;
}
static GLuint MakeImmutable2DArray(GLsizei levels = 1, GLsizei size = 16, GLsizei layers = 6) {
const GLuint texture = GenTexture();
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, texture);
MG_Impl::GLImpl::TexStorage3D(GL_TEXTURE_2D_ARRAY, levels, GL_RGBA8, size, size, layers);
return texture;
}
static GLint GetViewParameter(GLuint texture, GLenum target, GLenum pname) {
GLint value = -1;
MG_Impl::GLImpl::BindTexture(target, texture);
MG_Impl::GLImpl::GetTexParameteriv(target, pname, &value);
return value;
}
UniquePtr<ScopedBackendOverride> m_backend;
};
// ============================ the state TexStorage* seeds ============================
// GL 4.6 core 8.19 leaves an immutable texture describing itself as a full-extent view of its
// own storage. That is not cosmetic: glTextureView COMPOSES onto these values, so if they
// stayed at the mutable default of 0 every view would clamp to zero levels.
TEST_F(TextureViewTest, MutableTextureReportsNoViewState) {
const GLuint texture = GenTexture();
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
DrainPendingGlErrors();
EXPECT_EQ(GetViewParameter(texture, GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL), 0);
EXPECT_EQ(GetViewParameter(texture, GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS), 0);
EXPECT_EQ(GetViewParameter(texture, GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LAYER), 0);
EXPECT_EQ(GetViewParameter(texture, GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LAYERS), 0);
}
TEST_F(TextureViewTest, TexStorageSeedsTheFullExtentAsViewState) {
const GLuint texture = MakeImmutable2D(3, 16, 16);
DrainPendingGlErrors();
EXPECT_EQ(GetViewParameter(texture, GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL), 0);
EXPECT_EQ(GetViewParameter(texture, GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS), 3);
EXPECT_EQ(GetViewParameter(texture, GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LAYER), 0);
EXPECT_EQ(GetViewParameter(texture, GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LAYERS), 1);
}
TEST_F(TextureViewTest, TexStorageOnAnArrayReportsItsLayerCount) {
const GLuint texture = MakeImmutable2DArray(1, 16, 6);
DrainPendingGlErrors();
EXPECT_EQ(GetViewParameter(texture, GL_TEXTURE_2D_ARRAY, GL_TEXTURE_VIEW_NUM_LAYERS), 6);
}
// ============================ the derived view state ============================
TEST_F(TextureViewTest, ViewDerivesItsRangeAndInheritsImmutableLevels) {
const GLuint storage = MakeImmutable2D(3, 16, 16);
const GLuint view = GenTexture();
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 1, 2, 0, 1);
ExpectSingleGlError(GL_NO_ERROR);
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL), 1);
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS), 2);
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LAYER), 0);
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LAYERS), 1);
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_FORMAT), GL_TRUE);
// 8.18: "TEXTURE_IMMUTABLE_LEVELS is set to the value of TEXTURE_IMMUTABLE_LEVELS from
// the ORIGINAL texture" - not to <numlevels>.
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_LEVELS), 3);
}
TEST_F(TextureViewTest, ViewClampsItsLevelCountToWhatRemains) {
const GLuint storage = MakeImmutable2D(3, 16, 16);
const GLuint view = GenTexture();
// 8.18: NUM_LEVELS is "the lesser of <numlevels> and TEXTURE_VIEW_NUM_LEVELS from the
// original minus <minlevel>", so an over-large request clamps rather than erroring.
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 2, 10, 0, 1);
ExpectSingleGlError(GL_NO_ERROR);
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL), 2);
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS), 1);
}
TEST_F(TextureViewTest, ViewOfAViewComposesOntoTheOriginalRatherThanRestarting) {
const GLuint storage = MakeImmutable2D(4, 32, 32);
const GLuint first = GenTexture();
MG_Impl::GLImpl::TextureView(first, GL_TEXTURE_2D, storage, GL_RGBA8, 1, 3, 0, 1);
ExpectSingleGlError(GL_NO_ERROR);
const GLuint second = GenTexture();
MG_Impl::GLImpl::TextureView(second, GL_TEXTURE_2D, first, GL_RGBA8, 2, 1, 0, 1);
ExpectSingleGlError(GL_NO_ERROR);
// 8.18: MIN_LEVEL is "<minlevel> plus TEXTURE_VIEW_MIN_LEVEL from the original".
EXPECT_EQ(GetViewParameter(second, GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL), 3);
EXPECT_EQ(GetViewParameter(second, GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS), 1);
EXPECT_EQ(GetViewParameter(second, GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_LEVELS), 4);
// And the composed view points at the ROOT, not at the intermediate one - which is what
// lets both backends resolve a view's storage in a single hop.
const auto& secondObject = MG_State::pGLContext->GetTextureObject(second);
const auto& storageObject = MG_State::pGLContext->GetTextureObject(storage);
ASSERT_TRUE(secondObject != nullptr);
EXPECT_TRUE(secondObject->IsTextureView());
EXPECT_EQ(secondObject->GetViewStorageOwner().get(), storageObject.get());
}
TEST_F(TextureViewTest, ViewCarriesItsOwnParametersAndFormat) {
const GLuint storage = MakeImmutable2D(1, 16, 16, GL_DEPTH24_STENCIL8);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
const GLuint view = GenTexture();
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_DEPTH24_STENCIL8, 0, 1, 0, 1);
ExpectSingleGlError(GL_NO_ERROR);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, view);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT);
DrainPendingGlErrors();
// This divergence IS the feature (it is what Better Clouds uses glTextureView for): one
// storage read through two names with two different aspects in the same shading pass.
EXPECT_EQ(GetViewParameter(storage, GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE), GL_STENCIL_INDEX);
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE), GL_DEPTH_COMPONENT);
}
TEST_F(TextureViewTest, DeletingTheOriginalLeavesTheViewsStorageAlive) {
const GLuint storage = MakeImmutable2D(1, 16, 16);
const GLuint view = GenTexture();
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
ExpectSingleGlError(GL_NO_ERROR);
const auto storageObject = MG_State::pGLContext->GetTextureObject(storage);
ASSERT_TRUE(storageObject != nullptr);
MG_Impl::GLImpl::DeleteTextures(1, &storage);
DrainPendingGlErrors();
// GL 4.6 core 5.1.2: the NAME is gone, the object is not - something still refers to it.
EXPECT_EQ(MG_Impl::GLImpl::IsTexture(storage), static_cast<GLboolean>(GL_FALSE));
const auto& viewObject = MG_State::pGLContext->GetTextureObject(view);
ASSERT_TRUE(viewObject != nullptr);
EXPECT_EQ(viewObject->GetViewStorageOwner().get(), storageObject.get());
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_FORMAT), GL_TRUE);
}
// ============================ the error surface ============================
// KHR-GL43.texture_view.errors walks these in this order; the letters are its own.
TEST_F(TextureViewTest, ZeroTextureIsInvalidValue) { // (a)
const GLuint storage = MakeImmutable2D();
DrainPendingGlErrors();
MG_Impl::GLImpl::TextureView(0, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
ExpectSingleGlError(GL_INVALID_VALUE);
}
TEST_F(TextureViewTest, TextureThatGenTexturesNeverReturnedIsInvalidOperation) { // (b)
const GLuint storage = MakeImmutable2D();
DrainPendingGlErrors();
MG_Impl::GLImpl::TextureView(0xFFFFFFFFu, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
TEST_F(TextureViewTest, AlreadyBoundTextureIsInvalidOperation) { // (c)
const GLuint storage = MakeImmutable2D();
const GLuint alreadyBound = GenTexture();
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, alreadyBound);
DrainPendingGlErrors();
MG_Impl::GLImpl::TextureView(alreadyBound, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
TEST_F(TextureViewTest, OrigTextureThatIsNotATextureObjectIsInvalidValue) { // (d)
const GLuint view = GenTexture();
DrainPendingGlErrors();
// Note the code differs from (b): INVALID_VALUE here, INVALID_OPERATION there.
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, 0xFFFFFFFFu, GL_RGBA8, 0, 1, 0, 1);
ExpectSingleGlError(GL_INVALID_VALUE);
}
TEST_F(TextureViewTest, MutableOrigTextureIsInvalidOperation) { // (e)
const GLuint mutableTexture = GenTexture();
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, mutableTexture);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
const GLuint view = GenTexture();
DrainPendingGlErrors();
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, mutableTexture, GL_RGBA8, 0, 1, 0, 1);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
TEST_F(TextureViewTest, IncompatibleTargetPairIsInvalidOperation) { // (f)
const GLuint storage = MakeImmutable2D();
const GLuint view = GenTexture();
DrainPendingGlErrors();
// Table 8.20 admits 2D -> 2D and 2D -> 2D_ARRAY, and nothing else.
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_3D, storage, GL_RGBA8, 0, 1, 0, 1);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
TEST_F(TextureViewTest, LegalTargetPairsAreAccepted) { // (f), the other way round
const GLuint storage = MakeImmutable2D(1, 16, 16);
const GLuint sameTarget = GenTexture();
MG_Impl::GLImpl::TextureView(sameTarget, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
ExpectSingleGlError(GL_NO_ERROR);
const GLuint arrayView = GenTexture();
MG_Impl::GLImpl::TextureView(arrayView, GL_TEXTURE_2D_ARRAY, storage, GL_RGBA8, 0, 1, 0, 1);
ExpectSingleGlError(GL_NO_ERROR);
}
TEST_F(TextureViewTest, FormatFromAnotherViewClassIsInvalidOperation) { // (g)
const GLuint storage = MakeImmutable2D(1, 16, 16, GL_RGBA8);
const GLuint view = GenTexture();
DrainPendingGlErrors();
// GL_RGBA8 is VIEW_CLASS_32_BITS; GL_R8 is VIEW_CLASS_8_BITS.
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_R8, 0, 1, 0, 1);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
TEST_F(TextureViewTest, FormatFromTheSameViewClassIsAccepted) { // (g), the other way round
const GLuint storage = MakeImmutable2D(1, 16, 16, GL_RGBA8);
const GLuint view = GenTexture();
// Both VIEW_CLASS_32_BITS.
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_R32UI, 0, 1, 0, 1);
ExpectSingleGlError(GL_NO_ERROR);
GLint internalFormat = 0;
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, view);
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &internalFormat);
DrainPendingGlErrors();
EXPECT_EQ(internalFormat, GL_R32UI) << "the view must take the format it was asked for, not its parent's";
}
TEST_F(TextureViewTest, ClasslessFormatMayOnlyBeViewedAsItself) { // (h)
// Depth, stencil and depth/stencil formats have NO entry in table 8.21, which the spec
// turns into a stricter rule than "same class": the view's format must be IDENTICAL.
// This is the rule the Better Clouds D24S8 view depends on being permissive enough.
const GLuint storage = MakeImmutable2D(1, 16, 16, GL_DEPTH24_STENCIL8);
const GLuint sameFormat = GenTexture();
MG_Impl::GLImpl::TextureView(sameFormat, GL_TEXTURE_2D, storage, GL_DEPTH24_STENCIL8, 0, 1, 0, 1);
ExpectSingleGlError(GL_NO_ERROR);
const GLuint otherFormat = GenTexture();
MG_Impl::GLImpl::TextureView(otherFormat, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
TEST_F(TextureViewTest, MinLevelPastTheLastLevelIsInvalidValue) { // (i)
const GLuint storage = MakeImmutable2D(2, 16, 16);
const GLuint view = GenTexture();
DrainPendingGlErrors();
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 2, 1, 0, 1);
ExpectSingleGlError(GL_INVALID_VALUE);
}
TEST_F(TextureViewTest, MinLayerPastTheLastLayerIsInvalidValue) { // (j)
const GLuint storage = MakeImmutable2DArray(1, 16, 4);
const GLuint view = GenTexture();
DrainPendingGlErrors();
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 4, 1);
ExpectSingleGlError(GL_INVALID_VALUE);
}
TEST_F(TextureViewTest, CubeMapViewDemandsExactlySixLayers) { // (k)
const GLuint storage = MakeImmutable2DArray(1, 16, 6);
const GLuint view = GenTexture();
DrainPendingGlErrors();
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_CUBE_MAP, storage, GL_RGBA8, 0, 1, 0, 5);
ExpectSingleGlError(GL_INVALID_VALUE);
}
TEST_F(TextureViewTest, CubeMapArrayViewDemandsAMultipleOfSixLayers) { // (l)
const GLuint storage = MakeImmutable2DArray(1, 16, 12);
const GLuint view = GenTexture();
DrainPendingGlErrors();
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_CUBE_MAP_ARRAY, storage, GL_RGBA8, 0, 1, 0, 1);
ExpectSingleGlError(GL_INVALID_VALUE);
}
TEST_F(TextureViewTest, SingleLayerTargetsRejectMoreThanOneLayer) { // (m).. (q)
const GLuint storage = MakeImmutable2DArray(1, 16, 4);
DrainPendingGlErrors();
for (const GLenum target : {GL_TEXTURE_2D}) {
const GLuint view = GenTexture();
MG_Impl::GLImpl::TextureView(view, target, storage, GL_RGBA8, 0, 1, 0, 2);
ExpectSingleGlError(GL_INVALID_VALUE);
}
// The 1D and 3D forms take the same rule; check one of them from a legal parent so the
// target-pair rule cannot be what is rejecting it.
const GLuint texture3D = GenTexture();
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, texture3D);
MG_Impl::GLImpl::TexStorage3D(GL_TEXTURE_3D, 1, GL_RGBA8, 8, 8, 8);
DrainPendingGlErrors();
const GLuint view3D = GenTexture();
MG_Impl::GLImpl::TextureView(view3D, GL_TEXTURE_3D, texture3D, GL_RGBA8, 0, 1, 0, 2);
ExpectSingleGlError(GL_INVALID_VALUE);
}
TEST_F(TextureViewTest, CubeMapViewDemandsSquareLevels) { // (r)
const GLuint storage = GenTexture();
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, storage);
MG_Impl::GLImpl::TexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 32, 33, 6);
DrainPendingGlErrors();
const GLuint view = GenTexture();
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_CUBE_MAP, storage, GL_RGBA8, 0, 1, 0, 6);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
TEST_F(TextureViewTest, BufferTextureHasNoLegalViewTarget) {
// Table 8.20 lists nothing for GL_TEXTURE_BUFFER: its storage is a buffer object, so
// there is no image to view.
const GLuint storage = MakeImmutable2D();
const GLuint view = GenTexture();
DrainPendingGlErrors();
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_BUFFER, storage, GL_RGBA8, 0, 1, 0, 1);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
TEST_F(TextureViewTest, NonTextureTargetIsInvalidEnum) {
const GLuint storage = MakeImmutable2D();
const GLuint view = GenTexture();
DrainPendingGlErrors();
MG_Impl::GLImpl::TextureView(view, GL_ARRAY_BUFFER, storage, GL_RGBA8, 0, 1, 0, 1);
ExpectSingleGlError(GL_INVALID_ENUM);
}
TEST_F(TextureViewTest, AFailedCallLeavesTheNameUninstantiated) {
// The spec's "texture must not already have a target" rule means a rejected call has to
// leave the name exactly as GenTextures left it, or a retry would then fail with (c).
const GLuint storage = MakeImmutable2D();
const GLuint view = GenTexture();
DrainPendingGlErrors();
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_3D, storage, GL_RGBA8, 0, 1, 0, 1);
ExpectSingleGlError(GL_INVALID_OPERATION);
EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(view));
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
ExpectSingleGlError(GL_NO_ERROR);
EXPECT_TRUE(MG_State::pGLContext->ValidateTextureObject(view));
}
// ============================ the no-support contract ============================
TEST_F(TextureViewTest, BackendWithoutTextureViewSupportRaisesInvalidOperation) {
const GLuint storage = MakeImmutable2D();
DrainPendingGlErrors();
// A backend that cannot give two texture names one storage - ES without
// EXT/OES_texture_view - withholds GL_ARB_texture_view, and glTextureView must then FAIL
// rather than quietly produce a view with no storage. A silent no-op is the one
// unacceptable answer: it is indistinguishable from success at the call site, and the
// application renders from a texture that aliases nothing.
const ScopedBackendOverride noTextureView(false);
const GLuint view = GenTexture();
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
ExpectSingleGlError(GL_INVALID_OPERATION);
EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(view));
}
} // namespace
@@ -514,6 +514,10 @@ namespace MobileGL::MG_Util::BackendLoader {
// Optional: absent on an ES 3.2 core driver, and absent on ES 3.1 without the
// matching extension. The tier resolution below picks whichever spelling the
// driver's own support actually comes from.
// Optional by nature: ES never made texture views core, so both spellings are
// absent on plenty of drivers and neither absence is an error.
INIT_GLES_FUNC_OPTIONAL(glTextureViewEXT)
INIT_GLES_FUNC_OPTIONAL(glTextureViewOES)
INIT_GLES_FUNC_OPTIONAL(glTexBufferEXT)
INIT_GLES_FUNC_OPTIONAL(glTexBufferOES)
INIT_GLES_FUNC_OPTIONAL(glTexBufferRangeEXT)
@@ -889,6 +893,11 @@ namespace MobileGL::MG_Util::BackendLoader {
// Resolved into caps.TextureBufferSupport below, once the ES version is also known.
Bool hasExtTextureBuffer = false;
Bool hasOesTextureBuffer = false;
// Resolved into caps.SupportsTextureView below. Two spellings of one extension; the
// entry points differ only in suffix, so unlike the buffer-texture tier there is nothing
// downstream that needs to know WHICH one answered.
Bool hasExtTextureView = false;
Bool hasOesTextureView = false;
// Combined with the three entry points below; DirectGLES emulates baseInstance when this
// comes out false, so a stub pointer counting as support would silently break the draws.
Bool hasBaseInstanceExtension = false;
@@ -925,6 +934,12 @@ namespace MobileGL::MG_Util::BackendLoader {
std::strcmp(extension, "GL_OES_texture_cube_map_array") == 0) {
caps.SupportsTextureCubeMapArray = true;
}
if (std::strcmp(extension, "GL_EXT_texture_view") == 0) {
hasExtTextureView = true;
}
if (std::strcmp(extension, "GL_OES_texture_view") == 0) {
hasOesTextureView = true;
}
if (std::strcmp(extension, "GL_EXT_texture_buffer") == 0) {
hasExtTextureBuffer = true;
}
@@ -985,6 +1000,18 @@ namespace MobileGL::MG_Util::BackendLoader {
glesFuncs.glDrawArraysInstancedBaseInstanceEXT != nullptr &&
glesFuncs.glDrawElementsInstancedBaseInstanceEXT != nullptr &&
glesFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT != nullptr;
// ES has no core texture views at any version, so this is extension-only by nature.
// Each spelling must bring its OWN entry point: a driver that advertises the OES string
// is not required to export glTextureViewEXT.
caps.SupportsTextureView = (hasExtTextureView && glesFuncs.glTextureViewEXT != nullptr) ||
(hasOesTextureView && glesFuncs.glTextureViewOES != nullptr);
// Escape hatch for the integration suite: the no-extension path is the one MobileGL has
// to refuse honestly rather than emulate, and on a driver that HAS the extension there
// would otherwise be no way to exercise that refusal (see TextureViewScenario).
if (std::getenv("MOBILEGL_DISABLE_TEXTURE_VIEW") != nullptr) {
MGLOG_I("MOBILEGL_DISABLE_TEXTURE_VIEW is set; reporting no EXT/OES_texture_view support");
caps.SupportsTextureView = false;
}
// Core from ES 3.2 on, so an extension string is not required there; below 3.2 the
// extension is, and the pointer still has to have resolved either way.
const Bool esAtLeast32 = caps.GLESVersion.Major > 3 ||
@@ -604,6 +604,12 @@ namespace MobileGL {
// support comes from GL_EXT_texture_buffer or GL_OES_texture_buffer exports the
// suffixed spellings instead, and a strict eglGetProcAddress returns NULL for the core
// one there - so resolving only the core name makes both extension tiers look absent.
GL_FUNC_TYPEDEF(void, glTextureViewEXT, GLuint texture, GLenum target, GLuint origtexture,
GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer,
GLuint numlayers)
GL_FUNC_TYPEDEF(void, glTextureViewOES, GLuint texture, GLenum target, GLuint origtexture,
GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer,
GLuint numlayers)
GL_FUNC_TYPEDEF(void, glTexBufferEXT, GLenum target, GLenum internalformat, GLuint buffer)
GL_FUNC_TYPEDEF(void, glTexBufferOES, GLenum target, GLenum internalformat, GLuint buffer)
GL_FUNC_TYPEDEF(void, glTexBufferRangeEXT, GLenum target, GLenum internalformat, GLuint buffer,
@@ -1023,6 +1029,8 @@ namespace MobileGL {
GL_FUNC_DECL(glGetSamplerParameterIuiv)
GL_FUNC_DECL(glTexBuffer)
GL_FUNC_DECL(glTexBufferRange)
GL_FUNC_DECL(glTextureViewEXT)
GL_FUNC_DECL(glTextureViewOES)
GL_FUNC_DECL(glTexBufferEXT)
GL_FUNC_DECL(glTexBufferOES)
GL_FUNC_DECL(glTexBufferRangeEXT)
@@ -1086,6 +1094,14 @@ namespace MobileGL {
Bool SupportsTextureBorderClamp = false;
// GL_TEXTURE_CUBE_MAP_ARRAY: ES 3.2 core, or EXT/OES_texture_cube_map_array before it.
Bool SupportsTextureCubeMapArray = false;
// GL_EXT_texture_view / GL_OES_texture_view: two ES texture names sharing one
// storage, i.e. the only way DirectGLES can answer glTextureView at all. ES never
// made this core - not even in 3.2 - so unlike every other capability here there is
// no version that implies it, and a driver without it leaves MobileGL with no honest
// implementation (a copy is not a view: writes through one name must be visible
// through the other). Gate on this, never on the entry points: eglGetProcAddress
// hands back live-looking stubs (see AcquireGLESFunctions).
Bool SupportsTextureView = false;
// Which spelling of buffer-texture support the host driver has. Desktop GL makes buffer
// textures core from 3.1 on, so the frontend advertises them unconditionally and an app
// may call glTexBuffer at any time; ES only gained them in 3.2, and before that only
+2 -1
View File
@@ -1183,7 +1183,8 @@ namespace MobileGL::MG_Util::SelfTest {
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectGLES::BuildAdvertisedExtensions(
summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy,
summary.caps.SupportsDrawIndirect,
summary.caps.SupportsDrawIndirect && summary.caps.SupportsBaseInstance));
summary.caps.SupportsDrawIndirect && summary.caps.SupportsBaseInstance,
summary.caps.SupportsTextureView));
}
AppendMobileGLReportedRows(builder, MG_Backend::DirectGLES::GetRendererIdentity(), backendApiVersionString,
advertisedExtensions);