mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[MG_Impl, Test] preserve immutable view storage and plan mip generation within base/max levels
This commit is contained in:
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user