From 616f3c90d20f362da7c11b2b13b8a1f04fb8e1e3 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 20 Sep 2026 12:48:34 -0400 Subject: [PATCH] [Magma] preserve packed texture storage and read unbacked sparse levels from owned bytes --- .../Renderer/VkTextureManager.cpp | 55 +++++++++++++++- .../DirectVulkan/Renderer/VkTextureManager.h | 2 + .../Renderer/WireTextureReadback.inc | 64 +++++++++++++++++-- 3 files changed, 114 insertions(+), 7 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index ea1a162c..b9ed98f7 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -1992,6 +1992,46 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } + Bool VkTextureManager::ReadUnbackedWireLevel(MG_Pipe::MGPipeHandle handle, TextureUploadTarget target, + Uint32 level, const IntVec3& extent, VkFormat& format, + Vector& bytes) { + const auto& records = MG_Pipe::MGPipeApplier().TextureResources; + if (!handle.Slot || handle.Slot >= records.size()) return false; + const auto& record = records[handle.Slot]; + if (!record.Live || record.Gen != handle.Gen || !MG_Pipe::MGPipeHandleIsNull(record.Desc.ViewOf)) return false; + auto& store = MG_Remote::Server::ServerStagedTexture(); + const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(handle); + const auto upload = static_cast(target); + const auto native = m_wireTextureResources.find(key); + if (native != m_wireTextureResources.end() && native->second.image && level < native->second.mipLevels) { + const auto& image = native->second; + const IntVec3 nativeExtent = ToVulkanLevelExtent(WireTextureTargetOfPipeTarget(record.Desc.Target), extent); + const Bool volume = image.viewType == VK_IMAGE_VIEW_TYPE_3D; + const Uint64 firstLayer = ResolveUploadArrayLayer(target); + if (nativeExtent.x() == static_cast(std::max(image.extent.width >> level, 1u)) && + nativeExtent.y() == static_cast(std::max(image.extent.height >> level, 1u)) && + (volume ? nativeExtent.z() == static_cast(std::max(image.depth >> level, 1u)) + : firstLayer + static_cast(nativeExtent.z()) <= image.arrayLayers)) + return false; // a failed sync cannot substitute CPU bytes for this native image + } + if (!store.IsCovered(key, upload, level) || store.IsLevelGpuDirty(key, upload, level) || + store.LevelExtentOrUndefined(key, upload, level) != extent) return false; + const auto logical = static_cast(record.Desc.InternalFormat); + const auto formatInfo = ResolveTextureFormatInfo(logical); + format = formatInfo.format; + if (format == VK_FORMAT_UNDEFINED) return false; + const void* source = store.RequireLevelBytes(key, upload, level, "unbacked texture level readback"); + const void* converted = nullptr; + SizeT convertedSize = 0; + Vector owned; + if (!ConvertWireLevelBytes(source, store.LevelByteSize(key, upload, level), + ToVulkanLevelExtent(WireTextureTargetOfPipeTarget(record.Desc.Target), extent), format, + GetAspectMaskForFormat(format), logical, formatInfo, owned, converted, convertedSize)) return false; + const auto* begin = static_cast(converted); + bytes.assign(begin, begin + convertedSize); + return true; + } + // D-D5's consume, Magma-local: the level's entry leaves the applier's pending set ONLY // here, where its bytes were actually staged. (Espryt's twin is Managers.cpp's // ConsumePipeTextureUpload; the decode rule is the shared one.) @@ -2100,7 +2140,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkImageUsageFlags desiredUsage = VK_IMAGE_USAGE_SAMPLED_BIT | (supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) | - ((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) | + ((aspect & VK_IMAGE_ASPECT_COLOR_BIT) && + (formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) + ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) | (((aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || (aspect & VK_IMAGE_ASPECT_STENCIL_BIT)) ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : 0); @@ -2328,7 +2370,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: pending upload for level %u exceeds the " "image's %u levels; left pending", handle.Slot, handle.Gen, static_cast(level), resource.mipLevels); - incomplete = true; continue; } const IntVec3 glExtent = store.LevelExtentOrUndefined(key, uploadTarget, level); @@ -2341,6 +2382,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Vulkan geometry, like the image this stages into: a 1D array's layers move out of // the shadow's height into z. const IntVec3 texelSize = ToVulkanLevelExtent(target, glExtent); + // GL permits independently defined mutable mip images. A Vulkan + // image can only hold the canonical halving chain. Keep an unusual + // level in owned staging instead of copying outside a VkImage mip. + const Bool volume = resource.viewType == VK_IMAGE_VIEW_TYPE_3D; + if (texelSize.x() != static_cast(std::max(resource.extent.width >> level, 1u)) || + texelSize.y() != static_cast(std::max(resource.extent.height >> level, 1u)) || + (volume && texelSize.z() != static_cast(std::max(resource.depth >> level, 1u))) || + (!volume && Uint64(ResolveUploadArrayLayer(static_cast(uploadTarget))) + + static_cast(texelSize.z()) > resource.arrayLayers)) + continue; if (texelSize.x() <= 0 || texelSize.y() <= 0 || byteSize == 0) { incomplete = true; continue; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 26163f0d..064d547d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -559,6 +559,8 @@ public: // a NAMESPACE THE FRONTEND-KEYED m_textureResources NEVER MEETS, so a recycled {slot, gen} // can never inherit its predecessor's image. Texture views resolve the storage owner from // Desc.ViewOf and the window from ViewCso; buffer-backed textures remain P7's buffer arm. + Bool ReadUnbackedWireLevel(MG_Pipe::MGPipeHandle handle, TextureUploadTarget target, Uint32 level, + const IntVec3& extent, VkFormat& format, Vector& bytes); TextureResource* SyncTextureResourceByHandle(MG_Pipe::MGPipeHandle handle, Bool renderbuffer = false, Bool requireStorage = false); // Map a texture/view-relative subresource to its live storage owner. layerCount, when diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireTextureReadback.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireTextureReadback.inc index db6487ee..d4742aa5 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireTextureReadback.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireTextureReadback.inc @@ -67,6 +67,47 @@ return true; } + static Bool PackWireColorTight(const Uint8* source, SizeT sourceBytes, VkFormat format, + TextureInternalFormat logicalFormat, const MG_Pipe::MGPReadbackInfo& info, + Vector& result) { + using namespace DirectGLES::ReadbackImpl; + ReadbackChannelMapping mapping{}; + if (!GetReadbackChannelMapping(info.Format, mapping)) return false; + const SizeT sourcePixelSize = VulkanRenderer::GetReadbackTexelSize(format); + const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, info.Type); + const Uint64 rows64 = Uint64(info.Box.H) * info.Box.D; + if (!info.Box.W || !rows64 || rows64 > std::numeric_limits::max() || + rows64 > std::numeric_limits::max() / 16 / info.Box.W) return false; + const SizeT pixels = static_cast(info.Box.W) * static_cast(rows64); + if (!sourcePixelSize || !pixelBytes || sourceBytes != pixels * sourcePixelSize || + info.DstSize != pixels * pixelBytes) return false; + if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(GetPackedReadbackInternalFormat(format), + MG_Util::ConvertGLEnumToTextureInputFormat(info.Format), + MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type))) { + if (pixelBytes != sourcePixelSize) return false; + result.assign(source, source + sourceBytes); + return true; + } + Vector wide; + GLenum wideType = GL_NONE; + const GLsizei rows = static_cast(Uint64(info.Box.H) * info.Box.D); + if (!DecodeReadbackRowsToWide(source, format, info.Box.W, rows, wide, wideType) || + mapping.isInteger != (wideType == GL_INT || wideType == GL_UNSIGNED_INT)) return false; + if (ResolveTextureFormatInfo(logicalFormat).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(info.DstSize); + for (GLsizei row = 0; row < rows; ++row) + ConvertWideReadbackRow(wide.data() + row * static_cast(info.Box.W) * 16, + result.data() + row * static_cast(info.Box.W) * pixelBytes, + info.Box.W, wideType, mapping, info.Type); + return true; + } + Bool VulkanRenderer::ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector& ownedBytes) { ownedBytes.clear(); @@ -112,18 +153,33 @@ VkFormat viewFormat = VK_FORMAT_UNDEFINED; const auto storage = m_textureManager->ResolveWireTextureStorage(info.Res, level, layer, &viewFormat, &availableLayers); if (MG_Pipe::MGPipeHandleIsNull(storage)) return false; + const Uint64 pixels64 = Uint64(box.W) * box.H * box.D; + if (pixels64 > std::numeric_limits::max() / 16) return false; + const auto readUnbacked = [&]() { + // Only a level the VkImage cannot represent may use its owned, + // non-GPU-dirty staging bytes. A failed GPU read never falls back. + if (storage != info.Res || info.Target == GL_TEXTURE_CUBE_MAP) return false; + Vector raw; + VkFormat nativeFormat = VK_FORMAT_UNDEFINED; + if (!m_textureManager->ReadUnbackedWireLevel(storage, + MG_Util::ConvertGLEnumToTextureUploadTarget(info.Target), info.Level, + IntVec3{static_cast(box.W), static_cast(box.H), static_cast(box.D)}, + nativeFormat, raw)) return false; + return PackWireColorTight(raw.data(), raw.size(), nativeFormat, + static_cast(record.Desc.InternalFormat), info, ownedBytes); + }; RetireWireDrawPass(); MakeXfbWritesVisible(); auto* resource = m_textureManager->SyncTextureResourceByHandle(storage); - if (!resource || !resource->image || level >= resource->mipLevels || - resource->sampleCount != VK_SAMPLE_COUNT_1_BIT) return false; + if (!resource || !resource->image || level >= resource->mipLevels) return readUnbacked(); + if (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; + (!nativeVolume && layerCount > availableLayers)) return readUnbacked(); // 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; @@ -133,8 +189,6 @@ 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));