mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
[Fix, Test] (MG_Backend/DirectVulkan, MG_IntegrationTest): map a layered CopyImageSubData onto the axis each endpoint keeps its slices on, instead of copying slice 0 and calling it done
This commit is contained in:
@@ -8530,24 +8530,106 @@ void main() {
|
||||
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// GL hands CopyImageSubData ONE z/depth pair and lets the texture target decide what it
|
||||
// means. Vulkan splits that meaning across two different fields of VkImageCopy, chosen by
|
||||
// the image type:
|
||||
//
|
||||
// VK_IMAGE_TYPE_3D - slices live on the z axis: srcOffset.z/dstOffset.z select them and
|
||||
// extent.depth counts them. The subresource layer range must stay
|
||||
// (0, 1): Vulkan reads a 3D image as a single layer whose depth is
|
||||
// the mip level's depth (VUID-VkImageCopy-apiVersion-07932/-07933).
|
||||
// everything else - slices live in the array dimension: baseArrayLayer selects them and
|
||||
// layerCount counts them, while offset.z stays 0 and (when neither
|
||||
// endpoint is 3D) extent.depth stays 1.
|
||||
//
|
||||
// A mixed 2D-array <-> 3D pair is legal because maintenance1 - core since Vulkan 1.1 -
|
||||
// relaxed the old "layerCounts must match" rule into "the 3D side's extent.depth must
|
||||
// equal the array side's layerCount".
|
||||
struct CopyImageEndpoint {
|
||||
// True for a VK_IMAGE_TYPE_3D image, i.e. slices ride the z axis, not the layer axis.
|
||||
Bool slicesAreDepth = false;
|
||||
// The GL z offset, kept in whichever field this endpoint's image type reads it from.
|
||||
Uint32 baseSlice = 0;
|
||||
// Slices this endpoint can address at the selected mip level; the copy range check
|
||||
// needs the level's depth for a 3D image (3D mips shrink in z) and the image's array
|
||||
// size for a layered one (array layers do not shrink).
|
||||
Uint32 availableSlices = 1;
|
||||
|
||||
Uint32 BaseArrayLayer() const { return slicesAreDepth ? 0u : baseSlice; }
|
||||
Int32 OffsetZ() const { return slicesAreDepth ? static_cast<Int32>(baseSlice) : 0; }
|
||||
};
|
||||
|
||||
Bool TryResolveCopyImageEndpoint(TextureTarget target,
|
||||
const VkTextureManager::TextureResource& resource, Uint32 mipLevel,
|
||||
GLint glZ, GLsizei glDepth, CopyImageEndpoint& outEndpoint) {
|
||||
if (glZ < 0 || glDepth <= 0) {
|
||||
return false;
|
||||
}
|
||||
const Uint32 baseSlice = static_cast<Uint32>(glZ);
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
case TextureTarget::Texture2D:
|
||||
case TextureTarget::TextureRectangle:
|
||||
case TextureTarget::Texture2DMultisample:
|
||||
// Not layered at all: GL still requires the z/depth pair, and it can only name the
|
||||
// one slice these targets have.
|
||||
outEndpoint = {};
|
||||
return baseSlice == 0 && glDepth == 1;
|
||||
case TextureTarget::Texture3D:
|
||||
outEndpoint.slicesAreDepth = true;
|
||||
outEndpoint.baseSlice = baseSlice;
|
||||
outEndpoint.availableSlices = std::max(1u, resource.depth >> mipLevel);
|
||||
return true;
|
||||
case TextureTarget::Texture2DArray:
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
case TextureTarget::TextureCubeMap:
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
// A cube map is an array of six faces here (see TryResolveTextureShapeInfo), and GL
|
||||
// numbers its faces on the same z axis an array texture numbers its layers, so both
|
||||
// arrive as a plain layer range.
|
||||
outEndpoint.slicesAreDepth = false;
|
||||
outEndpoint.baseSlice = baseSlice;
|
||||
outEndpoint.availableSlices = resource.arrayLayers;
|
||||
return true;
|
||||
default:
|
||||
// GL_TEXTURE_1D_ARRAY carries its layers on the Y axis (srcY/srcHeight), which
|
||||
// would have to be remapped against a Vulkan extent that also has to stay height 1
|
||||
// for a VK_IMAGE_TYPE_1D image; GL_TEXTURE_BUFFER has no image at all. Declined
|
||||
// rather than mis-addressed.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void VulkanRenderer::CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
MOBILEGL_ASSERT(srcWidth > 0 && srcHeight > 0 && srcDepth > 0,
|
||||
"CopyImageSubData requires positive copy dimensions.");
|
||||
MOBILEGL_ASSERT(srcTexture != nullptr && dstTexture != nullptr,
|
||||
"CopyImageSubData requires valid source and destination textures.");
|
||||
// The frontend already declines a zero or negative extent, so anything else here is a
|
||||
// caller MobileGL wrote - but it still reaches vkCmdCopyImage in a release build, and a
|
||||
// zero extent.depth is as invalid as a zero width.
|
||||
if (srcWidth <= 0 || srcHeight <= 0 || srcDepth <= 0) {
|
||||
MGLOG_E_ONCE("%s: non-positive copy extent %dx%dx%d; declining the copy", __func__, srcWidth, srcHeight,
|
||||
srcDepth);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
|
||||
const auto dstTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(dstTarget);
|
||||
MOBILEGL_ASSERT(srcTextureTarget == TextureTarget::Texture2D && dstTextureTarget == TextureTarget::Texture2D,
|
||||
"CopyImageSubData currently only supports GL_TEXTURE_2D sources and destinations.");
|
||||
MOBILEGL_ASSERT(srcDepth == 1 && srcZ == 0 && dstZ == 0,
|
||||
"CopyImageSubData currently only supports single-layer 2D copies.");
|
||||
MOBILEGL_ASSERT(srcTexture.get() != dstTexture.get(),
|
||||
"CopyImageSubData does not support in-place texture copies yet.");
|
||||
// Both endpoints of a same-image copy would have to share one VkImageLayout, so the
|
||||
// TRANSFER_SRC/TRANSFER_DST pair below cannot express it (it needs VK_IMAGE_LAYOUT_GENERAL
|
||||
// and an overlap check). Refused outright, and refused for real rather than through an
|
||||
// assertion the release build drops: recording the pair anyway is a validation error and,
|
||||
// on a tiler, a copy whose source has already been overwritten.
|
||||
if (srcTexture.get() == dstTexture.get()) {
|
||||
MGLOG_E_ONCE("%s: in-place copy on textureId=%d is not supported; declining the copy", __func__,
|
||||
srcTexture->GetExternalIndex());
|
||||
return;
|
||||
}
|
||||
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
if (!frame.isCommandRecording) {
|
||||
@@ -8616,29 +8698,89 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// The supported envelope, replacing the "GL_TEXTURE_2D only" assertion that used to stand
|
||||
// here: every target whose slices this function can address on one of the two Vulkan axes.
|
||||
// A refusal has to be a real decline, not an assertion - the assertion compiled to nothing
|
||||
// in a release build and the unsupported shape reached vkCmdCopyImage anyway.
|
||||
CopyImageEndpoint srcEndpoint;
|
||||
CopyImageEndpoint dstEndpoint;
|
||||
if (!TryResolveCopyImageEndpoint(srcTextureTarget, *srcResource, srcMipLevel, srcZ, srcDepth, srcEndpoint) ||
|
||||
!TryResolveCopyImageEndpoint(dstTextureTarget, *dstResource, dstMipLevel, dstZ, srcDepth, dstEndpoint)) {
|
||||
MGLOG_E_ONCE("%s: unsupported target pair src=%s dst=%s (srcZ=%d dstZ=%d depth=%d); declining the copy",
|
||||
__func__, MG_Util::ConvertTextureTargetToString(srcTextureTarget).c_str(),
|
||||
MG_Util::ConvertTextureTargetToString(dstTextureTarget).c_str(), srcZ, dstZ, srcDepth);
|
||||
return;
|
||||
}
|
||||
// The slice half of the region-bounds guard above. A layered endpoint's bound is NOT the
|
||||
// mip-0 2D extent: an array texture is bounded by its layer count (which no mip level
|
||||
// shrinks) and a 3D texture by the selected level's depth (which every level halves), so
|
||||
// both come from the endpoint that resolved them.
|
||||
const Uint32 copySliceCount = static_cast<Uint32>(srcDepth);
|
||||
if (srcEndpoint.baseSlice + copySliceCount > srcEndpoint.availableSlices ||
|
||||
dstEndpoint.baseSlice + copySliceCount > dstEndpoint.availableSlices) {
|
||||
MGLOG_E_ONCE("%s: slice range outside image bounds (srcZ=%d of %u, dstZ=%d of %u, depth=%d); "
|
||||
"declining the copy",
|
||||
__func__, srcZ, srcEndpoint.availableSlices, dstZ, dstEndpoint.availableSlices, srcDepth);
|
||||
return;
|
||||
}
|
||||
|
||||
const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *srcTexture);
|
||||
MOBILEGL_ASSERT(clearReady, "%s: failed to materialize pending clear for source textureId=%d",
|
||||
__func__, srcTexture->GetExternalIndex());
|
||||
// A clear still parked on the destination would otherwise materialize AFTER this copy and
|
||||
// wipe the texels it just wrote.
|
||||
const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *dstTexture);
|
||||
MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination textureId=%d",
|
||||
__func__, dstTexture->GetExternalIndex());
|
||||
|
||||
const VkImageLayout srcOriginalLayout = srcResource->layout;
|
||||
const VkImageLayout dstOriginalLayout = dstResource->layout;
|
||||
MOBILEGL_ASSERT(srcOriginalLayout != VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
"CopyImageSubData source image has undefined layout.");
|
||||
const VkImageLayout dstRestoreLayout = dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED
|
||||
? ((copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0
|
||||
// A layout of UNDEFINED means nothing has ever been written to the image, which on the
|
||||
// SOURCE side is glTexStorage without an upload: legal GL, and the texels it copies are
|
||||
// undefined by the same spec sentence that lets the application ask. Both sides therefore
|
||||
// take the same shape - transition the whole image out of UNDEFINED and settle it on a
|
||||
// real layout afterwards, since UNDEFINED is not a layout a barrier may transition BACK to.
|
||||
const auto resolveRestoreLayout = [copyAspectMask](VkImageLayout originalLayout) {
|
||||
if (originalLayout != VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
return originalLayout;
|
||||
}
|
||||
return (copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0
|
||||
? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
|
||||
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
||||
: dstOriginalLayout;
|
||||
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
};
|
||||
const VkImageLayout srcRestoreLayout = resolveRestoreLayout(srcOriginalLayout);
|
||||
const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout);
|
||||
|
||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags srcAccessMask = 0;
|
||||
GetImageTransitionSourceState(srcOriginalLayout, srcStageMask, srcAccessMask);
|
||||
VkImageLayout srcCopyLayout = srcOriginalLayout;
|
||||
Bool srcReady = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, copyAspectMask, srcMipLevel, 1);
|
||||
MOBILEGL_ASSERT(srcReady, "%s: failed to transition source image", __func__);
|
||||
// The barrier has to name every layer the copy touches, not just layer 0 - otherwise the
|
||||
// slice fix above lands the copy on layers the barrier never transitioned, which is the
|
||||
// same defect one level down. TransitionImageLayout always starts its range at
|
||||
// baseArrayLayer 0, so VK_REMAINING_ARRAY_LAYERS is the whole range and a superset of
|
||||
// [baseSlice, baseSlice + depth).
|
||||
//
|
||||
// Not `arrayLayers`, which is 1 for a 3D image: MobileGL creates 3D images
|
||||
// 2D_ARRAY_COMPATIBLE, and a literal 1 on one of those means "every depth slice" today but
|
||||
// "depth slice 0" once VK_KHR_maintenance9 is enabled - i.e. it would silently become a
|
||||
// single-slice barrier again on a newer driver. The validation layer says so by name.
|
||||
static constexpr Uint32 kAllLayers = VK_REMAINING_ARRAY_LAYERS;
|
||||
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
Bool srcReady = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcResource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT,
|
||||
srcResource->aspect, 0, srcResource->mipLevels, kAllLayers);
|
||||
MOBILEGL_ASSERT(srcReady, "%s: failed to transition undefined source image", __func__);
|
||||
srcCopyLayout = srcResource->layout;
|
||||
} else {
|
||||
Bool srcReady = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, copyAspectMask, srcMipLevel, 1, kAllLayers);
|
||||
MOBILEGL_ASSERT(srcReady, "%s: failed to transition source image", __func__);
|
||||
}
|
||||
|
||||
VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags dstAccessMask = 0;
|
||||
@@ -8649,29 +8791,42 @@ void main() {
|
||||
frame.commandBuffer, dstResource->image, dstResource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
dstResource->aspect, 0, dstResource->mipLevels, dstResource->arrayLayers);
|
||||
dstResource->aspect, 0, dstResource->mipLevels, kAllLayers);
|
||||
MOBILEGL_ASSERT(dstReady, "%s: failed to transition undefined destination image", __func__);
|
||||
dstCopyLayout = dstResource->layout;
|
||||
} else {
|
||||
Bool dstReady = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, dstResource->image, dstCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, copyAspectMask, dstMipLevel, 1);
|
||||
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, copyAspectMask, dstMipLevel, 1, kAllLayers);
|
||||
MOBILEGL_ASSERT(dstReady, "%s: failed to transition destination image", __func__);
|
||||
}
|
||||
|
||||
// The GL slice count reaches Vulkan on the layer axis of whichever endpoint is NOT 3D, and
|
||||
// on extent.depth as soon as either endpoint IS: a 3D image's subresource is always the
|
||||
// single layer (0, 1) and its slices are counted by the depth of the copy extent. With two
|
||||
// non-3D endpoints both layer counts carry it and extent.depth stays 1.
|
||||
const Bool copyCrossesDepthAxis = srcEndpoint.slicesAreDepth || dstEndpoint.slicesAreDepth;
|
||||
VkImageCopy copyRegion{};
|
||||
copyRegion.srcSubresource.aspectMask = copyAspectMask;
|
||||
copyRegion.srcSubresource.mipLevel = srcMipLevel;
|
||||
copyRegion.srcSubresource.baseArrayLayer = 0;
|
||||
copyRegion.srcSubresource.layerCount = 1;
|
||||
copyRegion.srcOffset = {srcX, srcY, 0};
|
||||
copyRegion.srcSubresource.baseArrayLayer = srcEndpoint.BaseArrayLayer();
|
||||
copyRegion.srcSubresource.layerCount = srcEndpoint.slicesAreDepth ? 1u : copySliceCount;
|
||||
copyRegion.srcOffset = {srcX, srcY, srcEndpoint.OffsetZ()};
|
||||
copyRegion.dstSubresource.aspectMask = copyAspectMask;
|
||||
copyRegion.dstSubresource.mipLevel = dstMipLevel;
|
||||
copyRegion.dstSubresource.baseArrayLayer = 0;
|
||||
copyRegion.dstSubresource.layerCount = 1;
|
||||
copyRegion.dstOffset = {dstX, dstY, 0};
|
||||
copyRegion.extent = {static_cast<Uint32>(srcWidth), static_cast<Uint32>(srcHeight), 1};
|
||||
copyRegion.dstSubresource.baseArrayLayer = dstEndpoint.BaseArrayLayer();
|
||||
copyRegion.dstSubresource.layerCount = dstEndpoint.slicesAreDepth ? 1u : copySliceCount;
|
||||
copyRegion.dstOffset = {dstX, dstY, dstEndpoint.OffsetZ()};
|
||||
copyRegion.extent = {static_cast<Uint32>(srcWidth), static_cast<Uint32>(srcHeight),
|
||||
copyCrossesDepthAxis ? copySliceCount : 1u};
|
||||
MGLOG_D("CopyImageSubData: src(target=%s level=%u layer=%u+%u z=%d) -> dst(target=%s level=%u layer=%u+%u "
|
||||
"z=%d) extent=[%d x %d x %u]",
|
||||
MG_Util::ConvertTextureTargetToString(srcTextureTarget).c_str(), srcMipLevel,
|
||||
copyRegion.srcSubresource.baseArrayLayer, copyRegion.srcSubresource.layerCount,
|
||||
copyRegion.srcOffset.z, MG_Util::ConvertTextureTargetToString(dstTextureTarget).c_str(), dstMipLevel,
|
||||
copyRegion.dstSubresource.baseArrayLayer, copyRegion.dstSubresource.layerCount,
|
||||
copyRegion.dstOffset.z, srcWidth, srcHeight, copyRegion.extent.depth);
|
||||
vkCmdCopyImage(frame.commandBuffer,
|
||||
srcResource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
dstResource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
@@ -8679,12 +8834,21 @@ void main() {
|
||||
|
||||
VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags srcRestoreAccessMask = 0;
|
||||
GetImageTransitionDestinationState(srcOriginalLayout, srcRestoreStageMask, srcRestoreAccessMask);
|
||||
Bool srcRestored = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcCopyLayout, srcOriginalLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
|
||||
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, copyAspectMask, srcMipLevel, 1);
|
||||
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore source image layout", __func__);
|
||||
GetImageTransitionDestinationState(srcRestoreLayout, srcRestoreStageMask, srcRestoreAccessMask);
|
||||
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
Bool srcRestored = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcResource->layout, srcRestoreLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
|
||||
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask,
|
||||
srcResource->aspect, 0, srcResource->mipLevels, kAllLayers);
|
||||
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore undefined source image layout", __func__);
|
||||
} else {
|
||||
Bool srcRestored = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcCopyLayout, srcRestoreLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
|
||||
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, copyAspectMask, srcMipLevel, 1, kAllLayers);
|
||||
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore source image layout", __func__);
|
||||
}
|
||||
|
||||
VkPipelineStageFlags dstRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags dstRestoreAccessMask = 0;
|
||||
@@ -8694,13 +8858,13 @@ void main() {
|
||||
frame.commandBuffer, dstResource->image, dstResource->layout, dstRestoreLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask,
|
||||
dstResource->aspect, 0, dstResource->mipLevels, dstResource->arrayLayers);
|
||||
dstResource->aspect, 0, dstResource->mipLevels, kAllLayers);
|
||||
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore undefined destination image layout", __func__);
|
||||
} else {
|
||||
Bool dstRestored = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, dstResource->image, dstCopyLayout, dstRestoreLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1);
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1, kAllLayers);
|
||||
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__);
|
||||
}
|
||||
|
||||
@@ -9661,11 +9825,14 @@ void main() {
|
||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags srcAccessMask = 0;
|
||||
GetImageTransitionSourceState(originalLayout, srcStageMask, srcAccessMask);
|
||||
// The copy below reads EVERY layer of the level, so the barrier has to name every layer
|
||||
// too; a layerCount of 1 left an array texture's layers 1.. in whatever layout they were
|
||||
// last left in while the transfer read them.
|
||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, resource->aspect,
|
||||
static_cast<Uint32>(level), 1);
|
||||
static_cast<Uint32>(level), 1, VK_REMAINING_ARRAY_LAYERS);
|
||||
MOBILEGL_ASSERT(ok, "%s: failed to transition texture image", __func__);
|
||||
|
||||
VkBufferImageCopy copyRegion{};
|
||||
@@ -9685,7 +9852,7 @@ void main() {
|
||||
frame.commandBuffer, resource->image, resource->layout, originalLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, restoreStageMask,
|
||||
VK_ACCESS_TRANSFER_READ_BIT, restoreAccessMask, resource->aspect,
|
||||
static_cast<Uint32>(level), 1);
|
||||
static_cast<Uint32>(level), 1, VK_REMAINING_ARRAY_LAYERS);
|
||||
MOBILEGL_ASSERT(ok, "%s: failed to restore texture image layout", __func__);
|
||||
|
||||
if (!SubmitReadbackCommandsAndWait(frame)) {
|
||||
|
||||
@@ -79,6 +79,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/XfbCaptureBufferReuseScenario.cpp
|
||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||
Scenarios/CopyImageLayeredScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImageLayeredScenario.cpp
|
||||
// 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
|
||||
//
|
||||
// Scenario - glCopyImageSubData MOVES EVERY SLICE IT WAS ASKED FOR, NOT JUST SLICE 0.
|
||||
//
|
||||
// KHR-GL43.copy_image.functional_* copies a whole 12-layer region in one call whenever both
|
||||
// endpoints are layered, i.e. for the four target pairs 2d_array->2d_array, 2d_array->3d,
|
||||
// 3d->2d_array and 3d->3d. DirectVulkan built its VkImageCopy with baseArrayLayer 0, layerCount 1
|
||||
// and srcOffset.z 0 no matter what the call asked for, so slice 0 landed correctly and slices 1..N
|
||||
// were never written - 64 conformance cases (16 compatible format pairs x those 4 pairs) failing
|
||||
// with "first mismatch at [x, y, 1]", the first texel of the first slice the copy skipped.
|
||||
//
|
||||
// The reason one hardcode covered both shapes wrongly is that GL states a layered copy ONE way -
|
||||
// srcZ/dstZ and srcDepth - while Vulkan states it two ways and picks by image type:
|
||||
//
|
||||
// GL_TEXTURE_3D -> VK_IMAGE_TYPE_3D: slices are z, so srcOffset.z/dstOffset.z select them
|
||||
// and extent.depth counts them; the layer range must stay (0, 1).
|
||||
// GL_TEXTURE_2D_ARRAY -> VK_IMAGE_TYPE_2D: slices are array layers, so baseArrayLayer selects
|
||||
// them and layerCount counts them; offset.z stays 0.
|
||||
//
|
||||
// A mixed pair is legal (maintenance1, core in Vulkan 1.1) but only when the counts correspond:
|
||||
// the 3D side's extent.depth has to equal the array side's layerCount. So the four pairs below are
|
||||
// four DIFFERENT VkImageCopy shapes, not one shape with different arguments, which is why one
|
||||
// scenario per pair is the coverage that matters here.
|
||||
//
|
||||
// Every case also asserts the slices OUTSIDE the copied range still hold their fill. A backend
|
||||
// that "fixed" the miss by copying the whole image regardless of srcZ/srcDepth would pass a
|
||||
// slices-landed check and fail this one.
|
||||
//
|
||||
// The verification path is an FBO attachment per slice plus glReadPixels, not glGetTexImage: it is
|
||||
// the readback both backends share, and glFramebufferTextureLayer names an array layer and a 3D
|
||||
// slice through the same call, so the two texture kinds are read back identically.
|
||||
//
|
||||
// DirectGLES is the control - it forwards to the driver's own glCopyImageSubData - so a failure on
|
||||
// both backends means the scenario is wrong, and a failure on DirectVulkan alone means Magma is.
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kWidth = 4;
|
||||
constexpr int kHeight = 4;
|
||||
// Six is enough for a copy that starts and ends away from both edges of both endpoints
|
||||
// while still leaving untouched slices on either side to assert against.
|
||||
constexpr int kSlices = 6;
|
||||
|
||||
struct Rgba8 {
|
||||
GLubyte r = 0, g = 0, b = 0, a = 0;
|
||||
|
||||
bool operator==(const Rgba8& other) const {
|
||||
return r == other.r && g == other.g && b == other.b && a == other.a;
|
||||
}
|
||||
};
|
||||
|
||||
std::string Describe(const Rgba8& color) {
|
||||
return "(" + std::to_string(color.r) + ", " + std::to_string(color.g) + ", " + std::to_string(color.b) +
|
||||
", " + std::to_string(color.a) + ")";
|
||||
}
|
||||
|
||||
// Per-slice constants, uniform within a slice. A uniform fill is deliberate: the defect is
|
||||
// in which SLICE the copy addresses, and a value that also varied within the slice would
|
||||
// make the assertions depend on the framebuffer row order as well.
|
||||
Rgba8 SourceColor(int slice) {
|
||||
return {static_cast<GLubyte>(10 + slice * 20), static_cast<GLubyte>(40 + slice * 10),
|
||||
static_cast<GLubyte>(200 - slice * 15), 255};
|
||||
}
|
||||
|
||||
Rgba8 DestinationFill(int slice) {
|
||||
return {static_cast<GLubyte>(3 + slice), static_cast<GLubyte>(250 - slice * 7),
|
||||
static_cast<GLubyte>(120 + slice * 5), 255};
|
||||
}
|
||||
|
||||
class CopyImageLayeredScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
if (!CopyImageSubDataUsable()) {
|
||||
GTEST_SKIP() << "glCopyImageSubData is unavailable on backend " << Gl().BackendName();
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
for (const GLuint texture : m_textures) {
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
m_textures.clear();
|
||||
if (m_fbo != 0) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &m_fbo);
|
||||
m_fbo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// A trivial 1x1x1 array-to-array copy: it exercises the entry point without depending
|
||||
// on any of the behaviour under test, so a driver (or a backend function table) that
|
||||
// simply does not have the call skips instead of failing every case below.
|
||||
bool CopyImageSubDataUsable() {
|
||||
GLuint probe[2] = {0, 0};
|
||||
glGenTextures(2, probe);
|
||||
for (const GLuint texture : probe) {
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 1, 1, 1);
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
glCopyImageSubData(probe[0], GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, probe[1], GL_TEXTURE_2D_ARRAY, 0, 0, 0,
|
||||
0, 1, 1, 1);
|
||||
const bool usable = glGetError() == GL_NO_ERROR;
|
||||
glDeleteTextures(2, probe);
|
||||
return usable;
|
||||
}
|
||||
|
||||
// `target` is GL_TEXTURE_2D_ARRAY or GL_TEXTURE_3D; both take glTexStorage3D and
|
||||
// glTexSubImage3D with the slice on the same axis, which is the whole reason GL can
|
||||
// copy between them. `levels` > 1 puts a real mip chain behind the level the copy
|
||||
// names, so the level's own extent - a 3D level's depth included - has to be resolved
|
||||
// rather than assumed to be the image's.
|
||||
GLuint MakeTexture(GLenum target, int levels, Rgba8 (*colorForSlice)(int)) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(target, texture);
|
||||
glTexStorage3D(target, levels, GL_RGBA8, kWidth << (levels - 1), kHeight << (levels - 1),
|
||||
target == GL_TEXTURE_3D ? (kSlices << (levels - 1)) : kSlices);
|
||||
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
|
||||
// Fill every level, so nothing below can pass by reading a level that was never
|
||||
// written and happened to hold the expected bytes.
|
||||
for (int level = 0; level < levels; ++level) {
|
||||
const int levelWidth = kWidth << (levels - 1 - level);
|
||||
const int levelHeight = kHeight << (levels - 1 - level);
|
||||
const int levelSlices =
|
||||
target == GL_TEXTURE_3D ? (kSlices << (levels - 1 - level)) : kSlices;
|
||||
for (int slice = 0; slice < levelSlices; ++slice) {
|
||||
const Rgba8 color = colorForSlice(slice % kSlices);
|
||||
std::vector<Rgba8> texels(static_cast<size_t>(levelWidth) * levelHeight, color);
|
||||
glTexSubImage3D(target, level, 0, 0, slice, levelWidth, levelHeight, 1, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, texels.data());
|
||||
}
|
||||
}
|
||||
glBindTexture(target, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// One slice of one level, through an FBO attachment. glFramebufferTextureLayer takes an
|
||||
// array layer and a 3D slice through the same argument, so both targets read back the
|
||||
// same way.
|
||||
Rgba8 ReadSlice(GLuint texture, int level, int slice, int width, int height) {
|
||||
if (m_fbo == 0) {
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, level, slice);
|
||||
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "slice " << slice << " of level " << level << " is not attachable";
|
||||
std::vector<Rgba8> pixels(static_cast<size_t>(width) * height, Rgba8{});
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
// The fill is uniform within a slice, so any disagreement between texels is itself
|
||||
// a failure - reported here rather than silently reduced to pixels[0].
|
||||
for (size_t i = 1; i < pixels.size(); ++i) {
|
||||
EXPECT_TRUE(pixels[i] == pixels[0])
|
||||
<< "slice " << slice << " of level " << level << " is not uniform: texel 0 is "
|
||||
<< Describe(pixels[0]) << ", texel " << i << " is " << Describe(pixels[i]);
|
||||
}
|
||||
return pixels[0];
|
||||
}
|
||||
|
||||
// The assertion every case ends with: slices inside [dstZ, dstZ + depth) hold the
|
||||
// source slice they were fed, and every slice outside it still holds its own fill.
|
||||
void ExpectCopied(GLuint destination, int level, int width, int height, int sliceCount, int srcZ,
|
||||
int dstZ, int depth, const char* what) {
|
||||
for (int slice = 0; slice < sliceCount; ++slice) {
|
||||
const bool inRange = slice >= dstZ && slice < dstZ + depth;
|
||||
const Rgba8 expected =
|
||||
inRange ? SourceColor(srcZ + (slice - dstZ)) : DestinationFill(slice);
|
||||
const Rgba8 actual = ReadSlice(destination, level, slice, width, height);
|
||||
EXPECT_TRUE(actual == expected)
|
||||
<< what << ": destination slice " << slice << (inRange ? " (copied)" : " (untouched)")
|
||||
<< " is " << Describe(actual) << ", expected " << Describe(expected);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_textures;
|
||||
GLuint m_fbo = 0;
|
||||
};
|
||||
|
||||
// 2d_array -> 2d_array. Both endpoints put the slices on the layer axis, so BOTH layer
|
||||
// counts carry the depth and extent.depth must stay 1.
|
||||
TEST_F(CopyImageLayeredScenario, ArrayToArrayCopiesEverySlice) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0,
|
||||
kWidth, kHeight, kSlices);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, 0, 0, kSlices, "array->array, all slices");
|
||||
}
|
||||
|
||||
// The same pair with the layer ranges offset differently on the two sides: the shape that
|
||||
// separates "copies more than slice 0" from "copies the RIGHT slices". A backend that read
|
||||
// the source range but wrote from layer 0 (or vice versa) passes the case above.
|
||||
TEST_F(CopyImageLayeredScenario, ArrayToArrayHonoursDifferentLayerOffsets) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 3;
|
||||
constexpr int kDstZ = 1;
|
||||
constexpr int kDepth = 2;
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0,
|
||||
kDstZ, kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth,
|
||||
"array->array, offset layer ranges");
|
||||
}
|
||||
|
||||
// 3d -> 3d. Neither endpoint has array layers at all: the depth travels on extent.depth and
|
||||
// the offsets on srcOffset.z/dstOffset.z, with both layer counts pinned to 1.
|
||||
TEST_F(CopyImageLayeredScenario, VolumeToVolumeHonoursNonZeroZ) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_3D, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 1;
|
||||
constexpr int kDstZ = 3;
|
||||
constexpr int kDepth = 3;
|
||||
glCopyImageSubData(source, GL_TEXTURE_3D, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, 0, 0, 0, kDstZ,
|
||||
kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "3d->3d, non-zero z");
|
||||
}
|
||||
|
||||
// The same pair one mip level down. A 3D level's DEPTH halves with its width and height, so
|
||||
// this is the only case where the slice count the copy may name is not the image's own -
|
||||
// the bound a layered endpoint is checked against has to come from the level.
|
||||
TEST_F(CopyImageLayeredScenario, VolumeToVolumeAtNonZeroMipLevel) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_3D, 2, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 2, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kLevel = 1;
|
||||
constexpr int kSrcZ = 2;
|
||||
constexpr int kDstZ = 0;
|
||||
constexpr int kDepth = 4;
|
||||
glCopyImageSubData(source, GL_TEXTURE_3D, kLevel, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, kLevel, 0, 0,
|
||||
kDstZ, kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, kLevel, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth,
|
||||
"3d->3d at mip level 1");
|
||||
}
|
||||
|
||||
// 2d_array -> 3d. The mixed shape: the source counts its slices as layers, the destination
|
||||
// as depth, and Vulkan requires extent.depth to equal the source's layerCount.
|
||||
TEST_F(CopyImageLayeredScenario, ArrayToVolumeCopiesEverySlice) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 2;
|
||||
constexpr int kDstZ = 1;
|
||||
constexpr int kDepth = 4;
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, 0, 0, 0, kDstZ,
|
||||
kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "2d_array->3d");
|
||||
}
|
||||
|
||||
// 3d -> 2d_array, the mirror image: the depth now has to reach the DESTINATION's layerCount
|
||||
// while the source states it as extent.depth from a z offset.
|
||||
TEST_F(CopyImageLayeredScenario, VolumeToArrayCopiesEverySlice) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_3D, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 1;
|
||||
constexpr int kDstZ = 2;
|
||||
constexpr int kDepth = 4;
|
||||
glCopyImageSubData(source, GL_TEXTURE_3D, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kDstZ,
|
||||
kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "3d->2d_array");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
Reference in New Issue
Block a user