From 036c3a3d5e71723b23887309c3f491787f655086 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 19 Sep 2026 22:00:14 -0400 Subject: [PATCH] [MG_Backend, MG_Pipe, MG_Remote] (Disaggregated): P5f fm - route framebuffer verbs through server records --- .../Renderer/VkTextureManager.cpp | 878 +++++++++++++++++- .../DirectVulkan/Renderer/VkTextureManager.h | 64 ++ .../DirectVulkan/Renderer/VulkanRenderer.cpp | 83 +- .../DirectVulkan/Renderer/VulkanRenderer.h | 23 + .../DirectVulkan/Renderer/WireFramebuffer.inc | 358 +++++++ MobileGL/MG_Pipe/PipeApply.h | 3 + MobileGL/MG_Remote/Server/PipeApplier.cpp | 2 + 7 files changed, 1364 insertions(+), 47 deletions(-) create mode 100644 MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 47413792..5d3b3640 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -17,6 +17,12 @@ #include "MG_Util/Metrics/PipeStats.h" #include +#if MOBILEGL_BUILD_DISAGGREGATED +// P5f (fm): the handle-keyed texture arm's two sources - the applier's resource records +// (shape) and the server's staged-texture store (texels). +#include +#include +#endif #include #include #include @@ -281,7 +287,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkCommandPool commandPool, VkQueue graphicsQueue, const VkTextureManager::TextureResource& oldResource, - VkTextureManager::TextureResource& newResource) { + VkTextureManager::TextureResource& newResource +#if MOBILEGL_BUILD_DISAGGREGATED + , Bool splitAspects = false +#endif + ) { MOBILEGL_ASSERT(device != VK_NULL_HANDLE, "PreserveTextureContentsOnRecreate: device is null"); MOBILEGL_ASSERT(commandPool != VK_NULL_HANDLE, "PreserveTextureContentsOnRecreate: commandPool is null"); MOBILEGL_ASSERT(graphicsQueue != VK_NULL_HANDLE, "PreserveTextureContentsOnRecreate: graphicsQueue is null"); @@ -339,6 +349,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { copy.extent.width = std::max(oldResource.extent.width >> level, 1u); copy.extent.height = std::max(oldResource.extent.height >> level, 1u); copy.extent.depth = std::max(oldResource.depth >> level, 1u); +#if MOBILEGL_BUILD_DISAGGREGATED + if (splitAspects) { + for (const VkImageAspectFlags aspect : {VK_IMAGE_ASPECT_COLOR_BIT, + VK_IMAGE_ASPECT_DEPTH_BIT, + VK_IMAGE_ASPECT_STENCIL_BIT}) { + if ((oldResource.aspect & aspect) == 0) continue; + copy.srcSubresource.aspectMask = aspect; + copy.dstSubresource.aspectMask = aspect; + copyRegions.push_back(copy); + } + } else +#endif copyRegions.push_back(copy); } @@ -661,6 +683,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { DestroyDeferredReleases(); ++m_resourceEraseEpoch; // every memoized resource pointer dies with the map m_textureResources.clear(); +#if MOBILEGL_BUILD_DISAGGREGATED + // Destroy images while the device/allocator still exist, including after context death. + m_wireTextureResources.clear(); + m_wireRenderbufferResources.clear(); +#endif m_aliveObjects.clear(); m_storageImageTextures.clear(); @@ -1739,9 +1766,858 @@ namespace MobileGL::MG_Backend::DirectVulkan { EraseTrackedTexture(identity); } prunedCount += orphanIdentities.size(); +#if MOBILEGL_BUILD_DISAGGREGATED + const auto pruneWire = [&](auto& resources, const auto& records) { + for (auto it = resources.begin(); it != resources.end();) { + const Uint32 slot = static_cast(it->first >> 32) & 0x7fffffffu; + const Uint32 generation = static_cast(it->first); + if (slot < records.size() && records[slot].Live && records[slot].Gen == generation) { + ++it; + continue; + } + DeferResourceRelease(Move(it->second)); + it = resources.erase(it); + ++m_resourceEraseEpoch; + ++prunedCount; + } + }; + const auto& applier = MG_Pipe::MGPipeApplier(); + pruneWire(m_wireTextureResources, applier.TextureResources); + pruneWire(m_wireRenderbufferResources, applier.RenderbufferResources); +#endif return prunedCount; } +#if MOBILEGL_BUILD_DISAGGREGATED + // --------------------------------------------------------------------------------- + // P5f (fm): THE HANDLE-KEYED TEXTURE ARM + // --------------------------------------------------------------------------------- + // + // SyncTexture reads the frontend ITextureObject: the target, the format, the level + // chain, the dirty scan and - through UploadDirtyMipLevels - the CLIENT's mip shadow + // texels. Under an active transport none of that memory is the server's to read + // (rule E), which is exactly what Magma's texture-legacy-arm scope has been waiving + // since P5c. This arm is the same sync re-sourced: the SHAPE comes from the applier's + // resource record (MGPipeApplier().TextureResources - the resource_create / + // resource_respecify descriptors), the TEXELS from the server's staged-texture store + // (P5c tx, adopted at apply time), and the WHICH-LEVELS from the record's + // pending-upload set, consumed entry by entry exactly where the upload lands + // (D-D5's rule: the set survives a bail, so a consumption is only ever written after + // the bytes were actually staged). + // + // What this arm deliberately does NOT have, and the loud answer each gets: + // * GL texture VIEWS (desc.ViewOf non-null): no view chain crosses the wire yet - + // declined, the texture stays unbacked; + // * buffer textures (StorageKind == Buffer): the buffer twin is P7's - declined; + // * sampled/storage VIEW construction: clear/blit/readback/mipmap name images and + // subresources, never descriptors, so no VkImageView is built here at all; + // * the sampled-parameter sync (MGPTextureParams): nobody on those verbs samples. + // + // The map is keyed by StagedTextureStore::KeyForHandle - slot AND generation - so a + // recycled slot's new owner can never inherit its predecessor's image, and the sweep in + // PruneDeadTextures drops an entry whose record died. + + // TryResolveTextureShapeInfo's answers derived from the wire descriptor instead of the + // frontend object. The descriptor's Width/Height/Depth are the BASE level's GL extent and + // ArrayLayers the layer count the emitter already normalised (MGPipeTextureExtentOf: a 1D + // array's layers are already out of its height, a cube map reports 6). + static Bool ResolveWireTextureShapeInfo(const MG_Pipe::MGPResourceDesc& desc, TextureShapeInfo& outShape) { + using MG_Pipe::MGPipeResourceTarget; + outShape = {}; + switch (static_cast(desc.Target)) { + case MGPipeResourceTarget::Tex1D: + outShape.imageType = VK_IMAGE_TYPE_1D; + outShape.viewType = VK_IMAGE_VIEW_TYPE_1D; + return desc.Width > 0; + case MGPipeResourceTarget::Tex2D: + case MGPipeResourceTarget::TexRect: + case MGPipeResourceTarget::Tex2DMS: + case MGPipeResourceTarget::Renderbuffer: + return desc.Width > 0 && desc.Height > 0; + case MGPipeResourceTarget::Tex3D: + outShape.imageType = VK_IMAGE_TYPE_3D; + outShape.viewType = VK_IMAGE_VIEW_TYPE_3D; + outShape.depth = desc.Depth; + return desc.Width > 0 && desc.Height > 0 && desc.Depth > 0; + case MGPipeResourceTarget::Tex1DArray: + outShape.imageType = VK_IMAGE_TYPE_1D; + outShape.viewType = VK_IMAGE_VIEW_TYPE_1D_ARRAY; + outShape.arrayLayers = desc.ArrayLayers; + return desc.Width > 0 && desc.ArrayLayers > 0; + case MGPipeResourceTarget::Tex2DArray: + case MGPipeResourceTarget::Tex2DMSArray: + outShape.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY; + outShape.arrayLayers = desc.ArrayLayers; + return desc.Width > 0 && desc.Height > 0 && desc.ArrayLayers > 0; + case MGPipeResourceTarget::TexCube: + outShape.viewType = VK_IMAGE_VIEW_TYPE_CUBE; + outShape.imageFlags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; + outShape.arrayLayers = 6; + return desc.Width > 0 && desc.Width == desc.Height; + case MGPipeResourceTarget::TexCubeArray: + // glTexStorage3D hands 6n through as the depth; the emitter normalised it into + // ArrayLayers. A non-whole cube count has no Vulkan shape - declined like every + // other unrepresentable target. + outShape.viewType = VK_IMAGE_VIEW_TYPE_CUBE_ARRAY; + outShape.imageFlags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; + outShape.arrayLayers = desc.ArrayLayers; + return desc.Width > 0 && desc.Width == desc.Height && desc.ArrayLayers > 0 && + (desc.ArrayLayers % 6) == 0; + default: + // Buffer (a record routing fault on a texture path) and TexBuffer (P7's buffer + // twin) have no mipmapped image here. + return false; + } + } + + // WireTextureTargetOf: Espryt's BufferImpl::StagedTextureTargetForPipeTarget answered + // Magma-locally (the DirectGLES header is not this backend's to include). + static TextureTarget WireTextureTargetOfPipeTarget(Uint8 pipeTarget) { + using MG_Pipe::MGPipeResourceTarget; + switch (static_cast(pipeTarget)) { + case MGPipeResourceTarget::Tex1D: return TextureTarget::Texture1D; + case MGPipeResourceTarget::Tex2D: return TextureTarget::Texture2D; + case MGPipeResourceTarget::Tex3D: return TextureTarget::Texture3D; + case MGPipeResourceTarget::Tex1DArray: return TextureTarget::Texture1DArray; + case MGPipeResourceTarget::Tex2DArray: return TextureTarget::Texture2DArray; + case MGPipeResourceTarget::TexCube: return TextureTarget::TextureCubeMap; + case MGPipeResourceTarget::TexCubeArray: return TextureTarget::TextureCubeMapArray; + case MGPipeResourceTarget::Tex2DMS: return TextureTarget::Texture2DMultisample; + case MGPipeResourceTarget::Tex2DMSArray: return TextureTarget::Texture2DMultisampleArray; + case MGPipeResourceTarget::TexRect: return TextureTarget::TextureRectangle; + case MGPipeResourceTarget::TexBuffer: return TextureTarget::TextureBuffer; + default: return TextureTarget::Unknown; + } + } + + // The per-level byte transform UploadDirtyMipLevels runs between the shadow and the + // staging buffer, factored onto values so the wire arm can run it without the frontend + // object: RGB -> RGBA expansion, pure-depth word conversion (the image may be the + // D32_SFLOAT fallback for X8_D24), and the combined depth-stencil de-interleave. Returns + // false - the upload's decline - when the (format, layout) pair has no conversion here. + static Bool ConvertWireLevelBytes(const void* source, SizeT byteSize, const IntVec3& texelSize, + VkFormat imageFormat, VkImageAspectFlags aspect, + TextureInternalFormat internalFormat, + const TextureFormatInfo& formatInfo, Vector& outOwned, + const void*& outSource, SizeT& outByteSize) { + outSource = static_cast(source); + outByteSize = byteSize; + if (formatInfo.expandRgbToRgba) { + if (!ExpandRgbSourceToRgba(source, byteSize, texelSize, formatInfo, outOwned)) { + return false; + } + outSource = outOwned.data(); + outByteSize = outOwned.size(); + return true; + } + const Bool isCombinedDepthStencil = + (aspect & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 && (aspect & VK_IMAGE_ASPECT_STENCIL_BIT) != 0; + const SizeT texelCount = static_cast(texelSize.x()) * static_cast(texelSize.y()) * + static_cast(std::max(texelSize.z(), 1)); + if (texelCount == 0) return false; + if (isCombinedDepthStencil) { + const Bool srcIsD24S8 = imageFormat == VK_FORMAT_D24_UNORM_S8_UINT; + const Bool srcIsD32FS8 = imageFormat == VK_FORMAT_D32_SFLOAT_S8_UINT; + if (!srcIsD24S8 && !srcIsD32FS8) return false; + const SizeT shadowTexelSize = byteSize / texelCount; + if (shadowTexelSize != 4 && shadowTexelSize != 8) return false; + outOwned.resize(texelCount * 4 + texelCount); + Uint8* depthPlane = outOwned.data(); + Uint8* stencilPlane = outOwned.data() + texelCount * 4; + const Uint8* shadow = static_cast(source); + for (SizeT t = 0; t < texelCount; ++t) { + if (shadowTexelSize == 8) { + // GL_FLOAT_32_UNSIGNED_INT_24_8_REV: float depth, then a word with the + // stencil in its low 8 bits. + float depthValue; + Uint32 stencilWord; + std::memcpy(&depthValue, shadow + t * 8, sizeof(depthValue)); + std::memcpy(&stencilWord, shadow + t * 8 + 4, sizeof(stencilWord)); + if (srcIsD32FS8) { + std::memcpy(depthPlane + t * 4, &depthValue, sizeof(depthValue)); + } else { + const float clamped = std::min(std::max(depthValue, 0.0f), 1.0f); + const Uint32 depthWord = static_cast(clamped * 16777215.0f + 0.5f); + std::memcpy(depthPlane + t * 4, &depthWord, sizeof(depthWord)); + } + stencilPlane[t] = static_cast(stencilWord & 0xFFu); + } else { + // GL_UNSIGNED_INT_24_8: depth in the high 24 bits, stencil low 8. + Uint32 packed; + std::memcpy(&packed, shadow + t * 4, sizeof(packed)); + if (srcIsD24S8) { + const Uint32 depthWord = packed >> 8; + std::memcpy(depthPlane + t * 4, &depthWord, sizeof(depthWord)); + } else { + const float depthValue = static_cast(packed >> 8) / 16777215.0f; + std::memcpy(depthPlane + t * 4, &depthValue, sizeof(depthValue)); + } + stencilPlane[t] = static_cast(packed & 0xFFu); + } + } + outSource = outOwned.data(); + outByteSize = outOwned.size(); + return true; + } + if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT) { + const Bool shadowIsFloat = internalFormat == TextureInternalFormat::DepthComponent32F; + const Bool dstIsFloat = imageFormat == VK_FORMAT_D32_SFLOAT; + const Bool dstIsD24Word = imageFormat == VK_FORMAT_X8_D24_UNORM_PACK32; + const SizeT shadowTexelSize = byteSize / texelCount; + const Bool needsConversion = + (dstIsFloat && !shadowIsFloat) || (dstIsD24Word && shadowTexelSize == 4 && !shadowIsFloat); + if (needsConversion) { + outOwned.resize(texelCount * 4); + const Uint8* shadow = static_cast(source); + for (SizeT t = 0; t < texelCount; ++t) { + Uint32 wide = 0; + if (shadowTexelSize == 2) { + Uint16 raw = 0; + std::memcpy(&raw, shadow + t * 2, sizeof(raw)); + wide = (static_cast(raw) << 16) | raw; + } else { + std::memcpy(&wide, shadow + t * 4, sizeof(wide)); + } + if (dstIsFloat) { + const float value = static_cast(static_cast(wide) / 4294967295.0); + std::memcpy(outOwned.data() + t * 4, &value, sizeof(value)); + } else { // X8_D24: depth in the low 24 bits of a 32-bit word + const Uint32 word = wide >> 8; + std::memcpy(outOwned.data() + t * 4, &word, sizeof(word)); + } + } + outSource = outOwned.data(); + outByteSize = outOwned.size(); + } + } + 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.) + static void ConsumeWireTextureUpload(MG_Pipe::MGPipeResourceRecord& record, Uint16 uploadTarget, Uint16 level) { + for (SizeT index = 0; index < record.PendingUploads.size(); ++index) { + const auto& pending = record.PendingUploads[index]; + if (MG_Pipe::MGPipeSubDataUploadTargetOf(pending.UploadTarget) != static_cast(uploadTarget) || + pending.Level != level) { + continue; + } + record.PendingUploads[index] = std::move(record.PendingUploads.back()); + record.PendingUploads.pop_back(); + return; + } + } + + Bool VkTextureManager::SyncWireTextureShape(const MG_Pipe::MGPipeResourceRecord& record, + TextureResource& resource) { + const MG_Pipe::MGPResourceDesc& desc = record.Desc; + if (desc.StorageKind != static_cast(TextureStorageType::Mipmap)) { + MGLOG_W_ONCE("Magma wire texture {slot=%u, gen=%u}: buffer-backed textures are not migrated; declined", + desc.Resource.Slot, desc.Resource.Gen); + return false; + } + if (!MG_Pipe::MGPipeHandleIsNull(desc.ViewOf)) { + MGLOG_W_ONCE("Magma wire texture {slot=%u, gen=%u}: GL texture views do not cross the wire yet; declined", + desc.Resource.Slot, desc.Resource.Gen); + return false; + } + if (desc.Levels == 0) { + // A create before the first respecify: no storage, nothing to back. + return false; + } + TextureShapeInfo shapeInfo{}; + if (!ResolveWireTextureShapeInfo(desc, shapeInfo)) { + MGLOG_W_ONCE("Magma wire texture {slot=%u, gen=%u}: no Vulkan shape for pipe target %u " + "(%ux%ux%u, %u layers); declined", + desc.Resource.Slot, desc.Resource.Gen, static_cast(desc.Target), desc.Width, + desc.Height, desc.Depth, static_cast(desc.ArrayLayers)); + return false; + } + const TextureInternalFormat internalFormat = static_cast(desc.InternalFormat); + const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(internalFormat); + VkFormat format = formatInfo.format; + if (format == VK_FORMAT_UNDEFINED) { + MGLOG_W_ONCE("Magma wire texture {slot=%u, gen=%u}: no backing VkFormat for internal format 0x%x", + desc.Resource.Slot, desc.Resource.Gen, desc.InternalFormat); + return false; + } + // X8_D24 lacks optimal-tiling support on several drivers (lavapipe included); the same + // fallback SyncTextureResource applies, and the upload arm converts the shadow words. + if (format == VK_FORMAT_X8_D24_UNORM_PACK32) { + VkFormatProperties formatProperties{}; + vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties); + constexpr VkFormatFeatureFlags kDepthAttachmentAndSample = + VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; + if ((formatProperties.optimalTilingFeatures & kDepthAttachmentAndSample) != kDepthAttachmentAndSample) { + format = VK_FORMAT_D32_SFLOAT; + } + } + const Bool isMultisample = + desc.Target == static_cast(MG_Pipe::MGPipeResourceTarget::Tex2DMS) || + desc.Target == static_cast(MG_Pipe::MGPipeResourceTarget::Tex2DMSArray) || + (desc.Target == static_cast(MG_Pipe::MGPipeResourceTarget::Renderbuffer) && desc.Samples > 1); + VkSampleCountFlagBits resolvedSampleCount = VK_SAMPLE_COUNT_1_BIT; + if (isMultisample) { + if (!TryResolveSampleCountFlagBits(static_cast(desc.Samples), resolvedSampleCount)) { + MGLOG_W_ONCE("Magma wire texture {slot=%u, gen=%u}: unsupported sample count %u", + desc.Resource.Slot, desc.Resource.Gen, static_cast(desc.Samples)); + return false; + } + // glTexStorage*Multisample(samples = 1) is legal GL, but a one-sample image cannot + // back a sampler2DMS (VUID-RuntimeSpirv-samples-08726). + if (resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT) { + resolvedSampleCount = VK_SAMPLE_COUNT_2_BIT; + } + } + + const VkImageAspectFlags aspect = GetAspectMaskForFormat(format); + VkFormatProperties formatProperties{}; + vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties); + // The descriptor's sticky bind mask carries the image-unit hint, so the image is born + // with STORAGE usage and no re-mint ever pulls texels back (TextureEmit.h D-A3). + const Bool markedAsStorageImage = (desc.BindMask & MG_Pipe::kMGPipeBindShaderImage) != 0; + const Bool storageImageCapable = + !isMultisample && (aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 && + (formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0; + const Bool supportsStorageImage = storageImageCapable && markedAsStorageImage; + VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags; + if (shapeInfo.imageType == VK_IMAGE_TYPE_3D && !isMultisample && + m_2dArrayCompatibleUnsupported.find(format) == m_2dArrayCompatibleUnsupported.end()) { + imageCreateFlags |= VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT; + } + if (storageImageCapable && IsMutableStorageImageFormat(format) && + m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) { + imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; + } + // sRGB color images attach through their UNORM twin while GL_FRAMEBUFFER_SRGB is off. + if (ResolveSrgbAttachmentWriteFormat(format, false) != format && + (aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 && + m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) { + imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; + } + + 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_DEPTH_BIT) || (aspect & VK_IMAGE_ASPECT_STENCIL_BIT)) + ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT + : 0); + // Multisample attachments are also transfer sources for vkCmdResolveImage. + desiredUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + + // Round a multisample request up to a count the device supports for this format (GL + // only promises "at least"), the renderbuffer path's rule. + if (isMultisample && resolvedSampleCount != VK_SAMPLE_COUNT_1_BIT) { + auto supportedIt = m_multisampleCountsByFormat.find(format); + if (supportedIt == m_multisampleCountsByFormat.end()) { + VkImageFormatProperties imageFormatProperties{}; + VkSampleCountFlags supported = VK_SAMPLE_COUNT_1_BIT; + if (vkGetPhysicalDeviceImageFormatProperties(m_physicalDevice, format, shapeInfo.imageType, + VK_IMAGE_TILING_OPTIMAL, desiredUsage, imageCreateFlags, + &imageFormatProperties) == VK_SUCCESS) { + supported = imageFormatProperties.sampleCounts; + } + supportedIt = m_multisampleCountsByFormat.emplace(format, supported).first; + } + const VkSampleCountFlags supported = supportedIt->second; + if ((supported & resolvedSampleCount) == 0) { + Uint32 rounded = 0; + for (Uint32 bit = static_cast(resolvedSampleCount) << 1; bit <= VK_SAMPLE_COUNT_64_BIT; + bit <<= 1) { + if ((supported & bit) != 0) { + rounded = bit; + break; + } + } + if (rounded == 0) { + for (Uint32 bit = static_cast(resolvedSampleCount) >> 1; + bit > static_cast(VK_SAMPLE_COUNT_1_BIT); bit >>= 1) { + if ((supported & bit) != 0) { + rounded = bit; + break; + } + } + } + if (rounded == 0 && (supported & VK_SAMPLE_COUNT_1_BIT) != 0) { + MGLOG_W_ONCE("Magma wire texture: multisample format %d supports no count above one on this " + "device; backing it with a single sample", + static_cast(format)); + rounded = static_cast(VK_SAMPLE_COUNT_1_BIT); + } + if (rounded != 0) { + resolvedSampleCount = static_cast(rounded); + } + } + } + + // The ANGLE-shaped backing rule SyncTextureResource runs: a texture that has only ever + // defined level 0 gets a single-level backing; a second defined level recreates it ONCE + // with the full chain. The full-chain count bounds by the image's own extent (array + // layers are not a mip-able axis). + // The wire retains the GL base extent: a 1D-array's Height is its layer count. + const Uint32 imageHeight = shapeInfo.imageType == VK_IMAGE_TYPE_1D ? 1u : desc.Height; + const IntVec3 mipExtent{static_cast(desc.Width), static_cast(imageHeight), + static_cast(shapeInfo.depth)}; + const Uint32 fullMipLevels = ComputeFullMipLevelCount(mipExtent); + const Uint32 backingMipLevels = + isMultisample ? 1u + : (desc.Levels > 1 ? std::min(std::max(desc.Levels, fullMipLevels), fullMipLevels) + : 1u); + + const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format && + resource.extent.width == desc.Width && resource.extent.height == imageHeight && + resource.depth == shapeInfo.depth && resource.arrayLayers == shapeInfo.arrayLayers && + resource.viewType == shapeInfo.viewType && + resource.sampleCount == resolvedSampleCount && + resource.imageCreateFlags == imageCreateFlags && + resource.usageFlags == desiredUsage && resource.mipLevels >= backingMipLevels; + if (compatible) { + resource.storageUsageResolved = markedAsStorageImage; + return true; + } + + // A named-level respecify changes ONLY that level; a BindMask update changes no + // storage at all. Preserve every compatible old level when growing/upgrading the + // allocation. A whole-store redefinition permits (but does not require) old texels. + const Bool preserve = resource.image != VK_NULL_HANDLE && resource.format == format && + resource.extent.width == desc.Width && resource.extent.height == imageHeight && + resource.depth == shapeInfo.depth && resource.arrayLayers == shapeInfo.arrayLayers && + resource.viewType == shapeInfo.viewType && + resource.sampleCount == VK_SAMPLE_COUNT_1_BIT && + resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT && + resource.layout != VK_IMAGE_LAYOUT_UNDEFINED; + TextureResource replacement; + + VkImageCreateInfo imageInfo{}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.flags = imageCreateFlags; + imageInfo.imageType = shapeInfo.imageType; + imageInfo.extent.width = desc.Width; + imageInfo.extent.height = imageHeight; + imageInfo.extent.depth = shapeInfo.depth; + imageInfo.mipLevels = backingMipLevels; + imageInfo.arrayLayers = shapeInfo.arrayLayers; + imageInfo.format = format; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + imageInfo.usage = desiredUsage; + imageInfo.samples = resolvedSampleCount; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (isMultisample || (imageInfo.flags & (VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | + VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT)) != 0) { + VkImageFormatProperties imageFormatProperties{}; + VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties( + m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, imageInfo.flags, + &imageFormatProperties); + if (imageFormatResult != VK_SUCCESS && !isMultisample && + (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) { + // Losing reinterpreted views only degrades the formatless-image feature; + // failing creation would lose the texture entirely. + m_mutableFormatUnsupported.insert(format); + imageInfo.flags &= ~VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; + imageCreateFlags = imageInfo.flags; + imageFormatResult = vkGetPhysicalDeviceImageFormatProperties( + m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, + imageInfo.flags, &imageFormatProperties); + } + if (imageFormatResult != VK_SUCCESS && !isMultisample && + (imageInfo.flags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0) { + m_2dArrayCompatibleUnsupported.insert(format); + imageInfo.flags &= ~VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT; + imageCreateFlags = imageInfo.flags; + imageFormatResult = vkGetPhysicalDeviceImageFormatProperties( + m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, + imageInfo.flags, &imageFormatProperties); + } + if (imageFormatResult != VK_SUCCESS || + (isMultisample && (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0)) { + MGLOG_D("%s: image flags=0x%x sampleCount=%d are unsupported for texture {slot=%u, gen=%u}", + __func__, static_cast(imageInfo.flags), + static_cast(imageInfo.samples), desc.Resource.Slot, desc.Resource.Gen); + return false; + } + } + + VmaAllocationCreateInfo allocationInfo{}; + allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; + allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + // Soft failure, SyncTextureResource's rule: a driver can pass the pre-check yet refuse + // the creation; the texture stays unbacked and the caller declines. + const VkResult createImageResult = + vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &replacement.image, &replacement.allocation, nullptr); + if (createImageResult != VK_SUCCESS) { + MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: vmaCreateImage failed (%d) extent=%ux%u depth=%u " + "layers=%u mips=%u samples=%d format=%d", + desc.Resource.Slot, desc.Resource.Gen, createImageResult, imageInfo.extent.width, + imageInfo.extent.height, imageInfo.extent.depth, imageInfo.arrayLayers, + imageInfo.mipLevels, static_cast(imageInfo.samples), + static_cast(imageInfo.format)); + return false; + } + ++m_textureImageEpoch; // a new attachment image invalidates cached render passes + + replacement.layout = VK_IMAGE_LAYOUT_UNDEFINED; + replacement.extent = {imageInfo.extent.width, imageInfo.extent.height}; + replacement.depth = imageInfo.extent.depth; + replacement.arrayLayers = imageInfo.arrayLayers; + replacement.mipLevels = backingMipLevels; + replacement.sampledBaseMipLevel = 0; + replacement.sampledLevelCount = backingMipLevels; + replacement.format = format; + replacement.aspect = aspect; + replacement.viewType = shapeInfo.viewType; + replacement.sampleCount = resolvedSampleCount; + replacement.imageCreateFlags = imageInfo.flags; + replacement.usageFlags = desiredUsage; + replacement.storageUsageResolved = markedAsStorageImage; + if (preserve) { + FlushPendingUploads(); + if (!PreserveTextureContentsOnRecreate(m_device, m_commandPool, m_graphicsQueue, + resource, replacement, true)) return false; + } + DeferResourceRelease(Move(resource)); + std::destroy_at(&resource); + std::construct_at(&resource, Move(replacement)); + return true; + } + + Bool VkTextureManager::UploadPendingWireLevels(MG_Pipe::MGPipeHandle handle, + MG_Pipe::MGPipeResourceRecord& record, + TextureResource& resource) { + if (record.PendingUploads.empty()) { + return true; + } + auto& store = MG_Remote::Server::ServerStagedTexture(); + // This arm is only ever entered under an active transport, where the store copies. + MOBILEGL_ASSERT(store.CopiesIntoServerStorage(), + "UploadPendingWireLevels under a non-copying staged store: the wire arm is transport-only"); + const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(handle); + const TextureInternalFormat internalFormat = static_cast(record.Desc.InternalFormat); + const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(internalFormat); + const TextureTarget target = WireTextureTargetOfPipeTarget(record.Desc.Target); + + struct WireUploadItem { + Uint16 uploadTarget = 0; + Uint16 level = 0; + Uint32 baseArrayLayer = 0; + IntVec3 texelSize = {0, 0, 0}; + const void* source = nullptr; + SizeT byteSize = 0; + Vector owned; + VkDeviceSize offset = 0; + }; + Vector items; + VkDeviceSize stagingSize = 0; + Bool incomplete = false; + for (const auto& pending : record.PendingUploads) { + const Uint16 uploadTarget = MG_Pipe::MGPipeSubDataUploadTargetOf(pending.UploadTarget); + const Uint16 level = pending.Level; + if (level >= resource.mipLevels) { + // A pending entry the current image cannot hold: the level was defined past the + // accepted descriptor's chain. Loud, and left in the set - the next respecify + // or generation grows into it or drops it. + 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); + const SizeT byteSize = store.LevelByteSize(key, uploadTarget, level); + // The staged run IS the whole level shadow (StagedTextureStore.h's coverage rule): + // it exists because the applier accepted this entry, so a miss is the store's own + // Fatal{StageSnapshotTooNarrow}, not a silent zero. + const Uint8* bytes = store.RequireLevelBytes(key, uploadTarget, level, + "VkTextureManager::UploadPendingWireLevels"); + // 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); + if (texelSize.x() <= 0 || texelSize.y() <= 0 || byteSize == 0) { + incomplete = true; + continue; + } + WireUploadItem item{}; + item.uploadTarget = uploadTarget; + item.level = level; + item.baseArrayLayer = ResolveUploadArrayLayer(static_cast(uploadTarget)); + item.texelSize = texelSize; + if (!ConvertWireLevelBytes(bytes, byteSize, texelSize, resource.format, resource.aspect, + internalFormat, formatInfo, + item.owned, item.source, item.byteSize)) { + MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: no shadow-to-image conversion for format %d; " + "level %u left pending", + handle.Slot, handle.Gen, static_cast(resource.format), + static_cast(level)); + incomplete = true; + continue; + } + // Each VkBufferImageCopy offset must satisfy the format's texel-block alignment + // as well as Vulkan's four-byte rule. All supported blocks divide 16 bytes; + // AcquireUploadStagingSpace aligns the batch base to at least this boundary. + stagingSize = (stagingSize + 15u) & ~VkDeviceSize{15u}; + item.offset = stagingSize; + stagingSize += static_cast(item.byteSize); + items.push_back(Move(item)); + } + if (incomplete) return false; + if (items.empty()) { + return true; + } + + // UploadDirtyMipLevels' batching rules, verbatim: flush an open batch that already + // writes this image and was touched by the open recording, and bound the staging bytes + // a batch pins. + if (m_uploadBatchOpen && WasTouchedThisRecording(resource) && + std::find(m_uploadBatchImages.begin(), m_uploadBatchImages.end(), resource.image) != + m_uploadBatchImages.end()) { + FlushPendingUploads(); + } + constexpr VkDeviceSize kMaxBatchStagingBytes = 64u * 1024u * 1024u; + if (m_uploadBatchOpen && m_uploadBatchStagingBytes + stagingSize > kMaxBatchStagingBytes) { + FlushPendingUploads(); + } + + VkCommandBuffer commandBuffer = EnsureUploadBatchOpen(); + VkBuffer stagingBuffer = VK_NULL_HANDLE; + VkDeviceSize stagingBase = 0; + Uint8* mapped = AcquireUploadStagingSpace(stagingSize, stagingBuffer, stagingBase); + for (const auto& item : items) { + std::memcpy(mapped + item.offset, item.source, item.byteSize); + } + + const VkImageAspectFlags aspectMask = resource.aspect; + VkPipelineStageFlags uploadSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags uploadSrcAccessMask = 0; + GetImageTransitionSourceState(resource.layout, uploadSrcStageMask, uploadSrcAccessMask); + Bool ok = TransitionImageLayout(commandBuffer, resource.image, resource.layout, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, uploadSrcStageMask, + VK_PIPELINE_STAGE_TRANSFER_BIT, uploadSrcAccessMask, + VK_ACCESS_TRANSFER_WRITE_BIT, aspectMask, 0, resource.mipLevels); + MOBILEGL_ASSERT(ok, "UploadPendingWireLevels: transition to TRANSFER_DST failed"); + + const Bool isCombinedDepthStencil = + (aspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 && (aspectMask & VK_IMAGE_ASPECT_STENCIL_BIT) != 0; + const Bool depthSelectsArrayLayer = resource.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY || + resource.viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY || + resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY; + for (const auto& item : items) { + const Uint32 depthOrLayers = + item.texelSize.z() > 0 ? static_cast(item.texelSize.z()) : 1u; + VkBufferImageCopy copy{}; + copy.bufferOffset = stagingBase + item.offset; + copy.bufferRowLength = 0; + copy.bufferImageHeight = 0; + copy.imageSubresource.aspectMask = aspectMask; + copy.imageSubresource.mipLevel = item.level; + copy.imageSubresource.baseArrayLayer = item.baseArrayLayer; + copy.imageSubresource.layerCount = depthSelectsArrayLayer ? depthOrLayers : 1; + copy.imageOffset = {0, 0, 0}; + copy.imageExtent = {static_cast(item.texelSize.x()), + static_cast(item.texelSize.y()), + depthSelectsArrayLayer ? 1u : depthOrLayers}; + if (isCombinedDepthStencil) { + // Per-aspect copies: VkBufferImageCopy's aspectMask must name exactly one + // aspect; the de-interleaved run is the depth plane followed by stencil bytes. + const SizeT texelCount = static_cast(item.texelSize.x()) * + static_cast(item.texelSize.y()) * + static_cast(std::max(item.texelSize.z(), 1)); + VkBufferImageCopy depthCopy = copy; + depthCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + VkBufferImageCopy stencilCopy = copy; + stencilCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT; + stencilCopy.bufferOffset = stagingBase + item.offset + static_cast(texelCount) * 4; + const VkBufferImageCopy copies[2] = {depthCopy, stencilCopy}; + vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, resource.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 2, copies); + } else { + vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, resource.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + } + } + + const VkImageLayout finalLayout = ResolveSampledReadOnlyLayout(aspectMask); + VkImageLayout uploadLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + ok = TransitionImageLayout(commandBuffer, resource.image, uploadLayout, finalLayout, + VK_PIPELINE_STAGE_TRANSFER_BIT, s_sampledReadStages, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, aspectMask, 0, + resource.mipLevels); + MOBILEGL_ASSERT(ok, "UploadPendingWireLevels: transition to the sampled layout failed"); + resource.layout = finalLayout; + + if (std::find(m_uploadBatchImages.begin(), m_uploadBatchImages.end(), resource.image) == + m_uploadBatchImages.end()) { + m_uploadBatchImages.push_back(resource.image); + } + m_uploadBatchStagingBytes += stagingSize; + + // The bytes are staged; NOW the entries leave the pending set (D-D5). + for (const auto& item : items) { + ConsumeWireTextureUpload(record, item.uploadTarget, item.level); + } + return true; + } + + VkTextureManager::TextureResource* VkTextureManager::SyncTextureResourceByHandle( + MG_Pipe::MGPipeHandle handle, Bool renderbuffer) { + MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "SyncTextureResourceByHandle: m_device == VK_NULL_HANDLE"); + if (MG_Pipe::MGPipeHandleIsNull(handle)) { + return nullptr; + } + auto& applier = MG_Pipe::MGPipeApplier(); + auto& records = renderbuffer ? applier.RenderbufferResources : applier.TextureResources; + auto& resources = renderbuffer ? m_wireRenderbufferResources : m_wireTextureResources; + if (handle.Slot >= records.size()) { + MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: no applier record at that slot; declined", + handle.Slot, handle.Gen); + return nullptr; + } + auto& record = records[handle.Slot]; + if (!record.Live || record.Gen != handle.Gen) { + // FramebufferRecordFor's rule: a dead or recycled slot is a loud refusal, never a + // quiet answer from whatever the slot holds today. + MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: the applier's record is %s; declined", + handle.Slot, handle.Gen, + !record.Live ? "dead" : "of another generation"); + return nullptr; + } + const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(handle); + auto [it, inserted] = resources.try_emplace(key); + (void)inserted; + TextureResource& resource = it->second; + if (resource.image != VK_NULL_HANDLE && resource.syncedWireSerial == record.Serial && + record.PendingUploads.empty()) { + return &resource; + } + if (!SyncWireTextureShape(record, resource)) { + return nullptr; + } + if (!renderbuffer && !UploadPendingWireLevels(handle, record, resource)) { + return nullptr; + } + resource.syncedWireSerial = record.Serial; + return &resource; + } + + Bool VkTextureManager::GrowWireTextureMipChain(MG_Pipe::MGPipeHandle handle, Uint32 requiredMipLevels, + VkCommandBuffer commandBuffer) { + if (MG_Pipe::MGPipeHandleIsNull(handle)) { + return false; + } + const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(handle); + auto it = m_wireTextureResources.find(key); + if (it == m_wireTextureResources.end() || it->second.image == VK_NULL_HANDLE) { + return false; + } + TextureResource& resource = it->second; + if (resource.mipLevels >= requiredMipLevels) { + return true; + } + if (resource.sampleCount != VK_SAMPLE_COUNT_1_BIT) { + return false; // a multisample image has no mip chain to grow + } + + // The caller (GenerateMipmap's record arm) flushed every pending submission, so the old + // image is GPU-idle; the carry-over copy is recorded straight into the frame's command + // buffer and the old image is parked on the deferred-release ring behind it. + // Keep the live allocation intact until allocation of its replacement succeeds. + TextureResource& old = resource; + TextureResource grown; + + VkImageCreateInfo imageInfo{}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.flags = old.imageCreateFlags; + imageInfo.imageType = old.viewType == VK_IMAGE_VIEW_TYPE_1D || old.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY + ? VK_IMAGE_TYPE_1D + : (old.viewType == VK_IMAGE_VIEW_TYPE_3D ? VK_IMAGE_TYPE_3D : VK_IMAGE_TYPE_2D); + imageInfo.extent = {old.extent.width, old.extent.height, old.depth}; + imageInfo.mipLevels = requiredMipLevels; + imageInfo.arrayLayers = old.arrayLayers; + imageInfo.format = old.format; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + imageInfo.usage = old.usageFlags; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + VmaAllocationCreateInfo allocationInfo{}; + allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; + allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + const VkResult createImageResult = + vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &grown.image, &grown.allocation, nullptr); + if (createImageResult != VK_SUCCESS) { + MGLOG_E_ONCE("GrowWireTextureMipChain: vmaCreateImage failed (%d)", createImageResult); + return false; + } + ++m_textureImageEpoch; + + const VkImageAspectFlags aspect = old.aspect; + const VkImageLayout oldLayout = old.layout; + VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags srcAccessMask = 0; + GetImageTransitionSourceState(oldLayout, srcStageMask, srcAccessMask); + Bool ok = TransitionImageLayout(commandBuffer, old.image, old.layout, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, + VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, + VK_ACCESS_TRANSFER_READ_BIT, aspect, 0, old.mipLevels); + MOBILEGL_ASSERT(ok, "GrowWireTextureMipChain: source transition failed"); + VkImageLayout grownLayout = VK_IMAGE_LAYOUT_UNDEFINED; + ok = TransitionImageLayout(commandBuffer, grown.image, grownLayout, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT, aspect, 0, + requiredMipLevels); + MOBILEGL_ASSERT(ok, "GrowWireTextureMipChain: destination transition failed"); + + for (Uint32 level = 0; level < old.mipLevels; ++level) { + VkImageCopy copy{}; + copy.srcSubresource = {aspect, level, 0, old.arrayLayers}; + copy.dstSubresource = {aspect, level, 0, old.arrayLayers}; + copy.srcOffset = {0, 0, 0}; + copy.dstOffset = {0, 0, 0}; + copy.extent = {std::max(old.extent.width >> level, 1u), std::max(old.extent.height >> level, 1u), + std::max(old.depth >> level, 1u)}; + for (const VkImageAspectFlags copyAspect : {VK_IMAGE_ASPECT_COLOR_BIT, + VK_IMAGE_ASPECT_DEPTH_BIT, + VK_IMAGE_ASPECT_STENCIL_BIT}) { + if ((aspect & copyAspect) == 0) continue; + copy.srcSubresource.aspectMask = copyAspect; + copy.dstSubresource.aspectMask = copyAspect; + vkCmdCopyImage(commandBuffer, old.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, grown.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + } + } + + VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags dstAccessMask = 0; + GetImageTransitionDestinationState(oldLayout, dstStageMask, dstAccessMask); + ok = TransitionImageLayout(commandBuffer, grown.image, grownLayout, oldLayout, + VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, + dstAccessMask, aspect, 0, requiredMipLevels); + MOBILEGL_ASSERT(ok, "GrowWireTextureMipChain: restore transition failed"); + + grown.layout = oldLayout; + grown.extent = old.extent; + grown.depth = old.depth; + grown.arrayLayers = old.arrayLayers; + grown.mipLevels = requiredMipLevels; + grown.sampledBaseMipLevel = old.sampledBaseMipLevel; + grown.sampledLevelCount = requiredMipLevels; + grown.format = old.format; + grown.aspect = old.aspect; + grown.viewType = old.viewType; + grown.sampleCount = old.sampleCount; + grown.imageCreateFlags = old.imageCreateFlags; + grown.usageFlags = old.usageFlags; + grown.storageUsageResolved = old.storageUsageResolved; + grown.syncedWireSerial = old.syncedWireSerial; + + DeferResourceRelease(Move(resource)); + std::destroy_at(&resource); + std::construct_at(&resource, Move(grown)); + return true; + } +#endif // MOBILEGL_BUILD_DISAGGREGATED + Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture, TextureResource &outResource) { #if MOBILEGL_BUILD_DISAGGREGATED diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 117b8424..ac28bb1d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -17,9 +17,21 @@ #include #include +#include +#if MOBILEGL_BUILD_DISAGGREGATED +// The handle-keyed arm's key type. Header-only POD, disagg-only, exactly the shape +// MagmaPipeArms.h already gives this directory. +#include +#endif + namespace MobileGL::MG_State::GLState { class ITextureObject; } +#if MOBILEGL_BUILD_DISAGGREGATED +namespace MobileGL::MG_Pipe { +struct MGPipeResourceRecord; +} +#endif namespace MobileGL::MG_Backend::DirectVulkan { enum class SamplerNumericDomain : Uint8; @@ -333,6 +345,13 @@ public: // shape version and nothing else. Without this in the early-out key the image, its views // and therefore imageSize() all keep answering with the texture's PREVIOUS shape. Uint64 syncedShapeVersion = 0; + // The handle-keyed arm's (P5f fm) early-out key: the applier record's server-owned + // Serial at the last successful sync. 0 also means "this resource has only ever been + // synced by the frontend arm" - a wire record's Serial starts at 0 on a create and moves + // on every mutation, so the first handle sync of a fresh image always re-checks. +#if MOBILEGL_BUILD_DISAGGREGATED + Uint64 syncedWireSerial = 0; +#endif TextureResource() = default; TextureResource(const TextureResource&) = delete; @@ -365,6 +384,9 @@ public: std::swap(this->syncedContentVersion, that.syncedContentVersion); std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount); std::swap(this->syncedShapeVersion, that.syncedShapeVersion); +#if MOBILEGL_BUILD_DISAGGREGATED + std::swap(this->syncedWireSerial, that.syncedWireSerial); +#endif } void Reset() { @@ -429,6 +451,9 @@ public: syncedContentVersion = 0; syncedMipLevelCount = 0; syncedShapeVersion = 0; +#if MOBILEGL_BUILD_DISAGGREGATED + syncedWireSerial = 0; +#endif } ~TextureResource() { @@ -514,6 +539,30 @@ public: TextureResource* SyncTextureAndGetDescriptor( MG_State::GLState::ITextureObject& texture); +#if MOBILEGL_BUILD_DISAGGREGATED + // P5f (fm): THE HANDLE-KEYED ARM of SyncTextureAndGetDescriptor. Under an active transport + // the apply thread may not name the client's ITextureObject (rule E), so the clear / blit / + // readback / mipmap verbs resolve their attachment textures from the wire handle the + // framebuffer or mip record carried: shape from the applier's resource record + // (MGPipeApplier().TextureResources), texels from the server's staged-texture store + // (P5c tx), pending-upload bookkeeping consumed from the same record. Returns null - never + // a frontend fallback - when the record or the shape does not resolve; the caller declines + // loudly, which is the honest answer a missing record deserves. + // + // The resource lives in m_wireTextureResources keyed by StagedTextureStore::KeyForHandle - + // a NAMESPACE THE FRONTEND-KEYED m_textureResources NEVER MEETS, so a recycled {slot, gen} + // can never inherit its predecessor's image. Textures whose GL object is a VIEW of another + // (desc.ViewOf) or whose storage is a buffer are declined here: no view chain crosses today. + TextureResource* SyncTextureResourceByHandle(MG_Pipe::MGPipeHandle handle, Bool renderbuffer = false); + // Grows the live image of a handle-keyed resource to `requiredMipLevels`, carrying the + // existing levels' content across with an in-command-buffer copy. The caller + // (GenerateMipmap's record arm) has flushed every pending submission first, so the old + // image is GPU-idle and the copy may be recorded into `commandBuffer` directly; the old + // image goes to the deferred-release ring. False when the chain cannot grow (the resource + // stays as it was). + Bool GrowWireTextureMipChain(MG_Pipe::MGPipeHandle handle, Uint32 requiredMipLevels, + VkCommandBuffer commandBuffer); +#endif VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel); VkImageView GetOrCreateAttachmentViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel, Uint32 baseArrayLayer, Uint32 layerCount, @@ -670,6 +719,21 @@ private: void EraseTrackedTexture(const TextureIdentity& identity); void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture); SizeT PruneDeadTextures(); +#if MOBILEGL_BUILD_DISAGGREGATED + // The two halves of SyncTextureResourceByHandle: shape from the resource record's + // descriptor (create / recreate / compatibility), then the pending-upload walk against the + // server's staged store. UploadPendingWireLevels mutates the applier's record (consumed + // entries are dropped), which is why the record is a non-const reference. + Bool SyncWireTextureShape(const MG_Pipe::MGPipeResourceRecord& record, TextureResource& resource); + Bool UploadPendingWireLevels(MG_Pipe::MGPipeHandle handle, MG_Pipe::MGPipeResourceRecord& record, + TextureResource& resource); + // The handle-keyed twin of m_textureResources, keyed by StagedTextureStore::KeyForHandle. + // Node-based for the same reason m_renderbufferResources is: callers hold TextureResource* + // across map-touching calls. + std::unordered_map m_wireTextureResources; + // Handles are per-kind; a renderbuffer and texture may have identical slot/gen. + std::unordered_map m_wireRenderbufferResources; +#endif VkDevice m_device = VK_NULL_HANDLE; VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index b019c180..a3a3459b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -7636,7 +7636,22 @@ void main() { return ScissoredClearPrep::Ready; } + #include "WireFramebuffer.inc" + void VulkanRenderer::Clear(GLbitfield mask) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer(); + if (!fbo) MagmaWireFatal("clear-framebuffer-record"); + ClearAttachmentPayload payload{}; + payload.mask = mask; + payload.color = MGB_CTX->GetClearColor(); + payload.depth = MGB_CTX->GetClearDepth(); + payload.stencil = MGB_CTX->GetClearStencil(); + ClearWireFramebuffer(*fbo, payload); + return; + } +#endif m_clearManager->CollectGarbage(); if ((mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) == 0) { return; @@ -8013,6 +8028,14 @@ void main() { void VulkanRenderer::QueueClearBufferPayload(GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer(); + if (!fbo) MagmaWireFatal("clear-buffer-framebuffer-record"); + ClearWireFramebuffer(*fbo,clearPayload,buffer == GL_COLOR ? drawbuffer : -1); + return; + } +#endif auto* fbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get(); if (!fbo) { return; @@ -8972,53 +8995,9 @@ void main() { GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { #if MOBILEGL_BUILD_DISAGGREGATED - // P5c (G6, CONTRACT-P5C §3.3/§5.4): MAGMA'S NAMED ARM. A blit record whose - // ReadFbo/DrawFbo are non-null is a glBlitNamedFramebuffer: both framebuffers resolve - // from the record's handles, and the sink is told the pair was consumed - a backend - // that leaves the flag clear has no named arm and the verb declines there, loudly. - // Magma has no FBO twin registry (it reads the frontend object wherever it syncs), so - // the resolution here is frontend-keyed - the handle was minted over the frontend - // object's lifetime id, and the two probes below (the client allocator's slot entry - // and the frontend context's framebuffer pool) are named debt inside the scope, the - // same shape as Espryt's StateForHandle arm: P7 retires it by carrying the object - // identity in the record. P5e (id), ruling 12: the third of Magma's four debts, so the - // scope it rides in is MagmaP7AllocatorDebtScope. Magma stays lockstep for the whole of - // P5e (CONTRACT-P5E §6.1), so the record being applied here is always barriered and the - // exemption still holds. if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { - auto& applierState = MG_Pipe::MGPipeApplier(); - const MG_Pipe::MGPipeHandle readHandle = applierState.VerbBlitReadFbo; - const MG_Pipe::MGPipeHandle drawHandle = applierState.VerbBlitDrawFbo; - if (!MG_Pipe::MGPipeHandleIsNull(readHandle) || !MG_Pipe::MGPipeHandleIsNull(drawHandle)) { - const auto resolveEndpoint = [](MG_Pipe::MGPipeHandle handle) - -> SharedPtr { - const MG_Pipe::MagmaP7AllocatorDebtScope magmaP7AllocatorDebt; - if (handle == MG_Pipe::kMGPipeDefaultFramebuffer) { - return MG_State::pGLContext ? MG_State::pGLContext->GetFramebufferObject(0) : nullptr; - } - if (!MG_Pipe::MGPipeSlots().IsLive(MG_Pipe::MGPipeKind::Framebuffer, handle)) { - return nullptr; - } - const Uint64 lifetimeId = - MG_Pipe::MGPipeSlots().LifetimeIdOfSlot(MG_Pipe::MGPipeKind::Framebuffer, handle.Slot); - if (lifetimeId == 0 || MG_State::pGLContext == nullptr) return nullptr; - return MG_State::pGLContext->FindFramebufferObjectByLifetimeId(lifetimeId); - }; - auto readFbo = resolveEndpoint(readHandle); - auto drawFbo = resolveEndpoint(drawHandle); - if (!readFbo || !drawFbo) { - // Leave the pair UNCONSUMED: the sink's decline is the loud answer a - // missing endpoint deserves, not a silent blit of whatever is bound. - MGLOG_E_ONCE("MGPipe: Magma's named blit could not resolve an endpoint (read {%u, %u}, draw " - "{%u, %u}) to a frontend framebuffer; the verb declines at the sink", - readHandle.Slot, readHandle.Gen, drawHandle.Slot, drawHandle.Gen); - return; - } - applierState.VerbBlitNamedConsumed = true; - BlitNamedFramebuffer(readFbo, drawFbo, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, - dstX1, dstY1, mask, filter); - return; - } + BlitWireFramebuffers(srcX0,srcY0,srcX1,srcY1,dstX0,dstY0,dstX1,dstY1,mask,filter); + return; } #endif auto readFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(); @@ -10364,6 +10343,12 @@ void main() { void VulkanRenderer::ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + ReadWirePixels(x,y,width,height,format,type,pixels); + return; + } +#endif if (width <= 0 || height <= 0) { return; } @@ -11376,6 +11361,12 @@ void main() { } void VulkanRenderer::GenerateMipmap(GLenum target) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + GenerateWireMipmap(); + return; + } +#endif const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); // Whatever is left here is a coverage gap in this backend, not a broken invariant, so it // declines (leaving the mip chain unwritten) rather than asserting the process down. What diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index de43cb2c..da3801e5 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -433,6 +433,29 @@ namespace MobileGL::MG_Backend::DirectVulkan { }; void QueueClearBufferPayload(GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload); +#if MOBILEGL_BUILD_DISAGGREGATED + struct WireImage { + VkImage image = VK_NULL_HANDLE; + VkFormat format = VK_FORMAT_UNDEFINED; + VkExtent2D extent{}; + VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED; + VkImageLayout* trackedLayout = nullptr; + VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT; + VkImageAspectFlags aspect = 0; + Uint32 level = 0, layer = 0, layers = 1, levels = 1; + Bool isDefault = false; + }; + WireImage ResolveWireImage(const MG_Pipe::MGPFramebufferState& fbo, + const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect); + void TransitionWireImage(WireImage& image, VkImageLayout layout); + void ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo, + const ClearAttachmentPayload& payload, GLint drawbuffer = -1); + void BlitWireFramebuffers(GLint sx0, GLint sy0, GLint sx1, GLint sy1, + GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLbitfield mask, GLenum filter); + void ReadWirePixels(GLint x, GLint y, GLsizei width, GLsizei height, + GLenum format, GLenum type, void* pixels); + void GenerateWireMipmap(); +#endif void QueueClearBufferPayloadForFramebuffer(const MG_State::GLState::FramebufferObject& framebuffer, GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc new file mode 100644 index 00000000..d48e698b --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc @@ -0,0 +1,358 @@ +// P5f fm: included by VulkanRenderer.cpp inside the backend namespace. All inputs here +// belong to the applier; no frontend object, lifetime-id lookup or allocator is used. +#if MOBILEGL_BUILD_DISAGGREGATED + [[noreturn]] static void MagmaWireFatal(const char* detail) { + MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"Magma:%s\"}", detail); + std::abort(); + } + + VulkanRenderer::WireImage VulkanRenderer::ResolveWireImage( + const MG_Pipe::MGPFramebufferState& fbo, const MG_Pipe::MGPSurface& surface, + VkImageAspectFlags aspect) { + WireImage out{}; + out.aspect = aspect; + if (fbo.IsDefault) { + out.isDefault = true; + out.extent = m_swapchainObject.GetExtent(); + if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) { + out.image = m_swapchainObject.GetImage(m_imageIndexAcquired); + out.format = m_swapchainObject.GetSurfaceFormat().format; + out.layout = m_swapchainObject.GetImageLayout(m_imageIndexAcquired); + } else { + out.image = m_swapchainObject.GetDepthStencilImage(m_imageIndexAcquired); + out.format = m_swapchainObject.GetDepthStencilFormat(); + out.layout = m_swapchainObject.GetDepthStencilImageLayout(m_imageIndexAcquired); + } + return out; + } + if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone) return out; + auto* resource = m_textureManager->SyncTextureResourceByHandle(surface.Res, + surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer); + if (!resource) MagmaWireFatal("framebuffer-resource"); + out.image = resource->image; + out.format = resource->format; + out.extent = {std::max(resource->extent.width >> surface.Level, 1u), + std::max(resource->extent.height >> surface.Level, 1u)}; + out.layout = resource->layout; + out.trackedLayout = &resource->layout; + out.samples = resource->sampleCount; + out.level = surface.Level; + out.levels = resource->mipLevels; + out.layer = surface.Layer; + out.layers = surface.Layered ? resource->arrayLayers : 1; + if (out.level >= resource->mipLevels || out.layer + out.layers > resource->arrayLayers) + MagmaWireFatal("framebuffer-subresource"); + return out; + } + + void VulkanRenderer::TransitionWireImage(WireImage& image, VkImageLayout layout) { + auto& frame = m_frameContext.GetCurrent(); + if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); + if (VkRenderPassManager::GetActiveRenderPass()) VkRenderPassManager::EndRenderPass(frame.commandBuffer); + VkPipelineStageFlags stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags access = 0; + GetImageTransitionSourceState(image.layout, stage, access); + // One tracked layout per image: move the complete mip chain, never just the surface. + VkImageAspectFlags barrierAspect = image.aspect; + if (image.format == VK_FORMAT_D24_UNORM_S8_UINT || image.format == VK_FORMAT_D32_SFLOAT_S8_UINT) + barrierAspect = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer, image.image, image.layout, layout, + stage, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, access, + VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, barrierAspect, 0, image.levels)) + MagmaWireFatal("image-transition"); + if (image.trackedLayout) *image.trackedLayout = image.layout; + if (image.isDefault) { + if (image.aspect == VK_IMAGE_ASPECT_COLOR_BIT) + m_swapchainObject.SetImageLayout(m_imageIndexAcquired, image.layout); + else m_swapchainObject.SetDepthStencilImageLayout(m_imageIndexAcquired, image.layout); + } + } + + void VulkanRenderer::ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo, + const ClearAttachmentPayload& payload, GLint drawbuffer) { + if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) return; + const auto clearSurface = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect, + Uint32 colorIndex) { + if (!fbo.IsDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone) return; + if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) { + const auto mask = MGB_CTX->GetColorMaskIndexed(colorIndex); + if (!mask.r() && !mask.g() && !mask.b() && !mask.a()) return; + if (!mask.r() || !mask.g() || !mask.b() || !mask.a()) + MagmaWireFatal("partial-color-clear@P7"); + } + if (aspect & VK_IMAGE_ASPECT_DEPTH_BIT) { + if (!MGB_CTX->GetDepthMask()) aspect &= ~VK_IMAGE_ASPECT_DEPTH_BIT; + } + if (aspect & VK_IMAGE_ASPECT_STENCIL_BIT) { + const Uint32 mask = MGB_CTX->GetStencilState(StencilFace::Front).WriteMask & 255u; + if (!mask) aspect &= ~VK_IMAGE_ASPECT_STENCIL_BIT; + else if (mask != 255u) MagmaWireFatal("partial-stencil-clear@P7"); + } + if (!aspect) return; + WireImage image = ResolveWireImage(fbo, surface, aspect); + if (!image.image) return; + m_textureManager->FlushPendingUploads(); + VkRect2D area{{0, 0}, image.extent}; + if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) { + const auto& box = MGB_CTX->GetScissorBox(); + const Int32 x0 = std::max(box.x(), 0), y0 = std::max(box.y(), 0); + const Int32 x1 = std::min(box.x() + box.z(), static_cast(image.extent.width)); + const Int32 y1 = std::min(box.y() + box.w(), static_cast(image.extent.height)); + if (x0 >= x1 || y0 >= y1) return; + area = {{x0, y0}, {static_cast(x1-x0), static_cast(y1-y0)}}; + if (image.isDefault && !MapDefaultFramebufferReadbackRect(x0, y0, x1-x0, y1-y0, + image.extent, m_swapchainObject.GetPreTransform(), &area.offset, &area.extent)) + MagmaWireFatal("clear-surface-transform"); + } + const Bool color = aspect == VK_IMAGE_ASPECT_COLOR_BIT; + const auto layout = color ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + TransitionWireImage(image, layout); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image.image; + viewInfo.viewType = image.layers > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = image.format; + viewInfo.subresourceRange = {color ? VK_IMAGE_ASPECT_COLOR_BIT : + (image.format == VK_FORMAT_D24_UNORM_S8_UINT || image.format == VK_FORMAT_D32_SFLOAT_S8_UINT ? VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT : image.aspect), image.level, 1, image.layer, image.layers}; + VkImageView view = VK_NULL_HANDLE; + VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &view)); + VkAttachmentDescription attachment{}; + attachment.format = image.format; + attachment.samples = image.samples; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.initialLayout = layout; + attachment.finalLayout = layout; + VkAttachmentReference ref{0, layout}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = color ? 1 : 0; + subpass.pColorAttachments = color ? &ref : nullptr; + subpass.pDepthStencilAttachment = color ? nullptr : &ref; + VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + passInfo.attachmentCount = 1; passInfo.pAttachments = &attachment; + passInfo.subpassCount = 1; passInfo.pSubpasses = &subpass; + VkRenderPass pass = VK_NULL_HANDLE; + VK_VERIFY(vkCreateRenderPass(m_device, &passInfo, nullptr, &pass)); + VkFramebufferCreateInfo fbInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + fbInfo.renderPass = pass; fbInfo.attachmentCount = 1; fbInfo.pAttachments = &view; + fbInfo.width = image.extent.width; fbInfo.height = image.extent.height; fbInfo.layers = image.layers; + VkFramebuffer fb = VK_NULL_HANDLE; + VK_VERIFY(vkCreateFramebuffer(m_device, &fbInfo, nullptr, &fb)); + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = pass; begin.framebuffer = fb; begin.renderArea = area; + auto clearPayload = payload; + if (color) PreCompensateSrgbClearColor(clearPayload, image.format); + VkClearAttachment clear{}; + clear.aspectMask = aspect; + clear.clearValue.color = MakeVkClearColorValue(clearPayload, false); + if (!color) clear.clearValue.depthStencil = {payload.depth, payload.stencil}; + VkClearRect rect{area, 0, image.layers}; + auto& frame = m_frameContext.GetCurrent(); + vkCmdBeginRenderPass(frame.commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE); + vkCmdClearAttachments(frame.commandBuffer, 1, &clear, 1, &rect); + vkCmdEndRenderPass(frame.commandBuffer); + // P5f changes ownership, not Magma's wait contract. Synchronous retirement keeps + // these temporary server resources bounded and makes their lifetime explicit. + if (!SubmitReadbackCommandsAndWait(frame)) MagmaWireFatal("clear-submit"); + vkDestroyFramebuffer(m_device, fb, nullptr); + vkDestroyRenderPass(m_device, pass, nullptr); + vkDestroyImageView(m_device, view, nullptr); + if (image.isDefault) { + if (color) m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired, true); + else m_swapchainObject.SetDepthStencilContentDefined(m_imageIndexAcquired, true); + } + }; + if (payload.mask & GL_COLOR_BUFFER_BIT) { + for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) { + if (drawbuffer >= 0 && static_cast(drawbuffer) != i) continue; + const Int32 attachment = fbo.DrawBuffers[i]; + if (attachment >= 0 && attachment < static_cast(MG_Pipe::kMGPipeMaxColorAttachments)) + clearSurface(fbo.Color[attachment], VK_IMAGE_ASPECT_COLOR_BIT, i); + } + } + if (payload.mask & GL_DEPTH_BUFFER_BIT) clearSurface(fbo.Depth, VK_IMAGE_ASPECT_DEPTH_BIT, 0); + if (payload.mask & GL_STENCIL_BUFFER_BIT) clearSurface(fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT, 0); + } + + void VulkanRenderer::ReadWirePixels(GLint x, GLint y, GLsizei width, GLsizei height, + GLenum format, GLenum type, void* pixels) { + if (width <= 0 || height <= 0) return; + const auto* fbo = MG_Pipe::MGPipeApplier().ReadFramebuffer(); + if (!fbo) MagmaWireFatal("read-framebuffer-record"); + VkImageAspectFlags aspect = format == GL_DEPTH_COMPONENT ? VK_IMAGE_ASPECT_DEPTH_BIT : + format == GL_STENCIL_INDEX ? VK_IMAGE_ASPECT_STENCIL_BIT : + VK_IMAGE_ASPECT_COLOR_BIT; + if (format == GL_DEPTH_STENCIL) MagmaWireFatal("combined-depth-stencil-read@P7"); + const auto& surface = aspect == VK_IMAGE_ASPECT_DEPTH_BIT ? fbo->Depth : + aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? fbo->Stencil : fbo->ReadSurface; + WireImage source = ResolveWireImage(*fbo, surface, aspect); + if (!source.image) return; + m_textureManager->FlushPendingUploads(); + TransitionWireImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + const SizeT texelSize = aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? 1 : GetReadbackTexelSize(source.format); + if (!texelSize) MagmaWireFatal("read-format"); + const VkDeviceSize size = static_cast(width) * height * texelSize; + VkBufferObject buffer; + if (!buffer.Create({.allocator=m_allocator, .size=size, .usage=VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memoryUsage=VMA_MEMORY_USAGE_AUTO, .allocationFlags=VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT})) + MagmaWireFatal("read-buffer-allocation"); + VkOffset2D offset{x,y}; + VkExtent2D extent{static_cast(width), static_cast(height)}; + if (source.isDefault && !MapDefaultFramebufferReadbackRect(x,y,width,height,source.extent, + m_swapchainObject.GetPreTransform(),&offset,&extent)) MagmaWireFatal("read-surface-transform"); + VkBufferImageCopy region{}; + region.imageSubresource = {aspect,source.level,source.layer,1}; + region.imageOffset = {offset.x,offset.y,0}; + region.imageExtent = {extent.width,extent.height,1}; + auto& frame = m_frameContext.GetCurrent(); + vkCmdCopyImageToBuffer(frame.commandBuffer,source.image,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + buffer.GetHandle(),1,®ion); + if (!SubmitReadbackCommandsAndWait(frame)) MagmaWireFatal("read-submit"); + const auto* bytes = static_cast(buffer.Map()); + if (!bytes || !buffer.Invalidate(size)) MagmaWireFatal("read-map"); + Vector remapped; + if (source.isDefault) { + remapped.resize(static_cast(size)); + if (!RemapDefaultFramebufferReadback(bytes,width,height,m_swapchainObject.GetPreTransform(), + texelSize,remapped.data())) MagmaWireFatal("read-remap"); + bytes = remapped.data(); + } + if (aspect != VK_IMAGE_ASPECT_COLOR_BIT) { + if (format == GL_DEPTH_COMPONENT && type == GL_FLOAT && source.format == VK_FORMAT_D32_SFLOAT) { + std::memcpy(pixels,bytes,static_cast(size)); + return; + } + MagmaWireFatal("depth-read-conversion@P7"); + } + if (!PackReadbackToClientOrPbo(bytes,source.format,width,height,1,format,type,pixels,false,true)) + MagmaWireFatal("read-conversion"); + } + + void VulkanRenderer::BlitWireFramebuffers(GLint sx0, GLint sy0, GLint sx1, GLint sy1, + GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLbitfield mask, GLenum filter) { + auto& state = MG_Pipe::MGPipeApplier(); + const Bool named = !MG_Pipe::MGPipeHandleIsNull(state.VerbBlitReadFbo) || + !MG_Pipe::MGPipeHandleIsNull(state.VerbBlitDrawFbo); + const auto* read = named ? state.FramebufferRecordFor(state.VerbBlitReadFbo) : state.ReadFramebuffer(); + const auto* draw = named ? state.FramebufferRecordFor(state.VerbBlitDrawFbo) : state.DrawFramebuffer(); + if (!read || !draw) MagmaWireFatal("blit-framebuffer-record"); + state.VerbBlitNamedConsumed = named; + const auto blit = [&](const MG_Pipe::MGPSurface& src, const MG_Pipe::MGPSurface& dst, + VkImageAspectFlags aspect) { + WireImage source = ResolveWireImage(*read,src,aspect); + WireImage destination = ResolveWireImage(*draw,dst,aspect); + if (!source.image || !destination.image) return; + m_textureManager->FlushPendingUploads(); + TransitionWireImage(source,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + TransitionWireImage(destination,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + VkImageBlit region{}; + region.srcSubresource = {aspect,source.level,source.layer,1}; + region.dstSubresource = {aspect,destination.level,destination.layer,1}; + region.srcOffsets[0] = {sx0,sy0,0}; region.srcOffsets[1] = {sx1,sy1,1}; + region.dstOffsets[0] = {dx0,dy0,0}; region.dstOffsets[1] = {dx1,dy1,1}; + // Headless and identity surfaces store GL's bottom row at Vulkan's last row. + if (source.isDefault) { + region.srcOffsets[0].y = static_cast(source.extent.height)-sy0; + region.srcOffsets[1].y = static_cast(source.extent.height)-sy1; + } + if (destination.isDefault) { + region.dstOffsets[0].y = static_cast(destination.extent.height)-dy0; + region.dstOffsets[1].y = static_cast(destination.extent.height)-dy1; + } + vkCmdBlitImage(m_frameContext.GetCurrent().commandBuffer,source.image,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + destination.image,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,1,®ion, + filter == GL_LINEAR ? VK_FILTER_LINEAR : VK_FILTER_NEAREST); + if (destination.isDefault) m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired,true); + }; + if (mask & GL_COLOR_BUFFER_BIT) { + for (Uint32 i=0;iDrawBuffers[i]; + if (attachment>=0 && attachment(MG_Pipe::kMGPipeMaxColorAttachments)) + blit(read->ReadSurface,draw->Color[attachment],VK_IMAGE_ASPECT_COLOR_BIT); + } + } + if (mask & GL_DEPTH_BUFFER_BIT) blit(read->Depth,draw->Depth,VK_IMAGE_ASPECT_DEPTH_BIT); + if (mask & GL_STENCIL_BUFFER_BIT) blit(read->Stencil,draw->Stencil,VK_IMAGE_ASPECT_STENCIL_BIT); + } + void VulkanRenderer::GenerateWireMipmap() { + auto& state = MG_Pipe::MGPipeApplier(); + const auto handle = state.VerbMipRes; + auto* resource = m_textureManager->SyncTextureResourceByHandle(handle); + if (!resource) MagmaWireFatal("mipmap-resource"); + m_textureManager->FlushPendingUploads(); + if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("mipmap-flush"); + auto& frame = m_frameContext.GetCurrent(); + if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); + const Uint32 base = state.VerbMipBaseLevel; + const Uint32 count = state.VerbMipLevelCount; + if (count <= 1) return; + if (!m_textureManager->GrowWireTextureMipChain(handle,base+count,frame.commandBuffer)) + MagmaWireFatal("mipmap-growth"); + if (resource->aspect != VK_IMAGE_ASPECT_COLOR_BIT && resource->aspect != VK_IMAGE_ASPECT_DEPTH_BIT) + MagmaWireFatal("depth-stencil-mipmap@P7"); + VkFormatProperties props{}; + vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle,resource->format,&props); + const auto required = VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT; + if ((props.optimalTilingFeatures & required) != required) MagmaWireFatal("mipmap-native-format@P7"); + const VkFilter filter = props.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT ? + VK_FILTER_LINEAR : VK_FILTER_NEAREST; + VkPipelineStageFlags stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags access = 0; + GetImageTransitionSourceState(resource->layout,stage,access); + if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,resource->layout, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,stage,VK_PIPELINE_STAGE_TRANSFER_BIT,access, + VK_ACCESS_TRANSFER_WRITE_BIT,resource->aspect,0,resource->mipLevels)) MagmaWireFatal("mipmap-transition"); + for (Uint32 level=base+1;levelimage,previous, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT,VK_ACCESS_TRANSFER_READ_BIT,resource->aspect,level-1,1); + VkImageBlit blit{}; + blit.srcSubresource = {resource->aspect,level-1,0,resource->arrayLayers}; + blit.dstSubresource = {resource->aspect,level,0,resource->arrayLayers}; + blit.srcOffsets[1] = {static_cast(std::max(resource->extent.width>>(level-1),1u)), + static_cast(std::max(resource->extent.height>>(level-1),1u)), + static_cast(std::max(resource->depth>>(level-1),1u))}; + blit.dstOffsets[1] = {static_cast(std::max(resource->extent.width>>level,1u)), + static_cast(std::max(resource->extent.height>>level,1u)), + static_cast(std::max(resource->depth>>level,1u))}; + vkCmdBlitImage(frame.commandBuffer,resource->image,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + resource->image,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,1,&blit,filter); + VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,previous, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_ACCESS_TRANSFER_READ_BIT,VK_ACCESS_TRANSFER_WRITE_BIT,resource->aspect,level-1,1); + } + resource->layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + auto& record = state.TextureResources[handle.Slot]; + record.Desc.Levels = std::max(record.Desc.Levels,static_cast(base+count)); + resource->syncedWireSerial = ++record.Serial; + auto& store = MG_Remote::Server::ServerStagedTexture(); + const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(handle); + const auto target = static_cast(record.Desc.Target); + // The resource descriptor determines the upload-target set; no texture unit or + // client upload-target vector participates in defining the generated shadow. + for (Uint32 face=0;face<(target==MG_Pipe::MGPipeResourceTarget::TexCube ? 6u : 1u);++face) { + TextureUploadTarget upload = TextureUploadTarget::Texture2D; + switch (target) { + case MG_Pipe::MGPipeResourceTarget::Tex1D: upload=TextureUploadTarget::Texture1D; break; + case MG_Pipe::MGPipeResourceTarget::Tex3D: upload=TextureUploadTarget::Texture3D; break; + case MG_Pipe::MGPipeResourceTarget::Tex1DArray: upload=TextureUploadTarget::Texture1DArray; break; + case MG_Pipe::MGPipeResourceTarget::Tex2DArray: upload=TextureUploadTarget::Texture2DArray; break; + case MG_Pipe::MGPipeResourceTarget::TexCubeArray: upload=TextureUploadTarget::TextureCubeMapArray; break; + case MG_Pipe::MGPipeResourceTarget::TexCube: + upload=static_cast(static_cast(TextureUploadTarget::TextureCubeMapPositiveX)+face); break; + default: break; + } + for (Uint32 level=base+1;level(std::max(resource->extent.width>>level,1u)), + static_cast(std::max(resource->extent.height>>level,1u)), + static_cast(std::max(resource->depth>>level,1u))}; + store.NoteLevelDefined(key,static_cast(upload),static_cast(level),extent); + store.MarkLevelGpuDirty(key,static_cast(upload),static_cast(level),true); + } + } + } +#endif diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index 7d41d405..34ce71d0 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -848,6 +848,8 @@ namespace MobileGL::MG_Pipe { MGPipeHandle VerbCopyTexDst = kMGPipeNullHandle; // generate_mipmap's texture (MGPMipPlan::Res). MGPipeHandle VerbMipRes = kMGPipeNullHandle; + Uint16 VerbMipBaseLevel = 0; + Uint16 VerbMipLevelCount = 0; // The current indirect draw's command buffer and (for the *Count forms) parameter // buffer (MGPDrawIndirect::Buffer / ParameterBuffer). MGPipeHandle VerbIndirectBuffer = kMGPipeNullHandle; @@ -871,6 +873,7 @@ namespace MobileGL::MG_Pipe { VerbBlitNamedConsumed = false; VerbCopyTexDst = kMGPipeNullHandle; VerbMipRes = kMGPipeNullHandle; + VerbMipBaseLevel = VerbMipLevelCount = 0; VerbIndirectBuffer = kMGPipeNullHandle; VerbIndirectParameterBuffer = kMGPipeNullHandle; VerbDispatchIndirectBuffer = kMGPipeNullHandle; diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index 3c9f557f..acdcac71 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -1039,6 +1039,8 @@ namespace MobileGL::MG_Remote::Server { auto& applierState = MG_Pipe::MGPipeApplier(); applierState.ClearVerbHandles(); applierState.VerbMipRes = plan.Res; + applierState.VerbMipBaseLevel = plan.BaseLevel; + applierState.VerbMipLevelCount = plan.LevelCount; #endif table->GL.GenerateMipmap(plan.Target); return true;