[Feat] (DirectVulkan, MG_Impl): depth-stencil ReadPixels and combined-attachment queries

glReadPixels now serves GL_DEPTH_COMPONENT, GL_DEPTH_STENCIL and
GL_STENCIL_INDEX from the read framebuffer's depth/stencil attachment:
per-aspect vkCmdCopyImageToBuffer copies (4-byte-aligned stencil region)
with CPU repacking into GL_FLOAT / GL_UNSIGNED_SHORT / GL_UNSIGNED_INT /
GL_UNSIGNED_INT_24_8 / GL_FLOAT_32_UNSIGNED_INT_24_8_REV /
GL_UNSIGNED_BYTE layouts, honoring pack state and pixel-pack buffers.

GL_DEPTH_STENCIL_ATTACHMENT parameter queries follow the spec's combined
rules: differing depth/stencil attachment images (or a lone half) fail
with GL_INVALID_OPERATION, as does GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE
on the combined name. packed_depth_stencil.verify_parameters.* and
verify_read_pixels.depth24_stencil8 now pass.
This commit is contained in:
BZLZHH
2026-07-31 18:41:08 -04:00
parent e6ebe7078d
commit 282dd69230
3 changed files with 370 additions and 12 deletions
@@ -6800,6 +6800,11 @@ void main() {
return;
}
if (format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL || format == GL_STENCIL_INDEX) {
ReadDepthStencilPixels(*readFbo, x, y, width, height, format, type, pixels);
return;
}
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
@@ -6945,6 +6950,301 @@ void main() {
/*applyPackImageParams=*/false);
}
void VulkanRenderer::ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y,
GLsizei width, GLsizei height, GLenum format, GLenum type,
void* pixels) {
if (readFbo.IsDefaultFramebuffer()) {
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: default framebuffer readback is unsupported");
return;
}
if (width <= 0 || height <= 0) {
return;
}
const Bool wantDepth = format != GL_STENCIL_INDEX;
const Bool wantStencil = format != GL_DEPTH_COMPONENT;
// GL_DEPTH_STENCIL requires both halves; the state layer already rejected
// framebuffers lacking either, so resolving via the depth attachment is enough.
const auto attachmentType = wantDepth ? MobileGL::FramebufferAttachmentType::Depth
: MobileGL::FramebufferAttachmentType::Stencil;
const auto& attachment = readFbo.GetAttachment(attachmentType);
if (!attachment.IsValid() || attachment.IsEmpty()) {
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: no depth/stencil attachment image");
return;
}
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
}
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
}
VkImage image = VK_NULL_HANDLE;
VkFormat vkFormat = VK_FORMAT_UNDEFINED;
VkImageLayout* trackedLayout = nullptr;
VkImageAspectFlags imageAspect = VK_IMAGE_ASPECT_NONE;
Uint32 mipLevel = 0;
Uint32 baseArrayLayer = 0;
if (attachment.IsTexture() && attachment.GetTexture()) {
auto textureObject = attachment.GetTexture();
const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *textureObject);
MOBILEGL_ASSERT(clearReady, "ReadDepthStencilPixels: failed to materialize pending clear for textureId=%d",
textureObject->GetExternalIndex());
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*textureObject);
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: failed to sync depth textureId=%u",
textureObject->GetExternalIndex());
return;
}
image = resource->image;
vkFormat = resource->format;
trackedLayout = &resource->layout;
imageAspect = resource->aspect;
mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0));
baseArrayLayer = static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
} else if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
const auto& renderbufferObject = attachment.GetRenderbuffer();
const Bool clearReady = MaterializePendingClearForRenderbuffer(frame.commandBuffer, renderbufferObject);
MOBILEGL_ASSERT(clearReady,
"ReadDepthStencilPixels: failed to materialize pending clear for renderbuffer %u",
renderbufferObject->GetExternalIndex());
auto* resource = m_renderPassManager->GetOrCreateRenderbufferResource(renderbufferObject);
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: failed to resolve renderbuffer %u",
renderbufferObject->GetExternalIndex());
return;
}
image = resource->image;
vkFormat = resource->format;
trackedLayout = &resource->layout;
imageAspect = resource->aspect;
} else {
return;
}
if (*trackedLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: source layout is undefined");
return;
}
if (wantDepth && (imageAspect & VK_IMAGE_ASPECT_DEPTH_BIT) == 0) {
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: attachment has no depth aspect");
return;
}
if (wantStencil && (imageAspect & VK_IMAGE_ASPECT_STENCIL_BIT) == 0) {
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: attachment has no stencil aspect");
return;
}
// Per-aspect buffer-copy texel sizes (Vulkan defines the depth aspect of packed
// formats to copy as its own tightly defined layout).
SizeT depthCopyBytes = 0;
switch (vkFormat) {
case VK_FORMAT_D16_UNORM:
depthCopyBytes = 2;
break;
case VK_FORMAT_X8_D24_UNORM_PACK32:
case VK_FORMAT_D24_UNORM_S8_UINT:
case VK_FORMAT_D32_SFLOAT:
case VK_FORMAT_D32_SFLOAT_S8_UINT:
depthCopyBytes = 4;
break;
case VK_FORMAT_S8_UINT:
break;
default:
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: unsupported source format=%d",
static_cast<Int>(vkFormat));
return;
}
const SizeT pixelCount = static_cast<SizeT>(width) * static_cast<SizeT>(height);
const VkDeviceSize depthBytes = wantDepth ? pixelCount * depthCopyBytes : 0;
// Buffer offsets for depth/stencil copies must be 4-byte aligned.
const VkDeviceSize stencilOffset = (depthBytes + 3) & ~VkDeviceSize{3};
const VkDeviceSize stencilBytes = wantStencil ? pixelCount : 0;
VkBufferObject readback;
if (!readback.Create({
.allocator = m_allocator,
.size = stencilOffset + stencilBytes,
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT,
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
.allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT,
})) {
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: failed to create readback buffer");
return;
}
const VkImageLayout originalLayout = *trackedLayout;
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags srcAccessMask = 0;
GetImageTransitionSourceState(originalLayout, srcStageMask, srcAccessMask);
Bool ok = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, image, *trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask,
VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, imageAspect, mipLevel, 1);
MOBILEGL_ASSERT(ok, "%s: failed to transition depth-stencil source image", __func__);
VkBufferImageCopy regions[2]{};
Uint32 regionCount = 0;
if (wantDepth) {
auto& region = regions[regionCount++];
region.bufferOffset = 0;
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
region.imageSubresource.mipLevel = mipLevel;
region.imageSubresource.baseArrayLayer = baseArrayLayer;
region.imageSubresource.layerCount = 1;
region.imageOffset = {x, y, 0};
region.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
}
if (wantStencil) {
auto& region = regions[regionCount++];
region.bufferOffset = stencilOffset;
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
region.imageSubresource.mipLevel = mipLevel;
region.imageSubresource.baseArrayLayer = baseArrayLayer;
region.imageSubresource.layerCount = 1;
region.imageOffset = {x, y, 0};
region.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
}
vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(),
regionCount, regions);
VkPipelineStageFlags restoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags restoreAccessMask = 0;
GetImageTransitionDestinationState(originalLayout, restoreStageMask, restoreAccessMask);
ok = VkTextureManager::TransitionImageLayout(
frame.commandBuffer, image, *trackedLayout, originalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT,
restoreStageMask, VK_ACCESS_TRANSFER_READ_BIT, restoreAccessMask, imageAspect, mipLevel, 1);
MOBILEGL_ASSERT(ok, "%s: failed to restore depth-stencil source image layout", __func__);
if (!SubmitReadbackCommandsAndWait(frame)) {
return;
}
const auto* mapped = static_cast<const Uint8*>(readback.Map());
if (mapped == nullptr || !readback.Invalidate(stencilOffset + stencilBytes)) {
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: failed to map readback buffer");
return;
}
const Uint8* depthSrc = mapped;
const Uint8* stencilSrc = mapped + stencilOffset;
const auto depthValueAt = [&](SizeT i) -> Float {
switch (vkFormat) {
case VK_FORMAT_D16_UNORM: {
Uint16 raw = 0;
Memcpy(&raw, depthSrc + i * 2, sizeof(raw));
return static_cast<Float>(raw) / 65535.0f;
}
case VK_FORMAT_X8_D24_UNORM_PACK32:
case VK_FORMAT_D24_UNORM_S8_UINT: {
Uint32 raw = 0;
Memcpy(&raw, depthSrc + i * 4, sizeof(raw));
return static_cast<Float>(raw & 0xFFFFFFu) / static_cast<Float>(0xFFFFFFu);
}
default: { // D32_SFLOAT / D32_SFLOAT_S8_UINT
Float raw = 0.0f;
Memcpy(&raw, depthSrc + i * 4, sizeof(raw));
return raw;
}
}
};
SizeT dstPixelBytes = 0;
switch (type) {
case GL_FLOAT:
case GL_UNSIGNED_INT:
case GL_UNSIGNED_INT_24_8:
dstPixelBytes = 4;
break;
case GL_UNSIGNED_SHORT:
dstPixelBytes = 2;
break;
case GL_UNSIGNED_BYTE:
dstPixelBytes = 1;
break;
case GL_FLOAT_32_UNSIGNED_INT_24_8_REV:
dstPixelBytes = 8;
break;
default:
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: unsupported type=0x%x", type);
return;
}
Vector<Uint8> packed(pixelCount * dstPixelBytes);
for (SizeT i = 0; i < pixelCount; ++i) {
Uint8* dst = packed.data() + i * dstPixelBytes;
switch (type) {
case GL_FLOAT: {
const Float value = depthValueAt(i);
Memcpy(dst, &value, sizeof(value));
break;
}
case GL_UNSIGNED_SHORT: {
const Uint16 value =
static_cast<Uint16>(std::lround(static_cast<double>(depthValueAt(i)) * 65535.0));
Memcpy(dst, &value, sizeof(value));
break;
}
case GL_UNSIGNED_INT: {
const Uint32 value = format == GL_STENCIL_INDEX
? stencilSrc[i]
: static_cast<Uint32>(static_cast<double>(depthValueAt(i)) * 4294967295.0);
Memcpy(dst, &value, sizeof(value));
break;
}
case GL_UNSIGNED_BYTE: {
dst[0] = stencilSrc[i];
break;
}
case GL_UNSIGNED_INT_24_8: {
const Uint32 depth24 =
static_cast<Uint32>(std::lround(static_cast<double>(depthValueAt(i)) * 16777215.0)) & 0xFFFFFFu;
const Uint32 value = (depth24 << 8) | stencilSrc[i];
Memcpy(dst, &value, sizeof(value));
break;
}
case GL_FLOAT_32_UNSIGNED_INT_24_8_REV: {
const Float depthValue = depthValueAt(i);
const Uint32 stencilValue = stencilSrc[i];
Memcpy(dst, &depthValue, sizeof(depthValue));
Memcpy(dst + 4, &stencilValue, sizeof(stencilValue));
break;
}
default:
break;
}
}
// Store honoring the client pack state (single slice).
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT packAlignment = packParams.Alignment > 0 ? static_cast<SizeT>(packParams.Alignment) : 1;
const SizeT dstRowStride = ((rowPixels * dstPixelBytes) + packAlignment - 1) / packAlignment * packAlignment;
const SizeT dstSkipOffset = static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels);
if (pixelPackBufferObject != nullptr) {
const SizeT requiredSize =
pboBaseOffset + dstSkipOffset + static_cast<SizeT>(height - 1) * dstRowStride + dstRowBytes;
if (requiredSize > pixelPackBufferObject->GetSize()) {
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: pixel pack buffer is too small");
return;
}
}
for (GLsizei row = 0; row < height; ++row) {
Uint8* srcRow = packed.data() + static_cast<SizeT>(row) * dstRowBytes;
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(row) * dstRowStride;
if (pixelPackBufferObject != nullptr) {
pixelPackBufferObject->WritebackFromBackend({srcRow, dstRowBytes}, pboBaseOffset + dstOffset);
} else {
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, srcRow, dstRowBytes);
}
}
}
void VulkanRenderer::GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
@@ -200,6 +200,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
// GL_DEPTH_COMPONENT / GL_DEPTH_STENCIL / GL_STENCIL_INDEX readback from the
// read framebuffer's depth/stencil attachment (per-aspect buffer copies with
// CPU repacking into the requested client layout).
void ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y, GLsizei width,
GLsizei height, GLenum format, GLenum type, void* pixels);
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
GLsizei width, GLsizei height, GLenum destinationFormat,
@@ -212,8 +212,8 @@ namespace MobileGL::MG_Impl::GLImpl {
// Handles the format-derived pnames shared by GetFramebufferAttachmentParameteriv
// and its DSA variant. Returns true when pname was one of them.
Bool TryAnswerAttachmentFormatQuery(const MG_State::GLState::FramebufferAttachmentObject* attachmentObject,
FramebufferAttachmentType attachmentType, GLenum pname, GLint* params,
const char* caller) {
FramebufferAttachmentType attachmentType, Bool depthStencilAlias,
GLenum pname, GLint* params, const char* caller) {
switch (pname) {
case GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE:
case GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE:
@@ -228,6 +228,18 @@ namespace MobileGL::MG_Impl::GLImpl {
return false;
}
if (pname == GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE && depthStencilAlias) {
// The depth and stencil components have different types, so the combined
// attachment name has no single answer.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller,
"GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE cannot be queried on "
"GL_DEPTH_STENCIL_ATTACHMENT."));
return true;
}
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
// With OBJECT_TYPE == GL_NONE only OBJECT_TYPE and OBJECT_NAME may be queried.
MG_State::pGLContext->RecordError(
@@ -579,20 +591,40 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
Bool depthStencilMismatch = false;
const auto* attachmentObject = [&]() -> const MG_State::GLState::FramebufferAttachmentObject* {
if (!depthStencilAlias) {
return &framebufferObject->GetAttachment(attachmentType);
}
const auto& depthAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Depth);
if (depthAttachment.IsValid() && !depthAttachment.IsEmpty()) return &depthAttachment;
const auto& stencilAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Stencil);
if (stencilAttachment.IsValid() && !stencilAttachment.IsEmpty()) return &stencilAttachment;
const Bool depthLive = depthAttachment.IsValid() && !depthAttachment.IsEmpty();
const Bool stencilLive = stencilAttachment.IsValid() && !stencilAttachment.IsEmpty();
if (depthLive && stencilLive) {
const Bool sameObject = depthAttachment.IsTexture() == stencilAttachment.IsTexture() &&
(!depthAttachment.IsTexture() || depthAttachment.GetTexture() == stencilAttachment.GetTexture()) &&
(!depthAttachment.IsRenderbuffer() ||
depthAttachment.GetRenderbuffer() == stencilAttachment.GetRenderbuffer());
depthStencilMismatch = !sameObject;
} else {
// GL_DEPTH_STENCIL_ATTACHMENT means "both halves"; a lone half does not answer it.
depthStencilMismatch = depthLive != stencilLive;
}
if (depthLive) return &depthAttachment;
if (stencilLive) return &stencilAttachment;
return nullptr;
}();
if (depthStencilMismatch) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetFramebufferAttachmentParameteriv_State",
"GL_DEPTH_STENCIL_ATTACHMENT query with different depth and stencil "
"attachment images."));
return;
}
switch (pname) {
case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
@@ -656,7 +688,7 @@ namespace MobileGL::MG_Impl::GLImpl {
: GL_FALSE;
break;
default:
if (TryAnswerAttachmentFormatQuery(attachmentObject, attachmentType, pname, params,
if (TryAnswerAttachmentFormatQuery(attachmentObject, attachmentType, depthStencilAlias, pname, params,
"GetFramebufferAttachmentParameteriv_State")) {
return;
}
@@ -1623,20 +1655,40 @@ namespace MobileGL::MG_Impl::GLImpl {
: MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment);
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
Bool depthStencilMismatch = false;
const auto* attachmentObject = [&]() -> const MG_State::GLState::FramebufferAttachmentObject* {
if (!depthStencilAlias) {
return &framebufferObject->GetAttachment(attachmentType);
}
const auto& depthAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Depth);
if (depthAttachment.IsValid() && !depthAttachment.IsEmpty()) return &depthAttachment;
const auto& stencilAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Stencil);
if (stencilAttachment.IsValid() && !stencilAttachment.IsEmpty()) return &stencilAttachment;
const Bool depthLive = depthAttachment.IsValid() && !depthAttachment.IsEmpty();
const Bool stencilLive = stencilAttachment.IsValid() && !stencilAttachment.IsEmpty();
if (depthLive && stencilLive) {
const Bool sameObject = depthAttachment.IsTexture() == stencilAttachment.IsTexture() &&
(!depthAttachment.IsTexture() || depthAttachment.GetTexture() == stencilAttachment.GetTexture()) &&
(!depthAttachment.IsRenderbuffer() ||
depthAttachment.GetRenderbuffer() == stencilAttachment.GetRenderbuffer());
depthStencilMismatch = !sameObject;
} else {
// GL_DEPTH_STENCIL_ATTACHMENT means "both halves"; a lone half does not answer it.
depthStencilMismatch = depthLive != stencilLive;
}
if (depthLive) return &depthAttachment;
if (stencilLive) return &stencilAttachment;
return nullptr;
}();
if (depthStencilMismatch) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"GL_DEPTH_STENCIL_ATTACHMENT query with different depth and stencil "
"attachment images."));
return;
}
switch (pname) {
case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
@@ -1682,7 +1734,8 @@ namespace MobileGL::MG_Impl::GLImpl {
: GL_FALSE;
break;
default:
if (TryAnswerAttachmentFormatQuery(attachmentObject, attachmentType, pname, params, caller)) {
if (TryAnswerAttachmentFormatQuery(attachmentObject, attachmentType, depthStencilAlias, pname, params,
caller)) {
return;
}
MG_State::pGLContext->RecordError(