[MG_Impl, Test] preserve immutable view storage and plan mip generation within base/max levels

This commit is contained in:
2026-09-20 13:10:58 -04:00
parent ed01f4e31a
commit 70c8230b3e
5 changed files with 183 additions and 2 deletions
@@ -12,6 +12,7 @@
#include "MG_Util/Types.h"
#include "Validators.h"
#include "ProxyTexture.h"
#include "MipmapGenerationPlan.h"
#include <MG_State/GLState/Core.h>
#include <MG_Backend/BackendObjects.h>
@@ -510,6 +511,32 @@ namespace MobileGL::MG_Impl::GLImpl {
Bool EnsureGeneratedMipmapStorageAllocated(
MG_State::GLState::TextureObjectMipmap& texture,
TextureUploadTarget uploadTarget) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const auto plan = ComputeMipmapGenerationRange(texture, uploadTarget);
if (plan.End <= plan.Base) return false;
// Immutable textures and views already own every generated level.
// Allocating through a view would redefine its owner's complete
// level, destroying layers and levels outside the view window.
if (texture.IsImmutable()) return true;
const IntVec3 baseSize = texture.GetMipmapTexelSize(uploadTarget, plan.Base);
const SizeT baseBytes = texture.GetMipmapByteSize(uploadTarget, plan.Base);
const SizeT texels = static_cast<SizeT>(baseSize.x()) * baseSize.y() * baseSize.z();
if (!baseBytes || !texels || baseBytes % texels) return false;
const SizeT pixelBytes = baseBytes / texels;
const Int axes = MipShrinkingAxisCount(texture.GetTarget());
for (Uint level = plan.Base + 1; level < plan.End; ++level) {
const auto size = ComputeMipmapTexelSize(baseSize, level - plan.Base, axes);
const SizeT bytes = pixelBytes * static_cast<SizeT>(size.x()) * size.y() * size.z();
texture.AllocateStorage(uploadTarget, level, {size, bytes});
texture.MarkStorageDirty(uploadTarget, level, false);
}
// Levels before BASE_LEVEL and after the generated end remain
// valid images. In particular, MAX_LEVEL is not a truncate call.
texture.BumpContentVersion();
return true;
}
#endif
const Uint existingLevelCount = texture.GetMipmapLevelCount();
if (existingLevelCount == 0) {
return false;
@@ -0,0 +1,45 @@
// MobileGL - MobileGL/MG_Impl/GLImpl/Texture/MipmapGenerationPlan.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 <Includes.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_State/GLState/TextureState/TextureObject.h>
#include <algorithm>
namespace MobileGL::MG_Impl::GLImpl {
struct MipmapGenerationRange {
Uint Base = 0;
Uint End = 0; // logical end-exclusive, including the source base level
};
inline MipmapGenerationRange ComputeMipmapGenerationRange(
const MG_State::GLState::TextureObjectMipmap& texture, TextureUploadTarget uploadTarget) {
const auto range = texture.GetLevelRange();
MipmapGenerationRange result{range.x(), range.x()};
const Uint available = texture.GetMipmapLevelCount();
if (result.Base >= available || range.y() < result.Base) return result;
const auto extent = texture.GetMipmapTexelSize(uploadTarget, result.Base);
if (extent.x() <= 0 || extent.y() <= 0 || extent.z() <= 0) return result;
Int largest = extent.x();
if (texture.GetTarget() != TextureTarget::Texture1D &&
texture.GetTarget() != TextureTarget::Texture1DArray)
largest = std::max(largest, extent.y());
if (texture.GetTarget() == TextureTarget::Texture3D) largest = std::max(largest, extent.z());
Uint levels = 1;
while (largest > 1) { largest /= 2; ++levels; }
Uint64 end = std::min(static_cast<Uint64>(result.Base) + levels,
static_cast<Uint64>(range.y()) + 1);
// A view's level count is already relative to its own window. Neither
// immutable storage nor a view may acquire levels beyond that allocation.
if (texture.IsImmutable()) end = std::min(end, static_cast<Uint64>(available));
result.End = static_cast<Uint>(end);
return result;
}
}
#endif
+1 -1
View File
@@ -754,7 +754,7 @@ include(GoogleTest)
set(MGL_ITEST_MONOLITH_MECHANISM_FILTER
"HandleRecycleScenario.*:CsoContentAddressingScenario.*:ResourceSubsystemControlScenario.*:ObjectSubsystemControlScenario.*:PipeVerifyArmingScenario.*:PoisonOmissionScenario.*:StorageBufferRegrowScenario.NStorageDefinitionsCostNMapPersistentRoundtripsNotOnePerDraw:LargeArenaAdoptionScenario.AnAdoptionCostsExactlyOneMapPersistentRoundtrip")
set(MGL_ITEST_GLES_VIEW_FILTER
"TextureViewScenario.*:F1WireScenario.ComputeImageStoreThroughViewPreservesOtherRootLayer:F1WireScenario.TextureViewClearTargetsItsRootMipAndLayer")
"TextureViewScenario.*:F1WireScenario.ComputeImageStoreThroughViewPreservesOtherRootLayer:F1WireScenario.TextureViewClearTargetsItsRootMipAndLayer:F1WireScenario.GenerateMipmapThroughViewKeepsOwnerLevelsAndLayersOutsideItsWindow")
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES."
@@ -500,6 +500,109 @@ TEST_F(F1WireScenario, GenerateMipmapDepthPixels) {
EXPECT_NEAR(pixel, 0.375f, 0.00001f);
}
TEST_F(F1WireScenario, GenerateMipmapHonorsMutableBaseAndMaxWithoutChangingOtherLevels) {
if (!Ready()) return;
const std::array<GLubyte, 4> colors[5] = {
{255, 0, 0, 255}, {0, 255, 255, 255}, {0, 0, 255, 255},
{255, 255, 0, 255}, {255, 0, 255, 255}};
for (int level = 0; level < 5; ++level) {
const int side = 16 >> level;
std::vector<GLubyte> pixels(static_cast<size_t>(side * side * 4));
for (size_t at = 0; at < pixels.size(); at += 4)
std::copy(colors[level].begin(), colors[level].end(), pixels.begin() + at);
glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, side, side, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 1);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
// The actual base differs from its uploaded shadow. No readback separates
// this GPU write from generation, and level zero contains a different color.
glClearColor(0, 1, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
glGenerateMipmap(GL_TEXTURE_2D);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
for (int level = 0; level < 5; ++level) {
const int side = 16 >> level;
GLint width = 0, height = 0;
glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &width);
glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_HEIGHT, &height);
EXPECT_EQ(width, side) << "level " << level;
EXPECT_EQ(height, side) << "level " << level;
std::vector<GLubyte> pixels(static_cast<size_t>(side * side * 4), 17);
glGetTexImage(GL_TEXTURE_2D, level, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
const std::array<GLubyte, 4> expected = level == 1 || level == 2
? std::array<GLubyte, 4>{0, 255, 0, 255} : colors[level];
size_t wrong = 0;
for (size_t at = 0; at < pixels.size(); at += 4)
if (!std::equal(expected.begin(), expected.end(), pixels.begin() + at)) ++wrong;
EXPECT_EQ(wrong, 0u) << "level " << level << " must retain or generate its own pixels";
}
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
}
TEST_F(F1WireScenario, GenerateMipmapThroughViewKeepsOwnerLevelsAndLayersOutsideItsWindow) {
if (!Ready()) return;
GLuint root = 0, view = 0;
glGenTextures(1, &root);
glBindTexture(GL_TEXTURE_2D_ARRAY, root);
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 5, GL_RGBA8, 16, 16, 4);
const std::array<GLubyte, 4> colors[5] = {
{255, 0, 0, 255}, {0, 0, 255, 255}, {255, 255, 0, 255},
{255, 0, 255, 255}, {0, 255, 255, 255}};
for (int level = 0; level < 5; ++level) {
const int side = 16 >> level;
std::vector<GLubyte> pixels(static_cast<size_t>(side * side * 4 * 4));
for (size_t at = 0; at < pixels.size(); at += 4)
std::copy(colors[level].begin(), colors[level].end(), pixels.begin() + at);
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, level, 0, 0, 0, side, side, 4,
GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
}
glGenTextures(1, &view);
// Logical levels 0..2 alias owner levels 1..3; only owner layers 1..2 belong
// to this view. Its nonzero BASE_LEVEL makes owner level 2 the source.
glTextureView(view, GL_TEXTURE_2D_ARRAY, root, GL_RGBA8, 1, 3, 1, 2);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
glTextureParameteri(view, GL_TEXTURE_BASE_LEVEL, 1);
glTextureParameteri(view, GL_TEXTURE_MAX_LEVEL, 2);
for (const int layer : {1, 2}) {
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, root, 2, layer);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
glClearColor(0, 1, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
}
glGenerateTextureMipmap(view);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
glBindTexture(GL_TEXTURE_2D_ARRAY, root);
for (int level = 0; level < 5; ++level) {
const int side = 16 >> level;
GLint width = 0, height = 0, layers = 0;
glGetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, level, GL_TEXTURE_WIDTH, &width);
glGetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, level, GL_TEXTURE_HEIGHT, &height);
glGetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, level, GL_TEXTURE_DEPTH, &layers);
EXPECT_EQ(width, side) << "owner mip " << level;
EXPECT_EQ(height, side) << "owner mip " << level;
ASSERT_EQ(layers, 4) << "generation through a two-layer view must not resize its owner's level";
const size_t layerBytes = static_cast<size_t>(side * side * 4);
std::vector<GLubyte> pixels(layerBytes * 4, 17);
glGetTexImage(GL_TEXTURE_2D_ARRAY, level, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
for (int layer = 0; layer < 4; ++layer) {
const bool generated = (level == 2 || level == 3) && (layer == 1 || layer == 2);
const std::array<GLubyte, 4> expected = generated
? std::array<GLubyte, 4>{0, 255, 0, 255} : colors[level];
size_t wrong = 0;
for (size_t at = 0; at < layerBytes; at += 4)
if (!std::equal(expected.begin(), expected.end(), pixels.begin() + layerBytes * layer + at)) ++wrong;
EXPECT_EQ(wrong, 0u) << "owner mip " << level << ", layer " << layer;
}
}
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 0, 0);
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
glDeleteTextures(1, &view);
glDeleteTextures(1, &root);
}
TEST_F(F1WireScenario, VertexIdSamplerAndScalarUniformPixels) {
if (!Ready()) return;
Attach(GL_RGBA8);
+7 -1
View File
@@ -54,6 +54,7 @@
// set_index_buffer / the buffer's own constructor did that), for MGPDrawInfo::IndexResource and
// the two indirect-buffer handles.
#include <MG_Impl/Pipe/ResourceTracker.h>
#include <MG_Impl/GLImpl/Texture/MipmapGenerationPlan.h>
#include <MG_Impl/Pipe/OwnedDrawInputs.h>
#include <cstdio>
@@ -369,7 +370,12 @@ namespace MobileGL::MG_Remote::Client {
record.Target = static_cast<Uint16>(target);
record.BaseLevel = texture->GetLevelRange().x();
const auto* mipmap = dynamic_cast<const MG_State::GLState::TextureObjectMipmap*>(texture.get());
record.LevelCount = mipmap ? mipmap->GetMipmapLevelCount() : 0;
if (mipmap && !texture->GetUploadTargets().empty()) {
const auto plan = MG_Impl::GLImpl::ComputeMipmapGenerationRange(*mipmap, texture->GetUploadTargets()[0]);
// LevelCount is the logical end-exclusive, not the number of
// levels following BaseLevel. Preserve that existing carrier.
record.LevelCount = static_cast<Uint16>(std::min<Uint>(plan.End, mipmap->GetMipmapLevelCount()));
}
session.EmitAndWait(MG_Pipe::MGPWireOp::GenerateMipmap, &record, sizeof(record),
nullptr, 0, nullptr, 0, nullptr);
}