[Refactor] (CMakeLists.txt, MG_Backend/DirectVulkan): split VulkanRenderer.cpp and its fragments by area

- code moves unchanged; helpers several files use are in VulkanRendererInternal.h
This commit is contained in:
2026-10-16 04:28:47 -04:00
parent b0e229f2a3
commit 98c8e8833c
28 changed files with 9460 additions and 9348 deletions
+13
View File
@@ -510,6 +510,19 @@ set(SOURCE_FILES
MobileGL/MG_Backend/DirectVulkan/MagmaSession.cpp
MobileGL/MG_Backend/DirectVulkan/VmaImpl.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRendererCopy.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRendererDevice.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRendererDraw.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRendererPipeline.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRendererPresent.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRendererSubmit.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlit.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthMipmap.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolve.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/WireYuvImage.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp
@@ -145,11 +145,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 baseInstance = 0;
};
// The verb's handles identify server stores; never inspect a client binding.
//
// P8-D: THE GPU READS THE WORDS (VulkanRenderer::DrawWireIndirectNative, WireDraw.inc), as
// P8-D: THE GPU READS THE WORDS (VulkanRenderer::DrawWireIndirectNative, WireDraw.cpp), as
// on the monolith arm. The CPU expansion below is kept for the one shape the monolith arm
// also expands - a COUNT form the device cannot issue natively (no VK_KHR_draw_indirect_count,
// or maxdrawcount > 1 without multiDrawIndirect: VulkanRenderer.cpp's
@@ -432,7 +430,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DispatchComputeIndirect(GLintptr indirect) {
// P8-SV: THE GPU READS THE GROUP COUNTS on the wire arm too - VulkanRenderer's
// DispatchComputeIndirect takes its transport branch into DispatchWireComputeIndirect
// (WireDraw.inc), which issues vkCmdDispatchIndirect from the verb's store. The CPU read
// (WireDraw.cpp), which issues vkCmdDispatchIndirect from the verb's store. The CPU read
// that was here (ReadWireBuffer, a whole-GPU wait once a shader had written the store) is gone.
pVulkanRenderer->DispatchComputeIndirect(indirect);
return;
@@ -165,7 +165,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
//
// What stood here re-ran spvReflectCreateShaderModule over EVERY stage module of the
// program, and it did so PER DRAW: a MagmaProgramSource is a borrowed view that
// WireDraw.inc's WireProgramSource constructs afresh for each wire draw, so the lazy
// WireDraw.cpp's WireProgramSource constructs afresh for each wire draw, so the lazy
// `m_storageBlocksReady` latch could never outlive one. The list it rebuilt is a pure
// function of the linked program, so it is now built once at link time and travels in
// the archive (LinkArtifacts::storageBlocks), which takes the SPIR-V reflector off the
@@ -65,7 +65,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return hash;
}
// P7 wave 0, the descriptor half of WireFramebuffer.inc's MagmaWireFatal - same message,
// P7 wave 0, the descriptor half of WireFramebuffer.cpp's MagmaWireFatal - same message,
// same seam, same reason. Four of the fifteen P7-marked refusals die here.
[[noreturn]] static void WireDescriptorFatal(const char* detail) {
MG_Pipe::MGPipeSessionFail(MG_Pipe::MGPipeFatalFamily::UnmigratedVerb,
@@ -939,7 +939,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// MarkWireBufferGpuWritten below already assumes any binding here may be
// written. A correct copy needs a post-dispatch copy-back from the transient
// slice to the unaligned source, ordered with a barrier - and the place to
// record that is the draw/dispatch tail in VulkanRenderer.cpp / WireDraw.inc,
// record that is the draw/dispatch tail in VulkanRenderer.cpp / WireDraw.cpp,
// which belong to wave-2 packages B and C, not to this file. Losing the
// dispatch with a named line in the role log beats returning stale counters
// with no error at all. docs/archive/notes/p7/magma-a.md carries the recipe.
@@ -78,7 +78,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool VertexInputStateFactory::BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask, BackendVertexInputState& out) const {
// The caller's state is a per-draw scratch (WireDraw.inc): reset it without freeing.
// The caller's state is a per-draw scratch (WireDraw.cpp): reset it without freeing.
out.ResetKeepingCapacity();
for (Uint32 location = 0; location < elements.AttributeCount; ++location) {
const auto& attr = elements.Attributes[location];
@@ -981,7 +981,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const MG_Pipe::MGPResourceDesc& desc = record.Desc;
// A YUV shared image is converted INTO the texture's own RGBA8 storage, which the rest of
// this function defines as for any texture; the image is only named here, and the
// renderer converts it at its first use in each frame (WireYuvImage.inc).
// renderer converts it at its first use in each frame (WireYuvImage.cpp).
if (record.SharedImageId != resource.yuvImageId && !m_syncingYuvTextureStorage) {
// Respecified away from (or to another) YUV image: its source goes with it, once the
// GPU is done with it (a released resource's Reset is deferred).
@@ -1847,7 +1847,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// the image to VK_QUEUE_FAMILY_FOREIGN_EXT in GENERAL - also the layout every wire
// descriptor samples in - once per frame it writes. Each use acquires it again when the
// write generation moved, waiting for that write's fence on the GPU, and the frame boundary
// releases it back (VulkanRenderer::AcquireSharedImage, WireSharedImage.inc).
// releases it back (VulkanRenderer::AcquireSharedImage, WireSharedImage.cpp).
TextureResource replacement;
replacement.image = imported.image;
@@ -352,7 +352,7 @@ public:
Uint64 sharedImageKey = 0;
// A YUV shared image (NV12/P010) is never the texture's image: the texture keeps its own
// RGBA8 level 0, into which the renderer converts the image at its first use in each frame
// (WireYuvImage.inc) - `yuvSource` being that conversion's import of the buffer, made on
// (WireYuvImage.cpp) - `yuvSource` being that conversion's import of the buffer, made on
// first use, and `yuvConvertedFrame` the frame it last ran in. `yuvOwner` holds the
// registry's image for as long as the texture names it.
Uint64 yuvImageId = 0;
File diff suppressed because it is too large Load Diff
@@ -185,13 +185,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// queue submission order already puts every earlier draw's access to the old texels
// ahead of the copy. The preserve arm keeps the wait: it retires the old image.
// (rd12 Magma: the wait was ~1.8 ms/frame of fence stall, one per lightmap update.)
// P15 (WireFramebuffer.inc): runs an image's deferred clears now, outside any pass.
// P15 (WireFramebuffer.cpp): runs an image's deferred clears now, outside any pass.
void MaterializeWirePendingClears(VkTextureManager::TextureResource& resource);
Bool FlushWirePendingCommandsForTextureUpload() {
return !HasPendingRecordedWork() || FlushPendingCommands();
}
// P7 wave 2 package B3: the submission that will carry whatever is recorded NEXT.
// RetireWireObjects (WireDraw.inc) already tags future objects with exactly this, and
// RetireWireObjects (WireDraw.cpp) already tags future objects with exactly this, and
// for the same reason: a draw being set up now is not in any submission yet, so the
// index that names it is one past the counter. Deliberately NOT
// GetSyncPointSubmitIndex(), which answers m_submitCounter when nothing is recorded -
@@ -201,7 +201,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// from the wire stores MGPipeApplier().VerbIndirectBuffer / VerbIndirectParameterBuffer
// name. `offset` / `countOffset` are byte offsets into those stores; `stride` 0 means
// tightly packed. FALSE = this device cannot issue the COUNT form natively (the monolith
// arm's own condition, see the definition in WireDraw.inc) and the caller must read the
// arm's own condition, see the definition in WireDraw.cpp) and the caller must read the
// words on the CPU; TRUE = issued, or declined by name.
Bool DrawWireIndirectNative(GLenum mode, GLenum type, Uint64 offset, GLsizei drawcount, GLsizei stride,
Bool indexed, Bool counted, Uint64 countOffset);
@@ -236,7 +236,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// submission's sync_file in `*fence` (-1: none, and the work is waited out here), and - for
// a session that writes images - the boundary PublishSharedImageAccesses makes.
Bool ExportNativeFence(int* fence);
// The reading side's per-use hook (WireSharedImage.inc): every shared-image texture the
// The reading side's per-use hook (WireSharedImage.cpp): every shared-image texture the
// wire draw or dispatch being set up samples (VkTextureManager::NoteSharedImageUse) is
// acquired from the foreign family before it, when it moved or is not held.
void AcquireNotedSharedImages() {
@@ -538,7 +538,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// P7 wave 2-B2, CONTRACT-P7 §5.2 (B'): THE FRONTEND MEMBERS ARE PULL-BUILD ONLY. In a
// disaggregated build the monolith arm blits and generates depth mips with the baked
// modules (WireColorBlit.inc, WireDepthMipmap.inc), so no DirectVulkan object holds a
// modules (WireColorBlit.cpp, WireDepthMipmap.cpp), so no DirectVulkan object holds a
// ProgramObject, a ShaderObject or a SamplerObject any more - which is what takes
// ProgramObject::{AllocateLifetimeId, AttachShader, Link, ~ProgramObject},
// ShaderObject::{SetShaderSource, Compile, JoinPendingCompile, ReleaseCompileNode,
@@ -614,7 +614,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect,
Bool isWriteTarget = false);
void TransitionWireImage(WireImage& image, VkImageLayout layout, Bool coalesceWithLastFullBarrier = false);
// P15: WIRE DEFERRED CLEARS (WireFramebuffer.inc). A clear of images the open pass
// P15: WIRE DEFERRED CLEARS (WireFramebuffer.cpp). A clear of images the open pass
// holds is recorded inside it; a whole-image clear of others waits on the image
// (WirePendingClear) for the next pass that draws into it (LOAD_OP_CLEAR) or the first
// other use (MaterializeWirePendingClears).
@@ -674,7 +674,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// P7 wave 2-B2, CONTRACT-P7 §3.2 (`multisample-blit-aspect`): the SECOND arm of the
// multisample depth/stencil resolve, for the device that does not carry
// VK_KHR_depth_stencil_resolve. One baked pass per aspect, sample zero, no frontend
// object - see WireMultisampleResolve.inc. `resolved` must be a single-sample image of
// object - see WireMultisampleResolve.cpp. `resolved` must be a single-sample image of
// the source's format and extent; the rect is expressed by the scissor.
Bool ResolveWireDepthStencilWithShader(WireImage source, WireImage resolved,
VkImageAspectFlags aspect, GLint sx0, GLint sy0,
@@ -695,7 +695,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// one entry point the import needs (PFN_vkGetAndroidHardwareBufferPropertiesANDROID).
Bool m_wireAhbImport = false;
void* m_wireGetAhbProperties = nullptr;
// YUV shared images (WireYuvImage.inc): the device enabled samplerYcbcrConversion, and the
// YUV shared images (WireYuvImage.cpp): the device enabled samplerYcbcrConversion, and the
// conversion pipelines made so far (one per buffer format and colour conversion).
Bool m_samplerYcbcrConversion = false;
struct WireYuvResources;
@@ -718,13 +718,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
UnorderedMap<Uint64, SharedImagePresentTarget> m_sharedImagePresentTargets;
void DestroySharedImagePresentTargets(Bool onlyDead);
// SHARED-IMAGE SYNCHRONISATION (WireSharedImage.inc). The sync_file fences other sessions
// SHARED-IMAGE SYNCHRONISATION (WireSharedImage.cpp). The sync_file fences other sessions
// publish become binary semaphores: a temporary SYNC_FD import is a WAIT, an exportable
// semaphore a SIGNAL, of the next frame submission - whichever of SubmitPendingCommandBuffer
// and Present's submit comes first, which is the one carrying what was recorded since.
// Each semaphore is recycled once the submission that used it has completed.
// m_sharedImageSyncFd: the device imports and exports SYNC_FD binary semaphores; without
// it the waits happen on the CPU and nothing is exported (see WireSharedImage.inc).
// it the waits happen on the CPU and nothing is exported (see WireSharedImage.cpp).
Bool m_sharedImageSyncFd = false;
// The session writes images - it ends frames at publishing flushes
// (PublishSharedImageAccesses), or it reached one through a framebuffer (ResolveWireImage):
@@ -832,7 +832,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DestroyWireColorBlitResources();
void ResetWireColorBlitFramePool(Uint32 frameIndex);
void RetireWireColorBlitTargets(Bool all);
// P7 wave 2-B, CONTRACT-P7 §5.1 (A): the baked depth-mip program (WireDepthMipmap.inc).
// P7 wave 2-B, CONTRACT-P7 §5.1 (A): the baked depth-mip program (WireDepthMipmap.cpp).
// One level of a depth chain, source and destination being two depth subresources of
// the same image; the caller loops. It owns no frontend object, which is what the
// monolith arm's GenerateDepthMipmapWithShader cannot say.
@@ -841,7 +841,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
WireDepthMipmapResources* m_wireDepthMipmapResources = nullptr;
void DestroyWireDepthMipmapResources();
// P7 wave 2-B (exit gate 3): the readback's own write-visibility barrier, recorded before
// the copy instead of inferred from the image's tracked layout. See WireFramebuffer.inc.
// the copy instead of inferred from the image's tracked layout. See WireFramebuffer.cpp.
void RecordWireReadbackWriteBarrier();
void ReadWirePixels(GLint x, GLint y, GLsizei width, GLsizei height,
GLenum format, GLenum type, void* pixels);
@@ -905,7 +905,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DestroyWireDrawPassCacheEntry(WireDrawPassCacheEntry& entry);
Vector<WireDrawPassCacheEntry> m_wireDrawPassCache;
WireDrawPassKey m_wireDrawPassKey;
// P15: the open pass's attachment that is `image` (WireFramebuffer.inc's deferred clears).
// P15: the open pass's attachment that is `image` (WireFramebuffer.cpp's deferred clears).
static Uint32 OpenPassAttachmentOf(const WireImage& image, const Vector<WireDrawAttachmentKey>& attachments);
WireRenderPassCompatibilityTable m_wireRenderPassCompatibility;
// The interned compatibility id of m_wireDrawPass's attachments, or 0 when not known yet.
@@ -914,7 +914,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 m_wireDrawPassCompatibilityId = 0;
// P14: SetupWireDraw's per-draw containers, cleared and refilled every draw instead of
// allocated and freed (the heap was ~10% of a Minecraft frame on the wire arm). A nested
// SetupWireDraw - nothing does that today - would use a local set (WireDraw.inc).
// SetupWireDraw - nothing does that today - would use a local set (WireDraw.cpp).
struct WireDrawScratch {
VertexInputStateFactory::BackendVertexInputState input;
Vector<BufferSlice> vertexSlices;
@@ -989,7 +989,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool m_wireNativeIndirectDraw = false;
void DispatchWireCompute(GLuint x, GLuint y, GLuint z);
// P8-SV: glDispatchComputeIndirect on the wire arm - vkCmdDispatchIndirect from the
// dispatch-indirect store, as the monolith arm issues it (WireDraw.inc).
// dispatch-indirect store, as the monolith arm issues it (WireDraw.cpp).
void DispatchWireComputeIndirect(GLintptr offset);
// False: the device was lost and the session latched; the caller drops its draw/dispatch.
Bool RewindWireDescriptorSetsIfDue();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,846 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRendererDraw.cpp
// 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
#include "VulkanRendererInternal.h"
namespace MobileGL::MG_Backend::DirectVulkan {
// Keyed on the frontend's never-reused lifetime id, NOT on the GL name. The name is
// recycled the moment glDeleteTransformFeedbacks gives it back, so a name-keyed slot
// handed a brand-new object the counter group - and the m_xfbCountersValid /
// m_xfbLastSeenGeneration entries - of the object that died under that name.
//
// Slots are never handed back (there is no backend entry telling this renderer that a span
// closed - registering the EndTransformFeedback one would flip the "captures through its own
// driver" test FixupGsStripCaptureOrder makes of it), so once all sixteen are owned a new
// object has to take one over. The victim is chosen among owners with NO OPEN SPAN: an object
// whose span is closed, or which no longer exists at all, can never resume, so its counter
// bytes are dead. Least-recently-used ALONE would be exactly the wrong rule - GL only permits
// another object to capture while this one is PAUSED, so the paused span whose counters the
// slots exist to protect is by construction the least recently used entry. Taking a group over
// resets its counter state, because those bytes describe the previous owner's span.
Uint32 VulkanRenderer::CurrentXfbCounterSlot() {
constexpr Uint32 kNoSlot = static_cast<Uint32>(kXfbCounterObjectSlots);
const Uint64 identity = MG_Pipe::gPipeInputs->GetBoundTransformFeedbackLifetimeId();
MOBILEGL_ASSERT(identity != 0,
"transform feedback object reported the free-slot sentinel (0) as its identity - "
"every slot would then read as 'mine' without ever being claimed");
Uint32 freeSlot = kNoSlot;
for (Uint32 slot = 0; slot < kNoSlot; ++slot) {
if (m_xfbCounterSlotOwner[slot] == identity) {
m_xfbCounterSlotLastUse[slot] = ++m_xfbCounterSlotUseSerial;
return slot;
}
if (m_xfbCounterSlotOwner[slot] == 0 && freeSlot == kNoSlot) {
freeSlot = slot;
}
}
Uint32 slot = freeSlot;
if (slot == kNoSlot) {
for (Uint32 candidate = 0; candidate < kNoSlot; ++candidate) {
if (m_xfbCounterSlotOwner[candidate] != 0 &&
MG_Pipe::MGPipeApplier().StreamOutputSpans.count(m_xfbCounterSlotOwner[candidate]) != 0) {
continue;
}
if (slot == kNoSlot || m_xfbCounterSlotLastUse[candidate] < m_xfbCounterSlotLastUse[slot]) {
slot = candidate;
}
}
}
if (slot == kNoSlot) {
// Sixteen capture spans open at once. Whatever is taken loses its resume offset and
// restarts at byte 0 of its capture buffers, which is a wrong picture rather than a
// slow one - hence a report rather than a silent choice.
MGLOG_E_ONCE("CurrentXfbCounterSlot: all %zu counter groups belong to transform feedback objects "
"with an open capture span; the least recently used one is taken over and that span "
"will restart at offset 0 instead of appending",
kXfbCounterObjectSlots);
slot = 0;
for (Uint32 candidate = 1; candidate < kNoSlot; ++candidate) {
if (m_xfbCounterSlotLastUse[candidate] < m_xfbCounterSlotLastUse[slot]) {
slot = candidate;
}
}
}
m_xfbCounterSlotOwner[slot] = identity;
m_xfbCounterSlotLastUse[slot] = ++m_xfbCounterSlotUseSerial;
m_xfbCountersValid[slot] = false;
m_xfbLastSeenGeneration[slot] = 0;
return slot;
}
Bool VulkanRenderer::BeginXfbCaptureForDraw(FrameContext::FrameData& frame) {
if (!m_transformFeedbackFeatureEnabled || !MG_Pipe::gPipeInputs->IsLive() ||
!MG_Pipe::gPipeInputs->IsTransformFeedbackActive()) {
return false;
}
// A paused span captures nothing, and the counter buffers keep their values, so the
// next resumed draw appends exactly where the last captured one stopped - which is
// what pause/resume means (ARB_transform_feedback2).
if (MG_Pipe::gPipeInputs->IsTransformFeedbackPaused()) {
return false;
}
// The bound pipeline's last pre-rasterization stage has to have been declared with Xfb
// (VUID-vkCmdBeginTransformFeedbackEXT-None-04128). Everything above this line reads GL
// state, which cannot answer that: a program can be built as a capture variant and still
// end up with a module carrying no Xfb mode - the clip/XFB validation backstop rewinding
// past the decoration, or XfbCaptureDecoratePass resolving none of the requested varyings
// and changing nothing. Declining the span leaves the capture buffers untouched, which is
// the same nothing the driver would have written, without the undefined behaviour.
if (m_currentDrawXfbCaptureDeclined) {
MGLOG_E_ONCE("BeginXfbCaptureForDraw: declining the capture span - the bound program's last "
"pre-rasterization stage carries no Xfb execution mode, so recording one would be "
"undefined behaviour rather than a capture");
return false;
}
return BeginWireXfbCaptureForDraw(frame);
}
Bool VulkanRenderer::BeginXfbCaptureWithBuffers(FrameContext::FrameData& frame, Uint32 bufferCount,
const VkBuffer* buffers, const VkDeviceSize* offsets, const VkDeviceSize* sizes) {
if (!m_xfbCounterBuffer.IsValid()) {
if (!m_xfbCounterBuffer.Create({
.allocator = m_allocator,
.size = 16 * kXfbCounterObjectSlots,
.usage = VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_COUNTER_BUFFER_BIT_EXT |
VK_BUFFER_USAGE_TRANSFER_DST_BIT,
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
})) {
MGLOG_E_ONCE("BeginXfbCaptureForDraw: failed to create the counter buffer");
return false;
}
}
m_currentDrawXfbBufferCount = bufferCount;
m_currentDrawXfbBufferMask = 0;
for (Uint32 i = 0; i < bufferCount; ++i) {
if (buffers[i] == VK_NULL_HANDLE) continue;
m_currentDrawXfbBufferMask |= 1u << i;
s_vkCmdBindTransformFeedbackBuffersEXT(frame.commandBuffer, i, 1, buffers + i,
offsets + i, sizes + i);
}
const Uint32 counterSlot = CurrentXfbCounterSlot();
const Uint64 generation = MG_Pipe::gPipeInputs->GetTransformFeedbackGeneration();
const Bool resume = m_xfbCountersValid[counterSlot] && m_xfbLastSeenGeneration[counterSlot] == generation;
m_xfbLastSeenGeneration[counterSlot] = generation;
VkBuffer counterBuffers[4] = {};
VkDeviceSize counterOffsets[4] = {};
for (SizeT i = 0; i < bufferCount; ++i) {
if (!(m_currentDrawXfbBufferMask & (1u << i))) continue;
counterBuffers[i] = m_xfbCounterBuffer.GetHandle();
counterOffsets[i] = static_cast<VkDeviceSize>(counterSlot) * 16 + static_cast<VkDeviceSize>(i) * 4;
}
if (resume) {
s_vkCmdBeginTransformFeedbackEXT(frame.commandBuffer, 0, static_cast<Uint32>(bufferCount),
counterBuffers, counterOffsets);
} else {
s_vkCmdBeginTransformFeedbackEXT(frame.commandBuffer, 0, 0, nullptr, nullptr);
}
return true;
}
void VulkanRenderer::EndXfbCaptureForDraw(FrameContext::FrameData& frame, Bool began) {
if (!began) {
return;
}
// Exactly the targets used by this draw's Begin, independent of frontend
// objects and of whether the span came from the wire or monolith state.
const Uint32 bufferCount = m_currentDrawXfbBufferCount;
const Uint32 counterSlot = CurrentXfbCounterSlot();
VkBuffer counterBuffers[4] = {};
VkDeviceSize counterOffsets[4] = {};
for (SizeT i = 0; i < bufferCount; ++i) {
if (!(m_currentDrawXfbBufferMask & (1u << i))) continue;
counterBuffers[i] = m_xfbCounterBuffer.GetHandle();
counterOffsets[i] = static_cast<VkDeviceSize>(counterSlot) * 16 + static_cast<VkDeviceSize>(i) * 4;
}
s_vkCmdEndTransformFeedbackEXT(frame.commandBuffer, 0, static_cast<Uint32>(bufferCount), counterBuffers,
counterOffsets);
m_xfbCountersValid[counterSlot] = true;
m_xfbWritesPendingVisibility = true;
}
// GL makes transform feedback results visible to every later command on their own, with no
// glMemoryBarrier in between - unlike shader storage writes, which is why the barrier the
// Vulkan memory model requires has to be supplied here rather than by the application. It
// cannot be recorded where the write happens (inside the capturing draw's render pass, which
// declares no self-dependency), so it is emitted at the next point that could read the
// captured buffer: the following draw, or a readback.
void VulkanRenderer::MakeXfbWritesVisible() {
if (!m_xfbWritesPendingVisibility) {
return;
}
m_xfbWritesPendingVisibility = false;
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
}
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
}
VkMemoryBarrier memoryBarrier{};
memoryBarrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER;
memoryBarrier.srcAccessMask =
VK_ACCESS_TRANSFORM_FEEDBACK_WRITE_BIT_EXT | VK_ACCESS_TRANSFORM_FEEDBACK_COUNTER_WRITE_BIT_EXT;
// Every way a captured buffer can be read back: replayed as vertex attributes or indices
// by glDrawTransformFeedback, sampled through a uniform or storage binding, sourced as an
// indirect command, copied out, or mapped.
memoryBarrier.dstAccessMask =
VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT |
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT |
VK_ACCESS_HOST_READ_BIT | VK_ACCESS_MEMORY_READ_BIT |
VK_ACCESS_TRANSFORM_FEEDBACK_COUNTER_READ_BIT_EXT;
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_TRANSFORM_FEEDBACK_BIT_EXT,
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memoryBarrier, 0, nullptr, 0, nullptr);
}
void VulkanRenderer::DrawArrays(const DrawCmd& payload) {
auto& frame = m_frameContext.GetCurrent();
if (!SetupDraw(frame, payload.mode, 0, payload.params)) {
return;
}
MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__);
VkCommandBuffer& commandBuffer = frame.commandBuffer;
const Bool xfbActive = BeginXfbCaptureForDraw(frame);
BeginXfbQueryForDraw(commandBuffer, xfbActive);
const Bool occlusionActive = BeginOcclusionForDraw(commandBuffer);
vkCmdDraw(commandBuffer,
payload.params.vertexCount,
payload.params.instanceCount,
payload.params.firstVertex,
payload.params.firstInstance);
EndOcclusionForDraw(commandBuffer, occlusionActive);
EndXfbCaptureForDraw(frame, xfbActive);
EndXfbQueryForDraw(commandBuffer);
}
Bool VulkanRenderer::StartOcclusionQueryCapture() {
if (!m_hostQueryResetEnabled || s_vkResetQueryPool == nullptr) {
return false;
}
if (m_occlusionQueryPool == VK_NULL_HANDLE) {
VkQueryPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
poolInfo.queryType = VK_QUERY_TYPE_OCCLUSION;
poolInfo.queryCount = kOcclusionQuerySlots;
if (vkCreateQueryPool(m_device, &poolInfo, nullptr, &m_occlusionQueryPool) != VK_SUCCESS) {
MGLOG_E_ONCE("StartOcclusionQueryCapture: vkCreateQueryPool failed");
m_occlusionQueryPool = VK_NULL_HANDLE;
return false;
}
s_vkResetQueryPool(m_device, m_occlusionQueryPool, 0, kOcclusionQuerySlots);
}
m_occlusionActiveSlots.clear();
m_occlusionCaptureActive = true;
return true;
}
void VulkanRenderer::StopOcclusionQueryCapture(Vector<Uint32>& outSlots) {
outSlots = Move(m_occlusionActiveSlots);
m_occlusionActiveSlots.clear();
m_occlusionCaptureActive = false;
}
Bool VulkanRenderer::ResolveOcclusionQueryResult(const Vector<Uint32>& slots, Uint64& outSamples) {
outSamples = 0;
if (slots.empty()) {
return true;
}
if (m_occlusionQueryPool == VK_NULL_HANDLE) {
return true;
}
auto& frame = m_frameContext.GetCurrent();
if (frame.isCommandRecording) {
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
}
if (!SubmitReadbackCommandsAndWait(frame)) {
return false;
}
}
for (const Uint32 slot : slots) {
Uint64 value = 0;
const VkResult result =
vkGetQueryPoolResults(m_device, m_occlusionQueryPool, slot, 1, sizeof(value), &value, sizeof(value),
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT);
if (result == VK_SUCCESS) {
outSamples += value;
}
s_vkResetQueryPool(m_device, m_occlusionQueryPool, slot, 1);
}
return true;
}
Bool VulkanRenderer::StartXfbQueryCapture(Uint32 kind) {
if (!m_xfbQueriesSupported || !m_hostQueryResetEnabled || s_vkResetQueryPool == nullptr ||
s_vkCmdBeginQueryIndexedEXT == nullptr || kind > 1) {
return false;
}
if (m_xfbQueryPool == VK_NULL_HANDLE) {
VkQueryPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
poolInfo.queryType = VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT;
poolInfo.queryCount = kXfbQuerySlots;
if (vkCreateQueryPool(m_device, &poolInfo, nullptr, &m_xfbQueryPool) != VK_SUCCESS) {
MGLOG_E_ONCE("StartXfbQueryCapture: vkCreateQueryPool failed");
m_xfbQueryPool = VK_NULL_HANDLE;
return false;
}
s_vkResetQueryPool(m_device, m_xfbQueryPool, 0, kXfbQuerySlots);
}
// The reroute pool, on the first GENERATED span that needs it. A creation
// failure disarms rather than failing the capture: the stream path still
// answers (with the driver's defect), which beats answering nothing.
if (kind == 1 && m_primGenRerouteKind != MG_Util::SelfTest::PrimGenRerouteKind::None &&
m_primGenReroutePool == VK_NULL_HANDLE) {
VkQueryPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
poolInfo.queryCount = kXfbQuerySlots;
if (m_primGenRerouteKind == MG_Util::SelfTest::PrimGenRerouteKind::PrimitivesGeneratedExt) {
// The query Vulkan defines for this GL target; counts vertex stream 0
// when begun with plain vkCmdBeginQuery.
poolInfo.queryType = VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT;
} else {
poolInfo.queryType = VK_QUERY_TYPE_PIPELINE_STATISTICS;
// The clipping-stage INVOCATION counter: one per primitive reaching
// primitive clipping (GL's CLIPPING_INPUT_PRIMITIVES) - post-tess/GS,
// pre-clip, and per spec still counted under rasterizer discard, which
// is exactly the set GL_PRIMITIVES_GENERATED is defined over. The
// stage's OUTPUT count (CLIPPING_PRIMITIVES_BIT) would be wrong:
// clipping may drop or split primitives.
poolInfo.pipelineStatistics = VK_QUERY_PIPELINE_STATISTIC_CLIPPING_INVOCATIONS_BIT;
}
if (vkCreateQueryPool(m_device, &poolInfo, nullptr, &m_primGenReroutePool) != VK_SUCCESS) {
MGLOG_E_ONCE("StartXfbQueryCapture: reroute pool creation failed; the "
"PRIMITIVES_GENERATED reroute is disarmed and XFB-inactive draws keep "
"the stream query");
m_primGenReroutePool = VK_NULL_HANDLE;
m_primGenRerouteKind = MG_Util::SelfTest::PrimGenRerouteKind::None;
} else {
s_vkResetQueryPool(m_device, m_primGenReroutePool, 0, kXfbQuerySlots);
}
}
m_xfbQueryActiveSlots[kind].clear();
m_xfbQueryCaptureActive[kind] = true;
if (kind == 1) {
m_primGenRerouteActiveSlots.clear();
}
return true;
}
Bool VulkanRenderer::ArePausedDrawsGpuCounted() const {
// Exactly the gate BeginXfbQueryForDraw applies per draw, so a span told "armed"
// really does get a reroute slot for every draw with no open capture - a paused
// span's draws included.
const Bool rerouteArmed = m_primGenRerouteKind != MG_Util::SelfTest::PrimGenRerouteKind::None &&
m_primGenReroutePool != VK_NULL_HANDLE;
// Otherwise the paused draw takes a stream slot, which is an exact count of it
// on a driver the probe measured as counting capture-less draws.
return rerouteArmed || m_primGenStreamCountsXfbInactiveDraws;
}
void VulkanRenderer::StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots,
Vector<Uint32>& outRerouteSlots) {
if (kind > 1) {
return;
}
outSlots = Move(m_xfbQueryActiveSlots[kind]);
m_xfbQueryActiveSlots[kind].clear();
m_xfbQueryCaptureActive[kind] = false;
outRerouteSlots.clear();
if (kind == 1) {
outRerouteSlots = Move(m_primGenRerouteActiveSlots);
m_primGenRerouteActiveSlots.clear();
}
}
Bool VulkanRenderer::ResolveXfbQueryResult(const Vector<Uint32>& slots, const Vector<Uint32>& rerouteSlots,
Bool wantGenerated, Uint64& outPrimitives) {
outPrimitives = 0;
const Bool haveStreamSlots = !slots.empty() && m_xfbQueryPool != VK_NULL_HANDLE;
// Reroute slots only ever accumulate the GENERATED target (see
// BeginXfbQueryForDraw); WRITTEN never opens one.
const Bool haveRerouteSlots =
wantGenerated && !rerouteSlots.empty() && m_primGenReroutePool != VK_NULL_HANDLE;
if (!haveStreamSlots && !haveRerouteSlots) {
return true;
}
auto& frame = m_frameContext.GetCurrent();
if (frame.isCommandRecording) {
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
}
if (!SubmitReadbackCommandsAndWait(frame)) {
return false;
}
}
if (haveStreamSlots) {
for (const Uint32 slot : slots) {
Uint64 pair[2] = {0, 0}; // {primitivesWritten, primitivesNeeded}
const VkResult result =
vkGetQueryPoolResults(m_device, m_xfbQueryPool, slot, 1, sizeof(pair), pair, sizeof(pair),
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT);
if (result == VK_SUCCESS) {
outPrimitives += pair[wantGenerated ? 1 : 0];
}
}
}
if (haveRerouteSlots) {
for (const Uint32 slot : rerouteSlots) {
// Both reroute pool kinds answer one 64-bit primitive count per slot.
Uint64 generated = 0;
const VkResult result = vkGetQueryPoolResults(
m_device, m_primGenReroutePool, slot, 1, sizeof(generated), &generated,
sizeof(generated), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT);
if (result == VK_SUCCESS) {
outPrimitives += generated;
}
}
}
return true;
}
void VulkanRenderer::BeginXfbQueryForDraw(VkCommandBuffer commandBuffer, Bool xfbActive) {
m_xfbQuerySlotOpen = false;
m_primGenRerouteSlotOpen = false;
if ((!m_xfbQueryCaptureActive[0] && !m_xfbQueryCaptureActive[1]) || m_xfbQueryPool == VK_NULL_HANDLE) {
return;
}
// Every draw with no OPEN capture is the stream query's silent case, and that
// includes a draw made while the GL span is merely PAUSED (the pause closes the
// capture, so BeginXfbCaptureForDraw already answered false for it). Paused
// draws are rerouted like any other: the frontend's CPU paused-primitive
// counter cannot stand in for them - it is written by only 3 of the ~15 draw
// entry points (never the instanced, indirect or multi-draw ones) and answers 0
// for GL_PATCHES by design, since the tessellator's amplification is not
// knowable on the CPU - which is exactly the CTS's shape. Double counting is
// prevented on the other side instead: a GENERATED span opened while this
// reroute is armed ignores that CPU counter entirely (see
// ArePausedDrawsGpuCounted and DirectVulkan.cpp's XfbGenerated resolve), so
// every XFB-inactive draw in the span is priced exactly once, by this pool.
const Bool rerouteGenerated = m_xfbQueryCaptureActive[1] &&
m_primGenRerouteKind != MG_Util::SelfTest::PrimGenRerouteKind::None &&
m_primGenReroutePool != VK_NULL_HANDLE && !xfbActive;
// The stream slot stays for WRITTEN whatever the reroute does (with capture
// inactive its primitivesWritten is 0, which is the correct WRITTEN answer),
// and for GENERATED wherever this draw is not rerouted - so one GL query span
// may accumulate stream slots (XFB-active draws) and reroute slots
// (XFB-inactive draws) side by side.
const Bool wantStreamSlot =
m_xfbQueryCaptureActive[0] || (m_xfbQueryCaptureActive[1] && !rerouteGenerated);
if (wantStreamSlot) {
const Uint32 slot = m_xfbQuerySlotCursor;
m_xfbQuerySlotCursor = (m_xfbQuerySlotCursor + 1) % kXfbQuerySlots;
// Slots are never host-reset at read time (both GL targets may reference one
// slot); recycle them here instead.
s_vkResetQueryPool(m_device, m_xfbQueryPool, slot, 1);
s_vkCmdBeginQueryIndexedEXT(commandBuffer, m_xfbQueryPool, slot, 0, 0);
if (m_xfbQueryCaptureActive[0]) {
m_xfbQueryActiveSlots[0].push_back(slot);
}
if (m_xfbQueryCaptureActive[1] && !rerouteGenerated) {
m_xfbQueryActiveSlots[1].push_back(slot);
}
m_xfbQuerySlotOpen = true;
m_xfbQueryOpenSlot = slot;
}
if (rerouteGenerated) {
// Latched at INFO on purpose: it is the pinned integration lane's arming
// observable (the shape UnlocatedIoBlockScenario asserts), and the builds
// CI runs compile INFO in.
MGLOG_I_ONCE("PRIMITIVES_GENERATED reroute engaged: an XFB-inactive draw accumulates "
"through the %s pool",
m_primGenRerouteKind ==
MG_Util::SelfTest::PrimGenRerouteKind::PrimitivesGeneratedExt
? "VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT"
: "clipping-invocations statistics");
const Uint32 slot = m_primGenRerouteSlotCursor;
m_primGenRerouteSlotCursor = (m_primGenRerouteSlotCursor + 1) % kXfbQuerySlots;
// Same recycle-at-begin discipline as the stream pool. Both pool kinds
// are begun with plain vkCmdBeginQuery (a PRIMITIVES_GENERATED_EXT
// query begun this way counts vertex stream 0).
s_vkResetQueryPool(m_device, m_primGenReroutePool, slot, 1);
vkCmdBeginQuery(commandBuffer, m_primGenReroutePool, slot, 0);
m_primGenRerouteActiveSlots.push_back(slot);
m_primGenRerouteSlotOpen = true;
m_primGenRerouteOpenSlot = slot;
}
}
void VulkanRenderer::EndXfbQueryForDraw(VkCommandBuffer commandBuffer) {
if (m_xfbQuerySlotOpen) {
s_vkCmdEndQueryIndexedEXT(commandBuffer, m_xfbQueryPool, m_xfbQueryOpenSlot, 0);
m_xfbQuerySlotOpen = false;
}
if (m_primGenRerouteSlotOpen) {
vkCmdEndQuery(commandBuffer, m_primGenReroutePool, m_primGenRerouteOpenSlot);
m_primGenRerouteSlotOpen = false;
}
}
Bool VulkanRenderer::BeginOcclusionForDraw(VkCommandBuffer commandBuffer) {
if (!m_occlusionCaptureActive || m_occlusionQueryPool == VK_NULL_HANDLE) {
return false;
}
const Uint32 slot = m_occlusionSlotCursor;
m_occlusionSlotCursor = (m_occlusionSlotCursor + 1) % kOcclusionQuerySlots;
// Slots recycle after their read; a wrapped-past unread slot is stale, so
// reset it here (host reset - the slot's prior GPU use has long retired).
s_vkResetQueryPool(m_device, m_occlusionQueryPool, slot, 1);
vkCmdBeginQuery(commandBuffer, m_occlusionQueryPool, slot,
m_occlusionQueryPreciseEnabled ? VK_QUERY_CONTROL_PRECISE_BIT : 0);
m_occlusionActiveSlots.push_back(slot);
return true;
}
void VulkanRenderer::EndOcclusionForDraw(VkCommandBuffer commandBuffer, Bool began) {
if (!began) {
return;
}
vkCmdEndQuery(commandBuffer, m_occlusionQueryPool, m_occlusionActiveSlots.back());
}
void VulkanRenderer::DrawElements(const DrawIndexedCmd& payload) {
auto& frame = m_frameContext.GetCurrent();
DrawCmdParam vertexRange{};
vertexRange.vertexCount = payload.params.indexCount + (payload.params.vertexOffset > 0
? static_cast<Uint32>(payload.params.vertexOffset)
: 0);
vertexRange.instanceCount = payload.params.instanceCount;
vertexRange.firstVertex = 0;
vertexRange.firstInstance = static_cast<Uint32>(payload.params.firstInstance);
vertexRange.baseVertex = payload.params.vertexOffset;
// Direct DrawElements fetches exactly the indices in its view, so vertex-stream
// conversion may bound its work by scanning them.
vertexRange.indexRangeIsExactView = true;
if (!SetupDraw(frame, payload.mode, DrawSetupAspect::IndexBuffer, vertexRange,
&payload.indexBufferView)) {
return;
}
MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__);
VkCommandBuffer& commandBuffer = frame.commandBuffer;
const Bool xfbActive = BeginXfbCaptureForDraw(frame);
BeginXfbQueryForDraw(commandBuffer, xfbActive);
const Bool occlusionActive = BeginOcclusionForDraw(commandBuffer);
vkCmdDrawIndexed(commandBuffer,
payload.params.indexCount,
payload.params.instanceCount,
payload.params.firstIndex,
payload.params.vertexOffset,
payload.params.firstInstance);
EndOcclusionForDraw(commandBuffer, occlusionActive);
EndXfbCaptureForDraw(frame, xfbActive);
EndXfbQueryForDraw(commandBuffer);
}
void VulkanRenderer::MultiDrawArrays(const MultiDrawCmd& payload) {
auto& frame = m_frameContext.GetCurrent();
// One state/pipeline setup covering the union of all sub-draw vertex ranges, then a vkCmdDraw
// per range -- mirrors MultiDrawElements.
DrawCmdParam vertexRange{};
for (Uint32 idraw = 0; idraw < payload.drawCount; ++idraw) {
vertexRange.vertexCount = std::max(vertexRange.vertexCount,
payload.pParams[idraw].firstVertex + payload.pParams[idraw].vertexCount);
vertexRange.instanceCount = std::max(vertexRange.instanceCount, payload.pParams[idraw].instanceCount);
vertexRange.firstInstance = std::max(vertexRange.firstInstance, payload.pParams[idraw].firstInstance);
}
if (!SetupDraw(frame, payload.mode, 0, vertexRange)) {
return;
}
MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__);
EmitMultiDraw(frame.commandBuffer, payload.pParams, payload.drawCount);
}
// The tier-2 indirect batch uploads the param arrays as-is: the leading members of the
// renderer's draw-parameter structs are exactly Vulkan's indirect command layouts, and
// vkCmdDraw(Indexed)Indirect accepts any 4-aligned stride >= the command size, so the
// trailing CPU-side metadata rides along unread instead of forcing a repack.
static_assert(sizeof(DrawIndexedCmdParam) == sizeof(VkDrawIndexedIndirectCommand) &&
offsetof(DrawIndexedCmdParam, indexCount) == offsetof(VkDrawIndexedIndirectCommand, indexCount) &&
offsetof(DrawIndexedCmdParam, instanceCount) ==
offsetof(VkDrawIndexedIndirectCommand, instanceCount) &&
offsetof(DrawIndexedCmdParam, firstIndex) == offsetof(VkDrawIndexedIndirectCommand, firstIndex) &&
offsetof(DrawIndexedCmdParam, vertexOffset) ==
offsetof(VkDrawIndexedIndirectCommand, vertexOffset) &&
offsetof(DrawIndexedCmdParam, firstInstance) ==
offsetof(VkDrawIndexedIndirectCommand, firstInstance),
"DrawIndexedCmdParam must alias VkDrawIndexedIndirectCommand for the tier-2 multi-draw upload");
static_assert(sizeof(DrawCmdParam) % 4 == 0 && sizeof(DrawCmdParam) >= sizeof(VkDrawIndirectCommand) &&
offsetof(DrawCmdParam, vertexCount) == offsetof(VkDrawIndirectCommand, vertexCount) &&
offsetof(DrawCmdParam, instanceCount) == offsetof(VkDrawIndirectCommand, instanceCount) &&
offsetof(DrawCmdParam, firstVertex) == offsetof(VkDrawIndirectCommand, firstVertex) &&
offsetof(DrawCmdParam, firstInstance) == offsetof(VkDrawIndirectCommand, firstInstance),
"DrawCmdParam must lead with VkDrawIndirectCommand for the tier-2 multi-draw upload");
void VulkanRenderer::EmitMultiDraw(VkCommandBuffer commandBuffer, const DrawCmdParam* pParams, Uint32 drawCount) {
if (drawCount == 0) {
return;
}
if (drawCount == 1) {
vkCmdDraw(commandBuffer, pParams[0].vertexCount, pParams[0].instanceCount, pParams[0].firstVertex,
pParams[0].firstInstance);
return;
}
// Tier 1: VK_EXT_multi_draw. vkCmdDrawMultiEXT shares one instanceCount/firstInstance
// across the whole batch, so the batch must be uniform in both (GL's glMultiDrawArrays
// always is: 1/0).
if (m_multiDrawAllowExt) {
Bool uniformInstances = true;
for (Uint32 idraw = 1; idraw < drawCount; ++idraw) {
if (pParams[idraw].instanceCount != pParams[0].instanceCount ||
pParams[idraw].firstInstance != pParams[0].firstInstance) {
uniformInstances = false;
break;
}
}
if (uniformInstances) {
static thread_local Vector<VkMultiDrawInfoEXT> infos;
infos.resize(drawCount);
for (Uint32 idraw = 0; idraw < drawCount; ++idraw) {
infos[idraw].firstVertex = pParams[idraw].firstVertex;
infos[idraw].vertexCount = pParams[idraw].vertexCount;
}
for (Uint32 base = 0; base < drawCount; base += m_maxMultiDrawCount) {
const Uint32 chunk = std::min(drawCount - base, m_maxMultiDrawCount);
s_vkCmdDrawMultiEXT(commandBuffer, chunk, infos.data() + base, pParams[0].instanceCount,
pParams[0].firstInstance, sizeof(VkMultiDrawInfoEXT));
}
return;
}
}
// Tier 2: multiDrawIndirect - one vkCmdDrawIndirect over a transient command array.
// A sub-draw with firstInstance != 0 is illegal in an indirect command without the
// drawIndirectFirstInstance feature; such a batch falls to the unrolled tier.
if (m_multiDrawAllowIndirect) {
Bool firstInstanceLegal = m_drawIndirectFirstInstanceFeatureEnabled;
if (!firstInstanceLegal) {
firstInstanceLegal = true;
for (Uint32 idraw = 0; idraw < drawCount; ++idraw) {
if (pParams[idraw].firstInstance != 0) {
firstInstanceLegal = false;
break;
}
}
}
const Uint32 maxIndirectCount = m_physicalDevice.properties.limits.maxDrawIndirectCount;
if (firstInstanceLegal && maxIndirectCount > 0) {
BufferSlice commandSlice{};
if (m_bufferManager.UploadTransient(BufferKind::Indirect, m_frameContext.GetCurrentFrameIndex(),
pParams,
static_cast<VkDeviceSize>(drawCount) * sizeof(DrawCmdParam),
sizeof(Uint32), commandSlice)) {
for (Uint32 base = 0; base < drawCount; base += maxIndirectCount) {
const Uint32 chunk = std::min(drawCount - base, maxIndirectCount);
vkCmdDrawIndirect(commandBuffer, commandSlice.buffer,
commandSlice.offset +
static_cast<VkDeviceSize>(base) * sizeof(DrawCmdParam),
chunk, sizeof(DrawCmdParam));
}
return;
}
// Transient arena refused the upload: fall through to the unrolled tier.
}
}
// Tier 3: unrolled loop, byte-identical fallback (and the only tier where a SPIR-V
// DrawIndex consumer sees 0 for every sub-draw instead of the sub-draw index).
for (Uint32 idraw = 0; idraw < drawCount; ++idraw) {
vkCmdDraw(commandBuffer, pParams[idraw].vertexCount, pParams[idraw].instanceCount,
pParams[idraw].firstVertex, pParams[idraw].firstInstance);
}
}
void VulkanRenderer::EmitMultiDrawIndexed(VkCommandBuffer commandBuffer, const DrawIndexedCmdParam* pParams,
Uint32 drawCount) {
if (drawCount == 0) {
return;
}
if (drawCount == 1) {
vkCmdDrawIndexed(commandBuffer, pParams[0].indexCount, pParams[0].instanceCount, pParams[0].firstIndex,
pParams[0].vertexOffset, pParams[0].firstInstance);
return;
}
// Tier 1: VK_EXT_multi_draw. VkMultiDrawIndexedInfoEXT carries per-draw
// firstIndex/indexCount/vertexOffset (pVertexOffset = nullptr keeps the per-draw
// offsets), but instanceCount/firstInstance are batch-wide, so the batch must be
// uniform in both (GL's glMultiDrawElements* always is: 1/0).
if (m_multiDrawAllowExt) {
Bool uniformInstances = true;
for (Uint32 idraw = 1; idraw < drawCount; ++idraw) {
if (pParams[idraw].instanceCount != pParams[0].instanceCount ||
pParams[idraw].firstInstance != pParams[0].firstInstance) {
uniformInstances = false;
break;
}
}
if (uniformInstances) {
static thread_local Vector<VkMultiDrawIndexedInfoEXT> infos;
infos.resize(drawCount);
for (Uint32 idraw = 0; idraw < drawCount; ++idraw) {
infos[idraw].firstIndex = pParams[idraw].firstIndex;
infos[idraw].indexCount = pParams[idraw].indexCount;
infos[idraw].vertexOffset = pParams[idraw].vertexOffset;
}
for (Uint32 base = 0; base < drawCount; base += m_maxMultiDrawCount) {
const Uint32 chunk = std::min(drawCount - base, m_maxMultiDrawCount);
s_vkCmdDrawMultiIndexedEXT(commandBuffer, chunk, infos.data() + base,
pParams[0].instanceCount,
static_cast<Uint32>(pParams[0].firstInstance),
sizeof(VkMultiDrawIndexedInfoEXT), nullptr);
}
return;
}
}
// Tier 2: multiDrawIndirect - one vkCmdDrawIndexedIndirect over a transient command
// array (DrawIndexedCmdParam aliases VkDrawIndexedIndirectCommand, see static_assert).
if (m_multiDrawAllowIndirect) {
Bool firstInstanceLegal = m_drawIndirectFirstInstanceFeatureEnabled;
if (!firstInstanceLegal) {
firstInstanceLegal = true;
for (Uint32 idraw = 0; idraw < drawCount; ++idraw) {
if (pParams[idraw].firstInstance != 0) {
firstInstanceLegal = false;
break;
}
}
}
const Uint32 maxIndirectCount = m_physicalDevice.properties.limits.maxDrawIndirectCount;
if (firstInstanceLegal && maxIndirectCount > 0) {
BufferSlice commandSlice{};
if (m_bufferManager.UploadTransient(BufferKind::Indirect, m_frameContext.GetCurrentFrameIndex(),
pParams,
static_cast<VkDeviceSize>(drawCount) *
sizeof(DrawIndexedCmdParam),
sizeof(Uint32), commandSlice)) {
for (Uint32 base = 0; base < drawCount; base += maxIndirectCount) {
const Uint32 chunk = std::min(drawCount - base, maxIndirectCount);
vkCmdDrawIndexedIndirect(commandBuffer, commandSlice.buffer,
commandSlice.offset +
static_cast<VkDeviceSize>(base) * sizeof(DrawIndexedCmdParam),
chunk, sizeof(DrawIndexedCmdParam));
}
return;
}
}
}
// Tier 3: unrolled loop, byte-identical fallback (and the only tier where a SPIR-V
// DrawIndex consumer sees 0 for every sub-draw instead of the sub-draw index).
for (Uint32 idraw = 0; idraw < drawCount; ++idraw) {
vkCmdDrawIndexed(commandBuffer, pParams[idraw].indexCount, pParams[idraw].instanceCount,
pParams[idraw].firstIndex, pParams[idraw].vertexOffset, pParams[idraw].firstInstance);
}
}
void VulkanRenderer::MultiDrawElements(const MultiDrawIndexedCmd& payload) {
auto& frame = m_frameContext.GetCurrent();
DrawCmdParam vertexRange{};
for (Uint32 idraw = 0; idraw < payload.drawCount; ++idraw) {
vertexRange.vertexCount = std::max(vertexRange.vertexCount, payload.pParams[idraw].indexCount);
vertexRange.instanceCount = std::max(vertexRange.instanceCount, payload.pParams[idraw].instanceCount);
vertexRange.firstInstance = std::max(vertexRange.firstInstance,
static_cast<Uint32>(payload.pParams[idraw].firstInstance));
}
if (!SetupDraw(frame, payload.mode, DrawSetupAspect::IndexBuffer, vertexRange,
&payload.indexBufferView)) {
return;
}
MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__);
// Collapse contiguous sub-draw runs BEFORE tier dispatch: merging shrinks the
// param span every tier consumes (fewer VkMultiDrawIndexedInfoEXT entries, a
// smaller transient command array, fewer unrolled vkCmdDrawIndexed). Per-sub-draw
// command emission in the driver dominates a Sodium-shaped multi-draw
// (steady-state profile: >60% of the case inside the Vulkan driver's
// vkCmdDrawIndexed encoding for 132x32 sub-draws/frame), and a chunk
// renderer's sub-draws are runs of adjacent index ranges over one buffer.
// Two draws are one iff they concatenate to an identical index stream:
// - a LIST topology (points/lines/triangles). Strips/fans/loops would
// weld primitives across the seam.
// - the accumulated count ends on a primitive boundary, otherwise GL
// discards the dangling indices at the sub-draw's end but the merged
// stream would assemble them with the next sub-draw's indices.
// - primitive restart is off: with restart on, a sentinel mid-stream
// resets assembly, so a partial primitive before the seam would
// otherwise be discarded per sub-draw (same dangling-index argument).
// - identical baseVertex/instancing and firstIndex adjacency, so the
// merged range fetches exactly the two sub-draws' indices in order.
Uint32 mergeGranularity = 0;
switch (payload.mode) {
case GL_POINTS: mergeGranularity = 1; break;
case GL_LINES: mergeGranularity = 2; break;
case GL_TRIANGLES: mergeGranularity = 3; break;
default: break;
}
if (mergeGranularity != 0) {
const RenderStateParameters& rsp = MG_Pipe::gPipeInputs->GetRenderStateParameters();
if (rsp.PrimitiveRestartEnabled || rsp.PrimitiveRestartFixedIndexEnabled) {
mergeGranularity = 0;
}
}
const DrawIndexedCmdParam* pParams = payload.pParams;
Uint32 drawCount = payload.drawCount;
static thread_local Vector<DrawIndexedCmdParam> mergedParams;
if (mergeGranularity != 0) {
mergedParams.clear();
mergedParams.reserve(drawCount);
Uint32 idraw = 0;
while (idraw < drawCount) {
DrawIndexedCmdParam head = pParams[idraw];
++idraw;
if (head.indexCount == 0) {
continue; // draws nothing, contributes nothing to a run
}
if (head.instanceCount == 1) {
while (idraw < drawCount) {
const DrawIndexedCmdParam& next = pParams[idraw];
if (next.indexCount == 0) {
++idraw;
continue;
}
if (head.indexCount % mergeGranularity != 0 ||
next.instanceCount != 1 ||
next.vertexOffset != head.vertexOffset ||
next.firstInstance != head.firstInstance ||
next.firstIndex != head.firstIndex + head.indexCount ||
head.indexCount + next.indexCount < head.indexCount) {
break;
}
head.indexCount += next.indexCount;
++idraw;
}
}
mergedParams.push_back(head);
}
pParams = mergedParams.data();
drawCount = static_cast<Uint32>(mergedParams.size());
}
EmitMultiDrawIndexed(frame.commandBuffer, pParams, drawCount);
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,413 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRendererInternal.h
// 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
#pragma once
#include "RenderPassGuard.h"
#include "VulkanRenderer.h"
#include "SubmitFencePrefix.h"
#include "MG_Util/X11/DisplayGuard.h"
#include "OffscreenSurfaceRoute.h"
#include "MG_Backend/DirectVulkan/SubgroupSupportPolicy.h"
#include "MG_Backend/ClientReadback.h"
#include "VertexInputStateFactory.h"
#include "VertexInputStateBuilder.h"
#include "MG_State/GLState/Core.h"
#include <MG_Backend/MGPipe/PipeInputs.h>
// P5c ev: the GPU-write announcement routes through the reverse channel (R2).
#include <MG_Impl/Pipe/ResourceTracker.h>
#include "MG_State/GLState/ProgramState/ProgramObject.h"
#include "MG_State/GLState/ProgramState/ShaderObject.h"
#include "MG_State/GLState/SamplerState/SamplerObject.h"
#include "MG_State/GLState/TextureState/TextureObject.h"
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
#include "MG_Impl/GLImpl/Texture/GL_Texture.h"
// The generation window GenerateMipmap defines its chain in: one definition, shared with the
// frontend arm and with the wire carrier that publishes it (MGPMipPlan::LevelCount).
#include "MG_Impl/GLImpl/Texture/MipmapGenerationPlan.h"
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
// Only reached from an MGLOG_W, which the shipping INFO log level compiles out - so the
// missing include never broke a default build and did break every WARN/DEBUG-level one.
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToVk/RenderStateEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Math/HalfFloat.h"
#include "MG_Util/Metrics/PipeStats.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include "MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h"
#include "MG_Util/Texture/PixelStoreProcessor.h"
#include <Config.h>
// P7 wave 0: the seam WireFramebuffer.cpp's MagmaWireFatal and WireDraw.cpp's
// WireBufferLegacyFatal die through. Declared by MG_Pipe on purpose - see PipeSessionFail.h.
#include <MG_Pipe/PipeSessionFail.h>
// P5c (T5 / tx): the server's staged-texture shadow GenerateMipmap defines its chain on.
#include <MG_Backend/Record/StagedTextureStore.h>
// P7 wave 2 package B3: rule I's tally for WireDraw.cpp's silent draw drops.
#include "WireDeclineTally.h"
#include "WireColorBlitFilter.h"
#include "WireDepthResolveArm.h"
#include "WireCopySliceProbe.h"
#include "WireDiscardStateProbe.h"
#include "WireDepthResolveProbe.h"
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Remote/Server/ServerLoop.h>
// Shared images: the present side resolves the image it imports (WireSharedImage.cpp).
#include <MG_Remote/Server/SharedImageRegistry.h>
#endif
#include <algorithm>
#include <bit>
#include <cstdlib>
#include <cstring>
#include <vulkan/utility/vk_format_utils.h>
#include <vulkan/vulkan_core.h>
#ifdef __ANDROID__
#include <sys/system_properties.h>
#endif
#if MOBILEGL_BUILD_DISAGGREGATED && !defined(_WIN32)
#include <unistd.h> // close(): shared-image sync_file descriptors (WireSharedImage.cpp)
#endif
#if defined(__APPLE__)
#include <CoreGraphics/CoreGraphics.h>
#include <objc/message.h>
#include <objc/objc.h>
#include <objc/runtime.h>
#endif
namespace MobileGL::MG_Backend::DirectVulkan {
#if defined(__APPLE__)
namespace {
template <typename Fn>
Fn ObjcMsgSend() {
return reinterpret_cast<Fn>(objc_msgSend);
}
} // namespace
#endif
static Bool IsQuarterTurnPreTransform(VkSurfaceTransformFlagBitsKHR preTransform) {
return preTransform == VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR ||
preTransform == VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR;
}
// Redundant dynamic-state elimination for the per-draw hot path: within one
// command-buffer recording, a vkCmdSet* whose values already match what the
// command buffer holds is skipped. Valid because every PipelineFactory
// pipeline declares the same eight dynamic states, so the values persist
// across those pipeline binds; the shadow resets whenever a recording
// (re)begins, and whenever an auxiliary pipeline with a narrower dynamic
// set (blit, depth-mipmap) binds - their static state makes the
// corresponding dynamic values undefined per the spec.
struct DynamicStateShadow {
// Last graphics pipeline bound on the frame command buffer. Pipeline
// binds are command-buffer state (they survive render-pass boundaries),
// so the same reset points that invalidate dynamic state - recording
// (re)begin and the aux blit pipelines' raw binds - are exactly the
// points where this becomes unknown.
Bool graphicsPipelineValid = false;
VkPipeline graphicsPipeline = VK_NULL_HANDLE;
// Index/vertex buffer binds are command-buffer state too. Terrain
// sections and GUI quads share one sequential index buffer, and GUI
// batches often reuse a vertex arena buffer, so skipping identical
// rebinds removes a large share of per-draw driver calls.
Bool indexBindValid = false;
VkBuffer indexBuffer = VK_NULL_HANDLE;
VkDeviceSize indexOffset = 0;
VkIndexType indexType = VK_INDEX_TYPE_MAX_ENUM;
static constexpr Uint32 kMaxShadowedVertexBindings = 8;
// The wire buffer manager's store-destroy epoch the vertex/index halves were stamped at:
// a wire store can be destroyed mid-recording and its VkBuffer handle value re-minted, so
// an equal handle is only the same buffer while this epoch has not moved.
Uint64 bindStoreEpoch = 0;
Bool vertexBindValid = false;
Uint32 vertexBindingCount = 0;
VkBuffer vertexBuffers[kMaxShadowedVertexBindings] = {};
VkDeviceSize vertexOffsets[kMaxShadowedVertexBindings] = {};
Bool viewportValid = false;
VkViewport viewport{};
Bool scissorValid = false;
VkRect2D scissor{};
Bool blendConstantsValid = false;
Float blendConstants[4] = {0.0f, 0.0f, 0.0f, 0.0f};
Bool depthBiasValid = false;
Float depthBiasConstantFactor = 0.0f;
Float depthBiasSlopeFactor = 0.0f;
Float depthBiasClamp = 0.0f;
Bool lineWidthValid = false;
Float lineWidth = 0.0f;
Bool stencilValid = false;
Uint32 stencilFrontCompareMask = 0;
Uint32 stencilBackCompareMask = 0;
Uint32 stencilFrontWriteMask = 0;
Uint32 stencilBackWriteMask = 0;
Uint32 stencilFrontReference = 0;
Uint32 stencilBackReference = 0;
// glShadingRateEXT / glShadingRateCombinerOpsEXT, in RenderStateParameters' packed form
// (MGPackShadingRate). Only pipelines built with fragmentShadingRateDynamic carry the state.
Bool fragmentShadingRateValid = false;
Uint8 fragmentShadingRate = 0;
Uint8 fragmentShadingRateCombinerOp0 = 0;
Uint8 fragmentShadingRateCombinerOp1 = 0;
// Gate over the whole per-draw dynamic-state tail (viewport, scissor, blend
// constants, depth bias, line width, stencil) - see ApplyDynamicDrawStateTail.
// Every GL input of that tail lives in RenderState's value-shadowed parameters:
// each setter early-outs on an equal value and bumps the parameters version
// otherwise, and capability toggles (scissor test) bump it too. So an unchanged
// version + unchanged pass geometry means re-running the tail could only
// re-derive the exact values already applied on this command buffer. The
// remaining input, the swapchain pre-transform, cannot change mid-recording
// (a swapchain recreate retires the command buffer, and recording begin resets
// this whole shadow); the value key below pins it anyway.
Bool dynamicTailValid = false;
Uint dynamicTailParamsVersion = 0;
Int dynamicTailExtentX = 0;
Int dynamicTailExtentY = 0;
Bool dynamicTailIsDefaultFbo = false;
// VALUE key over the tail's inputs, as a second-level gate behind the version.
// The parameters version is ONE counter for all of RenderState, so anything that
// is not tail input - a GL_BLEND toggle, a glBlendFuncSeparate, a glColorMask -
// moves it and forced a full tail re-run. Blaze3D toggles blend around every
// batch, so that was a per-draw re-derivation of six dynamic states that could
// not have changed. Equal key => the six Apply* below would each re-derive the
// value their shadow already holds and emit nothing, so the tail is skippable.
//
// Complete input inventory of ApplyDynamicDrawStateTail, one line per reader
// (each accessor it replaces is a verified plain field read of the same
// RenderStateParameters field - RenderState.cpp):
// ApplyGLViewportState : Viewports[0], DepthRanges[0], + extent/isDefaultFbo/preTransform
// ApplyBlendConstants : BlendColor
// ApplyPolygonOffsetState : PolygonOffsetUnits, PolygonOffsetFactor, PolygonOffsetClamp
// ApplyLineWidthState : LineWidth (see the caveat below)
// ApplyStencilState : StencilStates[0..1].{ValueMask, WriteMask, Ref}
// scissor rect : ScissorTestEnabledMask bit 0, ScissorBoxes[0],
// + extent/isDefaultFbo/preTransform
// Caveat, unchanged from the version-only gate: ApplyLineWidthState also clamps
// to the ACTIVE BACKEND OBJECT's aliased line-width range. Those are device
// limits queried once at backend init and constant for the renderer's lifetime,
// so they are not part of the key (the version gate never covered them either).
struct DynamicTailKey {
Float viewport[4] = {0.0f, 0.0f, 0.0f, 0.0f};
Float depthRange[2] = {0.0f, 0.0f};
Float blendColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
Float polygonOffsetFactor = 0.0f;
Float polygonOffsetUnits = 0.0f;
Float polygonOffsetClamp = 0.0f;
Float lineWidth = 0.0f;
Uint32 stencilValueMask[2] = {0, 0};
Uint32 stencilWriteMask[2] = {0, 0};
Int stencilRef[2] = {0, 0};
Int scissorBox[4] = {0, 0, 0, 0};
Int extentX = 0;
Int extentY = 0;
Uint32 preTransform = 0;
Bool scissorEnabled = false;
Bool isDefaultFbo = false;
Bool operator==(const DynamicTailKey& other) const {
// NaN in any float input makes this false, which only costs a redundant
// tail run - never a skipped one.
for (Uint32 i = 0; i < 4; ++i) {
if (viewport[i] != other.viewport[i] || blendColor[i] != other.blendColor[i] ||
scissorBox[i] != other.scissorBox[i]) {
return false;
}
}
for (Uint32 i = 0; i < 2; ++i) {
if (depthRange[i] != other.depthRange[i] ||
stencilValueMask[i] != other.stencilValueMask[i] ||
stencilWriteMask[i] != other.stencilWriteMask[i] ||
stencilRef[i] != other.stencilRef[i]) {
return false;
}
}
return polygonOffsetFactor == other.polygonOffsetFactor &&
polygonOffsetUnits == other.polygonOffsetUnits &&
polygonOffsetClamp == other.polygonOffsetClamp && lineWidth == other.lineWidth &&
extentX == other.extentX && extentY == other.extentY &&
preTransform == other.preTransform && scissorEnabled == other.scissorEnabled &&
isDefaultFbo == other.isDefaultFbo;
}
};
DynamicTailKey dynamicTailKey{};
};
// Per Magma session (MagmaSession.h): it shadows the session's own command buffer. Inline,
// so every file of the renderer shares the one instance.
inline SessionLocal<DynamicStateShadow> g_dynamicStateShadow;
static void ResetDynamicStateShadow() {
*g_dynamicStateShadow = {};
}
// Whether the GL scissor test clips viewport index `index`. The scissor rectangles start
// all-zero, and all-zero there means "never written" (RenderState's constructor), not an
// empty box: GL's initial box is the whole window, so until the application's first
// glScissor an enabled test clips nothing. Read as a box it would clip EVERYTHING, so every
// reader of the box asks this first. The resolution matches the viewport's (ComputeGLViewport
// resolves a never-written viewport to the current framebuffer's extent).
static Bool GLScissorTestClips(Uint32 index = 0) {
const RenderStateParameters& parameters = MG_Pipe::gPipeInputs->GetRenderStateParameters();
const Uint32 bit = 1u << index;
return (parameters.ScissorTestEnabledMask & bit) != 0 && (parameters.ScissorBoxWrittenMask & bit) != 0;
}
// Vertex attribute locations are tracked in Uint32 bitmasks, so MAX_VERTEX_ATTRIBS is both the
// state-layer storage bound and the width of every mask below. Keep them in lockstep.
static constexpr Uint32 kMaxVertexAttribs =
static_cast<Uint32>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
static_assert(kMaxVertexAttribs <= 32, "Vertex attribute masks are Uint32");
// The loops below walk locations [0, kMaxVertexAttribs) and index programObj.vertexInputTypes with
// each one, so that array must be at least as wide.
static_assert(kMaxVertexAttribs <= ProgramFactory::VkProgramObject::kMaxVertexInputLocations,
"vertexInputTypes is indexed by vertex attribute location");
namespace {
static void GetImageTransitionSourceState(VkImageLayout oldLayout, VkPipelineStageFlags& outSrcStageMask,
VkAccessFlags& outSrcAccessMask) {
switch (oldLayout) {
case VK_IMAGE_LAYOUT_UNDEFINED:
case VK_IMAGE_LAYOUT_PRESENT_SRC_KHR:
outSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
outSrcAccessMask = 0;
break;
case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
outSrcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
outSrcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
break;
case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL:
outSrcStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
outSrcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
break;
case VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL:
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
outSrcStageMask = VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT;
outSrcAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT;
break;
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
outSrcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT;
outSrcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
break;
case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:
outSrcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT;
outSrcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
break;
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
outSrcStageMask = VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT;
outSrcAccessMask = VK_ACCESS_SHADER_READ_BIT;
break;
default:
outSrcStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
outSrcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
break;
}
}
} // namespace
// A tightly packed depth/stencil readback, one aspect or both, in the client's own width.
// The two arms that read depth or stencil off a Vulkan image - the monolith
// ReadDepthStencilImageToClient and the wire ReadTextureImageWire - copy their aspect into a
// host-visible buffer and both owe the application the same bytes, so they share this one
// encoding rather than each carrying a copy of it. A copy is exactly the kind of pair where
// a fix landed on one side only: the signed ranges and GL_HALF_FLOAT were added to the wire
// caller and the monolith caller kept scaling a GL_SHORT depth into 65535, which put a 0.5
// depth at 0x8000 - a negative index.
static Bool PackDepthStencilTight(const Uint8* depth, const Uint8* stencil, VkFormat sourceFormat, SizeT count,
GLenum format, GLenum type, Vector<Uint8>& result) {
const Bool stencilOnly = format == GL_STENCIL_INDEX;
const auto readDepth = [&](SizeT i) -> Float {
if (sourceFormat == VK_FORMAT_D16_UNORM) {
Uint16 word{}; Memcpy(&word, depth + i * 2, sizeof(word));
return static_cast<Float>(word) / 65535.0f;
}
if (sourceFormat == VK_FORMAT_X8_D24_UNORM_PACK32 || sourceFormat == VK_FORMAT_D24_UNORM_S8_UINT) {
Uint32 word{}; Memcpy(&word, depth + i * 4, sizeof(word));
return static_cast<Float>(word & 0xffffffu) / 16777215.0f;
}
Float value{}; Memcpy(&value, depth + i * sizeof(value), sizeof(value));
return value;
};
SizeT size = 0;
switch (type) {
case GL_BYTE: case GL_UNSIGNED_BYTE: size = 1; break;
case GL_SHORT: case GL_UNSIGNED_SHORT: case GL_HALF_FLOAT: size = 2; break;
case GL_INT: case GL_UNSIGNED_INT: case GL_FLOAT: size = 4; break;
case GL_UNSIGNED_INT_24_8: size = 4; break;
case GL_FLOAT_32_UNSIGNED_INT_24_8_REV: size = 8; break;
default: return false;
}
const Bool packed = type == GL_UNSIGNED_INT_24_8 || type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV;
if ((format == GL_DEPTH_STENCIL) != packed) return false;
result.resize(count * size);
for (SizeT i = 0; i < count; ++i) {
Uint8* dst = result.data() + i * size;
const Uint32 s = stencil ? stencil[i] : 0;
const Float d = stencilOnly ? 0.0f : readDepth(i);
switch (type) {
case GL_FLOAT: {
const Float value = stencilOnly ? static_cast<Float>(s) : d;
Memcpy(dst, &value, sizeof(value)); break;
}
case GL_HALF_FLOAT: {
const Uint16 value = MG_Util::EncodeFloatToHalfBits(stencilOnly ? static_cast<Float>(s) : d);
Memcpy(dst, &value, sizeof(value)); break;
}
case GL_UNSIGNED_INT_24_8: {
Uint32 depth24 = 0;
if (sourceFormat == VK_FORMAT_D24_UNORM_S8_UINT) {
Memcpy(&depth24, depth + i * 4, sizeof(depth24));
depth24 &= 0xffffffu;
} else {
depth24 = static_cast<Uint32>(std::llround(std::clamp<double>(d, 0.0, 1.0) * 16777215.0));
}
const Uint32 value = (depth24 << 8) | s;
Memcpy(dst, &value, sizeof(value)); break;
}
case GL_FLOAT_32_UNSIGNED_INT_24_8_REV:
Memcpy(dst, &d, sizeof(d)); Memcpy(dst + 4, &s, sizeof(s)); break;
default: {
const Bool signedType = type == GL_BYTE || type == GL_SHORT || type == GL_INT;
const Uint64 maximum = signedType ? ((Uint64{1} << (size * 8 - 1)) - 1)
: ((Uint64{1} << (size * 8)) - 1);
const Uint32 value = stencilOnly ? s : static_cast<Uint32>(
std::llround(std::clamp<double>(d, 0.0, 1.0) * static_cast<double>(maximum)));
Memcpy(dst, &value, size); break;
}
}
}
return true;
}
// P7 wave 0. THE MESSAGE IS UNCHANGED; where it goes is not. This funnel is how eleven of
// the fifteen P7-marked refusals die (the other four are WireDescriptorFatal's), and until now
// every one of them raised its own abort: no SessionFault frame for the peer, no
// SessionFaultCount() for exit gate S8, and nothing tying the hand-typed word to
// FatalFamilies.def. MGPipeSessionFail hands the line to the hook MG_Remote installs at
// server-role init, whose arm for UnmigratedVerb is SessionFail(MGFatalFamily::
// UnmigratedVerb, ...) - and with no hook installed it logs and aborts exactly as this did.
[[noreturn]] static void MagmaWireFatal(const char* detail) {
MG_Pipe::MGPipeSessionFail(MG_Pipe::MGPipeFatalFamily::UnmigratedVerb,
"MGPipe: Fatal{UnmigratedVerb, \"Magma:%s\"}", detail);
}
// AND THE SAME PROBLEM AGAIN FOR THE SHADER MULTISAMPLE RESOLVE (P7 wave 2-B2). lavapipe
// carries VK_KHR_create_renderpass2 AND VK_KHR_depth_stencil_resolve, so the extension arm
// is taken for every depth/stencil resolve this host can run and the shader arm - the one
// that exists for the device WITHOUT them, which is where the session used to die at
// `Magma:multisample-depth-resolve-capability` - would never execute under any lane. The
// knob drops the extension arm. It does not pretend the extension is missing anywhere else:
// nothing but the arm choice reads it. Inline, so the renderer reads the variable once.
inline Bool MagmaWireForcedShaderDepthResolve() {
static const Bool forced = [] {
const char* value = std::getenv("MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE");
return value && *value && value[0] != '0';
}();
return forced;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,636 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRendererPresent.cpp
// 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
#include "VulkanRendererInternal.h"
namespace MobileGL::MG_Backend::DirectVulkan {
void VulkanRenderer::Present() {
#if MOBILEGL_BUILD_DISAGGREGATED
// A lost device presents nothing (its session is latched and closing).
if (m_deviceLost && LatchWireDeviceLoss("present")) return;
// Present has its own submit path. Tag its last wire pass before that
// submission, rather than retaining it until a draw in the next frame.
RetireWireDrawPass();
#endif
if (m_swapchainObject.GetHandle() == VK_NULL_HANDLE || m_presentSuspended) {
// No usable swapchain: the window was zero-area at initialization, or
// presentation was suspended when the window minimized. Try to bring a
// swapchain up now that the window may have a real size; until then, drop
// this frame's recording instead of submitting - a submit would wait on a
// never-signaled acquire semaphore and reuse a still-signaled fence.
if (!RecreateSwapchain()) {
auto& suspendedFrame = m_frameContext.GetCurrent();
if (VkRenderPassManager::GetActiveRenderPass()) {
VkRenderPassManager::EndRenderPass(suspendedFrame.commandBuffer);
}
if (suspendedFrame.isCommandRecording) {
m_frameContext.EndCommandRecording();
}
m_frameContext.AbandonPreCommandRecording();
suspendedFrame.isCommandRecording = false;
suspendedFrame.hasCommandBufferRecorded = false;
InvalidatePipelineMemo();
#if MOBILEGL_BUILD_DISAGGREGATED
AbandonSharedImageReads();
#endif
// The dropped recording is never submitted, so once the fence
// poll shows the pre-suspension submissions complete the frame
// transients (descriptor sets, transient arenas, deferred
// releases, conversion caches) can rewind; without this a
// minimized-window app accumulates them for the whole
// suspension.
TryDrainFrameTransients();
// The application counted this swap; no image did.
m_swapchainObject.OnFrameDropped();
MGLOG_D("Present skipped: no usable swapchain (zero-area window)");
return;
}
m_presentSuspended = false;
// A slot's Present fence alone does not prove its older pooled
// flushes retired. Wait the full prefix before FrameContext may
// free its retired command buffers and reset the slot fence.
const Uint64 slotSubmit = m_frameContext.GetCurrent().lastSubmitIndex;
if (slotSubmit > m_completedSubmitCounter &&
!WaitForSubmitsUpTo(slotSubmit, UINT64_MAX)) {
if (LatchWireDeviceLoss("deferred-acquire-submit-wait")) return;
MagmaWireFatal("deferred-acquire-submit-wait");
}
const VkResult acquireResult =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
if (acquireResult == VK_SUBOPTIMAL_KHR) {
// Usable image with its acquire signal already armed; a rebuild is scheduled
// only if the surface genuinely no longer matches (see step 4 of Present).
m_swapchainResizeRequested = m_swapchainResizeRequested || SwapchainIsOutOfDate();
} else {
VK_VERIFY(acquireResult, "Present, deferred first WaitAndAcquireNextImage");
}
}
MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(),
"Present, acquired image index out of range");
// A frame that never drew into the window still presents its image: what it recorded
// goes out first, unwaited, and the present's own submission waits for the release.
SubmitAheadOfAcquiredImage();
// A real presented frame is the canonical aging cadence; mid-frame drains
// count against this and only age when presents stop coming.
m_drainsSinceLastPresent = 0;
// Age the content-addressed caches on the same frame-boundary cadence. Each
// keeps its own internal 256-sweep gate, so the per-frame cost is one counter
// increment and compare per cache; entries used by this frame's still-
// unsubmitted recording were stamped this boundary (every command-buffer
// boundary drops the pipeline memo, so the first draw of each recording
// performs a real, stamping lookup) and can never age out.
m_programFactory->OnFrameBoundary();
if (m_pipelineFactory->OnFrameBoundary() > 0) {
InvalidatePipelineMemo(); // an aged-out pipeline may still be memoized
// A recreated pipeline could reuse a freed handle value and alias
// the bind-dedup shadow; force the next draw to re-bind.
g_dynamicStateShadow->graphicsPipelineValid = false;
}
m_samplerManager->OnFrameBoundary();
auto& frame = m_frameContext.GetCurrent();
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
if (activeRenderPass)
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
#if MOBILEGL_BUILD_DISAGGREGATED
// The frame boundary of the shared images this frame read (WireSharedImage.cpp): handed
// back to the foreign family in this recording, and this submission signals the read
// fence their next writer waits for.
ReleaseHeldSharedImages();
if (m_sharedImageSyncFd && m_sharedImageReads.Pending() > 0 && m_sharedImageSignal == VK_NULL_HANDLE)
m_sharedImageSignal = TakeSharedImageSemaphore(/*exportable=*/true);
#endif
// Transition while this frame's recording is still open. A frame that
// rendered only into FBOs has no default-framebuffer render pass, and that
// pass's finalLayout is the only other thing that carries the swapchain
// image to PRESENT_SRC_KHR - so closing the buffer first, which made
// TransitionToPresent refuse to record, handed the image to
// vkQueuePresentKHR in the layout it was acquired in (UNDEFINED on a fresh
// swapchain). The SetImageLayout below then made the tracker's
// disagreement with reality permanent for that image index.
const auto acquiredImageLayout = m_swapchainObject.GetImageLayout(m_imageIndexAcquired);
m_frameContext.TransitionToPresent(m_swapchainObject.GetImage(m_imageIndexAcquired), acquiredImageLayout);
if (frame.isCommandRecording) {
m_frameContext.EndCommandRecording();
frame.hasCommandBufferRecorded = true;
InvalidatePipelineMemo(); // command-buffer boundary: drop the pipeline memo
}
m_frameContext.EndPreCommandRecordingIfOpen();
const Bool shouldSubmitCommandBuffer = frame.hasCommandBufferRecorded;
// 1) Submit current frame work (the pre-pass stream, when recorded,
// rides the same submission strictly ahead of the frame commands).
// Batched texture uploads go first: the frame's commands may sample
// images whose texels only exist in the open upload batch, and
// flushing here also bounds upload latency to one frame.
if (m_textureManager) {
m_textureManager->FlushPendingUploads();
}
auto submitPacket = m_frameContext.GetSubmitInfo(shouldSubmitCommandBuffer, m_imageIndexAcquired);
#if MOBILEGL_BUILD_DISAGGREGATED
if (LatchIfGpuHung("present-submit")) return;
SharedImageSubmitSync sharedImageSync;
AttachSharedImageSync(submitPacket.submitInfo, sharedImageSync);
const GpuProgressMarkers::Bracket bracket = m_progressMarkers.Acquire(m_completedSubmitCounter);
const VkResult presentSubmit = GpuProgressMarkers::SubmitBracketed(m_graphicsQueue, submitPacket.submitInfo,
frame.imageInFlightFence, bracket);
if (presentSubmit == VK_SUCCESS) m_progressMarkers.Submitted(bracket, m_submitCounter + 1); // RegisterSubmit below
else m_progressMarkers.Abandon(bracket);
#else
const VkResult presentSubmit = vkQueueSubmit(m_graphicsQueue, 1, &submitPacket.submitInfo, frame.imageInFlightFence);
#endif
NoteDeviceLoss(presentSubmit, "Present vkQueueSubmit");
if (presentSubmit != VK_SUCCESS && LatchWireDeviceLoss("present-submit")) return;
VK_VERIFY(presentSubmit);
RegisterSubmit(frame.imageInFlightFence, /*pooledFence=*/false);
frame.lastSubmitIndex = m_submitCounter;
#if MOBILEGL_BUILD_DISAGGREGATED
CommitSharedImageSync(sharedImageSync);
PublishSharedImageReads();
#endif
frame.isCommandRecording = false;
frame.hasCommandBufferRecorded = false;
frame.hasPreCommandBufferRecorded = false;
m_swapchainObject.SetImageLayout(m_imageIndexAcquired, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
// 2) Present current frame.
auto presentPacket = m_frameContext.GetPresentInfo(m_swapchainObject.GetHandle(), m_imageIndexAcquired);
auto result = vkQueuePresentKHR(m_presentQueue, &presentPacket.presentInfo);
if (result == VK_SUBOPTIMAL_KHR) {
// Suboptimal is not a reason to rebuild on its own: a driver may report it for a
// surface whose size and orientation still match what we built from (Android does
// this routinely), and rebuilding on it alone destroys every pipeline and
// reallocates the default framebuffer once per frame - flicker, then garbage.
// Defer to the surface-capabilities comparison below.
result = VK_SUCCESS;
}
if (result == VK_ERROR_OUT_OF_DATE_KHR) {
MGLOG_D("Present, vkQueuePresentKHR got %d, recreating swapchain", result);
if (!RecreateSwapchain()) {
// Window went zero-area (minimize) with the swapchain out of date:
// stop submitting/acquiring until it has a size again.
m_presentSuspended = true;
m_swapchainResizeRequested = false;
MGLOG_D("Present, zero-area window with out-of-date swapchain; suspending presentation");
return;
}
m_swapchainResizeRequested = false;
result = VK_SUCCESS;
}
NoteDeviceLoss(result, "vkQueuePresentKHR");
if (result != VK_SUCCESS && LatchWireDeviceLoss("present-queue")) return;
VK_VERIFY(result, "Present, vkQueuePresentKHR");
// EGL swap semantics: the presented color buffer's content is undefined the
// next time this image is acquired (EGL_BUFFER_DESTROYED, the default swap
// behaviour), and EVERY ancillary depth/stencil buffer's content is
// undefined after any swap. The render-pass manager turns the undefined
// attachments' next tile loads into LOAD_OP_DONT_CARE.
//
// EGL_EXT_buffer_age changes the first half: once the client has asked for an age it
// repaints only what changed since the image it is given was last shown, so a presented
// image keeps its content (the wire passes load it; Vulkan preserves an image's content
// through a present and a layout transition out of PRESENT_SRC) and its age is counted.
m_swapchainObject.OnImagePresented(m_imageIndexAcquired);
if (!m_swapchainObject.BufferAgeAsked()) m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired, false);
m_swapchainObject.SetAllDepthStencilContentUndefined();
// The authoritative check, done here - after the frame is presented, before the next
// acquire. This is what makes a launcher-side resolution change take effect: shrinking
// the window's buffer (SurfaceHolder.setFixedSize) moves currentExtent, the swapchain
// follows, and the compositor scales the smaller image up to the view for free.
if (!m_swapchainResizeRequested && SwapchainIsOutOfDate()) {
m_swapchainResizeRequested = true;
}
// A new eglSwapInterval lands here too, between frames: this one is on screen and the
// next has recorded nothing. Only a different present mode is worth the rebuild (it
// drops every pipeline); otherwise the live swapchain already paces as asked.
if (m_config.SwapInterval != m_swapchainSwapInterval) {
if (SwapchainObject::ChooseSwapchainPresentMode(m_swapchainObject.GetSupportedPresentModes(),
m_config.SwapInterval) !=
m_swapchainObject.GetPresentMode()) {
m_swapchainResizeRequested = true;
} else {
MGLOG_D("DirectVulkan: swap interval %d -> %s", *m_config.SwapInterval,
SwapchainObject::GetPresentModeName(m_swapchainObject.GetPresentMode()));
m_swapchainSwapInterval = m_config.SwapInterval;
}
}
if (m_swapchainResizeRequested) {
MGLOG_D("Present, processing requested swapchain rebuild (resize or swap interval)");
if (!RecreateSwapchain()) {
m_presentSuspended = true;
m_swapchainResizeRequested = false;
MGLOG_D("Present, zero-area window on requested resize; suspending presentation");
return;
}
m_swapchainResizeRequested = false;
}
// 3) Advance frame slot.
m_frameContext.AdvanceToNext();
// FrameContext waits then resets the slot fence and frees retired
// command buffers. A later Present fence is not aggregate proof for
// earlier pooled flushes, so wait every registered submission in this
// slot's prefix BEFORE the reset/free happens.
const Uint64 slotSubmit = m_frameContext.GetCurrent().lastSubmitIndex;
if (slotSubmit > m_completedSubmitCounter &&
!WaitForSubmitsUpTo(slotSubmit, UINT64_MAX)) {
if (LatchWireDeviceLoss("present-slot-submit-wait")) return;
MagmaWireFatal("present-slot-submit-wait");
}
// 4) Wait/reset/acquire for next frame.
result = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
if (result == VK_SUBOPTIMAL_KHR) {
// An image WAS acquired and its signal is armed on this slot's
// imageAvailableSemaphore, so the frame proceeds normally. Whether a rebuild is
// actually needed is decided by the surface-capabilities comparison at the next
// Present - suboptimal alone must not schedule one, or a driver that reports it
// every frame would rebuild every frame.
m_swapchainResizeRequested = m_swapchainResizeRequested || SwapchainIsOutOfDate();
result = VK_SUCCESS;
} else if (result == VK_ERROR_OUT_OF_DATE_KHR) {
// Nothing acquired, nothing signaled: safe to rebuild and re-acquire.
MGLOG_D("Present, vkAcquireNextImageKHR got %d, recreating swapchain", result);
if (!RecreateSwapchain()) {
m_presentSuspended = true;
m_swapchainResizeRequested = false;
MGLOG_D("Present, zero-area window on next-frame acquire; suspending presentation");
return;
}
m_swapchainResizeRequested = false;
result =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
}
NoteDeviceLoss(result, "vkAcquireNextImageKHR");
if (result != VK_SUCCESS && LatchWireDeviceLoss("present-acquire")) return;
VK_VERIFY(result, "Present, vkAcquireNextImageKHR");
// Pull keeps its historical slot-fence inference. In a disaggregated
// build the aggregate wait above already retired the registered prefix
// before FrameContext reset the slot fence; this repeat is idempotent.
OnSubmitsCompletedUpTo(m_frameContext.GetCurrent().lastSubmitIndex);
CollectDeferredDepthMipmapCleanup(m_frameContext.GetCurrentFrameIndex());
// Its previous recording has completed. Drop cached attachment views
// before the texture manager can release their retired images.
ClearWireDrawPassCache(m_frameContext.GetCurrentFrameIndex());
m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
// Descriptor-set reuse cursors rewind exactly once per frame, here,
// after the slot's fence wait proved its previous sets GPU-idle. (The
// per-draw-path lazy rewind missed frames whose recording was opened
// by a staged buffer copy or timer-query timestamp, leaking a fresh
// descriptor set per draw for the whole frame; it would also be unsafe
// after a mid-frame FlushPendingCommands, which does not wait.)
m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
}
Int32 VulkanRenderer::CurrentDrawBufferAge() {
if (m_surface == VK_NULL_HANDLE || m_swapchainObject.GetHandle() == VK_NULL_HANDLE || m_presentSuspended)
return 0;
m_swapchainObject.NoteBufferAgeAsked();
if (m_imageIndexAcquired >= m_swapchainObject.GetImageCount()) return 0;
// The image the next frame's first draw makes the default framebuffer's (it was acquired at
// the end of the last present). Its content is a frame only if nothing undefined it since.
if (!m_swapchainObject.IsImageContentDefined(m_imageIndexAcquired)) return 0;
return m_swapchainObject.BufferAgeOf(m_imageIndexAcquired);
}
void VulkanRenderer::ShutdownSwapchain() {
MOBILEGL_ASSERT(m_renderPassManager != nullptr, "ShutdownSwapchain: render pass manager is null");
m_renderPassManager->Shutdown();
m_swapchainObject.Shutdown(m_device);
}
Bool VulkanRenderer::RecreateSwapchain() {
// No surface target is active (its surface was released): nothing to build on.
if (m_surface == VK_NULL_HANDLE) return false;
// Handle cases like minimize on Windows, where swapchain could return a 0x0 extent
const auto swapchainCapabilities =
SwapchainObject::GetSwapchainCapabilities(m_physicalDevice.handle, m_surface);
if (swapchainCapabilities.capabilities.currentExtent.width == 0 ||
swapchainCapabilities.capabilities.currentExtent.height == 0) {
return false;
}
vkDeviceWaitIdle(m_device);
OnSubmitsCompletedUpTo(m_submitCounter);
// The old swapchain will be destroyed below, including recordings that
// are being abandoned instead of submitted. Release all their views now.
DestroyWireDrawPass();
CollectWireObjects(m_submitCounter, true);
ClearAllWireDrawPassCaches();
if (m_timerQueryManager) {
// The in-progress command buffer is abandoned below (its recording
// flags are force-cleared), so timestamp writes recorded into it
// will never execute; resolve or invalidate all pending records now
// to keep later waits from hanging on never-available queries.
m_timerQueryManager->InvalidatePendingRecords();
}
DestroyDeferredDepthMipmapCleanup();
m_deferredDepthMipmapCleanup.assign(m_frameContext.GetFrameCount(), {});
const VkExtent2D previousExtent = m_swapchainObject.GetSurfaceExtent();
ShutdownSwapchain();
// The new swapchain publishes its extent (SwapchainObject::Create), which is how a client
// sizing its viewport from eglQuerySurface learns that the window moved.
CreateSwapchain();
const VkExtent2D extent = m_swapchainObject.GetSurfaceExtent();
if (extent.width != previousExtent.width || extent.height != previousExtent.height) {
MGLOG_I("DirectVulkan: swapchain rebuilt at %ux%u (was %ux%u); the new extent is published", extent.width,
extent.height, previousExtent.width, previousExtent.height);
}
// Every image of the fresh swapchain holds garbage, so the default framebuffer restarts
// at index 0: the next write into it re-points the GL-visible index at whatever the next
// acquire returns (see m_defaultFramebufferImageIndex). Without this the index could name
// an image of the swapchain just destroyed.
m_defaultFramebufferImageIndex = 0;
VK_VERIFY(m_frameContext.InitializeSwapchainSemaphores(m_device,
static_cast<Uint32>(m_swapchainObject.GetImageCount())),
"RecreateSwapchain, InitializeSwapchainSemaphores");
MOBILEGL_ASSERT(m_renderPassManager != nullptr, "RecreateSwapchain: render pass manager is null");
Bool ok = m_renderPassManager->Initialize();
MOBILEGL_ASSERT(ok, "RecreateSwapchain: render pass manager initialization failed");
if (m_pipelineFactory) {
m_pipelineFactory->DestroyAll();
}
InvalidatePipelineMemo(); // pipelines freed -> the memoized handle would dangle
g_dynamicStateShadow->graphicsPipelineValid = false;
DestroyComputePipelines();
if (m_frameContext.GetFrameCount() > 0) {
m_frameContext.GetCurrent().isCommandRecording = false;
m_frameContext.GetCurrent().hasCommandBufferRecorded = false;
// The pre-pass stream paired with the abandoned recording is
// dropped with it (its next Begin resets the buffer).
m_frameContext.GetCurrent().isPreCommandRecording = false;
m_frameContext.GetCurrent().hasPreCommandBufferRecorded = false;
}
const Bool okArena = m_bufferManager.RecreateTransientArenas(m_frameContext.GetFrameCount());
MOBILEGL_ASSERT(okArena, "RecreateSwapchain: buffer manager transient arena initialization failed");
if (m_frameContext.GetFrameCount() > 0) {
if (m_textureManager) {
m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
}
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
}
return true;
}
const PhysicalDevice& VulkanRenderer::GetPhysicalDevice() const {
return m_physicalDevice;
}
Bool VulkanRenderer::SwapchainIsOutOfDate() {
if (m_surface == VK_NULL_HANDLE || m_swapchainObject.GetHandle() == VK_NULL_HANDLE) {
return false;
}
VkSurfaceCapabilitiesKHR surfaceCaps{};
if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_physicalDevice.handle, m_surface, &surfaceCaps) !=
VK_SUCCESS) {
return false;
}
// A driver-defined currentExtent (UINT32_MAX) means the surface takes its size from the
// swapchain, so there is nothing to compare against - the app's requested size wins and
// only an explicit RequestSwapchainResize can change it.
if (surfaceCaps.currentExtent.width == UINT32_MAX || surfaceCaps.currentExtent.height == UINT32_MAX) {
return false;
}
// Compare in SURFACE space against the extent the live swapchain was created from. Using
// the swapchain's own (quarter-turn swapped) extent here would report a difference on
// every rotated frame and rebuild forever.
const VkExtent2D builtFrom = m_swapchainObject.GetSurfaceExtent();
const Bool extentChanged = surfaceCaps.currentExtent.width != builtFrom.width ||
surfaceCaps.currentExtent.height != builtFrom.height;
const Bool transformChanged = surfaceCaps.currentTransform != m_swapchainObject.GetPreTransform();
if (!extentChanged && !transformChanged) {
return false;
}
MGLOG_D("Swapchain out of date: surface %ux%u transform %u -> %ux%u transform %u",
builtFrom.width, builtFrom.height, static_cast<Uint32>(m_swapchainObject.GetPreTransform()),
surfaceCaps.currentExtent.width, surfaceCaps.currentExtent.height,
static_cast<Uint32>(surfaceCaps.currentTransform));
return true;
}
void VulkanRenderer::RequestSwapchainResize(Uint32 width, Uint32 height) {
width = std::max<Uint32>(width, 1);
height = std::max<Uint32>(height, 1);
if (m_config.SurfaceWidth == width && m_config.SurfaceHeight == height) {
return;
}
m_config.SurfaceWidth = width;
m_config.SurfaceHeight = height;
m_swapchainResizeRequested = true;
}
void VulkanRenderer::SetSwapInterval(Int interval) {
if (m_presentsToAppWindow) {
m_config.SwapInterval = interval;
}
}
void VulkanRenderer::ExchangeActiveTarget(SurfaceTarget& target) {
std::swap(m_activeTargetSerial, target.serial);
std::swap(m_window, target.window);
std::swap(m_platformDisplay, target.platformDisplay);
std::swap(m_platformLibrary, target.platformLibrary);
std::swap(m_platformCloseDisplay, target.platformCloseDisplay);
std::swap(m_fallbackImageReader, target.fallbackImageReader);
std::swap(m_config.SurfaceWidth, target.surfaceWidth);
std::swap(m_config.SurfaceHeight, target.surfaceHeight);
std::swap(m_config.SwapInterval, target.swapInterval);
std::swap(m_presentsToAppWindow, target.presentsToAppWindow);
std::swap(m_swapchainSwapInterval, target.swapchainSwapInterval);
std::swap(m_swapchainResizeRequested, target.swapchainResizeRequested);
std::swap(m_presentSuspended, target.presentSuspended);
std::swap(m_surface, target.surface);
std::swap(m_swapchainObject, target.swapchain);
m_frameContext.ExchangeSwapchainSemaphores(target.renderFinishedSemaphores);
std::swap(m_imageIndexAcquired, target.imageIndexAcquired);
std::swap(m_defaultFramebufferImageIndex, target.defaultFramebufferImageIndex);
}
void VulkanRenderer::ParkActiveTarget() {
if (m_surface == VK_NULL_HANDLE) return;
// The active surface's recorded work goes to the queue now, while its images are still
// the default framebuffer's.
FlushPendingCommands();
auto& frame = m_frameContext.GetCurrent();
// Its acquired image keeps its acquire for when the surface comes back, but the signal
// lives on THIS slot's semaphore, which the next surface acquires with. Wait it out with
// an empty submission; the image is then simply held, and its next submit waits nothing.
if (m_swapchainObject.GetHandle() != VK_NULL_HANDLE && !m_presentSuspended &&
!frame.imageAvailableSemaphoreConsumed) {
const VkFence fence = AcquirePooledSubmitFence();
if (fence == VK_NULL_HANDLE ||
!SubmitPendingCommandBuffer(frame, fence, /*pooledFence=*/true, /*waitAcquire=*/true)) {
MGLOG_E("DirectVulkan: a parked surface's pending acquire could not be waited out");
}
}
SurfaceTarget parked;
ExchangeActiveTarget(parked);
m_parkedTargets[m_activeTargetKey] = std::move(parked);
m_activeTargetKey = 0;
}
void VulkanRenderer::OnActiveTargetChanged() {
if (m_swapchainObject.GetHandle() != VK_NULL_HANDLE) {
m_swapchainObject.PublishDefaultFramebufferInfo();
if (m_programFactory) m_programFactory->SetDefaultFramebufferHeight(m_swapchainObject.GetExtent().height);
}
if (m_renderPassManager) m_renderPassManager->SetDefaultFramebufferTarget(m_activeTargetSerial);
InvalidatePipelineMemo();
ResetDynamicStateShadow();
}
Bool VulkanRenderer::ActivateSurfaceTarget(Uint64 key, NativeWindowType window,
const VulkanRendererConfig& surfaceConfig) {
if (key == m_activeTargetKey && m_surface != VK_NULL_HANDLE) return true;
ParkActiveTarget();
auto parked = m_parkedTargets.find(key);
if (parked != m_parkedTargets.end()) {
ExchangeActiveTarget(parked->second);
m_parkedTargets.erase(parked);
// Its acquire was waited when it was parked (ParkActiveTarget).
m_frameContext.GetCurrent().imageAvailableSemaphoreConsumed = true;
} else {
m_activeTargetSerial = m_nextTargetSerial++;
m_window = window;
m_presentsToAppWindow = window != NativeWindowType{};
m_config.SurfaceWidth = surfaceConfig.SurfaceWidth;
m_config.SurfaceHeight = surfaceConfig.SurfaceHeight;
m_config.SwapInterval = m_presentsToAppWindow ? surfaceConfig.SwapInterval : Nullopt;
m_swapchainSwapInterval.reset();
m_swapchainResizeRequested = false;
m_presentSuspended = false;
if (!CreateSurface()) {
// Nothing of the half-built target stays active: its image reader and library go
// with it, and the renderer has no target until the caller activates another.
SurfaceTarget failed;
ExchangeActiveTarget(failed);
DestroyParkedTarget(failed);
m_activeTargetKey = 0;
MGLOG_E("DirectVulkan: surface target for key %llu could not be built (%s)",
static_cast<unsigned long long>(key), window != NativeWindowType{} ? "window" : "pbuffer");
return false;
}
CreateSwapchain();
m_defaultFramebufferImageIndex = 0;
VK_VERIFY(m_frameContext.InitializeSwapchainSemaphores(
m_device, static_cast<Uint32>(m_swapchainObject.GetImageCount())),
"ActivateSurfaceTarget, InitializeSwapchainSemaphores");
if (m_swapchainObject.GetHandle() != VK_NULL_HANDLE) {
VkResult result = m_frameContext.AcquireNextImageInCurrentSlot(
m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
if (result == VK_SUBOPTIMAL_KHR) result = VK_SUCCESS;
VK_VERIFY(result, "ActivateSurfaceTarget, AcquireNextImageInCurrentSlot");
m_defaultFramebufferImageIndex = m_imageIndexAcquired;
} else {
m_presentSuspended = true;
}
MGLOG_I("DirectVulkan: surface target %llu built (%ux%u, %s)",
static_cast<unsigned long long>(m_activeTargetSerial), m_swapchainObject.GetExtent().width,
m_swapchainObject.GetExtent().height, m_presentsToAppWindow ? "window" : "pbuffer");
}
m_activeTargetKey = key;
OnActiveTargetChanged();
return true;
}
void VulkanRenderer::DestroyParkedTarget(SurfaceTarget& target) {
target.swapchain.Shutdown(m_device);
for (VkSemaphore semaphore : target.renderFinishedSemaphores) {
if (semaphore != VK_NULL_HANDLE) vkDestroySemaphore(m_device, semaphore, nullptr);
}
target.renderFinishedSemaphores.clear();
if (target.surface != VK_NULL_HANDLE) {
vkDestroySurfaceKHR(m_instance, target.surface, nullptr);
target.surface = VK_NULL_HANDLE;
}
#if defined(VK_USE_PLATFORM_ANDROID_KHR)
if (target.fallbackImageReader != nullptr && target.platformLibrary != nullptr) {
using AImageReaderDeleteFn = void (*)(void*);
auto* imageReaderDelete =
reinterpret_cast<AImageReaderDeleteFn>(dlsym(target.platformLibrary, "AImageReader_delete"));
if (imageReaderDelete) imageReaderDelete(target.fallbackImageReader);
target.fallbackImageReader = nullptr;
dlclose(target.platformLibrary);
target.platformLibrary = nullptr;
}
#elif defined(VK_USE_PLATFORM_XLIB_KHR)
if (target.platformDisplay != nullptr) {
using XCloseDisplayFn = int (*)(Display*);
auto* closeDisplay = reinterpret_cast<XCloseDisplayFn>(target.platformCloseDisplay);
if (closeDisplay) closeDisplay(static_cast<Display*>(target.platformDisplay));
target.platformDisplay = nullptr;
}
if (target.platformLibrary != nullptr) {
dlclose(target.platformLibrary);
target.platformLibrary = nullptr;
}
#endif
}
void VulkanRenderer::DestroySurfaceTarget(Uint64 key) {
if (key == m_activeTargetKey && m_surface != VK_NULL_HANDLE) ParkActiveTarget();
auto parked = m_parkedTargets.find(key);
if (parked == m_parkedTargets.end()) return;
// Whatever is recorded may name the target's images (a blit out of it, say): submit it,
// then let the GPU finish with everything before the views go.
FlushPendingCommands();
VK_VERIFY(vkDeviceWaitIdle(m_device));
OnSubmitsCompletedUpTo(m_submitCounter);
DestroyWireDrawPass();
CollectWireObjects(m_submitCounter, true);
ClearAllWireDrawPassCaches();
InvalidatePipelineMemo();
ResetDynamicStateShadow();
DestroyParkedTarget(parked->second);
m_parkedTargets.erase(parked);
}
Bool VulkanRenderer::FollowActiveWindowResize() {
if (m_surface == VK_NULL_HANDLE || !m_presentsToAppWindow) return false;
const auto& frame = m_frameContext.GetCurrent();
if (frame.isCommandRecording || frame.hasCommandBufferRecorded || frame.isPreCommandRecording ||
frame.hasPreCommandBufferRecorded) {
m_swapchainResizeRequested = true;
MGLOG_I("DirectVulkan: the window resized mid-frame; its swapchain is rebuilt at this frame's present");
return true;
}
// The surface already reports the swapchain's extent: nothing to rebuild.
if (m_swapchainObject.GetHandle() != VK_NULL_HANDLE && !SwapchainIsOutOfDate()) return true;
// The target's own rebuild - the path a surface's re-activation takes: its pending acquire waited
// out, a new VkSurfaceKHR and swapchain at the surface's current extent and pre-transform, a new
// target serial for the render passes, and the extent published.
const Uint64 key = m_activeTargetKey;
const NativeWindowType window = m_window;
VulkanRendererConfig config;
config.SurfaceWidth = m_config.SurfaceWidth;
config.SurfaceHeight = m_config.SurfaceHeight;
config.SwapInterval = m_config.SwapInterval;
DestroySurfaceTarget(key);
return ActivateSurfaceTarget(key, window, config);
}
VkExtent2D VulkanRenderer::SurfaceTargetExtent(Uint64 key) const {
if (key == m_activeTargetKey && m_surface != VK_NULL_HANDLE) {
return m_swapchainObject.GetHandle() != VK_NULL_HANDLE ? m_swapchainObject.GetSurfaceExtent()
: VkExtent2D{0, 0};
}
const auto parked = m_parkedTargets.find(key);
if (parked == m_parkedTargets.end() || parked->second.swapchain.GetHandle() == VK_NULL_HANDLE) {
return VkExtent2D{0, 0};
}
return parked->second.swapchain.GetSurfaceExtent();
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,815 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRendererSubmit.cpp
// 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
#include "VulkanRendererInternal.h"
namespace MobileGL::MG_Backend::DirectVulkan {
VkCommandBuffer VulkanRenderer::AcquireBufferCopyCommandBuffer() {
if (m_device == VK_NULL_HANDLE || m_frameContext.GetFrameCount() == 0) {
return VK_NULL_HANDLE;
}
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
}
// vkCmdCopyBuffer must be recorded outside a render pass; draws re-begin
// their render pass lazily, matching the existing blit/clear pattern.
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
}
return frame.commandBuffer;
}
Bool VulkanRenderer::IsFrameSerialComplete(Uint64 serial) const {
return serial <= m_bufferManager.GetCompletedSerial();
}
Bool VulkanRenderer::WaitForFrameSerial(Uint64 serial, Uint64 timeoutNs) {
(void)timeoutNs;
if (IsFrameSerialComplete(serial)) {
return true;
}
// Work recorded under the current serial has not been submitted yet
// (submission happens in Present, on this same thread), so blocking
// can never make progress; the caller reports a timeout instead.
if (serial >= m_bufferManager.GetFrameSerial()) {
return false;
}
if (m_device == VK_NULL_HANDLE || m_graphicsQueue == VK_NULL_HANDLE) {
return IsFrameSerialComplete(serial);
}
// More than one submit can carry the same frame serial (mid-frame flush,
// then Present). The completed floor is clamped below any serial still
// held by an in-flight record; the first matching fence is not proof.
// OnSubmitsCompletedUpTo erases records, so copy fields and restart the
// search after every wait instead of continuing an invalidated iterator.
for (;;) {
const auto record = std::find_if(m_inFlightSubmits.begin(), m_inFlightSubmits.end(),
[serial](const auto& candidate) {
return candidate.frameSerial >= serial && candidate.fence != VK_NULL_HANDLE;
});
if (record == m_inFlightSubmits.end()) break;
const Uint64 submitIndex = record->submitIndex;
if (!WaitForSubmitsUpTo(submitIndex, UINT64_MAX))
break; // fall through to the queue drain below
TryDrainFrameTransients();
if (IsFrameSerialComplete(serial)) return true;
}
// No usable record - fall back to draining the graphics queue. This over-waits (bounded by
// the in-flight frame count) but never deadlocks.
const VkResult result = vkQueueWaitIdle(m_graphicsQueue);
if (result != VK_SUCCESS) {
NoteDeviceLoss(result, "WaitForFrameSerial vkQueueWaitIdle");
MGLOG_E_ONCE("WaitForFrameSerial: vkQueueWaitIdle returned %d", result);
return false;
}
m_bufferManager.NotifyDeviceIdle();
OnSubmitsCompletedUpTo(m_submitCounter);
// The queue was just drained; take the free frame-boundary drain when
// nothing is recorded (present-less timer-query loops). No-op otherwise.
TryDrainFrameTransients();
return IsFrameSerialComplete(serial);
}
Uint64 VulkanRenderer::GetSyncPointSubmitIndex() const {
// Commands recorded (or still recording) since the last submission are
// carried by the NEXT submission; a fence created now must wait for it.
return m_submitCounter + (HasPendingRecordedWork() ? 1 : 0);
}
Bool VulkanRenderer::HasPendingRecordedWork() const {
if (m_frameContext.GetFrameCount() == 0) {
return false;
}
const auto& frame = m_frameContext.GetCurrent();
return frame.isCommandRecording || frame.hasCommandBufferRecorded ||
frame.isPreCommandRecording || frame.hasPreCommandBufferRecorded;
}
Bool VulkanRenderer::RewindWireDescriptorSetsIfDue() {
if (!m_uniformManager || m_frameContext.GetFrameCount() == 0) return true;
const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex();
if (!m_uniformManager->WireDescriptorSetBudgetReached(frameIndex)) return true;
// A set cannot be rewritten while a recorded or submitted command
// buffer might still read it. This wire-only boundary first submits
// the current recording, then idles the graphics queue before reuse.
if (HasPendingRecordedWork() && !FlushPendingCommands()) {
if (LatchWireDeviceLoss("descriptor-rewind-flush")) return false;
MagmaWireFatal("descriptor-rewind-flush");
}
const Uint64 through = m_submitCounter;
if (through > m_completedSubmitCounter) {
// B4 has not yet made every old retirement site aggregate-safe.
// Queue idle proves even a submission a previous single-fence path
// prematurely removed from m_inFlightSubmits has finished; only
// then may any layout cursor be rewound and its sets rewritten.
if (const VkResult idle = vkQueueWaitIdle(m_graphicsQueue); idle != VK_SUCCESS) {
NoteDeviceLoss(idle, "descriptor rewind vkQueueWaitIdle");
if (LatchWireDeviceLoss("descriptor-rewind-wait")) return false;
MagmaWireFatal("descriptor-rewind-wait");
}
OnSubmitsCompletedUpTo(through);
}
if (HasPendingRecordedWork() || m_completedSubmitCounter < through)
MagmaWireFatal("descriptor-rewind-proof");
const SizeT cachedSets = m_uniformManager->RewindWireDescriptorSets(frameIndex);
MGLOG_I("Magma descriptor rewind: frame=%u retiredSubmit=%llu budget=%u cached=%zu",
frameIndex, static_cast<unsigned long long>(through),
UniformManager::kWireDescriptorSetBudget, cachedSets);
return true;
}
Bool VulkanRenderer::IsSubmitIndexComplete(Uint64 submitIndex) {
if (submitIndex <= m_completedSubmitCounter) {
return true;
}
if (submitIndex > m_submitCounter) {
return false; // not even submitted; no point polling fences
}
// P15: a fence just seen unsignaled is not polled again for a short while. Callers ask per
// buffer write (FCL MC 1.21.5: vkGetFenceStatus was 0.020 ms/frame), and every one of them
// treats "not yet" as "still busy" - rename, defer, report unsignaled - so a late "yes" costs
// at most kUnsignaledPollQuietNs of extra caution, never correctness.
constexpr Uint64 kUnsignaledPollQuietNs = 50000;
const Uint64 now = static_cast<Uint64>(std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
if (!m_inFlightSubmits.empty() && m_unsignaledPollFront == m_inFlightSubmits.front().submitIndex &&
now - m_unsignaledPollNs < kUnsignaledPollQuietNs) {
return false;
}
RefreshCompletedSubmits();
if (submitIndex <= m_completedSubmitCounter) return true;
if (!m_inFlightSubmits.empty()) {
m_unsignaledPollFront = m_inFlightSubmits.front().submitIndex;
m_unsignaledPollNs = now;
}
return false;
}
void VulkanRenderer::RegisterSubmit(VkFence fence, Bool pooledFence) {
++m_submitCounter;
m_inFlightSubmits.push_back({m_submitCounter, m_bufferManager.GetFrameSerial(), fence, pooledFence});
}
void VulkanRenderer::RefreshCompletedSubmits() {
if (m_device == VK_NULL_HANDLE) {
return;
}
// Prefix-only scan: submissions to a single queue complete in order,
// and stopping at the first unsignaled fence stays conservative even
// if they did not.
while (!m_inFlightSubmits.empty()) {
// Copy before OnSubmitsCompletedUpTo erases the front record.
const Uint64 frontIndex = m_inFlightSubmits.front().submitIndex;
if (vkGetFenceStatus(m_device, m_inFlightSubmits.front().fence) != VK_SUCCESS) {
break;
}
OnSubmitsCompletedUpTo(frontIndex);
}
}
Bool VulkanRenderer::WaitForSubmitsUpTo(Uint64 submitIndex, Uint64 timeoutNs) {
if (submitIndex <= m_completedSubmitCounter) return true;
if (submitIndex > m_submitCounter || m_device == VK_NULL_HANDLE) return false;
Vector<VkFence> fences;
if (!CollectSubmitFencePrefix(m_inFlightSubmits, submitIndex, VkFence{}, fences))
return false;
const VkResult result = vkWaitForFences(m_device, static_cast<Uint32>(fences.size()),
fences.data(), VK_TRUE, timeoutNs);
if (result == VK_SUCCESS) {
OnSubmitsCompletedUpTo(submitIndex);
return true;
}
NoteDeviceLoss(result, "WaitForSubmitsUpTo vkWaitForFences");
if (result != VK_TIMEOUT)
MGLOG_E_ONCE("WaitForSubmitsUpTo: vkWaitForFences returned %d", result);
return false;
}
void VulkanRenderer::OnSubmitsCompletedUpTo(Uint64 submitIndex) {
m_completedSubmitCounter = std::max(m_completedSubmitCounter, submitIndex);
// ---- P7 wave 2 package B3: THE COMPLETED-FRAME-SERIAL FLOOR MUST BE PROVABLE --------
//
// "Frame-serial completion piggybacks on submission completion" holds only while a
// frame serial has ONE submission. On the wire arm it has at least two: the per-frame
// palette glTexImage2D reaches UploadPendingWireLevels ->
// FlushWirePendingCommandsForTextureUpdate -> FlushPendingCommands at the first
// palette-sampling draw and submits S1 under a POOLED fence, and the frame's draws
// (and the barriered vkCmdCopyBuffer that StagedWireRangeCopy records for every
// streamed glBufferSubData) then go out in the Present submission S2.
//
// S1 is tiny and signals almost immediately. Retiring it used to advance the floor to
// ITS frameSerial - which is also S2's - so VkBufferManager was told "frame N-1 is
// complete" while S2(N-1), the submission actually carrying frame N-1's buffer copies,
// was still executing. The next frame's first write to that buffer then failed
// WriteWireBuffer's busy predicate and took the unordered host memcpy, and S2(N-1)'s
// queued copies landed on top of it afterwards: a PREFIX TEAR of one draw's vertex
// range, no log line, no GL error, no Fatal.
//
// The floor now advances only to a serial NO remaining in-flight submission still
// carries. NotifyFrameSerialComplete is monotone and already refuses the still-
// recording serial, so this can only ever advance the floor later, never further.
// On the disaggregated build's MONOLITH arm this is behaviour-identical (one
// submission per serial). The #else branch below is the pull build's statement,
// unchanged, which is what keeps G1's .text byte-identical.
Bool retiredAny = false;
Uint64 advanceTo = 0;
while (!m_inFlightSubmits.empty() && m_inFlightSubmits.front().submitIndex <= submitIndex) {
SubmitRecord record = m_inFlightSubmits.front();
m_inFlightSubmits.erase(m_inFlightSubmits.begin());
retiredAny = true;
advanceTo = std::max(advanceTo, record.frameSerial);
if (!record.pooledFence || m_device == VK_NULL_HANDLE) {
continue; // frame-slot fences are reset/destroyed by FrameContext
}
if (vkResetFences(m_device, 1, &record.fence) == VK_SUCCESS) {
m_freeSubmitFences.push_back(record.fence);
} else {
vkDestroyFence(m_device, record.fence, nullptr);
}
}
if (retiredAny) {
// Clamp to one below the lowest serial still in flight.
for (const auto& remaining : m_inFlightSubmits) {
advanceTo = std::min(advanceTo, remaining.frameSerial > 0 ? remaining.frameSerial - 1 : Uint64{0});
}
// Lens C's probe, and the red-once's reading: an advance that names a serial some
// remaining submission still carries. Unreachable with the clamp above; the R-16
// revert is to delete the clamp loop, and then this counts.
for (const auto& remaining : m_inFlightSubmits) {
if (remaining.frameSerial <= advanceTo) {
WireDeclineTally::CountUnsoundSerialComplete();
// Logged per event, not once: this is the red-once's reading and the log is
// the only place it can be read from: the renderer this trace builds is
// never Shutdown() (the process exits), so a teardown dump reports the
// start-up context's zero instead of the replay's count.
MGLOG_W("MGWIRE-FLOOR unsound-serial-complete #%llu: advancing the completed "
"frame-serial floor to %llu, which submission %llu (serial %llu) still carries",
static_cast<unsigned long long>(WireDeclineTally::UnsoundSerialCompleteEvents()),
static_cast<unsigned long long>(advanceTo),
static_cast<unsigned long long>(remaining.submitIndex),
static_cast<unsigned long long>(remaining.frameSerial));
break;
}
}
{
static const Bool probe = [] {
const char* value = std::getenv("MOBILEGL_MAGMA_WIREBUF_PROBE");
return value && value[0] == '1';
}();
if (probe) {
Uint64 minRemaining = ~Uint64{0};
for (const auto& r : m_inFlightSubmits) minRemaining = std::min(minRemaining, r.frameSerial);
MGLOG_I("FLOOR retire upTo=%llu advanceTo=%llu remaining=%zu minRemainingSerial=%lld "
"frameSerial=%llu",
static_cast<unsigned long long>(submitIndex),
static_cast<unsigned long long>(advanceTo), m_inFlightSubmits.size(),
m_inFlightSubmits.empty() ? -1LL : static_cast<long long>(minRemaining),
static_cast<unsigned long long>(m_bufferManager.GetFrameSerial()));
}
}
m_bufferManager.NotifyFrameSerialComplete(advanceTo);
}
// Mid-frame-flushed command buffers whose submission just completed can
// be freed now; present-less flush loops have no other reclaim point.
m_frameContext.FreeRetiredCommandBuffersCompletedUpTo(m_completedSubmitCounter);
CollectWireObjects(m_completedSubmitCounter);
#if MOBILEGL_BUILD_DISAGGREGATED
RecycleSharedImageSemaphores(m_completedSubmitCounter);
#endif
}
Bool VulkanRenderer::TryDrainFrameTransients() {
if (m_wirePreparationDepth != 0) return false;
if (m_device == VK_NULL_HANDLE || m_frameContext.GetFrameCount() == 0) {
return false;
}
if (m_completedSubmitCounter != m_submitCounter) {
RefreshCompletedSubmits();
if (m_completedSubmitCounter != m_submitCounter) {
return false;
}
}
if (HasPendingRecordedWork()) {
return false;
}
// Texture uploads submit on this queue with their own fences. The
// renderer watermark alone cannot prove images/views are idle.
if (m_textureManager && !m_textureManager->WireUploadsAreIdle()) return false;
// No preparation, recording or GPU work remains. A minimized Present
// may have abandoned a recording tagged for a submission that will
// never occur, so reclaim those future-tagged objects as well.
CollectWireObjects(m_completedSubmitCounter, true);
ClearAllWireDrawPassCaches();
// Every submission is complete and nothing recorded references the
// per-frame transients. Pure-reclaim work runs on every drain: it only
// releases memory that is provably dead, never invalidates anything a
// later draw would have to rebuild. Raise the buffer manager's
// completed floor first so busy-tracking reflects the proven idleness.
m_bufferManager.NotifyDeviceIdle();
const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex();
m_frameContext.FreeAllRetiredCommandBuffers();
for (Uint32 slot = 0; slot < m_deferredDepthMipmapCleanup.size(); ++slot) {
CollectDeferredDepthMipmapCleanup(slot);
}
if (m_textureManager) {
m_textureManager->CollectAllDeferredReleases();
}
m_bufferManager.CollectAllDeferredReleases();
// Descriptor cursors rewind on every drain (the pre-drain readback path
// already did exactly this), keeping fence/readback loops' set usage bounded.
if (m_uniformManager) {
m_uniformManager->BeginFrame(frameIndex);
}
// Frame-boundary-equivalent work - transient arena rewind (which invalidates
// the conversion cache) and the cache-aging clocks - is gated to every 8th
// drain since the last Present: a presenting app's mid-frame readbacks/waits
// must neither force re-conversion/re-upload churn for the rest of the frame
// nor multiply the aging rate (which would shrink the 1024-boundary retire
// window and thrash periodically-used pipelines/programs), while present-less
// loops still rewind the arena and age their caches every 8 iterations -
// bounded by 8 iterations' transient usage.
++m_drainsSinceLastPresent;
// B3 probe: a drain that reaches here found the GPU caught up and nothing recording.
// Every 8th one is treated as a FRAME BOUNDARY - mid-frame. This probe is what REFUTED
// that as the OpenRA mechanism (one successful drain, zero boundary works across the
// replay; magma-b3.md §2.4): the cause was the floor in OnSubmitsCompletedUpTo.
{
static const Bool probe = [] {
const char* value = std::getenv("MOBILEGL_MAGMA_WIREBUF_PROBE");
return value && value[0] == '1';
}();
if (probe) {
MGLOG_I("WBUF drain#%llu%s submit=%llu completed=%llu",
static_cast<unsigned long long>(m_drainsSinceLastPresent),
(m_drainsSinceLastPresent % 8) == 0 ? " ***FRAME-BOUNDARY-WORK***" : "",
static_cast<unsigned long long>(m_submitCounter),
static_cast<unsigned long long>(m_completedSubmitCounter));
}
}
if ((m_drainsSinceLastPresent % 8) != 0) {
return true;
}
if (m_textureManager) {
m_textureManager->BeginFrame(frameIndex);
}
m_bufferManager.BeginFrame(frameIndex);
// The pipeline memo can survive across these boundaries (no per-frame reset
// on this path), so it must drop whenever the sweep destroys anything.
if (m_programFactory) {
m_programFactory->OnFrameBoundary();
}
if (m_pipelineFactory && m_pipelineFactory->OnFrameBoundary() > 0) {
InvalidatePipelineMemo();
}
if (m_samplerManager) {
m_samplerManager->OnFrameBoundary();
}
return true;
}
VkFence VulkanRenderer::AcquirePooledSubmitFence() {
if (!m_freeSubmitFences.empty()) {
VkFence fence = m_freeSubmitFences.back();
m_freeSubmitFences.pop_back();
return fence;
}
VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
VkFence fence = VK_NULL_HANDLE;
const VkResult result = vkCreateFence(m_device, &fenceInfo, nullptr, &fence);
if (result != VK_SUCCESS) {
MGLOG_E_ONCE("AcquirePooledSubmitFence: vkCreateFence returned %d", result);
return VK_NULL_HANDLE;
}
return fence;
}
void VulkanRenderer::DestroySubmitFencePool() {
// Callers guarantee device idle, so in-flight fences are inert.
for (const auto& record : m_inFlightSubmits) {
if (record.pooledFence && m_device != VK_NULL_HANDLE) {
vkDestroyFence(m_device, record.fence, nullptr);
}
}
m_inFlightSubmits.clear();
for (auto fence : m_freeSubmitFences) {
if (m_device != VK_NULL_HANDLE) {
vkDestroyFence(m_device, fence, nullptr);
}
}
m_freeSubmitFences.clear();
m_completedSubmitCounter = m_submitCounter;
}
Bool VulkanRenderer::SubmitPendingCommandBuffer(FrameContext::FrameData& frame, VkFence fence, Bool pooledFence,
Bool waitAcquire) {
RetireWireDrawPass();
// Batched texture uploads must reach the queue before the frame's
// commands: the recording being submitted may sample images whose
// texels only exist in the texture manager's open upload batch.
if (m_textureManager) {
m_textureManager->FlushPendingUploads();
}
VkPipelineStageFlags waitDstStageMask = FrameContext::kAcquireWaitStages;
VkSemaphore waitSemaphore = frame.imageAvailableSemaphore;
VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO};
// A recording that never touched the acquired image does not wait for its release: on a
// queue that runs submissions in order, that wait would hold this work, and everything
// after it, until the display lets go of the image (SubmitAheadOfAcquiredImage).
const Bool waitsAcquire =
!frame.imageAvailableSemaphoreConsumed && (frame.acquiredImageReferenced || waitAcquire);
if (waitsAcquire) {
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = &waitSemaphore;
submitInfo.pWaitDstStageMask = &waitDstStageMask;
}
// The pre-pass stream, when recorded, executes strictly before the
// frame's commands within the same submission.
VkCommandBuffer commandBuffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
Uint32 commandBufferCount = 0;
if (frame.hasPreCommandBufferRecorded) {
commandBuffers[commandBufferCount++] = frame.preCommandBuffer;
}
if (frame.hasCommandBufferRecorded) {
commandBuffers[commandBufferCount++] = frame.commandBuffer;
}
submitInfo.commandBufferCount = commandBufferCount;
submitInfo.pCommandBuffers = commandBuffers;
#if MOBILEGL_BUILD_DISAGGREGATED
// Shared-image fences staged for the work recorded since the last submission.
if (LatchIfGpuHung("flush-submit")) return false;
SharedImageSubmitSync sharedImageSync;
AttachSharedImageSync(submitInfo, sharedImageSync);
const GpuProgressMarkers::Bracket bracket = m_progressMarkers.Acquire(m_completedSubmitCounter);
const VkResult result = GpuProgressMarkers::SubmitBracketed(m_graphicsQueue, submitInfo, fence, bracket);
if (result == VK_SUCCESS) m_progressMarkers.Submitted(bracket, m_submitCounter + 1); // RegisterSubmit below
else m_progressMarkers.Abandon(bracket);
#else
const VkResult result = vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, fence);
#endif
if (result != VK_SUCCESS) {
NoteDeviceLoss(result, "SubmitPendingCommandBuffer vkQueueSubmit");
MGLOG_E_ONCE("SubmitPendingCommandBuffer: vkQueueSubmit returned %d", result);
return false;
}
if (waitsAcquire) frame.imageAvailableSemaphoreConsumed = true;
frame.hasCommandBufferRecorded = false;
frame.hasPreCommandBufferRecorded = false;
RegisterSubmit(fence, pooledFence);
frame.lastSubmitIndex = m_submitCounter;
#if MOBILEGL_BUILD_DISAGGREGATED
CommitSharedImageSync(sharedImageSync);
#endif
return true;
}
Bool VulkanRenderer::FlushPendingCommands() {
if (m_device == VK_NULL_HANDLE || m_graphicsQueue == VK_NULL_HANDLE || m_frameContext.GetFrameCount() == 0) {
return false;
}
// Non-blocking completion poll: gives flush-only workloads (no sync
// objects, no present) a point where finished submissions retire their
// pooled fences and mid-frame command buffers.
RefreshCompletedSubmits();
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording && !frame.hasCommandBufferRecorded &&
!frame.isPreCommandRecording && !frame.hasPreCommandBufferRecorded) {
// GL flush semantics still demand batched texture uploads start
// executing in finite time even when no draw was recorded.
if (m_textureManager) {
m_textureManager->FlushPendingUploads();
}
return false;
}
// Acquire the fence while recording is still open: failing here must
// not end recording, or the next draw's BeginCommandRecording would
// reset the command buffer and silently drop the frame's commands.
VkFence fence = AcquirePooledSubmitFence();
if (fence == VK_NULL_HANDLE) {
return false;
}
if (frame.isCommandRecording) {
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
}
m_frameContext.EndCommandRecording();
}
m_frameContext.EndPreCommandRecordingIfOpen();
const Bool submittingPreCommandBuffer = frame.hasPreCommandBufferRecorded;
if (!SubmitPendingCommandBuffer(frame, fence, /*pooledFence=*/true)) {
// Submit failure (device loss regime): the ended command buffer
// stays marked recorded so Present can still try to submit it.
m_freeSubmitFences.push_back(fence); // still unsignaled, reusable
return false;
}
// Command-buffer boundary: the pipeline memo must not survive it, or a
// pipeline bound only through memo hits is never re-stamped in the factory
// cache and the aging sweep could destroy it while the flushed submission
// still references it. Mirrors the drops at the readback and Present
// boundaries; costs one full pipeline lookup on the next draw.
InvalidatePipelineMemo();
// The submitted command buffer may still be executing; recording must
// restart on a fresh one. If none can be allocated, fall back to
// draining this submission so reusing the buffer stays legal.
const VkResult retireResult = m_frameContext.RetireCurrentCommandBuffer(submittingPreCommandBuffer);
if (retireResult != VK_SUCCESS) {
MGLOG_E_ONCE("FlushPendingCommands: RetireCurrentCommandBuffer returned %d; draining submission", retireResult);
if (WaitForSubmitsUpTo(m_submitCounter, UINT64_MAX)) {
// Every registered fence through this submission was waited.
} else if (vkQueueWaitIdle(m_graphicsQueue) == VK_SUCCESS) {
m_bufferManager.NotifyDeviceIdle();
OnSubmitsCompletedUpTo(m_submitCounter);
} else {
// Device is effectively lost; the command buffer may still be
// pending, but no recovery can make reuse legal.
MGLOG_E_ONCE("FlushPendingCommands: drain failed; command buffer reuse is unsafe");
}
}
return true;
}
void VulkanRenderer::SubmitAheadOfAcquiredImage() {
if (m_frameContext.GetFrameCount() == 0) return;
auto& frame = m_frameContext.GetCurrent();
if (frame.imageAvailableSemaphoreConsumed || frame.acquiredImageReferenced) return;
if (m_swapchainObject.GetHandle() == VK_NULL_HANDLE || m_presentSuspended) return;
// The frame's work so far renders off screen. Submitted together with the window pass it
// would wait behind the acquire semaphore, which signals only when the display releases
// the image - about every other frame that is the image it has just stopped scanning out,
// so the whole frame's GPU work started after the next display commit. Submitted now it
// runs while the display still holds the image. A failed flush leaves the work recorded,
// and it goes out later with the wait, as before. Inside a draw's preparation nothing is
// submitted (the draw may already hold state of the open recording); the image is only
// marked, so the submission that carries the use still waits.
if (m_wirePreparationDepth == 0 && HasPendingRecordedWork() && FlushPendingCommands())
MGLOG_I_ONCE("DirectVulkan: a frame's off-screen work goes to the queue ahead of its window pass; only "
"the submission that uses the swapchain image waits for its acquire");
frame.acquiredImageReferenced = true;
}
void VulkanRenderer::NoteDeviceLoss(VkResult result, const char* where) {
if (result != VK_ERROR_DEVICE_LOST || m_deviceLost) return;
m_deviceLost = true;
MGLOG_E("DirectVulkan: the VkDevice is LOST - %s returned VK_ERROR_DEVICE_LOST (the driver reset a GPU "
"fault or hang); nothing this renderer recorded or submits from here on will execute",
where != nullptr ? where : "a Vulkan call");
#if MOBILEGL_BUILD_DISAGGREGATED
// Latch NOW where a session latch exists, so the session stops at its next record however
// the verb in hand unwinds - including through a caller that only logs a failed submit.
if (MG_Pipe::MGPipeSessionLatchArmed()) (void)LatchWireDeviceLoss(where);
#endif
}
Bool VulkanRenderer::IsDeviceLost() {
// Only failure paths ask, so the probe's wait never lands on a healthy frame: a device
// that is merely slow answers VK_SUCCESS once its work drains, a lost one answers at once.
if (!m_deviceLost && m_device != VK_NULL_HANDLE) {
NoteDeviceLoss(vkDeviceWaitIdle(m_device), "the device-loss probe's vkDeviceWaitIdle");
}
return m_deviceLost;
}
Bool VulkanRenderer::LatchWireDeviceLoss(const char* site) {
if (!IsDeviceLost()) return false;
if (!m_deviceLossLatched) {
m_deviceLossLatched = true;
// Returns (latched) on a served session; dies, as the site's own Fatal would have,
// where no session latch is armed.
(void)MG_Pipe::MGPipeSessionLatch(MG_Pipe::MGPipeFatalFamily::DeviceLost,
"MGPipe: Fatal{BackendDeviceLost, \"Magma:%s\"} - this session's "
"VkDevice was lost to a GPU fault or hang; the session ends and its "
"client reads a lost context, every other session keeps running",
site != nullptr ? site : "?");
}
return true;
}
Bool VulkanRenderer::LatchIfGpuHung(const char* site) {
if (!m_progressMarkers.Hung()) return false;
if (!m_deviceLost) {
// NOT LOST TO THE DRIVER: a submission that a failed preemption leaves running is reset
// without a device loss, and the next one would stop the GPU again. Treated as lost from
// here on - nothing more is submitted - and the session ends as one whose device was.
m_deviceLost = true;
MGLOG_E("DirectVulkan: the GPU hang watch named this session's VkDevice (a submission ran %llu ms "
"without finishing, at %s); the device is treated as LOST and nothing more is submitted",
static_cast<unsigned long long>(m_progressMarkers.HungRunningMs()),
site != nullptr ? site : "?");
}
// Where no session latch is armed (no served session) the device only stays "lost".
if (!m_deviceLossLatched && MG_Pipe::MGPipeSessionLatchArmed()) {
m_deviceLossLatched = true;
(void)MG_Pipe::MGPipeSessionLatch(MG_Pipe::MGPipeFatalFamily::DeviceLost,
"MGPipe: Fatal{BackendDeviceLost, \"Magma:gpu-hang:%s\"} - this "
"session's GPU work ran past the hang budget and stopped the GPU for "
"every process; the session ends and its client reads a lost context, "
"every other session keeps running",
site != nullptr ? site : "?");
}
return true;
}
Bool VulkanRenderer::FlushForSyncPoint(Uint64 submitIndex) {
// A flush only helps a sync point whose commands are not submitted
// yet; for an already-submitted index it would just split the frame's
// render pass (a full tile load/store on TBDR GPUs) without advancing
// the fence.
if (submitIndex <= m_submitCounter) {
return false;
}
return FlushPendingCommands();
}
void VulkanRenderer::SplitOversizedRecording() {
const Uint32 maxDraws = MG_Config::Features.MagmaMaxDrawsPerCommandBuffer;
if (maxDraws == 0) {
return;
}
// Counted before the check, so a buffer carries at most maxDraws calls before the one
// that splits it. OnFrameCommandRecordingBegan resets the count on every new recording.
if (++m_drawsInRecording <= maxDraws || !HasPendingRecordedWork()) {
return;
}
// Nothing of the current call is recorded yet, so the split falls between two GL calls,
// exactly where a glFlush would put it: the render pass ends with STORE, the next draw
// re-begins it with LOAD, and queries and transform feedback are bracketed per draw.
if (!FlushPendingCommands()) {
return;
}
// Fresh command buffer: the same reset every other mid-frame flush site applies.
++m_oversizedRecordingSplits;
MGLOG_I_ONCE("DirectVulkan: a frame recorded more than %u draws into one command buffer; submitting it and "
"continuing on a fresh one (MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER, 0 = unbounded)",
maxDraws);
MGLOG_D("DirectVulkan: split an oversized recording (%llu so far, submit index %llu)",
static_cast<unsigned long long>(m_oversizedRecordingSplits),
static_cast<unsigned long long>(m_submitCounter));
// Splitting only bounds the driver's command memory if the submitted buffers are freed
// as fast as they are produced. A GPU that falls behind would park them all on the
// retired list, which is the same growth spread over more buffers - so hold the queue
// to frames-in-flight + 1 outstanding submissions, the steady state of a pipelined
// frame plus this split. Wait on the fence directly rather than through
// WaitForSubmitIndex: its opportunistic TryDrainFrameTransients may rewind the transient
// arena, and this runs at the start of a draw whose caller may already hold slices of it.
const SizeT maxOutstanding = static_cast<SizeT>(m_frameContext.GetFrameCount()) + 1;
while (m_inFlightSubmits.size() > maxOutstanding) {
const SubmitRecord oldest = m_inFlightSubmits.front();
if (oldest.fence == VK_NULL_HANDLE) {
break;
}
const VkResult waitResult = vkWaitForFences(m_device, 1, &oldest.fence, VK_TRUE, UINT64_MAX);
if (waitResult != VK_SUCCESS) {
NoteDeviceLoss(waitResult, "SplitOversizedRecording vkWaitForFences");
MGLOG_E_ONCE("SplitOversizedRecording: vkWaitForFences returned %d", waitResult);
break;
}
OnSubmitsCompletedUpTo(oldest.submitIndex);
}
}
Bool VulkanRenderer::WaitForSubmitIndex(Uint64 submitIndex, Uint64 timeoutNs, Bool flushIfPending) {
if (IsSubmitIndexComplete(submitIndex)) {
return true;
}
if (submitIndex > m_submitCounter) {
if (!flushIfPending) {
return false;
}
FlushPendingCommands();
if (submitIndex > m_submitCounter) {
// Nothing could be submitted (empty batch or submit failure);
// the index cannot complete yet.
return false;
}
}
if (!WaitForSubmitsUpTo(submitIndex, timeoutNs)) return false;
// A blocking wait can drain a present-less loop's frame transients.
TryDrainFrameTransients();
return true;
}
void VulkanRenderer::OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) {
m_drawsInRecording = 0;
// Dynamic state does not survive a command-buffer boundary.
ResetDynamicStateShadow();
if (m_uniformManager) {
m_uniformManager->OnCommandBufferBoundary();
}
// Pre-pass stream bookkeeping: a fresh frame recording references no
// textures yet.
if (m_textureManager) {
m_textureManager->AdvanceRecordingGeneration();
}
if (m_timerQueryManager) {
m_timerQueryManager->OnFrameCommandRecordingBegan(commandBuffer, m_frameContext.GetCurrentFrameIndex(),
m_bufferManager.GetFrameSerial());
}
}
VkProvokingVertexModeEXT VulkanRenderer::SelectProvokingVertexMode(VkPrimitiveTopology topology,
Bool capturesXfbFromGeometryStage) const {
if (!m_provokingVertexLastEnabled) {
return VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
}
// Measured, and identical on lavapipe and on the NVIDIA Vulkan driver: a geometry shader's
// emitted triangle strip is already recorded in GL's provoking-last vertex order, so asking
// for LAST rotates it a second time. The input-assembler path has the opposite problem, and
// the mode is a single pipeline bit, so the two cannot be satisfied at once: a program that
// both runs a geometry shader and captures transform feedback keeps Vulkan's own convention,
// and pays for it with a GL-wrong flat vertex in that one case. Deliberately a link-time
// program property, not IsTransformFeedbackActive() - see the memo note in the header.
if (capturesXfbFromGeometryStage) {
return VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
}
// VUID-VkGraphicsPipelineCreateInfo-topology-04884 only bites when
// transformFeedbackPreservesProvokingVertex is enabled; when it is not, a fan may take LAST.
if (m_provokingVertexXfbPreserveEnabled && topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN &&
!m_provokingVertexFanPreserved) {
return VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
}
// Only provokingVertexModePerPipeline lets modes differ inside one render pass instance;
// elsewhere every pipeline takes GL's default so the render pass stays self-consistent, and
// glProvokingVertex(GL_FIRST_VERTEX_CONVENTION) goes unhonoured. Honouring it there would
// mean ending the render pass on every glProvokingVertex change; not worth it until a target
// device actually lacks the property.
if (!m_provokingVertexModePerPipeline) {
return VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT;
}
return (MG_Pipe::gPipeInputs->IsLive() &&
MG_Pipe::gPipeInputs->GetProvokingVertexMode() == ProvokingVertexMode::FirstVertex)
? VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT
: VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT;
}
Bool VulkanRenderer::IsTimerQuerySupported() const {
return m_timerQuerySupported && m_timerQueryManager != nullptr;
}
SharedPtr<VkTimerQueryManager::TimestampRecord> VulkanRenderer::WriteTimerQueryTimestamp() {
if (!IsTimerQuerySupported() || m_device == VK_NULL_HANDLE || m_frameContext.GetFrameCount() == 0) {
return nullptr;
}
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
}
// vkCmdWriteTimestamp is valid both inside and outside a render pass,
// so any active render pass is left untouched.
return m_timerQueryManager->WriteTimestamp(frame.commandBuffer, m_frameContext.GetCurrentFrameIndex(),
m_bufferManager.GetFrameSerial());
}
Bool VulkanRenderer::IsTimerQueryResultReady(VkTimerQueryManager::TimestampRecord& record) {
if (record.harvested) {
return true;
}
if (!m_timerQueryManager) {
return false;
}
// Ask the pool first. It polls with VK_QUERY_RESULT_WITH_AVAILABILITY_BIT and is the
// authority on whether the timestamp has landed; the frame serial is not, because it only
// advances at Present and neither completion notifier will mark the CURRENT serial done - so
// a timestamp written and fence-waited inside one GL frame could never be read back in it.
if (m_timerQueryManager->TryHarvest(record)) {
return true;
}
return IsFrameSerialComplete(record.frameSerial) && m_timerQueryManager->TryHarvest(record);
}
Bool VulkanRenderer::WaitForTimerQueryResult(VkTimerQueryManager::TimestampRecord& record) {
if (IsTimerQueryResultReady(record)) {
return true;
}
// WaitForFrameSerial refuses serials that cannot complete without
// further submissions (a timestamp written this frame only executes
// once Present submits the command buffer), so this returns false
// instead of deadlocking; the record resolves after a later Present.
if (!WaitForFrameSerial(record.frameSerial, UINT64_MAX)) {
return false;
}
return IsTimerQueryResultReady(record);
}
Uint64 VulkanRenderer::GetTimerQueryElapsedNs(const VkTimerQueryManager::TimestampRecord& begin,
const VkTimerQueryManager::TimestampRecord& end) const {
return m_timerQueryManager ? m_timerQueryManager->ElapsedNs(begin, end) : 0;
}
Uint64 VulkanRenderer::GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const {
return m_timerQueryManager ? m_timerQueryManager->TimestampNs(record) : 0;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,4 +1,4 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlit.inc
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlit.cpp
// 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
@@ -6,7 +6,31 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// Included by VulkanRenderer.cpp inside the backend namespace, after WireFramebuffer.inc.
#include "VulkanRendererInternal.h"
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
enum class BlitSurfaceTransform : Uint32 {
Identity = 0,
Rotate90 = 1,
Rotate180 = 2,
Rotate270 = 3,
};
static BlitSurfaceTransform ToBlitSurfaceTransform(VkSurfaceTransformFlagBitsKHR preTransform) {
switch (preTransform) {
case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR:
return BlitSurfaceTransform::Rotate90;
case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR:
return BlitSurfaceTransform::Rotate180;
case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR:
return BlitSurfaceTransform::Rotate270;
default:
return BlitSurfaceTransform::Identity;
}
}
} // namespace
// The native blit owns no frontend object and never invokes the GL shader compiler.
#include "WireColorBlitSpirv.h"
@@ -540,3 +564,4 @@
if (destination.isDefault) m_swapchainObject.SetImageContentDefined(destination.swapchainImageIndex, true);
return true;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -15,7 +15,7 @@
//
// GL applies a blit's `filter` only where the image is STRETCHED (GL 4.6 core 18.3.1). A blit whose
// source and destination rectangles are the same size is a copy, whatever `filter` says, and so is
// a mirrored one. The wire arm's shader blit (WireColorBlit.inc, BlitWireColorImage) draws a quad
// a mirrored one. The wire arm's shader blit (WireColorBlit.cpp, BlitWireColorImage) draws a quad
// and samples `textureLod(sourceImage, texCoord, 0)` at interpolated coordinates, and it used the
// LINEAR sampler for every GL_LINEAR blit. At 1:1 every fragment centre maps to a texel centre, so
// in exact arithmetic linear filtering reads the same single texel. Adreno 830 quantises the
@@ -1,4 +1,4 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthMipmap.inc
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthMipmap.cpp
// 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
@@ -6,8 +6,11 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// Included by VulkanRenderer.cpp inside the backend namespace, after WireColorBlit.inc.
// The native depth mip owns no frontend object and never invokes the GL shader compiler.
#include "VulkanRendererInternal.h"
namespace MobileGL::MG_Backend::DirectVulkan {
// The native depth mip owns no frontend object and never invokes the GL shader compiler. It
// reuses WireColorBlit.cpp's window-Y convention and is written to be read beside it.
//
// WHY THIS FILE EXISTS AT ALL. vkCmdBlitImage is the ordinary way to halve a mip, and for a
// depth format it is often not available: VK_FORMAT_FEATURE_BLIT_DST is optional for depth,
@@ -333,3 +336,4 @@
RetireWireObjects(Move(retired));
return true;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -15,9 +15,9 @@
// P7 gate 5 (g5-msrbo, g5-msprobe): WHICH ARM RESOLVES A MULTISAMPLE DEPTH/STENCIL ASPECT FIRST.
//
// The wire arm has two (WireFramebuffer.inc, ResolveWireDepthStencil): a render pass that carries
// The wire arm has two (WireFramebuffer.cpp, ResolveWireDepthStencil): a render pass that carries
// a VK_KHR_depth_stencil_resolve attachment and no draw, and the baked shader pass of
// WireMultisampleResolve.inc, which draws. P7 wave 2-B2 took the render pass wherever the
// WireMultisampleResolve.cpp, which draws. P7 wave 2-B2 took the render pass wherever the
// extension exists. On the Redmi (Adreno 830, Vulkan 1.3.284, driver 512.800.71) that pass left
// its resolve target unwritten - KHR-GL46.direct_state_access.renderbuffers_storage_multisample
// read every resolved depth and stencil value back as 0 on the inproc arm while the monolith arm
@@ -473,7 +473,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
objects.framebuffers.push_back(out);
return true;
};
// The wire's clear (WireFramebuffer.inc, ClearWireFramebuffer): a LOAD/STORE pass over every
// The wire's clear (WireFramebuffer.cpp, ClearWireFramebuffer): a LOAD/STORE pass over every
// aspect of the format with vkCmdClearAttachments inside it. Recorded later; built now.
struct ClearPass {
VkRenderPass pass = VK_NULL_HANDLE;
@@ -18,8 +18,8 @@
// THE ARMS LIVE HERE, NOT ONLY IN THE RENDERER. A probe that rebuilt "something like" the wire's
// render pass would measure its own copy; the render-pass description, the shader pass's modules,
// pipeline and draw, and the view shapes both arms attach are defined once below and called by
// ResolveWireDepthStencil (WireFramebuffer.inc), ResolveWireDepthStencilWithShader
// (WireMultisampleResolve.inc) and the probe alike.
// ResolveWireDepthStencil (WireFramebuffer.cpp), ResolveWireDepthStencilWithShader
// (WireMultisampleResolve.cpp) and the probe alike.
//
// Two callers: the Magma renderer at device bring-up (server side, once per process, only where
// the render-pass arm exists at all - VulkanRenderer::ArmWireDepthResolveOrder) and the POST's
@@ -1,4 +1,4 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.cpp
// 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
@@ -6,6 +6,109 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "VulkanRendererInternal.h"
namespace MobileGL::MG_Backend::DirectVulkan {
// GL_EXT_fragment_shading_rate's pipeline rate and combiner ops for the draw about to be
// recorded. The packed rate is log2(width) << 2 | log2(height), which is exactly the extent
// VK_KHR_fragment_shading_rate takes, and the combiner op indices follow
// VkFragmentShadingRateCombinerOpKHR's order. Combiner op 1 combines with the attachment rate,
// which this backend does not enable, so it stays KEEP whatever the GL state holds.
static void ShadowedSetFragmentShadingRate(VkCommandBuffer commandBuffer,
PFN_vkCmdSetFragmentShadingRateKHR setFragmentShadingRate) {
const RenderStateParameters& p = MG_Pipe::gPipeInputs->GetRenderStateParameters();
auto& shadow = *g_dynamicStateShadow;
if (shadow.fragmentShadingRateValid && shadow.fragmentShadingRate == p.ShadingRate &&
shadow.fragmentShadingRateCombinerOp0 == p.ShadingRateCombinerOp0 &&
shadow.fragmentShadingRateCombinerOp1 == p.ShadingRateCombinerOp1) {
return;
}
shadow.fragmentShadingRateValid = true;
shadow.fragmentShadingRate = p.ShadingRate;
shadow.fragmentShadingRateCombinerOp0 = p.ShadingRateCombinerOp0;
shadow.fragmentShadingRateCombinerOp1 = p.ShadingRateCombinerOp1;
const Uint8 rate = p.ShadingRate == kMGInvalidShadingRateEncoding ? 0 : p.ShadingRate;
const VkExtent2D fragmentSize{1u << ((rate >> 2) & 3u), 1u << (rate & 3u)};
const VkFragmentShadingRateCombinerOpKHR combinerOps[2] = {
p.ShadingRateCombinerOp0 <= VK_FRAGMENT_SHADING_RATE_COMBINER_OP_MUL_KHR
? static_cast<VkFragmentShadingRateCombinerOpKHR>(p.ShadingRateCombinerOp0)
: VK_FRAGMENT_SHADING_RATE_COMBINER_OP_KEEP_KHR,
VK_FRAGMENT_SHADING_RATE_COMBINER_OP_KEEP_KHR};
setFragmentShadingRate(commandBuffer, &fragmentSize, combinerOps);
}
static Bool TryGetCurrentVertexAttributeUploadPayload(
const MG_State::GLState::CurrentVertexAttributeValue& currentValue,
GLenum glType,
VkFormat& outFormat,
const void*& outData,
VkDeviceSize& outSize) {
switch (glType) {
case GL_FLOAT:
outFormat = VK_FORMAT_R32_SFLOAT;
outData = currentValue.floatValue.data();
outSize = sizeof(Float);
return true;
case GL_FLOAT_VEC2:
outFormat = VK_FORMAT_R32G32_SFLOAT;
outData = currentValue.floatValue.data();
outSize = sizeof(Float) * 2;
return true;
case GL_FLOAT_VEC3:
outFormat = VK_FORMAT_R32G32B32_SFLOAT;
outData = currentValue.floatValue.data();
outSize = sizeof(Float) * 3;
return true;
case GL_FLOAT_VEC4:
outFormat = VK_FORMAT_R32G32B32A32_SFLOAT;
outData = currentValue.floatValue.data();
outSize = sizeof(Float) * 4;
return true;
case GL_INT:
outFormat = VK_FORMAT_R32_SINT;
outData = currentValue.intValue.data();
outSize = sizeof(Int32);
return true;
case GL_INT_VEC2:
outFormat = VK_FORMAT_R32G32_SINT;
outData = currentValue.intValue.data();
outSize = sizeof(Int32) * 2;
return true;
case GL_INT_VEC3:
outFormat = VK_FORMAT_R32G32B32_SINT;
outData = currentValue.intValue.data();
outSize = sizeof(Int32) * 3;
return true;
case GL_INT_VEC4:
outFormat = VK_FORMAT_R32G32B32A32_SINT;
outData = currentValue.intValue.data();
outSize = sizeof(Int32) * 4;
return true;
case GL_UNSIGNED_INT:
outFormat = VK_FORMAT_R32_UINT;
outData = currentValue.uintValue.data();
outSize = sizeof(Uint32);
return true;
case GL_UNSIGNED_INT_VEC2:
outFormat = VK_FORMAT_R32G32_UINT;
outData = currentValue.uintValue.data();
outSize = sizeof(Uint32) * 2;
return true;
case GL_UNSIGNED_INT_VEC3:
outFormat = VK_FORMAT_R32G32B32_UINT;
outData = currentValue.uintValue.data();
outSize = sizeof(Uint32) * 3;
return true;
case GL_UNSIGNED_INT_VEC4:
outFormat = VK_FORMAT_R32G32B32A32_UINT;
outData = currentValue.uintValue.data();
outSize = sizeof(Uint32) * 4;
return true;
default:
return false;
}
}
// Server-owned draw/dispatch setup. The legacy setup remains the monolith implementation.
static MagmaProgramSource WireProgramSource(MG_Pipe::MGPipeHandle handle) {
auto& state = MG_Pipe::MGPipeApplier();
@@ -322,7 +425,7 @@
// P15: the draw pass for `fbo`, continued (passContinues: the WirePassMemo holds) or
// looked up / built from a full attachment walk. Shared by SetupWireDraw and the in-pass
// clear (WireFramebuffer.inc): a clear into the attachments of the open pass is recorded
// clear (WireFramebuffer.cpp): a clear into the attachments of the open pass is recorded
// inside it instead of closing it and building a pass of its own. Leaves m_wireDrawPass and
// m_wireDrawPassKey describing the pass, `continuing` true when it is the open one.
Bool VulkanRenderer::PrepareWireDrawPass(FrameContext::FrameData& frame,
@@ -368,7 +471,7 @@
m_wirePassMemo.attachments.clear();
const auto add = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) -> Uint32 {
// An attachment resolve: its deferred clears stay pending, to become this pass's load
// op (BeginWireDrawPassFolding) - WireFramebuffer.inc's deferred-clear section.
// op (BeginWireDrawPassFolding) - WireFramebuffer.cpp's deferred-clear section.
WireImage image;
{
VkTextureManager::PendingClearHold hold(*m_textureManager);
@@ -385,8 +488,10 @@
VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_GENERAL;
// Already in its attachment layout: its barrier (which closes any open pass) waits until
// it is known whether the open pass continues with this very attachment.
if (image.layout == layout) sameLayoutAttachments.push_back(image);
else TransitionWireImage(image, layout);
if (image.layout == layout)
sameLayoutAttachments.push_back(image);
else
TransitionWireImage(image, layout);
VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
info.image = image.image;
info.viewType = image.viewType;
@@ -418,8 +523,10 @@
if (!image.isDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture)
scratch.gpuWriteMarks.push_back({image.storage, image.level, image.layer, image.layers});
if (image.isDefault) {
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) m_swapchainObject.SetImageContentDefined(image.swapchainImageIndex, true);
else m_swapchainObject.SetDepthStencilContentDefined(image.swapchainImageIndex, true);
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT)
m_swapchainObject.SetImageContentDefined(image.swapchainImageIndex, true);
else
m_swapchainObject.SetDepthStencilContentDefined(image.swapchainImageIndex, true);
}
return static_cast<Uint32>(attachments.size() - 1);
};
@@ -629,7 +736,7 @@
return false;
}
// Shared images another session writes: acquired ahead of the recording that samples them
// (WireSharedImage.inc).
// (WireSharedImage.cpp).
AcquireNotedSharedImages();
// Lazy image uploads/promotions can submit the old recording. Resolve
// ALL attachments before descriptor binding captures the current cmd - unless the open
@@ -1453,7 +1560,7 @@
MGL_WIRE_DECLINE_AT(ComputeTextureResourcesUnprepared, "a sampled/storage image could not be made ready for the dispatch");
return;
}
AcquireNotedSharedImages(); // shared images another session writes (WireSharedImage.inc)
AcquireNotedSharedImages(); // shared images another session writes (WireSharedImage.cpp)
const auto pipeline = GetOrCreateComputePipeline(programObj);
if (!pipeline) {
MGL_WIRE_DECLINE_AT(ComputePipelineUnavailable, "no compute pipeline could be created for this dispatch");
@@ -1530,7 +1637,7 @@
MGL_WIRE_DECLINE_AT(ComputeTextureResourcesUnprepared, "a sampled/storage image could not be made ready for the dispatch");
return;
}
AcquireNotedSharedImages(); // shared images another session writes (WireSharedImage.inc)
AcquireNotedSharedImages(); // shared images another session writes (WireSharedImage.cpp)
const auto pipeline = GetOrCreateComputePipeline(programObj);
if (!pipeline) {
MGL_WIRE_DECLINE_AT(ComputePipelineUnavailable, "no compute pipeline could be created for this dispatch");
@@ -1551,3 +1658,4 @@
if (MG_Util::PipeStats::Enabled())
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectNativeDispatches, 1);
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,4 +1,4 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.cpp
// 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
@@ -6,20 +6,469 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// 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.
// P7 wave 0. THE MESSAGE IS UNCHANGED; where it goes is not. This funnel is how eleven of
// the fifteen P7-marked refusals die (the other four are WireDescriptorFatal's), and until now
// every one of them raised its own abort: no SessionFault frame for the peer, no
// SessionFaultCount() for exit gate S8, and nothing tying the hand-typed word to
// FatalFamilies.def. MGPipeSessionFail hands the line to the hook MG_Remote installs at
// server-role init, whose arm for UnmigratedVerb is SessionFail(MGFatalFamily::
// UnmigratedVerb, ...) - and with no hook installed it logs and aborts exactly as this did.
[[noreturn]] static void MagmaWireFatal(const char* detail) {
MG_Pipe::MGPipeSessionFail(MG_Pipe::MGPipeFatalFamily::UnmigratedVerb,
"MGPipe: Fatal{UnmigratedVerb, \"Magma:%s\"}", detail);
#include "VulkanRendererInternal.h"
namespace MobileGL::MG_Backend::DirectVulkan {
// The wire arm's framebuffer verbs and texture readback. All inputs here belong to the
// applier; no frontend object, lifetime-id lookup or allocator is used.
static void RecordTextureCopyError(const char* func, ErrorCode code, const char* message) {
MG_Pipe::gPipeInputs->RecordError(code, MakeUnique<GenericErrorInfo>("DirectVulkan", func, message));
}
namespace {
// Generic VkFormat texel decode into the wide RGBA row layouts the shared readback
// store expects: GL_FLOAT rows for normalized/float sources, GL_INT / GL_UNSIGNED_INT
// rows for integer sources. Missing channels take GL defaults (0,0,0,1).
enum class ReadbackSourceClass : Uint8 { Unsupported, Float, SignedInt, UnsignedInt };
struct ReadbackSourceDesc {
ReadbackSourceClass sourceClass = ReadbackSourceClass::Unsupported;
Int channels = 0; // component count stored per texel
Int componentBits = 0; // per-component bits for regular formats; 0 for special packed
Bool isSnorm = false;
Bool isSrgb = false;
Bool bgraSwizzle = false;
VkFormat special = VK_FORMAT_UNDEFINED; // set for packed/special formats
};
static Bool GetReadbackSourceDesc(VkFormat format, ReadbackSourceDesc& out) {
out = ReadbackSourceDesc{};
switch (format) {
// --- regular UNORM ---
case VK_FORMAT_R8_UNORM: out = {ReadbackSourceClass::Float, 1, 8}; return true;
case VK_FORMAT_R8G8_UNORM: out = {ReadbackSourceClass::Float, 2, 8}; return true;
case VK_FORMAT_R8G8B8A8_UNORM: out = {ReadbackSourceClass::Float, 4, 8}; return true;
case VK_FORMAT_B8G8R8A8_UNORM: out = {ReadbackSourceClass::Float, 4, 8, false, false, true}; return true;
case VK_FORMAT_R16_UNORM: out = {ReadbackSourceClass::Float, 1, 16}; return true;
case VK_FORMAT_R16G16_UNORM: out = {ReadbackSourceClass::Float, 2, 16}; return true;
case VK_FORMAT_R16G16B16A16_UNORM: out = {ReadbackSourceClass::Float, 4, 16}; return true;
// --- SRGB (decode to linear like GL readback of sRGB textures) ---
// GL GetTexImage/ReadPixels of sRGB textures return the raw sRGB-encoded
// bytes (GL 3.3 has no FRAMEBUFFER_SRGB read decode) - do NOT linearize.
case VK_FORMAT_R8G8B8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8}; return true;
case VK_FORMAT_B8G8R8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8, false, false, true}; return true;
// --- SNORM ---
case VK_FORMAT_R8_SNORM: out = {ReadbackSourceClass::Float, 1, 8, true}; return true;
case VK_FORMAT_R8G8_SNORM: out = {ReadbackSourceClass::Float, 2, 8, true}; return true;
case VK_FORMAT_R8G8B8A8_SNORM: out = {ReadbackSourceClass::Float, 4, 8, true}; return true;
case VK_FORMAT_R16_SNORM: out = {ReadbackSourceClass::Float, 1, 16, true}; return true;
case VK_FORMAT_R16G16_SNORM: out = {ReadbackSourceClass::Float, 2, 16, true}; return true;
case VK_FORMAT_R16G16B16A16_SNORM: out = {ReadbackSourceClass::Float, 4, 16, true}; return true;
// --- SFLOAT ---
case VK_FORMAT_R16_SFLOAT: out = {ReadbackSourceClass::Float, 1, 16}; out.special = format; return true;
case VK_FORMAT_R16G16_SFLOAT: out = {ReadbackSourceClass::Float, 2, 16}; out.special = format; return true;
case VK_FORMAT_R16G16B16A16_SFLOAT: out = {ReadbackSourceClass::Float, 4, 16}; out.special = format; return true;
case VK_FORMAT_R32_SFLOAT: out = {ReadbackSourceClass::Float, 1, 32}; out.special = format; return true;
case VK_FORMAT_R32G32_SFLOAT: out = {ReadbackSourceClass::Float, 2, 32}; out.special = format; return true;
case VK_FORMAT_R32G32B32A32_SFLOAT: out = {ReadbackSourceClass::Float, 4, 32}; out.special = format; return true;
// --- UINT ---
case VK_FORMAT_R8_UINT: out = {ReadbackSourceClass::UnsignedInt, 1, 8}; return true;
case VK_FORMAT_R8G8_UINT: out = {ReadbackSourceClass::UnsignedInt, 2, 8}; return true;
case VK_FORMAT_R8G8B8A8_UINT: out = {ReadbackSourceClass::UnsignedInt, 4, 8}; return true;
case VK_FORMAT_R16_UINT: out = {ReadbackSourceClass::UnsignedInt, 1, 16}; return true;
case VK_FORMAT_R16G16_UINT: out = {ReadbackSourceClass::UnsignedInt, 2, 16}; return true;
case VK_FORMAT_R16G16B16A16_UINT: out = {ReadbackSourceClass::UnsignedInt, 4, 16}; return true;
case VK_FORMAT_R32_UINT: out = {ReadbackSourceClass::UnsignedInt, 1, 32}; return true;
case VK_FORMAT_R32G32_UINT: out = {ReadbackSourceClass::UnsignedInt, 2, 32}; return true;
case VK_FORMAT_R32G32B32A32_UINT: out = {ReadbackSourceClass::UnsignedInt, 4, 32}; return true;
// --- SINT ---
case VK_FORMAT_R8_SINT: out = {ReadbackSourceClass::SignedInt, 1, 8}; return true;
case VK_FORMAT_R8G8_SINT: out = {ReadbackSourceClass::SignedInt, 2, 8}; return true;
case VK_FORMAT_R8G8B8A8_SINT: out = {ReadbackSourceClass::SignedInt, 4, 8}; return true;
case VK_FORMAT_R16_SINT: out = {ReadbackSourceClass::SignedInt, 1, 16}; return true;
case VK_FORMAT_R16G16_SINT: out = {ReadbackSourceClass::SignedInt, 2, 16}; return true;
case VK_FORMAT_R16G16B16A16_SINT: out = {ReadbackSourceClass::SignedInt, 4, 16}; return true;
case VK_FORMAT_R32_SINT: out = {ReadbackSourceClass::SignedInt, 1, 32}; return true;
case VK_FORMAT_R32G32_SINT: out = {ReadbackSourceClass::SignedInt, 2, 32}; return true;
case VK_FORMAT_R32G32B32A32_SINT: out = {ReadbackSourceClass::SignedInt, 4, 32}; return true;
// --- packed / special ---
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
case VK_FORMAT_A2B10G10R10_UINT_PACK32:
case VK_FORMAT_A2R10G10B10_UNORM_PACK32:
case VK_FORMAT_A2R10G10B10_UINT_PACK32:
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
case VK_FORMAT_R5G6B5_UNORM_PACK16:
case VK_FORMAT_B5G6R5_UNORM_PACK16:
case VK_FORMAT_A1R5G5B5_UNORM_PACK16:
case VK_FORMAT_R5G5B5A1_UNORM_PACK16:
case VK_FORMAT_B5G5R5A1_UNORM_PACK16:
case VK_FORMAT_R4G4B4A4_UNORM_PACK16:
case VK_FORMAT_B4G4R4A4_UNORM_PACK16:
out.sourceClass = (format == VK_FORMAT_A2B10G10R10_UINT_PACK32 ||
format == VK_FORMAT_A2R10G10B10_UINT_PACK32) ?
ReadbackSourceClass::UnsignedInt : ReadbackSourceClass::Float;
out.special = format;
return true;
default:
return false;
}
}
static Float SrgbToLinear(Float value) {
if (value <= 0.04045f) {
return value / 12.92f;
}
return std::pow((value + 0.055f) / 1.055f, 2.4f);
}
static Float DecodeUnsignedF11(Uint32 bits) {
const Uint32 exponent = (bits >> 6) & 0x1F;
const Uint32 mantissa = bits & 0x3F;
if (exponent == 0) {
return static_cast<Float>(mantissa) / 64.0f * std::pow(2.0f, -14.0f);
}
if (exponent == 31) {
return mantissa == 0 ? std::numeric_limits<Float>::infinity()
: std::numeric_limits<Float>::quiet_NaN();
}
return (1.0f + static_cast<Float>(mantissa) / 64.0f) *
std::pow(2.0f, static_cast<Float>(static_cast<Int>(exponent)) - 15.0f);
}
static Float DecodeUnsignedF10(Uint32 bits) {
const Uint32 exponent = (bits >> 5) & 0x1F;
const Uint32 mantissa = bits & 0x1F;
if (exponent == 0) {
return static_cast<Float>(mantissa) / 32.0f * std::pow(2.0f, -14.0f);
}
if (exponent == 31) {
return mantissa == 0 ? std::numeric_limits<Float>::infinity()
: std::numeric_limits<Float>::quiet_NaN();
}
return (1.0f + static_cast<Float>(mantissa) / 32.0f) *
std::pow(2.0f, static_cast<Float>(static_cast<Int>(exponent)) - 15.0f);
}
static void DecodeReadbackTexelSpecialFloat(const Uint8* source, VkFormat format, Float* rgba) {
rgba[0] = 0.0f; rgba[1] = 0.0f; rgba[2] = 0.0f; rgba[3] = 1.0f;
switch (format) {
case VK_FORMAT_R16_SFLOAT:
case VK_FORMAT_R16G16_SFLOAT:
case VK_FORMAT_R16G16B16A16_SFLOAT: {
const Int channels = format == VK_FORMAT_R16_SFLOAT ? 1 :
(format == VK_FORMAT_R16G16_SFLOAT ? 2 : 4);
for (Int c = 0; c < channels; ++c) {
Uint16 bits = 0;
Memcpy(&bits, source + static_cast<SizeT>(c) * sizeof(bits), sizeof(bits));
rgba[c] = MG_Util::DecodeHalfBitsToFloat(bits);
}
return;
}
case VK_FORMAT_R32_SFLOAT:
case VK_FORMAT_R32G32_SFLOAT:
case VK_FORMAT_R32G32B32A32_SFLOAT: {
const Int channels = format == VK_FORMAT_R32_SFLOAT ? 1 :
(format == VK_FORMAT_R32G32_SFLOAT ? 2 : 4);
Memcpy(rgba, source, static_cast<SizeT>(channels) * sizeof(Float));
return;
}
case VK_FORMAT_A2B10G10R10_UNORM_PACK32: {
Uint32 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[0] = static_cast<Float>(word & 0x3FFu) / 1023.0f;
rgba[1] = static_cast<Float>((word >> 10) & 0x3FFu) / 1023.0f;
rgba[2] = static_cast<Float>((word >> 20) & 0x3FFu) / 1023.0f;
rgba[3] = static_cast<Float>((word >> 30) & 0x3u) / 3.0f;
return;
}
case VK_FORMAT_A2R10G10B10_UNORM_PACK32: {
Uint32 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[2] = static_cast<Float>(word & 0x3FFu) / 1023.0f;
rgba[1] = static_cast<Float>((word >> 10) & 0x3FFu) / 1023.0f;
rgba[0] = static_cast<Float>((word >> 20) & 0x3FFu) / 1023.0f;
rgba[3] = static_cast<Float>((word >> 30) & 0x3u) / 3.0f;
return;
}
case VK_FORMAT_B10G11R11_UFLOAT_PACK32: {
Uint32 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[0] = DecodeUnsignedF11(word & 0x7FFu);
rgba[1] = DecodeUnsignedF11((word >> 11) & 0x7FFu);
rgba[2] = DecodeUnsignedF10((word >> 22) & 0x3FFu);
return;
}
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32: {
Uint32 word = 0;
Memcpy(&word, source, sizeof(word));
const Int exponent = static_cast<Int>((word >> 27) & 0x1Fu) - 15 - 9;
const Float scale = std::pow(2.0f, static_cast<Float>(exponent));
rgba[0] = static_cast<Float>(word & 0x1FFu) * scale;
rgba[1] = static_cast<Float>((word >> 9) & 0x1FFu) * scale;
rgba[2] = static_cast<Float>((word >> 18) & 0x1FFu) * scale;
return;
}
case VK_FORMAT_R5G6B5_UNORM_PACK16:
case VK_FORMAT_B5G6R5_UNORM_PACK16: {
Uint16 word = 0;
Memcpy(&word, source, sizeof(word));
const Float c0 = static_cast<Float>((word >> 11) & 0x1Fu) / 31.0f;
const Float c1 = static_cast<Float>((word >> 5) & 0x3Fu) / 63.0f;
const Float c2 = static_cast<Float>(word & 0x1Fu) / 31.0f;
const Bool bgr = format == VK_FORMAT_B5G6R5_UNORM_PACK16;
rgba[0] = bgr ? c2 : c0;
rgba[1] = c1;
rgba[2] = bgr ? c0 : c2;
return;
}
case VK_FORMAT_A1R5G5B5_UNORM_PACK16: {
Uint16 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[3] = static_cast<Float>((word >> 15) & 0x1u);
rgba[0] = static_cast<Float>((word >> 10) & 0x1Fu) / 31.0f;
rgba[1] = static_cast<Float>((word >> 5) & 0x1Fu) / 31.0f;
rgba[2] = static_cast<Float>(word & 0x1Fu) / 31.0f;
return;
}
case VK_FORMAT_R5G5B5A1_UNORM_PACK16: {
Uint16 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[0] = static_cast<Float>((word >> 11) & 0x1Fu) / 31.0f;
rgba[1] = static_cast<Float>((word >> 6) & 0x1Fu) / 31.0f;
rgba[2] = static_cast<Float>((word >> 1) & 0x1Fu) / 31.0f;
rgba[3] = static_cast<Float>(word & 0x1u);
return;
}
case VK_FORMAT_B5G5R5A1_UNORM_PACK16: {
Uint16 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[2] = static_cast<Float>((word >> 11) & 0x1Fu) / 31.0f;
rgba[1] = static_cast<Float>((word >> 6) & 0x1Fu) / 31.0f;
rgba[0] = static_cast<Float>((word >> 1) & 0x1Fu) / 31.0f;
rgba[3] = static_cast<Float>(word & 0x1u);
return;
}
case VK_FORMAT_R4G4B4A4_UNORM_PACK16: {
Uint16 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[0] = static_cast<Float>((word >> 12) & 0xFu) / 15.0f;
rgba[1] = static_cast<Float>((word >> 8) & 0xFu) / 15.0f;
rgba[2] = static_cast<Float>((word >> 4) & 0xFu) / 15.0f;
rgba[3] = static_cast<Float>(word & 0xFu) / 15.0f;
return;
}
case VK_FORMAT_B4G4R4A4_UNORM_PACK16: {
Uint16 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[2] = static_cast<Float>((word >> 12) & 0xFu) / 15.0f;
rgba[1] = static_cast<Float>((word >> 8) & 0xFu) / 15.0f;
rgba[0] = static_cast<Float>((word >> 4) & 0xFu) / 15.0f;
rgba[3] = static_cast<Float>(word & 0xFu) / 15.0f;
return;
}
default:
return;
}
}
static Bool DecodeReadbackRowsToWide(const Uint8* srcPixels, VkFormat srcFormat, GLsizei width,
GLsizei height, Vector<Uint8>& outWide, GLenum& outWideType) {
ReadbackSourceDesc desc{};
if (!GetReadbackSourceDesc(srcFormat, desc)) {
return false;
}
const SizeT texelSize = VulkanRenderer::GetReadbackTexelSize(srcFormat);
if (texelSize == 0) {
return false;
}
const SizeT pixelCount = static_cast<SizeT>(width) * static_cast<SizeT>(height);
outWide.assign(pixelCount * 4 * sizeof(Uint32), 0);
if (desc.sourceClass == ReadbackSourceClass::Float) {
outWideType = GL_FLOAT;
Float* wide = reinterpret_cast<Float*>(outWide.data());
for (SizeT i = 0; i < pixelCount; ++i) {
const Uint8* source = srcPixels + i * texelSize;
Float rgba[4] = {0.0f, 0.0f, 0.0f, 1.0f};
if (desc.special != VK_FORMAT_UNDEFINED) {
DecodeReadbackTexelSpecialFloat(source, desc.special, rgba);
} else {
for (Int c = 0; c < desc.channels; ++c) {
Float value = 0.0f;
if (desc.componentBits == 8) {
if (desc.isSnorm) {
Int8 raw = 0;
Memcpy(&raw, source + c, sizeof(raw));
value = std::max(static_cast<Float>(raw) / 127.0f, -1.0f);
} else {
value = static_cast<Float>(source[c]) / 255.0f;
}
} else { // 16
if (desc.isSnorm) {
Int16 raw = 0;
Memcpy(&raw, source + static_cast<SizeT>(c) * 2, sizeof(raw));
value = std::max(static_cast<Float>(raw) / 32767.0f, -1.0f);
} else {
Uint16 raw = 0;
Memcpy(&raw, source + static_cast<SizeT>(c) * 2, sizeof(raw));
value = static_cast<Float>(raw) / 65535.0f;
}
}
if (desc.isSrgb && c < 3) {
value = SrgbToLinear(value);
}
rgba[c] = value;
}
if (desc.bgraSwizzle) {
std::swap(rgba[0], rgba[2]);
}
}
Memcpy(wide + i * 4, rgba, sizeof(rgba));
}
return true;
}
// Integer classes: decode to 4 x (U)Int32 per texel; missing alpha reads 1.
outWideType = desc.sourceClass == ReadbackSourceClass::SignedInt ? GL_INT : GL_UNSIGNED_INT;
Uint32* wide = reinterpret_cast<Uint32*>(outWide.data());
for (SizeT i = 0; i < pixelCount; ++i) {
const Uint8* source = srcPixels + i * texelSize;
Uint32 rgba[4] = {0, 0, 0, 1};
if (srcFormat == VK_FORMAT_A2B10G10R10_UINT_PACK32) {
Uint32 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[0] = word & 0x3FFu;
rgba[1] = (word >> 10) & 0x3FFu;
rgba[2] = (word >> 20) & 0x3FFu;
rgba[3] = (word >> 30) & 0x3u;
} else if (srcFormat == VK_FORMAT_A2R10G10B10_UINT_PACK32) {
Uint32 word = 0;
Memcpy(&word, source, sizeof(word));
rgba[2] = word & 0x3FFu;
rgba[1] = (word >> 10) & 0x3FFu;
rgba[0] = (word >> 20) & 0x3FFu;
rgba[3] = (word >> 30) & 0x3u;
} else {
for (Int c = 0; c < desc.channels; ++c) {
if (desc.componentBits == 8) {
if (desc.sourceClass == ReadbackSourceClass::SignedInt) {
Int8 raw = 0;
Memcpy(&raw, source + c, sizeof(raw));
rgba[c] = static_cast<Uint32>(static_cast<Int32>(raw));
} else {
rgba[c] = source[c];
}
} else if (desc.componentBits == 16) {
if (desc.sourceClass == ReadbackSourceClass::SignedInt) {
Int16 raw = 0;
Memcpy(&raw, source + static_cast<SizeT>(c) * 2, sizeof(raw));
rgba[c] = static_cast<Uint32>(static_cast<Int32>(raw));
} else {
Uint16 raw = 0;
Memcpy(&raw, source + static_cast<SizeT>(c) * 2, sizeof(raw));
rgba[c] = raw;
}
} else {
Memcpy(&rgba[c], source + static_cast<SizeT>(c) * 4, sizeof(Uint32));
}
}
}
Memcpy(wide + i * 4, rgba, sizeof(rgba));
}
return true;
}
// True floating-point color formats are exempt from GL_FIXED_ONLY read clamping.
static Bool IsFloatingPointReadbackFormat(VkFormat format) {
switch (format) {
case VK_FORMAT_R16_SFLOAT:
case VK_FORMAT_R16G16_SFLOAT:
case VK_FORMAT_R16G16B16_SFLOAT:
case VK_FORMAT_R16G16B16A16_SFLOAT:
case VK_FORMAT_R32_SFLOAT:
case VK_FORMAT_R32G32_SFLOAT:
case VK_FORMAT_R32G32B32_SFLOAT:
case VK_FORMAT_R32G32B32A32_SFLOAT:
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
return true;
default:
return false;
}
}
// The GL internal format a packed VkFormat stores, for the raw-word readback test below.
// Only the packed 32-bit layouts MobileGL keeps natively need an entry; anything else takes
// the wide decode path.
static TextureInternalFormat GetPackedReadbackInternalFormat(VkFormat format) {
switch (format) {
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
return TextureInternalFormat::RGB9E5;
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
return TextureInternalFormat::R11FG11FB10F;
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
return TextureInternalFormat::RGB10A2;
case VK_FORMAT_A2B10G10R10_UINT_PACK32:
return TextureInternalFormat::RGB10A2UI;
default:
return TextureInternalFormat::Unknown;
}
}
static Bool PackReadbackToClientOrPbo(const Uint8* srcPixels, VkFormat srcFormat, GLsizei width,
GLsizei sliceHeight, GLsizei sliceCount, GLenum format, GLenum type,
void* pixels, Bool applyPackImageParams,
Bool applyReadColorClamp = false) {
if (width <= 0 || sliceHeight <= 0 || sliceCount <= 0) {
return true;
}
ClientReadback::ReadbackChannelMapping mapping{};
if (!ClientReadback::GetReadbackChannelMapping(format, mapping) ||
ClientReadback::GetReadbackDstPixelSize(mapping, type) == 0) {
MGLOG_E_ONCE("DirectVulkan readback skipped: unsupported format=0x%x type=0x%x", format, type);
return false;
}
// A packed image read with the matching client type hands back its own words: the
// decode-to-float / re-encode round trip is lossy in the bits (it canonicalizes an
// RGB9_E5 shared exponent), which glGetTexImage must not do. Left to the wide path when
// GL_CLAMP_READ_COLOR may still have to act, i.e. for glReadPixels.
if (!applyReadColorClamp &&
MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
GetPackedReadbackInternalFormat(srcFormat), MG_Util::ConvertGLEnumToTextureInputFormat(format),
MG_Util::ConvertGLEnumToTexturePixelDataType(type))) {
return ClientReadback::StorePackedWordsToClient(srcPixels, width, sliceHeight, sliceCount, type, pixels,
applyPackImageParams);
}
Vector<Uint8> wide;
GLenum wideType = GL_FLOAT;
if (!DecodeReadbackRowsToWide(srcPixels, srcFormat, width,
sliceHeight * sliceCount, wide, wideType)) {
MGLOG_E_ONCE("DirectVulkan readback skipped: unsupported source format=%d",
static_cast<Int>(srcFormat));
return false;
}
// glReadPixels final conversion: GL_CLAMP_READ_COLOR defaults to GL_FIXED_ONLY,
// clamping fixed-point (normalized) buffers to [0,1] - visible for SNORM reads.
if (applyReadColorClamp && wideType == GL_FLOAT) {
const GLenum clampMode = MG_Pipe::gPipeInputs->GetClampReadColor();
const Bool clamp = clampMode == GL_TRUE ||
(clampMode == GL_FIXED_ONLY && !IsFloatingPointReadbackFormat(srcFormat));
if (clamp) {
Float* values = reinterpret_cast<Float*>(wide.data());
const SizeT count = wide.size() / sizeof(Float);
for (SizeT i = 0; i < count; ++i) {
values[i] = std::min(std::max(values[i], 0.0f), 1.0f);
}
}
}
const Bool sourceIsInteger = wideType == GL_INT || wideType == GL_UNSIGNED_INT;
if (sourceIsInteger != mapping.isInteger) {
MGLOG_E_ONCE("DirectVulkan readback skipped: integerness mismatch (format=0x%x source=%d)",
format, static_cast<Int>(srcFormat));
return false;
}
return ClientReadback::StoreWideRowsToClient(wide.data(), wideType, width, sliceHeight, sliceCount, mapping,
type, pixels, applyPackImageParams);
}
} // namespace
// P7 wave 2-B. THE NAME STAYS FIRST AND THE DRIVER'S ANSWER FOLLOWS IT. Two Iris cases
// died on the Redmi (Adreno 830v2) with `Fatal{UnmigratedVerb, "Magma:mipmap-shader-
// format-or-shape"}` and that line was the whole evidence: five disjunctions share the
@@ -110,21 +559,6 @@
return forced;
}
// AND THE SAME PROBLEM AGAIN FOR THE SHADER MULTISAMPLE RESOLVE (P7 wave 2-B2). lavapipe
// carries VK_KHR_create_renderpass2 AND VK_KHR_depth_stencil_resolve, so the extension arm
// is taken for every depth/stencil resolve this host can run and the shader arm - the one
// that exists for the device WITHOUT them, which is where the session used to die at
// `Magma:multisample-depth-resolve-capability` - would never execute under any lane. The
// knob drops the extension arm. It does not pretend the extension is missing anywhere else:
// nothing but the arm choice reads it. Inline, so the renderer reads the variable once.
inline Bool MagmaWireForcedShaderDepthResolve() {
static const Bool forced = [] {
const char* value = std::getenv("MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE");
return value && *value && value[0] != '0';
}();
return forced;
}
static Bool WireFormatIsPackedDepthStencil(VkFormat format) {
return format == VK_FORMAT_D16_UNORM_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT ||
format == VK_FORMAT_D32_SFLOAT_S8_UINT;
@@ -170,7 +604,7 @@
if (MG_Pipe::MGPipeHandleIsNull(out.storage)) MagmaWireFatal("framebuffer-view-record");
auto* resource = m_textureManager->SyncTextureResourceByHandle(out.storage, renderbuffer);
if (!resource) MagmaWireFatal("framebuffer-resource");
// Another session writes it: acquired (WireSharedImage.inc) before the layout is read below.
// Another session writes it: acquired (WireSharedImage.cpp) before the layout is read below.
// An image reached through a framebuffer is one this session renders into (or copies
// through), which makes it a writer of images from now on.
if (resource->sharedImageId != 0) {
@@ -586,7 +1020,7 @@
// any of the clear's paths (in-pass, deferred, standalone) records or resolves anything.
if (fbo.IsDefault) SubmitAheadOfAcquiredImage();
// P15 S0: what the draw pass can take is cleared inside it (ClearWireFramebufferInPass,
// WireDraw.inc); only the buffers it hands back take the standalone pass below.
// WireDraw.cpp); only the buffers it hands back take the standalone pass below.
// P15: in the open pass when it is this framebuffer's (the in-pass clear) or merely holds
// its images (another framebuffer's), deferred on the images when it covers them whole,
// and only then a pass of its own (the in-pass clear again, which begins one) or the
@@ -1158,7 +1592,7 @@
// first because the probe measured the render pass leaving its target unwritten here,
// so a shader decline (a stencil aspect without VK_EXT_shader_stencil_export, a format
// that is not SAMPLED_IMAGE + DEPTH_STENCIL_ATTACHMENT, a failed pipeline create -
// WireMultisampleResolve.inc) that lands on the render pass is the resolve most likely
// WireMultisampleResolve.cpp) that lands on the render pass is the resolve most likely
// to read back as zeroes with no GL error. A recorded debt (docs/archive/notes/p7/g5-msrbo.md §4).
if (resolvedByShader) {
MGLOG_I_ONCE("DirectVulkan::ResolveWireDepthStencil: resolved by the shader pass "
@@ -1659,7 +2093,7 @@
// ResolveWireDepthStencil's: a multisample depth or stencil SOURCE now
// reaches a single-sample destination on every device, through
// VK_KHR_depth_stencil_resolve where it exists and through the baked pass in
// WireMultisampleResolve.inc where it does not. This is the half with none -
// WireMultisampleResolve.cpp where it does not. This is the half with none -
// the sample counts differ and the DESTINATION is the multisampled side, so
// it is either one sample fanned out to N or N reduced to M.
//
@@ -1841,7 +2275,7 @@
const Bool planar = resource->viewType != VK_IMAGE_VIEW_TYPE_1D &&
resource->viewType != VK_IMAGE_VIEW_TYPE_1D_ARRAY &&
resource->viewType != VK_IMAGE_VIEW_TYPE_3D;
// The depth arm's shape: the layer window, and what WireDepthMipmap.inc's pass refuses
// The depth arm's shape: the layer window, and what WireDepthMipmap.cpp's pass refuses
// for an image this function hands it (the view type is passed to it below) - a 1D,
// 1D-array or 3D view type, and a format that does not both sample and attach as depth.
// Its other refusals, multisample and a default image, are never built here. P8-SV: the
@@ -1891,7 +2325,7 @@
// texture reached `mipmap-shader-format-or-shape` on its very first disjunction
// while the monolith arm quietly generated the chain with a shader.
//
// The per-level pass is WireDepthMipmap.inc's, which is the monolith's fragment
// The per-level pass is WireDepthMipmap.cpp's, which is the monolith's fragment
// shader with the monolith's arithmetic on a baked pipeline that owns no frontend
// object - §3.2's "observably the monolith's" without the GL program the monolith
// needs to get there. Its layer window and format were checked before the growth.
@@ -2096,3 +2530,258 @@
}
}
}
// Server-owned texture readback. Image layout tracking and frame retirement are the same as
// every other wire verb's.
static Bool PackWireColorTight(const Uint8* source, SizeT sourceBytes, VkFormat format,
TextureInternalFormat logicalFormat, const MG_Pipe::MGPReadbackInfo& info,
Vector<Uint8>& result) {
using namespace ClientReadback;
ReadbackChannelMapping mapping{};
if (!GetReadbackChannelMapping(info.Format, mapping)) return false;
const SizeT sourcePixelSize = VulkanRenderer::GetReadbackTexelSize(format);
const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, info.Type);
const Uint64 rows64 = Uint64(info.Box.H) * info.Box.D;
if (!info.Box.W || !rows64 || rows64 > std::numeric_limits<GLsizei>::max() ||
rows64 > std::numeric_limits<SizeT>::max() / 16 / info.Box.W) return false;
const SizeT pixels = static_cast<SizeT>(info.Box.W) * static_cast<SizeT>(rows64);
if (!sourcePixelSize || !pixelBytes || sourceBytes != pixels * sourcePixelSize ||
info.DstSize != pixels * pixelBytes) return false;
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(GetPackedReadbackInternalFormat(format),
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type))) {
if (pixelBytes != sourcePixelSize) return false;
result.assign(source, source + sourceBytes);
return true;
}
Vector<Uint8> wide;
GLenum wideType = GL_NONE;
const GLsizei rows = static_cast<GLsizei>(Uint64(info.Box.H) * info.Box.D);
if (!DecodeReadbackRowsToWide(source, format, info.Box.W, rows, wide, wideType) ||
mapping.isInteger != (wideType == GL_INT || wideType == GL_UNSIGNED_INT)) return false;
if (ResolveTextureFormatInfo(logicalFormat).expandRgbToRgba) {
const Float floatOne = 1.0f;
const Uint32 integerOne = 1;
const void* alpha = wideType == GL_FLOAT ? static_cast<const void*>(&floatOne) : &integerOne;
for (SizeT i = 0; i < pixels; ++i)
Memcpy(wide.data() + (i * 4 + 3) * sizeof(Uint32), alpha, sizeof(Uint32));
}
result.resize(info.DstSize);
for (GLsizei row = 0; row < rows; ++row)
ConvertWideReadbackRow(wide.data() + row * static_cast<SizeT>(info.Box.W) * 16,
result.data() + row * static_cast<SizeT>(info.Box.W) * pixelBytes,
info.Box.W, wideType, mapping, info.Type);
return true;
}
Bool VulkanRenderer::ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info,
Vector<Uint8>& ownedBytes) {
ownedBytes.clear();
const auto& box = info.Box;
if (box.X || box.Y || box.Z || !box.W || !box.H || !box.D ||
box.W > static_cast<Uint32>(std::numeric_limits<GLsizei>::max()) ||
Uint64(box.H) * box.D > static_cast<Uint64>(std::numeric_limits<GLsizei>::max())) return false;
const auto& state = MG_Pipe::MGPipeApplier();
if (MG_Pipe::MGPipeHandleIsNull(info.Res) || info.Res.Slot >= state.TextureResources.size()) return false;
const auto& record = state.TextureResources[info.Res.Slot];
if (!record.Live || record.Gen != info.Res.Gen) return false;
using Target = MG_Pipe::MGPipeResourceTarget;
const auto target = static_cast<Target>(record.Desc.Target);
const Bool oneDArray = target == Target::Tex1DArray;
const Bool volume = target == Target::Tex3D;
Uint32 level = info.Level, layer = 0, layerCount = 1, availableLayers = 1;
VkExtent3D extent{box.W, box.H, 1};
if (oneDArray) {
if (info.Target != GL_TEXTURE_1D_ARRAY || box.D != 1) return false;
layerCount = box.H; extent.height = 1;
} else if (volume) {
if (info.Target != GL_TEXTURE_3D) return false;
extent.depth = box.D;
} else if (target == Target::Tex2DArray || target == Target::TexCubeArray) {
if (info.Target != (target == Target::Tex2DArray ? GL_TEXTURE_2D_ARRAY : GL_TEXTURE_CUBE_MAP_ARRAY)) return false;
layerCount = box.D;
} else if (target == Target::TexCube) {
if (info.Target == GL_TEXTURE_CUBE_MAP) {
if (box.D != 6) return false;
layerCount = 6;
} else if (info.Target >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && info.Target <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z) {
if (box.D != 1) return false;
layer = info.Target - GL_TEXTURE_CUBE_MAP_POSITIVE_X;
} else return false;
} else {
if (box.D != 1) return false;
const GLenum expected = target == Target::Tex1D ? GL_TEXTURE_1D :
target == Target::Tex2D ? GL_TEXTURE_2D :
target == Target::TexRect ? GL_TEXTURE_RECTANGLE : GL_NONE;
if (!expected || info.Target != expected || (target == Target::Tex1D && box.H != 1)) return false;
}
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
const auto storage = m_textureManager->ResolveWireTextureStorage(info.Res, level, layer, &viewFormat, &availableLayers);
if (MG_Pipe::MGPipeHandleIsNull(storage)) return false;
const Uint64 pixels64 = Uint64(box.W) * box.H * box.D;
if (pixels64 > std::numeric_limits<SizeT>::max() / 16) return false;
const auto readUnbacked = [&]() {
// Only a level the VkImage cannot represent may use its owned,
// non-GPU-dirty staging bytes. A failed GPU read never falls back.
if (storage != info.Res || info.Target == GL_TEXTURE_CUBE_MAP) return false;
Vector<Uint8> raw;
VkFormat nativeFormat = VK_FORMAT_UNDEFINED;
if (!m_textureManager->ReadUnbackedWireLevel(storage,
MG_Util::ConvertGLEnumToTextureUploadTarget(info.Target), info.Level,
IntVec3{static_cast<Int>(box.W), static_cast<Int>(box.H), static_cast<Int>(box.D)},
nativeFormat, raw)) return false;
// A depth-only level's shadow holds the format's own depth words, which the
// depth packer reads exactly as it reads them off a Vulkan copy.
if (nativeFormat == VK_FORMAT_D16_UNORM || nativeFormat == VK_FORMAT_D32_SFLOAT ||
nativeFormat == VK_FORMAT_X8_D24_UNORM_PACK32) {
if (info.Format != GL_DEPTH_COMPONENT) return false;
const SizeT texels = static_cast<SizeT>(Uint64(box.W) * box.H * box.D);
const SizeT wordBytes = nativeFormat == VK_FORMAT_D16_UNORM ? 2 : 4;
if (raw.size() < texels * wordBytes) return false;
Vector<Uint8> packed;
if (!PackDepthStencilTight(raw.data(), nullptr, nativeFormat, texels, info.Format, info.Type, packed) ||
packed.size() != info.DstSize) return false;
ownedBytes = Move(packed);
return true;
}
return PackWireColorTight(raw.data(), raw.size(), nativeFormat,
static_cast<TextureInternalFormat>(record.Desc.InternalFormat), info, ownedBytes);
};
RetireWireDrawPass();
MakeXfbWritesVisible();
auto* resource = m_textureManager->SyncTextureResourceByHandle(storage);
if (!resource || !resource->image || level >= resource->mipLevels) return readUnbacked();
if (resource->sampleCount != VK_SAMPLE_COUNT_1_BIT) return false;
m_textureManager->FlushPendingUploads();
const Bool nativeVolume = resource->viewType == VK_IMAGE_VIEW_TYPE_3D;
const Uint32 nativeDepth = nativeVolume ? std::max(resource->depth >> level, 1u) : resource->arrayLayers;
if (extent.width != std::max(resource->extent.width >> level, 1u) ||
extent.height != std::max(resource->extent.height >> level, 1u) ||
layer >= nativeDepth || (nativeVolume ? extent.depth : layerCount) > nativeDepth - layer ||
(!nativeVolume && layerCount > availableLayers)) return readUnbacked();
// A view can reinterpret the same native words. Decode using the outer
// view format, while Vulkan copies the storage allocation's bytes.
const VkFormat format = viewFormat == VK_FORMAT_UNDEFINED ? resource->format : viewFormat;
const Bool depth = info.Format == GL_DEPTH_COMPONENT || info.Format == GL_DEPTH_STENCIL;
const Bool stencil = info.Format == GL_STENCIL_INDEX || info.Format == GL_DEPTH_STENCIL;
const Bool depthStencil = depth || stencil;
const VkImageAspectFlags aspects = depthStencil ?
((depth ? VK_IMAGE_ASPECT_DEPTH_BIT : 0) | (stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0)) : VK_IMAGE_ASPECT_COLOR_BIT;
if ((resource->aspect & aspects) != aspects) return false;
const SizeT pixels = static_cast<SizeT>(pixels64);
const SizeT destinationPixelSize = MG_Util::GetInputBytesPerPixel(
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format), MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type));
if (!destinationPixelSize || pixels64 * destinationPixelSize != info.DstSize) return false;
SizeT sourcePixelSize = GetReadbackTexelSize(resource->format);
if (depthStencil) {
switch (resource->format) {
case VK_FORMAT_D16_UNORM: sourcePixelSize = 2; break;
case VK_FORMAT_D24_UNORM_S8_UINT: case VK_FORMAT_X8_D24_UNORM_PACK32:
case VK_FORMAT_D32_SFLOAT: case VK_FORMAT_D32_SFLOAT_S8_UINT: sourcePixelSize = 4; break;
case VK_FORMAT_S8_UINT: sourcePixelSize = 0; break;
default: return false;
}
} else if (!sourcePixelSize || sourcePixelSize != GetReadbackTexelSize(format)) return false;
const VkDeviceSize firstBytes = pixels * (depthStencil ? (depth ? sourcePixelSize : 0) : sourcePixelSize);
const VkDeviceSize stencilOffset = (firstBytes + 3) & ~VkDeviceSize{3};
const VkDeviceSize copyBytes = depthStencil ? stencilOffset + (stencil ? pixels : 0) : firstBytes;
if (!copyBytes) return false;
VkBufferObject readback;
if (!readback.Create({.allocator = m_allocator, .size = copyBytes, .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT,
.memoryUsage = VMA_MEMORY_USAGE_AUTO, .allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT})) return false;
WireImage image{};
image.image = resource->image; image.format = resource->format; image.aspect = resource->aspect;
image.layout = resource->layout; image.trackedLayout = &resource->layout; image.levels = resource->mipLevels;
const auto restore = image.layout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_IMAGE_LAYOUT_GENERAL : image.layout;
auto& frame = m_frameContext.GetCurrent();
VkBufferImageCopy regions[2]{};
Uint32 regionCount = 0;
const auto addCopy = [&](VkImageAspectFlags aspect, VkDeviceSize offset) {
auto& copy = regions[regionCount++];
copy.bufferOffset = offset;
copy.imageSubresource = {aspect, level, nativeVolume ? 0u : layer, nativeVolume ? 1u : layerCount};
copy.imageOffset = {0, 0, nativeVolume ? static_cast<Int32>(layer) : 0};
copy.imageExtent = extent;
};
if (!depthStencil) addCopy(VK_IMAGE_ASPECT_COLOR_BIT, 0);
if (depth) addCopy(VK_IMAGE_ASPECT_DEPTH_BIT, 0);
if (stencil) addCopy(VK_IMAGE_ASPECT_STENCIL_BIT, stencilOffset);
// E5B9G9R9 on a device whose transfer reads canonicalize it: the words leave through a
// shader into a twin (WireE5RawCopy.h), which is what the buffer copy then reads.
VkImage copySource = image.image;
if (!depthStencil && m_textureManager->WireE5RawReadApplies(*resource)) {
TransitionWireImage(image, VK_IMAGE_LAYOUT_GENERAL);
WireE5RawCopyRegion region;
region.source = image.image;
region.volume = nativeVolume;
region.level = level;
region.baseLayer = nativeVolume ? 0u : layer;
region.layerCount = nativeVolume ? 1u : layerCount;
region.offset = regions[0].imageOffset;
region.extent = extent;
auto twin = MakeUnique<VkTextureManager::TextureResource>();
WireE5RawCopyObjects objects;
if (m_textureManager->RecordWireE5RawTwin(frame.commandBuffer, region, VK_IMAGE_LAYOUT_GENERAL, *twin,
objects)) {
copySource = twin->image;
regions[0].imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, region.layerCount};
regions[0].imageOffset = {0, 0, 0};
WireRetiredObjects retired{};
retired.textures.push_back(Move(twin));
retired.imageViews = {objects.sourceView, objects.twinView};
retired.descriptorPools = {objects.descriptorPool};
RetireWireObjects(Move(retired));
}
}
if (copySource == image.image) TransitionWireImage(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
vkCmdCopyImageToBuffer(frame.commandBuffer, copySource, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
readback.GetHandle(), regionCount, regions);
TransitionWireImage(image, restore);
VkMemoryBarrier host{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
host.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; host.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT,
0, 1, &host, 0, nullptr, 0, nullptr);
if (!SubmitReadbackCommandsAndWait(frame)) return false;
const auto* mapped = static_cast<const Uint8*>(readback.Map());
if (!mapped || !readback.Invalidate(copyBytes)) return false;
Vector<Uint8> result;
if (depthStencil) {
if (!PackDepthStencilTight(depth ? mapped : nullptr, stencil ? mapped + stencilOffset : nullptr,
resource->format, pixels, info.Format, info.Type, result)) return false;
} else {
using namespace ClientReadback;
ReadbackChannelMapping mapping{};
if (!GetReadbackChannelMapping(info.Format, mapping) ||
GetReadbackDstPixelSize(mapping, info.Type) != destinationPixelSize) return false;
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(GetPackedReadbackInternalFormat(format),
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type))) {
if (destinationPixelSize != sourcePixelSize) return false;
result.assign(mapped, mapped + firstBytes);
} else {
Vector<Uint8> wide;
GLenum wideType = GL_NONE;
const GLsizei rows = static_cast<GLsizei>(Uint64(box.H) * box.D);
if (!DecodeReadbackRowsToWide(mapped, format, box.W, rows, wide, wideType) ||
mapping.isInteger != (wideType == GL_INT || wideType == GL_UNSIGNED_INT)) return false;
if (ResolveTextureFormatInfo(static_cast<TextureInternalFormat>(record.Desc.InternalFormat)).expandRgbToRgba) {
const Float floatOne = 1.0f;
const Uint32 integerOne = 1;
const void* alpha = wideType == GL_FLOAT ? static_cast<const void*>(&floatOne) : &integerOne;
for (SizeT i = 0; i < pixels; ++i)
Memcpy(wide.data() + (i * 4 + 3) * sizeof(Uint32), alpha, sizeof(Uint32));
}
result.resize(pixels * destinationPixelSize);
const SizeT srcRow = static_cast<SizeT>(box.W) * 4 * sizeof(Uint32);
const SizeT dstRow = static_cast<SizeT>(box.W) * destinationPixelSize;
for (GLsizei row = 0; row < rows; ++row)
ConvertWideReadbackRow(wide.data() + row * srcRow, result.data() + row * dstRow,
box.W, wideType, mapping, info.Type);
}
}
if (result.size() != info.DstSize) return false;
ownedBytes = Move(result);
return true;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,4 +1,4 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolve.inc
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolve.cpp
// 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
@@ -6,23 +6,26 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// Included by VulkanRenderer.cpp inside the backend namespace, after WireDepthMipmap.inc.
// The native multisample depth/stencil resolve owns no frontend object and never invokes the
// GL shader compiler.
//
// WHY THIS FILE EXISTS. vkCmdResolveImage is COLOUR ONLY. The one way Vulkan resolves a depth
// or stencil aspect without a shader is a render pass with a depth/stencil resolve attachment,
// which needs VK_KHR_create_renderpass2 and VK_KHR_depth_stencil_resolve - and until this
// package a device without either killed the session at
// `Magma:multisample-depth-resolve-capability` the first time an application resolved a
// multisample depth buffer, which is what glBlitFramebuffer(GL_DEPTH_BUFFER_BIT) from a
// multisampled read framebuffer IS. CONTRACT-P7 §3.2's `multisample-blit-aspect` row names both
// arms; this is the second one.
//
// SAMPLE ZERO IN BOTH ARMS. The extension arm asks for VK_RESOLVE_MODE_SAMPLE_ZERO_BIT, so the
// shader arm fetches sample 0 - not an average. Two arms that answer differently on two devices
// would make "the wire arm is observably the monolith's" untestable, and GL 4.6 core 18.3.1
// requires a SELECTED sample for stencil anyway.
#include "VulkanRendererInternal.h"
namespace MobileGL::MG_Backend::DirectVulkan {
// The native multisample depth/stencil resolve owns no frontend object and never invokes the
// GL shader compiler. It is WireDepthMipmap.cpp's pass with the box filter taken out and a
// sample index put in.
//
// WHY THIS FILE EXISTS. vkCmdResolveImage is COLOUR ONLY. The one way Vulkan resolves a depth
// or stencil aspect without a shader is a render pass with a depth/stencil resolve attachment,
// which needs VK_KHR_create_renderpass2 and VK_KHR_depth_stencil_resolve - and until this
// package a device without either killed the session at
// `Magma:multisample-depth-resolve-capability` the first time an application resolved a
// multisample depth buffer, which is what glBlitFramebuffer(GL_DEPTH_BUFFER_BIT) from a
// multisampled read framebuffer IS. CONTRACT-P7 §3.2's `multisample-blit-aspect` row names both
// arms; this is the second one.
//
// SAMPLE ZERO IN BOTH ARMS. The extension arm asks for VK_RESOLVE_MODE_SAMPLE_ZERO_BIT, so the
// shader arm fetches sample 0 - not an average. Two arms that answer differently on two devices
// would make "the wire arm is observably the monolith's" untestable, and GL 4.6 core 18.3.1
// requires a SELECTED sample for stencil anyway.
// The pass's modules, layouts, render passes and pipeline, and the draw it records, are
// WireDepthResolveProbe.cpp's (g5-msprobe): the multisample depth/stencil resolve probe runs
// this exact arm as its CONTROL, so it is defined once for both.
@@ -171,3 +174,4 @@
RetireWireObjects(Move(retired));
return true;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,4 +1,4 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.cpp
// 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
@@ -6,29 +6,32 @@
// 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.
#include "VulkanRendererInternal.h"
namespace MobileGL::MG_Backend::DirectVulkan {
// SHARED IMAGES (docs/archive/notes/anland/plan-ahb-dmabuf.md). The present side's copy is
// WireFramebuffer.cpp's readback, into an image: 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.
@@ -499,7 +502,7 @@
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).
// A YUV image is converted into the texture's own image instead (WireYuvImage.cpp).
if (resource.yuvImageId != 0) {
(void)ConvertYuvSharedImage(resource);
return;
@@ -716,3 +719,4 @@
m_freeSharedImageSignalSemaphores.push_back(std::exchange(m_sharedImageSignal, VK_NULL_HANDLE));
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,261 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireTextureReadback.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
// Server-owned texture readback. Included after WireFramebuffer.inc so image
// layout tracking and frame retirement are the same as every other wire verb.
static Bool PackWireColorTight(const Uint8* source, SizeT sourceBytes, VkFormat format,
TextureInternalFormat logicalFormat, const MG_Pipe::MGPReadbackInfo& info,
Vector<Uint8>& result) {
using namespace ClientReadback;
ReadbackChannelMapping mapping{};
if (!GetReadbackChannelMapping(info.Format, mapping)) return false;
const SizeT sourcePixelSize = VulkanRenderer::GetReadbackTexelSize(format);
const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, info.Type);
const Uint64 rows64 = Uint64(info.Box.H) * info.Box.D;
if (!info.Box.W || !rows64 || rows64 > std::numeric_limits<GLsizei>::max() ||
rows64 > std::numeric_limits<SizeT>::max() / 16 / info.Box.W) return false;
const SizeT pixels = static_cast<SizeT>(info.Box.W) * static_cast<SizeT>(rows64);
if (!sourcePixelSize || !pixelBytes || sourceBytes != pixels * sourcePixelSize ||
info.DstSize != pixels * pixelBytes) return false;
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(GetPackedReadbackInternalFormat(format),
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type))) {
if (pixelBytes != sourcePixelSize) return false;
result.assign(source, source + sourceBytes);
return true;
}
Vector<Uint8> wide;
GLenum wideType = GL_NONE;
const GLsizei rows = static_cast<GLsizei>(Uint64(info.Box.H) * info.Box.D);
if (!DecodeReadbackRowsToWide(source, format, info.Box.W, rows, wide, wideType) ||
mapping.isInteger != (wideType == GL_INT || wideType == GL_UNSIGNED_INT)) return false;
if (ResolveTextureFormatInfo(logicalFormat).expandRgbToRgba) {
const Float floatOne = 1.0f;
const Uint32 integerOne = 1;
const void* alpha = wideType == GL_FLOAT ? static_cast<const void*>(&floatOne) : &integerOne;
for (SizeT i = 0; i < pixels; ++i)
Memcpy(wide.data() + (i * 4 + 3) * sizeof(Uint32), alpha, sizeof(Uint32));
}
result.resize(info.DstSize);
for (GLsizei row = 0; row < rows; ++row)
ConvertWideReadbackRow(wide.data() + row * static_cast<SizeT>(info.Box.W) * 16,
result.data() + row * static_cast<SizeT>(info.Box.W) * pixelBytes,
info.Box.W, wideType, mapping, info.Type);
return true;
}
Bool VulkanRenderer::ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info,
Vector<Uint8>& ownedBytes) {
ownedBytes.clear();
const auto& box = info.Box;
if (box.X || box.Y || box.Z || !box.W || !box.H || !box.D ||
box.W > static_cast<Uint32>(std::numeric_limits<GLsizei>::max()) ||
Uint64(box.H) * box.D > static_cast<Uint64>(std::numeric_limits<GLsizei>::max())) return false;
const auto& state = MG_Pipe::MGPipeApplier();
if (MG_Pipe::MGPipeHandleIsNull(info.Res) || info.Res.Slot >= state.TextureResources.size()) return false;
const auto& record = state.TextureResources[info.Res.Slot];
if (!record.Live || record.Gen != info.Res.Gen) return false;
using Target = MG_Pipe::MGPipeResourceTarget;
const auto target = static_cast<Target>(record.Desc.Target);
const Bool oneDArray = target == Target::Tex1DArray;
const Bool volume = target == Target::Tex3D;
Uint32 level = info.Level, layer = 0, layerCount = 1, availableLayers = 1;
VkExtent3D extent{box.W, box.H, 1};
if (oneDArray) {
if (info.Target != GL_TEXTURE_1D_ARRAY || box.D != 1) return false;
layerCount = box.H; extent.height = 1;
} else if (volume) {
if (info.Target != GL_TEXTURE_3D) return false;
extent.depth = box.D;
} else if (target == Target::Tex2DArray || target == Target::TexCubeArray) {
if (info.Target != (target == Target::Tex2DArray ? GL_TEXTURE_2D_ARRAY : GL_TEXTURE_CUBE_MAP_ARRAY)) return false;
layerCount = box.D;
} else if (target == Target::TexCube) {
if (info.Target == GL_TEXTURE_CUBE_MAP) {
if (box.D != 6) return false;
layerCount = 6;
} else if (info.Target >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && info.Target <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z) {
if (box.D != 1) return false;
layer = info.Target - GL_TEXTURE_CUBE_MAP_POSITIVE_X;
} else return false;
} else {
if (box.D != 1) return false;
const GLenum expected = target == Target::Tex1D ? GL_TEXTURE_1D :
target == Target::Tex2D ? GL_TEXTURE_2D :
target == Target::TexRect ? GL_TEXTURE_RECTANGLE : GL_NONE;
if (!expected || info.Target != expected || (target == Target::Tex1D && box.H != 1)) return false;
}
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
const auto storage = m_textureManager->ResolveWireTextureStorage(info.Res, level, layer, &viewFormat, &availableLayers);
if (MG_Pipe::MGPipeHandleIsNull(storage)) return false;
const Uint64 pixels64 = Uint64(box.W) * box.H * box.D;
if (pixels64 > std::numeric_limits<SizeT>::max() / 16) return false;
const auto readUnbacked = [&]() {
// Only a level the VkImage cannot represent may use its owned,
// non-GPU-dirty staging bytes. A failed GPU read never falls back.
if (storage != info.Res || info.Target == GL_TEXTURE_CUBE_MAP) return false;
Vector<Uint8> raw;
VkFormat nativeFormat = VK_FORMAT_UNDEFINED;
if (!m_textureManager->ReadUnbackedWireLevel(storage,
MG_Util::ConvertGLEnumToTextureUploadTarget(info.Target), info.Level,
IntVec3{static_cast<Int>(box.W), static_cast<Int>(box.H), static_cast<Int>(box.D)},
nativeFormat, raw)) return false;
// A depth-only level's shadow holds the format's own depth words, which the
// depth packer reads exactly as it reads them off a Vulkan copy.
if (nativeFormat == VK_FORMAT_D16_UNORM || nativeFormat == VK_FORMAT_D32_SFLOAT ||
nativeFormat == VK_FORMAT_X8_D24_UNORM_PACK32) {
if (info.Format != GL_DEPTH_COMPONENT) return false;
const SizeT texels = static_cast<SizeT>(Uint64(box.W) * box.H * box.D);
const SizeT wordBytes = nativeFormat == VK_FORMAT_D16_UNORM ? 2 : 4;
if (raw.size() < texels * wordBytes) return false;
Vector<Uint8> packed;
if (!PackDepthStencilTight(raw.data(), nullptr, nativeFormat, texels, info.Format, info.Type, packed) ||
packed.size() != info.DstSize) return false;
ownedBytes = Move(packed);
return true;
}
return PackWireColorTight(raw.data(), raw.size(), nativeFormat,
static_cast<TextureInternalFormat>(record.Desc.InternalFormat), info, ownedBytes);
};
RetireWireDrawPass();
MakeXfbWritesVisible();
auto* resource = m_textureManager->SyncTextureResourceByHandle(storage);
if (!resource || !resource->image || level >= resource->mipLevels) return readUnbacked();
if (resource->sampleCount != VK_SAMPLE_COUNT_1_BIT) return false;
m_textureManager->FlushPendingUploads();
const Bool nativeVolume = resource->viewType == VK_IMAGE_VIEW_TYPE_3D;
const Uint32 nativeDepth = nativeVolume ? std::max(resource->depth >> level, 1u) : resource->arrayLayers;
if (extent.width != std::max(resource->extent.width >> level, 1u) ||
extent.height != std::max(resource->extent.height >> level, 1u) ||
layer >= nativeDepth || (nativeVolume ? extent.depth : layerCount) > nativeDepth - layer ||
(!nativeVolume && layerCount > availableLayers)) return readUnbacked();
// A view can reinterpret the same native words. Decode using the outer
// view format, while Vulkan copies the storage allocation's bytes.
const VkFormat format = viewFormat == VK_FORMAT_UNDEFINED ? resource->format : viewFormat;
const Bool depth = info.Format == GL_DEPTH_COMPONENT || info.Format == GL_DEPTH_STENCIL;
const Bool stencil = info.Format == GL_STENCIL_INDEX || info.Format == GL_DEPTH_STENCIL;
const Bool depthStencil = depth || stencil;
const VkImageAspectFlags aspects = depthStencil ?
((depth ? VK_IMAGE_ASPECT_DEPTH_BIT : 0) | (stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0)) : VK_IMAGE_ASPECT_COLOR_BIT;
if ((resource->aspect & aspects) != aspects) return false;
const SizeT pixels = static_cast<SizeT>(pixels64);
const SizeT destinationPixelSize = MG_Util::GetInputBytesPerPixel(
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format), MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type));
if (!destinationPixelSize || pixels64 * destinationPixelSize != info.DstSize) return false;
SizeT sourcePixelSize = GetReadbackTexelSize(resource->format);
if (depthStencil) {
switch (resource->format) {
case VK_FORMAT_D16_UNORM: sourcePixelSize = 2; break;
case VK_FORMAT_D24_UNORM_S8_UINT: case VK_FORMAT_X8_D24_UNORM_PACK32:
case VK_FORMAT_D32_SFLOAT: case VK_FORMAT_D32_SFLOAT_S8_UINT: sourcePixelSize = 4; break;
case VK_FORMAT_S8_UINT: sourcePixelSize = 0; break;
default: return false;
}
} else if (!sourcePixelSize || sourcePixelSize != GetReadbackTexelSize(format)) return false;
const VkDeviceSize firstBytes = pixels * (depthStencil ? (depth ? sourcePixelSize : 0) : sourcePixelSize);
const VkDeviceSize stencilOffset = (firstBytes + 3) & ~VkDeviceSize{3};
const VkDeviceSize copyBytes = depthStencil ? stencilOffset + (stencil ? pixels : 0) : firstBytes;
if (!copyBytes) return false;
VkBufferObject readback;
if (!readback.Create({.allocator = m_allocator, .size = copyBytes, .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT,
.memoryUsage = VMA_MEMORY_USAGE_AUTO, .allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT})) return false;
WireImage image{};
image.image = resource->image; image.format = resource->format; image.aspect = resource->aspect;
image.layout = resource->layout; image.trackedLayout = &resource->layout; image.levels = resource->mipLevels;
const auto restore = image.layout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_IMAGE_LAYOUT_GENERAL : image.layout;
auto& frame = m_frameContext.GetCurrent();
VkBufferImageCopy regions[2]{};
Uint32 regionCount = 0;
const auto addCopy = [&](VkImageAspectFlags aspect, VkDeviceSize offset) {
auto& copy = regions[regionCount++];
copy.bufferOffset = offset;
copy.imageSubresource = {aspect, level, nativeVolume ? 0u : layer, nativeVolume ? 1u : layerCount};
copy.imageOffset = {0, 0, nativeVolume ? static_cast<Int32>(layer) : 0};
copy.imageExtent = extent;
};
if (!depthStencil) addCopy(VK_IMAGE_ASPECT_COLOR_BIT, 0);
if (depth) addCopy(VK_IMAGE_ASPECT_DEPTH_BIT, 0);
if (stencil) addCopy(VK_IMAGE_ASPECT_STENCIL_BIT, stencilOffset);
// E5B9G9R9 on a device whose transfer reads canonicalize it: the words leave through a
// shader into a twin (WireE5RawCopy.h), which is what the buffer copy then reads.
VkImage copySource = image.image;
if (!depthStencil && m_textureManager->WireE5RawReadApplies(*resource)) {
TransitionWireImage(image, VK_IMAGE_LAYOUT_GENERAL);
WireE5RawCopyRegion region;
region.source = image.image;
region.volume = nativeVolume;
region.level = level;
region.baseLayer = nativeVolume ? 0u : layer;
region.layerCount = nativeVolume ? 1u : layerCount;
region.offset = regions[0].imageOffset;
region.extent = extent;
auto twin = MakeUnique<VkTextureManager::TextureResource>();
WireE5RawCopyObjects objects;
if (m_textureManager->RecordWireE5RawTwin(frame.commandBuffer, region, VK_IMAGE_LAYOUT_GENERAL, *twin,
objects)) {
copySource = twin->image;
regions[0].imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, region.layerCount};
regions[0].imageOffset = {0, 0, 0};
WireRetiredObjects retired{};
retired.textures.push_back(Move(twin));
retired.imageViews = {objects.sourceView, objects.twinView};
retired.descriptorPools = {objects.descriptorPool};
RetireWireObjects(Move(retired));
}
}
if (copySource == image.image) TransitionWireImage(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
vkCmdCopyImageToBuffer(frame.commandBuffer, copySource, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
readback.GetHandle(), regionCount, regions);
TransitionWireImage(image, restore);
VkMemoryBarrier host{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
host.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; host.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT,
0, 1, &host, 0, nullptr, 0, nullptr);
if (!SubmitReadbackCommandsAndWait(frame)) return false;
const auto* mapped = static_cast<const Uint8*>(readback.Map());
if (!mapped || !readback.Invalidate(copyBytes)) return false;
Vector<Uint8> result;
if (depthStencil) {
if (!PackDepthStencilTight(depth ? mapped : nullptr, stencil ? mapped + stencilOffset : nullptr,
resource->format, pixels, info.Format, info.Type, result)) return false;
} else {
using namespace ClientReadback;
ReadbackChannelMapping mapping{};
if (!GetReadbackChannelMapping(info.Format, mapping) ||
GetReadbackDstPixelSize(mapping, info.Type) != destinationPixelSize) return false;
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(GetPackedReadbackInternalFormat(format),
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type))) {
if (destinationPixelSize != sourcePixelSize) return false;
result.assign(mapped, mapped + firstBytes);
} else {
Vector<Uint8> wide;
GLenum wideType = GL_NONE;
const GLsizei rows = static_cast<GLsizei>(Uint64(box.H) * box.D);
if (!DecodeReadbackRowsToWide(mapped, format, box.W, rows, wide, wideType) ||
mapping.isInteger != (wideType == GL_INT || wideType == GL_UNSIGNED_INT)) return false;
if (ResolveTextureFormatInfo(static_cast<TextureInternalFormat>(record.Desc.InternalFormat)).expandRgbToRgba) {
const Float floatOne = 1.0f;
const Uint32 integerOne = 1;
const void* alpha = wideType == GL_FLOAT ? static_cast<const void*>(&floatOne) : &integerOne;
for (SizeT i = 0; i < pixels; ++i)
Memcpy(wide.data() + (i * 4 + 3) * sizeof(Uint32), alpha, sizeof(Uint32));
}
result.resize(pixels * destinationPixelSize);
const SizeT srcRow = static_cast<SizeT>(box.W) * 4 * sizeof(Uint32);
const SizeT dstRow = static_cast<SizeT>(box.W) * destinationPixelSize;
for (GLsizei row = 0; row < rows; ++row)
ConvertWideReadbackRow(wide.data() + row * srcRow, result.data() + row * dstRow,
box.W, wideType, mapping, info.Type);
}
}
if (result.size() != info.DstSize) return false;
ownedBytes = Move(result);
return true;
}
@@ -1,4 +1,4 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireYuvImage.inc
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireYuvImage.cpp
// 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
@@ -6,20 +6,24 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// Included by VulkanRenderer.cpp inside the backend namespace, after WireSharedImage.inc.
//
// YUV SHARED IMAGES (NV12, P010; MG_Remote/Server/SharedImageYuv.h). A YUV image is never a
// texture's image: the texture keeps its own RGBA8 level 0 (VkTextureManager::SyncWireTextureShape)
// and, at its first use in each frame, the image is CONVERTED into it here - the image's
// AHardwareBuffer imported with its external format, sampled through an immutable sampler that
// carries a VkSamplerYcbcrConversion built from the import's colour hints, one triangle over the
// target. Zero CPU copies for an image this server allocated; a foreign one (a video decoder's
// buffer) is first brought up to date by the registry's labelled CPU copy (RefreshForeign).
//
// Why converted rather than sampled in place: a Y'CbCr conversion is an immutable sampler, which
// lives in the descriptor set layout and therefore in every application pipeline that samples the
// texture. A conversion pass keeps those pipelines exactly what they are for an RGBA texture, at
// the cost of one small pass per new frame of the video.
#include "VulkanRendererInternal.h"
namespace MobileGL::MG_Backend::DirectVulkan {
// A YUV image is WireSharedImage.cpp's reading side, converted on the way.
//
// YUV SHARED IMAGES (NV12, P010; MG_Remote/Server/SharedImageYuv.h). A YUV image is never a
// texture's image: the texture keeps its own RGBA8 level 0 (VkTextureManager::SyncWireTextureShape)
// and, at its first use in each frame, the image is CONVERTED into it here - the image's
// AHardwareBuffer imported with its external format, sampled through an immutable sampler that
// carries a VkSamplerYcbcrConversion built from the import's colour hints, one triangle over the
// target. Zero CPU copies for an image this server allocated; a foreign one (a video decoder's
// buffer) is first brought up to date by the registry's labelled CPU copy (RefreshForeign).
//
// Why converted rather than sampled in place: a Y'CbCr conversion is an immutable sampler, which
// lives in the descriptor set layout and therefore in every application pipeline that samples the
// texture. A conversion pass keeps those pipelines exactly what they are for an RGBA texture, at
// the cost of one small pass per new frame of the video.
#if MOBILEGL_BUILD_DISAGGREGATED
#include "WireYuvConvertSpirv.h"
@@ -521,3 +525,4 @@
#endif
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
} // namespace MobileGL::MG_Backend::DirectVulkan