mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
719 lines
42 KiB
C++
719 lines
42 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc
|
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
|
|
// SHARED IMAGES (docs/archive/notes/anland/plan-ahb-dmabuf.md). Included by VulkanRenderer.cpp
|
|
// inside the backend namespace, after WireFramebuffer.inc, whose readback the present side's copy
|
|
// is modelled on: the same flush, the same write-visibility barrier, the same transition of the
|
|
// default framebuffer's image.
|
|
//
|
|
// OWNERSHIP. The image lives on more than one device: a client session writes it, the
|
|
// compositor's session (another VulkanRenderer, another VkDevice) samples it. Each present
|
|
// acquires it from VK_QUEUE_FAMILY_FOREIGN_EXT (UNDEFINED: the copy overwrites every texel) and
|
|
// releases it back in GENERAL. A reader acquires it (GENERAL) before the first use of a frame and
|
|
// again whenever the write generation moved, and releases it back at its frame boundary, so every
|
|
// acquire on either side matches a release on the other.
|
|
//
|
|
// ORDER (MG_Remote/Server/SharedImageRegistry.h). Every fence is a sync_file. The writer's copy
|
|
// waits on its GPU for the read fences still pending, signals a semaphore exported as the write
|
|
// fence, and the present returns without waiting. A reader's use waits on its GPU for the write
|
|
// fence of the generation it acquires; its frame's submission signals a semaphore exported as the
|
|
// read fence of every image the frame used. A device without SYNC_FD semaphore import and export
|
|
// waits on the CPU instead where it would have waited on the GPU - before the writer's copy, after
|
|
// it, and before the reader's use - and exports nothing for its reads: there is no fence to give,
|
|
// and waiting for the frame on the CPU only to say it is over would stall every frame of the
|
|
// compositor. The writer's next copy is then ordered after the reads by nothing but the client's
|
|
// own buffer rotation (a Wayland client writes a buffer only after its release) - which is all
|
|
// this path had before - on a device Android's Vulkan 1.1 requirements leave rare.
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// A CPU wait on another session's fence is a fallback, and bounded: a peer that never signals
|
|
// costs a frame's worth of stall, not a hang.
|
|
static constexpr Uint32 kSharedImageCpuWaitMs = 1000;
|
|
// Every stage a reader's use of a shared image can reach: sampling (draws and dispatches), a
|
|
// blit or readback source, an attachment. The write fence's wait blocks these, and the
|
|
// acquire's first scope is the same set, so it is ordered after that wait.
|
|
static constexpr VkPipelineStageFlags kSharedImageUseStages =
|
|
VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
|
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT |
|
|
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
|
|
static void CloseSyncFile(int fd) {
|
|
#if !defined(_WIN32)
|
|
if (fd >= 0) ::close(fd);
|
|
#else
|
|
(void)fd;
|
|
#endif
|
|
}
|
|
|
|
void VulkanRenderer::DestroySharedImagePresentTargets(Bool onlyDead) {
|
|
if (onlyDead) RefreshCompletedSubmits();
|
|
for (auto it = m_sharedImagePresentTargets.begin(); it != m_sharedImagePresentTargets.end();) {
|
|
// A dead image's last copy may still be executing: the present no longer waits for it.
|
|
if (onlyDead && (!it->second.owner.expired() || it->second.lastSubmit > m_completedSubmitCounter)) {
|
|
++it;
|
|
continue;
|
|
}
|
|
VkTextureManager::DestroyImportedSharedImage(m_device, it->second.image);
|
|
it = m_sharedImagePresentTargets.erase(it);
|
|
}
|
|
}
|
|
|
|
// A copy moves bytes; a blit converts through the format. The bytes are right only between
|
|
// formats with one memory layout, which for the RGBA8 buffers means RGBA8 itself or its sRGB
|
|
// twin (a blit would decode the sRGB bytes on the way).
|
|
static Bool SharedImageCopiesAsIs(VkFormat source, VkFormat destination) {
|
|
if (source == destination) return true;
|
|
return destination == VK_FORMAT_R8G8B8A8_UNORM && source == VK_FORMAT_R8G8B8A8_SRGB;
|
|
}
|
|
|
|
Bool VulkanRenderer::BlitDefaultFramebufferToSharedImage(const SharedImageView& view,
|
|
const MG_Util::Damage::Region& region) {
|
|
if (m_device == VK_NULL_HANDLE || view.Width == 0 || view.Height == 0) return false;
|
|
// The debug knob's device check (MGPipeDebugDeviceLossDue) rides the frame boundary here.
|
|
if (MG_Pipe::MGPipeDebugDeviceLossDue()) NoteDeviceLoss(VK_ERROR_DEVICE_LOST, "the debug knob (shared-image present)");
|
|
if (m_deviceLost && LatchWireDeviceLoss("shared-image-present")) return false;
|
|
DestroySharedImagePresentTargets(true);
|
|
|
|
// The default framebuffer's image is stored in display orientation (row 0 is the top of
|
|
// the frame) on an identity surface, and turned half round on a 180 one; a blit expresses
|
|
// both. A quarter turn transposes, which no copy or blit can - and only the compositor's
|
|
// own window is ever quarter-turned, which presents to the display rather than here.
|
|
const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform();
|
|
if (IsQuarterTurnPreTransform(preTransform)) {
|
|
MGLOG_E_ONCE("Magma: shared image %llu: the current surface %llx is quarter-turned (transform %u); a "
|
|
"present into a shared image is declined there",
|
|
static_cast<unsigned long long>(view.Id), static_cast<unsigned long long>(m_activeTargetKey),
|
|
static_cast<Uint32>(preTransform));
|
|
return false;
|
|
}
|
|
|
|
auto found = m_sharedImagePresentTargets.find(view.Id);
|
|
if (found == m_sharedImagePresentTargets.end()) {
|
|
const auto owner = MG_Remote::Server::SharedImages::Find(view.Id);
|
|
if (owner == nullptr) {
|
|
MGLOG_E_ONCE("Magma: shared image %llu is not live; the present is declined",
|
|
static_cast<unsigned long long>(view.Id));
|
|
return false;
|
|
}
|
|
SharedImagePresentTarget target;
|
|
String why;
|
|
if (!VkTextureManager::ImportSharedImage(view.NativeBuffer, view.Width, view.Height, target.image, why)) {
|
|
MGLOG_E_ONCE("Magma: shared image %llu (%ux%u) not imported - %s; the present is declined",
|
|
static_cast<unsigned long long>(view.Id), view.Width, view.Height, why.c_str());
|
|
return false;
|
|
}
|
|
target.owner = owner;
|
|
found = m_sharedImagePresentTargets.emplace(view.Id, Move(target)).first;
|
|
}
|
|
const VkTextureManager::ImportedSharedImage& destination = found->second.image;
|
|
// The registry's record carries the image's fences. The server holds a reference for as
|
|
// long as the present runs; a lapsed one is an image released under it.
|
|
const auto image =
|
|
std::static_pointer_cast<const MG_Remote::Server::SharedImages::Image>(found->second.owner.lock());
|
|
if (image == nullptr) {
|
|
MGLOG_E_ONCE("Magma: shared image %llu is not live; the present is declined",
|
|
static_cast<unsigned long long>(view.Id));
|
|
return false;
|
|
}
|
|
|
|
// The frame's draws reach the queue first; the copy below is a later submission that the
|
|
// readback barrier orders behind them.
|
|
if (HasPendingRecordedWork() && !FlushPendingCommands()) {
|
|
MGLOG_E("Magma: shared image %llu: the frame's recorded work could not be submitted",
|
|
static_cast<unsigned long long>(view.Id));
|
|
return false;
|
|
}
|
|
MG_Pipe::MGPFramebufferState defaultFramebuffer{};
|
|
defaultFramebuffer.IsDefault = 1;
|
|
const MG_Pipe::MGPSurface noSurface{};
|
|
WireImage source = ResolveWireImage(defaultFramebuffer, noSurface, VK_IMAGE_ASPECT_COLOR_BIT,
|
|
/*isWriteTarget=*/false);
|
|
if (source.image == VK_NULL_HANDLE || source.extent.width == 0 || source.extent.height == 0) {
|
|
MGLOG_E_ONCE("Magma: shared image %llu: no default framebuffer is current; the present is declined",
|
|
static_cast<unsigned long long>(view.Id));
|
|
return false;
|
|
}
|
|
if (!m_swapchainObject.IsImageContentDefined(source.swapchainImageIndex)) {
|
|
MGLOG_W_ONCE("Magma: shared image present: no write is recorded for image %u of the default "
|
|
"framebuffer; its raw pixels are copied",
|
|
source.swapchainImageIndex);
|
|
}
|
|
|
|
const Bool sameExtent = source.extent.width == view.Width && source.extent.height == view.Height;
|
|
const Bool halfTurn = preTransform == VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR;
|
|
const Bool copy = sameExtent && !halfTurn && SharedImageCopiesAsIs(source.format, destination.format);
|
|
// ONLY WHERE THE IMAGE DIFFERS FROM THE FRAME: an image this renderer wrote before, copied
|
|
// one to one (no scaling, no half turn). Its other texels must survive, so the acquire then
|
|
// keeps the layout the last reader released it in rather than discarding it.
|
|
MG_Util::Damage::Region part = region;
|
|
if (!part.IsFull()) part.Normalize(static_cast<Int32>(view.Width), static_cast<Int32>(view.Height));
|
|
const Bool partial = !part.IsFull() && found->second.written && sameExtent && !halfTurn;
|
|
// The region's rectangles, top row first and made disjoint: a copy's regions may not overlap.
|
|
Vector<MG_Util::Damage::Rect> rows;
|
|
if (partial) {
|
|
for (const MG_Util::Damage::Rect& rect : MG_Util::Damage::Disjoint(part.Rects()))
|
|
rows.push_back(MG_Util::Damage::FlipY(rect, static_cast<Int32>(view.Height)));
|
|
}
|
|
VkFormatProperties sourceProperties{};
|
|
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, source.format, &sourceProperties);
|
|
if (!copy) {
|
|
if ((sourceProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) == 0 ||
|
|
(destination.features & VK_FORMAT_FEATURE_BLIT_DST_BIT) == 0) {
|
|
MGLOG_E_ONCE("Magma: shared image %llu: a %ux%u format-%d default framebuffer needs a blit into "
|
|
"the %ux%u format-%d image, which this device cannot do; the present is declined",
|
|
static_cast<unsigned long long>(view.Id), source.extent.width, source.extent.height,
|
|
static_cast<Int>(source.format), view.Width, view.Height,
|
|
static_cast<Int>(destination.format));
|
|
return false;
|
|
}
|
|
if (vkuFormatIsSRGB(source.format) && !vkuFormatIsSRGB(destination.format)) {
|
|
MGLOG_W_ONCE("Magma: shared image present: the sRGB default framebuffer is blitted into a UNORM "
|
|
"image, which stores linear values");
|
|
}
|
|
}
|
|
|
|
m_textureManager->FlushPendingUploads();
|
|
RecordWireReadbackWriteBarrier();
|
|
TransitionWireImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
|
auto& frame = m_frameContext.GetCurrent();
|
|
|
|
// NOT OVER WHAT IS STILL READ. The reads published since this image's last write (a
|
|
// compositor frame sampling it) end before the copy starts: on the GPU, as waits of the
|
|
// submission below at the transfer stage; on the CPU, where they cannot be imported.
|
|
// The frame's draws were flushed above, so the next submission is the copy's.
|
|
namespace SI = MG_Remote::Server::SharedImages;
|
|
if (m_sharedImageSyncFd) {
|
|
for (const int fence : SI::DupPendingReadFences(*image))
|
|
StageSharedImageWait(fence, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
|
} else if (!SI::WaitForReads(*image, kSharedImageCpuWaitMs)) {
|
|
MGLOG_W_ONCE("Magma: shared image %llu: a read of the previous frame did not finish within %u ms; it is "
|
|
"overwritten anyway",
|
|
static_cast<unsigned long long>(view.Id), kSharedImageCpuWaitMs);
|
|
}
|
|
|
|
// The acquire's layout transition runs after those waits: its first scope is the stage
|
|
// they block.
|
|
VkImageMemoryBarrier acquire{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
|
acquire.srcAccessMask = 0;
|
|
acquire.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
// Every present releases the image in GENERAL, and so does every reader.
|
|
acquire.oldLayout = partial ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_UNDEFINED;
|
|
acquire.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
acquire.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT;
|
|
acquire.dstQueueFamilyIndex = destination.queueFamily;
|
|
acquire.image = destination.image;
|
|
acquire.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
|
|
0, nullptr, 0, nullptr, 1, &acquire);
|
|
|
|
const VkImageSubresourceLayers layers{VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
|
if (partial) {
|
|
// An empty region copies nothing: the image already is the frame.
|
|
if (copy && !rows.empty()) {
|
|
Vector<VkImageCopy> regions;
|
|
regions.reserve(rows.size());
|
|
for (const MG_Util::Damage::Rect& row : rows) {
|
|
VkImageCopy one{};
|
|
one.srcSubresource = layers;
|
|
one.dstSubresource = layers;
|
|
one.srcOffset = {row.X, row.Y, 0};
|
|
one.dstOffset = {row.X, row.Y, 0};
|
|
one.extent = {static_cast<Uint32>(row.Width), static_cast<Uint32>(row.Height), 1};
|
|
regions.push_back(one);
|
|
}
|
|
vkCmdCopyImage(frame.commandBuffer, source.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
|
destination.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
static_cast<Uint32>(regions.size()), regions.data());
|
|
} else if (!rows.empty()) {
|
|
Vector<VkImageBlit> regions;
|
|
regions.reserve(rows.size());
|
|
for (const MG_Util::Damage::Rect& row : rows) {
|
|
VkImageBlit one{};
|
|
one.srcSubresource = layers;
|
|
one.dstSubresource = layers;
|
|
one.srcOffsets[0] = {row.X, row.Y, 0};
|
|
one.srcOffsets[1] = {row.X + row.Width, row.Y + row.Height, 1};
|
|
one.dstOffsets[0] = one.srcOffsets[0];
|
|
one.dstOffsets[1] = one.srcOffsets[1];
|
|
regions.push_back(one);
|
|
}
|
|
vkCmdBlitImage(frame.commandBuffer, source.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
|
destination.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
static_cast<Uint32>(regions.size()), regions.data(), VK_FILTER_NEAREST);
|
|
}
|
|
} else if (copy) {
|
|
VkImageCopy region{};
|
|
region.srcSubresource = layers;
|
|
region.dstSubresource = layers;
|
|
region.extent = {view.Width, view.Height, 1};
|
|
vkCmdCopyImage(frame.commandBuffer, source.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.image,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion);
|
|
} else {
|
|
// Identity: row 0 is already the top. 180: the stored image is the frame turned half
|
|
// round, so the destination rectangle runs backwards on both axes.
|
|
const Int32 width = static_cast<Int32>(view.Width);
|
|
const Int32 height = static_cast<Int32>(view.Height);
|
|
VkImageBlit region{};
|
|
region.srcSubresource = layers;
|
|
region.srcOffsets[1] = {static_cast<Int32>(source.extent.width), static_cast<Int32>(source.extent.height),
|
|
1};
|
|
region.dstSubresource = layers;
|
|
region.dstOffsets[0] = halfTurn ? VkOffset3D{width, height, 0} : VkOffset3D{0, 0, 0};
|
|
region.dstOffsets[1] = halfTurn ? VkOffset3D{0, 0, 1} : VkOffset3D{width, height, 1};
|
|
const VkFilter filter =
|
|
!sameExtent && (sourceProperties.optimalTilingFeatures &
|
|
VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0
|
|
? VK_FILTER_LINEAR
|
|
: VK_FILTER_NEAREST;
|
|
vkCmdBlitImage(frame.commandBuffer, source.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.image,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion, filter);
|
|
}
|
|
|
|
VkImageMemoryBarrier release{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
|
release.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
release.dstAccessMask = 0;
|
|
release.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
release.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
|
release.srcQueueFamilyIndex = destination.queueFamily;
|
|
release.dstQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT;
|
|
release.image = destination.image;
|
|
release.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
|
|
0, 0, nullptr, 0, nullptr, 1, &release);
|
|
|
|
// THE WRITE FENCE. The client attaches the buffer to its surface as soon as this returns;
|
|
// what orders the compositor's read after the copy is the fence published here, which the
|
|
// compositor's session waits for on its GPU when it next acquires the image. The copy's
|
|
// submission signals a semaphore exported as that fence, and nothing waits for it here.
|
|
// The submission is an ordinary pooled-fence flush: the renderer's in-flight tracking keeps
|
|
// the command buffer and every resource it names alive until it has completed.
|
|
if (m_sharedImageSyncFd && m_sharedImageSignal == VK_NULL_HANDLE)
|
|
m_sharedImageSignal = TakeSharedImageSemaphore(/*exportable=*/true);
|
|
if (m_sharedImageSignal != VK_NULL_HANDLE) {
|
|
m_sharedImageSignalSubmitted = VK_NULL_HANDLE;
|
|
if (!FlushPendingCommands()) {
|
|
MGLOG_E("Magma: shared image %llu: the copy could not be submitted",
|
|
static_cast<unsigned long long>(view.Id));
|
|
return false;
|
|
}
|
|
found->second.lastSubmit = m_submitCounter;
|
|
found->second.written = true;
|
|
const VkSemaphore signal = std::exchange(m_sharedImageSignalSubmitted, VK_NULL_HANDLE);
|
|
int fence = -1;
|
|
if (signal != VK_NULL_HANDLE && ExportSharedImageSignal(signal, fence)) {
|
|
SI::PublishWrite(*image, fence);
|
|
return true;
|
|
}
|
|
// Submitted, but no fence came out of it: the copy is waited for, as on a device that
|
|
// exports none.
|
|
if (!WaitForSubmitsUpTo(m_submitCounter, UINT64_MAX)) {
|
|
MGLOG_E("Magma: shared image %llu: the copy could not be waited for",
|
|
static_cast<unsigned long long>(view.Id));
|
|
return false;
|
|
}
|
|
SI::PublishWrite(*image, -1);
|
|
return true;
|
|
}
|
|
if (!SubmitReadbackCommandsAndWait(frame)) {
|
|
MGLOG_E("Magma: shared image %llu: the copy could not be submitted or waited for",
|
|
static_cast<unsigned long long>(view.Id));
|
|
return false;
|
|
}
|
|
found->second.lastSubmit = m_submitCounter;
|
|
found->second.written = true;
|
|
SI::PublishWrite(*image, -1);
|
|
return true;
|
|
}
|
|
|
|
// ---- fences as semaphores ------------------------------------------------------------------
|
|
|
|
VkSemaphore VulkanRenderer::TakeSharedImageSemaphore(Bool exportable) {
|
|
auto& pool = exportable ? m_freeSharedImageSignalSemaphores : m_freeSharedImageWaitSemaphores;
|
|
if (!pool.empty()) {
|
|
const VkSemaphore semaphore = pool.back();
|
|
pool.pop_back();
|
|
return semaphore;
|
|
}
|
|
VkExportSemaphoreCreateInfo exportInfo{VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO};
|
|
exportInfo.handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT;
|
|
VkSemaphoreCreateInfo info{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
|
|
if (exportable) info.pNext = &exportInfo;
|
|
VkSemaphore semaphore = VK_NULL_HANDLE;
|
|
const VkResult result = vkCreateSemaphore(m_device, &info, nullptr, &semaphore);
|
|
if (result != VK_SUCCESS) {
|
|
MGLOG_E_ONCE("Magma: a shared-image semaphore could not be created (vkCreateSemaphore -> %d)",
|
|
static_cast<Int>(result));
|
|
return VK_NULL_HANDLE;
|
|
}
|
|
return semaphore;
|
|
}
|
|
|
|
void VulkanRenderer::StageSharedImageWait(int fence, VkPipelineStageFlags stages) {
|
|
if (fence < 0) return;
|
|
if (m_sharedImageSyncFd) {
|
|
const VkSemaphore semaphore = TakeSharedImageSemaphore(/*exportable=*/false);
|
|
if (semaphore != VK_NULL_HANDLE) {
|
|
// A sync_file import is always temporary: the wait consumes it, and the semaphore
|
|
// is a plain unsignaled one again once that wait has executed.
|
|
VkImportSemaphoreFdInfoKHR import{VK_STRUCTURE_TYPE_IMPORT_SEMAPHORE_FD_INFO_KHR};
|
|
import.semaphore = semaphore;
|
|
import.flags = VK_SEMAPHORE_IMPORT_TEMPORARY_BIT;
|
|
import.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT;
|
|
import.fd = fence;
|
|
const VkResult result = m_importSemaphoreFd(m_device, &import);
|
|
if (result == VK_SUCCESS) {
|
|
m_sharedImageWaits.push_back({semaphore, stages}); // the import owns `fence` now
|
|
return;
|
|
}
|
|
m_freeSharedImageWaitSemaphores.push_back(semaphore);
|
|
MGLOG_W_ONCE("Magma: a shared-image fence could not be imported (vkImportSemaphoreFdKHR -> %d); it "
|
|
"is waited for on the CPU",
|
|
static_cast<Int>(result));
|
|
}
|
|
}
|
|
// Nothing recorded since reaches the queue before this returns.
|
|
if (!MG_Remote::Server::SharedImages::SyncFile::Wait(fence, kSharedImageCpuWaitMs)) {
|
|
MGLOG_W_ONCE("Magma: a shared-image fence did not signal within %u ms; the image is used anyway",
|
|
kSharedImageCpuWaitMs);
|
|
}
|
|
CloseSyncFile(fence);
|
|
}
|
|
|
|
Bool VulkanRenderer::ExportSharedImageSignal(VkSemaphore semaphore, int& fence) {
|
|
fence = -1;
|
|
VkSemaphoreGetFdInfoKHR info{VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR};
|
|
info.semaphore = semaphore;
|
|
info.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT;
|
|
// A sync_file export has a wait's effect on the semaphore: it is unsignaled again, and
|
|
// reusable once the submission that signaled it has completed. -1 back means it had
|
|
// already signaled.
|
|
const VkResult result = m_getSemaphoreFd(m_device, &info, &fence);
|
|
if (result != VK_SUCCESS) {
|
|
fence = -1;
|
|
MGLOG_W_ONCE("Magma: a shared-image fence could not be exported (vkGetSemaphoreFdKHR -> %d)",
|
|
static_cast<Int>(result));
|
|
return false;
|
|
}
|
|
for (auto& entry : m_sharedImageSemaphoresInFlight) {
|
|
if (entry.semaphore == semaphore) entry.exported = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void VulkanRenderer::AttachSharedImageSync(VkSubmitInfo& info, SharedImageSubmitSync& storage) {
|
|
storage.attached = false;
|
|
if (m_sharedImageWaits.empty() && m_sharedImageSignal == VK_NULL_HANDLE) return;
|
|
storage.waits.assign(info.pWaitSemaphores, info.pWaitSemaphores + info.waitSemaphoreCount);
|
|
storage.stages.assign(info.pWaitDstStageMask, info.pWaitDstStageMask + info.waitSemaphoreCount);
|
|
for (const auto& wait : m_sharedImageWaits) {
|
|
storage.waits.push_back(wait.semaphore);
|
|
storage.stages.push_back(wait.stages);
|
|
}
|
|
storage.signals.assign(info.pSignalSemaphores, info.pSignalSemaphores + info.signalSemaphoreCount);
|
|
if (m_sharedImageSignal != VK_NULL_HANDLE) storage.signals.push_back(m_sharedImageSignal);
|
|
info.waitSemaphoreCount = static_cast<Uint32>(storage.waits.size());
|
|
info.pWaitSemaphores = storage.waits.data();
|
|
info.pWaitDstStageMask = storage.stages.data();
|
|
info.signalSemaphoreCount = static_cast<Uint32>(storage.signals.size());
|
|
info.pSignalSemaphores = storage.signals.data();
|
|
storage.attached = true;
|
|
}
|
|
|
|
void VulkanRenderer::CommitSharedImageSync(const SharedImageSubmitSync& storage) {
|
|
if (!storage.attached) return;
|
|
for (const auto& wait : m_sharedImageWaits)
|
|
m_sharedImageSemaphoresInFlight.push_back({wait.semaphore, m_submitCounter, false, false});
|
|
m_sharedImageWaits.clear();
|
|
if (m_sharedImageSignal != VK_NULL_HANDLE) {
|
|
m_sharedImageSemaphoresInFlight.push_back({m_sharedImageSignal, m_submitCounter, true, false});
|
|
m_sharedImageSignalSubmitted = std::exchange(m_sharedImageSignal, VK_NULL_HANDLE);
|
|
}
|
|
}
|
|
|
|
void VulkanRenderer::RecycleSharedImageSemaphores(Uint64 completedSubmit) {
|
|
SizeT done = 0;
|
|
for (; done < m_sharedImageSemaphoresInFlight.size() &&
|
|
m_sharedImageSemaphoresInFlight[done].submitIndex <= completedSubmit;
|
|
++done) {
|
|
const auto& entry = m_sharedImageSemaphoresInFlight[done];
|
|
if (entry.semaphore == m_sharedImageSignalSubmitted) m_sharedImageSignalSubmitted = VK_NULL_HANDLE;
|
|
if (!entry.exportable) {
|
|
m_freeSharedImageWaitSemaphores.push_back(entry.semaphore);
|
|
} else if (entry.exported) {
|
|
m_freeSharedImageSignalSemaphores.push_back(entry.semaphore);
|
|
} else if (m_device != VK_NULL_HANDLE) {
|
|
// Signaled and never exported, so still signaled: a binary semaphore cannot be
|
|
// signaled again until something waits on it, and nothing will.
|
|
vkDestroySemaphore(m_device, entry.semaphore, nullptr);
|
|
}
|
|
}
|
|
if (done != 0) {
|
|
m_sharedImageSemaphoresInFlight.erase(
|
|
m_sharedImageSemaphoresInFlight.begin(),
|
|
m_sharedImageSemaphoresInFlight.begin() + static_cast<std::ptrdiff_t>(done));
|
|
}
|
|
}
|
|
|
|
void VulkanRenderer::DestroySharedImageSync() {
|
|
// The device is idle: every semaphore is inert.
|
|
const auto destroy = [this](VkSemaphore semaphore) {
|
|
if (semaphore != VK_NULL_HANDLE && m_device != VK_NULL_HANDLE)
|
|
vkDestroySemaphore(m_device, semaphore, nullptr);
|
|
};
|
|
for (const auto& entry : m_sharedImageSemaphoresInFlight) destroy(entry.semaphore);
|
|
m_sharedImageSemaphoresInFlight.clear();
|
|
for (const auto& wait : m_sharedImageWaits) destroy(wait.semaphore);
|
|
m_sharedImageWaits.clear();
|
|
destroy(std::exchange(m_sharedImageSignal, VK_NULL_HANDLE));
|
|
m_sharedImageSignalSubmitted = VK_NULL_HANDLE;
|
|
for (const VkSemaphore semaphore : m_freeSharedImageWaitSemaphores) destroy(semaphore);
|
|
m_freeSharedImageWaitSemaphores.clear();
|
|
for (const VkSemaphore semaphore : m_freeSharedImageSignalSemaphores) destroy(semaphore);
|
|
m_freeSharedImageSignalSemaphores.clear();
|
|
m_sharedImagesHeld.clear();
|
|
m_sharedImageReads.PublishFrame(-1); // drops the frame's references; publishes nothing
|
|
}
|
|
|
|
// ---- the reading side ------------------------------------------------------------------------
|
|
//
|
|
// REACHED ON EVERY USE. A wire draw or dispatch prepares its sampled textures through
|
|
// PrepareWireTextureResources on every call - the wire arm keeps no sampled-set cache across
|
|
// draws (the walk-skip is the monolith arm's SetupDraw) - and that notes each shared-image
|
|
// texture it resolves; SetupWireDraw and both dispatch paths acquire the noted ones straight
|
|
// after (AcquireNotedSharedImages). ResolveWireImage, through which every attachment, blit,
|
|
// copy and readback reaches a texture's image, acquires directly. The test is the write
|
|
// generation against the one last acquired, so a generation that moves between two draws of
|
|
// one frame is caught at the second.
|
|
|
|
void VulkanRenderer::AcquireNotedSharedImagesSlow() {
|
|
m_textureManager->TakeNotedSharedImageUses(m_sharedImageUseScratch);
|
|
for (const Uint64 key : m_sharedImageUseScratch) {
|
|
if (auto* resource = m_textureManager->FindWireTextureResource(key)) AcquireSharedImage(*resource);
|
|
}
|
|
m_sharedImageUseScratch.clear();
|
|
}
|
|
|
|
void VulkanRenderer::AcquireSharedImage(VkTextureManager::TextureResource& resource) {
|
|
namespace SI = MG_Remote::Server::SharedImages;
|
|
// A YUV image is converted into the texture's own image instead (WireYuvImage.inc).
|
|
if (resource.yuvImageId != 0) {
|
|
(void)ConvertYuvSharedImage(resource);
|
|
return;
|
|
}
|
|
if (resource.sharedImageId == 0 || resource.image == VK_NULL_HANDLE || resource.sharedImageOwner == nullptr)
|
|
return;
|
|
const auto image = std::static_pointer_cast<const SI::Image>(resource.sharedImageOwner);
|
|
Uint64 generation = 0;
|
|
const int fence = SI::DupWriteFence(*image, &generation);
|
|
// Held, and not written since it was acquired: every use after a frame's first.
|
|
if (resource.sharedImageHeld && generation == resource.sharedImageGeneration) {
|
|
CloseSyncFile(fence);
|
|
return;
|
|
}
|
|
|
|
// Out of any render pass, in the recording the use goes into (or one ahead of it).
|
|
auto& frame = m_frameContext.GetCurrent();
|
|
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
|
|
if (VkRenderPassManager::GetActiveRenderPass()) VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
|
// The write's fence gates the submission carrying the acquire: this recording's, or an
|
|
// earlier one if a flush takes it first - either way ahead of every later command.
|
|
StageSharedImageWait(fence, kSharedImageUseStages);
|
|
// A session that writes images (implicit sync by flush) may be about to overwrite what
|
|
// another session still samples: those reads go first.
|
|
if (m_sharedImageImplicitSync && !resource.sharedImageHeld) {
|
|
for (const int read : SI::DupPendingReadFences(*image)) StageSharedImageWait(read, kSharedImageUseStages);
|
|
}
|
|
|
|
const Uint32 family = static_cast<Uint32>(m_physicalDevice.queueFamilies.graphicsFamily);
|
|
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
|
barrier.image = resource.image;
|
|
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
VkPipelineStageFlags acquireAfter = kSharedImageUseStages;
|
|
if (resource.sharedImageHeld) {
|
|
// Written again while this frame holds it - a buffer reused before the compositor let
|
|
// it go. It is handed back first, so the acquire below matches a release.
|
|
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
|
barrier.dstAccessMask = 0;
|
|
barrier.oldLayout = resource.layout;
|
|
barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
|
barrier.srcQueueFamilyIndex = family;
|
|
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT;
|
|
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
|
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
|
acquireAfter = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
|
|
}
|
|
// GENERAL: the layout the writer releases in, and the one every wire descriptor samples
|
|
// in. An image no present has written yet (generation 0) has nothing to keep.
|
|
barrier.srcAccessMask = 0;
|
|
barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
|
barrier.oldLayout = generation == 0 ? VK_IMAGE_LAYOUT_UNDEFINED : VK_IMAGE_LAYOUT_GENERAL;
|
|
barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
|
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT;
|
|
barrier.dstQueueFamilyIndex = family;
|
|
vkCmdPipelineBarrier(frame.commandBuffer, acquireAfter, kSharedImageUseStages, 0, 0, nullptr, 0, nullptr, 1,
|
|
&barrier);
|
|
// The open recording names the image now: its out-of-pass work may not move ahead of it.
|
|
m_textureManager->StampResourceRecordingUse(resource);
|
|
resource.layout = VK_IMAGE_LAYOUT_GENERAL;
|
|
resource.sharedImageGeneration = generation;
|
|
if (!resource.sharedImageHeld) {
|
|
resource.sharedImageHeld = true;
|
|
// The current recording is the next submission (RetireWireObjects' rule).
|
|
m_sharedImagesHeld.push_back({resource.sharedImageKey, m_submitCounter + 1});
|
|
}
|
|
m_sharedImageReads.NoteRead(image);
|
|
}
|
|
|
|
// THE FRAME BOUNDARY (Present). Every image this frame acquired goes back to the foreign
|
|
// family in the frame's last recording, and that recording's submission signals the semaphore
|
|
// whose sync_file is published as the frame's read fence for all of them at once.
|
|
void VulkanRenderer::ReleaseHeldSharedImages() {
|
|
// A frame boundary: a YUV texture's next use converts its image again.
|
|
++m_yuvFrameSerial;
|
|
if (m_sharedImagesHeld.empty()) return;
|
|
const Uint32 family = static_cast<Uint32>(m_physicalDevice.queueFamilies.graphicsFamily);
|
|
Vector<VkImageMemoryBarrier> releases;
|
|
releases.reserve(m_sharedImagesHeld.size());
|
|
for (const auto& hold : m_sharedImagesHeld) {
|
|
// Gone or re-imaged since: a destroyed image needs no release, and a replacement
|
|
// acquired this frame has an entry of its own.
|
|
auto* resource = m_textureManager->FindWireTextureResource(hold.key);
|
|
if (resource == nullptr || !resource->sharedImageHeld) continue;
|
|
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
|
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
|
barrier.dstAccessMask = 0;
|
|
barrier.oldLayout = resource->layout;
|
|
barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
|
barrier.srcQueueFamilyIndex = family;
|
|
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT;
|
|
barrier.image = resource->image;
|
|
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
releases.push_back(barrier);
|
|
resource->sharedImageHeld = false;
|
|
resource->layout = VK_IMAGE_LAYOUT_GENERAL;
|
|
m_textureManager->StampResourceRecordingUse(*resource);
|
|
}
|
|
m_sharedImagesHeld.clear();
|
|
if (releases.empty()) return;
|
|
auto& frame = m_frameContext.GetCurrent();
|
|
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
|
|
if (VkRenderPassManager::GetActiveRenderPass()) VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
|
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
|
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr,
|
|
static_cast<Uint32>(releases.size()), releases.data());
|
|
}
|
|
|
|
Bool VulkanRenderer::PublishSharedImageAccesses() {
|
|
m_sharedImageImplicitSync = true;
|
|
if (m_sharedImageReads.Pending() == 0 && m_sharedImagesHeld.empty()) return true;
|
|
ReleaseHeldSharedImages();
|
|
if (m_sharedImageSyncFd && m_sharedImageSignal == VK_NULL_HANDLE)
|
|
m_sharedImageSignal = TakeSharedImageSemaphore(/*exportable=*/true);
|
|
m_sharedImageSignalSubmitted = VK_NULL_HANDLE;
|
|
if (!FlushPendingCommands()) {
|
|
MGLOG_E_ONCE("Magma: a flush's shared-image accesses could not be submitted");
|
|
m_sharedImageReads.PublishFrame(-1); // drops the references; nothing to order against
|
|
return false;
|
|
}
|
|
const VkSemaphore signal = std::exchange(m_sharedImageSignalSubmitted, VK_NULL_HANDLE);
|
|
int fence = -1;
|
|
if (signal == VK_NULL_HANDLE || !ExportSharedImageSignal(signal, fence)) {
|
|
// No fence came out of it: the submission is waited for and published as completed.
|
|
fence = -1;
|
|
if (!WaitForSubmitsUpTo(m_submitCounter, UINT64_MAX)) {
|
|
MGLOG_E_ONCE("Magma: a flush's shared-image accesses could not be waited for");
|
|
}
|
|
}
|
|
m_sharedImageReads.PublishFrameAsWrite(fence);
|
|
return true;
|
|
}
|
|
|
|
// THE NATIVE FENCE. A sync_file of everything the session submitted so far, for the client's
|
|
// EGL_ANDROID_native_fence_sync: the application hands the descriptor on (Chrome's GPU process,
|
|
// to the browser that commits the buffer), so it has to signal no earlier than the last of that
|
|
// work. With nothing recorded, an empty
|
|
// recording is submitted to carry the signal: the in-order queue runs it after every earlier
|
|
// submission, and it is tracked like any other (the semaphore's reuse waits for it).
|
|
Bool VulkanRenderer::ExportNativeFence(int* fence) {
|
|
*fence = -1;
|
|
// Every fence the application asks for ends one of its frames as far as a YUV texture is
|
|
// concerned (an offscreen producer - a browser's GPU process - has no present to do it).
|
|
++m_yuvFrameSerial;
|
|
if (m_device == VK_NULL_HANDLE || m_graphicsQueue == VK_NULL_HANDLE || m_frameContext.GetFrameCount() == 0)
|
|
return true; // nothing was ever submitted: nothing to fence
|
|
// A session that writes images ends its frame here, as at a flush; a compositor that only
|
|
// samples them keeps its reads for its present, which follows the fence's work anyway.
|
|
const Bool boundary = m_sharedImageImplicitSync;
|
|
if (boundary) ReleaseHeldSharedImages();
|
|
if (m_sharedImageSyncFd && m_sharedImageSignal == VK_NULL_HANDLE)
|
|
m_sharedImageSignal = TakeSharedImageSemaphore(/*exportable=*/true);
|
|
m_sharedImageSignalSubmitted = VK_NULL_HANDLE;
|
|
if (!HasPendingRecordedWork() && m_sharedImageSignal != VK_NULL_HANDLE)
|
|
m_frameContext.BeginCommandRecording();
|
|
const Bool submitted = HasPendingRecordedWork() && FlushPendingCommands();
|
|
if (HasPendingRecordedWork()) MGLOG_E_ONCE("Magma: a native fence's commands could not be submitted");
|
|
const VkSemaphore signal =
|
|
submitted ? std::exchange(m_sharedImageSignalSubmitted, VK_NULL_HANDLE) : VK_NULL_HANDLE;
|
|
int exported = -1;
|
|
if (signal == VK_NULL_HANDLE || !ExportSharedImageSignal(signal, exported)) exported = -1;
|
|
if (exported < 0) {
|
|
// No descriptor: the work is waited out, so the caller may report it completed.
|
|
if (!WaitForSubmitsUpTo(m_submitCounter, UINT64_MAX))
|
|
MGLOG_E_ONCE("Magma: a native fence's work could not be waited for");
|
|
}
|
|
if (boundary && m_sharedImageReads.Pending() != 0) {
|
|
int published = -1;
|
|
#if !defined(_WIN32)
|
|
if (exported >= 0) published = ::dup(exported);
|
|
#endif
|
|
m_sharedImageReads.PublishFrameAsWrite(published);
|
|
}
|
|
*fence = exported;
|
|
return true;
|
|
}
|
|
|
|
void VulkanRenderer::PublishSharedImageReads() {
|
|
const VkSemaphore signal = std::exchange(m_sharedImageSignalSubmitted, VK_NULL_HANDLE);
|
|
int fence = -1;
|
|
// An export also unsignals the semaphore, which is what lets it be signaled again: a
|
|
// submitted signal is exported even when nothing is left to publish it to.
|
|
if (signal != VK_NULL_HANDLE && !ExportSharedImageSignal(signal, fence)) fence = -1;
|
|
if (m_sharedImageReads.Pending() == 0) {
|
|
CloseSyncFile(fence);
|
|
return;
|
|
}
|
|
// No fence (no export on this device): the frame's reads go unpublished - the file
|
|
// comment says why that is the choice.
|
|
// A session that writes images publishes every boundary as a write (PublishSharedImageAccesses).
|
|
if (m_sharedImageImplicitSync)
|
|
m_sharedImageReads.PublishFrameAsWrite(fence);
|
|
else
|
|
m_sharedImageReads.PublishFrame(fence);
|
|
}
|
|
|
|
void VulkanRenderer::AbandonSharedImageReads() {
|
|
// Acquires recorded into the dropped recording never ran, so those images are not held;
|
|
// ones an earlier flush submitted are, and the next frame boundary releases them.
|
|
SizeT kept = 0;
|
|
for (const auto& hold : m_sharedImagesHeld) {
|
|
if (hold.acquireSubmit <= m_submitCounter) {
|
|
m_sharedImagesHeld[kept++] = hold;
|
|
continue;
|
|
}
|
|
if (auto* resource = m_textureManager->FindWireTextureResource(hold.key))
|
|
resource->sharedImageHeld = false;
|
|
}
|
|
m_sharedImagesHeld.resize(kept);
|
|
// Its waits and signal were never submitted. A wait semaphore's unconsumed temporary
|
|
// payload is replaced by its next import; the signal semaphore was never signaled.
|
|
for (const auto& wait : m_sharedImageWaits) m_freeSharedImageWaitSemaphores.push_back(wait.semaphore);
|
|
m_sharedImageWaits.clear();
|
|
if (m_sharedImageSignal != VK_NULL_HANDLE)
|
|
m_freeSharedImageSignalSemaphores.push_back(std::exchange(m_sharedImageSignal, VK_NULL_HANDLE));
|
|
}
|
|
#endif // MOBILEGL_BUILD_DISAGGREGATED
|