mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[MG_Backend, MG_Pipe, MG_Remote] (Disaggregated): P5f fm - route framebuffer verbs through server records
This commit is contained in:
@@ -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, ©);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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,®ion);
|
||||
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,®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;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
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user