mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Feature, Fix, Test] (GLState, GLImpl, DirectVulkan, DirectGLES): implement glTextureView over shared texture storage
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user