mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 05:08:31 +09:00
[Fix] (MG_State, MG_Backend/DirectGLES): preserve legacy texture reuse and clear default backend bindings - keep generated-but-unbound names alive for Minecraft 1.7.10 atlas uploads, synchronize bind-0 to native GLES without 1D/2D alias churn, and cover both paths with BGRA sub-image and binding-cache regressions
This commit is contained in:
@@ -1034,7 +1034,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
static void BindCurrentProgramWithResources();
|
||||
static void BindCurrentTextures();
|
||||
|
||||
void PrepareForDraw(DrawSyncBit syncBit) {
|
||||
#ifdef TRACY_ENABLE
|
||||
@@ -1077,7 +1076,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// sample whatever texture the last sync left behind (e.g. Flywheel's depth
|
||||
// pyramid downsample reading a stale unit-0 binding instead of the depth
|
||||
// attachment).
|
||||
static void BindCurrentTextures() {
|
||||
void BindCurrentTextures() {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedNC("BindCurrentTextures", TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
@@ -1085,27 +1084,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const Int maxTouchedUnit = MG_State::pGLContext->GetMaxTouchedTextureUnit();
|
||||
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
Array<Bool, (SizeT)TextureTarget::TextureTargetCount> boundBackendTargets{};
|
||||
|
||||
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.
|
||||
|
||||
// An image-less default texture is the frontend's bind-0 state. Defer native
|
||||
// unbinding until all slots have been considered: desktop 1D/1D-array targets
|
||||
// alias ES 2D/2D-array targets, so a default alias must not clear a real binding.
|
||||
if (MG_State::GLState::IsUndefinedDefaultTexture(textureObject.get())) continue;
|
||||
|
||||
// Bind texture object
|
||||
auto target = textureObject->GetTarget();
|
||||
if (!TextureImpl::IsSupportedTextureTarget(target)) {
|
||||
MGLOG_D(" Texture target %s is not supported, skipping.",
|
||||
MG_Util::ConvertTextureTargetToString(target).c_str());
|
||||
continue;
|
||||
}
|
||||
const GLenum targetGL = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
|
||||
|
||||
// Bind texture object
|
||||
const auto& backendTextureIt = TextureImpl::g_backendTextureObjects.find(textureObject.get());
|
||||
if (backendTextureIt == TextureImpl::g_backendTextureObjects.end()) continue;
|
||||
|
||||
GLenum targetGL = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
|
||||
backendTextureIt->second->Bind(targetGL, unit);
|
||||
const auto backendTarget = TextureImpl::MapToBackendTextureTarget(target);
|
||||
boundBackendTargets[static_cast<SizeT>(backendTarget)] = true;
|
||||
}
|
||||
|
||||
// Clear each native target that has no resolved frontend binding. This is the backend
|
||||
// half of glBindTexture(..., 0); skipping image-less default objects would otherwise
|
||||
// leave the previously sampled ES texture resident. Deduplicate mapped desktop targets
|
||||
// so 1D/2D and 1D-array/2D-array aliases do not cause redundant binds.
|
||||
Array<Bool, (SizeT)TextureTarget::TextureTargetCount> visitedBackendTargets{};
|
||||
for (const auto& bindingSlot : textureUnit.GetAllBindingSlots()) {
|
||||
const auto target = bindingSlot.GetTarget();
|
||||
if (!TextureImpl::IsSupportedTextureTarget(target)) continue;
|
||||
|
||||
const auto backendTarget = TextureImpl::MapToBackendTextureTarget(target);
|
||||
const auto backendTargetIndex = static_cast<SizeT>(backendTarget);
|
||||
if (visitedBackendTargets[backendTargetIndex]) continue;
|
||||
visitedBackendTargets[backendTargetIndex] = true;
|
||||
|
||||
if (!boundBackendTargets[backendTargetIndex]) {
|
||||
const GLenum targetGL = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
|
||||
TextureImpl::UnbindTexture(unit, targetGL);
|
||||
}
|
||||
}
|
||||
|
||||
// Bind sampler object if necessary
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
operation Utils::CheckGLESError();
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Re-establishes the frontend texture-unit bindings on the native ES context.
|
||||
// Content uploads use scratch bindings, so draws and dispatches call this after
|
||||
// texture synchronization.
|
||||
void BindCurrentTextures();
|
||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||
|
||||
@@ -122,11 +122,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
// re-resolved instead of dangling.
|
||||
BumpTextureBindGeneration();
|
||||
m_textureObjects.erase(index);
|
||||
m_indexGenerator.Delete(index);
|
||||
}
|
||||
// Release the name itself even when GenTextures only reserved it and no bind ever
|
||||
// instantiated an object: GL 3.3 core 3.8.1 makes a deleted name unused again (so a
|
||||
// later bind of it must fail), and the reservation has to return to the free list.
|
||||
m_indexGenerator.Delete(index);
|
||||
// Compatibility: legacy Minecraft may delete a generated name before its first bind,
|
||||
// then bind and populate that same name. Keep such a reservation alive; only a real
|
||||
// texture object reaching deletion releases its index above.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||
@@ -103,6 +104,79 @@ namespace {
|
||||
private:
|
||||
MobileGL::MG_External::GLESCapabilities m_snapshot;
|
||||
};
|
||||
|
||||
struct TextureBindCall {
|
||||
GLenum target;
|
||||
GLuint texture;
|
||||
};
|
||||
|
||||
MobileGL::Vector<TextureBindCall>* g_textureBindCalls = nullptr;
|
||||
GLuint g_nextBackendTextureId = 73;
|
||||
|
||||
void RecordTextureBind(GLenum target, GLuint texture) {
|
||||
if (g_textureBindCalls) {
|
||||
g_textureBindCalls->push_back({target, texture});
|
||||
}
|
||||
}
|
||||
|
||||
void GenerateBackendTextures(GLsizei count, GLuint* textures) {
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
textures[i] = g_nextBackendTextureId++;
|
||||
}
|
||||
}
|
||||
|
||||
void DeleteBackendTextures(GLsizei, const GLuint*) {}
|
||||
|
||||
GLenum NoBackendError() {
|
||||
return GL_NO_ERROR;
|
||||
}
|
||||
|
||||
// Isolates the DirectGLES globals touched by the binding-cache regression test. The test
|
||||
// installs only the native ES entry points needed to construct/bind a backend texture and
|
||||
// restores the process-wide state even when a gtest assertion unwinds the test body.
|
||||
struct ScopedDirectGLESTextureBindings {
|
||||
ScopedDirectGLESTextureBindings():
|
||||
previousContext(MobileGL::Move(MobileGL::MG_State::pGLContext)),
|
||||
previousFunctions(MobileGL::MG_Backend::DirectGLES::g_GLESFuncs),
|
||||
previousActiveUnit(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_activeTextureUnit),
|
||||
previousCache(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache),
|
||||
previousRegistry(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects) {
|
||||
MobileGL::MG_State::pGLContext = MobileGL::MakeUnique<MobileGL::MG_State::GLState::GLContext>();
|
||||
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_activeTextureUnit = 0;
|
||||
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache = {};
|
||||
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects = {};
|
||||
|
||||
MobileGL::MG_External::GLESFunctionsTable functions{};
|
||||
functions.glBindTexture = RecordTextureBind;
|
||||
functions.glDeleteTextures = DeleteBackendTextures;
|
||||
functions.glGenTextures = GenerateBackendTextures;
|
||||
functions.glGetError = NoBackendError;
|
||||
MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(functions);
|
||||
g_textureBindCalls = &bindCalls;
|
||||
}
|
||||
|
||||
~ScopedDirectGLESTextureBindings() {
|
||||
g_textureBindCalls = nullptr;
|
||||
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache = {};
|
||||
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects = previousRegistry;
|
||||
MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(previousFunctions);
|
||||
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_activeTextureUnit = previousActiveUnit;
|
||||
MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache = previousCache;
|
||||
MobileGL::MG_State::pGLContext = MobileGL::Move(previousContext);
|
||||
}
|
||||
|
||||
ScopedDirectGLESTextureBindings(const ScopedDirectGLESTextureBindings&) = delete;
|
||||
ScopedDirectGLESTextureBindings& operator=(const ScopedDirectGLESTextureBindings&) = delete;
|
||||
|
||||
MobileGL::Vector<TextureBindCall> bindCalls;
|
||||
|
||||
private:
|
||||
MobileGL::UniquePtr<MobileGL::MG_State::GLState::GLContext> previousContext;
|
||||
MobileGL::MG_External::GLESFunctionsTable previousFunctions;
|
||||
MobileGL::Uint previousActiveUnit;
|
||||
decltype(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_boundTexturesCache) previousCache;
|
||||
decltype(MobileGL::MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects) previousRegistry;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST(Sanity, BasicAssertions) {
|
||||
@@ -148,6 +222,52 @@ TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensionsWithoutRaisingGL
|
||||
extensions.end());
|
||||
}
|
||||
|
||||
TEST(DirectGLESSanity, BindingZeroClearsPreviousNativeTextureBinding) {
|
||||
using namespace MobileGL;
|
||||
namespace DirectGLES = MG_Backend::DirectGLES;
|
||||
|
||||
ScopedDirectGLESTextureBindings state;
|
||||
|
||||
GLuint frontendTexture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &frontendTexture);
|
||||
ASSERT_NE(frontendTexture, 0u);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, frontendTexture);
|
||||
const auto& frontendTextureObject = MG_State::pGLContext->GetTextureUnitObject(0)
|
||||
.GetBindingSlot(TextureTarget::Texture2D)
|
||||
.GetBoundObject();
|
||||
ASSERT_NE(frontendTextureObject, nullptr);
|
||||
ASSERT_EQ(frontendTextureObject->GetExternalIndex(), frontendTexture);
|
||||
|
||||
auto& backendTexture = DirectGLES::TextureImpl::g_backendTextureObjects.GetOrCreate(frontendTextureObject);
|
||||
backendTexture = MakeShared<DirectGLES::TextureImpl::BackendTextureObject>();
|
||||
const GLuint backendTextureId = backendTexture->GetBackendTextureId();
|
||||
|
||||
DirectGLES::BindCurrentTextures();
|
||||
ASSERT_EQ(state.bindCalls.size(), 1u);
|
||||
EXPECT_EQ(state.bindCalls[0].target, GL_TEXTURE_2D);
|
||||
EXPECT_EQ(state.bindCalls[0].texture, backendTextureId);
|
||||
|
||||
// The default 1D slot maps to the same native ES target as 2D. It must not clear and force a
|
||||
// redundant rebind while the real 2D frontend object remains current.
|
||||
DirectGLES::BindCurrentTextures();
|
||||
EXPECT_EQ(state.bindCalls.size(), 1u);
|
||||
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
|
||||
ASSERT_TRUE(MG_State::GLState::IsUndefinedDefaultTexture(
|
||||
MG_State::pGLContext->GetTextureUnitObject(0)
|
||||
.GetBindingSlot(TextureTarget::Texture2D)
|
||||
.GetBoundObject()
|
||||
.get()));
|
||||
|
||||
DirectGLES::BindCurrentTextures();
|
||||
|
||||
ASSERT_EQ(state.bindCalls.size(), 2u);
|
||||
EXPECT_EQ(state.bindCalls[1].target, GL_TEXTURE_2D);
|
||||
EXPECT_EQ(state.bindCalls[1].texture, 0u);
|
||||
EXPECT_EQ(DirectGLES::TextureImpl::g_boundTexturesCache[0][static_cast<SizeT>(TextureTarget::Texture2D)],
|
||||
nullptr);
|
||||
}
|
||||
|
||||
TEST(DirectGLESSanity, ProvidesNamedFramebufferBlitForDirectStateAccess) {
|
||||
MobileGL::MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
|
||||
const auto& funcs = backend.GetBackendFunctions().GL;
|
||||
|
||||
@@ -380,10 +380,10 @@ TEST_F(TextureTest, GenThenBindCreatesObjectForUnsizedPackedBgraSubImageUpload)
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL 3.3 core 3.8.1: DeleteTextures makes the name unused again whether or not a bind ever
|
||||
// instantiated an object, so the reservation must go back to the generator's free list rather
|
||||
// than leaking, and binding the dead name afterwards must fail.
|
||||
TEST_F(TextureTest, DeleteGeneratedButUnboundNameReleasesReservationAndBindFails) {
|
||||
// Legacy Minecraft reserves a texture name, deletes it before first bind, then reuses the same
|
||||
// name for the atlas upload. Preserve that generated reservation so the later bind can instantiate
|
||||
// the object and subsequent sub-image uploads target it instead of the default texture.
|
||||
TEST_F(TextureTest, DeleteGeneratedReservationThenBindCreatesObjectForSubImageUpload) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
ASSERT_NE(texture, 0u);
|
||||
@@ -392,22 +392,36 @@ TEST_F(TextureTest, DeleteGeneratedButUnboundNameReleasesReservationAndBindFails
|
||||
|
||||
MG_Impl::GLImpl::DeleteTextures(1, &texture);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
EXPECT_FALSE(MG_State::pGLContext->ValidateTextureName(texture));
|
||||
EXPECT_TRUE(MG_State::pGLContext->ValidateTextureName(texture));
|
||||
EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(texture));
|
||||
// IsTexture answers about a dead name without raising anything (GL 3.3 core 6.1.4).
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsTexture(texture), GL_FALSE);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(texture));
|
||||
|
||||
// The freed reservation is recycled (the generator's free list is LIFO, so the very same
|
||||
// name comes back) - a delete that skipped the release would hand out a fresh name here.
|
||||
GLuint recycled = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &recycled);
|
||||
EXPECT_EQ(recycled, texture);
|
||||
EXPECT_TRUE(MG_State::pGLContext->ValidateTextureName(recycled));
|
||||
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||
ASSERT_NE(textureObject, nullptr);
|
||||
EXPECT_TRUE(MG_State::pGLContext->ValidateTextureName(texture));
|
||||
EXPECT_TRUE(MG_State::pGLContext->ValidateTextureObject(texture));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsTexture(texture), GL_TRUE);
|
||||
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 1, 0, GL_BGRA,
|
||||
GL_UNSIGNED_INT_8_8_8_8_REV, nullptr);
|
||||
const Uint8 pixels[] = {
|
||||
10, 20, 30, 40,
|
||||
50, 60, 70, 80,
|
||||
};
|
||||
MG_Impl::GLImpl::TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, GL_BGRA,
|
||||
GL_UNSIGNED_INT_8_8_8_8_REV, pixels);
|
||||
|
||||
const auto* stored = GetBoundTexture2DLevelBytes(texture);
|
||||
ASSERT_NE(stored, nullptr);
|
||||
const Uint8 expected[] = {
|
||||
30, 20, 10, 40,
|
||||
70, 60, 50, 80,
|
||||
};
|
||||
EXPECT_EQ(std::memcmp(stored, expected, sizeof(expected)), 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(TextureTest, DeleteInstantiatedTextureInvalidatesNameUntilRegenerated) {
|
||||
|
||||
Reference in New Issue
Block a user