diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 7de26bfe..1efd6b8e 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -12,6 +12,7 @@ #include "MG_Util/Types.h" #include "Validators.h" #include "ProxyTexture.h" +#include "MipmapGenerationPlan.h" #include #include @@ -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(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(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; diff --git a/MobileGL/MG_Impl/GLImpl/Texture/MipmapGenerationPlan.h b/MobileGL/MG_Impl/GLImpl/Texture/MipmapGenerationPlan.h new file mode 100644 index 00000000..4b1c298f --- /dev/null +++ b/MobileGL/MG_Impl/GLImpl/Texture/MipmapGenerationPlan.h @@ -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 + +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#include + +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(result.Base) + levels, + static_cast(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(available)); + result.End = static_cast(end); + return result; + } +} +#endif diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 856fac2b..eb5351ef 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -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." diff --git a/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp index a9570d9e..f6fea7df 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp @@ -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 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 pixels(static_cast(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 pixels(static_cast(side * side * 4), 17); + glGetTexImage(GL_TEXTURE_2D, level, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + const std::array expected = level == 1 || level == 2 + ? std::array{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 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 pixels(static_cast(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(side * side * 4); + std::vector 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 expected = generated + ? std::array{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); diff --git a/MobileGL/MG_Remote/Client/EmitTables.cpp b/MobileGL/MG_Remote/Client/EmitTables.cpp index eb14b127..e5da241a 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.cpp +++ b/MobileGL/MG_Remote/Client/EmitTables.cpp @@ -54,6 +54,7 @@ // set_index_buffer / the buffer's own constructor did that), for MGPDrawInfo::IndexResource and // the two indirect-buffer handles. #include +#include #include #include @@ -369,7 +370,12 @@ namespace MobileGL::MG_Remote::Client { record.Target = static_cast(target); record.BaseLevel = texture->GetLevelRange().x(); const auto* mipmap = dynamic_cast(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(std::min(plan.End, mipmap->GetMipmapLevelCount())); + } session.EmitAndWait(MG_Pipe::MGPWireOp::GenerateMipmap, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); }