mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[MG_Backend] (Magma): keep a view's mip window and readback inside the layers it names
Magma's generate path (VulkanRenderer::GenerateMipmap) took its window from the
BOUND object and its blits from the STORAGE image, and the two do not coincide
for a texture view: glTextureView gives a view its own BASE_LEVEL/MAX_LEVEL and
layer slice over somebody else's image. Three things followed from the mix-up,
all visible through a two-layer view over a four-layer 2D array:
* EnsureGenerateMipmapStorageAllocated allocated the chain THROUGH the bound
object from that object's OWN base extent, so the storage owner's level was
resized to the view's layer count - the storage behind its other layers was
gone - and the levels outside the view's window were redefined with it.
Immutable storage and views already own every level generation can write,
so neither allocates anything now, and a mutable owner gets only the levels
its window is missing (AllocateStorage RESIZES the level it is handed, so
an existing level keeps the shadow bytes its own upload path wrote).
* The level the chain starts at was the view's, applied to the storage image:
a view whose MIN_LEVEL is 1 generated from the storage's level 1 instead of
its level 2. The window now comes from the frontend's own plan
(MipmapGenerationPlan.h - the same numbers MGPMipPlan::LevelCount publishes)
and is shifted into the storage's numbering before any blit or barrier.
* Every layer of the storage was written instead of the view's window. The
blits carry the view's baseArrayLayer and layerCount now; every layer
outside that window belongs to the storage owner and is left as it was.
The end of the window follows MAX_LEVEL for the same reason the frontend's plan
does: a chain MAX_LEVEL cuts short is no longer generated past it.
The readback had the same shape of bug pointing the other way.
VulkanRenderer::GetTextureImage sized its copy by the storage image's array
layer count while starting it at the caller's layer origin, so a two-layer view
of a four-layer array copied four layers - two outside the window, and the last
one past the end of the image. glGetTexImage carries no destination size, while
the size glGetTextureImage validates is built from the VIEW's own extents, so a
caller that sized its buffer for the view had it overrun by exactly the layers
outside its window. The copy covers the caller's own layers now.
TextureViewScenario.GenerateMipmapThroughAViewLeavesTheOwnerShapeAlone runs on
both backends now. Its DirectVulkan skip named the reallocation above, and the
sub-image readback the skip blamed as well is not broken: a control run of the
same scenario with the generate removed reads every seeded level and layer of a
2D array back, mip 2 and 3 included, through both glGetTexImage and a per-layer
FBO attachment - the two mismatches that control reported are the colours the
generate was supposed to write. GetTexImageThroughAViewReturnsOnlyItsOwnLayers
is new and pins the readback window with canary slices past the view's last
layer; it fails on the reverted readback and passes on DirectGLES too.
This commit is contained in:
@@ -25,6 +25,9 @@
|
||||
#include "MG_State/GLState/TextureState/TextureObject.h"
|
||||
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
|
||||
#include "MG_Impl/GLImpl/Texture/GL_Texture.h"
|
||||
// The generation window GenerateMipmap defines its chain in: one definition, shared with the
|
||||
// frontend arm and with the wire carrier that publishes it (MGPMipPlan::LevelCount).
|
||||
#include "MG_Impl/GLImpl/Texture/MipmapGenerationPlan.h"
|
||||
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
||||
// Only reached from an MGLOG_W, which the shipping INFO log level compiles out - so the
|
||||
// missing include never broke a default build and did break every WARN/DEBUG-level one.
|
||||
@@ -1573,22 +1576,20 @@ void main() {
|
||||
return size;
|
||||
}
|
||||
|
||||
static Uint32 ComputeFullMipLevelCountWithFixedComponents(const IntVec3& baseTexelSize,
|
||||
Int shrinkingComponents) {
|
||||
Int maxDimension = 1;
|
||||
for (Int component = 0; component < shrinkingComponents && component < 3; ++component) {
|
||||
maxDimension = std::max<Int>(maxDimension, baseTexelSize[component]);
|
||||
}
|
||||
Uint32 mipLevelCount = 1;
|
||||
while (maxDimension > 1) {
|
||||
maxDimension = std::max<Int>(maxDimension / 2, 1);
|
||||
++mipLevelCount;
|
||||
}
|
||||
return mipLevelCount;
|
||||
}
|
||||
|
||||
// glGenerateMipmap defines levels BASE_LEVEL+1 up to the level the base image's extent and
|
||||
// MAX_LEVEL admit, and leaves every other level exactly as it was (GL 4.6 core 8.17). That
|
||||
// window is a property of the GL call rather than of the backend, so the caller hands in
|
||||
// the frontend's own plan (MG_Impl/GLImpl/Texture/MipmapGenerationPlan.h) and this defines
|
||||
// exactly those levels - neither a chain derived here nor a truncation of the tail.
|
||||
//
|
||||
// IMMUTABLE storage - glTexStorage*'s chain, and ANY view, which GL 4.6 core 8.18 makes
|
||||
// immutable from birth - already owns every level generation can write, so none may be
|
||||
// redefined here. Allocating through a view is destructive in a way the view's own extents
|
||||
// cannot describe: its base extent is ONE slice of the storage's layers, so AllocateStorage
|
||||
// through a two-layer view of a four-layer array resizes the storage owner's level to two
|
||||
// layers and the storage behind its other two is gone.
|
||||
static Bool EnsureGenerateMipmapStorageAllocated(::MobileGL::MG_State::GLState::TextureObjectMipmap& texture,
|
||||
Uint32 baseMipLevel) {
|
||||
Uint32 baseMipLevel, Uint32 endMipLevel) {
|
||||
const Uint32 existingMipLevelCount = static_cast<Uint32>(texture.GetMipmapLevelCount());
|
||||
if (existingMipLevelCount <= baseMipLevel) {
|
||||
return false;
|
||||
@@ -1599,6 +1600,10 @@ void main() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (texture.IsImmutable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const Int shrinkingComponents = MipShrinkingComponentCount(texture.GetTarget());
|
||||
|
||||
for (const auto uploadTarget : uploadTargets) {
|
||||
@@ -1617,13 +1622,12 @@ void main() {
|
||||
}
|
||||
|
||||
const SizeT bytesPerTexel = baseByteSize / baseTexelCount;
|
||||
const Uint32 requiredMipLevelCount =
|
||||
baseMipLevel + ComputeFullMipLevelCountWithFixedComponents(baseTexelSize, shrinkingComponents);
|
||||
if (existingMipLevelCount >= requiredMipLevelCount) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (Uint32 level = existingMipLevelCount; level < requiredMipLevelCount; ++level) {
|
||||
// AllocateStorage RESIZES the level it is handed, so only levels the chain does not
|
||||
// have yet are allocated: the ones inside the window that already exist - the
|
||||
// levels a mutable chain defined by hand, or a previous generate built - keep the
|
||||
// shadow bytes their own upload path wrote.
|
||||
const Uint32 firstMissingMipLevel = std::max(baseMipLevel + 1, existingMipLevelCount);
|
||||
for (Uint32 level = firstMissingMipLevel; level < endMipLevel; ++level) {
|
||||
const IntVec3 levelTexelSize = ComputeMipTexelSizeWithFixedComponents(
|
||||
baseTexelSize, level - baseMipLevel, shrinkingComponents);
|
||||
const SizeT levelByteSize = bytesPerTexel * static_cast<SizeT>(levelTexelSize.x()) *
|
||||
@@ -11478,6 +11482,10 @@ void main() {
|
||||
const Int glCubeFaceLayer = isCubeFaceTarget
|
||||
? static_cast<Int>(textureUploadTarget) - static_cast<Int>(TextureUploadTarget::CubeMapPositiveX)
|
||||
: 0;
|
||||
// The storage-image layer a copy starts at: the view's layer origin plus, for the one shape
|
||||
// that names a face, the face the target token chose. Every copy below asks here, so the
|
||||
// depth branch and the colour path cannot disagree about which layer was read.
|
||||
const Uint32 baseArrayLayer = ToStorageArrayLayer(textureObject.get(), glCubeFaceLayer);
|
||||
|
||||
if ((resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
|
||||
if (format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL || format == GL_STENCIL_INDEX) {
|
||||
@@ -11485,7 +11493,7 @@ void main() {
|
||||
textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, static_cast<Uint>(level));
|
||||
// Storage space: `resource` is the storage texture's, so a view's level and
|
||||
// layer have to be shifted into its numbering (see ToStorageMipLevel).
|
||||
const Uint32 arrayLayer = ToStorageArrayLayer(textureObject.get(), glCubeFaceLayer);
|
||||
const Uint32 arrayLayer = baseArrayLayer;
|
||||
const Uint32 storageLevel = ToStorageMipLevel(textureObject.get(), level);
|
||||
// A 1D array's levelSize.y() is its LAYER count, and those layers are the rows
|
||||
// GL wants back - but in Vulkan they are array layers of a one-row image, not
|
||||
@@ -11520,7 +11528,21 @@ void main() {
|
||||
imageTextureTarget == TextureTarget::Texture2DArray ||
|
||||
imageTextureTarget == TextureTarget::TextureCubeMapArray;
|
||||
const GLsizei depthSlices = is3dImage ? std::max<GLsizei>(texelSize.z(), 1) : 1;
|
||||
const GLsizei arrayLayers = isArrayImage ? static_cast<GLsizei>(resource->arrayLayers) : 1;
|
||||
// How many layers of the CALLER's texture a copy has to cover. `texelSize` above is already
|
||||
// measured in the caller's space - a view's layer axis is the VIEW's, and the destination
|
||||
// size the frontend validates a by-name readback against is built from the same extents -
|
||||
// so the copy has to cover exactly those layers. resource->arrayLayers is the STORAGE
|
||||
// owner's count: reading that many layers through a layer-windowed view covers layers the
|
||||
// view does not name and, because the copy starts at the view's layer origin, runs past the
|
||||
// last layer the image has.
|
||||
const Uint32 storageArrayLayers = static_cast<Uint32>(resource->arrayLayers);
|
||||
const Uint32 layersFromOrigin =
|
||||
baseArrayLayer < storageArrayLayers ? storageArrayLayers - baseArrayLayer : 0u;
|
||||
const GLsizei arrayLayers = static_cast<GLsizei>(
|
||||
isArrayImage ? (textureObject->IsTextureView()
|
||||
? std::min(static_cast<Uint32>(textureObject->GetViewNumLayers()), layersFromOrigin)
|
||||
: storageArrayLayers)
|
||||
: 1u);
|
||||
// A 1D array level comes back as ONE two-dimensional image whose rows are its layers
|
||||
// (GL 4.6 core 8.11.4), so its layers are already counted by `height` above and must not
|
||||
// multiply the slice count the way a 2D-array's or a cube-array's do. Vulkan still keeps
|
||||
@@ -11580,10 +11602,11 @@ void main() {
|
||||
// Storage space, as above: a texture view reads its own level 0 out of whichever level
|
||||
// and layer of the parent it opened onto.
|
||||
copyRegion.imageSubresource.mipLevel = ToStorageMipLevel(textureObject.get(), level);
|
||||
// glCubeFaceLayer, not 0: the cube face the target token named (see above). Non-zero for
|
||||
// exactly one shape - a plain cube map read one face at a time - and layerCount is 1 there,
|
||||
// so the copy stays inside the six layers the image has.
|
||||
copyRegion.imageSubresource.baseArrayLayer = ToStorageArrayLayer(textureObject.get(), glCubeFaceLayer);
|
||||
// baseArrayLayer, not 0: the cube face the target token named, plus the view's layer origin
|
||||
// (see above). The face is non-zero for exactly one shape - a plain cube map read one face
|
||||
// at a time - and layerCount is 1 there, so the copy stays inside the six layers the image
|
||||
// has.
|
||||
copyRegion.imageSubresource.baseArrayLayer = baseArrayLayer;
|
||||
copyRegion.imageSubresource.layerCount = static_cast<Uint32>(arrayLayers);
|
||||
copyRegion.imageExtent = {static_cast<Uint32>(width),
|
||||
is1dArrayImage ? 1u : static_cast<Uint32>(height),
|
||||
@@ -11632,10 +11655,10 @@ void main() {
|
||||
//
|
||||
// Every ARRAY target - 1D array, 2D array, cube map array - needs no blit code of its own:
|
||||
// its layers live in the VkImage's arrayLayers, so resource->extent/depth already describe
|
||||
// one layer's image and the loop below already copies every layer per level via
|
||||
// srcSubresource.layerCount = resource->arrayLayers. The one thing they DO need is that the
|
||||
// GL-space storage allocation not shrink the layer count down the chain, which
|
||||
// MipShrinkingComponentCount handles.
|
||||
// one layer's image and the loop below already copies every layer of the window per level
|
||||
// via srcSubresource.layerCount. The one thing they DO need is that the GL-space storage
|
||||
// allocation not shrink the layer count down the chain, which MipShrinkingComponentCount
|
||||
// handles.
|
||||
if (textureTarget != TextureTarget::Texture2D && textureTarget != TextureTarget::Texture2DArray &&
|
||||
textureTarget != TextureTarget::Texture3D && textureTarget != TextureTarget::TextureCubeMap &&
|
||||
textureTarget != TextureTarget::TextureCubeMapArray &&
|
||||
@@ -11657,7 +11680,21 @@ void main() {
|
||||
const Uint32 currentMipLevelCount = static_cast<Uint32>(mipmapTexture->GetMipmapLevelCount());
|
||||
MOBILEGL_ASSERT(currentMipLevelCount > 0, "GenerateMipmap requires level 0 storage.");
|
||||
|
||||
const Uint32 baseMipLevel = std::min(static_cast<Uint32>(texture->GetLevelRange().x()), currentMipLevelCount - 1);
|
||||
const auto& uploadTargets = mipmapTexture->GetUploadTargets();
|
||||
MOBILEGL_ASSERT(!uploadTargets.empty(), "GenerateMipmap requires at least one upload target.");
|
||||
|
||||
// The generation window the call names, in the BOUND object's coordinates: BASE_LEVEL is
|
||||
// where generation starts, and it runs to the smaller of the base image's full chain and
|
||||
// MAX_LEVEL + 1 (GL 4.6 core 8.17). Two things make this the FRONTEND's plan rather than
|
||||
// an arithmetic of this function's own: that window is what the storage allocation below
|
||||
// has to cover, and through a view the numbers are the VIEW's own - BASE_LEVEL 1 of a view
|
||||
// whose MIN_LEVEL is 1 describes the STORAGE's level 2, and MAX_LEVEL 2 ends the chain at
|
||||
// the storage's level 3.
|
||||
const auto mipmapPlan = MG_Impl::GLImpl::ComputeMipmapGenerationRange(*mipmapTexture, uploadTargets.front());
|
||||
const Uint32 baseMipLevel = std::min(mipmapPlan.Base, currentMipLevelCount - 1);
|
||||
// A base level the storage does not reach leaves nothing to generate; the storage window
|
||||
// below collapses to a single level, which every consumer reads as a no-op.
|
||||
const Uint32 endMipLevel = std::max(mipmapPlan.End, baseMipLevel + 1);
|
||||
|
||||
// A texture that has only ever defined level 0 carries a single-level backing, so defining
|
||||
// the rest of the chain below recreates the image and carries the old contents over with a
|
||||
@@ -11715,10 +11752,11 @@ void main() {
|
||||
// level storage is never written; in monolith the client-object path runs unchanged.
|
||||
const Bool allocatedMipmapStorage =
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith
|
||||
? EnsureGenerateMipmapShadowAllocated(*resource, baseMipLevel, texture->GetUploadTargets())
|
||||
: EnsureGenerateMipmapStorageAllocated(*mipmapTexture, baseMipLevel);
|
||||
? EnsureGenerateMipmapShadowAllocated(*resource, baseMipLevel, uploadTargets)
|
||||
: EnsureGenerateMipmapStorageAllocated(*mipmapTexture, baseMipLevel, endMipLevel);
|
||||
#else
|
||||
const Bool allocatedMipmapStorage = EnsureGenerateMipmapStorageAllocated(*mipmapTexture, baseMipLevel);
|
||||
const Bool allocatedMipmapStorage =
|
||||
EnsureGenerateMipmapStorageAllocated(*mipmapTexture, baseMipLevel, endMipLevel);
|
||||
#endif
|
||||
MOBILEGL_ASSERT(allocatedMipmapStorage, "GenerateMipmap could not allocate a full mip chain for this texture.");
|
||||
|
||||
@@ -11735,19 +11773,43 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Every blit below runs in the STORAGE image's coordinates. A view owns no image of its
|
||||
// own (VkTextureManager::StorageTextureOf), so the window the application named through
|
||||
// the view - its BASE_LEVEL..MAX_LEVEL and its layer slice - has to land at the levels and
|
||||
// layers the view aliases, and everything outside that window belongs to the storage owner
|
||||
// and must not move (GL 4.6 core 8.18).
|
||||
const Uint32 storageBaseMipLevel = ToStorageMipLevel(texture.get(), baseMipLevel);
|
||||
const Uint32 storageEndMipLevel = ToStorageMipLevel(texture.get(), endMipLevel);
|
||||
|
||||
// The chain may also stop where the base image's extent stops: levels past 1x1 are not
|
||||
// generatable, and a chain the storage does not carry cannot be written.
|
||||
const IntVec3 storageBaseTexelSize = {
|
||||
static_cast<Int>(resource->extent.width),
|
||||
static_cast<Int>(resource->extent.height),
|
||||
static_cast<Int>(resource->depth),
|
||||
};
|
||||
const IntVec3 baseTexelSize = ComputeMipTexelSize(storageBaseTexelSize, baseMipLevel);
|
||||
const Uint32 requiredMipLevelCount = baseMipLevel + ComputeFullMipLevelCount(baseTexelSize);
|
||||
const Uint32 generateMipLevelCount = std::min(requiredMipLevelCount, resource->mipLevels);
|
||||
if (generateMipLevelCount <= baseMipLevel + 1) {
|
||||
const IntVec3 baseTexelSize = ComputeMipTexelSize(storageBaseTexelSize, storageBaseMipLevel);
|
||||
const Uint32 storageChainEndMipLevel = storageBaseMipLevel + ComputeFullMipLevelCount(baseTexelSize);
|
||||
const Uint32 generateMipLevelCount =
|
||||
std::min({storageEndMipLevel, storageChainEndMipLevel, static_cast<Uint32>(resource->mipLevels)});
|
||||
if (generateMipLevelCount <= storageBaseMipLevel + 1) {
|
||||
resource->layout = ResolveGenerateMipmapFinalLayout(resource->aspect);
|
||||
return;
|
||||
}
|
||||
|
||||
// A view writes ONLY the layers it names; the storage owner keeps every other one. A
|
||||
// texture that is not a view addresses the whole image, which is what layerCount
|
||||
// resource->arrayLayers already means.
|
||||
Uint32 baseArrayLayer = 0;
|
||||
Uint32 arrayLayerCount = static_cast<Uint32>(resource->arrayLayers);
|
||||
if (texture->IsTextureView()) {
|
||||
baseArrayLayer = std::min(ToStorageArrayLayer(texture.get(), 0), arrayLayerCount);
|
||||
arrayLayerCount = std::min(static_cast<Uint32>(texture->GetViewNumLayers()),
|
||||
static_cast<Uint32>(resource->arrayLayers) - baseArrayLayer);
|
||||
}
|
||||
MOBILEGL_ASSERT(arrayLayerCount > 0, "GenerateMipmap: empty layer window for textureId=%d",
|
||||
texture->GetExternalIndex());
|
||||
|
||||
const VkImageLayout originalLayout = resource->layout;
|
||||
const VkImageLayout finalLayout = ResolveGenerateMipmapFinalLayout(resource->aspect);
|
||||
if (isDepthOrStencilTexture && !supportsNativeBlit) {
|
||||
@@ -11762,7 +11824,7 @@ void main() {
|
||||
"GenerateMipmap: depth texture format %d lacks sampled/depth-attachment support for shader fallback.",
|
||||
static_cast<Int>(resource->format));
|
||||
const Bool depthReady = GenerateDepthMipmapWithShader(frame, *texture, *resource,
|
||||
baseMipLevel, generateMipLevelCount,
|
||||
storageBaseMipLevel, generateMipLevelCount,
|
||||
storageBaseTexelSize, originalLayout, finalLayout);
|
||||
MOBILEGL_ASSERT(depthReady,
|
||||
"GenerateMipmap: depth fallback failed for textureId=%d target=%d internalFormat=%d vkFormat=%d",
|
||||
@@ -11786,13 +11848,13 @@ void main() {
|
||||
GetImageTransitionDestinationState(finalLayout, finalDstStageMask, finalDstAccessMask);
|
||||
|
||||
if (originalLayout != finalLayout) {
|
||||
if (baseMipLevel > 0) {
|
||||
if (storageBaseMipLevel > 0) {
|
||||
VkImageLayout lowerMipLayout = originalLayout;
|
||||
const Bool lowerReady = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, resource->image, lowerMipLayout, finalLayout,
|
||||
originalSrcStageMask, finalDstStageMask,
|
||||
originalSrcAccessMask, finalDstAccessMask,
|
||||
resource->aspect, 0, baseMipLevel);
|
||||
resource->aspect, 0, storageBaseMipLevel);
|
||||
MOBILEGL_ASSERT(lowerReady, "%s: failed to transition lower untouched mip levels", __func__);
|
||||
}
|
||||
|
||||
@@ -11812,7 +11874,7 @@ void main() {
|
||||
frame.commandBuffer, resource->image, srcMipLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
originalSrcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
originalSrcAccessMask, VK_ACCESS_TRANSFER_READ_BIT,
|
||||
resource->aspect, baseMipLevel, 1);
|
||||
resource->aspect, storageBaseMipLevel, 1);
|
||||
MOBILEGL_ASSERT(srcReady, "%s: failed to transition base mip level to transfer source", __func__);
|
||||
|
||||
// Every generated level starts from originalLayout and ends up TRANSFER_DST_OPTIMAL, and
|
||||
@@ -11821,31 +11883,31 @@ void main() {
|
||||
// 12-level chain's 3(N-1)+1 barrier commands into 2(N-1)+2. Each level is still
|
||||
// individually transitioned to TRANSFER_SRC before it is read, so the write-then-read
|
||||
// dependency between consecutive levels is unchanged.
|
||||
if (generateMipLevelCount > baseMipLevel + 1) {
|
||||
if (generateMipLevelCount > storageBaseMipLevel + 1) {
|
||||
VkImageLayout dstRangeLayout = originalLayout;
|
||||
const Bool dstRangeReady = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, resource->image, dstRangeLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
originalSrcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
originalSrcAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
resource->aspect, baseMipLevel + 1, generateMipLevelCount - (baseMipLevel + 1));
|
||||
resource->aspect, storageBaseMipLevel + 1, generateMipLevelCount - (storageBaseMipLevel + 1));
|
||||
MOBILEGL_ASSERT(dstRangeReady, "%s: failed to transition mip levels to transfer destination", __func__);
|
||||
}
|
||||
|
||||
for (Uint32 level = baseMipLevel + 1; level < generateMipLevelCount; ++level) {
|
||||
for (Uint32 level = storageBaseMipLevel + 1; level < generateMipLevelCount; ++level) {
|
||||
const IntVec3 srcTexelSize = ComputeMipTexelSize(storageBaseTexelSize, level - 1);
|
||||
const IntVec3 dstTexelSize = ComputeMipTexelSize(storageBaseTexelSize, level);
|
||||
|
||||
VkImageBlit blitRegion{};
|
||||
blitRegion.srcSubresource.aspectMask = resource->aspect;
|
||||
blitRegion.srcSubresource.mipLevel = level - 1;
|
||||
blitRegion.srcSubresource.baseArrayLayer = 0;
|
||||
blitRegion.srcSubresource.layerCount = resource->arrayLayers;
|
||||
blitRegion.srcSubresource.baseArrayLayer = baseArrayLayer;
|
||||
blitRegion.srcSubresource.layerCount = arrayLayerCount;
|
||||
blitRegion.srcOffsets[0] = {0, 0, 0};
|
||||
blitRegion.srcOffsets[1] = {srcTexelSize.x(), srcTexelSize.y(), srcTexelSize.z()};
|
||||
blitRegion.dstSubresource.aspectMask = resource->aspect;
|
||||
blitRegion.dstSubresource.mipLevel = level;
|
||||
blitRegion.dstSubresource.baseArrayLayer = 0;
|
||||
blitRegion.dstSubresource.layerCount = resource->arrayLayers;
|
||||
blitRegion.dstSubresource.baseArrayLayer = baseArrayLayer;
|
||||
blitRegion.dstSubresource.layerCount = arrayLayerCount;
|
||||
blitRegion.dstOffsets[0] = {0, 0, 0};
|
||||
blitRegion.dstOffsets[1] = {dstTexelSize.x(), dstTexelSize.y(), dstTexelSize.z()};
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -921,20 +922,17 @@ void main() {
|
||||
// runs, and what the split-only F1WireScenario never reaches (its fixture skips under any
|
||||
// other transport, which is why its copy of this rule runs nowhere).
|
||||
//
|
||||
// DirectGLES ONLY, for two facts about the other backend and neither of them this arm's:
|
||||
// Magma reallocates the chain through the bound object in its OWN generate path
|
||||
// (DirectVulkan/Renderer/VulkanRenderer.cpp's EnsureGenerateMipmapStorageAllocated), which
|
||||
// through a view means its owner's levels - the same defect on the far side of the door this
|
||||
// fix closes; and a CPU sub-image into layer 1 or 2 of mip 2 or 3 does not read back through
|
||||
// Magma at all - a run with the generate removed fails identically - so the per-level colours
|
||||
// this case is built on are unobservable there rather than wrong.
|
||||
// BOTH BACKENDS. DirectGLES had the reallocating arm in the frontend it shares with Magma
|
||||
// (MG_Impl/GLImpl/Texture/GL_Texture.cpp), and it clips the generation to the view's LAYER
|
||||
// window itself since 73b37bbe. Magma has its own generate path
|
||||
// (DirectVulkan/Renderer/VulkanRenderer.cpp's GenerateMipmap) - it allocates through the
|
||||
// BOUND object, so through a view it resized the owner's levels to the view's layer count
|
||||
// and regenerated the owner's levels outside the view's window as well. It now takes the
|
||||
// same frontend plan for the window and translates it into the storage image's own level
|
||||
// and layer numbering before it blits anything.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, GenerateMipmapThroughAViewLeavesTheOwnerShapeAlone) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
if (Gl().BackendName() != "DirectGLES") {
|
||||
GTEST_SKIP() << "the DirectGLES monolith arm is the one that reallocated through the "
|
||||
"view; Magma does its own reallocation there and reads no such sub-image";
|
||||
}
|
||||
|
||||
constexpr int kLevels = 5;
|
||||
constexpr int kBase = 16;
|
||||
@@ -1040,5 +1038,88 @@ void main() {
|
||||
}
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// glGetTexImage THROUGH a layer-windowed view returns the VIEW's image.
|
||||
//
|
||||
// THE DEFECT this pins. Magma answers a readback out of the storage OWNER's image, and its
|
||||
// copy was sized by that image's array layer count while being started at the view's layer
|
||||
// origin. A two-layer view of a four-layer array therefore copied four layers: two the view
|
||||
// does not name, and the fourth past the end of the image. Nothing stopped it, because
|
||||
// glGetTexImage carries no destination size - while the size glGetTextureImage validates is
|
||||
// built from the VIEW's own extents. A caller that sized its buffer for the view (the only
|
||||
// size that definition offers) had it overrun by exactly the layers outside the window.
|
||||
//
|
||||
// The canary slices are the assertion that matters: they are past the view's last layer and
|
||||
// inside the caller's allocation, so a copy that covers the storage's layers writes them.
|
||||
// ------------------------------------------------------------------------------------
|
||||
TEST_F(TextureViewScenario, GetTexImageThroughAViewReturnsOnlyItsOwnLayers) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
constexpr int kLevels = 3;
|
||||
constexpr int kBase = 8;
|
||||
constexpr int kLayers = 4;
|
||||
constexpr int kViewMinLayer = 1;
|
||||
constexpr int kViewNumLayers = 2;
|
||||
constexpr std::uint8_t kCanaryByte = 0xAB;
|
||||
// One colour per storage layer, so a lost layer origin and a read past the window are
|
||||
// both visible as another layer's colour rather than as a plausible one.
|
||||
const auto layerColour = [](int layer) {
|
||||
return Rgba8{static_cast<std::uint8_t>(10 * (layer + 1)), 40, 7, 255};
|
||||
};
|
||||
const Rgba8 canary{kCanaryByte, kCanaryByte, kCanaryByte, kCanaryByte};
|
||||
|
||||
const GLuint storage = MakeTexture();
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, storage);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, kLevels, GL_RGBA8, kBase, kBase, kLayers);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
for (int level = 0; level < kLevels; ++level) {
|
||||
const int side = kBase >> level;
|
||||
std::vector<std::uint8_t> texels(static_cast<std::size_t>(side) * side * kLayers * 4);
|
||||
for (int layer = 0; layer < kLayers; ++layer) {
|
||||
for (int t = 0; t < side * side; ++t) {
|
||||
std::uint8_t* p =
|
||||
texels.data() + (static_cast<std::size_t>(layer) * side * side + t) * 4;
|
||||
const Rgba8 colour = layerColour(layer);
|
||||
p[0] = colour.r;
|
||||
p[1] = colour.g;
|
||||
p[2] = colour.b;
|
||||
p[3] = colour.a;
|
||||
}
|
||||
}
|
||||
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, level, 0, 0, 0, side, side, kLayers, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, texels.data());
|
||||
}
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the owner raised an error";
|
||||
|
||||
const GLuint view = MakeTexture();
|
||||
glTextureView(view, GL_TEXTURE_2D_ARRAY, storage, GL_RGBA8, 0, kLevels, kViewMinLayer,
|
||||
kViewNumLayers);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, view);
|
||||
|
||||
const int side = kBase >> 2;
|
||||
// Exactly what a caller sizes for the view, plus two canary slices.
|
||||
std::vector<std::uint8_t> dst(static_cast<std::size_t>(side) * side * 4 *
|
||||
(kViewNumLayers + 2),
|
||||
kCanaryByte);
|
||||
glGetTexImage(GL_TEXTURE_2D_ARRAY, 2, GL_RGBA, GL_UNSIGNED_BYTE, dst.data());
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< "glGetTexImage through the view raised an error";
|
||||
|
||||
for (int slice = 0; slice <= kViewNumLayers + 1; ++slice) {
|
||||
Image image(side, side);
|
||||
std::memcpy(image.Data(), dst.data() + static_cast<std::size_t>(slice) * side * side * 4,
|
||||
static_cast<std::size_t>(side) * side * 4);
|
||||
const bool inWindow = slice < kViewNumLayers;
|
||||
const Rgba8 expected = inWindow ? layerColour(kViewMinLayer + slice) : canary;
|
||||
const std::string what =
|
||||
inWindow ? "the view's slice " + std::to_string(slice) + ", the owner's layer " +
|
||||
std::to_string(kViewMinLayer + slice)
|
||||
: "slice " + std::to_string(slice) +
|
||||
", past the view's last layer: the readback must not write it";
|
||||
ExpectRegion(image, 0, side - 1, 0, side - 1, expected, 2, what.c_str());
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
|
||||
Reference in New Issue
Block a user