mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
@@ -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
|
||||
+27
-2
@@ -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
|
||||
|
||||
+7
-3
@@ -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
|
||||
|
||||
+118
-10
@@ -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
|
||||
+723
-34
@@ -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
|
||||
+22
-18
@@ -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
|
||||
+29
-25
@@ -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;
|
||||
}
|
||||
+20
-15
@@ -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
|
||||
Reference in New Issue
Block a user