diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 1a4840b8..2e3b4cf3 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -7766,6 +7766,7 @@ void main() { } #include "WireFramebuffer.inc" + #include "WireTextureReadback.inc" #include "WireColorBlit.inc" #include "WireDraw.inc" @@ -11934,6 +11935,7 @@ void main() { VkDeviceSize offsets[4] = {}; VkDeviceSize sizes[4] = {}; for (SizeT i = 0; i < bufferCount; ++i) { + if (!program->GetTransformFeedbackStride(static_cast(i))) continue; auto& point = MGB_CTX->GetBufferBindingPoint(BufferTarget::TransformFeedback, static_cast(i)); const auto& bufferObject = point.GetBoundObject(); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index ee84a346..433474ac 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -222,6 +222,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); +#if MOBILEGL_BUILD_DISAGGREGATED + // Server resource handle in, tightly packed owned bytes out. No frontend + // texture, pixel-pack state or PBO is consulted by this readback. + Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector& ownedBytes); +#endif // 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). diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireTextureReadback.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireTextureReadback.inc new file mode 100644 index 00000000..e6496f5a --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireTextureReadback.inc @@ -0,0 +1,222 @@ +// Server-owned texture readback. Included after WireFramebuffer.inc so image +// layout tracking and frame retirement are the same as every other wire verb. +#if MOBILEGL_BUILD_DISAGGREGATED + static Bool PackWireDepthStencilTight(const Uint8* depth, const Uint8* stencil, + VkFormat sourceFormat, SizeT count, GLenum format, GLenum type, Vector& result) { + const Bool stencilOnly = format == GL_STENCIL_INDEX; + const auto readDepth = [&](SizeT i) -> Float { + if (sourceFormat == VK_FORMAT_D16_UNORM) { + Uint16 word{}; Memcpy(&word, depth + i * 2, sizeof(word)); + return static_cast(word) / 65535.0f; + } + if (sourceFormat == VK_FORMAT_X8_D24_UNORM_PACK32 || sourceFormat == VK_FORMAT_D24_UNORM_S8_UINT) { + Uint32 word{}; Memcpy(&word, depth + i * 4, sizeof(word)); + return static_cast(word & 0xffffffu) / 16777215.0f; + } + Float value{}; Memcpy(&value, depth + i * sizeof(value), sizeof(value)); + return value; + }; + SizeT size = 0; + switch (type) { + case GL_BYTE: case GL_UNSIGNED_BYTE: size = 1; break; + case GL_SHORT: case GL_UNSIGNED_SHORT: size = 2; break; + case GL_INT: case GL_UNSIGNED_INT: case GL_FLOAT: size = 4; break; + case GL_UNSIGNED_INT_24_8: size = 4; break; + case GL_FLOAT_32_UNSIGNED_INT_24_8_REV: size = 8; break; + default: return false; + } + const Bool packed = type == GL_UNSIGNED_INT_24_8 || type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV; + if ((format == GL_DEPTH_STENCIL) != packed) return false; + result.resize(count * size); + for (SizeT i = 0; i < count; ++i) { + Uint8* dst = result.data() + i * size; + const Uint32 s = stencil ? stencil[i] : 0; + const Float d = stencilOnly ? 0.0f : readDepth(i); + switch (type) { + case GL_FLOAT: { + const Float value = stencilOnly ? static_cast(s) : d; + Memcpy(dst, &value, sizeof(value)); break; + } + case GL_UNSIGNED_INT_24_8: { + Uint32 depth24 = 0; + if (sourceFormat == VK_FORMAT_D24_UNORM_S8_UINT) { + Memcpy(&depth24, depth + i * 4, sizeof(depth24)); + depth24 &= 0xffffffu; + } else { + depth24 = static_cast(std::llround(std::clamp(d, 0.0, 1.0) * 16777215.0)); + } + const Uint32 value = (depth24 << 8) | s; + Memcpy(dst, &value, sizeof(value)); break; + } + case GL_FLOAT_32_UNSIGNED_INT_24_8_REV: + Memcpy(dst, &d, sizeof(d)); Memcpy(dst + 4, &s, sizeof(s)); break; + default: { + const Bool signedType = type == GL_BYTE || type == GL_SHORT || type == GL_INT; + const Uint64 maximum = signedType ? ((Uint64{1} << (size * 8 - 1)) - 1) + : ((Uint64{1} << (size * 8)) - 1); + const Uint32 value = stencilOnly ? s : static_cast( + std::llround(std::clamp(d, 0.0, 1.0) * static_cast(maximum))); + Memcpy(dst, &value, size); break; + } + } + } + return true; + } + + Bool VulkanRenderer::ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, + Vector& ownedBytes) { + ownedBytes.clear(); + const auto& box = info.Box; + if (box.X || box.Y || box.Z || !box.W || !box.H || !box.D || + box.W > static_cast(std::numeric_limits::max()) || + Uint64(box.H) * box.D > static_cast(std::numeric_limits::max())) return false; + const auto& state = MG_Pipe::MGPipeApplier(); + if (MG_Pipe::MGPipeHandleIsNull(info.Res) || info.Res.Slot >= state.TextureResources.size()) return false; + const auto& record = state.TextureResources[info.Res.Slot]; + if (!record.Live || record.Gen != info.Res.Gen) return false; + + using Target = MG_Pipe::MGPipeResourceTarget; + const auto target = static_cast(record.Desc.Target); + const Bool oneDArray = target == Target::Tex1DArray; + const Bool volume = target == Target::Tex3D; + Uint32 level = info.Level, layer = 0, layerCount = 1, availableLayers = 1; + VkExtent3D extent{box.W, box.H, 1}; + if (oneDArray) { + if (info.Target != GL_TEXTURE_1D_ARRAY || box.D != 1) return false; + layerCount = box.H; extent.height = 1; + } else if (volume) { + if (info.Target != GL_TEXTURE_3D) return false; + extent.depth = box.D; + } else if (target == Target::Tex2DArray || target == Target::TexCubeArray) { + if (info.Target != (target == Target::Tex2DArray ? GL_TEXTURE_2D_ARRAY : GL_TEXTURE_CUBE_MAP_ARRAY)) return false; + layerCount = box.D; + } else if (target == Target::TexCube) { + if (info.Target == GL_TEXTURE_CUBE_MAP) { + if (box.D != 6) return false; + layerCount = 6; + } else if (info.Target >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && info.Target <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z) { + if (box.D != 1) return false; + layer = info.Target - GL_TEXTURE_CUBE_MAP_POSITIVE_X; + } else return false; + } else { + if (box.D != 1) return false; + const GLenum expected = target == Target::Tex1D ? GL_TEXTURE_1D : + target == Target::Tex2D ? GL_TEXTURE_2D : + target == Target::TexRect ? GL_TEXTURE_RECTANGLE : GL_NONE; + if (!expected || info.Target != expected || (target == Target::Tex1D && box.H != 1)) return false; + } + VkFormat viewFormat = VK_FORMAT_UNDEFINED; + const auto storage = m_textureManager->ResolveWireTextureStorage(info.Res, level, layer, &viewFormat, &availableLayers); + if (MG_Pipe::MGPipeHandleIsNull(storage)) return false; + RetireWireDrawPass(); + MakeXfbWritesVisible(); + auto* resource = m_textureManager->SyncTextureResourceByHandle(storage); + if (!resource || !resource->image || level >= resource->mipLevels || + resource->sampleCount != VK_SAMPLE_COUNT_1_BIT) return false; + m_textureManager->FlushPendingUploads(); + const Bool nativeVolume = resource->viewType == VK_IMAGE_VIEW_TYPE_3D; + const Uint32 nativeDepth = nativeVolume ? std::max(resource->depth >> level, 1u) : resource->arrayLayers; + if (extent.width != std::max(resource->extent.width >> level, 1u) || + extent.height != std::max(resource->extent.height >> level, 1u) || + layer >= nativeDepth || (nativeVolume ? extent.depth : layerCount) > nativeDepth - layer || + (!nativeVolume && layerCount > availableLayers)) return false; + // A view can reinterpret the same native words. Decode using the outer + // view format, while Vulkan copies the storage allocation's bytes. + const VkFormat format = viewFormat == VK_FORMAT_UNDEFINED ? resource->format : viewFormat; + const Bool depth = info.Format == GL_DEPTH_COMPONENT || info.Format == GL_DEPTH_STENCIL; + const Bool stencil = info.Format == GL_STENCIL_INDEX || info.Format == GL_DEPTH_STENCIL; + const Bool depthStencil = depth || stencil; + const VkImageAspectFlags aspects = depthStencil ? + ((depth ? VK_IMAGE_ASPECT_DEPTH_BIT : 0) | (stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0)) : VK_IMAGE_ASPECT_COLOR_BIT; + if ((resource->aspect & aspects) != aspects) return false; + const Uint64 pixels64 = Uint64(box.W) * box.H * box.D; + if (pixels64 > std::numeric_limits::max() / 16) return false; + const SizeT pixels = static_cast(pixels64); + const SizeT destinationPixelSize = MG_Util::GetInputBytesPerPixel( + MG_Util::ConvertGLEnumToTextureInputFormat(info.Format), MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)); + if (!destinationPixelSize || pixels64 * destinationPixelSize != info.DstSize) return false; + SizeT sourcePixelSize = GetReadbackTexelSize(resource->format); + if (depthStencil) { + switch (resource->format) { + case VK_FORMAT_D16_UNORM: sourcePixelSize = 2; break; + case VK_FORMAT_D24_UNORM_S8_UINT: case VK_FORMAT_X8_D24_UNORM_PACK32: + case VK_FORMAT_D32_SFLOAT: case VK_FORMAT_D32_SFLOAT_S8_UINT: sourcePixelSize = 4; break; + case VK_FORMAT_S8_UINT: sourcePixelSize = 0; break; + default: return false; + } + } else if (!sourcePixelSize || sourcePixelSize != GetReadbackTexelSize(format)) return false; + const VkDeviceSize firstBytes = pixels * (depthStencil ? (depth ? sourcePixelSize : 0) : sourcePixelSize); + const VkDeviceSize stencilOffset = (firstBytes + 3) & ~VkDeviceSize{3}; + const VkDeviceSize copyBytes = depthStencil ? stencilOffset + (stencil ? pixels : 0) : firstBytes; + if (!copyBytes) return false; + VkBufferObject readback; + if (!readback.Create({.allocator = m_allocator, .size = copyBytes, .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memoryUsage = VMA_MEMORY_USAGE_AUTO, .allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT})) return false; + WireImage image{}; + image.image = resource->image; image.format = resource->format; image.aspect = resource->aspect; + image.layout = resource->layout; image.trackedLayout = &resource->layout; image.levels = resource->mipLevels; + const auto restore = image.layout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_IMAGE_LAYOUT_GENERAL : image.layout; + TransitionWireImage(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + VkBufferImageCopy regions[2]{}; + Uint32 regionCount = 0; + const auto addCopy = [&](VkImageAspectFlags aspect, VkDeviceSize offset) { + auto& copy = regions[regionCount++]; + copy.bufferOffset = offset; + copy.imageSubresource = {aspect, level, nativeVolume ? 0u : layer, nativeVolume ? 1u : layerCount}; + copy.imageOffset = {0, 0, nativeVolume ? static_cast(layer) : 0}; + copy.imageExtent = extent; + }; + if (!depthStencil) addCopy(VK_IMAGE_ASPECT_COLOR_BIT, 0); + if (depth) addCopy(VK_IMAGE_ASPECT_DEPTH_BIT, 0); + if (stencil) addCopy(VK_IMAGE_ASPECT_STENCIL_BIT, stencilOffset); + auto& frame = m_frameContext.GetCurrent(); + vkCmdCopyImageToBuffer(frame.commandBuffer, image.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + readback.GetHandle(), regionCount, regions); + TransitionWireImage(image, restore); + VkMemoryBarrier host{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + host.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; host.dstAccessMask = VK_ACCESS_HOST_READ_BIT; + vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, + 0, 1, &host, 0, nullptr, 0, nullptr); + if (!SubmitReadbackCommandsAndWait(frame)) return false; + const auto* mapped = static_cast(readback.Map()); + if (!mapped || !readback.Invalidate(copyBytes)) return false; + Vector result; + if (depthStencil) { + if (!PackWireDepthStencilTight(depth ? mapped : nullptr, stencil ? mapped + stencilOffset : nullptr, + resource->format, pixels, info.Format, info.Type, result)) return false; + } else { + using namespace DirectGLES::ReadbackImpl; + ReadbackChannelMapping mapping{}; + if (!GetReadbackChannelMapping(info.Format, mapping) || + GetReadbackDstPixelSize(mapping, info.Type) != destinationPixelSize) return false; + if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(GetPackedReadbackInternalFormat(format), + MG_Util::ConvertGLEnumToTextureInputFormat(info.Format), + MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type))) { + if (destinationPixelSize != sourcePixelSize) return false; + result.assign(mapped, mapped + firstBytes); + } else { + Vector wide; + GLenum wideType = GL_NONE; + const GLsizei rows = static_cast(Uint64(box.H) * box.D); + if (!DecodeReadbackRowsToWide(mapped, format, box.W, rows, wide, wideType) || + mapping.isInteger != (wideType == GL_INT || wideType == GL_UNSIGNED_INT)) return false; + if (ResolveTextureFormatInfo(static_cast(record.Desc.InternalFormat)).expandRgbToRgba) { + const Float floatOne = 1.0f; + const Uint32 integerOne = 1; + const void* alpha = wideType == GL_FLOAT ? static_cast(&floatOne) : &integerOne; + for (SizeT i = 0; i < pixels; ++i) + Memcpy(wide.data() + (i * 4 + 3) * sizeof(Uint32), alpha, sizeof(Uint32)); + } + result.resize(pixels * destinationPixelSize); + const SizeT srcRow = static_cast(box.W) * 4 * sizeof(Uint32); + const SizeT dstRow = static_cast(box.W) * destinationPixelSize; + for (GLsizei row = 0; row < rows; ++row) + ConvertWideReadbackRow(wide.data() + row * srcRow, result.data() + row * dstRow, + box.W, wideType, mapping, info.Type); + } + } + if (result.size() != info.DstSize) return false; + ownedBytes = Move(result); + return true; + } +#endif diff --git a/MobileGL/MG_IntegrationTest/Scenarios/XfbRepeatedCaptureScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/XfbRepeatedCaptureScenario.cpp index 1aea71f4..88c27b0f 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/XfbRepeatedCaptureScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/XfbRepeatedCaptureScenario.cpp @@ -586,31 +586,17 @@ void main() EXPECT_EQ(glGetError(), GL_NO_ERROR); - // ...and, where the backend places this layout at all, the buffer it WAS asked to - // write gets the records. That placement is the DirectGLES scatter path, whose - // scratch sizing used to read each target's stride at its POSITION in a list that - // skips unbound capture buffers - which for a leading gl_NextBuffer read stride 0 - // for every target and sized the scratch at zero. DirectVulkan does not implement a - // leading-gl_NextBuffer layout at all (it captures nothing into bufferB); that is a - // pre-existing gap of its own, and the assertion above - that it corrupts nothing - // while declining - is what matters for it. - const bool backendPlacesLeadingNextBuffer = Gl().BackendName() != "DirectVulkan"; - if (backendPlacesLeadingNextBuffer) { - std::vector bufferBAfter(capturedInts, kPoison); - glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBytes, bufferBAfter.data()); - EXPECT_TRUE(CapturedIs(bufferBAfter, spanAExpected)); - } + // Sparse binding indices must be preserved by both GLES scatter and + // Vulkan capture: slot 1 receives the values while slot 0 stays untouched. + std::vector bufferBAfter(capturedInts, kPoison); + glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBytes, bufferBAfter.data()); + EXPECT_TRUE(CapturedIs(bufferBAfter, spanAExpected)); - // Unbound and deleted BEFORE any skip: a capture point left pointing at a buffer - // this test deleted would follow the process into the next scenario. + // A capture point must not retain a buffer deleted by this scenario. glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, 0); glDeleteBuffers(1, &bufferA); glDeleteBuffers(1, &bufferB); - if (!backendPlacesLeadingNextBuffer) { - GTEST_SKIP() << "DirectVulkan does not place a capture list beginning with gl_NextBuffer; it " - "captures nothing, which the no-corruption assertion above has already covered."; - } } // The control for all of the above: a span that never draws must leave the capture