[Feat] (MG_State, MG_Impl/GLImpl): per-target default texture objects (name 0) - binding 0 binds a real per-context default object (the initial binding of every unit/target slot, rebound on delete of a bound texture), so glTexImage*/glTexParameter*/glGetTex* on it work like any texture while glIsTexture(0)/Gen/Delete keep excluding it and TexStorage* rejects it per spec; backends skip image-less defaults as cheaply as the old null slots (DirectGLES per-draw sync/bind loops, DirectVulkan sampler-fallback resolve); also accept the full advertised GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS range in glActiveTexture, allow zero-layer TexImage3DMultisample, and let glTexBuffer(buffer=0) detach - the texture section of GL CTS per-case state reset (gluStateReset) now runs clean

This commit is contained in:
2026-07-16 23:36:43 -04:00
parent efd7b47388
commit 076cd0d19d
11 changed files with 435 additions and 60 deletions
@@ -430,7 +430,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto& unit = MG_State::pGLContext->GetTextureUnitObject(index);
for (const auto& bindingSlot : unit.GetAllBindingSlots()) {
auto& textureObject = bindingSlot.GetBoundObject();
if (textureObject) {
// An image-less default texture (name 0) is the slot's initial / "unbound"
// state; it has nothing to sync, so skip it as cheaply as the old null slot.
if (textureObject && !MG_State::GLState::IsUndefinedDefaultTexture(textureObject.get())) {
SyncTextureObjectToBackend(textureObject);
}
}
@@ -1025,6 +1027,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (const auto& bindingSlot : textureUnit.GetAllBindingSlots()) {
const auto& textureObject = bindingSlot.GetBoundObject();
if (!textureObject) continue;
// A default texture (name 0) that was never given an image is the initial /
// "unbound" state of the slot; skip it exactly like the old null slot so bind-0
// heavy apps pay nothing per draw for the always-populated slots.
if (MG_State::GLState::IsUndefinedDefaultTexture(textureObject.get())) continue;
// Bind texture object
auto target = textureObject->GetTarget();
@@ -360,6 +360,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
outTexture = textureUnit.GetBindingSlot(preferredTarget).GetBoundObject();
// The slot always holds at least the target's default texture (name 0). While that
// default has no image it is unsampleable; report it as "unbound" so callers keep
// taking their fallback paths instead of trying to sync a storage-less texture.
if (MG_State::GLState::IsUndefinedDefaultTexture(outTexture.get())) {
outTexture.reset();
}
return true;
}
@@ -380,7 +386,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
// GetBoundObject() returns the SharedPtr by const ref; .get() reads the pointer without
// touching the refcount (no atomic inc/dec per binding per draw).
return textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
MG_State::GLState::ITextureObject* texture =
textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
// The slot always holds at least the target's default texture (name 0). While that
// default has no image it is unsampleable; report it as "unbound" so the caller
// substitutes its fallback texture exactly like it did for the old null slot.
if (MG_State::GLState::IsUndefinedDefaultTexture(texture)) {
return nullptr;
}
return texture;
}
Bool UniformManager::ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
+34 -14
View File
@@ -422,13 +422,10 @@ namespace MobileGL::MG_Impl::GLImpl {
"2D multisample textures must use depth 1."));
return false;
}
if (textureTarget == TextureTarget::Texture2DMultisampleArray && depth == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"2D multisample array textures must have at least one layer."));
return false;
}
// Zero layers is NOT an error for multisample arrays: GL 4.5 8.8 only raises
// INVALID_VALUE for negative dimensions, and GL CTS's per-case state reset
// (gluStateReset) clears the default GL_TEXTURE_2D_MULTISAMPLE_ARRAY texture with
// glTexImage3DMultisample(..., depth = 0) after every case.
const Int maxSamples = GetMaxSupportedTextureSamples(textureInternalFormat);
if (samples > maxSamples) {
@@ -1718,8 +1715,11 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return;
// TODO: make sure `internalformat` is in one of supported format for TexBuffer
// GL 3.3 core 3.8.5: buffer zero detaches any buffer from the buffer texture - only a
// nonzero name that is not an existing buffer object is an error. This is reachable on
// the default buffer texture now that binding texture 0 binds a real object.
auto& bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
if (!bufferObject) {
if (buffer != 0 && !bufferObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
@@ -2578,11 +2578,13 @@ namespace MobileGL::MG_Impl::GLImpl {
// ===================== Error Checking ==============================
if (!TextureImpl::ValidateTextureTarget(textureTarget)) return;
// Name 0 unbinds the current target from the active texture unit.
// GL 3.3 core 3.8: name 0 is the target's default texture object - a real texture that
// glTexImage*/glTexParameter*/glGetTex* must operate on - not "nothing bound". Binding it
// restores the unit/target slot to its initial state.
if (texture == 0) {
auto& currentUnit = MG_State::pGLContext->GetTextureUnitObject(activeUnit);
auto& bindingSlot = currentUnit.GetBindingSlot(textureTarget);
bindingSlot.Bind(nullptr);
bindingSlot.Bind(MG_State::pGLContext->GetDefaultTextureObject(textureTarget));
MG_State::pGLContext->NoteTextureUnitTouched(activeUnit);
return;
}
@@ -2623,14 +2625,27 @@ namespace MobileGL::MG_Impl::GLImpl {
void ActiveTexture_State(GLenum texture) {
// ===================== Error Checking ==============================
if (texture < GL_TEXTURE0 || texture > GL_TEXTURE31) {
// GL 3.3 core 3.8: ActiveTexture accepts TEXTUREi for i in
// [0, MAX_COMBINED_TEXTURE_IMAGE_UNITS - 1] - NOT a fixed 0..31 range. GL CTS's per-case
// state reset walks every advertised combined unit, so rejecting units the getter
// advertises aborts whole test batches. The backend already clamps its advertised value
// to the state layer's MAX_TEXTURE_IMAGE_UNITS capacity.
GLenum maxCombinedUnits = MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS;
if (MG_Backend::pActiveBackendObject != nullptr) {
maxCombinedUnits = std::min<GLenum>(
maxCombinedUnits,
static_cast<GLenum>(
std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxCombinedTextureImageUnits,
1)));
}
if (texture < GL_TEXTURE0 || texture >= GL_TEXTURE0 + maxCombinedUnits) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ActiveTexture_State",
std::format("Texture must be one of GL_TEXTUREi, where i is in the range 0 to 31, but got "
std::format("Texture must be one of GL_TEXTUREi, where i is in the range 0 to {}, but got "
"invalid enum: 0x{:X}, which may stand for unit {}.",
texture, texture - GL_TEXTURE0)));
maxCombinedUnits - 1, texture, texture - GL_TEXTURE0)));
return;
}
@@ -3032,6 +3047,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
auto& textureObject = bindingSlot.GetBoundObject();
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return;
TextureStorage1D(textureObject->GetExternalIndex(), levels, internalformat, width);
}
@@ -3046,6 +3062,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
auto& textureObject = bindingSlot.GetBoundObject();
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return;
TextureStorage2D(textureObject->GetExternalIndex(), levels, internalformat, width, height);
}
@@ -3061,6 +3078,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
auto& textureObject = bindingSlot.GetBoundObject();
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return;
TextureStorage3D(textureObject->GetExternalIndex(), levels, internalformat, width, height, depth);
}
@@ -3216,8 +3234,10 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(static_cast<Int>(unit));
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit));
if (texture == 0) {
// GL 4.5 8.1: texture zero unbinds every target of the unit, i.e. rebinds each
// target's default texture object (the unit's initial state).
for (auto& slot : textureUnit.GetAllBindingSlots()) {
slot.Bind(nullptr);
slot.Bind(MG_State::pGLContext->GetDefaultTextureObject(slot.GetTarget()));
}
return;
}
@@ -360,6 +360,19 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return true;
}
Bool ValidateTextureNotDefault(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
const char* caller) {
if (textureObject && textureObject->GetExternalIndex() == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"This operation is not allowed on the default texture (zero is "
"bound to the target)."));
return false;
}
return true;
}
Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject,
TextureTarget target) {
if (!textureObject) return true; // should be created later
@@ -29,6 +29,11 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
TexturePixelDataType type);
Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level);
Bool ValidateTextureObject(SharedPtr<MG_State::GLState::ITextureObject> textureObject);
// Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry
// points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION
// error is generated if zero is bound to target", ARB_texture_storage).
Bool ValidateTextureNotDefault(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
const char* caller);
Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject,
TextureTarget target);
Bool ValidateTextureSubImageOffsets(SharedPtr<MG_State::GLState::ITextureObject> textureObject, Int xoffset,
+4
View File
@@ -226,6 +226,10 @@ namespace MobileGL::MG_State {
return m_textureState.GetTextureObject(index);
}
const SharedPtr<ITextureObject>& GLContext::GetDefaultTextureObject(TextureTarget target) const {
return m_textureState.GetDefaultTextureObject(target);
}
const SharedPtr<ITextureObject>& GLContext::CreateTextureObject(Uint index, TextureTarget target) {
return m_textureState.CreateTextureObject(index, target);
}
+2
View File
@@ -95,6 +95,8 @@ namespace MobileGL {
// Texture
void GenTextureNames(Uint number, Vector<Uint>& textures);
const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
// 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);
void MarkTextureObjectForDeletion(Uint index);
TextureUnit& GetTextureUnitObject(Int unit);
@@ -154,6 +154,17 @@ namespace MobileGL::MG_State::GLState {
: nullptr;
}
// The per-target default texture objects (name 0) sit permanently in every texture unit's
// binding slots, so "nothing useful bound" is no longer a null slot. While a default texture
// has never been given an image (its internal format is still Unknown) it can contribute
// nothing to sampling; backends treat such a binding exactly like the old empty slot and
// skip per-draw sync/bind work for it. Once an application defines an image on a default
// texture it loses this shortcut and is synced like any other texture.
inline Bool IsUndefinedDefaultTexture(const ITextureObject* texture) {
return texture != nullptr && texture->GetExternalIndex() == 0 &&
texture->GetFormat() == TextureInternalFormat::Unknown;
}
class TextureObjectWithOneMipmap : public TextureObjectMipmap {
public:
TextureObjectWithOneMipmap(TextureTarget target, Uint externalIndex)
@@ -18,9 +18,51 @@
#include "TextureObjectStubs.h"
namespace MobileGL::MG_State::GLState {
static SharedPtr<ITextureObject> MakeTextureObjectForTarget(Uint index, TextureTarget target) {
switch (target) {
case TextureTarget::Texture1D:
return MakeShared<TextureObject1D>(index);
case TextureTarget::TextureCubeMap:
return MakeShared<TextureObject2DCube>(index);
case TextureTarget::Texture2D:
return MakeShared<TextureObject2D>(index);
case TextureTarget::Texture3D:
return MakeShared<TextureObject3D>(index);
case TextureTarget::TextureBuffer:
return MakeShared<TextureObjectBuffer>(index);
// These texture types are still stubbed:
case TextureTarget::TextureRectangle:
return MakeShared<TextureObjectRectangle>(index);
case TextureTarget::Texture2DMultisample:
return MakeShared<TextureObject2DMultisample>(index);
case TextureTarget::Texture1DArray:
return MakeShared<TextureObject1DArray>(index);
case TextureTarget::Texture2DArray:
return MakeShared<TextureObject2DArray>(index);
case TextureTarget::TextureCubeMapArray:
return MakeShared<TextureObjectCubeMapArray>(index);
case TextureTarget::Texture2DMultisampleArray:
return MakeShared<TextureObject2DMultisampleArray>(index);
default:
MOBILEGL_ASSERT(false, "Unimplemented texture type when creating texture object!: %d", (int)target);
return nullptr;
}
}
TextureState::TextureState() : m_indexGenerator(1024, 1) {
// GL 3.3 core 3.8: each target owns one default texture object (name 0) per context,
// shared across all texture units, and it is the initial binding of every unit/target
// slot. It is created outside m_textureObjects so name-based paths (glIsTexture,
// GenTextures/DeleteTextures, by-name DSA lookups) never see it.
for (int i = 0; i < (int)TextureTarget::TextureTargetCount; ++i) {
m_defaultTextureObjects[i] = MakeTextureObjectForTarget(0, static_cast<TextureTarget>(i));
}
for (int i = 0; i < MAX_TEXTURE_IMAGE_UNITS; ++i) {
m_textureUnits[i] = TextureUnit();
for (auto& bindingSlot : m_textureUnits[i].GetAllBindingSlots()) {
bindingSlot.Bind(m_defaultTextureObjects[(int)bindingSlot.GetTarget()]);
}
}
}
@@ -33,6 +75,12 @@ namespace MobileGL::MG_State::GLState {
return nullTextureObject;
}
const SharedPtr<ITextureObject>& TextureState::GetDefaultTextureObject(TextureTarget target) const {
MOBILEGL_ASSERT(target > TextureTarget::Unknown && target < TextureTarget::TextureTargetCount,
"GetDefaultTextureObject: invalid texture target %d", (int)target);
return m_defaultTextureObjects[(int)target];
}
void TextureState::GenerateNames(Uint number, Vector<Uint>& textures) {
textures.resize(number);
m_indexGenerator.Generate(number, textures.data());
@@ -40,48 +88,11 @@ namespace MobileGL::MG_State::GLState {
const SharedPtr<ITextureObject>& TextureState::CreateTextureObject(Uint index, TextureTarget target) {
auto& textureObject = m_textureObjects[index];
switch (target) {
case TextureTarget::Texture1D:
textureObject = MakeShared<TextureObject1D>(index);
break;
case TextureTarget::TextureCubeMap:
textureObject = MakeShared<TextureObject2DCube>(index);
break;
case TextureTarget::Texture2D:
textureObject = MakeShared<TextureObject2D>(index);
break;
case TextureTarget::Texture3D:
textureObject = MakeShared<TextureObject3D>(index);
break;
case TextureTarget::TextureBuffer:
textureObject = MakeShared<TextureObjectBuffer>(index);
break;
// These texture types are still stubbed:
case TextureTarget::TextureRectangle:
textureObject = MakeShared<TextureObjectRectangle>(index);
break;
case TextureTarget::Texture2DMultisample:
textureObject = MakeShared<TextureObject2DMultisample>(index);
break;
case TextureTarget::Texture1DArray:
textureObject = MakeShared<TextureObject1DArray>(index);
break;
case TextureTarget::Texture2DArray:
textureObject = MakeShared<TextureObject2DArray>(index);
break;
case TextureTarget::TextureCubeMapArray:
textureObject = MakeShared<TextureObjectCubeMapArray>(index);
break;
case TextureTarget::Texture2DMultisampleArray:
textureObject = MakeShared<TextureObject2DMultisampleArray>(index);
break;
default:
MOBILEGL_ASSERT(false, "Unimplemented texture type when creating texture object!: %d", (int)target);
textureObject = MakeTextureObjectForTarget(index, target);
if (!textureObject) {
static SharedPtr<ITextureObject> nullTextureObject = nullptr;
return nullTextureObject;
}
return textureObject;
}
@@ -94,7 +105,9 @@ namespace MobileGL::MG_State::GLState {
auto& bindingSlots = m_textureUnits[unit].GetAllBindingSlots();
for (auto& bindingSlot : bindingSlots) {
if (bindingSlot.GetBoundObject() == it->second) {
bindingSlot.Bind(nullptr);
// GL 3.3 core 3.8.1: deleting a bound texture rebinds zero, i.e. the
// target's default texture object, on every unit it was bound to.
bindingSlot.Bind(m_defaultTextureObjects[(int)bindingSlot.GetTarget()]);
}
}
}
@@ -49,6 +49,12 @@ namespace MobileGL::MG_State::GLState {
void GenerateNames(Uint number, Vector<Uint>& textures);
const SharedPtr<ITextureObject>& CreateTextureObject(Uint index, TextureTarget target);
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
// binds it, and image/parameter calls on it must work like on any texture. It is not a
// GenTextures name: it lives outside m_textureObjects (so glIsTexture(0) stays GL_FALSE
// and by-name lookups keep failing for 0) and can never be deleted.
const SharedPtr<ITextureObject>& GetDefaultTextureObject(TextureTarget target) const;
TextureUnit& GetUnitObject(Int unit);
ImageTextureBinding& GetImageTextureBinding(Int unit);
const ImageTextureBinding& GetImageTextureBinding(Int unit) const;
@@ -83,5 +89,8 @@ namespace MobileGL::MG_State::GLState {
Array<ImageTextureBinding, MAX_TEXTURE_IMAGE_UNITS> m_imageTextureBindings;
IndexGenerator<Uint> m_indexGenerator;
UnorderedMap<GLuint, SharedPtr<ITextureObject>> m_textureObjects;
// One default texture object (external name 0) per target, created with the context and
// immortal for its lifetime; the initial binding of every unit/target slot.
Array<SharedPtr<ITextureObject>, (int)TextureTarget::TextureTargetCount> m_defaultTextureObjects;
};
} // namespace MobileGL::MG_State::GLState
+282 -4
View File
@@ -151,14 +151,20 @@ namespace {
} // namespace
TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) {
auto& unit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
const auto boundBefore = unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
GLuint texture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
EXPECT_NE(texture, 0u);
EXPECT_TRUE(MG_State::pGLContext->ValidateTextureObject(texture));
auto& unit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
EXPECT_EQ(unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject(), nullptr);
// CreateTextures must not disturb the unit's binding (which is never empty anymore: at
// minimum it holds the target's default texture object).
EXPECT_EQ(unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject(), boundBefore);
EXPECT_NE(unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject(),
MG_State::pGLContext->GetTextureObject(texture));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
@@ -441,14 +447,17 @@ TEST_F(TextureTest, BindTextureUnitEnumAsNameIsSilentNoOp) {
EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(textureUnitEnum));
}
TEST_F(TextureTest, TexSubImage2DWithoutBoundTextureReportsErrorInsteadOfDereferencingNull) {
TEST_F(TextureTest, TexSubImage2DOnImagelessDefaultTextureReportsErrorInsteadOfDereferencingNull) {
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// Texture zero is a real (default) texture object now, so TexSubImage2D no longer fails for
// want of a bound object - it fails because the region exceeds the default texture's (empty
// or zero-sized) level 0, which is GL_INVALID_VALUE per GL 3.3 core 3.8.2.
const Uint8 pixel[] = {1, 2, 3, 4};
MG_Impl::GLImpl::TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
}
TEST_F(TextureTest, TextureStorageAndSubImageModifyNamedObjectOnly) {
@@ -1629,3 +1638,272 @@ TEST_F(TextureTest, NormalizeDepth24Stencil8UsesPackedDepthStencilType) {
EXPECT_EQ(format, GL_DEPTH_STENCIL);
EXPECT_EQ(type, GL_UNSIGNED_INT_24_8);
}
// ==================== Default texture objects (name 0), GL 3.3 core 3.8 ====================
TEST_F(TextureTest, DefaultTextureIsBoundInitiallyAndIsPerTarget) {
// The initial binding of every unit/target slot is the target's default texture object.
const auto& default2D = MG_State::pGLContext->GetDefaultTextureObject(TextureTarget::Texture2D);
const auto& default3D = MG_State::pGLContext->GetDefaultTextureObject(TextureTarget::Texture3D);
ASSERT_NE(default2D, nullptr);
ASSERT_NE(default3D, nullptr);
EXPECT_NE(default2D, default3D);
EXPECT_EQ(default2D->GetExternalIndex(), 0u);
EXPECT_EQ(default2D->GetTarget(), TextureTarget::Texture2D);
EXPECT_EQ(default3D->GetTarget(), TextureTarget::Texture3D);
// Binding 0 restores the default object, and the binding query reports name 0.
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(0)
.GetBindingSlot(TextureTarget::Texture2D)
.GetBoundObject(),
default2D);
GLint binding = -1;
MG_Impl::GLImpl::GetIntegerv(GL_TEXTURE_BINDING_2D, &binding);
EXPECT_EQ(binding, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// One default per target per context, shared across all texture units.
EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(5)
.GetBindingSlot(TextureTarget::Texture2D)
.GetBoundObject(),
default2D);
}
TEST_F(TextureTest, DefaultTextureAcceptsImageAndParameterCalls) {
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The exact shape of GL CTS's per-case state reset (gluStateReset resetStateGLCore): a
// zero-sized TexImage2D plus parameter resets on the default texture, all of which must
// succeed without recording anything.
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint minFilter = 0;
MG_Impl::GLImpl::GetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, &minFilter);
EXPECT_EQ(minFilter, GL_NEAREST);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// A real upload works like on any texture: data lands in the default object's shadow store.
const Uint8 pixels[] = {
1, 2, 3, 4,
5, 6, 7, 8,
};
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const auto& default2D = MG_State::pGLContext->GetDefaultTextureObject(TextureTarget::Texture2D);
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(default2D.get());
EXPECT_EQ(mipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2D, 0), IntVec3(2, 1, 1));
const auto* stored =
static_cast<const Uint8*>(mipmapObject->MapMipmapData(TextureUploadTarget::Texture2D, 0));
ASSERT_NE(stored, nullptr);
EXPECT_EQ(std::memcmp(stored, pixels, sizeof(pixels)), 0);
// Restore the CTS-reset shape (zero-sized level 0, default parameters) so later tests see
// the default texture in its usual post-reset state regardless of execution order.
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(TextureTest, DefaultTextureParametersAreSharedAcrossUnits) {
MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE0);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The same default object is bound on every unit, so the parameter shows up on unit 1 too.
MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE1);
GLint wrapS = 0;
MG_Impl::GLImpl::GetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, &wrapS);
EXPECT_EQ(wrapS, GL_CLAMP_TO_EDGE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The 3D default is a distinct object and keeps its own (initial) wrap mode.
GLint wrapS3D = 0;
MG_Impl::GLImpl::GetTexParameteriv(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, &wrapS3D);
EXPECT_EQ(wrapS3D, GL_REPEAT);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(TextureTest, DefaultTextureIsNotAnObjectNameAndSurvivesDeleteCalls) {
// glIsTexture(0) is GL_FALSE (name 0 is never a GenTextures name), with no error.
EXPECT_EQ(MG_Impl::GLImpl::IsTexture(0), GL_FALSE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(0));
EXPECT_FALSE(MG_State::pGLContext->ValidateTextureName(0));
// Deleting name 0 is silently ignored and leaves the default object fully usable.
constexpr GLuint zero = 0;
MG_Impl::GLImpl::DeleteTextures(1, &zero);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_NE(MG_State::pGLContext->GetDefaultTextureObject(TextureTarget::Texture2D), nullptr);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(TextureTest, DeletingBoundTextureRebindsDefaultTexture) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteTextures(1, &texture);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// GL 3.3 core 3.8.1: deleting the bound texture is as if BindTexture(target, 0) had run.
EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(0)
.GetBindingSlot(TextureTarget::Texture2D)
.GetBoundObject(),
MG_State::pGLContext->GetDefaultTextureObject(TextureTarget::Texture2D));
GLint binding = -1;
MG_Impl::GLImpl::GetIntegerv(GL_TEXTURE_BINDING_2D, &binding);
EXPECT_EQ(binding, 0);
// Image and parameter calls keep working against the (now bound) default texture.
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(TextureTest, NamedTextureRebindsAndWorksAfterUsingDefault) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
const Uint8 pixels[] = {9, 8, 7, 6};
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// Detour through the default texture, then rebind the named one.
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
GLint binding = -1;
MG_Impl::GLImpl::GetIntegerv(GL_TEXTURE_BINDING_2D, &binding);
EXPECT_EQ(binding, static_cast<GLint>(texture));
// The named texture's contents were not disturbed by the operations on the default.
const auto* stored = GetBoundTexture2DLevelBytes(texture);
ASSERT_NE(stored, nullptr);
EXPECT_EQ(std::memcmp(stored, pixels, sizeof(pixels)), 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteTextures(1, &texture);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(TextureTest, TexStorageOnDefaultTextureIsInvalidOperation) {
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// ARB_texture_storage: "An INVALID_OPERATION error is generated if zero is bound to target"
// - immutable storage can never be established on a default texture.
MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 2, 2);
ExpectSingleGlError(GL_INVALID_OPERATION);
EXPECT_FALSE(MG_State::pGLContext->GetDefaultTextureObject(TextureTarget::Texture2D)->IsImmutable());
}
TEST_F(TextureTest, CtsStyleStateResetOnDefaultTexturesLeavesNoError) {
// Mirrors the texture section of VK-GL-CTS gluStateReset resetStateGLCore, which runs after
// EVERY case: bind 0 on each target, clear the default texture's image with a zero-sized
// TexImage*, and reset sampler-ish parameters. Any leftover error aborts the whole batch
// ("Texture state reset failed"), so this exact sequence must stay clean end to end.
const GLfloat borderColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
const auto resetCommonTexParams = [&borderColor](GLenum target) {
MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
MG_Impl::GLImpl::TexParameterfv(target, GL_TEXTURE_BORDER_COLOR, borderColor);
MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_WRAP_S, GL_REPEAT);
MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_WRAP_T, GL_REPEAT);
MG_Impl::GLImpl::TexParameterf(target, GL_TEXTURE_MIN_LOD, -1000.0f);
MG_Impl::GLImpl::TexParameterf(target, GL_TEXTURE_MAX_LOD, 1000.0f);
MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_BASE_LEVEL, 0);
MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_MAX_LEVEL, 1000);
MG_Impl::GLImpl::TexParameterf(target, GL_TEXTURE_LOD_BIAS, 0.0f);
MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_COMPARE_MODE, GL_NONE);
MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_SWIZZLE_R, GL_RED);
MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_SWIZZLE_G, GL_GREEN);
MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_SWIZZLE_B, GL_BLUE);
MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_SWIZZLE_A, GL_ALPHA);
};
MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE0);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_1D, 0);
MG_Impl::GLImpl::TexImage1D(GL_TEXTURE_1D, 0, GL_RGBA, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
resetCommonTexParams(GL_TEXTURE_1D);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_1D reset failed";
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
resetCommonTexParams(GL_TEXTURE_2D);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_2D reset failed";
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_CUBE_MAP, 0);
for (int face = 0; face < 6; ++face) {
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, GL_RGBA, 0, 0, 0, GL_RGBA,
GL_UNSIGNED_BYTE, nullptr);
}
resetCommonTexParams(GL_TEXTURE_CUBE_MAP);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_CUBE_MAP reset failed";
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0);
MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, 0, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE,
nullptr);
resetCommonTexParams(GL_TEXTURE_2D_ARRAY);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_2D_ARRAY reset failed";
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, 0);
MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_3D, 0, GL_RGBA, 0, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_REPEAT);
resetCommonTexParams(GL_TEXTURE_3D);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_3D reset failed";
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_1D_ARRAY, 0);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE,
nullptr);
resetCommonTexParams(GL_TEXTURE_1D_ARRAY);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_1D_ARRAY reset failed";
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_RECTANGLE, 0);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE,
nullptr);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_RECTANGLE reset failed";
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_BUFFER, 0);
MG_Impl::GLImpl::TexBuffer(GL_TEXTURE_BUFFER, GL_R8, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_BUFFER reset failed";
// 3.2-core section: multisample defaults are cleared with ZERO-sized (and, for the array
// target, zero-layer) TexImage*Multisample calls - GL only rejects negative dimensions.
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_SWIZZLE_R, GL_RED);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_SWIZZLE_G, GL_GREEN);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_SWIZZLE_B, GL_BLUE);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_SWIZZLE_A, GL_ALPHA);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_BASE_LEVEL, 0);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_MAX_LEVEL, 1000);
MG_Impl::GLImpl::TexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 1, GL_RGBA8, 0, 0, GL_TRUE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_2D_MULTISAMPLE reset failed";
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, 0);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, GL_TEXTURE_SWIZZLE_R, GL_RED);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, GL_TEXTURE_SWIZZLE_G, GL_GREEN);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, GL_TEXTURE_SWIZZLE_B, GL_BLUE);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, GL_TEXTURE_SWIZZLE_A, GL_ALPHA);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, GL_TEXTURE_BASE_LEVEL, 0);
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, GL_TEXTURE_MAX_LEVEL, 1000);
MG_Impl::GLImpl::TexImage3DMultisample(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, 1, GL_RGBA8, 0, 0, 0, GL_TRUE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_2D_MULTISAMPLE_ARRAY reset failed";
}