diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 726cbf8d..6c26f1f6 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -1412,10 +1413,21 @@ namespace MobileGL::MG_Backend::DirectGLES { static Uint64 CurrentUnitBindingsEpoch(Int maxTouchedUnit) { const Uint64 contextId = MG_State::pGLContext->GetTextureContextId(); const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration(); + if (MG_Util::PipeStats::Enabled()) { + // Two accessor calls whichever way the shutter goes; only the unit WALK is + // gated, and that walk reads no GLContext accessor of its own. + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 2); + } if (g_observedUnitBindingsContextId == contextId && g_observedUnitBindingsMaxUnit == maxTouchedUnit && g_observedUnitBindingsGeneration == bindGeneration) { + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytUnitBindingsEpoch, /*hit=*/true); + } return g_unitBindingsEpoch; } + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytUnitBindingsEpoch, /*hit=*/false); + } if (g_observedUnitBindingsContextId != contextId || g_observedUnitBindingsMaxUnit != maxTouchedUnit || !UnitBindingsUnchanged(maxTouchedUnit, g_observedUnitBindings)) { CaptureUnitBindings(maxTouchedUnit, g_observedUnitBindings); @@ -1505,6 +1517,10 @@ namespace MobileGL::MG_Backend::DirectGLES { keys.maxTouchedUnit = MG_State::pGLContext->GetMaxTouchedTextureUnit(); keys.samplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration(); keys.unitBindingsEpoch = CurrentUnitBindingsEpoch(keys.maxTouchedUnit); + if (MG_Util::PipeStats::Enabled()) { + // The three reads above; CurrentUnitBindingsEpoch counts its own two. + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3); + } return keys; } @@ -1534,6 +1550,11 @@ namespace MobileGL::MG_Backend::DirectGLES { g_unitTextureSyncListEpoch == unitBindingsEpoch && g_unitTextureSyncListSamplingGeneration == samplingGeneration && PairingsIntact(g_unitTextureSyncList)) { + if (MG_Util::PipeStats::Enabled()) { + // Gate 2 of section 2.3.1. The served path walks the memoised entries + // and reads no GLContext accessor at all. + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytTextureSyncList, /*hit=*/true); + } for (const auto& entry : g_unitTextureSyncList) { // Aggregate gate == the conjunction of the three callees' own // early-outs (see IsDrawSyncClean); skipping on true is @@ -1546,6 +1567,13 @@ namespace MobileGL::MG_Backend::DirectGLES { entry.backend->SyncMipmapsToBackend(*entry.slot); } } else { + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytTextureSyncList, /*hit=*/false); + // One GetTextureUnitObject per touched unit in the rebuild walk below. + MG_Util::PipeStats::AddCalls( + MG_Util::PipeStats::CallClass::AccessorCalls, + maxTouchedUnit >= 0 ? static_cast(maxTouchedUnit) + 1u : 0u); + } g_unitTextureSyncListValid = false; g_unitTextureSyncList.clear(); for (Int index = 0; index <= maxTouchedUnit; ++index) { @@ -2006,8 +2034,22 @@ namespace MobileGL::MG_Backend::DirectGLES { const Bool colorMaskWidenDirty = appliedWidenMask != g_syncedColorMaskAlphaWidenMask; if (!forceFullPush && !colorMaskWidenDirty && g_hasSyncedRenderState && currentRenderStateVersion == g_syncedRenderStateVersion) { + // Gate 1 of section 2.3.1: the steady-state cost of this whole function is + // the one Uint16 read above plus this compare. + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytRenderState, /*hit=*/true); + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 1); + } return; } + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytRenderState, /*hit=*/false); + // The version read above, the parameter-block fetch and the viewport fetch + // below - the three accessor calls this function makes unconditionally on a + // miss. The conditional sRGB capability read further down is deliberately + // NOT counted (see the inventory in PipeStats.cpp). + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3); + } const auto& parameters = MG_State::pGLContext->GetRenderStateParameters(); @@ -2924,6 +2966,14 @@ namespace MobileGL::MG_Backend::DirectGLES { // cross-TU call with a guarded static inside - repeating it per stage showed // up in draw-loop profiles. const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw(); + if (MG_Util::PipeStats::Enabled()) { + // THE per-draw denominator for Espryt, plus this function's own two accessor + // calls (the VAO and the draw program). Everything the callees below read is + // counted by the callees that are instrumented; the rest is not counted (see + // the inventory in PipeStats.cpp). + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::Draws, 1); + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 2); + } const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys(); BufferImpl::SyncNeccessaryBuffers(currentVAO, vaoTwin, vaoConfigVersion, @@ -3370,6 +3420,10 @@ namespace MobileGL::MG_Backend::DirectGLES { if (BufferImpl::UboRingAllocate(bindSize, offset)) { std::memcpy(static_cast(BufferImpl::UboRingMappedPtr()) + offset, currentProgram->MapUBO(), uboSize); + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboGlobal, + static_cast(uboSize)); + } ringSlot = {uboContentVersion, BufferImpl::UboRingGeneration(), frameSerial, offset}; slotValid = true; @@ -3389,6 +3443,11 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, backendProgram.GetBackendGlobalUBOId()); g_GLESFuncs.glBufferSubData(GL_UNIFORM_BUFFER, 0, currentProgram->GetUBOSize(), currentProgram->MapUBO()); + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::AddBytes( + MG_Util::PipeStats::ByteClass::StageUboGlobal, + static_cast(currentProgram->GetUBOSize())); + } g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0); backendProgram.SetLastUploadedGlobalUboVersion(uboContentVersion); } @@ -4306,6 +4365,12 @@ namespace MobileGL::MG_Backend::DirectGLES { g_restartIndices.capacity = capacity; if (data != nullptr && bytes != 0) { g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast(bytes), data); + if (MG_Util::PipeStats::Enabled()) { + // Rewritten index list staged on the draw path: in a split build these + // bytes are the index-mirror-versus-ship decision of section 8. + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndexClient, + static_cast(bytes)); + } } return true; } @@ -10638,6 +10703,12 @@ namespace MobileGL::MG_Backend::DirectGLES { BufferImpl::UnpackRingOnPresent(); BufferImpl::UploadRingOnPresent(); BufferImpl::TrimBufferPool(); + + // THE frame boundary for the MGPipe counters: publish this frame's plots, fold the + // frame into the run totals and, every 120th frame, emit the summary line. + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::OnPresent(); + } } void DestroyEGLContext() { diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index f18f0f50..30b60aad 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -779,6 +780,12 @@ namespace MobileGL::MG_Backend::DirectGLES { const void* initialData = (size > 0 && bufferObject.HasDefinedContent()) ? bufferObject.MappedData() : nullptr; g_GLESFuncs.glBufferData(TempBufferTarget, (GLsizeiptr)size, initialData, usage); + if (MG_Util::PipeStats::Enabled() && initialData != nullptr) { + // An ORPHANING respecify passes NULL and moves nothing, which is exactly + // why the test is on initialData rather than on size. + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, + static_cast(size)); + } resource.storageSize = size; resource.storageInitialized = true; resource.pendingRespecify = false; @@ -886,6 +893,13 @@ namespace MobileGL::MG_Backend::DirectGLES { const SizeT start = std::min(range.start, end); const SizeT size = end - start; if (size == 0) continue; + if (MG_Util::PipeStats::Enabled()) { + // Counted once per queued range, before the three delivery shapes + // below diverge: all three move exactly these bytes, and it is the + // byte count - not the shape - that sizes SEG_STAGE. + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, + static_cast(size)); + } // The invalidating map's fast path is SHAPE-dependent on this Mali // driver: a whole-buffer invalidation renames the store outright, // and a large range gets fresh pages - but a small unaligned range @@ -953,6 +967,10 @@ namespace MobileGL::MG_Backend::DirectGLES { if (write.offset >= limit) continue; const SizeT size = std::min(write.bytes.size(), limit - write.offset); if (size == 0) continue; + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, + static_cast(size)); + } SizeT ringOffset = 0; if (ringUsable && size <= kUploadRingMaxBytes && RingAllocate(g_uploadRing, size, ringOffset)) { @@ -2545,6 +2563,10 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast(converted.size() * sizeof(Float)), converted.data(), GL_STREAM_DRAW); + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient, + static_cast(converted.size() * sizeof(Float))); + } // GL ignores `normalized` for floating-point array types, so it is not // forwarded here either. g_GLESFuncs.glVertexAttribPointer(attribIndex, attrib.Size, GL_FLOAT, GL_FALSE, @@ -2575,6 +2597,10 @@ namespace MobileGL::MG_Backend::DirectGLES { BufferImpl::BindBufferId(GL_ARRAY_BUFFER, bufferId); g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast(uploadSize), clientData, GL_STREAM_DRAW); + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient, + static_cast(uploadSize)); + } if (!attrib.IsInteger) { const GLint glSize = attrib.IsBgra ? static_cast(GL_BGRA) : attrib.Size; @@ -4391,6 +4417,42 @@ namespace MobileGL::MG_Backend::DirectGLES { if (ringStaged) { BufferImpl::BindPixelUnpackBufferId(BufferImpl::UnpackRingBufferId()); } + if (MG_Util::PipeStats::Enabled()) { + // One emission per (upload target, level) that ships texels; + // the switch below turns it into either one union-box job or + // dirtyRectCount rect jobs. The box/rect split is counted + // separately from the bytes on purpose: SSIM is blind to it + // and the +6 ms/frame Mali cliff was a shape regression, not + // a byte regression (plan section 7.3). + const Bool rectShape = subRectEligible && dirtyRectCount >= 2; + Uint64 shippedBytes = 0; + if (rectShape) { + for (SizeT r = 0; r < dirtyRectCount; ++r) { + const auto& rect = dirtyRects[r]; + shippedBytes += static_cast(rect.hi.x() - rect.lo.x()) * + static_cast(rect.hi.y() - rect.lo.y()) * + static_cast(std::max(rect.hi.z() - rect.lo.z(), 1)) * + static_cast(bpp); + } + } else if (subRectEligible) { + shippedBytes = static_cast(regionSize.x()) * + static_cast(regionSize.y()) * + static_cast(std::max(regionSize.z(), 1)) * + static_cast(bpp); + } else { + shippedBytes = static_cast(byteSize); + } + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageTexture, + shippedBytes); + MG_Util::PipeStats::AddCalls( + MG_Util::PipeStats::CallClass::TextureUploadEmissions, 1); + MG_Util::PipeStats::AddCalls( + rectShape ? MG_Util::PipeStats::CallClass::TextureUploadRectEmissions + : MG_Util::PipeStats::CallClass::TextureUploadBoxEmissions, + 1); + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadJobs, + rectShape ? static_cast(dirtyRectCount) : 1u); + } switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) { case TextureTarget::Texture2D: case TextureTarget::TextureCubeMap: diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index f135a4aa..16cc7263 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -13,6 +13,7 @@ #include "MG_State/GLState/ErrorState/ErrorInfo.h" #include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h" #include "MG_Util/Converters/GLToMG/TextureEnumConverter.h" +#include "MG_Util/Metrics/PipeStats.h" #include "MG_Util/Metrics/TextureMetrics.h" #include "MG_Util/Miscellany/IndexGenerator.h" #include @@ -1430,5 +1431,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { void Present() { MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Present called with null VulkanRenderer"); pVulkanRenderer->Present(); + // THE frame boundary for the MGPipe counters, at the backend entry point rather + // than inside VulkanRenderer::Present: that function has an early return for the + // no-usable-swapchain case, and a suspended frame is still a frame the counters + // must close. + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::OnPresent(); + } } } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index a2465a58..a34a5c9f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -20,6 +20,7 @@ #include "MG_Util/Converters/GLToMG/TextureEnumConverter.h" #include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h" #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" +#include "MG_Util/Metrics/PipeStats.h" #include "MG_Util/Metrics/TextureMetrics.h" #include "MG_Util/ShaderTranspiler/Types.h" #include @@ -2058,6 +2059,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { } out.payload = outData; out.payloadSize = outSize; + if (MG_Util::PipeStats::Enabled()) { + // D-B8: these are the bytes Magma repacks into its own UBO ring, i.e. exactly + // the host payload a split build would have to ship with set_shader_buffers. + // Espryt binds the frontend buffer to the driver and contributes nothing here, + // which is why the class is named for the payload and not for the call. + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboNamed, + static_cast(outSize)); + } // Zero-copy direct bind: for a persistent-mapped coherent app buffer whose full reflected // block fits within the aligned bound range, point the descriptor straight at the app's diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index b600511c..80d69b49 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -27,6 +27,7 @@ #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" @@ -4999,9 +5000,19 @@ void main() { entry.pipelineStateHash == pipelineStateHash && entry.primitiveRestartEnable == primitiveRestartEnable && entry.transformFlags == transformFlags) { + if (MG_Util::PipeStats::Enabled()) { + // Gate 5 of section 2.3.1. On a hit this whole function cost the one + // GetPipelineStateVersion read above plus this value compare. + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::MagmaPipelineMemo, /*hit=*/true); + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 1); + } return entry.pipeline; } } + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::MagmaPipelineMemo, /*hit=*/false); + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 1); + } // Shape gate. Behind the memo probe deliberately: only a pipeline that was created // successfully is ever memoized, so a program refused here can never be sitting in the @@ -5157,6 +5168,17 @@ void main() { syntheticVertexInputState.pNext = vis.state.pNext; pipelineVertexInputState = &syntheticVertexInputState; } + if (MG_Util::PipeStats::Enabled()) { + // THE payload-builder walk section 2.3.1 says only runs on a pipeline memo + // miss. Counted as a constant: the unconditional accessor reads between here + // and the end of the payload build (the six capability reads, the draw-FBO + // slot, the two stencil faces, the polygon mode, sample shading + min sample + // shading, patch vertices, the depth mask and the depth func, and the second + // draw-FBO slot read). Reads that are themselves conditional - the cull-mode + // ternary, the logic-op fetch, the two tessellation default-level reads - are + // deliberately excluded, so this stays a LOWER bound like every other tally. + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 15); + } auto cullFaceEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::CullFace); auto depthTestEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest); auto polygonOffsetFillEnabled = @@ -5890,8 +5912,18 @@ void main() { if (shadow.dynamicTailValid && shadow.dynamicTailParamsVersion == paramsVersion && shadow.dynamicTailExtentX == extent.x() && shadow.dynamicTailExtentY == extent.y() && shadow.dynamicTailIsDefaultFbo == isDefaultFbo) { + // Gate 6 of section 2.3.1: one version read plus a four-integer compare. + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::MagmaDynamicTail, /*hit=*/true); + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 1); + } return; } + if (MG_Util::PipeStats::Enabled()) { + // The version read above and the bulk parameter fetch that builds the value key. + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::MagmaDynamicTail, /*hit=*/false); + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 2); + } const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform(); // Second-level VALUE gate: the version moved, but RenderState's version counts // every parameter, most of which this tail never reads. Build the key over @@ -6365,6 +6397,15 @@ void main() { MOBILEGL_ASSERT(idxUploadOk, "SetupDraw fast path: failed to upload index buffer"); } ApplyDynamicDrawStateTail(frame, snap.renderPassExtent, snap.drawFboIsDefault, snap.viewportCount); + if (MG_Util::PipeStats::Enabled()) { + // The six accessor reads this function makes unconditionally on the path that + // reaches here: the draw program, the VAO, the draw-FBO slot, the pipeline + // state version, the texture bind generation and the sampling-resolution + // generation. The XFB-active probe is elided on a device without the feature + // and the parameter-block fetch only runs when the pipeline state version + // moved, so neither is counted (lower bound, as everywhere else). + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 6); + } return true; } @@ -6390,9 +6431,27 @@ void main() { return false; } } + if (MG_Util::PipeStats::Enabled()) { + // THE per-draw denominator for Magma, plus the draw-program read above. Placed + // here rather than inside TrySetupDrawFastPath because the fast path has 27 + // decline returns and one success return: counting the gate from the caller is + // the only shape that cannot miss one. + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::Draws, 1); + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 1); + } if (TrySetupDrawFastPath(frame, mode, aspects, drawParams, pIndexBufferView)) { + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::MagmaDrawFastPath, /*hit=*/true); + } return true; } + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::MagmaDrawFastPath, /*hit=*/false); + // The three reads the full path makes immediately below (draw FBO, VAO, + // program). The accessor reads the declined fast path had already made before + // it turned back are NOT counted. + MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3); + } const auto& drawFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); if (drawFbo != nullptr && IsUnsupportedFramebufferForDirectVulkan(*drawFbo)) {