[MG_Backend, MG_Pipe, MG_Remote] (Disaggregated): P5f fm - route framebuffer verbs through server records

This commit is contained in:
2026-09-19 22:00:14 -04:00
parent 418c765c58
commit 036c3a3d5e
7 changed files with 1364 additions and 47 deletions
@@ -17,6 +17,12 @@
#include "MG_Util/Metrics/PipeStats.h"
#include <Config.h>
#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 <MG_Pipe/PipeApply.h>
#include <MG_Remote/Server/StagedTextureStore.h>
#endif
#include <algorithm>
#include <cstdio>
#include <cstdlib>
@@ -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<Uint32>(it->first >> 32) & 0x7fffffffu;
const Uint32 generation = static_cast<Uint32>(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<MGPipeResourceTarget>(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<MGPipeResourceTarget>(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<Uint8>& outOwned,
const void*& outSource, SizeT& outByteSize) {
outSource = static_cast<const Uint8*>(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<SizeT>(texelSize.x()) * static_cast<SizeT>(texelSize.y()) *
static_cast<SizeT>(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<const Uint8*>(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<Uint32>(clamped * 16777215.0f + 0.5f);
std::memcpy(depthPlane + t * 4, &depthWord, sizeof(depthWord));
}
stencilPlane[t] = static_cast<Uint8>(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<float>(packed >> 8) / 16777215.0f;
std::memcpy(depthPlane + t * 4, &depthValue, sizeof(depthValue));
}
stencilPlane[t] = static_cast<Uint8>(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<const Uint8*>(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<Uint32>(raw) << 16) | raw;
} else {
std::memcpy(&wide, shadow + t * 4, sizeof(wide));
}
if (dstIsFloat) {
const float value = static_cast<float>(static_cast<double>(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<Uint8>(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<Uint8>(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<Uint32>(desc.Target), desc.Width,
desc.Height, desc.Depth, static_cast<Uint32>(desc.ArrayLayers));
return false;
}
const TextureInternalFormat internalFormat = static_cast<TextureInternalFormat>(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<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2DMS) ||
desc.Target == static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2DMSArray) ||
(desc.Target == static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Renderbuffer) && desc.Samples > 1);
VkSampleCountFlagBits resolvedSampleCount = VK_SAMPLE_COUNT_1_BIT;
if (isMultisample) {
if (!TryResolveSampleCountFlagBits(static_cast<Int>(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<Uint32>(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<Uint32>(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<Uint32>(resolvedSampleCount) >> 1;
bit > static_cast<Uint32>(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<Int>(format));
rounded = static_cast<Uint32>(VK_SAMPLE_COUNT_1_BIT);
}
if (rounded != 0) {
resolvedSampleCount = static_cast<VkSampleCountFlagBits>(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<Int>(desc.Width), static_cast<Int>(imageHeight),
static_cast<Int>(shapeInfo.depth)};
const Uint32 fullMipLevels = ComputeFullMipLevelCount(mipExtent);
const Uint32 backingMipLevels =
isMultisample ? 1u
: (desc.Levels > 1 ? std::min(std::max<Uint32>(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<Uint32>(imageInfo.flags),
static_cast<Int>(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<Int>(imageInfo.samples),
static_cast<Int>(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<TextureInternalFormat>(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<Uint8> owned;
VkDeviceSize offset = 0;
};
Vector<WireUploadItem> 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<Uint32>(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<TextureUploadTarget>(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<Int>(resource.format),
static_cast<Uint32>(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<VkDeviceSize>(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<Uint32>(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<Uint32>(item.texelSize.x()),
static_cast<Uint32>(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<SizeT>(item.texelSize.x()) *
static_cast<SizeT>(item.texelSize.y()) *
static_cast<SizeT>(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<VkDeviceSize>(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, &copy);
}
}
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, &copy);
}
}
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
@@ -17,9 +17,21 @@
#include <unordered_map>
#include <unordered_set>
#include <Config.h>
#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 <MG_Pipe/MGPipeHandles.h>
#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<Uint64, TextureResource> m_wireTextureResources;
// Handles are per-kind; a renderbuffer and texture may have identical slot/gen.
std::unordered_map<Uint64, TextureResource> m_wireRenderbufferResources;
#endif
VkDevice m_device = VK_NULL_HANDLE;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
@@ -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<MG_State::GLState::FramebufferObject> {
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
@@ -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);
@@ -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<Int32>(image.extent.width));
const Int32 y1 = std::min(box.y() + box.w(), static_cast<Int32>(image.extent.height));
if (x0 >= x1 || y0 >= y1) return;
area = {{x0, y0}, {static_cast<Uint32>(x1-x0), static_cast<Uint32>(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<Uint32>(drawbuffer) != i) continue;
const Int32 attachment = fbo.DrawBuffers[i];
if (attachment >= 0 && attachment < static_cast<Int32>(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<VkDeviceSize>(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<Uint32>(width), static_cast<Uint32>(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,&region);
if (!SubmitReadbackCommandsAndWait(frame)) MagmaWireFatal("read-submit");
const auto* bytes = static_cast<const Uint8*>(buffer.Map());
if (!bytes || !buffer.Invalidate(size)) MagmaWireFatal("read-map");
Vector<Uint8> remapped;
if (source.isDefault) {
remapped.resize(static_cast<SizeT>(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<SizeT>(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<GLint>(source.extent.height)-sy0;
region.srcOffsets[1].y = static_cast<GLint>(source.extent.height)-sy1;
}
if (destination.isDefault) {
region.dstOffsets[0].y = static_cast<GLint>(destination.extent.height)-dy0;
region.dstOffsets[1].y = static_cast<GLint>(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,&region,
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;i<MG_Pipe::kMGPipeMaxColorAttachments;++i) {
const Int32 attachment = draw->DrawBuffers[i];
if (attachment>=0 && attachment<static_cast<Int32>(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;level<base+count;++level) {
VkImageLayout previous = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,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<Int32>(std::max(resource->extent.width>>(level-1),1u)),
static_cast<Int32>(std::max(resource->extent.height>>(level-1),1u)),
static_cast<Int32>(std::max(resource->depth>>(level-1),1u))};
blit.dstOffsets[1] = {static_cast<Int32>(std::max(resource->extent.width>>level,1u)),
static_cast<Int32>(std::max(resource->extent.height>>level,1u)),
static_cast<Int32>(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<Uint16>(record.Desc.Levels,static_cast<Uint16>(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<MG_Pipe::MGPipeResourceTarget>(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<TextureUploadTarget>(static_cast<Uint32>(TextureUploadTarget::TextureCubeMapPositiveX)+face); break;
default: break;
}
for (Uint32 level=base+1;level<base+count;++level) {
const IntVec3 extent{static_cast<Int>(std::max(resource->extent.width>>level,1u)),
static_cast<Int>(std::max(resource->extent.height>>level,1u)),
static_cast<Int>(std::max(resource->depth>>level,1u))};
store.NoteLevelDefined(key,static_cast<Uint16>(upload),static_cast<Uint16>(level),extent);
store.MarkLevelGpuDirty(key,static_cast<Uint16>(upload),static_cast<Uint16>(level),true);
}
}
}
#endif
+3
View File
@@ -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;
@@ -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;