// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.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 "VulkanRenderer.h" #include "SubmitFencePrefix.h" #include "MG_Backend/DirectVulkan/SubgroupSupportPolicy.h" #include "MG_Backend/DirectGLES/Utils.h" #include "VertexInputStateFactory.h" #include "VertexInputStateBuilder.h" #include "MG_State/GLState/Core.h" #include #if MOBILEGL_PIPE_PUSH // P5c ev: the GPU-write announcement routes through the reverse channel (R2). #include #endif #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 #if MOBILEGL_BUILD_DISAGGREGATED // P7 wave 0: the seam WireFramebuffer.inc's MagmaWireFatal and WireDraw.inc's // WireBufferLegacyFatal die through. Declared by MG_Pipe on purpose - see PipeSessionFail.h. #include // P5c (T5 / tx): the server's staged-texture shadow GenerateMipmap defines its chain on. #include #include // P5c (G6): the named-blit arm's endpoint resolution runs inside the frontend-keyed scope. #include // P7 wave 2 package B3: rule I's tally for WireDraw.inc's silent draw drops. #include "WireDeclineTally.h" #include "WireDepthResolveArm.h" #endif #include #include #include #include #include #include #ifdef __ANDROID__ #include #endif #if defined(__APPLE__) #include #include #include #include #endif namespace MobileGL::MG_Backend::DirectVulkan { #if defined(__APPLE__) namespace { constexpr unsigned long kNSWindowStyleMaskBorderless = 0; constexpr unsigned long kNSBackingStoreBuffered = 2; template Fn ObjcMsgSend() { return reinterpret_cast(objc_msgSend); } id SendId(id receiver, const char* selector) { return ObjcMsgSend()(receiver, sel_registerName(selector)); } void SendVoid(id receiver, const char* selector) { ObjcMsgSend()(receiver, sel_registerName(selector)); } void SendVoidBool(id receiver, const char* selector, bool value) { ObjcMsgSend()(receiver, sel_registerName(selector), value); } void SendVoidId(id receiver, const char* selector, id value) { ObjcMsgSend()(receiver, sel_registerName(selector), value); } void SendVoidCGRect(id receiver, const char* selector, CGRect value) { ObjcMsgSend()(receiver, sel_registerName(selector), value); } void SendVoidCGSize(id receiver, const char* selector, CGSize value) { ObjcMsgSend()(receiver, sel_registerName(selector), value); } id Retain(id object) { return object ? SendId(object, "retain") : nil; } void Release(id object) { if (object) { SendVoid(object, "release"); } } void* CreateInternalMetalLayer(Uint32 width, Uint32 height, void** outWindow) { const auto surfaceWidth = static_cast(std::max(width, 1)); const auto surfaceHeight = static_cast(std::max(height, 1)); id windowClass = reinterpret_cast(objc_getClass("NSWindow")); id metalLayerClass = reinterpret_cast(objc_getClass("CAMetalLayer")); MOBILEGL_ASSERT(windowClass && metalLayerClass, "Failed to resolve NSWindow/CAMetalLayer for DirectVulkan pbuffer"); CGRect frame = {{0.0, 0.0}, {surfaceWidth, surfaceHeight}}; id window = SendId(windowClass, "alloc"); window = ObjcMsgSend()( window, sel_registerName("initWithContentRect:styleMask:backing:defer:"), frame, kNSWindowStyleMaskBorderless, kNSBackingStoreBuffered, true); MOBILEGL_ASSERT(window, "Failed to create hidden NSWindow for DirectVulkan pbuffer"); id contentView = SendId(window, "contentView"); MOBILEGL_ASSERT(contentView, "Failed to query hidden NSWindow contentView"); SendVoidBool(contentView, "setWantsLayer:", true); id metalLayer = SendId(metalLayerClass, "layer"); MOBILEGL_ASSERT(metalLayer, "Failed to create hidden CAMetalLayer for DirectVulkan pbuffer"); Retain(metalLayer); SendVoidCGRect(metalLayer, "setFrame:", frame); SendVoidCGSize(metalLayer, "setDrawableSize:", frame.size); SendVoidId(contentView, "setLayer:", metalLayer); *outWindow = window; return metalLayer; } } // namespace #endif static Bool IsPowerVRDevice(const VkPhysicalDeviceProperties& properties) { return std::strstr(properties.deviceName, "PowerVR") != nullptr; } static VkPipelineColorBlendAttachmentState MakeColorBlendAttachmentState( Bool blendEnable, VkBlendFactor srcColorBlendFactor, VkBlendFactor dstColorBlendFactor, VkBlendOp colorBlendOp, VkBlendFactor srcAlphaBlendFactor, VkBlendFactor dstAlphaBlendFactor, VkBlendOp alphaBlendOp, VkColorComponentFlags colorWriteMask) { VkPipelineColorBlendAttachmentState attachment{}; attachment.blendEnable = blendEnable ? VK_TRUE : VK_FALSE; attachment.srcColorBlendFactor = srcColorBlendFactor; attachment.dstColorBlendFactor = dstColorBlendFactor; attachment.colorBlendOp = colorBlendOp; attachment.srcAlphaBlendFactor = srcAlphaBlendFactor; attachment.dstAlphaBlendFactor = dstAlphaBlendFactor; attachment.alphaBlendOp = alphaBlendOp; attachment.colorWriteMask = colorWriteMask; return attachment; } static Bool IsDualSourceBlendFactor(BlendFactor v) { switch (v) { case BlendFactor::Src1Color: case BlendFactor::OneMinusSrc1Color: case BlendFactor::Src1Alpha: case BlendFactor::OneMinusSrc1Alpha: return true; default: return false; } } static Bool ShouldUseTransientVertexIndexBuffer(const MG_State::GLState::BufferObject& bufferObject) { switch (bufferObject.GetUsage()) { case BufferUsage::StreamDraw: case BufferUsage::StreamRead: case BufferUsage::StreamCopy: case BufferUsage::DynamicDraw: case BufferUsage::DynamicRead: case BufferUsage::DynamicCopy: return true; case BufferUsage::StaticDraw: case BufferUsage::StaticRead: case BufferUsage::StaticCopy: default: return false; } } static VkColorComponentFlags GetSupportedColorWriteMaskForComponentCount(SizeT componentCount) { switch (componentCount) { case 1: return VK_COLOR_COMPONENT_R_BIT; case 2: return VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT; case 3: return VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT; case 4: return VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; default: MOBILEGL_ASSERT(false, "GetSupportedColorWriteMaskForComponentCount: unsupported componentCount=%zu", componentCount); return 0; } } // GL 4.6 core 15.2.3: a colour format with no alpha channel reads as if alpha were one. // The substitution has to happen in the clear value's own type, so this reports the condition // and MakeVkClearColorValue applies it to whichever union member the encoding selects. static Bool ColorFormatLacksAlpha(const MG_State::GLState::ITextureObject* texture) { return texture != nullptr && MG_Util::GetBaseInternalFormatComponentCount(texture->GetFormat()) == 3; } static Bool IsQuarterTurnPreTransform(VkSurfaceTransformFlagBitsKHR preTransform) { return preTransform == VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR || preTransform == VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR; } static IntVec2 ResolveDefaultFramebufferLogicalExtent(VkSurfaceTransformFlagBitsKHR preTransform, const IntVec2& rawExtent) { if (IsQuarterTurnPreTransform(preTransform)) { return {rawExtent.y(), rawExtent.x()}; } return rawExtent; } static Int ScaleFramebufferCoordinate(Int value, Int fromExtent, Int toExtent) { if (fromExtent <= 0 || toExtent <= 0) { return value; } return static_cast((static_cast(value) * toExtent + fromExtent / 2) / fromExtent); } // --------------------------------------------------------------------------------------- // Default-framebuffer rectangles. // // GL's window origin is the BOTTOM-left. The default framebuffer's Vulkan image is stored in // DISPLAY (top-left) orientation, and the difference is reconciled for VERTICES by negating // gl_Position.y - but only for default-FBO draws (GetShaderTransformFlags -> // CompileOptionBit::PositionYFlip, applied in ProgramFactory::InsertPositionFixup). // // Rectangles were never converted. The viewport, the scissor and the ReadPixels copy offset // all used the GL bottom-origin Y verbatim as a Vulkan top-origin Y, which is correct only // when y == H - y - h (full height, or vertically centred) - and full height is the only case // any test ever exercised. In the conformance suite the errors CANCEL in placement (the draw // lands in Vulkan rows [y, y+h) and the readback copies the same rows back) and compose into // an exact vertical flip: 1,759 of Magma's 1,793 non-passing cases, 861 vertical flips and // nothing else across all of gl33. // // The mapping below is derived from - and at full extent exactly reproduces - the pixel // mapping VulkanRenderer::RemapDefaultFramebufferReadback uses: // identity : image(x, H-1-y) -> flip Y // 180 : image(W-1-x, y) -> mirror X (the rotation already flips the rows) // Quarter turns swap the axes and are handled by MapDefaultFramebufferReadbackRect rather than // this same-axis helper. struct DefaultFramebufferRectMapping { Bool flipY = false; Bool mirrorX = false; }; static DefaultFramebufferRectMapping GetDefaultFramebufferRectMapping( VkSurfaceTransformFlagBitsKHR preTransform) { if (preTransform == VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR) return {false, true}; if (IsQuarterTurnPreTransform(preTransform)) return {false, false}; return {true, false}; } // [origin, origin+size) counted from one end is [extent-origin-size, extent-origin) counted // from the other. A full-extent rect is a fixed point, which is why this can be introduced // without moving anything that works today. static Int MapDefaultFramebufferRectAxis(Int origin, Int size, Int extent, Bool invert) { return invert ? extent - origin - size : origin; } // 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; 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; 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; // 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 // 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 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 && lineWidth == other.lineWidth && extentX == other.extentX && extentY == other.extentY && preTransform == other.preTransform && scissorEnabled == other.scissorEnabled && isDefaultFbo == other.isDefaultFbo; } }; DynamicTailKey dynamicTailKey{}; }; static DynamicStateShadow g_dynamicStateShadow; #if MOBILEGL_PIPE_PUSH // ---- D12.3: DynamicTailKey's inputs against the P2 chunk table ---- // // DynamicTailKey's inventory (declared above, one line per reader) is an exact, // hand-maintained enumeration of what the six Apply* in the tail read. The P2 chunk table // (MG_Pipe/MGPipeRenderStateSpans.h) is an independent, offsetof-derived statement of // which bytes of RenderStateParameters are dynamic state. The two were written for // different reasons, so making them check each other is free evidence: if a later chunk // edit demotes or promotes one of these members, the mismatch is a BUILD BREAK here rather // than a tail that silently stops being re-run when its input moves. // // The brief (P2 D12.3) expects every input to be dynamic; the tree says otherwise for // exactly one, and the tree is right - see the ScissorTestEnabledMask note below. // // These assertions ARE D19's DynamicChunksCoverMagmasDynamicTailKey, in the only file this // package owns. D19 names it as a case in MG_Test/Pipe/RenderStateSpansTest.cpp, which // belongs to package A (C.5). INTEGRATOR: make sure the outcome is not "neither" - if // package A did not land that case, this static_assert block is the whole gate, and if it // did, the two are redundant on purpose and both should stay. namespace { // Is [begin, begin + size) covered entirely by DYNAMIC chunks? constexpr Bool MagmaRenderStateRangeIsDynamic(SizeT begin, SizeT size) { const SizeT end = begin + size; for (SizeT i = 0; i < MG_Pipe::kMGPipeRenderStateChunkCount; ++i) { const SizeT chunkBegin = MG_Pipe::kMGPipeRenderStateChunkBoundaries[i]; const SizeT chunkEnd = MG_Pipe::kMGPipeRenderStateChunkBoundaries[i + 1]; if (end <= chunkBegin || begin >= chunkEnd) continue; // disjoint if (MG_Pipe::MGPipeRenderStateChunkIsPipeline(i)) return false; } return true; } using MagmaTailRsp = RenderStateParameters; #define MAGMA_TAIL_INPUT_IS_DYNAMIC(Member) \ static_assert(MagmaRenderStateRangeIsDynamic(offsetof(MagmaTailRsp, Member), \ sizeof(MagmaTailRsp::Member)), \ "ApplyDynamicDrawStateTail reads " #Member \ ", which the P2 chunk table no longer calls dynamic state: a change to it would " \ "move the pipeline version, not the parameters version, and the tail would stop " \ "being re-run for it") // Viewports, DepthRanges and ScissorBoxes are asserted over the WHOLE array while the // tail reads only element 0. That is deliberately stricter than the reader needs: the // chunk table has no per-element granularity today, so an array that is dynamic at all // is dynamic entirely, and asserting the whole of it says so. If a later phase ever // splits a per-viewport chunk out, this is a build break by design - narrow the assert // to element 0 then, and say why in the same commit. MAGMA_TAIL_INPUT_IS_DYNAMIC(Viewports); // ApplyGLViewportState: Viewports[0] MAGMA_TAIL_INPUT_IS_DYNAMIC(DepthRanges); // ApplyGLViewportState: DepthRanges[0] MAGMA_TAIL_INPUT_IS_DYNAMIC(BlendColor); // ApplyBlendConstants MAGMA_TAIL_INPUT_IS_DYNAMIC(PolygonOffsetFactor); // ApplyPolygonOffsetState MAGMA_TAIL_INPUT_IS_DYNAMIC(PolygonOffsetUnits); // ApplyPolygonOffsetState MAGMA_TAIL_INPUT_IS_DYNAMIC(LineWidth); // ApplyLineWidthState MAGMA_TAIL_INPUT_IS_DYNAMIC(ScissorBoxes); // the scissor rect: ScissorBoxes[0] #undef MAGMA_TAIL_INPUT_IS_DYNAMIC // ApplyStencilState reads three of the seven members of each face, and D6 splits // StencilFaceState at sub-member granularity for exactly this reason: Ref, ValueMask // and WriteMask are VK_DYNAMIC_STATE_STENCIL_{REFERENCE,COMPARE_MASK,WRITE_MASK}, while // Func and the three ops are baked into the pipeline. Asserted per member, per face, // because the split runs THROUGH the struct rather than around it. constexpr SizeT kMagmaStencilFace1 = offsetof(MagmaTailRsp, StencilStates) + sizeof(StencilFaceState); #define MAGMA_TAIL_STENCIL_IS_DYNAMIC(Member) \ static_assert(MagmaRenderStateRangeIsDynamic(offsetof(MagmaTailRsp, StencilStates) + \ offsetof(StencilFaceState, Member), \ sizeof(StencilFaceState::Member)), \ "ApplyStencilState reads the FRONT face's " #Member " as dynamic state"); \ static_assert(MagmaRenderStateRangeIsDynamic(kMagmaStencilFace1 + offsetof(StencilFaceState, Member), \ sizeof(StencilFaceState::Member)), \ "ApplyStencilState reads the BACK face's " #Member " as dynamic state") MAGMA_TAIL_STENCIL_IS_DYNAMIC(Ref); MAGMA_TAIL_STENCIL_IS_DYNAMIC(ValueMask); MAGMA_TAIL_STENCIL_IS_DYNAMIC(WriteMask); #undef MAGMA_TAIL_STENCIL_IS_DYNAMIC // THE ONE INPUT THAT IS NOT DYNAMIC, and the brief's D12.3 says it should be. // The tree wins, and it is right: the split's only rule is "a byte is pipeline state // iff a public setter that calls BumpVersions() writes it", and ScissorTestEnabledMask // is written by SetCapability(ScissorTest), which does. It sits in pipeline chunk P6 // with the other capability bools. The tail reads it only to decide between the // scissor box and a full-extent rect, and it is HARMLESS there for a reason worth // stating: a pipeline-half write moves the pipeline version, and the pipeline version // moves only together with the parameters version (BumpVersions bumps both), so the // tail's version gate is invalidated by it just the same. A DYNAMIC member promoted // into the pipeline half would break that direction, which is what the asserts above // are for; this one is pinned in the opposite direction so that DEMOTING it - which // would be a real G7 violation - is also a build break. static_assert(!MagmaRenderStateRangeIsDynamic(offsetof(MagmaTailRsp, ScissorTestEnabledMask), sizeof(MagmaTailRsp::ScissorTestEnabledMask)), "ScissorTestEnabledMask is written by SetCapability(ScissorTest), which calls " "BumpVersions(), so the chunk table must keep it in the pipeline half"); } // namespace #endif // MOBILEGL_PIPE_PUSH static void ResetDynamicStateShadow() { g_dynamicStateShadow = {}; } // vkCmdBindVertexBuffers, skipped when this command buffer already holds these // buffers and offsets at binding 0. static void ShadowedBindVertexBuffers(VkCommandBuffer commandBuffer, const VkBuffer* buffers, const VkDeviceSize* offsets, Uint32 count) { auto& shadow = g_dynamicStateShadow; Bool identical = shadow.vertexBindValid && shadow.vertexBindingCount == count && count <= DynamicStateShadow::kMaxShadowedVertexBindings; if (identical) { for (Uint32 i = 0; i < count; ++i) { if (shadow.vertexBuffers[i] != buffers[i] || shadow.vertexOffsets[i] != offsets[i]) { identical = false; break; } } } if (identical) { return; } vkCmdBindVertexBuffers(commandBuffer, 0, count, buffers, offsets); if (count <= DynamicStateShadow::kMaxShadowedVertexBindings) { shadow.vertexBindValid = true; shadow.vertexBindingCount = count; std::copy_n(buffers, count, shadow.vertexBuffers); std::copy_n(offsets, count, shadow.vertexOffsets); } else { shadow.vertexBindValid = false; } } static void ShadowedSetScissor(VkCommandBuffer commandBuffer, const VkRect2D& scissor) { auto& shadow = g_dynamicStateShadow; if (shadow.scissorValid && shadow.scissor.offset.x == scissor.offset.x && shadow.scissor.offset.y == scissor.offset.y && shadow.scissor.extent.width == scissor.extent.width && shadow.scissor.extent.height == scissor.extent.height) { return; } shadow.scissorValid = true; shadow.scissor = scissor; vkCmdSetScissor(commandBuffer, 0, 1, &scissor); } // One viewport of the ARB_viewport_array state, mapped into Vulkan's frame. Split out of // ApplyGLViewportState so the multi-viewport path derives index i through EXACTLY the same // arithmetic as index 0 - the default-framebuffer Y-flip and pre-transform rotation // especially, which is the classic way a multi-viewport port comes out upside down for every // index but the one that was tested. static VkViewport ComputeGLViewport(Uint32 index, const IntVec2& framebufferExtent, VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFramebuffer) { // Snapped to integers. The viewport is float STATE (glViewportIndexedf may set a // fractional origin, and GetFloati_v hands it back verbatim), but what rasterizes here is // the rounded rectangle - a deliberate, documented infidelity rather than a spec claim: // MobileGL passes the driver's VIEWPORT_SUBPIXEL_BITS through, so it does advertise // subpixel viewport precision it does not deliver. Nothing in KHR-GL43.viewport_array or // in Minecraft sets a fractional viewport (the conformance checks are all on the state // round trip), which is why the honest-but-lossy path was kept over widening every // default-framebuffer Y-flip/pre-transform helper to floats. See the KNOWN INFIDELITY // note in MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp. const FloatVec4& stored = MGB_CTX->GetViewportIndexed(index); const IntVec4 viewportState(static_cast(std::lround(stored.x())), static_cast(std::lround(stored.y())), static_cast(std::lround(stored.z())), static_cast(std::lround(stored.w()))); const FloatVec2& depthRange = MGB_CTX->GetDepthRangeIndexed(index); const IntVec2 logicalExtent = isDefaultFramebuffer ? ResolveDefaultFramebufferLogicalExtent(preTransform, framebufferExtent) : framebufferExtent; Int viewportX = viewportState.x(); Int viewportY = viewportState.y(); Int viewportWidth = viewportState.z() > 0 ? viewportState.z() : logicalExtent.x(); Int viewportHeight = viewportState.w() > 0 ? viewportState.w() : logicalExtent.y(); if (isDefaultFramebuffer && IsQuarterTurnPreTransform(preTransform)) { viewportX = ScaleFramebufferCoordinate(viewportX, logicalExtent.x(), framebufferExtent.x()); viewportY = ScaleFramebufferCoordinate(viewportY, logicalExtent.y(), framebufferExtent.y()); viewportWidth = ScaleFramebufferCoordinate(viewportWidth, logicalExtent.x(), framebufferExtent.x()); viewportHeight = ScaleFramebufferCoordinate(viewportHeight, logicalExtent.y(), framebufferExtent.y()); } // The GL viewport rect, expressed against the default framebuffer's stored orientation. // A full-height viewport is unchanged by this, which is why every existing scenario keeps // its exact behaviour. if (isDefaultFramebuffer) { const DefaultFramebufferRectMapping mapping = GetDefaultFramebufferRectMapping(preTransform); viewportX = MapDefaultFramebufferRectAxis(viewportX, viewportWidth, framebufferExtent.x(), mapping.mirrorX); viewportY = MapDefaultFramebufferRectAxis(viewportY, viewportHeight, framebufferExtent.y(), mapping.flipY); } VkViewport viewport{}; viewport.x = static_cast(viewportX); viewport.y = static_cast(viewportY); viewport.width = static_cast(viewportWidth); viewport.height = static_cast(viewportHeight); viewport.minDepth = depthRange.x(); viewport.maxDepth = depthRange.y(); return viewport; } static void ApplyGLViewportState(VkCommandBuffer commandBuffer, const IntVec2& framebufferExtent, VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFramebuffer) { const VkViewport viewport = ComputeGLViewport(0, framebufferExtent, preTransform, isDefaultFramebuffer); auto& shadow = g_dynamicStateShadow; if (shadow.viewportValid && shadow.viewport.x == viewport.x && shadow.viewport.y == viewport.y && shadow.viewport.width == viewport.width && shadow.viewport.height == viewport.height && shadow.viewport.minDepth == viewport.minDepth && shadow.viewport.maxDepth == viewport.maxDepth) { return; } shadow.viewportValid = true; shadow.viewport = viewport; vkCmdSetViewport(commandBuffer, 0, 1, &viewport); } static void ApplyBlendConstants(VkCommandBuffer commandBuffer) { const FloatVec4& blendColor = MGB_CTX->GetBlendColor(); const float blendConstants[4] = { blendColor.x(), blendColor.y(), blendColor.z(), blendColor.w(), }; auto& shadow = g_dynamicStateShadow; if (shadow.blendConstantsValid && shadow.blendConstants[0] == blendConstants[0] && shadow.blendConstants[1] == blendConstants[1] && shadow.blendConstants[2] == blendConstants[2] && shadow.blendConstants[3] == blendConstants[3]) { return; } shadow.blendConstantsValid = true; shadow.blendConstants[0] = blendConstants[0]; shadow.blendConstants[1] = blendConstants[1]; shadow.blendConstants[2] = blendConstants[2]; shadow.blendConstants[3] = blendConstants[3]; vkCmdSetBlendConstants(commandBuffer, blendConstants); } static Bool DrawModeUsesPolygonFill(GLenum mode) { switch (mode) { case GL_TRIANGLES: case GL_TRIANGLE_STRIP: case GL_TRIANGLE_FAN: return true; default: return false; } } static void ApplyPolygonOffsetState(VkCommandBuffer commandBuffer) { const Float constantFactor = MGB_CTX->GetPolygonOffsetUnits(); const Float slopeFactor = MGB_CTX->GetPolygonOffsetFactor(); auto& shadow = g_dynamicStateShadow; if (shadow.depthBiasValid && shadow.depthBiasConstantFactor == constantFactor && shadow.depthBiasSlopeFactor == slopeFactor) { return; } shadow.depthBiasValid = true; shadow.depthBiasConstantFactor = constantFactor; shadow.depthBiasSlopeFactor = slopeFactor; vkCmdSetDepthBias(commandBuffer, constantFactor, 0.0f, slopeFactor); } static void ApplyLineWidthState(VkCommandBuffer commandBuffer) { Float lineWidth = MGB_CTX->GetLineWidth(); #if MOBILEGL_BUILD_DISAGGREGATED // P5c (hd, CONTRACT-P5C ยง3.7): with an active transport the dynamic parameters are the // SERVER's own backend's - the client caps mirror is client memory (rule E). Monolith // reads the mirror as it always did. if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { if (MG_Backend::BackendObject* server = MG_Remote::Server::ServerLoopInstance().Backend()) { const auto& dynamicParameters = server->GetDynamicParameters(); const Float minLineWidth = dynamicParameters.AliasedLineWidthRangeMin; const Float maxLineWidth = dynamicParameters.AliasedLineWidthRangeMax; if (lineWidth < minLineWidth) { lineWidth = minLineWidth; } else if (lineWidth > maxLineWidth) { lineWidth = maxLineWidth; } } } else #endif if (MG_Backend::pActiveBackendObject != nullptr) { const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters(); const Float minLineWidth = dynamicParameters.AliasedLineWidthRangeMin; const Float maxLineWidth = dynamicParameters.AliasedLineWidthRangeMax; if (lineWidth < minLineWidth) { lineWidth = minLineWidth; } else if (lineWidth > maxLineWidth) { lineWidth = maxLineWidth; } } auto& shadow = g_dynamicStateShadow; if (shadow.lineWidthValid && shadow.lineWidth == lineWidth) { return; } shadow.lineWidthValid = true; shadow.lineWidth = lineWidth; vkCmdSetLineWidth(commandBuffer, lineWidth); } static VkRect2D MakeClampedScissorRect(const IntVec4& scissorBox, const IntVec2& framebufferExtent) { const Int x0 = std::max(0, scissorBox.x()); const Int y0 = std::max(0, scissorBox.y()); const Int x1 = std::min(framebufferExtent.x(), scissorBox.x() + std::max(0, scissorBox.z())); const Int y1 = std::min(framebufferExtent.y(), scissorBox.y() + std::max(0, scissorBox.w())); VkRect2D scissor{}; scissor.offset = {x0, y0}; scissor.extent = { static_cast(std::max(0, x1 - x0)), static_cast(std::max(0, y1 - y0)), }; return scissor; } // The clamped rect, re-expressed against the default framebuffer's stored orientation. Same // conversion as the viewport - and it must be the same one, or the scissor would cut a band // the draw never touched. static VkRect2D MapScissorRectToDefaultFramebuffer(VkRect2D scissor, const IntVec2& framebufferExtent, VkSurfaceTransformFlagBitsKHR preTransform) { const DefaultFramebufferRectMapping mapping = GetDefaultFramebufferRectMapping(preTransform); scissor.offset.x = MapDefaultFramebufferRectAxis(scissor.offset.x, static_cast(scissor.extent.width), framebufferExtent.x(), mapping.mirrorX); scissor.offset.y = MapDefaultFramebufferRectAxis(scissor.offset.y, static_cast(scissor.extent.height), framebufferExtent.y(), mapping.flipY); return scissor; } static VkRect2D MakeDefaultFramebufferScissorRect(const IntVec4& scissorBox, const IntVec2& framebufferExtent, VkSurfaceTransformFlagBitsKHR preTransform) { if (!IsQuarterTurnPreTransform(preTransform)) { return MapScissorRectToDefaultFramebuffer(MakeClampedScissorRect(scissorBox, framebufferExtent), framebufferExtent, preTransform); } const IntVec2 logicalExtent = ResolveDefaultFramebufferLogicalExtent(preTransform, framebufferExtent); const Int logicalX0 = std::max(0, scissorBox.x()); const Int logicalY0 = std::max(0, scissorBox.y()); const Int logicalX1 = std::min(logicalExtent.x(), scissorBox.x() + std::max(0, scissorBox.z())); const Int logicalY1 = std::min(logicalExtent.y(), scissorBox.y() + std::max(0, scissorBox.w())); const Int rawX0 = ScaleFramebufferCoordinate(logicalX0, logicalExtent.x(), framebufferExtent.x()); const Int rawY0 = ScaleFramebufferCoordinate(logicalY0, logicalExtent.y(), framebufferExtent.y()); const Int rawX1 = ScaleFramebufferCoordinate(logicalX1, logicalExtent.x(), framebufferExtent.x()); const Int rawY1 = ScaleFramebufferCoordinate(logicalY1, logicalExtent.y(), framebufferExtent.y()); VkRect2D scissor{}; scissor.offset = {std::max(0, rawX0), std::max(0, rawY0)}; scissor.extent = { static_cast(std::max(0, rawX1 - rawX0)), static_cast(std::max(0, rawY1 - rawY0)), }; // A quarter turn maps to {false, false}, so this is a no-op today; it is here so the // branch cannot drift away from the identity/180 one when quarter turns are modelled. return MapScissorRectToDefaultFramebuffer(scissor, framebufferExtent, preTransform); } static void ApplyStencilState(VkCommandBuffer commandBuffer) { const StencilFaceState& frontStencil = MGB_CTX->GetStencilState(StencilFace::Front); const StencilFaceState& backStencil = MGB_CTX->GetStencilState(StencilFace::Back); const Uint32 frontReference = static_cast(std::max(frontStencil.Ref, 0)); const Uint32 backReference = static_cast(std::max(backStencil.Ref, 0)); auto& shadow = g_dynamicStateShadow; if (shadow.stencilValid && shadow.stencilFrontCompareMask == frontStencil.ValueMask && shadow.stencilBackCompareMask == backStencil.ValueMask && shadow.stencilFrontWriteMask == frontStencil.WriteMask && shadow.stencilBackWriteMask == backStencil.WriteMask && shadow.stencilFrontReference == frontReference && shadow.stencilBackReference == backReference) { return; } shadow.stencilValid = true; shadow.stencilFrontCompareMask = frontStencil.ValueMask; shadow.stencilBackCompareMask = backStencil.ValueMask; shadow.stencilFrontWriteMask = frontStencil.WriteMask; shadow.stencilBackWriteMask = backStencil.WriteMask; shadow.stencilFrontReference = frontReference; shadow.stencilBackReference = backReference; vkCmdSetStencilCompareMask(commandBuffer, VK_STENCIL_FACE_FRONT_BIT, frontStencil.ValueMask); vkCmdSetStencilCompareMask(commandBuffer, VK_STENCIL_FACE_BACK_BIT, backStencil.ValueMask); vkCmdSetStencilWriteMask(commandBuffer, VK_STENCIL_FACE_FRONT_BIT, frontStencil.WriteMask); vkCmdSetStencilWriteMask(commandBuffer, VK_STENCIL_FACE_BACK_BIT, backStencil.WriteMask); vkCmdSetStencilReference(commandBuffer, VK_STENCIL_FACE_FRONT_BIT, frontReference); vkCmdSetStencilReference(commandBuffer, VK_STENCIL_FACE_BACK_BIT, backReference); } enum class NumericDomain { Unknown, FloatLike, Sint, Uint, }; static NumericDomain GetNumericDomainForShaderValueType(GLenum glType) { switch (glType) { case GL_FLOAT: case GL_FLOAT_VEC2: case GL_FLOAT_VEC3: case GL_FLOAT_VEC4: return NumericDomain::FloatLike; case GL_INT: case GL_INT_VEC2: case GL_INT_VEC3: case GL_INT_VEC4: return NumericDomain::Sint; case GL_UNSIGNED_INT: case GL_UNSIGNED_INT_VEC2: case GL_UNSIGNED_INT_VEC3: case GL_UNSIGNED_INT_VEC4: return NumericDomain::Uint; default: return NumericDomain::Unknown; } } static SizeT GetComponentCountForShaderValueType(GLenum glType) { switch (glType) { case GL_FLOAT: case GL_INT: case GL_UNSIGNED_INT: return 1; case GL_FLOAT_VEC2: case GL_INT_VEC2: case GL_UNSIGNED_INT_VEC2: return 2; case GL_FLOAT_VEC3: case GL_INT_VEC3: case GL_UNSIGNED_INT_VEC3: return 3; case GL_FLOAT_VEC4: case GL_INT_VEC4: case GL_UNSIGNED_INT_VEC4: return 4; default: return 0; } } static NumericDomain GetNumericDomainForVertexFormat(VkFormat format) { switch (format) { case VK_FORMAT_R32_SFLOAT: case VK_FORMAT_R32G32_SFLOAT: case VK_FORMAT_R32G32B32_SFLOAT: case VK_FORMAT_R32G32B32A32_SFLOAT: case VK_FORMAT_R16_SNORM: case VK_FORMAT_R16G16_SNORM: case VK_FORMAT_R16G16B16_SNORM: case VK_FORMAT_R16G16B16A16_SNORM: case VK_FORMAT_R16_UNORM: case VK_FORMAT_R16G16_UNORM: case VK_FORMAT_R16G16B16_UNORM: case VK_FORMAT_R16G16B16A16_UNORM: case VK_FORMAT_R16_SSCALED: case VK_FORMAT_R16G16_SSCALED: case VK_FORMAT_R16G16B16_SSCALED: case VK_FORMAT_R16G16B16A16_SSCALED: case VK_FORMAT_R16_USCALED: case VK_FORMAT_R16G16_USCALED: case VK_FORMAT_R16G16B16_USCALED: case VK_FORMAT_R16G16B16A16_USCALED: case VK_FORMAT_R8_SNORM: case VK_FORMAT_R8G8_SNORM: case VK_FORMAT_R8G8B8_SNORM: case VK_FORMAT_R8G8B8A8_SNORM: case VK_FORMAT_R8_UNORM: case VK_FORMAT_R8G8_UNORM: case VK_FORMAT_R8G8B8_UNORM: case VK_FORMAT_R8G8B8A8_UNORM: case VK_FORMAT_R8_SSCALED: case VK_FORMAT_R8G8_SSCALED: case VK_FORMAT_R8G8B8_SSCALED: case VK_FORMAT_R8G8B8A8_SSCALED: case VK_FORMAT_R8_USCALED: case VK_FORMAT_R8G8_USCALED: case VK_FORMAT_R8G8B8_USCALED: case VK_FORMAT_R8G8B8A8_USCALED: return NumericDomain::FloatLike; case VK_FORMAT_R32_SINT: case VK_FORMAT_R32G32_SINT: case VK_FORMAT_R32G32B32_SINT: case VK_FORMAT_R32G32B32A32_SINT: case VK_FORMAT_R16_SINT: case VK_FORMAT_R16G16_SINT: case VK_FORMAT_R16G16B16_SINT: case VK_FORMAT_R16G16B16A16_SINT: case VK_FORMAT_R8_SINT: case VK_FORMAT_R8G8_SINT: case VK_FORMAT_R8G8B8_SINT: case VK_FORMAT_R8G8B8A8_SINT: return NumericDomain::Sint; case VK_FORMAT_R32_UINT: case VK_FORMAT_R32G32_UINT: case VK_FORMAT_R32G32B32_UINT: case VK_FORMAT_R32G32B32A32_UINT: case VK_FORMAT_R16_UINT: case VK_FORMAT_R16G16_UINT: case VK_FORMAT_R16G16B16_UINT: case VK_FORMAT_R16G16B16A16_UINT: case VK_FORMAT_R8_UINT: case VK_FORMAT_R8G8_UINT: case VK_FORMAT_R8G8B8_UINT: case VK_FORMAT_R8G8B8A8_UINT: return NumericDomain::Uint; default: return NumericDomain::Unknown; } } static Bool TryCoerceVertexFormatNumericDomain(VkFormat sourceFormat, NumericDomain targetDomain, VkFormat& outFormat) { const NumericDomain sourceDomain = GetNumericDomainForVertexFormat(sourceFormat); if (sourceDomain == targetDomain || targetDomain == NumericDomain::Unknown) { outFormat = sourceFormat; return true; } if (sourceDomain == NumericDomain::FloatLike) { return false; } switch (sourceFormat) { case VK_FORMAT_R32_SINT: if (targetDomain == NumericDomain::Uint) { outFormat = VK_FORMAT_R32_UINT; return true; } return false; case VK_FORMAT_R32G32_SINT: if (targetDomain == NumericDomain::Uint) { outFormat = VK_FORMAT_R32G32_UINT; return true; } return false; case VK_FORMAT_R32G32B32_SINT: if (targetDomain == NumericDomain::Uint) { outFormat = VK_FORMAT_R32G32B32_UINT; return true; } return false; case VK_FORMAT_R32G32B32A32_SINT: if (targetDomain == NumericDomain::Uint) { outFormat = VK_FORMAT_R32G32B32A32_UINT; return true; } return false; case VK_FORMAT_R32_UINT: if (targetDomain == NumericDomain::Sint) { outFormat = VK_FORMAT_R32_SINT; return true; } return false; case VK_FORMAT_R32G32_UINT: if (targetDomain == NumericDomain::Sint) { outFormat = VK_FORMAT_R32G32_SINT; return true; } return false; case VK_FORMAT_R32G32B32_UINT: if (targetDomain == NumericDomain::Sint) { outFormat = VK_FORMAT_R32G32B32_SINT; return true; } return false; case VK_FORMAT_R32G32B32A32_UINT: if (targetDomain == NumericDomain::Sint) { outFormat = VK_FORMAT_R32G32B32A32_SINT; return true; } return false; case VK_FORMAT_R16_SINT: outFormat = targetDomain == NumericDomain::Uint ? VK_FORMAT_R16_UINT : VK_FORMAT_R16_SSCALED; return true; case VK_FORMAT_R16G16_SINT: outFormat = targetDomain == NumericDomain::Uint ? VK_FORMAT_R16G16_UINT : VK_FORMAT_R16G16_SSCALED; return true; case VK_FORMAT_R16G16B16_SINT: outFormat = targetDomain == NumericDomain::Uint ? VK_FORMAT_R16G16B16_UINT : VK_FORMAT_R16G16B16_SSCALED; return true; case VK_FORMAT_R16G16B16A16_SINT: outFormat = targetDomain == NumericDomain::Uint ? VK_FORMAT_R16G16B16A16_UINT : VK_FORMAT_R16G16B16A16_SSCALED; return true; case VK_FORMAT_R16_UINT: outFormat = targetDomain == NumericDomain::Sint ? VK_FORMAT_R16_SINT : VK_FORMAT_R16_USCALED; return true; case VK_FORMAT_R16G16_UINT: outFormat = targetDomain == NumericDomain::Sint ? VK_FORMAT_R16G16_SINT : VK_FORMAT_R16G16_USCALED; return true; case VK_FORMAT_R16G16B16_UINT: outFormat = targetDomain == NumericDomain::Sint ? VK_FORMAT_R16G16B16_SINT : VK_FORMAT_R16G16B16_USCALED; return true; case VK_FORMAT_R16G16B16A16_UINT: outFormat = targetDomain == NumericDomain::Sint ? VK_FORMAT_R16G16B16A16_SINT : VK_FORMAT_R16G16B16A16_USCALED; return true; case VK_FORMAT_R8_SINT: outFormat = targetDomain == NumericDomain::Uint ? VK_FORMAT_R8_UINT : VK_FORMAT_R8_SSCALED; return true; case VK_FORMAT_R8G8_SINT: outFormat = targetDomain == NumericDomain::Uint ? VK_FORMAT_R8G8_UINT : VK_FORMAT_R8G8_SSCALED; return true; case VK_FORMAT_R8G8B8_SINT: outFormat = targetDomain == NumericDomain::Uint ? VK_FORMAT_R8G8B8_UINT : VK_FORMAT_R8G8B8_SSCALED; return true; case VK_FORMAT_R8G8B8A8_SINT: outFormat = targetDomain == NumericDomain::Uint ? VK_FORMAT_R8G8B8A8_UINT : VK_FORMAT_R8G8B8A8_SSCALED; return true; case VK_FORMAT_R8_UINT: outFormat = targetDomain == NumericDomain::Sint ? VK_FORMAT_R8_SINT : VK_FORMAT_R8_USCALED; return true; case VK_FORMAT_R8G8_UINT: outFormat = targetDomain == NumericDomain::Sint ? VK_FORMAT_R8G8_SINT : VK_FORMAT_R8G8_USCALED; return true; case VK_FORMAT_R8G8B8_UINT: outFormat = targetDomain == NumericDomain::Sint ? VK_FORMAT_R8G8B8_SINT : VK_FORMAT_R8G8B8_USCALED; return true; case VK_FORMAT_R8G8B8A8_UINT: outFormat = targetDomain == NumericDomain::Sint ? VK_FORMAT_R8G8B8A8_SINT : VK_FORMAT_R8G8B8A8_USCALED; return true; default: return false; } } template static Float ConvertIntegerVertexComponentToFloat(ComponentT value, Bool normalized) { if (!normalized) { return static_cast(value); } if constexpr (std::is_signed_v) { const Float scaled = static_cast(value) / static_cast(std::numeric_limits::max()); return std::max(-1.0f, scaled); } else { return static_cast(value) / static_cast(std::numeric_limits::max()); } } template static Bool ConvertIntegerVertexStreamToFloat32( const MG_State::GLState::VertexAttribute& attribute, const Uint8* sourceData, SizeT sourceStride, SizeT elementCount, Vector& outData) { if (sourceData == nullptr || attribute.Size < 1 || attribute.Size > 4 || sourceStride == 0) { return false; } const SizeT componentCount = static_cast(attribute.Size); outData.resize(elementCount * componentCount); for (SizeT element = 0; element < elementCount; ++element) { const Uint8* sourceElement = sourceData + element * sourceStride; Float* destinationElement = outData.data() + element * componentCount; for (SizeT component = 0; component < componentCount; ++component) { ComponentT value{}; Memcpy(&value, sourceElement + component * sizeof(ComponentT), sizeof(ComponentT)); destinationElement[component] = ConvertIntegerVertexComponentToFloat(value, attribute.Normalized); } } return true; } static Bool ConvertScaledIntegerVertexStreamToFloat32( const MG_State::GLState::VertexAttribute& attribute, const Uint8* sourceData, SizeT sourceStride, SizeT elementCount, Vector& outData) { switch (attribute.Type) { case DataType::Int8: return ConvertIntegerVertexStreamToFloat32( attribute, sourceData, sourceStride, elementCount, outData); case DataType::Uint8: return ConvertIntegerVertexStreamToFloat32( attribute, sourceData, sourceStride, elementCount, outData); case DataType::Int16: return ConvertIntegerVertexStreamToFloat32( attribute, sourceData, sourceStride, elementCount, outData); case DataType::Uint16: return ConvertIntegerVertexStreamToFloat32( attribute, sourceData, sourceStride, elementCount, outData); default: return false; } } // The fetch half of the 64-bit vertex narrowing, whose shader half is guaranteed by // SupportsFloat64VertexAttributes staying false on this backend: any program with a Float64 // vertex INPUT is demoted whole, native fp64 or not, so the input is always a 32-bit one. The // source bytes are ordinary IEEE-754 doubles, so a GL_DOUBLE array is deinterleaved into a // tightly packed float32 stream rather than dropped. `normalized` is not consulted - GL // ignores it for floating-point array types. static Bool ConvertFloat64VertexStreamToFloat32( const MG_State::GLState::VertexAttribute& attribute, const Uint8* sourceData, SizeT sourceStride, SizeT elementCount, Vector& outData) { if (sourceData == nullptr || attribute.Size < 1 || attribute.Size > 4 || sourceStride == 0) { return false; } const SizeT componentCount = static_cast(attribute.Size); outData.resize(elementCount * componentCount); for (SizeT element = 0; element < elementCount; ++element) { const Uint8* sourceElement = sourceData + element * sourceStride; Float* destinationElement = outData.data() + element * componentCount; for (SizeT component = 0; component < componentCount; ++component) { // GL byte strides and offsets are arbitrary, so no component carries an 8-byte // alignment guarantee; copy it out before narrowing it. Double value = 0.0; Memcpy(&value, sourceElement + component * sizeof(Double), sizeof(Double)); destinationElement[component] = static_cast(value); } } return true; } static Bool RepackVertexStream(const Uint8* sourceData, SizeT sourceStride, SizeT elementSize, SizeT elementCount, Vector& outData) { if (sourceData == nullptr || sourceStride == 0 || elementSize == 0) { return false; } outData.resize(elementCount * elementSize); for (SizeT element = 0; element < elementCount; ++element) { Memcpy(outData.data() + element * elementSize, sourceData + element * sourceStride, elementSize); } return true; } static NumericDomain GetNumericDomainForTextureInternalFormat(TextureInternalFormat format) { switch (format) { case TextureInternalFormat::R8I: case TextureInternalFormat::R16I: case TextureInternalFormat::R32I: case TextureInternalFormat::RG8I: case TextureInternalFormat::RG16I: case TextureInternalFormat::RG32I: case TextureInternalFormat::RGB8I: case TextureInternalFormat::RGB16I: case TextureInternalFormat::RGB32I: case TextureInternalFormat::RGBA8I: case TextureInternalFormat::RGBA16I: case TextureInternalFormat::RGBA32I: return NumericDomain::Sint; case TextureInternalFormat::R8UI: case TextureInternalFormat::R16UI: case TextureInternalFormat::R32UI: case TextureInternalFormat::RG8UI: case TextureInternalFormat::RG16UI: case TextureInternalFormat::RG32UI: case TextureInternalFormat::RGB8UI: case TextureInternalFormat::RGB16UI: case TextureInternalFormat::RGB32UI: case TextureInternalFormat::RGBA8UI: case TextureInternalFormat::RGBA16UI: case TextureInternalFormat::RGBA32UI: case TextureInternalFormat::RGB10A2UI: return NumericDomain::Uint; case TextureInternalFormat::DepthComponent: case TextureInternalFormat::DepthComponent16: case TextureInternalFormat::DepthComponent24: case TextureInternalFormat::DepthComponent32: case TextureInternalFormat::DepthComponent32F: case TextureInternalFormat::Depth24Stencil8: case TextureInternalFormat::Depth32FStencil8: case TextureInternalFormat::DepthStencil: return NumericDomain::Unknown; default: return NumericDomain::FloatLike; } } // 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(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"); static Bool TryGetCurrentVertexAttributeFormat(GLenum glType, VkFormat& outFormat) { switch (glType) { case GL_FLOAT: outFormat = VK_FORMAT_R32_SFLOAT; return true; case GL_FLOAT_VEC2: outFormat = VK_FORMAT_R32G32_SFLOAT; return true; case GL_FLOAT_VEC3: outFormat = VK_FORMAT_R32G32B32_SFLOAT; return true; case GL_FLOAT_VEC4: outFormat = VK_FORMAT_R32G32B32A32_SFLOAT; return true; case GL_INT: outFormat = VK_FORMAT_R32_SINT; return true; case GL_INT_VEC2: outFormat = VK_FORMAT_R32G32_SINT; return true; case GL_INT_VEC3: outFormat = VK_FORMAT_R32G32B32_SINT; return true; case GL_INT_VEC4: outFormat = VK_FORMAT_R32G32B32A32_SINT; return true; case GL_UNSIGNED_INT: outFormat = VK_FORMAT_R32_UINT; return true; case GL_UNSIGNED_INT_VEC2: outFormat = VK_FORMAT_R32G32_UINT; return true; case GL_UNSIGNED_INT_VEC3: outFormat = VK_FORMAT_R32G32B32_UINT; return true; case GL_UNSIGNED_INT_VEC4: outFormat = VK_FORMAT_R32G32B32A32_UINT; return true; default: return false; } } 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; } } static const char* VkImageLayoutToString(VkImageLayout layout) { switch (layout) { case VK_IMAGE_LAYOUT_UNDEFINED: return "VK_IMAGE_LAYOUT_UNDEFINED"; case VK_IMAGE_LAYOUT_GENERAL: return "VK_IMAGE_LAYOUT_GENERAL"; case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: return "VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL"; case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: return "VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL"; case VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL: return "VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL"; case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: return "VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL"; case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: return "VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL"; case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: return "VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL"; case VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: return "VK_IMAGE_LAYOUT_PRESENT_SRC_KHR"; case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL: return "VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL"; case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL: return "VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL"; default: return "VK_IMAGE_LAYOUT_OTHER"; } } static Bool ActiveRenderPassUsesTexture(const ActiveRenderPassInfo& activeRenderPass, const MG_State::GLState::ITextureObject& texture) { for (const auto& trackedAttachment : activeRenderPass.trackedAttachmentLayouts) { if (trackedAttachment.target != TrackedAttachmentTarget::Texture) { continue; } // Raw identity compare (see textureRaw): the caller's texture is // live, so a dangling tracked pointer can never equal its address // unless the allocator reused it - and that false positive merely // ends the render pass early, never misses a genuine use. if (trackedAttachment.textureRaw == &texture) { return true; } } return false; } static void RecordClearBufferError(const char* func, ErrorCode code, const char* message) { MGB_CTX->RecordError(code, MakeUnique("DirectVulkan", func, message)); } static void RecordTextureCopyError(const char* func, ErrorCode code, const char* message) { MGB_CTX->RecordError(code, MakeUnique("DirectVulkan", func, message)); } static Bool HasDistinctCompleteDepthStencilTextureAttachments( const MG_State::GLState::FramebufferObject& framebufferObject) { if (framebufferObject.GetExternalIndex() == 0) { return false; } const auto& depthAttachment = framebufferObject.GetAttachment(FramebufferAttachmentType::Depth); const auto& stencilAttachment = framebufferObject.GetAttachment(FramebufferAttachmentType::Stencil); if (!depthAttachment.IsComplete() || !stencilAttachment.IsComplete() || !depthAttachment.IsTexture() || !stencilAttachment.IsTexture()) { return false; } return depthAttachment.GetTexture().get() != stencilAttachment.GetTexture().get() || depthAttachment.GetTextureUploadTarget() != stencilAttachment.GetTextureUploadTarget() || ToStorageMipLevel(depthAttachment.GetTexture().get(), depthAttachment.GetTextureLevel()) != ToStorageMipLevel(stencilAttachment.GetTexture().get(), stencilAttachment.GetTextureLevel()); } static Bool IsColorAttachment(FramebufferAttachmentType attachmentType) { return attachmentType >= FramebufferAttachmentType::Color0 && attachmentType <= FramebufferAttachmentType::Color31; } static Bool HasUnsupportedCompleteRenderbufferAttachment( const MG_State::GLState::FramebufferObject& framebufferObject) { if (framebufferObject.GetExternalIndex() == 0) { return false; } const auto& depthAttachment = framebufferObject.GetAttachment(FramebufferAttachmentType::Depth); const auto& stencilAttachment = framebufferObject.GetAttachment(FramebufferAttachmentType::Stencil); if (!depthAttachment.IsComplete() || !stencilAttachment.IsComplete()) { return false; } if (depthAttachment.IsRenderbuffer() && stencilAttachment.IsRenderbuffer()) { return depthAttachment.GetRenderbuffer().get() != stencilAttachment.GetRenderbuffer().get(); } if ((depthAttachment.IsRenderbuffer() || stencilAttachment.IsRenderbuffer()) && (depthAttachment.IsTexture() || stencilAttachment.IsTexture())) { return true; } return false; } static Bool IsUnsupportedFramebufferForDirectVulkan( const MG_State::GLState::FramebufferObject& framebufferObject) { // TODO: Revisit this gate when DirectVulkan has full color renderbuffer render/blit/readback support. return HasDistinctCompleteDepthStencilTextureAttachments(framebufferObject) || HasUnsupportedCompleteRenderbufferAttachment(framebufferObject); } static void RecordUnsupportedFramebufferError(const char* func) { MGB_CTX->RecordError( ErrorCode::InvalidFramebufferOperation, MakeUnique( "DirectVulkan", func, "DirectVulkan does not support this non-default framebuffer configuration.")); } static Bool IsValidSampledImageLayout(VkImageLayout layout) { switch (layout) { case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: case VK_IMAGE_LAYOUT_GENERAL: 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: return true; default: return false; } } namespace { static constexpr Uint32 kDescriptorSetsPerFrame = 64; #if !MOBILEGL_BUILD_DISAGGREGATED // P7 wave 2-B2, CONTRACT-P7 ยง5.2 (B'): THE HIDDEN GL PROGRAMS ONLY EXIST IN THE PULL // BUILD NOW. In a disaggregated build the monolith arm drives WireColorBlit.inc and // WireDepthMipmap.inc - the baked modules the wire arm already uses - so nothing below // this line is compiled: no GLSL text, no hidden object ids, no ShaderObject, // ProgramObject or SamplerObject construction on a DirectVulkan path. That is what // takes those three classes out of the link-closure ratchet's `p7-magma` bucket (ยง4.2), // and the reason it is an `#if` and not a runtime branch is exactly that: a runtime // early return leaves the symbols referenced. // // The pull build keeps every statement below byte for byte, which is what G1 pins. static constexpr Uint kHiddenBlitProgramId = 0xFFFFFFF0u; static constexpr Uint kHiddenBlitVertexShaderId = 0xFFFFFFF1u; static constexpr Uint kHiddenBlitFragmentShaderId = 0xFFFFFFF2u; static constexpr Uint kHiddenBlitNearestSamplerId = 0xFFFFFFF3u; static constexpr Uint kHiddenBlitLinearSamplerId = 0xFFFFFFF4u; static constexpr Uint kHiddenDepthMipmapProgramId = 0xFFFFFFF5u; static constexpr Uint kHiddenDepthMipmapVertexShaderId = 0xFFFFFFF6u; static constexpr Uint kHiddenDepthMipmapFragmentShaderId = 0xFFFFFFF7u; static constexpr const char* kFullscreenTriangleVertexShaderSource = R"(#version 460 core uniform vec4 uSrcRect; uniform vec4 uDstRect; uniform int uSurfaceTransform; layout(location = 0) out vec2 vTexCoord; vec2 ApplySurfaceTransform(vec2 position, int transform) { vec2 p = position; p.y = -p.y; if (transform == 1) { p = vec2(-p.y, p.x); } else if (transform == 2) { p = -p; } else if (transform == 3) { p = vec2(p.y, -p.x); } return p; } void main() { const vec2 uvTri[3] = vec2[]( vec2(0.0, 0.0), vec2(2.0, 0.0), vec2(0.0, 2.0) ); vec2 uv = uvTri[gl_VertexID]; vec2 dst = uDstRect.xy + uv * uDstRect.zw; vec2 clip = dst * 2.0 - 1.0; clip = ApplySurfaceTransform(clip, uSurfaceTransform); gl_Position = vec4(clip, 0.0, 1.0); vTexCoord = uSrcRect.xy + uv * uSrcRect.zw; } )"; static constexpr const char* kBlitFragmentShaderSource = R"(#version 460 core layout(binding = 0) uniform sampler2D uSource; layout(location = 0) in vec2 vTexCoord; layout(location = 0) out vec4 outColor; void main() { // Explicit LOD, not texture(): a blit reads exactly the selected level, so // derivative-based mip selection has no business here. It is also load-bearing: // on Adreno 650 (driver 512.502) an implicit-LOD sample of this single-mip // UBWC render target through the pre-rotation (ROTATE_90) mapping reads past // the image's allocation - despite the sampler's maxLod=0 and a nominal 1:1 // texel mapping whose LOD is 0, so the driver's implicit-LOD path itself is at // fault - and page-faults the GPU once the neighbouring memory is returned to // the kernel (frame 2 of Minecraft 26.2's resource reload; the kernel then // invalidates the context and the next submit dies with EDEADLK -> // VK_ERROR_DEVICE_LOST at Present). Verified on device: texture() faults on // the second frame every run, textureLod survives with identical state. outColor = textureLod(uSource, vTexCoord, 0.0); } )"; static constexpr const char* kDepthMipmapFragmentShaderSource = R"(#version 460 core layout(binding = 0) uniform sampler2D uSource; layout(location = 0) in vec2 vTexCoord; uniform ivec2 uSrcTexelSize; void main() { ivec2 srcBase = ivec2(vTexCoord * vec2(uSrcTexelSize)); ivec2 srcMax = uSrcTexelSize - ivec2(1); float depth0 = texelFetch(uSource, clamp(srcBase, ivec2(0), srcMax), 0).r; float depth1 = texelFetch(uSource, clamp(srcBase + ivec2(1, 0), ivec2(0), srcMax), 0).r; float depth2 = texelFetch(uSource, clamp(srcBase + ivec2(0, 1), ivec2(0), srcMax), 0).r; float depth3 = texelFetch(uSource, clamp(srcBase + ivec2(1, 1), ivec2(0), srcMax), 0).r; gl_FragDepth = 0.25 * (depth0 + depth1 + depth2 + depth3); } )"; #endif // !MOBILEGL_BUILD_DISAGGREGATED static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) { Int maxDimension = std::max( baseTexelSize.x(), std::max(baseTexelSize.y(), std::max(baseTexelSize.z(), 1))); Uint32 mipLevelCount = 1; while (maxDimension > 1) { maxDimension = std::max(maxDimension / 2, 1); ++mipLevelCount; } return mipLevelCount; } static IntVec3 ComputeMipTexelSize(const IntVec3& baseTexelSize, Uint32 relativeMipLevel) { const Int width = std::max(baseTexelSize.x() >> static_cast(relativeMipLevel), 1); const Int height = std::max(baseTexelSize.y() >> static_cast(relativeMipLevel), 1); const Int depth = std::max(baseTexelSize.z() >> static_cast(relativeMipLevel), 1); return {width, height, depth}; } // How many components of a GL-space texel size actually halve down the mip chain. An array // texture's LAYER count is not a dimension of the image (GL 4.6 core 8.14.3): it stays put // all the way down, and GetMipmapTexelSize parks it in the slot after the image's own // dimensions. This is the same split IsMipmapCompleteForFilter applies, and the two have to // agree - allocating a chain whose layer count shrinks builds levels the completeness rule // then rejects. Vulkan-space extents need none of this: layers live in arrayLayers there, // so resource->depth is already 1 for every array target. static Int MipShrinkingComponentCount(TextureTarget target) { switch (target) { case TextureTarget::Texture1DArray: return 1; case TextureTarget::Texture2DArray: case TextureTarget::TextureCubeMapArray: return 2; default: return 3; } } static IntVec3 ComputeMipTexelSizeWithFixedComponents(const IntVec3& baseTexelSize, Uint32 relativeMipLevel, Int shrinkingComponents) { IntVec3 size = baseTexelSize; for (Int component = 0; component < shrinkingComponents && component < 3; ++component) { size[component] = std::max(size[component] >> static_cast(relativeMipLevel), 1); } return size; } // glGenerateMipmap defines levels BASE_LEVEL+1 up to the level the base image's extent and // MAX_LEVEL admit, and leaves every other level exactly as it was (GL 4.6 core 8.17). That // window is a property of the GL call rather than of the backend, so the caller hands in // the frontend's own plan (MG_Impl/GLImpl/Texture/MipmapGenerationPlan.h) and this defines // exactly those levels - neither a chain derived here nor a truncation of the tail. // // IMMUTABLE storage - glTexStorage*'s chain, and ANY view, which GL 4.6 core 8.18 makes // immutable from birth - already owns every level generation can write, so none may be // redefined here. Allocating through a view is destructive in a way the view's own extents // cannot describe: its base extent is ONE slice of the storage's layers, so AllocateStorage // through a two-layer view of a four-layer array resizes the storage owner's level to two // layers and the storage behind its other two is gone. static Bool EnsureGenerateMipmapStorageAllocated(::MobileGL::MG_State::GLState::TextureObjectMipmap& texture, Uint32 baseMipLevel, Uint32 endMipLevel) { const Uint32 existingMipLevelCount = static_cast(texture.GetMipmapLevelCount()); if (existingMipLevelCount <= baseMipLevel) { return false; } const auto& uploadTargets = texture.GetUploadTargets(); if (uploadTargets.empty()) { return false; } if (texture.IsImmutable()) { return true; } const Int shrinkingComponents = MipShrinkingComponentCount(texture.GetTarget()); for (const auto uploadTarget : uploadTargets) { const IntVec3 baseTexelSize = texture.GetMipmapTexelSize(uploadTarget, baseMipLevel); const SizeT baseByteSize = texture.GetMipmapByteSize(uploadTarget, baseMipLevel); if (baseTexelSize.x() <= 0 || baseTexelSize.y() <= 0 || baseTexelSize.z() <= 0 || baseByteSize == 0) { return false; } const SizeT baseTexelCount = static_cast(baseTexelSize.x()) * static_cast(baseTexelSize.y()) * static_cast(baseTexelSize.z()); if (baseTexelCount == 0 || (baseByteSize % baseTexelCount) != 0) { return false; } const SizeT bytesPerTexel = baseByteSize / baseTexelCount; // AllocateStorage RESIZES the level it is handed, so only levels the chain does not // have yet are allocated: the ones inside the window that already exist - the // levels a mutable chain defined by hand, or a previous generate built - keep the // shadow bytes their own upload path wrote. const Uint32 firstMissingMipLevel = std::max(baseMipLevel + 1, existingMipLevelCount); for (Uint32 level = firstMissingMipLevel; level < endMipLevel; ++level) { const IntVec3 levelTexelSize = ComputeMipTexelSizeWithFixedComponents( baseTexelSize, level - baseMipLevel, shrinkingComponents); const SizeT levelByteSize = bytesPerTexel * static_cast(levelTexelSize.x()) * static_cast(levelTexelSize.y()) * static_cast(levelTexelSize.z()); texture.AllocateStorage(uploadTarget, level, {levelTexelSize, levelByteSize}); texture.MarkStorageDirty(uploadTarget, level, false); } } return true; } #if MOBILEGL_BUILD_DISAGGREGATED // P5c (T5 / tx): the split arm of EnsureGenerateMipmapStorageAllocated. Under an active // transport the apply thread may not WRITE the client's level storage - AllocateStorage // and MarkStorageDirty on a frontend TextureObjectMipmap are ยง6 layer-1 surfaces // (CONTRACT-P5C ยง2.3) - so the generated chain is defined on the SERVER's staged-texture // shadow instead, keyed by this renderer's own texture twin (the TextureResource, // node-stable in VkTextureManager's map). Same levels, same extents - derived from the // Vulkan-space base extent, whose depth is already 1 for every array target (layers // live in arrayLayers there, so the fixed-component split the GL-space derivation // needs is unnecessary here; these shadow extents answer in Vulkan space, which every // consumer of a Magma-keyed entry shares) - and every generated level is marked // dirty-in-shadow, because its texels are generated on the GPU and no byte answer // exists on this side. The client's chain is left stale, which ยง2.3 rules CORRECT: the // two readers that could observe the staleness are both named refusals under split. // The upload-target list still comes from the frontend object - a shape READ, the P7 // registry's residual, not one of the writes this arm exists to remove. static Bool EnsureGenerateMipmapShadowAllocated(const VkTextureManager::TextureResource& resource, Uint32 baseMipLevel, const Vector& uploadTargets) { if (resource.mipLevels <= baseMipLevel || uploadTargets.empty()) { return false; } const IntVec3 storageBaseTexelSize = {static_cast(resource.extent.width), static_cast(resource.extent.height), static_cast(resource.depth)}; const IntVec3 baseTexelSize = ComputeMipTexelSize(storageBaseTexelSize, baseMipLevel); if (baseTexelSize.x() <= 0 || baseTexelSize.y() <= 0 || baseTexelSize.z() <= 0) { return false; } const Uint32 requiredMipLevelCount = baseMipLevel + ComputeFullMipLevelCount(baseTexelSize); auto& store = MG_Remote::Server::ServerStagedTexture(); const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForTwinAddress(&resource); for (const auto uploadTarget : uploadTargets) { for (Uint32 level = baseMipLevel + 1; level < requiredMipLevelCount; ++level) { // Bytes the shadow held for a generated level go: the declaration below moves its // format and byte bound (NoteLevelDefined clears them); the GPU copy is newer. // PH-4: a twin-address key never receives a staged run (no Adopt names one), // so these generated levels are declared BYTELESS - an Unknown format gives a // zero byte bound and skips the Tex2D device-limit rule the defaults would // apply to a 2D-array or 3D level's depth. store.NoteLevelDefined(key, static_cast(uploadTarget), static_cast(level), ComputeMipTexelSize(storageBaseTexelSize, level), static_cast(MG_Pipe::MGPipeResourceTarget::Tex2D), static_cast(TextureInternalFormat::Unknown)); store.MarkLevelGpuDirty(key, static_cast(uploadTarget), static_cast(level), true); } } return true; } #endif static VkImageLayout ResolveGenerateMipmapFinalLayout(VkImageAspectFlags aspectMask) { return (aspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0 ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; } static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) { return target >= TextureUploadTarget::CubeMapPositiveX && target <= TextureUploadTarget::CubeMapNegativeZ; } static Uint32 ResolveAttachmentBaseArrayLayer(const MG_State::GLState::FramebufferAttachmentObject& attachment) { const TextureUploadTarget uploadTarget = attachment.GetTextureUploadTarget(); if (IsCubeMapFaceUploadTarget(uploadTarget)) { // The face index IS the layer index, so it takes the same view shift as one that // arrived through GetTextureLayer (see ToStorageArrayLayer). const Int face = static_cast(uploadTarget) - static_cast(TextureUploadTarget::CubeMapPositiveX); return ToStorageArrayLayer(attachment.GetTexture().get(), face); } // Every other layered attachment names its layer directly. Returning 0 regardless made // every blit, copy and ReadPixels against such an attachment read layer zero. return ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer()); } // A 3D image has arrayLayers == 1: its "layer" is a z slice, which has to travel as an // image offset rather than a base array layer (VkBufferImageCopy requires baseArrayLayer 0 // for VK_IMAGE_TYPE_3D). static Bool AttachmentIsDepthSlice(const MG_State::GLState::FramebufferAttachmentObject& attachment) { return attachment.IsTexture() && attachment.GetTexture() && attachment.GetTexture()->GetTarget() == TextureTarget::Texture3D; } enum class BlitSurfaceTransform : Uint32 { Identity = 0, Rotate90 = 1, Rotate180 = 2, Rotate270 = 3, }; struct BlitImageBinding { VkImage image = VK_NULL_HANDLE; VkImageLayout* trackedLayout = nullptr; VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_NONE; VkFormat format = VK_FORMAT_UNDEFINED; VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT; IntVec2 extent = {0, 0}; Uint32 mipLevel = 0; Uint32 mipLevelCount = 1; Uint32 baseArrayLayer = 0; Uint32 layerCount = 1; // z slice for a VK_IMAGE_TYPE_3D source; array attachments use baseArrayLayer instead. Uint32 depthOffset = 0; const char* label = nullptr; }; static Uint32 ComputeMaxProgramBindings(const VkPhysicalDeviceProperties& properties, const ProgramFactory::UpdateAfterBindLimits& updateAfterBindLimits) { const auto& limits = properties.limits; static constexpr Uint32 kMinProgramBindings = 16; static constexpr Uint32 kMaxProgramBindingsCap = 256; const Uint32 maxCombinedImageSamplers = std::min(limits.maxPerStageDescriptorSamplers, limits.maxDescriptorSetSamplers); const Uint32 maxSampledImages = std::min(limits.maxPerStageDescriptorSampledImages, limits.maxDescriptorSetSampledImages); const Uint32 maxDynamicUniformBuffers = std::min(limits.maxPerStageDescriptorUniformBuffers, limits.maxDescriptorSetUniformBuffersDynamic); Uint32 maxBindings = limits.maxPerStageResources; maxBindings = std::min(maxBindings, maxCombinedImageSamplers); maxBindings = std::min(maxBindings, maxSampledImages + maxDynamicUniformBuffers); if (updateAfterBindLimits.enabled) { const Uint32 updateAfterBindSamplers = std::min(updateAfterBindLimits.maxPerStageSamplers, updateAfterBindLimits.maxSetSamplers); const Uint32 updateAfterBindSampledImages = std::min(updateAfterBindLimits.maxPerStageSampledImages, updateAfterBindLimits.maxSetSampledImages); const Uint32 updateAfterBindDynamicUniformBuffers = std::min(updateAfterBindLimits.maxPerStageUniformBuffers, updateAfterBindLimits.maxSetUniformBuffersDynamic); Uint32 updateAfterBindBindings = updateAfterBindLimits.maxPerStageResources; updateAfterBindBindings = std::min(updateAfterBindBindings, updateAfterBindSamplers); updateAfterBindBindings = std::min(updateAfterBindBindings, updateAfterBindSampledImages + updateAfterBindDynamicUniformBuffers); maxBindings = std::max(maxBindings, updateAfterBindBindings); } maxBindings = std::max(kMinProgramBindings, maxBindings); maxBindings = std::min(kMaxProgramBindingsCap, maxBindings); return maxBindings; } 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; } } static void GetImageTransitionDestinationState(VkImageLayout newLayout, VkPipelineStageFlags& outDstStageMask, VkAccessFlags& outDstAccessMask) { switch (newLayout) { case VK_IMAGE_LAYOUT_UNDEFINED: outDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; outDstAccessMask = 0; break; case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: outDstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; outDstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; break; case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: outDstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; outDstAccessMask = 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: case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: outDstStageMask = VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT; outDstAccessMask = VK_ACCESS_SHADER_READ_BIT; break; case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: outDstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT; outDstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; break; case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: outDstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT; outDstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; break; case VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: outDstStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; outDstAccessMask = VK_ACCESS_MEMORY_READ_BIT; break; default: outDstStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; outDstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; break; } } static VkImageAspectFlags GetSwapchainDepthStencilAspectMask(const SwapchainObject& swapchainObject) { VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; switch (swapchainObject.GetDepthStencilFormat()) { case VK_FORMAT_D24_UNORM_S8_UINT: case VK_FORMAT_D32_SFLOAT_S8_UINT: aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT; break; default: break; } return aspectMask; } static FramebufferAttachmentType ResolveFramebufferCopyAttachmentType( const MG_State::GLState::FramebufferObject& fbo, Bool isReadFramebuffer, VkImageAspectFlags aspectMask) { if ((aspectMask & VK_IMAGE_ASPECT_COLOR_BIT) != 0) { return isReadFramebuffer ? fbo.GetReadBuffer() : fbo.GetDrawBuffers()[0]; } if ((aspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) { return FramebufferAttachmentType::Depth; } if ((aspectMask & VK_IMAGE_ASPECT_STENCIL_BIT) != 0) { return FramebufferAttachmentType::Stencil; } return FramebufferAttachmentType::None; } static Bool ResolveColorBlitBinding(MG_State::GLState::FramebufferObject& fbo, Bool isReadFramebuffer, Uint32 swapchainImageIndex, SwapchainObject& swapchainObject, VkTextureManager& textureManager, VkRenderPassManager& renderPassManager, BlitImageBinding& outBinding) { const Bool isDefaultFbo = fbo.IsDefaultFramebuffer(); const FramebufferAttachmentType attachmentType = isReadFramebuffer ? fbo.GetReadBuffer() : fbo.GetDrawBuffers()[0]; outBinding.label = isReadFramebuffer ? "read" : "draw"; if (isDefaultFbo) { const Bool defaultColorAttachment = attachmentType == FramebufferAttachmentType::Color0 || (attachmentType >= FramebufferAttachmentType::FrontLeft && attachmentType <= FramebufferAttachmentType::BackRight); if (!defaultColorAttachment) { MGLOG_E_ONCE("BlitFramebuffer skipped: default framebuffer color attachment %d is not supported", static_cast(attachmentType)); return false; } outBinding.image = swapchainObject.GetImage(swapchainImageIndex); outBinding.trackedLayout = nullptr; outBinding.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; outBinding.format = swapchainObject.GetSurfaceFormat().format; const auto extent = swapchainObject.GetExtent(); outBinding.extent = {static_cast(extent.width), static_cast(extent.height)}; outBinding.mipLevel = 0; outBinding.mipLevelCount = 1; outBinding.baseArrayLayer = 0; outBinding.layerCount = 1; return true; } if (attachmentType < FramebufferAttachmentType::Color0 || attachmentType > FramebufferAttachmentType::Color31) { MGLOG_E_ONCE("BlitFramebuffer only supports color attachments right now (attachment=%d)", static_cast(attachmentType)); return false; } const auto& attachment = fbo.GetAttachment(attachmentType); if (!attachment.IsComplete()) { MGLOG_E_ONCE("BlitFramebuffer skipped: %s framebuffer color attachment is incomplete", isReadFramebuffer ? "read" : "draw"); return false; } if (attachment.IsRenderbuffer()) { const auto& renderbuffer = attachment.GetRenderbuffer(); auto* rbResource = renderPassManager.GetOrCreateRenderbufferResource(renderbuffer); if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) { MGLOG_E_ONCE("BlitFramebuffer skipped: %s framebuffer color renderbuffer %u is unsupported", outBinding.label, renderbuffer->GetExternalIndex()); return false; } outBinding.image = rbResource->image; outBinding.trackedLayout = &rbResource->layout; outBinding.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; outBinding.format = rbResource->format; outBinding.sampleCount = rbResource->sampleCount; outBinding.extent = {static_cast(rbResource->extent.width), static_cast(rbResource->extent.height)}; outBinding.mipLevel = 0; outBinding.mipLevelCount = 1; outBinding.baseArrayLayer = 0; outBinding.layerCount = 1; return true; } if (!attachment.IsTexture()) { MGLOG_E_ONCE("BlitFramebuffer skipped: unsupported framebuffer attachment type"); return false; } auto* texture = attachment.GetTexture().get(); MOBILEGL_ASSERT(texture != nullptr, "ResolveColorBlitBinding: texture attachment is null"); auto* resource = textureManager.SyncTextureAndGetDescriptor(*texture); if (resource == nullptr) { MGLOG_E_ONCE("BlitFramebuffer skipped: failed to sync %s framebuffer textureId=%d", outBinding.label, texture->GetExternalIndex()); return false; } if ((resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) { MGLOG_E_ONCE("BlitFramebuffer skipped: %s framebuffer attachment textureId=%d is not a color image", outBinding.label, texture->GetExternalIndex()); return false; } outBinding.image = resource->image; outBinding.trackedLayout = &resource->layout; outBinding.aspectMask = resource->aspect; outBinding.format = resource->format; outBinding.sampleCount = resource->sampleCount; const auto attachmentExtent = attachment.GetSize(); outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()}; outBinding.mipLevel = ToStorageMipLevel(attachment.GetTexture().get(), attachment.GetTextureLevel()); outBinding.mipLevelCount = resource->mipLevels; if (AttachmentIsDepthSlice(attachment)) { outBinding.depthOffset = ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer()); outBinding.baseArrayLayer = 0; } else { outBinding.baseArrayLayer = ResolveAttachmentBaseArrayLayer(attachment); } outBinding.layerCount = 1; return true; } static Bool ResolveFramebufferBlitBinding(MG_State::GLState::FramebufferObject& fbo, Bool isReadFramebuffer, Uint32 swapchainImageIndex, SwapchainObject& swapchainObject, VkTextureManager& textureManager, VkRenderPassManager& renderPassManager, VkImageAspectFlags requiredAspectMask, BlitImageBinding& outBinding) { const Bool isDefaultFbo = fbo.IsDefaultFramebuffer(); const auto attachmentType = ResolveFramebufferCopyAttachmentType(fbo, isReadFramebuffer, requiredAspectMask); if (attachmentType == FramebufferAttachmentType::None) { MGLOG_E_ONCE("BlitFramebuffer skipped: unsupported aspect mask=0x%x", static_cast(requiredAspectMask)); return false; } outBinding.label = isReadFramebuffer ? "read" : "draw"; if (isDefaultFbo) { const auto extent = swapchainObject.GetExtent(); outBinding.extent = {static_cast(extent.width), static_cast(extent.height)}; outBinding.mipLevel = 0; outBinding.mipLevelCount = 1; outBinding.baseArrayLayer = 0; outBinding.layerCount = 1; outBinding.trackedLayout = nullptr; if ((requiredAspectMask & VK_IMAGE_ASPECT_COLOR_BIT) != 0) { outBinding.image = swapchainObject.GetImage(swapchainImageIndex); outBinding.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; return true; } const VkImageAspectFlags swapchainAspectMask = GetSwapchainDepthStencilAspectMask(swapchainObject); if ((swapchainAspectMask & requiredAspectMask) != requiredAspectMask) { MGLOG_E_ONCE("BlitFramebuffer skipped: swapchain depth image missing required aspect mask=0x%x", static_cast(requiredAspectMask)); return false; } outBinding.image = swapchainObject.GetDepthStencilImage(swapchainImageIndex); outBinding.format = swapchainObject.GetDepthStencilFormat(); outBinding.aspectMask = requiredAspectMask; return true; } const auto& attachment = fbo.GetAttachment(attachmentType); if (!attachment.IsComplete()) { MGLOG_E_ONCE("BlitFramebuffer skipped: %s framebuffer attachment is incomplete (fbo=%u attachmentType=%d " "isTexture=%d isRenderbuffer=%d texId=%d)", outBinding.label, fbo.GetExternalIndex(), static_cast(attachmentType), attachment.IsTexture() ? 1 : 0, attachment.IsRenderbuffer() ? 1 : 0, attachment.IsTexture() && attachment.GetTexture() ? static_cast(attachment.GetTexture()->GetExternalIndex()) : -1); return false; } if (attachment.IsRenderbuffer()) { const auto& renderbuffer = attachment.GetRenderbuffer(); auto* rbResource = renderPassManager.GetOrCreateRenderbufferResource(renderbuffer); if (rbResource == nullptr) { MGLOG_E_ONCE("BlitFramebuffer skipped: %s framebuffer renderbuffer %u is unsupported", outBinding.label, renderbuffer->GetExternalIndex()); return false; } if ((rbResource->aspect & requiredAspectMask) != requiredAspectMask) { MGLOG_E_ONCE("BlitFramebuffer skipped: %s framebuffer renderbuffer %u is missing aspect mask=0x%x", outBinding.label, renderbuffer->GetExternalIndex(), static_cast(requiredAspectMask)); return false; } outBinding.image = rbResource->image; outBinding.trackedLayout = &rbResource->layout; outBinding.aspectMask = requiredAspectMask; outBinding.format = rbResource->format; outBinding.sampleCount = rbResource->sampleCount; outBinding.extent = {static_cast(rbResource->extent.width), static_cast(rbResource->extent.height)}; outBinding.mipLevel = 0; outBinding.mipLevelCount = 1; outBinding.baseArrayLayer = 0; outBinding.layerCount = 1; return true; } if (!attachment.IsTexture()) { MGLOG_E_ONCE("BlitFramebuffer skipped: unsupported framebuffer attachment type"); return false; } auto* texture = attachment.GetTexture().get(); MOBILEGL_ASSERT(texture != nullptr, "ResolveFramebufferBlitBinding: texture attachment is null"); auto* resource = textureManager.SyncTextureAndGetDescriptor(*texture); if (resource == nullptr) { MGLOG_E_ONCE("BlitFramebuffer skipped: failed to sync %s framebuffer textureId=%d", outBinding.label, texture->GetExternalIndex()); return false; } if ((resource->aspect & requiredAspectMask) != requiredAspectMask) { MGLOG_E_ONCE("BlitFramebuffer skipped: %s framebuffer attachment textureId=%d is missing aspect mask=0x%x", outBinding.label, texture->GetExternalIndex(), static_cast(requiredAspectMask)); return false; } outBinding.image = resource->image; outBinding.trackedLayout = &resource->layout; outBinding.aspectMask = requiredAspectMask; outBinding.format = resource->format; outBinding.sampleCount = resource->sampleCount; const auto attachmentExtent = attachment.GetSize(); outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()}; outBinding.mipLevel = ToStorageMipLevel(attachment.GetTexture().get(), attachment.GetTextureLevel()); outBinding.mipLevelCount = resource->mipLevels; if (AttachmentIsDepthSlice(attachment)) { outBinding.depthOffset = ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer()); outBinding.baseArrayLayer = 0; } else { outBinding.baseArrayLayer = ResolveAttachmentBaseArrayLayer(attachment); } outBinding.layerCount = 1; return true; } static Bool ResolveTextureCopyDestinationBinding(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel, VkTextureManager& textureManager, BlitImageBinding& outBinding) { auto* resource = textureManager.SyncTextureAndGetDescriptor(texture); if (resource == nullptr) { MGLOG_E_ONCE("CopyTexSubImage2D skipped: failed to sync destination textureId=%d", texture.GetExternalIndex()); return false; } const VkImageAspectFlags copyAspectMask = resource->aspect & (VK_IMAGE_ASPECT_COLOR_BIT | VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT); if (copyAspectMask == 0) { MGLOG_E_ONCE("CopyTexSubImage2D skipped: destination textureId=%d uses unsupported aspect mask=0x%x", texture.GetExternalIndex()); return false; } if (mipLevel >= resource->mipLevels) { MGLOG_E_ONCE("CopyTexSubImage2D skipped: destination textureId=%d mip=%u out of range (mips=%u)", texture.GetExternalIndex(), mipLevel, resource->mipLevels); return false; } outBinding.image = resource->image; outBinding.trackedLayout = &resource->layout; outBinding.aspectMask = copyAspectMask; outBinding.extent = { static_cast(std::max(1u, resource->extent.width >> mipLevel)), static_cast(std::max(1u, resource->extent.height >> mipLevel))}; outBinding.mipLevel = mipLevel; outBinding.mipLevelCount = 1; outBinding.baseArrayLayer = 0; outBinding.layerCount = 1; outBinding.label = "destination texture"; return true; } static Bool ResolveTextureCopySourceBinding(MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, SwapchainObject& swapchainObject, VkTextureManager& textureManager, VkRenderPassManager& renderPassManager, VkImageAspectFlags requiredAspectMask, BlitImageBinding& outBinding) { const Bool isDefaultFbo = fbo.IsDefaultFramebuffer(); const auto attachmentType = ResolveFramebufferCopyAttachmentType(fbo, true, requiredAspectMask); if (attachmentType == FramebufferAttachmentType::None) { MGLOG_E_ONCE("CopyTexSubImage2D skipped: unsupported source aspect mask=0x%x", static_cast(requiredAspectMask)); return false; } outBinding.label = "read"; if (isDefaultFbo) { const auto extent = swapchainObject.GetExtent(); outBinding.extent = {static_cast(extent.width), static_cast(extent.height)}; outBinding.mipLevel = 0; outBinding.mipLevelCount = 1; outBinding.baseArrayLayer = 0; outBinding.layerCount = 1; outBinding.trackedLayout = nullptr; if ((requiredAspectMask & VK_IMAGE_ASPECT_COLOR_BIT) != 0) { outBinding.image = swapchainObject.GetImage(swapchainImageIndex); outBinding.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; return true; } const VkImageAspectFlags swapchainAspectMask = GetSwapchainDepthStencilAspectMask(swapchainObject); if ((swapchainAspectMask & requiredAspectMask) != requiredAspectMask) { MGLOG_E_ONCE("CopyTexSubImage2D skipped: swapchain depth image missing required aspect mask=0x%x", static_cast(requiredAspectMask)); return false; } outBinding.image = swapchainObject.GetDepthStencilImage(swapchainImageIndex); outBinding.aspectMask = requiredAspectMask; return true; } const auto& attachment = fbo.GetAttachment(attachmentType); if (!attachment.IsComplete()) { MGLOG_E_ONCE("CopyTexSubImage2D skipped: read framebuffer attachment %d is incomplete", static_cast(attachmentType)); return false; } if (attachment.IsRenderbuffer()) { const auto& renderbuffer = attachment.GetRenderbuffer(); auto* rbResource = renderPassManager.GetOrCreateRenderbufferResource(renderbuffer); if (rbResource == nullptr) { MGLOG_E_ONCE("CopyTexSubImage2D skipped: read framebuffer renderbuffer %u is unsupported", renderbuffer->GetExternalIndex()); return false; } if ((rbResource->aspect & requiredAspectMask) != requiredAspectMask) { MGLOG_E_ONCE("CopyTexSubImage2D skipped: read framebuffer renderbuffer %u aspect mask=0x%x " "does not satisfy requested mask=0x%x", renderbuffer->GetExternalIndex(), static_cast(rbResource->aspect), static_cast(requiredAspectMask)); return false; } outBinding.image = rbResource->image; outBinding.trackedLayout = &rbResource->layout; outBinding.aspectMask = requiredAspectMask; outBinding.format = rbResource->format; outBinding.sampleCount = rbResource->sampleCount; outBinding.extent = {static_cast(rbResource->extent.width), static_cast(rbResource->extent.height)}; outBinding.mipLevel = 0; outBinding.mipLevelCount = 1; outBinding.baseArrayLayer = 0; outBinding.layerCount = 1; return true; } if (!attachment.IsTexture()) { MGLOG_E_ONCE("CopyTexSubImage2D skipped: unsupported read framebuffer attachment type"); return false; } auto* texture = attachment.GetTexture().get(); MOBILEGL_ASSERT(texture != nullptr, "ResolveTextureCopySourceBinding: source texture attachment is null"); auto* resource = textureManager.SyncTextureAndGetDescriptor(*texture); if (resource == nullptr) { MGLOG_E_ONCE("CopyTexSubImage2D skipped: failed to sync read framebuffer textureId=%d", texture->GetExternalIndex()); return false; } if ((resource->aspect & requiredAspectMask) != requiredAspectMask) { MGLOG_E_ONCE("CopyTexSubImage2D skipped: read framebuffer textureId=%d aspect mask=0x%x does not satisfy requested mask=0x%x", texture->GetExternalIndex(), static_cast(resource->aspect), static_cast(requiredAspectMask)); return false; } outBinding.image = resource->image; outBinding.trackedLayout = &resource->layout; outBinding.aspectMask = requiredAspectMask; outBinding.format = resource->format; outBinding.sampleCount = resource->sampleCount; const auto attachmentExtent = attachment.GetSize(); outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()}; outBinding.mipLevel = ToStorageMipLevel(attachment.GetTexture().get(), attachment.GetTextureLevel()); outBinding.mipLevelCount = 1; if (AttachmentIsDepthSlice(attachment)) { outBinding.depthOffset = ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer()); outBinding.baseArrayLayer = 0; } else { outBinding.baseArrayLayer = ResolveAttachmentBaseArrayLayer(attachment); } outBinding.layerCount = 1; return true; } 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; } } static Bool RequiresShaderBlitToDefaultFramebuffer(VkSurfaceTransformFlagBitsKHR preTransform) { switch (preTransform) { case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR: case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR: return true; default: return false; } } static void ApplyNativeBlitDefaultFramebufferTransform(VkSurfaceTransformFlagBitsKHR preTransform, const BlitImageBinding& dstBinding, VkImageBlit& blitRegion) { switch (preTransform) { case VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR: blitRegion.dstOffsets[0].y = dstBinding.extent.y() - blitRegion.dstOffsets[0].y; blitRegion.dstOffsets[1].y = dstBinding.extent.y() - blitRegion.dstOffsets[1].y; break; case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR: blitRegion.dstOffsets[0].x = dstBinding.extent.x() - blitRegion.dstOffsets[0].x; blitRegion.dstOffsets[1].x = dstBinding.extent.x() - blitRegion.dstOffsets[1].x; break; default: break; } } // The same conversion on the READ side, which never had one: a blit whose source is the // default framebuffer used raw GL offsets against a display-oriented image, so it sampled // the mirrored band and wrote it upside down. Mapping BOTH endpoints inverts the offset // pair, and an inverted pair is exactly how VkImageBlit spells "flip this axis" - so the // band and the row order are corrected in one step. A full-extent blit is unchanged in // band and gains the row flip it always needed. static void ApplyNativeBlitDefaultFramebufferSourceTransform(VkSurfaceTransformFlagBitsKHR preTransform, const BlitImageBinding& srcBinding, VkImageBlit& blitRegion) { switch (preTransform) { case VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR: blitRegion.srcOffsets[0].y = srcBinding.extent.y() - blitRegion.srcOffsets[0].y; blitRegion.srcOffsets[1].y = srcBinding.extent.y() - blitRegion.srcOffsets[1].y; break; case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR: blitRegion.srcOffsets[0].x = srcBinding.extent.x() - blitRegion.srcOffsets[0].x; blitRegion.srcOffsets[1].x = srcBinding.extent.x() - blitRegion.srcOffsets[1].x; break; default: break; } } static Bool DecodeReadbackPixel(const Uint8* source, VkFormat sourceFormat, Float* rgba) { switch (sourceFormat) { case VK_FORMAT_R8G8B8A8_UNORM: case VK_FORMAT_R8G8B8A8_SRGB: rgba[0] = static_cast(source[0]) / 255.0f; rgba[1] = static_cast(source[1]) / 255.0f; rgba[2] = static_cast(source[2]) / 255.0f; rgba[3] = static_cast(source[3]) / 255.0f; return true; case VK_FORMAT_B8G8R8A8_UNORM: case VK_FORMAT_B8G8R8A8_SRGB: rgba[0] = static_cast(source[2]) / 255.0f; rgba[1] = static_cast(source[1]) / 255.0f; rgba[2] = static_cast(source[0]) / 255.0f; rgba[3] = static_cast(source[3]) / 255.0f; return true; case VK_FORMAT_R16G16B16A16_UNORM: for (SizeT component = 0; component < 4; ++component) { Uint16 value = 0; Memcpy(&value, source + component * sizeof(value), sizeof(value)); rgba[component] = static_cast(value) / 65535.0f; } return true; case VK_FORMAT_R16G16B16A16_SFLOAT: for (SizeT component = 0; component < 4; ++component) { Uint16 value = 0; Memcpy(&value, source + component * sizeof(value), sizeof(value)); rgba[component] = MG_Util::DecodeHalfBitsToFloat(value); } return true; case VK_FORMAT_R32G32B32A32_SFLOAT: Memcpy(rgba, source, sizeof(Float) * 4); return true; // Single- and dual-channel formats the reinterpretation feature makes common // as readback sources (iterationRP custom images are R32F/R32UI-class). // Missing channels take GL's defaults: 0 for GB, 1 for alpha. case VK_FORMAT_R32_SFLOAT: { Float value = 0.0f; Memcpy(&value, source, sizeof(value)); rgba[0] = value; rgba[1] = 0.0f; rgba[2] = 0.0f; rgba[3] = 1.0f; return true; } case VK_FORMAT_R32G32_SFLOAT: { Float values[2] = {0.0f, 0.0f}; Memcpy(values, source, sizeof(values)); rgba[0] = values[0]; rgba[1] = values[1]; rgba[2] = 0.0f; rgba[3] = 1.0f; return true; } case VK_FORMAT_R32_UINT: { Uint32 value = 0; Memcpy(&value, source, sizeof(value)); rgba[0] = static_cast(value); rgba[1] = 0.0f; rgba[2] = 0.0f; rgba[3] = 1.0f; return true; } case VK_FORMAT_R32_SINT: { Int32 value = 0; Memcpy(&value, source, sizeof(value)); rgba[0] = static_cast(value); rgba[1] = 0.0f; rgba[2] = 0.0f; rgba[3] = 1.0f; return true; } case VK_FORMAT_R16_SFLOAT: { Uint16 value = 0; Memcpy(&value, source, sizeof(value)); rgba[0] = MG_Util::DecodeHalfBitsToFloat(value); rgba[1] = 0.0f; rgba[2] = 0.0f; rgba[3] = 1.0f; return true; } case VK_FORMAT_R16G16_SFLOAT: for (SizeT component = 0; component < 2; ++component) { Uint16 value = 0; Memcpy(&value, source + component * sizeof(value), sizeof(value)); rgba[component] = MG_Util::DecodeHalfBitsToFloat(value); } rgba[2] = 0.0f; rgba[3] = 1.0f; return true; default: return false; } } static Uint8 EncodeReadbackUnorm8(Float value) { if (!(value > 0.0f)) { return 0; } if (value >= 1.0f) { return 255; } return static_cast(value * 255.0f + 0.5f); } static SizeT AlignPixelRow(SizeT rowBytes, Int alignment) { const SizeT resolvedAlignment = static_cast(std::max(alignment, 1)); return (rowBytes + resolvedAlignment - 1) & ~(resolvedAlignment - 1); } static Int GetReadbackChannelCount(GLenum format) { switch (format) { case GL_RGB: case GL_BGR: return 3; case GL_RGBA: case GL_BGRA: return 4; default: return 0; } } static void StoreReadbackPixel(const Float* rgba, GLenum dstFormat, Uint8* dst) { const Uint8 r = EncodeReadbackUnorm8(rgba[0]); const Uint8 g = EncodeReadbackUnorm8(rgba[1]); const Uint8 b = EncodeReadbackUnorm8(rgba[2]); const Uint8 a = EncodeReadbackUnorm8(rgba[3]); switch (dstFormat) { case GL_RGB: dst[0] = r; dst[1] = g; dst[2] = b; break; case GL_BGR: dst[0] = b; dst[1] = g; dst[2] = r; break; case GL_RGBA: dst[0] = r; dst[1] = g; dst[2] = b; dst[3] = a; break; case GL_BGRA: dst[0] = b; dst[1] = g; dst[2] = r; dst[3] = a; break; default: break; } } static void StoreReadbackPixelFloat(const Float* rgba, GLenum dstFormat, Float* dst) { const Float r = rgba[0]; const Float g = rgba[1]; const Float b = rgba[2]; const Float a = rgba[3]; switch (dstFormat) { case GL_RGB: dst[0] = r; dst[1] = g; dst[2] = b; break; case GL_BGR: dst[0] = b; dst[1] = g; dst[2] = r; break; case GL_RGBA: dst[0] = r; dst[1] = g; dst[2] = b; dst[3] = a; break; case GL_BGRA: dst[0] = b; dst[1] = g; dst[2] = r; dst[3] = a; break; default: break; } } // 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(mantissa) / 64.0f * std::pow(2.0f, -14.0f); } if (exponent == 31) { return mantissa == 0 ? std::numeric_limits::infinity() : std::numeric_limits::quiet_NaN(); } return (1.0f + static_cast(mantissa) / 64.0f) * std::pow(2.0f, static_cast(static_cast(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(mantissa) / 32.0f * std::pow(2.0f, -14.0f); } if (exponent == 31) { return mantissa == 0 ? std::numeric_limits::infinity() : std::numeric_limits::quiet_NaN(); } return (1.0f + static_cast(mantissa) / 32.0f) * std::pow(2.0f, static_cast(static_cast(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(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(channels) * sizeof(Float)); return; } case VK_FORMAT_A2B10G10R10_UNORM_PACK32: { Uint32 word = 0; Memcpy(&word, source, sizeof(word)); rgba[0] = static_cast(word & 0x3FFu) / 1023.0f; rgba[1] = static_cast((word >> 10) & 0x3FFu) / 1023.0f; rgba[2] = static_cast((word >> 20) & 0x3FFu) / 1023.0f; rgba[3] = static_cast((word >> 30) & 0x3u) / 3.0f; return; } case VK_FORMAT_A2R10G10B10_UNORM_PACK32: { Uint32 word = 0; Memcpy(&word, source, sizeof(word)); rgba[2] = static_cast(word & 0x3FFu) / 1023.0f; rgba[1] = static_cast((word >> 10) & 0x3FFu) / 1023.0f; rgba[0] = static_cast((word >> 20) & 0x3FFu) / 1023.0f; rgba[3] = static_cast((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((word >> 27) & 0x1Fu) - 15 - 9; const Float scale = std::pow(2.0f, static_cast(exponent)); rgba[0] = static_cast(word & 0x1FFu) * scale; rgba[1] = static_cast((word >> 9) & 0x1FFu) * scale; rgba[2] = static_cast((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((word >> 11) & 0x1Fu) / 31.0f; const Float c1 = static_cast((word >> 5) & 0x3Fu) / 63.0f; const Float c2 = static_cast(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((word >> 15) & 0x1u); rgba[0] = static_cast((word >> 10) & 0x1Fu) / 31.0f; rgba[1] = static_cast((word >> 5) & 0x1Fu) / 31.0f; rgba[2] = static_cast(word & 0x1Fu) / 31.0f; return; } case VK_FORMAT_R5G5B5A1_UNORM_PACK16: { Uint16 word = 0; Memcpy(&word, source, sizeof(word)); rgba[0] = static_cast((word >> 11) & 0x1Fu) / 31.0f; rgba[1] = static_cast((word >> 6) & 0x1Fu) / 31.0f; rgba[2] = static_cast((word >> 1) & 0x1Fu) / 31.0f; rgba[3] = static_cast(word & 0x1u); return; } case VK_FORMAT_B5G5R5A1_UNORM_PACK16: { Uint16 word = 0; Memcpy(&word, source, sizeof(word)); rgba[2] = static_cast((word >> 11) & 0x1Fu) / 31.0f; rgba[1] = static_cast((word >> 6) & 0x1Fu) / 31.0f; rgba[0] = static_cast((word >> 1) & 0x1Fu) / 31.0f; rgba[3] = static_cast(word & 0x1u); return; } case VK_FORMAT_R4G4B4A4_UNORM_PACK16: { Uint16 word = 0; Memcpy(&word, source, sizeof(word)); rgba[0] = static_cast((word >> 12) & 0xFu) / 15.0f; rgba[1] = static_cast((word >> 8) & 0xFu) / 15.0f; rgba[2] = static_cast((word >> 4) & 0xFu) / 15.0f; rgba[3] = static_cast(word & 0xFu) / 15.0f; return; } case VK_FORMAT_B4G4R4A4_UNORM_PACK16: { Uint16 word = 0; Memcpy(&word, source, sizeof(word)); rgba[2] = static_cast((word >> 12) & 0xFu) / 15.0f; rgba[1] = static_cast((word >> 8) & 0xFu) / 15.0f; rgba[0] = static_cast((word >> 4) & 0xFu) / 15.0f; rgba[3] = static_cast(word & 0xFu) / 15.0f; return; } default: return; } } static Bool DecodeReadbackRowsToWide(const Uint8* srcPixels, VkFormat srcFormat, GLsizei width, GLsizei height, Vector& 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(width) * static_cast(height); outWide.assign(pixelCount * 4 * sizeof(Uint32), 0); if (desc.sourceClass == ReadbackSourceClass::Float) { outWideType = GL_FLOAT; Float* wide = reinterpret_cast(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(raw) / 127.0f, -1.0f); } else { value = static_cast(source[c]) / 255.0f; } } else { // 16 if (desc.isSnorm) { Int16 raw = 0; Memcpy(&raw, source + static_cast(c) * 2, sizeof(raw)); value = std::max(static_cast(raw) / 32767.0f, -1.0f); } else { Uint16 raw = 0; Memcpy(&raw, source + static_cast(c) * 2, sizeof(raw)); value = static_cast(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(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(static_cast(raw)); } else { rgba[c] = source[c]; } } else if (desc.componentBits == 16) { if (desc.sourceClass == ReadbackSourceClass::SignedInt) { Int16 raw = 0; Memcpy(&raw, source + static_cast(c) * 2, sizeof(raw)); rgba[c] = static_cast(static_cast(raw)); } else { Uint16 raw = 0; Memcpy(&raw, source + static_cast(c) * 2, sizeof(raw)); rgba[c] = raw; } } else { Memcpy(&rgba[c], source + static_cast(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; } DirectGLES::ReadbackImpl::ReadbackChannelMapping mapping{}; if (!DirectGLES::ReadbackImpl::GetReadbackChannelMapping(format, mapping) || DirectGLES::ReadbackImpl::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 DirectGLES::ReadbackImpl::StorePackedWordsToClient(srcPixels, width, sliceHeight, sliceCount, type, pixels, applyPackImageParams); } Vector 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(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 = MGB_CTX->GetClampReadColor(); const Bool clamp = clampMode == GL_TRUE || (clampMode == GL_FIXED_ONLY && !IsFloatingPointReadbackFormat(srcFormat)); if (clamp) { Float* values = reinterpret_cast(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(srcFormat)); return false; } return DirectGLES::ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, sliceHeight, sliceCount, mapping, type, pixels, applyPackImageParams); } } // namespace SizeT VulkanRenderer::GetReadbackTexelSize(VkFormat sourceFormat) { const VKU_FORMAT_INFO formatInfo = vkuGetFormatInfo(sourceFormat); if (formatInfo.texels_per_block != 1) { return 0; } return formatInfo.texel_block_size; } Bool VulkanRenderer::MapDefaultFramebufferReadbackRect( GLint x, GLint y, GLsizei width, GLsizei height, VkExtent2D imageExtent, VkSurfaceTransformFlagBitsKHR preTransform, VkOffset2D* imageOffset, VkExtent2D* imageCopyExtent) { if (width <= 0 || height <= 0 || imageOffset == nullptr || imageCopyExtent == nullptr) { return false; } const Int imageWidth = static_cast(imageExtent.width); const Int imageHeight = static_cast(imageExtent.height); Int mappedX = x; Int mappedY = y; Uint32 mappedWidth = static_cast(width); Uint32 mappedHeight = static_cast(height); // InsertPositionFixup first flips GL Y and then applies the surface transform. In pixel // coordinates that gives these half-open rectangle mappings into the stored image: // identity: (x, H-y-h), 90: (y, x), 180: (W-x-w, y), 270: (H-y-h, W-x-w). // Quarter turns also transpose the copied block's extent. switch (preTransform) { case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR: mappedX = y; mappedY = x; mappedWidth = static_cast(height); mappedHeight = static_cast(width); break; case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR: mappedX = imageWidth - x - width; mappedY = y; break; case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR: mappedX = imageWidth - y - height; mappedY = imageHeight - x - width; mappedWidth = static_cast(height); mappedHeight = static_cast(width); break; default: mappedY = imageHeight - y - height; break; } if (mappedX < 0 || mappedY < 0 || mappedWidth > imageExtent.width || mappedHeight > imageExtent.height || static_cast(mappedX) + mappedWidth > imageExtent.width || static_cast(mappedY) + mappedHeight > imageExtent.height) { return false; } *imageOffset = {mappedX, mappedY}; *imageCopyExtent = {mappedWidth, mappedHeight}; return true; } Bool VulkanRenderer::RemapDefaultFramebufferReadback( const Uint8* rawPixels, Uint32 logicalWidth, Uint32 logicalHeight, VkSurfaceTransformFlagBitsKHR preTransform, SizeT texelSize, Uint8* outPixels) { if (rawPixels == nullptr || outPixels == nullptr || logicalWidth == 0 || logicalHeight == 0 || texelSize == 0) { return false; } const Uint32 rawWidth = IsQuarterTurnPreTransform(preTransform) ? logicalHeight : logicalWidth; for (Uint32 outY = 0; outY < logicalHeight; ++outY) { for (Uint32 outX = 0; outX < logicalWidth; ++outX) { Uint32 srcX = outX; Uint32 srcY = outY; switch (preTransform) { case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR: srcX = outY; srcY = outX; break; case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR: srcX = logicalWidth - 1 - outX; break; case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR: srcX = logicalHeight - 1 - outY; srcY = logicalWidth - 1 - outX; break; default: srcY = logicalHeight - 1 - outY; break; } Memcpy(outPixels + (static_cast(outY) * logicalWidth + outX) * texelSize, rawPixels + (static_cast(srcY) * rawWidth + srcX) * texelSize, texelSize); } } return true; } Bool VulkanRenderer::ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat, GLsizei width, GLsizei height, GLenum destinationFormat, GLenum destinationType, SizeT destinationRowStride, Uint8* destinationPixels) { if (width <= 0 || height <= 0) { return true; } if (sourcePixels == nullptr || destinationPixels == nullptr) { return false; } const SizeT sourceTexelSize = GetReadbackTexelSize(sourceFormat); const Int destinationChannels = GetReadbackChannelCount(destinationFormat); if (sourceTexelSize == 0 || destinationChannels == 0 || (destinationType != GL_UNSIGNED_BYTE && destinationType != GL_FLOAT)) { return false; } const SizeT destinationComponentSize = destinationType == GL_FLOAT ? sizeof(Float) : sizeof(Uint8); const SizeT destinationPixelSize = static_cast(destinationChannels) * destinationComponentSize; if (destinationRowStride < static_cast(width) * destinationPixelSize) { return false; } for (GLsizei row = 0; row < height; ++row) { const Uint8* sourceRow = sourcePixels + static_cast(row) * static_cast(width) * sourceTexelSize; Uint8* destinationRow = destinationPixels + static_cast(row) * destinationRowStride; for (GLsizei column = 0; column < width; ++column) { const Uint8* source = sourceRow + static_cast(column) * sourceTexelSize; Uint8* destination = destinationRow + static_cast(column) * destinationPixelSize; Float rgba[4]{}; if (!DecodeReadbackPixel(source, sourceFormat, rgba)) { return false; } if (destinationType == GL_FLOAT) { Float converted[4]{}; StoreReadbackPixelFloat(rgba, destinationFormat, converted); Memcpy(destination, converted, destinationPixelSize); } else { StoreReadbackPixel(rgba, destinationFormat, destination); } } } return true; } VkBool32 VulkanRenderer::DebugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageType, const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) { auto typeToString = [](VkDebugUtilsMessageTypeFlagsEXT messageType) { switch (messageType) { case VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT: return "General"; case VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT: return "Validation"; case VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT: return "Performance"; case VK_DEBUG_UTILS_MESSAGE_TYPE_DEVICE_ADDRESS_BINDING_BIT_EXT: return "DeviceAddressBinding"; default: return "Other"; } }; switch (messageSeverity) { case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT: MGLOG_E_ONCE("Vulkan Debug: [%s] %s", typeToString(messageType), pCallbackData->pMessage); break; case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT: MGLOG_W_ONCE("Vulkan Debug: [%s] %s", typeToString(messageType), pCallbackData->pMessage); break; case VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT: MGLOG_D("Vulkan Debug: [%s] %s", typeToString(messageType), pCallbackData->pMessage); break; case VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT: MGLOG_D("Vulkan Debug: [%s] %s", typeToString(messageType), pCallbackData->pMessage); break; default: break; } return VK_FALSE; } VulkanRenderer::VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg) : m_window(window), m_config(cfg) { // Initialize(); } VulkanRenderer::~VulkanRenderer() { Shutdown(); } inline ProgramFactory::CompileOptionFlags GetShaderTransformFlags(VkSurfaceTransformFlagBitsKHR preTransform) { ProgramFactory::CompileOptionFlags flags = ProgramFactory::CompileOptionBit::PositionZRemap; #if MOBILEGL_BUILD_DISAGGREGATED const Bool wire = MG_Config::Transport != MG_Config::TransportMode::Monolith; const auto* wireFbo = wire ? MG_Pipe::MGPipeApplier().DrawFramebuffer() : nullptr; const auto currentDrawFBO = wire ? SharedPtr{} : MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); const Bool isDefault = wire ? (wireFbo && wireFbo->IsDefault) : (currentDrawFBO && currentDrawFBO->IsDefaultFramebuffer()); #else const auto& currentDrawFBO = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); const Bool isDefault = currentDrawFBO != nullptr && currentDrawFBO->IsDefaultFramebuffer(); #endif if (isDefault) { flags |= ProgramFactory::CompileOptionBit::PositionYFlip; // gl_FragCoord follows the same rule the default-framebuffer RECTANGLES follow // (GetDefaultFramebufferRectMapping): flipped for identity/180, left alone under a // quarter turn, which this renderer converts nothing for. Keeping the two in step // is the whole point - a fragment's window Y and the viewport that placed it must // agree on which end of the image they count from. if (!IsQuarterTurnPreTransform(preTransform)) { flags |= ProgramFactory::CompileOptionBit::FragCoordYFlip; } switch (preTransform) { case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR: flags |= ProgramFactory::CompileOptionBit::SurfaceRotate90; break; case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR: flags |= ProgramFactory::CompileOptionBit::SurfaceRotate180; break; case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR: flags |= ProgramFactory::CompileOptionBit::SurfaceRotate270; break; default: break; } } return flags; } void VulkanRenderer::Initialize() { #if MOBILEGL_PIPE_PUSH // P2 D14, and it belongs HERE rather than on a draw: "a Track-H subsystem whose bit is // clear is a STARTUP Fatal{PipeLegacyMemosDisabled}". Checks Magma's own bit only, and // only once this backend is the one being brought up, so an Espryt-side bitmask cannot // kill a Magma run and vice versa. MagmaPipeValidateSubsystemConfiguration(); #endif CreateInstance(); CreateSurface(); PickPhysicalDevice(); CreateLogicalDeviceAndQueues(); CreateAllocator(); CreateCommandPool(); // Frames-in-flight is a request, not a guarantee: it also seeds the swapchain image // count (SwapchainObject clamps the hint into [minImageCount, maxImageCount]). Not every // driver/surface supports >= 3 swapchain images, and keeping more frame slots than the // surface can present would leave the surplus slots stalling on vkAcquireNextImageKHR. // So clamp to the surface's real limits here, before any per-frame resource is sized off // it. (The standalone driver POST is headless and has no surface, so this check lives at // renderer init.) Existing logs already report the swapchain's min/actual image count; // this one adds the frames-in-flight decision itself. { // Desired depth comes from MOBILEGL_MAGMA_FRAMESINFLIGHT, parsed once by ConfigLoader // with a default of 3 when the variable is unset or invalid. Uint32 requestedFramesInFlight = MG_Config::Features.MagmaFramesInFlight; MGLOG_I("MaxFramesInFlight: configured request=%u", requestedFramesInFlight); VkSurfaceCapabilitiesKHR surfaceCaps{}; const VkResult capsResult = vkGetPhysicalDeviceSurfaceCapabilitiesKHR( m_physicalDevice.handle, m_surface, &surfaceCaps); if (capsResult != VK_SUCCESS) { MGLOG_W("MaxFramesInFlight: vkGetPhysicalDeviceSurfaceCapabilitiesKHR failed (VkResult=%d); " "keeping requested %u", static_cast(capsResult), requestedFramesInFlight); } else { // Frames-in-flight is the CPU pipeline depth; it only needs to stay <= the number // of swapchain images the surface can provide (maxImageCount), so the extra slots // never stall on vkAcquireNextImageKHR. It must NOT be forced up to minImageCount: // the swapchain independently gets >= minImageCount images (SwapchainObject raises // the count), and inflating the CPU depth would only add latency + memory. Uint32 chosenFramesInFlight = requestedFramesInFlight; if (surfaceCaps.maxImageCount != 0 && chosenFramesInFlight > surfaceCaps.maxImageCount) { chosenFramesInFlight = surfaceCaps.maxImageCount; // 0 == no upper bound } if (chosenFramesInFlight < 2) { chosenFramesInFlight = 2; // never drop below double buffering } m_config.MaxFramesInFlight = chosenFramesInFlight; if (chosenFramesInFlight != requestedFramesInFlight) { MGLOG_W("MaxFramesInFlight: requested %u unsupported by surface (minImageCount=%u, " "maxImageCount=%u); using %u", requestedFramesInFlight, surfaceCaps.minImageCount, surfaceCaps.maxImageCount, chosenFramesInFlight); } else { MGLOG_I("MaxFramesInFlight: using %u (surface minImageCount=%u, maxImageCount=%u)", chosenFramesInFlight, surfaceCaps.minImageCount, surfaceCaps.maxImageCount); } } } VK_VERIFY(m_frameContext.Initialize(m_device, m_commandPool, m_config.MaxFramesInFlight), "CreateFrameContexts"); MGLOG_I("CreateFrameContexts completed"); auto succeeded = false; succeeded = m_bufferManager.Initialize({ .allocator = m_allocator, .frameCount = m_frameContext.GetFrameCount(), .minUploadBytes = 4 * 1024 * 1024, .transientMemoryUsage = VMA_MEMORY_USAGE_AUTO, .transientAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT, .transientPersistentMapping = true, .transformFeedbackUsageEnabled = m_transformFeedbackFeatureEnabled, }); MOBILEGL_ASSERT(succeeded, "VkBufferManager initialization failed."); m_bufferManager.SetCopyCommandProvider(this); if (m_timerQuerySupported) { m_timerQueryManager = MakeUnique(); if (m_timerQueryManager->Initialize({.device = m_device, .frameCount = m_frameContext.GetFrameCount(), .timestampValidBits = m_timestampValidBits, .timestampPeriodNs = m_timestampPeriodNs})) { m_frameContext.SetRecordingObserver(this); } else { MGLOG_W("VkTimerQueryManager initialization failed; timer queries disabled"); m_timerQueryManager.reset(); m_timerQuerySupported = false; } } m_textureManager = MakeUnique(); MOBILEGL_ASSERT(m_textureManager != nullptr, "VkTextureManager creation failed."); succeeded = m_textureManager->Initialize( {m_device, m_physicalDevice.handle, m_allocator, m_commandPool, m_graphicsQueue, m_frameContext.GetFrameCount(), m_imageFormatListExtensionEnabled, m_sampledReadStageMask, static_cast(m_physicalDevice.queueFamilies.graphicsFamily)}); MOBILEGL_ASSERT(succeeded, "VkTextureManager initialization failed."); m_clearManager = MakeUnique(); MOBILEGL_ASSERT(m_clearManager != nullptr, "VkClearManager creation failed."); succeeded = m_clearManager->Initialize(); MOBILEGL_ASSERT(succeeded, "VkClearManager initialization failed."); m_renderPassManager = MakeUnique(m_device, m_physicalDevice.handle, m_allocator, m_config, *m_clearManager, *m_textureManager, m_swapchainObject); MOBILEGL_ASSERT(m_renderPassManager != nullptr, "VkRenderPassManager creation failed."); succeeded = m_renderPassManager->Initialize(); MOBILEGL_ASSERT(succeeded, "VkRenderPassManager initialization failed."); const Uint32 maxProgramBindings = ComputeMaxProgramBindings(m_physicalDevice.properties, m_updateAfterBindLimits); MGLOG_I("DirectVulkan: using %u program descriptor bindings", maxProgramBindings); if (IsPowerVRDevice(m_physicalDevice.properties)) { m_config.DisablePipelineCache = true; MGLOG_W("DirectVulkan: disabling pipeline cache on PowerVR device %s", m_physicalDevice.properties.deviceName); } RecreateSwapchain(); m_pipelineFactory = MakeUnique(m_device, m_config); MOBILEGL_ASSERT(m_pipelineFactory != nullptr, "PipelineFactory creation failed."); { // Qualcomm's pipeline compiler does not keep vertex positions invariant across // the pipelines of a multi-pass depth-equality chain (even with the SPIR-V // Invariant decoration), so a blended depth-writing prepass makes later // equality-compare passes drop whole primitives (MC 26.3 improved-transparency // clouds flicker black). Suppress depth writes on accumulation-blended pipelines // there (see PipelineFactory::ShouldSuppressDepthWrite for the exact scope); // MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE forces the quirk on or off on any // driver. const MG_Config::QuirkOverride quirkOverride = MG_Config::Features.MagmaDisableBlendedDepthWriteQuirk; const Bool suppressBlendedDepthWrite = PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice( quirkOverride, m_physicalDevice.properties.vendorID); if (suppressBlendedDepthWrite) { MGLOG_I("DirectVulkan: suppressing depth writes on accumulation-blended pipelines " "(driver lacks cross-pipeline position invariance)%s", quirkOverride == MG_Config::QuirkOverride::ForceOn ? " (forced on)" : ""); } PipelineFactory::SetSuppressBlendedDepthWrite(suppressBlendedDepthWrite); } ProgramFactory::SubgroupLoweringPolicy subgroupPolicy{}; subgroupPolicy.emulateSubgroups = ShouldEmulateSubgroups(m_nativeSubgroupSupported); subgroupPolicy.fixIterationRPSubgroupScratch = m_nativeSubgroupSupported && ShouldFixIterationRPSubgroupScratch(); subgroupPolicy.fixIterationRPBarrier = ShouldFixIterationRPBarrier(); subgroupPolicy.deriveNumSubgroups = m_nativeSubgroupSupported && ShouldDeriveNumSubgroups(); subgroupPolicy.requireFullSubgroups = m_computeFullSubgroupsFeatureEnabled; subgroupPolicy.nativeSubgroupSize = m_nativeSubgroupSize; subgroupPolicy.maxComputeWorkgroupSubgroups = m_maxComputeWorkgroupSubgroups; subgroupPolicy.maxComputeSharedMemoryBytes = m_physicalDevice.properties.limits.maxComputeSharedMemorySize; m_programFactory = MakeUnique(m_device, m_config, maxProgramBindings, m_shaderDrawParametersFeatureEnabled, m_unformattedFloatStorageImagesEnabled, m_tessellationAndGeometryPointSizeFeatureEnabled, MG_Config::Features.EnableSpirvValidation, m_updateAfterBindLimits, subgroupPolicy); MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed."); // The swapchain already exists at this point (Initialize creates it first), so seed the // height the factory could not be told about from CreateSwapchain. m_programFactory->SetDefaultFramebufferHeight(m_swapchainObject.GetExtent().height); // Aging evictions (render passes and program entries) must purge the dependent // pipeline / compute-pipeline / descriptor-set caches in the same step; both // sweeps only run from the frame-boundary seams, long after initialization. m_renderPassManager->SetEvictionObserver(this); m_programFactory->SetEvictionObserver(this); m_samplerManager = MakeUnique(); MOBILEGL_ASSERT(m_samplerManager != nullptr, "VkSamplerManager creation failed."); succeeded = m_samplerManager->Initialize({m_device, &m_config, m_samplerAnisotropyFeatureEnabled, m_physicalDevice.properties.limits.maxSamplerAnisotropy, m_customBorderColorFeatureEnabled, m_maxCustomBorderColorSamplers}); MOBILEGL_ASSERT(succeeded, "VkSamplerManager initialization failed."); succeeded = InitializeBlitResources(); MOBILEGL_ASSERT(succeeded, "Blit pipeline resource initialization failed."); succeeded = InitializeDepthMipmapResources(); MOBILEGL_ASSERT(succeeded, "Depth mipmap pipeline resource initialization failed."); m_uniformManager = MakeUnique(); MOBILEGL_ASSERT(m_uniformManager != nullptr, "UniformDescriptorBinder creation failed."); succeeded = m_uniformManager->Initialize( m_device, m_physicalDevice.handle, &m_bufferManager, m_programFactory.get(), m_physicalDevice.properties.limits.minUniformBufferOffsetAlignment, m_config.MaxFramesInFlight, maxProgramBindings, kDescriptorSetsPerFrame, m_textureManager.get(), m_samplerManager.get()); MOBILEGL_ASSERT(succeeded, "UniformDescriptorBinder initialization failed."); #if MOBILEGL_PIPE_PUSH m_vertexInputStateFactory = MakeUnique(m_config, m_physicalDevice.handle, m_pipeIdentity); #else m_vertexInputStateFactory = MakeUnique(m_config, m_physicalDevice.handle); #endif MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed."); // Prime the first frame so Render() always targets an acquired swapchain image. // A zero-area window (GLFW's hidden helper window during the WGL bootstrap, or a // window that is already minimized) legitimately yields no swapchain here; defer // the first acquire to Present in that case instead of acquiring from a null // swapchain handle. if (m_swapchainObject.GetHandle() != VK_NULL_HANDLE) { VkResult acquireResult = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); if (acquireResult == VK_ERROR_OUT_OF_DATE_KHR) { // Nothing was acquired and no semaphore signal was armed, so // rebuilding and re-acquiring on the same semaphore is safe. MGLOG_D("Initialize, vkAcquireNextImageKHR got %d, recreating swapchain", acquireResult); RecreateSwapchain(); acquireResult = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); } else if (acquireResult == VK_SUBOPTIMAL_KHR) { // The image is usable, and its acquire signal is already armed on // imageAvailableSemaphore. Re-acquiring here would arm a second signal on a // binary semaphore whose first one nobody has waited on yet; keep the image. // Only a real surface change schedules a rebuild. m_swapchainResizeRequested = m_swapchainResizeRequested || SwapchainIsOutOfDate(); acquireResult = VK_SUCCESS; } VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage"); // The first acquired image is the one the opening frame renders into, so the // default framebuffer addresses it from the start (see // m_defaultFramebufferImageIndex). m_defaultFramebufferImageIndex = m_imageIndexAcquired; } else { MGLOG_W("DirectVulkan: no swapchain at initialization (zero-area window); deferring first acquire"); } m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_convertedVertexStreams.clear(); MGLOG_D("VulkanRenderer initialized"); } void VulkanRenderer::Shutdown() { if (m_instance == VK_NULL_HANDLE && m_device == VK_NULL_HANDLE && m_surface == VK_NULL_HANDLE) { return; } if (m_device != VK_NULL_HANDLE) { VK_VERIFY(vkDeviceWaitIdle(m_device)); } #if MOBILEGL_BUILD_DISAGGREGATED // P7 wave 2 package B3: the session's decline tally. Silent when nothing declined, so a // green lane stays quiet and a lane that dropped a draw cannot (rule I's observable). WireDeclineTally::Dump("shutdown"); DestroyWireDrawPass(); CollectWireObjects(m_submitCounter, true); ClearAllWireDrawPassCaches(); DestroyWireColorBlitResources(); DestroyWireDepthMipmapResources(); DestroyWireMultisampleResolveResources(); #endif OnSubmitsCompletedUpTo(m_submitCounter); DestroySubmitFencePool(); DestroyDeferredDepthMipmapCleanup(); DestroyMultisampleResolveScratchImage(); DestroyComputePipelines(); // No sweep runs during teardown, but the observers point at this renderer // and the factories die at different times below; disconnect them first. if (m_renderPassManager) { m_renderPassManager->SetEvictionObserver(nullptr); } if (m_programFactory) { m_programFactory->SetEvictionObserver(nullptr); } m_pipelineFactory.reset(); ShutdownBlitResources(); ShutdownDepthMipmapResources(); if (m_samplerManager) { m_samplerManager->Shutdown(); m_samplerManager.reset(); } if (m_textureManager) { m_textureManager->Shutdown(); m_textureManager.reset(); } m_vertexInputStateFactory.reset(); m_xfbCounterBuffer.Destroy(); m_xfbCounterSlotOwner.fill(0); m_xfbCounterSlotLastUse.fill(0); m_xfbCounterSlotUseSerial = 0; m_xfbCountersValid.fill(false); m_xfbLastSeenGeneration.fill(0); if (m_occlusionQueryPool != VK_NULL_HANDLE) { vkDestroyQueryPool(m_device, m_occlusionQueryPool, nullptr); m_occlusionQueryPool = VK_NULL_HANDLE; } if (m_xfbQueryPool != VK_NULL_HANDLE) { vkDestroyQueryPool(m_device, m_xfbQueryPool, nullptr); m_xfbQueryPool = VK_NULL_HANDLE; } if (m_primGenReroutePool != VK_NULL_HANDLE) { vkDestroyQueryPool(m_device, m_primGenReroutePool, nullptr); m_primGenReroutePool = VK_NULL_HANDLE; } m_primGenRerouteActiveSlots.clear(); m_primGenRerouteSlotCursor = 0; m_primGenRerouteSlotOpen = false; // Not sticky across renderers: the next bring-up re-decides both (from the // per-process probe memo, so it re-decides without re-probing). m_primGenRerouteKind = MG_Util::SelfTest::PrimGenRerouteKind::None; m_primGenStreamCountsXfbInactiveDraws = false; m_bufferManager.Shutdown(); // Device is idle (vkDeviceWaitIdle above); query pools can be destroyed. m_frameContext.SetRecordingObserver(nullptr); if (m_timerQueryManager) { m_timerQueryManager->Shutdown(); m_timerQueryManager.reset(); } if (m_device != VK_NULL_HANDLE) { m_frameContext.Destroy(m_device, m_commandPool); } if (m_uniformManager) { m_uniformManager->Shutdown(); m_uniformManager.reset(); } m_programFactory.reset(); if (m_renderPassManager) { ShutdownSwapchain(); } else if (m_device != VK_NULL_HANDLE) { m_swapchainObject.Shutdown(m_device); } m_renderPassManager.reset(); if (m_clearManager) { m_clearManager->Shutdown(); m_clearManager.reset(); } if (m_commandPool != VK_NULL_HANDLE) { vkDestroyCommandPool(m_device, m_commandPool, nullptr); m_commandPool = VK_NULL_HANDLE; } DestroyAllocator(); if (m_device != VK_NULL_HANDLE) { vkDestroyDevice(m_device, nullptr); m_device = VK_NULL_HANDLE; } s_vkCmdDrawIndexedIndirectCount = nullptr; s_vkCmdDrawMultiEXT = nullptr; s_vkCmdDrawMultiIndexedEXT = nullptr; if (m_instance != VK_NULL_HANDLE && m_surface != VK_NULL_HANDLE) { vkDestroySurfaceKHR(m_instance, m_surface, nullptr); m_surface = VK_NULL_HANDLE; } #if defined(VK_USE_PLATFORM_METAL_EXT) if (m_platformLibrary != nullptr) { Release(reinterpret_cast(m_platformLibrary)); m_platformLibrary = nullptr; } if (m_platformDisplay != nullptr) { Release(reinterpret_cast(m_platformDisplay)); m_platformDisplay = nullptr; } #endif #if defined(VK_USE_PLATFORM_XLIB_KHR) if (m_platformDisplay != nullptr) { // No fallback window to destroy any more: the display here is only ever // one this renderer opened for a REAL window surface, and that window is // the caller's to own. The hidden-window pbuffer fallback that used to be // cleaned up here is gone (see CreateSurface). using XCloseDisplayFn = int (*)(Display*); auto* closeDisplay = reinterpret_cast(m_platformCloseDisplay); if (closeDisplay) { closeDisplay(static_cast(m_platformDisplay)); } m_platformDisplay = nullptr; } m_platformCloseDisplay = nullptr; if (m_platformLibrary != nullptr) { dlclose(m_platformLibrary); m_platformLibrary = nullptr; } #endif #if defined(VK_USE_PLATFORM_ANDROID_KHR) // The AImageReader owns the ANativeWindow the pbuffer fallback handed to the // WSI, so it outlives the surface and is released only here. if (m_fallbackImageReader != nullptr && m_platformLibrary != nullptr) { using AImageReaderDeleteFn = void (*)(void*); auto* imageReaderDelete = reinterpret_cast(dlsym(m_platformLibrary, "AImageReader_delete")); if (imageReaderDelete) { imageReaderDelete(m_fallbackImageReader); } m_fallbackImageReader = nullptr; m_window = 0; dlclose(m_platformLibrary); m_platformLibrary = nullptr; } #endif if (m_debugMessenger != VK_NULL_HANDLE) { DestroyDebugMessenger(); m_debugMessenger = VK_NULL_HANDLE; } DestroyDebugReportCallback(); if (m_instance != VK_NULL_HANDLE) { vkDestroyInstance(m_instance, nullptr); m_instance = VK_NULL_HANDLE; } MGLOG_I("VulkanRenderer shut down completed"); } // Scans the draw's index range from host-visible index bytes and returns the largest // fetchable vertex index. Usable only when the draw's range is exactly its // IndexBufferView (drawParams.indexRangeIsExactView). The view's byte offset is either // an offset into the bound element-array buffer or, with no bound buffer, a raw client // pointer. Primitive-restart sentinels are skipped so they cannot inflate the bound. static Bool TryComputeMaxIndexFromHostBytes(const MG_State::GLState::VertexArrayObject& vao, const IndexBufferView& indexView, Uint32& outMaxIndex) { SizeT indexSize = 0; switch (indexView.indexType) { case GL_UNSIGNED_BYTE: indexSize = 1; break; case GL_UNSIGNED_SHORT: indexSize = 2; break; case GL_UNSIGNED_INT: indexSize = 4; break; default: return false; } const Uint8* indexBytes = nullptr; const auto& indexBufferShared = indexView.forceClientMemory ? SharedPtr{} : vao.GetIndexBufferBindingSlot().GetBoundObject(); if (indexBufferShared != nullptr) { const SizeT bufferSize = indexBufferShared->GetSize(); if (indexBufferShared->MappedData() == nullptr || indexView.indexByteOffset > bufferSize || indexView.indexByteSize > bufferSize - indexView.indexByteOffset) { return false; } // Recorded-but-unexecuted GPU writes (XFB capture, SSBO, storage texel // buffer) land in the coherent mapping this scan is about to read; // submit-and-wait first, exactly like the restart-index rewrite does. // A no-op unless the gpu-write flag is set. indexBufferShared->SyncGpuWrites(); indexBufferShared->SyncPersistentMappedRange(); indexBytes = indexBufferShared->MappedData() + indexView.indexByteOffset; } else { indexBytes = reinterpret_cast(indexView.indexByteOffset); if (indexBytes == nullptr) { return false; } } const SizeT indexCount = indexView.indexByteSize / indexSize; // The all-ones sentinel is only a restart marker when primitive restart is enabled; // with restart off it is a legitimate index and excluding it would truncate the // converted stream by exactly that vertex. const Bool primitiveRestartActive = MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) || MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex); const Uint32 restartSentinel = indexSize == 1 ? 0xFFu : indexSize == 2 ? 0xFFFFu : 0xFFFFFFFFu; Uint32 maxIndex = 0; Bool sawIndex = false; for (SizeT i = 0; i < indexCount; ++i) { Uint32 index = 0; switch (indexSize) { case 1: index = indexBytes[i]; break; case 2: index = reinterpret_cast(indexBytes)[i]; break; default: index = reinterpret_cast(indexBytes)[i]; break; } if (primitiveRestartActive && index == restartSentinel) { continue; } maxIndex = std::max(maxIndex, index); sawIndex = true; } if (!sawIndex) { return false; } outMaxIndex = maxIndex; return true; } Bool VulkanRenderer::TryBindResolvedVertexBindings( VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao, ResolvedVertexBindings& entry, Uint64 vaoContentHash, Uint32 activeAttribMask, Uint64 frameSerial) { // Layout + buffer identity in two loads from data the caller already has: the // VAO's content hash mixes every enabled attribute's format AND its bound // buffer's address (any change bumps the config version, invalidating the hash // memo the caller read), and the program's active-location mask fixes the // synthetic-binding set. Together they pin bindings.size(), every base offset // and which buffer each binding reads, so the hit path never has to resolve the // vertex-input factory entry at all - that chase was the dominant cost of a // VAO-cycling frame's memo hit. if (entry.frameSerial == 0 || entry.vertexInputHash != vaoContentHash || entry.activeAttribMask != activeAttribMask) { return false; } if (entry.frameSerial == frameSerial) { // What is left to establish is that the buffers still hand back the slices // recorded here, and that none of them is a host map whose shadow needs // pushing down. An unmoved manager-wide epoch counter says both. if (!entry.anyBufferMapped && entry.sliceEpochCounter == m_bufferManager.GetSliceEpochCounter()) { ShadowedBindVertexBuffers(commandBuffer, entry.vkBuffers, entry.vkOffsets, entry.bindingCount); return true; } // Something moved somewhere; ask the buffers themselves. const auto& attributes = vao.GetAllAttributes(); const MG_State::GLState::BufferObject* synced = nullptr; for (Uint32 binding = 0; binding < entry.bindingCount; ++binding) { auto* bufferObject = attributes[entry.attributeLocations[binding]].Buffer.get(); if (bufferObject != entry.buffers[binding]) { return false; } // What the resolving path does before every acquire: a persistent map the // backend could not adopt into coherent GPU storage mutates its shadow with // no API call, so the write range has to be pushed down here too. It is a // no-op for every buffer that is not such a map; when it is not, it dispatches // a SubData that retires the epoch below, and this draw resolves in full. if (bufferObject != synced) { bufferObject->SyncPersistentMappedRange(); synced = bufferObject; } const auto* resource = static_cast(bufferObject->GetBackendResource().get()); if (resource == nullptr || resource->sliceEpoch != entry.sliceEpochs[binding]) { return false; } } ShadowedBindVertexBuffers(commandBuffer, entry.vkBuffers, entry.vkOffsets, entry.bindingCount); return true; } // NO cross-frame trust: a memo recorded in an earlier frame declines here and // the draw re-resolves through the full acquire path. The epoch-compare // revalidation that used to sit here shipped visible corruption (journeymap / // common-mods retraces, vertex anomalies on Adreno): the acquire path is the // frame's content-sync point, and skipping it across frames trusted the // BumpSliceEpoch inventory to cover every way a buffer's GPU copy can go stale. // At least one path escapes it. Until that inventory is proven complete the // hot layout memo above (same-frame) keeps the factory-chase win, and the // first draw of each (VAO, frame) pays one full resolve. return false; } VulkanRenderer::VaoDrawMemo* VulkanRenderer::LookupVaoDrawMemo( const MG_State::GLState::VertexArrayObject* vao) { if (m_vaoDrawMemoTable.empty()) { m_vaoDrawMemoTable.resize(kVaoDrawMemoSlotCount); } #if MOBILEGL_PIPE_PUSH if (MagmaPipeAbaControlDefeatsIdentity()) { // Negative control C (P2 brief D18), ahead of BOTH arms because it defeats the // identity half of both keys at once: the legacy arm's (address, lifetime id) pair // and the handle arm's {slot, gen}. Every VAO lands on one entry and the entry is // handed back without an identity compare and WITHOUT being cleared - which is // exactly what this table would do if a replacement object reproduced its dead // predecessor's address, or reused its slot without the generation moving. // // Nothing else about the entry is relaxed: whether the resolved bindings it holds // are then USED is still decided by TryBindResolvedVertexBindings' frame serial, // content hash, active-attribute mask and slice epochs. That is what keeps the arm // an assertion about identity rather than about the memo as a whole. VaoDrawMemo& aliased = m_vaoDrawMemoTable[kMagmaPipeAbaControlSlotIndex]; aliased.vaoKey = vao; aliased.vaoLifetimeId = vao->GetLifetimeId(); return &aliased; } // ---- P2 D12.4, the handle arm ---- // // The slot PICKS the entry, and the handle DECIDES whether the entry is this VAO's - // the same division of labour the legacy arm below gives the address and the lifetime // id, with two differences that are both improvements: // // * the slot is dense from 1, so below kVaoDrawMemoSlotCount live slots the map is a // bijection and the two-way probe never collides at all, where an address hash // collides by the birthday rule from the first few dozen VAOs; // * the handle is an exact identity - Gen moves whenever a slot changes owner - so // neither a deleted VAO's successor at the same heap address nor a VAO whose slot // was recycled can match a predecessor's entry, even byte-identically configured. // That is what makes the lifetime-id half of the legacy compare unnecessary here. // // The capacity and the victim rule are deliberately the base ref's, unchanged: this is // the one memo of the three that HAD a capacity before P2, and an entry lost to a // collision costs exactly what it cost then (one vertex-binding re-resolve). Above // kVaoDrawMemoSlotCount live VAOs a set of two ways serves four slots, and degrades // from there - never worse than the address-hashed table it replaces, which was already // colliding. if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) { const MG_Pipe::MGPipeHandle handle = ResolveVaoHandle(*vao); const Uint32 index = MagmaPipeSlotIndex(handle) & (kVaoDrawMemoSlotCount - 1u); VaoDrawMemo& first = m_vaoDrawMemoTable[index]; if (first.vaoHandle == handle) { return &first; } VaoDrawMemo& second = m_vaoDrawMemoTable[index ^ 1u]; if (second.vaoHandle == handle) { return &second; } // Miss: recycle a slot. Prefer an unclaimed one; otherwise evict the entry whose // bindings memo is older (its VAO is the one drawn less recently). VaoDrawMemo* victim = &first; if (!MG_Pipe::MGPipeHandleIsNull(first.vaoHandle) && (MG_Pipe::MGPipeHandleIsNull(second.vaoHandle) || second.bindings.frameSerial < first.bindings.frameSerial)) { victim = &second; } victim->vaoHandle = handle; victim->vaoKey = vao; victim->vaoLifetimeId = vao->GetLifetimeId(); victim->contentHash = 0; victim->layoutFactsValid = false; // Unmatchable until a resolve completes (same rule as the legacy arm: a bailed-out // resolve must never leave stale contents matchable). victim->bindings.frameSerial = 0; victim->bindings.indexFrameSerial = 0; victim->bindings.indexBuffer = nullptr; return victim; } #endif // Multiplicative mix of the (16-byte-aligned) address; take high bits, they // carry the most entropy of a multiply. const Uint64 mixed = static_cast(reinterpret_cast(vao) >> 4) * 0x9E3779B97F4A7C15ull; const Uint32 index = static_cast(mixed >> 32) & (kVaoDrawMemoSlotCount - 1); // The address still picks the slot (it is what the caller has in hand), but it is // the lifetime id that decides whether the slot is THIS object's: an address on // its own is recycled, and a slot matched on a recycled address hands the new VAO // the dead one's resolved bindings. const Uint64 lifetimeId = vao->GetLifetimeId(); // Negative control C has NO consumer here. It is answered once, ahead of both arms, by // the early return above, so a run that reaches this line has the knob off and the // lifetime-id half of the compare is unconditional. A fourth consumer here would be a // second site deciding the same question - what MagmaPipeAbaControlDefeatsIdentity // exists to prevent - and a trap: narrow that early return later and this one would // silently return to D18's retired semantics. If it is ever narrowed, ask the accessor // here rather than re-reading MG_Config::Features. VaoDrawMemo& first = m_vaoDrawMemoTable[index]; if (first.vaoKey == vao && first.vaoLifetimeId == lifetimeId) { return &first; } VaoDrawMemo& second = m_vaoDrawMemoTable[index ^ 1u]; if (second.vaoKey == vao && second.vaoLifetimeId == lifetimeId) { return &second; } // Miss: recycle a slot. Prefer an empty one; otherwise evict the entry whose // bindings memo is older (its VAO is the one drawn less recently). VaoDrawMemo* victim = &first; if (first.vaoKey != nullptr && (second.vaoKey == nullptr || second.bindings.frameSerial < first.bindings.frameSerial)) { victim = &second; } victim->vaoKey = vao; victim->vaoLifetimeId = lifetimeId; victim->contentHash = 0; victim->layoutFactsValid = false; // Unmatchable until a resolve completes (same rule as before: a bailed-out // resolve must never leave stale contents matchable). victim->bindings.frameSerial = 0; victim->bindings.indexFrameSerial = 0; victim->bindings.indexBuffer = nullptr; return victim; } Bool VulkanRenderer::UploadAndBindVertexBuffers( VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao, const ProgramFactory::VkProgramObject& programObj, const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView) { static_assert(ResolvedVertexBindings::kMaxBindings == DynamicStateShadow::kMaxShadowedVertexBindings, "the resolved-binding memo is sized to what the bind shadow can compare"); const Bool indexedDraw = pIndexBufferView != nullptr; // Exclusive upper bound on the vertex-stream elements this draw can fetch through // vertex-rate bindings, or 0 when unbounded (indirect/multi draws). Computed lazily // because the index scan is only worth doing when a conversion actually needs it. SizeT drawElementBound = 0; Bool drawElementBoundComputed = false; auto resolveDrawElementBound = [&]() -> SizeT { if (drawElementBoundComputed) { return drawElementBound; } drawElementBoundComputed = true; if (!indexedDraw) { drawElementBound = static_cast(drawParams.firstVertex) + drawParams.vertexCount; } else if (drawParams.indexRangeIsExactView) { Uint32 maxIndex = 0; if (TryComputeMaxIndexFromHostBytes(vao, *pIndexBufferView, maxIndex)) { drawElementBound = static_cast(maxIndex) + 1 + static_cast(std::max(drawParams.baseVertex, 0)); } } return drawElementBound; }; const Uint32 activeAttribMask = programObj.activeVertexInputLocationMask; const Uint64 frameSerial = m_bufferManager.GetFrameSerial(); // Probe the memo BEFORE resolving the vertex-input entry: a hit needs nothing // from it (the VAO's own hash memo pins layout and buffers - see // TryBindResolvedVertexBindings), and skipping the resolve also skips its // per-draw cold chase into the factory's heap entry. The direct-mapped slot // lookup replaces the old pointer-keyed hash-map find, whose metadata and // key-storage probing was the dominant per-draw cost of a VAO-cycling frame. m_currentDrawResolvedEntry = nullptr; VaoDrawMemo* slot = nullptr; ResolvedVertexBindings* memo = nullptr; Uint64 vaoContentHash = 0; const Bool vaoHashKnown = VaoContentHashIfKnown(vao, vaoContentHash); if (vaoHashKnown) { slot = LookupVaoDrawMemo(&vao); memo = &slot->bindings; if (TryBindResolvedVertexBindings(commandBuffer, vao, *memo, vaoContentHash, activeAttribMask, frameSerial)) { m_currentDrawResolvedEntry = memo; return true; } // Whatever it described is stale; a resolve that bails out below must not // leave the old contents matchable either. memo->frameSerial = 0; } auto& vertexInputState = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao); if (slot == nullptr) { // First sight since a config change: the factory resolve just stamped the // VAO's hash memo, so the slot can be claimed (and the facts below stored) // for every later draw of this configuration. slot = LookupVaoDrawMemo(&vao); memo = &slot->bindings; memo->frameSerial = 0; } // Refresh the layout facts served to TrySetupDrawFastPath. Pure values derived // from the content hash, so this is correct even for layouts whose BINDINGS are // not memoisable (client arrays, conversions). slot->contentHash = vertexInputState.hash; slot->layoutHash = vertexInputState.layoutHash; slot->layoutAuxMasks = VertexInputStateFactory::PackVertexInputAuxMasks( vertexInputState.unsupportedAttribMask, vertexInputState.attributeLocationMask); slot->layoutFactsValid = true; const Uint32 vertexInputAttribMask = vertexInputState.attributeLocationMask; const Uint32 missingAttribMask = activeAttribMask & ~vertexInputAttribMask; const auto bindingCount = vertexInputState.bindings.size() + static_cast(std::popcount(missingAttribMask)); // Anything the memo cannot key on (see ResolvedVertexBindings) clears this as // the resolve below discovers it. Bool memoisable = missingAttribMask == 0 && bindingCount > 0 && bindingCount <= ResolvedVertexBindings::kMaxBindings; Bool anyBufferMapped = false; auto& vkBuffers = m_vertexBuffersScratch; auto& vkOffsets = m_vertexOffsetsScratch; vkBuffers.assign(bindingCount, VK_NULL_HANDLE); vkOffsets.assign(bindingCount, 0); auto uploadConvertedStream = [&](VertexInputStateFactory::VertexStreamConversion conversion, const MG_State::GLState::VertexAttribute& attribute, const Uint8* sourceData, SizeT sourceStride, SizeT elementSize, SizeT elementCount, BufferSlice& outSlice) -> Bool { // A resolved stride of 0 is the binding model's "never advance" (see the // factory's layout notes): exactly one element is converted and every vertex // reads it. That single element is read at offset 0, so the stride is never // actually used - but both converters reject 0 as a degenerate input, which // made the documented single-element conversion unreachable and silently // dropped every draw using such a binding. Substitute the element's own // size; the caller's cache key still carries the distinct stride 0. if (sourceStride == 0 && elementCount == 1) { sourceStride = elementSize; } const void* uploadData = nullptr; VkDeviceSize uploadSize = 0; switch (conversion) { case VertexInputStateFactory::VertexStreamConversion::Repack: if (!RepackVertexStream(sourceData, sourceStride, elementSize, elementCount, m_vertexRepackScratch)) { return false; } uploadData = m_vertexRepackScratch.data(); uploadSize = static_cast(m_vertexRepackScratch.size()); break; case VertexInputStateFactory::VertexStreamConversion::ScaledIntegerToFloat32: if (!ConvertScaledIntegerVertexStreamToFloat32(attribute, sourceData, sourceStride, elementCount, m_vertexConversionScratch)) { return false; } uploadData = m_vertexConversionScratch.data(); uploadSize = static_cast(m_vertexConversionScratch.size() * sizeof(Float)); break; case VertexInputStateFactory::VertexStreamConversion::Float64ToFloat32: if (!ConvertFloat64VertexStreamToFloat32(attribute, sourceData, sourceStride, elementCount, m_vertexConversionScratch)) { return false; } uploadData = m_vertexConversionScratch.data(); uploadSize = static_cast(m_vertexConversionScratch.size() * sizeof(Float)); break; case VertexInputStateFactory::VertexStreamConversion::None: return false; } return uploadSize > 0 && m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), uploadData, uploadSize, 16, outSlice); }; for (SizeT binding = 0; binding < bindingCount; ++binding) { if (binding >= vertexInputState.bindings.size()) { break; } const Uint32 bindingLocation = binding < vertexInputState.bindingAttributeLocations.size() ? vertexInputState.bindingAttributeLocations[binding] : static_cast(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS); const Bool usesClientMemory = binding < vertexInputState.bindingUsesClientMemory.size() && vertexInputState.bindingUsesClientMemory[binding]; const auto conversion = binding < vertexInputState.bindingConversions.size() ? vertexInputState.bindingConversions[binding] : VertexInputStateFactory::VertexStreamConversion::None; if (usesClientMemory) { memoisable = false; const Uint32 location = bindingLocation; MOBILEGL_ASSERT(location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS, "UploadAndBindVertexStreams failed to resolve client attribute location"); const auto& attr = vao.GetAttribute(location); const SizeT elementSize = VertexInputStateFactory::GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra); const SizeT stride = attr.Stride > 0 ? static_cast(attr.Stride) : elementSize; const auto* clientData = reinterpret_cast(attr.Offset); if (!clientData || elementSize == 0 || stride == 0) { MGLOG_E_ONCE("UploadAndBindVertexStreams skipped: invalid client vertex attribute at location %u", location); return false; } // Client arrays have no queryable size, so bound the upload by the draw's // real fetch range. For indexed draws that means scanning the index bytes: // the guessed vertexCount (indexCount + baseVertex) can both truncate draws // whose max index exceeds their index count and over-read below it. // AN INSTANCE-RATE BINDING IS BOUNDED BY ITS INSTANCES, not by the vertex range: // it fetches one element per instance, shifted by the draw's baseInstance, so // its last element is baseInstance + (instanceCount - 1) / divisor. The two // bounds are independent and either can exceed the other (an indexed draw // naming element 2^16, or baseInstance 1000 over a three-element array), so // the upload covers their union. SizeT clientElementBound = resolveDrawElementBound(); if (vertexInputState.bindings[binding].inputRate == VK_VERTEX_INPUT_RATE_INSTANCE && drawParams.instanceCount > 0) { Uint32 divisor = 1; for (const auto& divisorEntry : vertexInputState.bindingDivisors) { if (divisorEntry.binding == vertexInputState.bindings[binding].binding) { divisor = divisorEntry.divisor; break; } } if (divisor > 0) { const Uint64 first = drawParams.firstInstance; const Uint64 last = first + (drawParams.instanceCount - 1) / divisor; clientElementBound = std::max(clientElementBound, static_cast(last + 1)); } } BufferSlice slice{}; Bool uploaded = false; if (conversion == VertexInputStateFactory::VertexStreamConversion::None) { const SizeT lastVertex = clientElementBound > 0 ? clientElementBound - 1 : (drawParams.vertexCount > 0 ? static_cast(drawParams.firstVertex) + drawParams.vertexCount - 1 : static_cast(drawParams.firstVertex)); const SizeT uploadSize = lastVertex * stride + elementSize; uploaded = m_bufferManager.UploadTransient( BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), clientData, static_cast(uploadSize), 16, slice); } else { if (clientElementBound == 0) { // Indirect/multi indexed draws have no CPU-visible index range and a // client array has no size to fall back to; a guessed range could // truncate the converted stream, so skip the draw loudly. MGLOG_E_ONCE("UploadAndBindVertexStreams skipped: converted client-memory attribute " "location=%u has no computable vertex range", location); return false; } uploaded = uploadConvertedStream(conversion, attr, clientData, stride, elementSize, clientElementBound, slice); } if (!uploaded) { MOBILEGL_ASSERT(false, "UploadAndBindVertexStreams skipped: failed to upload client attribute binding %zu", binding); return false; } vkBuffers[binding] = slice.buffer; vkOffsets[binding] = slice.offset; continue; } // VertexInputStateFactory fills bindingBufferKeys[b] and bindingAttributeLocations[b] // from the SAME loop iteration, one binding per enabled attribute with no merging, so // this attribute's Buffer IS the SharedPtr by construction - no need to search the VAO's // 32 slots for it. The client-memory branch above has already returned, so the location // is in range here. const auto& sourceBufferShared = vao.GetAttribute(bindingLocation).Buffer; MOBILEGL_ASSERT(sourceBufferShared != nullptr, "UploadAndBindVertexStreams failed to resolve source buffer"); BufferSlice slice{}; const SizeT sourceSize = sourceBufferShared->GetSize(); const SizeT baseOffset = binding < vertexInputState.bindingBaseOffsets.size() ? vertexInputState.bindingBaseOffsets[binding] : 0; MOBILEGL_ASSERT(baseOffset <= sourceSize, "UploadAndBindVertexStreams skipped: binding %zu base offset %zu exceeds buffer size %zu", binding, baseOffset, sourceSize); if (conversion != VertexInputStateFactory::VertexStreamConversion::None) { memoisable = false; MOBILEGL_ASSERT(bindingLocation < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS, "UploadAndBindVertexStreams failed to resolve converted attribute location"); const auto& attr = vao.GetAttribute(bindingLocation); const SizeT elementSize = VertexInputStateFactory::GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra); // Zero is a legal binding stride and means "never advance" (see // VertexAttribute::Stride), so it is NOT folded into the element size here - // it selects the single-element conversion below instead. const SizeT sourceStride = static_cast(attr.Stride); if (sourceBufferShared->MappedData() == nullptr || elementSize == 0 || baseOffset > sourceSize || elementSize > sourceSize - baseOffset) { MGLOG_E_ONCE("UploadAndBindVertexStreams skipped: invalid converted source binding=%zu " "location=%u base=%zu size=%zu element=%zu stride=%zu", binding, bindingLocation, baseOffset, sourceSize, elementSize, sourceStride); return false; } // A GPU-written source (XFB capture, SSBO, storage texel buffer) has its // bytes produced by commands that are merely RECORDED at this point, and // MappedData() aliases the coherent GPU memory they will write into - // converting now would read pre-write garbage. Submit-and-wait first, // mirroring the restart-index rewrite; a flag-test no-op otherwise. sourceBufferShared->SyncGpuWrites(); sourceBufferShared->SyncPersistentMappedRange(); const SizeT availableElementCount = sourceStride == 0 ? 1 : 1 + (sourceSize - baseOffset - elementSize) / sourceStride; const Bool cacheable = !sourceBufferShared->IsBackendPersistentMapped(); // Convert only what this draw can fetch instead of the whole buffer tail. // Instance-rate bindings index by instance, not the vertex range, so they // keep the tail. Indexed draws from cacheable buffers also keep the tail: a // single cached whole-range conversion per frame is cheaper than a per-draw // index scan. Persistent-mapped buffers are uncacheable and reconvert every // draw, so for them the scan plus bounded conversion is the cheaper trade. const Bool vertexRateBinding = vertexInputState.bindings[binding].inputRate == VK_VERTEX_INPUT_RATE_VERTEX; SizeT elementCount = availableElementCount; if (vertexRateBinding && (!indexedDraw || !cacheable)) { const SizeT elementBound = resolveDrawElementBound(); if (elementBound > 0) { elementCount = std::min(elementCount, elementBound); } } const ConvertedVertexStreamKey cacheKey{ .buffer = sourceBufferShared.get(), .changeSerial = sourceBufferShared->GetChangeSerial(), .baseOffset = baseOffset, .sourceStride = static_cast(sourceStride), .type = attr.Type, .size = attr.Size, .normalized = attr.Normalized, .isInteger = attr.IsInteger, .conversion = conversion, }; Bool reusedCachedStream = false; if (cacheable) { const auto cached = m_convertedVertexStreams.find(cacheKey); // A cached conversion covering at least this draw's range is a strict // prefix match: converted streams are tightly packed from element 0. if (cached != m_convertedVertexStreams.end() && cached->second.elementCount >= elementCount) { slice = cached->second.slice; reusedCachedStream = true; } } if (!reusedCachedStream) { const Uint8* sourceData = sourceBufferShared->MappedData() + baseOffset; if (!uploadConvertedStream(conversion, attr, sourceData, sourceStride, elementSize, elementCount, slice)) { MGLOG_E_ONCE("UploadAndBindVertexStreams skipped: failed to convert binding=%zu location=%u", binding, bindingLocation); return false; } if (cacheable) { m_convertedVertexStreams[cacheKey] = ConvertedVertexStream{slice, elementCount, sourceBufferShared}; } } vkBuffers[binding] = slice.buffer; vkOffsets[binding] = slice.offset; continue; } if (ShouldUseTransientVertexIndexBuffer(*sourceBufferShared)) { if (!m_bufferManager.AcquireStreamedSlice(BufferKind::Vertex, sourceBufferShared, slice)) { MOBILEGL_ASSERT(false, "UploadAndBindVertexStreams skipped: failed to upload transient binding %zu", binding); return false; } } else { if (!m_bufferManager.AcquireResidentSlice(BufferKind::Vertex, sourceBufferShared, slice)) { MGLOG_E_ONCE("UploadAndBindVertexStreams skipped: failed to sync resident binding %zu", binding); return false; } } vkBuffers[binding] = slice.buffer; vkOffsets[binding] = slice.offset + static_cast(baseOffset); // bindingAttributeLocations carries MAX_VERTEX_ATTRIBS as its "no location" // sentinel; GetAttribute() folds that to an empty attribute but the memo // indexes the raw array, so such a binding is not memoisable. memoisable = memoisable && bindingLocation < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; if (memoisable) { // memo is always non-null here: the slot was claimed (and its serial // zeroed) before the resolve started. memo->attributeLocations[binding] = static_cast(bindingLocation); memo->buffers[binding] = sourceBufferShared.get(); // Read after the acquire: it is the acquire that creates the resource // and mints the epoch this slice belongs to. const auto* resource = static_cast(sourceBufferShared->GetBackendResource().get()); memo->sliceEpochs[binding] = resource != nullptr ? resource->sliceEpoch : 0; anyBufferMapped = anyBufferMapped || sourceBufferShared->IsMapped(); } } SizeT syntheticBinding = vertexInputState.bindings.size(); for (Uint32 location = 0; location < kMaxVertexAttribs; ++location) { if ((missingAttribMask & (1u << location)) == 0) { continue; } const auto glType = programObj.vertexInputTypes[location]; const auto& currentValue = MGB_CTX->GetCurrentVertexAttribute(location); VkFormat format = VK_FORMAT_UNDEFINED; const void* sourceData = nullptr; VkDeviceSize sourceSize = 0; const Bool supported = TryGetCurrentVertexAttributeUploadPayload(currentValue, glType, format, sourceData, sourceSize); if (!supported) { // SetupDraw's pre-flight should have rejected this already; never upload a null payload. MGLOG_E_ONCE("UploadAndBindVertexStreams skipped: unsupported current generic vertex attribute type: " "programHash=%llu location=%u type=0x%x", static_cast(programObj.hash), location, glType); return false; } BufferSlice slice{}; if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), sourceData, sourceSize, 16, slice)) { MOBILEGL_ASSERT(false, "UploadAndBindVertexStreams skipped: failed to upload current attribute binding for location %u", location); return false; } vkBuffers[syntheticBinding] = slice.buffer; vkOffsets[syntheticBinding] = slice.offset; ++syntheticBinding; } if (bindingCount > 0) { const Uint32 count = static_cast(bindingCount); if (memoisable && memo != nullptr) { std::copy_n(vkBuffers.data(), count, memo->vkBuffers); std::copy_n(vkOffsets.data(), count, memo->vkOffsets); // The factory keys entries on the VAO content hash, so this is the same // value the hit path reads back from the VAO's own hash memo. memo->vertexInputHash = vertexInputState.hash; memo->activeAttribMask = activeAttribMask; memo->bindingCount = count; memo->anyBufferMapped = anyBufferMapped; // Read after every acquire above, so it covers the epochs they minted. memo->sliceEpochCounter = m_bufferManager.GetSliceEpochCounter(); // Published last: the entry is only matchable once every field above is // the one this completed resolve produced. memo->frameSerial = frameSerial; // The same draw's UploadAndBindIndexBuffer may extend this entry with the // EBO slice memo; the pointer dies at the map's next insert (next draw). m_currentDrawResolvedEntry = memo; } ShadowedBindVertexBuffers(commandBuffer, vkBuffers.data(), vkOffsets.data(), count); } return true; } namespace { // Copies index data, replacing every occurrence of the application's arbitrary restart // index with the fixed all-ones value of the index type - the only one Vulkan restarts // on. An index that already equals the fixed value would then be indistinguishable from // a restart, so it is nudged to the next-lowest value, which silently draws the wrong // vertex. That is a real (if narrow) loss and it is reported once rather than left // invisible; DirectGLES avoids it for 8- and 16-bit indices by widening the copy instead, // and the same treatment here is follow-up work. // // The caller guarantees applicationRestartIndex fits the index type, so no truncating // cast is needed - and none may be used: truncating turns glPrimitiveRestartIndex(0x100) // over 8-bit indices into "restart on index 0", which shreds every primitive that // references vertex 0. void RewriteRestartIndices(const void* source, SizeT sizeBytes, VkIndexType indexType, Uint32 applicationRestartIndex, Vector& output) { output.resize(sizeBytes); if (sizeBytes == 0 || source == nullptr) { return; } Memcpy(output.data(), source, sizeBytes); const auto rewrite = [&](auto* indices, auto fixedMax) { const SizeT count = sizeBytes / sizeof(*indices); for (SizeT i = 0; i < count; ++i) { if (indices[i] == static_cast(applicationRestartIndex)) { indices[i] = fixedMax; } else if (indices[i] == fixedMax) { MGLOG_E_ONCE("GL_PRIMITIVE_RESTART with restart index %u over index data that also uses " "the all-ones index %u: both cannot be spelled at this index width, so every " "all-ones index is drawn one vertex lower. Use " "GL_PRIMITIVE_RESTART_FIXED_INDEX, or keep the all-ones value out of the " "index data.", applicationRestartIndex, static_cast(fixedMax)); indices[i] = fixedMax - 1; } } }; switch (indexType) { case VK_INDEX_TYPE_UINT8: rewrite(reinterpret_cast(output.data()), static_cast(0xFFu)); break; case VK_INDEX_TYPE_UINT16: rewrite(reinterpret_cast(output.data()), static_cast(0xFFFFu)); break; case VK_INDEX_TYPE_UINT32: rewrite(reinterpret_cast(output.data()), static_cast(0xFFFFFFFFu)); break; default: break; } } } // namespace Bool VulkanRenderer::UploadAndBindIndexBuffer(FrameContext::FrameData& frame, const MG_State::GLState::VertexArrayObject& vao, const IndexBufferView* pIndexBufferView) { VkIndexType vkIndexType = VK_INDEX_TYPE_MAX_ENUM; switch (pIndexBufferView->indexType) { case GL_UNSIGNED_BYTE: MOBILEGL_ASSERT(m_indexTypeUint8ExtensionEnabled, "DrawElements with GL_UNSIGNED_BYTE requires VK_KHR_index_type_uint8 or VK_EXT_index_type_uint8"); vkIndexType = VK_INDEX_TYPE_UINT8; break; case GL_UNSIGNED_SHORT: vkIndexType = VK_INDEX_TYPE_UINT16; break; case GL_UNSIGNED_INT: vkIndexType = VK_INDEX_TYPE_UINT32; break; default: MGLOG_D("DrawElements skipped: index type %u is not supported yet", pIndexBufferView->indexType); return false; } // GL_PRIMITIVE_RESTART uses an arbitrary restart index (glPrimitiveRestartIndex), but Vulkan // only restarts on the fixed all-ones value of the index type. GL_PRIMITIVE_RESTART_FIXED_INDEX // already matches that, so only the arbitrary form needs handling: rewrite the indices into a // transient copy where the application's restart index becomes the fixed one. Uint32 substituteRestartIndex = 0; Bool substituteRestart = false; // One bulk parameters fetch instead of up to three accessor calls per indexed // draw; all three inputs are pure reads of these fields. const RenderStateParameters& rsp = MGB_CTX->GetRenderStateParameters(); if (rsp.PrimitiveRestartEnabled && !rsp.PrimitiveRestartFixedIndexEnabled) { const Uint32 restartIndex = rsp.PrimitiveRestartIndex; const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(pIndexBufferView->indexType); // STRICTLY less, and never truncated. Equal needs no rewrite (the driver already // restarts there); GREATER means the index type cannot hold the application's restart // index, so GL 4.6 core 10.3.6 says nothing matches it and the draw restarts nowhere - // which is exactly what ResolvePrimitiveRestartEnable told the pipeline, so rewriting // here would put restarts into a stream the pipeline was built not to restart on. substituteRestart = restartIndex < fixedMax; substituteRestartIndex = restartIndex; } // Bound by reference so the SharedPtr below is the one already in hand rather than a fresh // GL-name map lookup plus an atomic refcount pair on every indexed draw - the vertex path // above documents the same cost. const SharedPtr& indexBufferShared = vao.GetIndexBufferBindingSlot().GetBoundObject(); const auto* indexBuffer = pIndexBufferView->forceClientMemory ? nullptr : indexBufferShared.get(); if (indexBuffer == nullptr) { // No element-array buffer: the view's byte offset is a raw client pointer // (desktop drivers accept client-memory indices and the GL CTS relies on // this even in core contexts). Snapshot the data into a transient slice. const auto* clientIndices = reinterpret_cast(pIndexBufferView->indexByteOffset); if (clientIndices == nullptr || pIndexBufferView->indexByteSize == 0) { MGLOG_E_ONCE("DrawElements skipped: no element array buffer bound and no client index data"); return false; } Vector rewrittenIndices; const void* uploadSource = clientIndices; if (substituteRestart) { RewriteRestartIndices(clientIndices, pIndexBufferView->indexByteSize, vkIndexType, substituteRestartIndex, rewrittenIndices); uploadSource = rewrittenIndices.data(); } BufferSlice slice{}; if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(), uploadSource, pIndexBufferView->indexByteSize, 4, slice)) { MGLOG_E_ONCE("DrawElements skipped: failed to upload client index data"); return false; } auto& shadow = g_dynamicStateShadow; if (!shadow.indexBindValid || shadow.indexBuffer != slice.buffer || shadow.indexOffset != slice.offset || shadow.indexType != vkIndexType) { vkCmdBindIndexBuffer(frame.commandBuffer, slice.buffer, slice.offset, vkIndexType); shadow.indexBindValid = true; shadow.indexBuffer = slice.buffer; shadow.indexOffset = slice.offset; shadow.indexType = vkIndexType; } return true; } const SizeT indexDataSizeBytes = pIndexBufferView->indexByteSize; MOBILEGL_ASSERT(pIndexBufferView->indexByteOffset + indexDataSizeBytes <= indexBuffer->GetSize(), "DrawElements index range out of bounds"); // EBO slice memo (see ResolvedVertexBindings): skips the per-draw // AcquireResidentSlice when the live bound EBO and its resource epoch still // match what the recording draw resolved. Restart substitution re-uploads per // draw and never stores a memo, so a hit requires it off. The index TYPE is not // memo state: it flows from the draw's view into the shadowed bind below. ResolvedVertexBindings* indexMemo = m_currentDrawResolvedEntry; if (indexMemo != nullptr && !substituteRestart && indexMemo->indexFrameSerial != 0 && indexMemo->indexBuffer == indexBuffer) { // One-compare rescue first (mirrors TryBindResolvedVertexBindings): the // use-serial was stamped this frame and the manager-wide slice-epoch // counter has not moved, so no buffer anywhere - this EBO included - // changed its slice or gained a host map since the epoch was verified. // Skips the per-draw GetBackendResource chase into a cold resource object. Bool sliceStillValid = false; const Uint64 frameSerial = m_bufferManager.GetFrameSerial(); if (indexMemo->indexFrameSerial == frameSerial && !indexMemo->indexBufferMapped && indexMemo->indexSliceEpochCounter == m_bufferManager.GetSliceEpochCounter()) { sliceStillValid = true; } // NO cross-frame trust for the EBO either (same corruption class as the // vertex half, see TryBindResolvedVertexBindings): a memo from an earlier // frame declines and the draw re-runs the acquire, which is the sync point. if (sliceStillValid) { const VkDeviceSize memoBindOffset = indexMemo->indexSliceOffset + static_cast(pIndexBufferView->indexByteOffset); auto& shadow = g_dynamicStateShadow; if (!shadow.indexBindValid || shadow.indexBuffer != indexMemo->indexVkBuffer || shadow.indexOffset != memoBindOffset || shadow.indexType != vkIndexType) { vkCmdBindIndexBuffer(frame.commandBuffer, indexMemo->indexVkBuffer, memoBindOffset, vkIndexType); shadow.indexBindValid = true; shadow.indexBuffer = indexMemo->indexVkBuffer; shadow.indexOffset = memoBindOffset; shadow.indexType = vkIndexType; } return true; } } BufferSlice slice{}; MOBILEGL_ASSERT(indexBufferShared != nullptr, "UploadAndBindIndexBuffer failed to resolve shared EBO"); if (substituteRestart) { // The whole buffer is rewritten, not just this draw's range, so that every element // index keeps its position: an indirect draw's firstIndex lives in GPU memory and // cannot be adjusted from here. indexBufferShared->SyncGpuWrites(); Vector rewrittenIndices; RewriteRestartIndices(indexBufferShared->MappedData(), indexBufferShared->GetSize(), vkIndexType, substituteRestartIndex, rewrittenIndices); if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(), rewrittenIndices.data(), rewrittenIndices.size(), 4, slice)) { MGLOG_E_ONCE("DrawElements skipped: failed to upload restart-substituted index data"); return false; } } else if (ShouldUseTransientVertexIndexBuffer(*indexBufferShared)) { MOBILEGL_ASSERT(indexBufferShared->GetSize() != 0, "DrawElements requires non-empty EBO data"); if (!m_bufferManager.AcquireStreamedSlice(BufferKind::Index, indexBufferShared, slice)) { MOBILEGL_ASSERT(false, "DrawElements skipped: failed to prepare transient index buffer"); return false; } } else if (!m_bufferManager.AcquireResidentSlice(BufferKind::Index, indexBufferShared, slice)) { MGLOG_E_ONCE("DrawElements skipped: failed to sync resident index buffer"); return false; } else if (indexMemo != nullptr && !substituteRestart) { // Resident acquire succeeded: record the slice for the next draw of this VAO. // Read the epoch AFTER the acquire - it is the acquire that mints the epoch // this slice belongs to. const auto* resource = static_cast( indexBufferShared->GetBackendResource().get()); if (resource != nullptr) { indexMemo->indexBuffer = indexBuffer; indexMemo->indexSliceEpoch = resource->sliceEpoch; // Read after the acquire for the same reason as the epoch: the acquire // may have bumped the manager-wide counter minting this very epoch. indexMemo->indexSliceEpochCounter = m_bufferManager.GetSliceEpochCounter(); indexMemo->indexVkBuffer = slice.buffer; indexMemo->indexSliceOffset = slice.offset; indexMemo->indexFrameSerial = m_bufferManager.GetFrameSerial(); // A host-mapped EBO can mutate its shadow with no epoch bump; the hit // path declines on this flag (mirror of anyBufferMapped). indexMemo->indexBufferMapped = indexBufferShared->IsMapped(); } } const VkDeviceSize indexBindOffset = slice.offset + static_cast(pIndexBufferView->indexByteOffset); auto& shadow = g_dynamicStateShadow; if (!shadow.indexBindValid || shadow.indexBuffer != slice.buffer || shadow.indexOffset != indexBindOffset || shadow.indexType != vkIndexType) { vkCmdBindIndexBuffer(frame.commandBuffer, slice.buffer, indexBindOffset, vkIndexType); shadow.indexBindValid = true; shadow.indexBuffer = slice.buffer; shadow.indexOffset = indexBindOffset; shadow.indexType = vkIndexType; } return true; } Bool VulkanRenderer::InitializeBlitResources() { #if MOBILEGL_BUILD_DISAGGREGATED // P5f fv gave this an early return for every non-monolith transport, because the hidden // programs are frontend objects and creating them on the server would retain a client // compiler/allocator dependency. P7 wave 2-B2 (CONTRACT-P7 ยง5.2, (B')) takes the last // step: in a disaggregated build there is NOTHING TO INITIALIZE on any transport, // because the monolith arm now blits with WireColorBlit.inc's baked module too. // // AN `#if` AND NOT A RUNTIME BRANCH, which is the whole point (ยง4.2): the early return // left every ShaderObject / ProgramObject / SamplerObject symbol REFERENCED by this // object file, so the link-closure ratchet counted them whether or not the branch ever // ran. Compiling the construction out is what makes them fall. return true; #else ShutdownBlitResources(); auto vertexShader = MakeShared(ShaderStage::Vertex, kHiddenBlitVertexShaderId); vertexShader->SetShaderSource(kFullscreenTriangleVertexShaderSource); vertexShader->Compile(); if (!vertexShader->GetCompileStatus()) { MGLOG_E("InitializeBlitResources failed: vertex shader compile error: %s", vertexShader->GetInfoLog().c_str()); return false; } auto fragmentShader = MakeShared(ShaderStage::Fragment, kHiddenBlitFragmentShaderId); fragmentShader->SetShaderSource(kBlitFragmentShaderSource); fragmentShader->Compile(); if (!fragmentShader->GetCompileStatus()) { MGLOG_E("InitializeBlitResources failed: fragment shader compile error: %s", fragmentShader->GetInfoLog().c_str()); return false; } m_blitResources.program = MakeShared(kHiddenBlitProgramId); m_blitResources.program->AttachShader(vertexShader); m_blitResources.program->AttachShader(fragmentShader); m_blitResources.program->Link(false); if (!m_blitResources.program->GetLinkStatus()) { MGLOG_E("InitializeBlitResources failed: program link error: %s", m_blitResources.program->GetInfoLog().c_str()); return false; } m_blitResources.srcRectLocation = m_blitResources.program->GetUniformLocation("uSrcRect"); m_blitResources.dstRectLocation = m_blitResources.program->GetUniformLocation("uDstRect"); m_blitResources.surfaceTransformLocation = m_blitResources.program->GetUniformLocation("uSurfaceTransform"); MOBILEGL_ASSERT(m_blitResources.srcRectLocation >= 0, "InitializeBlitResources: missing uSrcRect"); MOBILEGL_ASSERT(m_blitResources.dstRectLocation >= 0, "InitializeBlitResources: missing uDstRect"); MOBILEGL_ASSERT(m_blitResources.surfaceTransformLocation >= 0, "InitializeBlitResources: missing uSurfaceTransform"); MOBILEGL_ASSERT(m_blitResources.program->GetUBOSize() > 0, "InitializeBlitResources: blit program global UBO is empty"); MOBILEGL_ASSERT(m_programFactory != nullptr, "InitializeBlitResources: program factory is null"); ProgramFactory::CompileOptionFlags blitTransformFlags = 0; const auto& blitProgramObj = m_programFactory->GetOrCreateProgram(*m_blitResources.program, blitTransformFlags); Bool foundBlitSamplerBinding = false; for (Uint32 binding = 0; binding < blitProgramObj.samplerNameByBinding.size(); ++binding) { if (blitProgramObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) { continue; } if (blitProgramObj.samplerNameByBinding[binding] == "uSource") { m_blitResources.samplerBinding = binding; foundBlitSamplerBinding = true; break; } } MOBILEGL_ASSERT(foundBlitSamplerBinding, "InitializeBlitResources: failed to resolve reflected binding for uSource"); auto createSampler = [](Uint externalIndex, SamplerFilterMode filter) { auto sampler = MakeShared(externalIndex); sampler->SetWrapS(SamplerWrapMode::ClampToEdge); sampler->SetWrapT(SamplerWrapMode::ClampToEdge); sampler->SetWrapR(SamplerWrapMode::ClampToEdge); sampler->SetMinFilter(filter); sampler->SetMagFilter(filter); sampler->SetMipmapMode(SamplerMipmapMode::None); sampler->SetLodRange(0.0f, 0.0f); return sampler; }; m_blitResources.nearestSampler = createSampler(kHiddenBlitNearestSamplerId, SamplerFilterMode::Nearest); m_blitResources.linearSampler = createSampler(kHiddenBlitLinearSamplerId, SamplerFilterMode::Linear); return true; #endif } void VulkanRenderer::ShutdownBlitResources() { // P7 wave 2-B2: TWO OF P5e RULING 12'S FOUR APPLY-THREAD ALLOCATOR DEBTS ARE GONE, and // the scope that named them with them. The debt was real when it was written: the hidden // blit program and its samplers are FRONTEND objects, and under an active transport the // server backend created them on the apply thread and destroyed them here on it, where // their destructors run the client death helper and its lifetime-id probe. // // What retires it is not a new exemption but the absence of the objects. P5f fv gave // InitializeBlitResources an early return for every non-monolith transport, so off the // monolith arm `m_blitResources` never holds anything and this assignment destroys // nothing - MEASURED, not argued: a temporary probe here and in // ShutdownDepthMipmapResources failed the session if either resource was non-null under // a non-monolith transport, and integration-magma-{split,spawn,tcp} (93/72/74), // integration-magma-full-{split,spawn} (524/524), integration-split (187) and // integration-magma-buffers (21) all stayed green with it armed. Dropping its transport // test killed EVERY monolith `DirectVulkan.` entry at teardown and left the split // entries green, which is what makes the first run evidence rather than an absence. // // The TYPE stays: MG_Test/Wire/RemoteClientTest's RemoteGuards // .BarrieredLegacyScopesCannotExemptAllocator and // .BarrieredFrontendRegistryMembersRefuseBothLegacyScopes use it as a NEGATIVE CONTROL - // they construct it on the apply thread and assert the allocator still refuses, which is // the thing P5f (fr) changed and the only remaining reason for it to exist. m_blitResources = {}; } Bool VulkanRenderer::InitializeDepthMipmapResources() { #if MOBILEGL_BUILD_DISAGGREGATED // P7 wave 2-B2 (CONTRACT-P7 ยง5.2, (B')): the depth-mip half of the same step. In a // disaggregated build the monolith arm generates its depth chain with // WireDepthMipmap.inc's baked pass, so there is no hidden GL program to build on any // transport. See InitializeBlitResources above for why this is an `#if`. return true; #else ShutdownDepthMipmapResources(); auto vertexShader = MakeShared(ShaderStage::Vertex, kHiddenDepthMipmapVertexShaderId); vertexShader->SetShaderSource(kFullscreenTriangleVertexShaderSource); vertexShader->Compile(); if (!vertexShader->GetCompileStatus()) { MGLOG_E("InitializeDepthMipmapResources failed: vertex shader compile error: %s", vertexShader->GetInfoLog().c_str()); return false; } auto fragmentShader = MakeShared(ShaderStage::Fragment, kHiddenDepthMipmapFragmentShaderId); fragmentShader->SetShaderSource(kDepthMipmapFragmentShaderSource); fragmentShader->Compile(); if (!fragmentShader->GetCompileStatus()) { MGLOG_E("InitializeDepthMipmapResources failed: fragment shader compile error: %s", fragmentShader->GetInfoLog().c_str()); return false; } m_depthMipmapResources.program = MakeShared(kHiddenDepthMipmapProgramId); m_depthMipmapResources.program->AttachShader(vertexShader); m_depthMipmapResources.program->AttachShader(fragmentShader); m_depthMipmapResources.program->Link(false); if (!m_depthMipmapResources.program->GetLinkStatus()) { MGLOG_E("InitializeDepthMipmapResources failed: program link error: %s", m_depthMipmapResources.program->GetInfoLog().c_str()); return false; } m_depthMipmapResources.srcRectLocation = m_depthMipmapResources.program->GetUniformLocation("uSrcRect"); m_depthMipmapResources.dstRectLocation = m_depthMipmapResources.program->GetUniformLocation("uDstRect"); m_depthMipmapResources.surfaceTransformLocation = m_depthMipmapResources.program->GetUniformLocation("uSurfaceTransform"); m_depthMipmapResources.srcTexelSizeLocation = m_depthMipmapResources.program->GetUniformLocation("uSrcTexelSize"); MOBILEGL_ASSERT(m_depthMipmapResources.srcRectLocation >= 0, "InitializeDepthMipmapResources: missing uSrcRect"); MOBILEGL_ASSERT(m_depthMipmapResources.dstRectLocation >= 0, "InitializeDepthMipmapResources: missing uDstRect"); MOBILEGL_ASSERT(m_depthMipmapResources.surfaceTransformLocation >= 0, "InitializeDepthMipmapResources: missing uSurfaceTransform"); MOBILEGL_ASSERT(m_depthMipmapResources.srcTexelSizeLocation >= 0, "InitializeDepthMipmapResources: missing uSrcTexelSize"); MOBILEGL_ASSERT(m_depthMipmapResources.program->GetUBOSize() > 0, "InitializeDepthMipmapResources: depth mipmap program global UBO is empty"); MOBILEGL_ASSERT(m_programFactory != nullptr, "InitializeDepthMipmapResources: program factory is null"); ProgramFactory::CompileOptionFlags transformFlags = 0; const auto& programObj = m_programFactory->GetOrCreateProgram(*m_depthMipmapResources.program, transformFlags); Bool foundSamplerBinding = false; for (Uint32 binding = 0; binding < programObj.samplerNameByBinding.size(); ++binding) { if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) { continue; } if (programObj.samplerNameByBinding[binding] == "uSource") { m_depthMipmapResources.samplerBinding = binding; foundSamplerBinding = true; break; } } MOBILEGL_ASSERT(foundSamplerBinding, "InitializeDepthMipmapResources: failed to resolve reflected binding for uSource"); return true; #endif } void VulkanRenderer::ShutdownDepthMipmapResources() { // P7 wave 2-B2: the second of the pair, retired for ShutdownBlitResources's reason and // proved by the same probe. InitializeDepthMipmapResources carries the same non-monolith // early return, so off the monolith arm this assignment destroys nothing. m_depthMipmapResources = {}; } void VulkanRenderer::CollectDeferredDepthMipmapCleanup(Uint32 frameIndex) { MOBILEGL_ASSERT(frameIndex < m_deferredDepthMipmapCleanup.size(), "CollectDeferredDepthMipmapCleanup: frame index %u out of range (size=%zu)", frameIndex, m_deferredDepthMipmapCleanup.size()); if (m_device == VK_NULL_HANDLE) { return; } auto& cleanup = m_deferredDepthMipmapCleanup[frameIndex]; for (auto framebuffer : cleanup.framebuffers) { if (framebuffer != VK_NULL_HANDLE) { vkDestroyFramebuffer(m_device, framebuffer, nullptr); } } for (auto pipeline : cleanup.pipelines) { if (pipeline != VK_NULL_HANDLE) { vkDestroyPipeline(m_device, pipeline, nullptr); } } for (auto renderPass : cleanup.renderPasses) { if (renderPass != VK_NULL_HANDLE) { vkDestroyRenderPass(m_device, renderPass, nullptr); } } for (auto imageView : cleanup.imageViews) { if (imageView != VK_NULL_HANDLE) { vkDestroyImageView(m_device, imageView, nullptr); } } cleanup.framebuffers.clear(); cleanup.pipelines.clear(); cleanup.renderPasses.clear(); cleanup.imageViews.clear(); } void VulkanRenderer::DestroyDeferredDepthMipmapCleanup() { for (Uint32 frameIndex = 0; frameIndex < m_deferredDepthMipmapCleanup.size(); ++frameIndex) { CollectDeferredDepthMipmapCleanup(frameIndex); } m_deferredDepthMipmapCleanup.clear(); } #if !MOBILEGL_BUILD_DISAGGREGATED // P7 wave 2-B2 (CONTRACT-P7 ยง5.2, (B')): the pull build's blit pipeline, built out of the // hidden GL program through the program/pipeline factories. A disaggregated build has no // such program on either arm - WireColorBlit.inc owns its pipeline - so this is compiled // out with its only caller's body. VkPipeline VulkanRenderer::GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry) { MOBILEGL_ASSERT(m_blitResources.program != nullptr, "GetOrCreateBlitPipeline: blit program is null"); MOBILEGL_ASSERT(m_programFactory != nullptr, "GetOrCreateBlitPipeline: program factory is null"); MOBILEGL_ASSERT(m_uniformManager != nullptr, "GetOrCreateBlitPipeline: descriptor binder is null"); static const VkPipelineVertexInputStateCreateInfo kEmptyVertexInputState { VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; ProgramFactory::CompileOptionFlags transformFlags = 0; const auto& programObj = m_programFactory->GetOrCreateProgram(*m_blitResources.program, transformFlags); PipelineFactory::PipelineCreatePayload payload{ .programHash = programObj.hash, .vertexInputHash = 0, .pipelineLayout = programObj.pipelineLayout, .renderPass = renderPassEntry.renderPass, .colorAttachmentCount = renderPassEntry.colorAttachmentCount, .rasterizationSamples = renderPassEntry.sampleCount, .subpass = 0, .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, .cullMode = VK_CULL_MODE_NONE, .frontFace = VK_FRONT_FACE_CLOCKWISE, // Functionally irrelevant to the blit (no flat varying, no capture), but on a device with // provokingVertexModePerPipeline == VK_FALSE a blit pipeline left on FIRST inside a render // pass whose draw pipelines are LAST is an illegal mix. Note this does NOT cover // GenerateDepthMipmapWithShader, which builds its pipeline directly and keeps Vulkan's // FIRST - legal only because it creates and begins its own render pass. Anything that ever // records that pipeline inside an outer render pass must route through this selector too. .provokingVertexMode = SelectProvokingVertexMode(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, false), .depthTestEnable = false, .depthWriteEnable = false, .depthCompareOp = VK_COMPARE_OP_ALWAYS, .stages = &programObj.stages, .vertexInputState = &kEmptyVertexInputState, .stageSpirvDigests = &programObj.stageSpirvDigests }; static constexpr VkColorComponentFlags kColorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; MOBILEGL_ASSERT(payload.colorAttachmentCount <= PipelineFactory::PipelineCreatePayload::kMaxColorAttachments, "GetOrCreateBlitPipeline: colorAttachmentCount=%u exceeds payload capacity", payload.colorAttachmentCount); for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) { payload.colorBlendAttachments[i] = MakeColorBlendAttachmentState( false, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, kColorWriteMask); } return m_pipelineFactory->GetOrCreatePipeline(payload); } #endif // !MOBILEGL_BUILD_DISAGGREGATED Bool VulkanRenderer::GenerateDepthMipmapWithShader(FrameContext::FrameData& frame, MG_State::GLState::ITextureObject& texture, VkTextureManager::TextureResource& resource, Uint32 baseMipLevel, Uint32 generateMipLevelCount, const IntVec3& storageBaseTexelSize, VkImageLayout originalLayout, VkImageLayout finalLayout) { #if MOBILEGL_BUILD_DISAGGREGATED // P7 wave 2-B2 (CONTRACT-P7 ยง5.2, (B')): THE MONOLITH ARM ON THE BAKED PASS. Every // level below is WireDepthMipmap.inc's GenerateWireDepthMipLevel - the same 2x2 // texelFetch box, the same clamp, the same average, the same half-texel offset, since // that shader is a PORT of the one this function used to compile at startup. What goes // away with the GL program is the four frontend objects and the compiler call; what the // texture ends up holding is the same chain. // // `texture`, `storageBaseTexelSize` and `originalLayout` are the pull build's; the pass // reads its shape from `resource`, whose tracked layout IS `originalLayout` at entry. (void)frame; (void)texture; (void)storageBaseTexelSize; (void)originalLayout; if (generateMipLevelCount <= 1) return true; if (resource.aspect != VK_IMAGE_ASPECT_DEPTH_BIT) return false; for (Uint32 level = baseMipLevel + 1; level < baseMipLevel + generateMipLevelCount; ++level) { WireImage source{}; source.image = resource.image; source.format = resource.format; source.aspect = VK_IMAGE_ASPECT_DEPTH_BIT; source.layout = resource.layout; source.trackedLayout = &resource.layout; source.levels = resource.mipLevels; source.level = level - 1; source.extent = {std::max(resource.extent.width >> (level - 1), 1u), std::max(resource.extent.height >> (level - 1), 1u)}; WireImage destination = source; destination.level = level; destination.extent = {std::max(resource.extent.width >> level, 1u), std::max(resource.extent.height >> level, 1u)}; if (!GenerateWireDepthMipLevel(source, destination)) return false; } // The caller's contract is that the image is left in `finalLayout`; the per-level pass // leaves it in the GENERAL it uses for a same-image source and destination. WireImage whole{}; whole.image = resource.image; whole.format = resource.format; whole.aspect = resource.aspect; whole.layout = resource.layout; whole.trackedLayout = &resource.layout; whole.levels = resource.mipLevels; whole.extent = resource.extent; TransitionWireImage(whole, finalLayout); return true; #else MOBILEGL_ASSERT(m_depthMipmapResources.program != nullptr, "GenerateDepthMipmapWithShader: depth mipmap program is null"); MOBILEGL_ASSERT(m_blitResources.nearestSampler != nullptr, "GenerateDepthMipmapWithShader: helper sampler is null"); MOBILEGL_ASSERT(m_programFactory != nullptr, "GenerateDepthMipmapWithShader: program factory is null"); MOBILEGL_ASSERT(m_uniformManager != nullptr, "GenerateDepthMipmapWithShader: uniform manager is null"); MOBILEGL_ASSERT(texture.GetTarget() == TextureTarget::Texture2D, "GenerateDepthMipmapWithShader only supports GL_TEXTURE_2D depth textures"); MOBILEGL_ASSERT(resource.aspect == VK_IMAGE_ASPECT_DEPTH_BIT, "GenerateDepthMipmapWithShader requires a depth-only aspect"); MOBILEGL_ASSERT(resource.depth == 1 && resource.arrayLayers == 1, "GenerateDepthMipmapWithShader only supports single-layer depth textures"); MOBILEGL_ASSERT(m_frameContext.GetCurrentFrameIndex() < m_deferredDepthMipmapCleanup.size(), "GenerateDepthMipmapWithShader: frame index %u out of range (cleanup slots=%zu)", m_frameContext.GetCurrentFrameIndex(), m_deferredDepthMipmapCleanup.size()); auto& deferredCleanup = m_deferredDepthMipmapCleanup[m_frameContext.GetCurrentFrameIndex()]; ProgramFactory::CompileOptionFlags transformFlags = 0; const auto& programObj = m_programFactory->GetOrCreateProgram(*m_depthMipmapResources.program, transformFlags); VkAttachmentDescription depthAttachment{}; depthAttachment.format = resource.format; depthAttachment.samples = VK_SAMPLE_COUNT_1_BIT; depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; depthAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; depthAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; depthAttachment.initialLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; depthAttachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; VkAttachmentReference depthAttachmentRef{}; depthAttachmentRef.attachment = 0; depthAttachmentRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; VkSubpassDescription subpassDesc{}; subpassDesc.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpassDesc.pDepthStencilAttachment = &depthAttachmentRef; VkRenderPassCreateInfo renderPassCreateInfo{}; renderPassCreateInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; renderPassCreateInfo.attachmentCount = 1; renderPassCreateInfo.pAttachments = &depthAttachment; renderPassCreateInfo.subpassCount = 1; renderPassCreateInfo.pSubpasses = &subpassDesc; VkRenderPass renderPass = VK_NULL_HANDLE; VK_VERIFY(vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass), "GenerateDepthMipmapWithShader: vkCreateRenderPass"); static const VkPipelineVertexInputStateCreateInfo kEmptyVertexInputState { VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; VkPipelineViewportStateCreateInfo viewportState{}; viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; viewportState.viewportCount = 1; viewportState.scissorCount = 1; VkPipelineRasterizationStateCreateInfo rasterizationState{}; rasterizationState.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; rasterizationState.polygonMode = VK_POLYGON_MODE_FILL; rasterizationState.cullMode = VK_CULL_MODE_NONE; rasterizationState.frontFace = VK_FRONT_FACE_CLOCKWISE; rasterizationState.lineWidth = 1.0f; VkPipelineMultisampleStateCreateInfo multisampleState{}; multisampleState.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; multisampleState.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; VkPipelineDepthStencilStateCreateInfo depthStencilState{}; depthStencilState.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; depthStencilState.depthTestEnable = VK_TRUE; depthStencilState.depthWriteEnable = VK_TRUE; depthStencilState.depthCompareOp = VK_COMPARE_OP_ALWAYS; depthStencilState.minDepthBounds = 0.0f; depthStencilState.maxDepthBounds = 1.0f; VkPipelineColorBlendStateCreateInfo colorBlendState{}; colorBlendState.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; const VkDynamicState dynamicStates[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; VkPipelineDynamicStateCreateInfo dynamicState{}; dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; dynamicState.dynamicStateCount = static_cast(sizeof(dynamicStates) / sizeof(dynamicStates[0])); dynamicState.pDynamicStates = dynamicStates; VkGraphicsPipelineCreateInfo pipelineCreateInfo{}; pipelineCreateInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; pipelineCreateInfo.stageCount = static_cast(programObj.stages.size()); pipelineCreateInfo.pStages = programObj.stages.data(); pipelineCreateInfo.pVertexInputState = &kEmptyVertexInputState; pipelineCreateInfo.pInputAssemblyState = &inputAssembly; pipelineCreateInfo.pViewportState = &viewportState; pipelineCreateInfo.pRasterizationState = &rasterizationState; pipelineCreateInfo.pMultisampleState = &multisampleState; pipelineCreateInfo.pDepthStencilState = &depthStencilState; pipelineCreateInfo.pColorBlendState = &colorBlendState; pipelineCreateInfo.pDynamicState = &dynamicState; pipelineCreateInfo.layout = programObj.pipelineLayout; pipelineCreateInfo.renderPass = renderPass; pipelineCreateInfo.subpass = 0; VkPipeline pipeline = VK_NULL_HANDLE; VK_VERIFY(vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &pipelineCreateInfo, nullptr, &pipeline), "GenerateDepthMipmapWithShader: vkCreateGraphicsPipelines"); deferredCleanup.renderPasses.push_back(renderPass); deferredCleanup.pipelines.push_back(pipeline); auto createMipView = [&](Uint32 mipLevel, VkImageAspectFlags aspectMask) { VkImageViewCreateInfo viewCreateInfo{}; viewCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; viewCreateInfo.image = resource.image; viewCreateInfo.viewType = resource.viewType; viewCreateInfo.format = resource.format; viewCreateInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; viewCreateInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; viewCreateInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; viewCreateInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; viewCreateInfo.subresourceRange.aspectMask = aspectMask; viewCreateInfo.subresourceRange.baseMipLevel = mipLevel; viewCreateInfo.subresourceRange.levelCount = 1; viewCreateInfo.subresourceRange.baseArrayLayer = 0; viewCreateInfo.subresourceRange.layerCount = 1; VkImageView view = VK_NULL_HANDLE; VK_VERIFY(vkCreateImageView(m_device, &viewCreateInfo, nullptr, &view), "GenerateDepthMipmapWithShader: vkCreateImageView"); return view; }; VkPipelineStageFlags originalSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags originalSrcAccessMask = 0; GetImageTransitionSourceState(originalLayout, originalSrcStageMask, originalSrcAccessMask); VkPipelineStageFlags finalDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags finalDstAccessMask = 0; GetImageTransitionDestinationState(finalLayout, finalDstStageMask, finalDstAccessMask); VkPipelineStageFlags attachmentStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags attachmentAccessMask = 0; GetImageTransitionDestinationState(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, attachmentStageMask, attachmentAccessMask); if (originalLayout != finalLayout) { if (baseMipLevel > 0) { VkImageLayout lowerMipLayout = originalLayout; const Bool lowerReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource.image, lowerMipLayout, finalLayout, originalSrcStageMask, finalDstStageMask, originalSrcAccessMask, finalDstAccessMask, resource.aspect, 0, baseMipLevel); MOBILEGL_ASSERT(lowerReady, "%s: failed to transition lower untouched mip levels", __func__); } if (generateMipLevelCount < resource.mipLevels) { VkImageLayout upperMipLayout = originalLayout; const Bool upperReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource.image, upperMipLayout, finalLayout, originalSrcStageMask, finalDstStageMask, originalSrcAccessMask, finalDstAccessMask, resource.aspect, generateMipLevelCount, resource.mipLevels - generateMipLevelCount); MOBILEGL_ASSERT(upperReady, "%s: failed to transition upper untouched mip levels", __func__); } VkImageLayout baseMipLayout = originalLayout; const Bool baseReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource.image, baseMipLayout, finalLayout, originalSrcStageMask, finalDstStageMask, originalSrcAccessMask, finalDstAccessMask, resource.aspect, baseMipLevel, 1); MOBILEGL_ASSERT(baseReady, "%s: failed to transition base mip level to sampled layout", __func__); } resource.layout = finalLayout; auto* depthProgramData = static_cast(m_depthMipmapResources.program->MapUBO()); MOBILEGL_ASSERT(depthProgramData != nullptr, "GenerateDepthMipmapWithShader: depth mipmap UBO is null"); auto writeUniform = [&](Int location, const void* data, SizeT size) { MOBILEGL_ASSERT(location >= 0, "GenerateDepthMipmapWithShader: invalid uniform location"); const Uint offset = m_depthMipmapResources.program->GetUniformOffset(static_cast(location)); // A RETURN, not only an assert: the assert compiles out in release, and a program // whose SPIR-V job settled cancelled reports kInvalidUniformOffset (~0u) with a // zero-sized shadow - which would make the memcpy below a wild write at // depthProgramData + 4 GiB rather than a dropped uniform. if (offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset || offset + size > m_depthMipmapResources.program->GetUBOSize()) { MOBILEGL_ASSERT(false, "GenerateDepthMipmapWithShader: uniform write out of bounds"); return; } memcpy(depthProgramData + offset, data, size); m_depthMipmapResources.program->MarkUBOContentDirty(); }; for (Uint32 level = baseMipLevel + 1; level < generateMipLevelCount; ++level) { VkImageLayout dstMipLayout = originalLayout; const Bool dstReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource.image, dstMipLayout, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, originalSrcStageMask, attachmentStageMask, originalSrcAccessMask, attachmentAccessMask, resource.aspect, level, 1); MOBILEGL_ASSERT(dstReady, "%s: failed to transition mip level %u to depth attachment layout", __func__, level); const IntVec3 srcTexelSize = ComputeMipTexelSize(storageBaseTexelSize, level - 1); const IntVec3 dstTexelSize = ComputeMipTexelSize(storageBaseTexelSize, level); const Int srcTexelSizeUniform[2] = {srcTexelSize.x(), srcTexelSize.y()}; const VkImageView sourceImageView = createMipView(level - 1, VK_IMAGE_ASPECT_DEPTH_BIT); const VkImageView depthAttachmentView = createMipView(level, resource.aspect); deferredCleanup.imageViews.push_back(sourceImageView); deferredCleanup.imageViews.push_back(depthAttachmentView); VkFramebufferCreateInfo framebufferCreateInfo{}; framebufferCreateInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; framebufferCreateInfo.renderPass = renderPass; framebufferCreateInfo.attachmentCount = 1; framebufferCreateInfo.pAttachments = &depthAttachmentView; framebufferCreateInfo.width = static_cast(dstTexelSize.x()); framebufferCreateInfo.height = static_cast(dstTexelSize.y()); framebufferCreateInfo.layers = 1; VkFramebuffer framebuffer = VK_NULL_HANDLE; VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer), "GenerateDepthMipmapWithShader: vkCreateFramebuffer"); deferredCleanup.framebuffers.push_back(framebuffer); VkRenderPassBeginInfo renderPassBeginInfo{}; renderPassBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; renderPassBeginInfo.renderPass = renderPass; renderPassBeginInfo.framebuffer = framebuffer; renderPassBeginInfo.renderArea.offset = {0, 0}; renderPassBeginInfo.renderArea.extent = { static_cast(dstTexelSize.x()), static_cast(dstTexelSize.y()) }; vkCmdBeginRenderPass(frame.commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE); VkViewport viewport{}; viewport.x = 0.0f; viewport.y = 0.0f; viewport.width = static_cast(dstTexelSize.x()); viewport.height = static_cast(dstTexelSize.y()); viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; vkCmdSetViewport(frame.commandBuffer, 0, 1, &viewport); VkRect2D scissor{}; scissor.offset = {0, 0}; scissor.extent = {static_cast(dstTexelSize.x()), static_cast(dstTexelSize.y())}; vkCmdSetScissor(frame.commandBuffer, 0, 1, &scissor); vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); // The depth-mipmap pipeline's narrower dynamic set (viewport/scissor // only) leaves the other dynamic states undefined; its raw scissor // and viewport writes also bypass the shadow. ResetDynamicStateShadow(); std::fill(depthProgramData, depthProgramData + m_depthMipmapResources.program->GetUBOSize(), Uint8{0}); BlitUniformData blitUniformData{}; writeUniform(m_depthMipmapResources.srcRectLocation, blitUniformData.srcRect, sizeof(blitUniformData.srcRect)); writeUniform(m_depthMipmapResources.dstRectLocation, blitUniformData.dstRect, sizeof(blitUniformData.dstRect)); writeUniform(m_depthMipmapResources.surfaceTransformLocation, &blitUniformData.surfaceTransform, sizeof(blitUniformData.surfaceTransform)); writeUniform(m_depthMipmapResources.srcTexelSizeLocation, srcTexelSizeUniform, sizeof(srcTexelSizeUniform)); const auto samplerBindingOverride = UniformManager::SamplerBindingOverride{ .binding = m_depthMipmapResources.samplerBinding, .texture = &texture, .sampler = m_blitResources.nearestSampler.get(), .imageView = sourceImageView, }; const Bool bound = m_uniformManager->BindProgramUniformBuffers( frame.commandBuffer, *m_depthMipmapResources.program, programObj, m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_GRAPHICS, &samplerBindingOverride); MOBILEGL_ASSERT(bound, "GenerateDepthMipmapWithShader: BindProgramUniformBuffers failed"); vkCmdDraw(frame.commandBuffer, 3, 1, 0, 0); vkCmdEndRenderPass(frame.commandBuffer); VkImageLayout finishedMipLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; const Bool finishedReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource.image, finishedMipLayout, finalLayout, attachmentStageMask, finalDstStageMask, attachmentAccessMask, finalDstAccessMask, resource.aspect, level, 1); MOBILEGL_ASSERT(finishedReady, "%s: failed to transition mip level %u to sampled layout", __func__, level); } return true; #endif } // Boost-style hash combine. The inputs are tiny enum ordinals and bit masks, so // full avalanche is unnecessary; the combine only has to keep distinct state // vectors apart under the memo's otherwise-exact key. static inline Uint64 CombinePipelineStateWord(Uint64 hash, Uint64 word) { return hash ^ (word + 0x9E3779B97F4A7C15ull + (hash << 6) + (hash >> 2)); } // Value hash over every fixed-function GL state the pipeline payload reads that // the memo key's other fields (mode, program hash, vertex-input hash, render-pass // hash, transform flags) do not already pin down. Enumerated against the payload // build in GetOrCreatePipeline - any new GL-state read there must be added here: // - capability bits: CullFace, DepthTest, PolygonOffsetFill (mode gating rides // the memo's mode key), RasterizerDiscard, ColorLogicOp, StencilTest, // PrimitiveRestart(+FixedIndex), SampleShading, SampleMask, plus the depth write mask // - patch vertices, polygon mode, cull face mode, depth func, logic op, // min sample shading, the glSampleMaski word // - front/back stencil ops + compare funcs (ref/mask are dynamic state) // - per draw buffer up to the render pass's colour span: indexed blend enable, // blend factors/equations, indexed colour write mask (broadcast from index 0 // when the device lacks independentBlend - the same read the payload does) // FBO-derived payload inputs (attachment presence/formats/draw-buffer gating) are // pinned by the render-pass hash key, exactly as the version-keyed memo relied on. // The fixed-function sample mask this draw actually gets, and the ONE place that decides it. // // GL 4.6 core 17.3.3 puts SAMPLE_MASK/SAMPLE_MASK_VALUE among the multisample fragment // operations and says they make no change "if MULTISAMPLE is disabled, or if the value of // SAMPLE_BUFFERS is not one" - so on a single-sample draw framebuffer the mask is a no-op. // Vulkan has no such rule: pSampleMask is ANDed with coverage at every rasterizationSamples, // and at one sample that coverage is bit 0 alone. Handing the raw GL word straight through // therefore turned `glEnable(GL_SAMPLE_MASK); glSampleMaski(0, 0x2);` followed by a draw to // the default framebuffer - the ordinary MSAA-render-then-present shape, and what dEQP's // multisample cases leave enabled - into a fully discarded, black draw. All-ones restores // the null-pSampleMask meaning the pipeline had before the mask was plumbed at all. // // SAMPLE_BUFFERS is the load-bearing half: MultisampleEnabled defaults to TRUE, so the // capability check alone would gate nothing. It is here for spec completeness - GL lets // glDisable(GL_MULTISAMPLE) switch the whole step off on a multisample target too. // // Both callers - the payload and ComputePipelineStateHash's memo word - go through this, so // the memo key cannot describe a different mask than the pipeline was built with. Uint32 VulkanRenderer::ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const { constexpr Uint32 kFullCoverage = 0xffffffffu; if (rasterizationSamples == VK_SAMPLE_COUNT_1_BIT) return kFullCoverage; if (!MGB_CTX->IsCapabilityEnabled(CapabilityInput::Multisample)) return kFullCoverage; if (!MGB_CTX->IsCapabilityEnabled(CapabilityInput::SampleMask)) return kFullCoverage; return MGB_CTX->GetRenderStateParameters().SampleMaskValue; } #if MOBILEGL_PIPE_LEGACY_MEMOS Uint64 VulkanRenderer::ComputePipelineStateHash(Uint32 colorAttachmentCount, VkSampleCountFlagBits rasterizationSamples) const { // One bulk fetch instead of ~17 per-field accessor calls into MG_State: every // input below is a plain field of RenderStateParameters, and each accessor this // replaces (IsCapabilityEnabled / Get*) is a verified pure read of that same // field (RenderState.cpp), so the hashed values are bit-identical. This runs on // every draw whose pipeline-state version moved (a per-draw GL_BLEND toggle), // where the accessor-call overhead dominated the hash itself. const RenderStateParameters& p = MGB_CTX->GetRenderStateParameters(); Uint64 capabilityBits = 0; capabilityBits |= p.CullFaceEnabled ? 1ull << 0 : 0; capabilityBits |= p.DepthTestEnabled ? 1ull << 1 : 0; capabilityBits |= p.PolygonOffsetFillEnabled ? 1ull << 2 : 0; capabilityBits |= p.RasterizerDiscardEnabled ? 1ull << 3 : 0; capabilityBits |= p.ColorLogicOpEnabled ? 1ull << 4 : 0; capabilityBits |= p.StencilTestEnabled ? 1ull << 5 : 0; capabilityBits |= p.PrimitiveRestartEnabled ? 1ull << 6 : 0; capabilityBits |= p.PrimitiveRestartFixedIndexEnabled ? 1ull << 7 : 0; capabilityBits |= p.DepthMask ? 1ull << 8 : 0; capabilityBits |= p.SampleShadingEnabled ? 1ull << 9 : 0; // The EFFECTIVE mask enable, not the raw GL bit: at one sample GL says the whole // multisample fragment-operations step makes no change, so the pipeline is built with // full coverage and the memo word has to say so too. Keying on the raw bit here while // the payload gates on the sample count would let one FBO's cached pipeline answer for // another whose sample count reads the mask differently. const Bool sampleMaskEffective = ResolveEffectiveSampleMask(rasterizationSamples) != 0xffffffffu; capabilityBits |= sampleMaskEffective ? 1ull << 10 : 0; Uint64 hash = CombinePipelineStateWord(0x243F6A8885A308D3ull, capabilityBits); // glMinSampleShading. Hashed by BITS, not by value: this memo compares hashes rather than // versions, so an unhashed float would let a pipeline built at one rate be handed back // after glMinSampleShading moved it - the memo would see identical state. { Uint32 minSampleShadingBits = 0; std::memcpy(&minSampleShadingBits, &p.MinSampleShadingValue, sizeof(minSampleShadingBits)); hash = CombinePipelineStateWord(hash, static_cast(minSampleShadingBits)); } // glSampleMaski's word, for the same reason glMinSampleShading's bits are hashed above: // this memo compares hashes, not versions, so a mask that moved between two otherwise // identical draws has to key a different pipeline. Hashed unconditionally rather than only // while GL_SAMPLE_MASK is enabled - the enable bit is already in capabilityBits, and // folding one more word costs nothing on a path that only recomputes when the // pipeline-state version moved. hash = CombinePipelineStateWord(hash, static_cast(ResolveEffectiveSampleMask(rasterizationSamples))); hash = CombinePipelineStateWord(hash, static_cast(p.PatchVertices)); // The default tessellation levels belong here for the same reason PatchVertices does: // when a program has an evaluation stage and no control stage, both are compiled into the // synthesized pass-through control stage, so two draws that differ only in a level need // different pipelines. Hashed over the RAW BITS so a NaN level - which glPatchParameterfv // accepts - keys to itself. Six extra words on a path that only recomputes when the // pipeline-state version moved. for (Uint32 i = 0; i < 4; ++i) { hash = CombinePipelineStateWord(hash, static_cast(std::bit_cast(p.PatchDefaultOuterLevel[i]))); } for (Uint32 i = 0; i < 2; ++i) { hash = CombinePipelineStateWord(hash, static_cast(std::bit_cast(p.PatchDefaultInnerLevel[i]))); } hash = CombinePipelineStateWord(hash, static_cast(p.PolygonModeFront)); hash = CombinePipelineStateWord(hash, static_cast(p.CullFaceModeSetting)); hash = CombinePipelineStateWord(hash, static_cast(p.DepthFunc)); hash = CombinePipelineStateWord(hash, static_cast(p.LogicOp)); // StencilStates[0] is Front, [1] is Back (RenderState::GetStencilFaceIndex) - // the same order the two GetStencilState(face) calls used to hash in. for (const StencilFaceState& stencil : p.StencilStates) { hash = CombinePipelineStateWord(hash, static_cast(stencil.FailOp) | (static_cast(stencil.PassDepthPassOp) << 16) | (static_cast(stencil.PassDepthFailOp) << 32) | (static_cast(stencil.Func) << 48)); } MOBILEGL_ASSERT(colorAttachmentCount <= p.BlendStates.size(), "ComputePipelineStateHash: colorAttachmentCount %u exceeds MAX_DRAW_BUFFERS", colorAttachmentCount); for (Uint32 i = 0; i < colorAttachmentCount; ++i) { const PerBufferBlendState& blend = p.BlendStates[i]; const BoolVec4 mask = p.ColorMasks[m_independentBlendFeatureEnabled ? i : 0]; Uint64 attachmentWord = blend.Enabled ? 1ull : 0; attachmentWord |= (mask.r() ? 1ull << 1 : 0) | (mask.g() ? 1ull << 2 : 0) | (mask.b() ? 1ull << 3 : 0) | (mask.a() ? 1ull << 4 : 0); attachmentWord |= static_cast(blend.SrcFactorRGB) << 8; attachmentWord |= static_cast(blend.DstFactorRGB) << 16; attachmentWord |= static_cast(blend.SrcFactorAlpha) << 24; attachmentWord |= static_cast(blend.DstFactorAlpha) << 32; attachmentWord |= static_cast(blend.ColorEquation) << 40; attachmentWord |= static_cast(blend.AlphaEquation) << 48; hash = CombinePipelineStateWord(hash, attachmentWord); } return hash; } #endif // MOBILEGL_PIPE_LEGACY_MEMOS #if MOBILEGL_PIPE_PUSH && !MOBILEGL_PIPE_LEGACY_MEMOS Uint64 VulkanRenderer::ComputePipelineSubsetStateHashFallback() const { // The client's own hash, over the client's own definition of the pipeline subset - the // seven pipeline chunks of the P2 chunk table, which is a strict SUPERSET of what // ComputePipelineStateHash enumerated by hand. The render-pass facts it does not carry // (colorAttachmentCount, the rasterization sample count, and through them the effective // sample mask) are exactly the facts entry.renderPassHash separates, which is why the // CSO handle can key this memo in the first place; this fallback inherits that argument // unchanged. // // Only reached with no render-state CSO bound, and only in a build with no pre-handle // arm to fall back to instead. return MG_Pipe::MGPipeComputePipelineSubsetHash(MGB_CTX->GetRenderStateParameters()); } #endif // A program that runs a geometry shader AND captures transform feedback. Both halves are // link-time properties, so this is safe to fold into a pipeline keyed on the program hash. static Bool ProgramCapturesXfbFromGeometryStage(const MagmaProgramSource& program) { if (program.GetTransformFeedbackVaryingCount() == 0) return false; // Both halves are link-time properties, so both are asked of the LAST LINK. Reading the // live attach list would let a glAttachShader that has not been linked in yet - which GL // 4.6 core 7.3 says changes nothing about what the program runs - flip a property this // pipeline is cached under, for an executable with no geometry stage in it. return program.HasLinkedShaderStage(ShaderStage::Geometry); } // GL primitive restart is defined on the INDEX STREAM (GL 4.6 core 10.3.6): it splits // primitives when a fetched index matches PRIMITIVE_RESTART_INDEX. Two consequences the // capability bits alone cannot express, both resolved here because only the caller knows them: // // - A non-indexed draw has no index stream, so restart is a no-op for it. Leaving the // pipeline's primitiveRestartEnable on for a glDrawArrays is what made the list-topology // guard below refuse those draws, so an application that enables GL_PRIMITIVE_RESTART once // at init lost every glDrawArrays on a device without the extension. // - The comparison is against the full 32-bit restart index with the fetched index // zero-extended, so a restart index the type cannot hold (0x100FF against UNSIGNED_BYTE // data) matches no index and that draw restarts NOWHERE. UploadAndBindIndexBuffer makes the // same call for the rewrite, and the two must agree or the pipeline says "restart" over // index data nothing rewrote. Bool VulkanRenderer::ResolvePrimitiveRestartEnable(Flags aspects, const IndexBufferView* pIndexBufferView) const { if (!(aspects & DrawSetupAspect::IndexBuffer) || pIndexBufferView == nullptr) { return false; } const RenderStateParameters& rsp = MGB_CTX->GetRenderStateParameters(); if (rsp.PrimitiveRestartFixedIndexEnabled) { return true; } if (!rsp.PrimitiveRestartEnabled) { return false; } return rsp.PrimitiveRestartIndex <= MG_Util::FixedRestartIndexForGLType(pIndexBufferView->indexType); } #if MOBILEGL_BUILD_DISAGGREGATED // MOBILEGL_TEST_PIPELINE_CREATE_DELAY_MS - milliseconds to spend in a pipeline-creation MISS // on the wire arm. See the call site below for why it exists: the device's divergence needs a // COLD driver pipeline cache, and this host's is always warm, so the only way to reproduce // what a cold cache costs is to buy the time. Read once; inert unless set; compiled only into // the disaggregated build, so the monolith image G1 pins never carries it. static Uint32 MagmaTestPipelineCreateDelayMs() { static const Uint32 milliseconds = [] { const char* value = std::getenv("MOBILEGL_TEST_PIPELINE_CREATE_DELAY_MS"); if (!value || !*value) return 0u; return static_cast(std::strtoul(value, nullptr, 10)); }(); return milliseconds; } #endif VkPipeline VulkanRenderer::GetOrCreatePipeline( GLenum mode, const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, ProgramFactory::CompileOptionFlags transformFlags, const MG_State::GLState::VertexArrayObject& vao, const RenderPassEntry& renderPassEntry, Bool primitiveRestartEnable) { #if MOBILEGL_BUILD_DISAGGREGATED return GetOrCreatePipelineWithInput(mode, program, programObj, transformFlags, m_vertexInputStateFactory->GetOrCreateVertexInputState(vao), renderPassEntry, primitiveRestartEnable); } VkPipeline VulkanRenderer::GetOrCreatePipelineWithInput(GLenum mode, const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, ProgramFactory::CompileOptionFlags transformFlags, const VertexInputStateFactory::BackendVertexInputState& vis, const RenderPassEntry& renderPassEntry, Bool primitiveRestartEnable, Uint64 wireRenderPassCompatibilityId) { #endif Bool invertClockwise = transformFlags & ProgramFactory::CompileOptionBit::PositionYFlip; if (programObj.stages.empty()) { MGLOG_D("GetOrCreatePipeline skipped: program has no shader stages"); return VK_NULL_HANDLE; } // Fast path: skip the full pipeline resolution when the pipeline state is unchanged from the // previous draw (the common intra-batch case). The key provably covers every // PipelineCreatePayload field: draw mode (topology + polygon-fill depth-bias gate), program // content hash (folds program identity + link version + transform flags + shader stages), // vertex-input hash (VAO layout), render-pass hash (render targets + the draw-buffer/format // driven blend & write-mask gating), and the pipeline-state value hash (all fixed-function state). // Reset per-frame and on pipeline destruction so a memoized handle can never dangle. // The identity hash mixes each bound buffer's never-reused lifetime id // (per-chunk VBOs mint a new one per buffer); the memo and the pipeline // payload key on the resolved LAYOUT hash instead, so draws over identical // layouts share one pipeline. // The one-arg fetch rides the VAO's state-pointer memo (no hash, no map). #if !MOBILEGL_BUILD_DISAGGREGATED auto& vis = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao); #endif const Uint64 vertexLayoutHash = vis.layoutHash; const Uint64 renderPassHash = #if MOBILEGL_BUILD_DISAGGREGATED wireRenderPassCompatibilityId != 0 ? 0 : #endif renderPassEntry.hash; // The pipeline-relevant subset only: glViewport / glScissor / glBlendColor / glStencilMask // and friends are dynamic state or not pipeline state at all, and keying the memo on the // all-state counter made any of them evict a perfectly good VkPipeline. The memo compares // the VALUE hash of that subset, never the version itself: the version is monotonic, so // per-draw state flips (GL_BLEND toggles) would otherwise miss entries the memo holds. // The version only guards recomputing the hash - unchanged version, unchanged bytes. const Uint renderStateVersion = MGB_CTX->GetPipelineStateVersion(); #if MOBILEGL_PIPE_PUSH // P2 D12.1. Non-null means the client's render-state CSO handle is this draw's state // key and the hash below is not computed at all; null means the pre-handle arm. The // two arms' entries can never match each other: the handle arm stores hash 0 and a // real handle, the legacy arm a real hash and the null handle, and the probe compares // both components. const MG_Pipe::MGPipeHandle renderStateCso = ResolveBoundRenderStateCso(); // Exactly one of the two state keys is live per draw, and the ternary short-circuits, // so a draw on the handle arm neither hashes nor touches the fallback cache. const Uint64 pipelineStateHash = !MG_Pipe::MGPipeHandleIsNull(renderStateCso) ? 0 : ResolveFallbackPipelineStateHash(renderStateVersion, renderPassEntry.colorAttachmentCount, renderPassEntry.sampleCount); #else // THE PULL BUILD'S TEXT, statement for statement what the base ref has: G1 admits no // resize of this function, and a helper the compiler merely inlines is not the same // instruction schedule. if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion || m_pipelineStateHashColorCount != renderPassEntry.colorAttachmentCount || m_pipelineStateHashSampleCount != renderPassEntry.sampleCount) { m_pipelineStateHash = ComputePipelineStateHash(renderPassEntry.colorAttachmentCount, renderPassEntry.sampleCount); m_pipelineStateHashVersion = renderStateVersion; m_pipelineStateHashColorCount = renderPassEntry.colorAttachmentCount; m_pipelineStateHashSampleCount = renderPassEntry.sampleCount; m_pipelineStateHashValid = true; } const Uint64 pipelineStateHash = m_pipelineStateHash; #endif for (Uint32 i = 0; i < m_pipelineMemoCount; ++i) { const PipelineMemoEntry& entry = m_pipelineMemo[i]; if (entry.pipeline != VK_NULL_HANDLE && entry.mode == mode && entry.programHash == programObj.hash && entry.vertexInputHash == vertexLayoutHash && entry.renderPassHash == renderPassHash && #if MOBILEGL_BUILD_DISAGGREGATED entry.wireRenderPassCompatibilityId == wireRenderPassCompatibilityId && #endif entry.pipelineStateHash == pipelineStateHash && #if MOBILEGL_PIPE_PUSH entry.renderStateCso == renderStateCso && #endif 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 // memo, and the steady-state draw keeps paying nothing for the check. // // vkCreateGraphicsPipelines is not a validating entry point: a stage set that a // conformant implementation would reject with VK_ERROR_* is, on Adreno 830, a SIGSEGV // inside the driver - process death instead of a failed draw. The separable-program path // is what made these shapes reachable at all (a monolithic glUseProgram program cannot // hold a compute stage together with graphics ones, a pipeline object can), so the three // it can produce are named and refused here. Same philosophy as the VK_NULL_HANDLE gate // in SetupDraw: hostile input degrades to a broken draw, never to a dead process. GL // leaves all three undefined for a draw, so nothing legal is being turned away. // MGLOG_E, latched: a refused program is never memoized, so the refusal is re-derived // on every draw that uses it. Parked at MGLOG_I until the Log.h ordering was fixed. { Bool hasVertexStage = false; for (const auto& stage : programObj.stages) { if (stage.module == VK_NULL_HANDLE) { MGLOG_E_ONCE("GetOrCreatePipeline skipped: program=%u has a null shader module for stage 0x%x", program.GetExternalIndex(), static_cast(stage.stage)); return VK_NULL_HANDLE; } if (stage.stage == VK_SHADER_STAGE_COMPUTE_BIT) { MGLOG_E_ONCE("GetOrCreatePipeline skipped: program=%u carries a compute stage, which no graphics " "pipeline may contain", program.GetExternalIndex()); return VK_NULL_HANDLE; } if (stage.stage == VK_SHADER_STAGE_VERTEX_BIT) { hasVertexStage = true; } } if (!hasVertexStage) { MGLOG_E_ONCE("GetOrCreatePipeline skipped: program=%u has no vertex stage", program.GetExternalIndex()); return VK_NULL_HANDLE; } } #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG const auto& limits = m_physicalDevice.properties.limits; if (programObj.fragmentInputComponentCount != 0) { MOBILEGL_ASSERT( programObj.fragmentInputComponentCount <= limits.maxFragmentInputComponents, "GetOrCreatePipeline: fragmentInputComponents=%u exceeds device limit=%u program=%u producerStage=%d", programObj.fragmentInputComponentCount, limits.maxFragmentInputComponents, program.GetExternalIndex(), static_cast(programObj.rasterizationProducerStage)); } if (programObj.producerOutputComponentCount != 0) { Uint32 producerOutputLimit = 0; switch (programObj.rasterizationProducerStage) { case ShaderStage::Vertex: producerOutputLimit = limits.maxVertexOutputComponents; break; case ShaderStage::Geometry: producerOutputLimit = limits.maxGeometryOutputComponents; break; case ShaderStage::TessEval: producerOutputLimit = limits.maxTessellationEvaluationOutputComponents; break; default: break; } if (producerOutputLimit != 0) { MOBILEGL_ASSERT( programObj.producerOutputComponentCount <= producerOutputLimit, "GetOrCreatePipeline: producerOutputComponents=%u exceeds stage limit=%u program=%u producerStage=%d", programObj.producerOutputComponentCount, producerOutputLimit, program.GetExternalIndex(), static_cast(programObj.rasterizationProducerStage)); } } #endif const Uint32 vertexInputAttribMask = vis.attributeLocationMask; const Uint32 activeAttribMask = programObj.activeVertexInputLocationMask; const Uint32 missingAttribMask = activeAttribMask & ~vertexInputAttribMask; auto& patchedAttributes = m_patchedAttributesScratch; patchedAttributes.assign(vis.attributes.begin(), vis.attributes.end()); Bool hasPatchedVertexAttributes = false; for (auto& attribute : patchedAttributes) { if (attribute.location >= kMaxVertexAttribs || (activeAttribMask & (1u << attribute.location)) == 0) { continue; } const GLenum shaderInputType = programObj.vertexInputTypes[attribute.location]; const NumericDomain shaderInputDomain = GetNumericDomainForShaderValueType(shaderInputType); const NumericDomain vertexInputDomain = GetNumericDomainForVertexFormat(attribute.format); if (shaderInputDomain == NumericDomain::Unknown || vertexInputDomain == NumericDomain::Unknown || shaderInputDomain == vertexInputDomain) { continue; } VkFormat patchedFormat = VK_FORMAT_UNDEFINED; const Bool canPatch = TryCoerceVertexFormatNumericDomain(attribute.format, shaderInputDomain, patchedFormat); MOBILEGL_ASSERT( canPatch, "GetOrCreatePipeline: vertex input location=%u format=%d mismatches shader input type=%u program=%u", attribute.location, static_cast(attribute.format), static_cast(shaderInputType), program.GetExternalIndex()); MGLOG_W_ONCE("GetOrCreatePipeline: patching vertex input location=%u format=%d -> %d to match shader input type=%u for program=%u", attribute.location, static_cast(attribute.format), static_cast(patchedFormat), static_cast(shaderInputType), program.GetExternalIndex()); attribute.format = patchedFormat; hasPatchedVertexAttributes = true; } VertexInputStateBuilder syntheticVertexInputBuilder; VkPipelineVertexInputStateCreateInfo syntheticVertexInputState{}; const VkPipelineVertexInputStateCreateInfo* pipelineVertexInputState = &vis.state; if (missingAttribMask != 0 || hasPatchedVertexAttributes) { for (const auto& binding : vis.bindings) { syntheticVertexInputBuilder.AddBinding(binding.binding, binding.stride, binding.inputRate); } for (const auto& attribute : patchedAttributes) { syntheticVertexInputBuilder.AddAttribute(attribute.location, attribute.binding, attribute.format, attribute.offset); } Uint32 syntheticBinding = static_cast(vis.bindings.size()); for (Uint32 location = 0; location < kMaxVertexAttribs; ++location) { if ((missingAttribMask & (1u << location)) == 0) { continue; } VkFormat format = VK_FORMAT_UNDEFINED; const Bool supported = TryGetCurrentVertexAttributeFormat(programObj.vertexInputTypes[location], format); MOBILEGL_ASSERT(supported, "DirectVulkan does not support current generic vertex attribute type yet: program=%u location=%u type=0x%x activeAttribMask=0x%x vertexInputAttribMask=0x%x", program.GetExternalIndex(), location, programObj.vertexInputTypes[location], activeAttribMask, vertexInputAttribMask); syntheticVertexInputBuilder.AddBinding(syntheticBinding, 0, VK_VERTEX_INPUT_RATE_VERTEX); syntheticVertexInputBuilder.AddAttribute(location, syntheticBinding, format, 0); ++syntheticBinding; } syntheticVertexInputState = syntheticVertexInputBuilder.Build(); // Carry the divisor chain over. The synthetic rebuild copies bindings and // attributes only, and it keeps every real binding's INDEX, so the divisor // descriptions built for them stay valid - but dropping the pNext silently // demoted every instanced binding to divisor 1. This path runs whenever the // program declares an input the VAO does not feed (which is most capture // shaders: KHR-GL43.vertex_attrib_binding declares 16 inputs and enables three), // so the loss was near-total rather than a corner case. syntheticVertexInputState.pNext = vis.state.pNext; pipelineVertexInputState = &syntheticVertexInputState; } auto cullFaceEnabled = MGB_CTX->IsCapabilityEnabled(CapabilityInput::CullFace); auto depthTestEnabled = MGB_CTX->IsCapabilityEnabled(CapabilityInput::DepthTest); auto polygonOffsetFillEnabled = MGB_CTX->IsCapabilityEnabled(CapabilityInput::PolygonOffsetFill) && DrawModeUsesPolygonFill(mode); auto rasterizerDiscardEnabled = MGB_CTX->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard); auto colorLogicOpEnabled = MGB_CTX->IsCapabilityEnabled(CapabilityInput::ColorLogicOp) && m_logicOpFeatureEnabled; auto stencilTestEnabled = MGB_CTX->IsCapabilityEnabled(CapabilityInput::StencilTest); // A framebuffer without a depth (stencil) attachment behaves as if the depth // (stencil) test always passes and nothing is written - even when the bound // image is a packed depth-stencil texture attached through only one half. { #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer(); if (fbo && !fbo->IsDefault) { if (fbo->Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone) depthTestEnabled = false; if (fbo->Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone) stencilTestEnabled = false; } } else #endif { const auto& gatingFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); if (gatingFbo != nullptr && !gatingFbo->IsDefaultFramebuffer()) { const auto& depthAtt = gatingFbo->GetAttachment(MobileGL::FramebufferAttachmentType::Depth); const auto& stencilAtt = gatingFbo->GetAttachment(MobileGL::FramebufferAttachmentType::Stencil); if (!depthAtt.IsValid() || depthAtt.IsEmpty()) { depthTestEnabled = false; } if (!stencilAtt.IsValid() || stencilAtt.IsEmpty()) { stencilTestEnabled = false; } } } } const StencilFaceState& frontStencil = MGB_CTX->GetStencilState(StencilFace::Front); const StencilFaceState& backStencil = MGB_CTX->GetStencilState(StencilFace::Back); const VkPolygonMode requestedPolygonMode = MG_Util::ConvertPolygonModeToVkEnum(MGB_CTX->GetPolygonModeFront()); // VK_POLYGON_MODE_LINE/_POINT require the fillModeNonSolid device feature; fall back to // VK_POLYGON_MODE_FILL when the device lacks it. const VkPolygonMode effectivePolygonMode = (requestedPolygonMode == VK_POLYGON_MODE_FILL || m_fillModeNonSolidFeatureEnabled) ? requestedPolygonMode : VK_POLYGON_MODE_FILL; const VkPrimitiveTopology vkTopology = MG_Util::ConvertPrimitiveModeToVkEnum(mode); // Resolved by the caller (ResolvePrimitiveRestartEnable), which knows whether the draw is // indexed and with what index type; the capability bits alone answer neither. Bool primitiveRestartEnabled = primitiveRestartEnable; // GL applies restart to PATCHES only when PRIMITIVE_RESTART_FOR_PATCHES_SUPPORTED is true // (GL 4.6 core 10.3.6). MobileGL supports no such thing - neither backend has a way to // restart a patch stream - and GL_FALSE is a legal answer to that query, so a patch draw // simply never restarts here. Doing this BEFORE the feature guard below is what keeps a // perfectly ordinary GL_PATCHES draw from being refused on a device that lacks // VK_EXT_primitive_topology_list_restart. (When GL_PRIMITIVE_RESTART_FOR_PATCHES_SUPPORTED // is eventually added to glGetIntegerv it has to report GL_FALSE to stay consistent with // this.) if (vkTopology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST) { primitiveRestartEnabled = false; } // Primitive restart on a *list* topology requires the primitiveTopologyListRestart feature; // strip/fan restart works without it. There is no fallback - silently dropping the restarts // would weld the primitives on either side of each one together - so the draw is declined // here with the reason. // // Declined, not thrown. This used to THROW_EXCEPTION, which unwinds a C++ exception through // the C GL ABI and takes the process down (the hazard GL_Texture.cpp and RenderState.cpp // already name); an application that merely enabled a legal desktop feature died instead of // getting a draw that rendered nothing. VK_NULL_HANDLE is this function's established // "skip this draw" answer, used by the no-stages case above. // // Reached only when this draw's index stream really does restart. Testing the raw // capability bits here instead - which is what it did - refused every NON-INDEXED // list-topology draw as well, so an application that enables GL_PRIMITIVE_RESTART once at // init and then calls glDrawArrays(GL_TRIANGLES, ...) rendered nothing at all. const auto isListTopology = [](VkPrimitiveTopology t) { return t == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || t == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || t == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || t == VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY || t == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY; }; if (primitiveRestartEnabled && !m_primitiveTopologyListRestartFeatureEnabled && isListTopology(vkTopology)) { MGLOG_E_ONCE("Draw skipped: primitive restart on a list topology (0x%x) requires the " "primitiveTopologyListRestart device feature (VK_EXT_primitive_topology_list_restart), " "which this device does not support; use a strip/fan topology, or disable primitive " "restart for list-topology draws.", mode); return VK_NULL_HANDLE; } 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, and counted HERE rather than at the top of the walk: // the list-topology primitive-restart refusal above returns VK_NULL_HANDLE after // only ten of these reads have run, and a tally that fires before an early return // is an OVER-count, which breaks the lower-bound contract every other tally keeps. // // The 15 are: the six capability reads (cull face, depth test, polygon offset // fill, rasterizer discard, colour logic op, stencil test), the draw-FBO slot // read that gates depth/stencil, the two stencil face states, the polygon mode, // the min sample shading value, the patch vertex count, the depth mask, the depth // func, and the second draw-FBO slot read below. The sample-shading CAPABILITY // read is the one excluded: it sits behind && on m_sampleRateShadingFeatureEnabled // and does not run on a device without the feature. The other conditional reads - // the cull-mode ternary, the logic-op fetch, the two tessellation default-level // reads - are excluded for the same reason, so this stays a LOWER bound. MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 15); } PipelineFactory::PipelineCreatePayload payload { .programHash = programObj.hash, .vertexInputHash = vertexLayoutHash, .pipelineLayout = programObj.pipelineLayout, .renderPass = renderPassEntry.renderPass, #if MOBILEGL_BUILD_DISAGGREGATED .wireRenderPassCompatibilityId = wireRenderPassCompatibilityId, #endif .colorAttachmentCount = renderPassEntry.colorAttachmentCount, .rasterizationSamples = renderPassEntry.sampleCount, // ARB_sample_shading. Dropped on a device without sampleRateShading rather than // hard-failing the draw: the rate is a hint, and the pipeline renders correctly at the // driver's own rate. Both halves move the render state's PIPELINE version, so a cached // pipeline built at the old rate cannot be handed back for the new one. .sampleShadingEnable = m_sampleRateShadingFeatureEnabled && MGB_CTX->IsCapabilityEnabled(CapabilityInput::SampleShading), .minSampleShading = MGB_CTX->GetMinSampleShadingValue(), // Word 1 keeps its all-ones initialiser: GL has no state for samples 32..63. .sampleMask = {ResolveEffectiveSampleMask(renderPassEntry.sampleCount), 0xffffffffu}, .subpass = 0, .topology = vkTopology, .primitiveRestartEnable = primitiveRestartEnabled, .patchControlPoints = static_cast(MGB_CTX->GetPatchVertices()), .viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin), .polygonMode = effectivePolygonMode, .cullMode = cullFaceEnabled ? MG_Util::ConvertCullFaceModeToVkEnum(MGB_CTX->GetCullFaceMode(), invertClockwise) : VK_CULL_MODE_NONE, .frontFace = VK_FRONT_FACE_CLOCKWISE, // Read the geometry stage off the program's own shader list rather than // programObj.rasterizationProducerStage: that field is filled by the clip-fixup analysis, // which does not run for every program, so it reads Unknown for exactly the // geometry-plus-capture programs this guard exists to catch. Both inputs are link-time // facts folded into programObj.hash, which is what the pipeline memo and the // SetupDrawSnapshot fast path key on - so no memo can hand back a pipeline built for the // other mode. IsTransformFeedbackActive() would be a live bug here: neither memo key // moves on glBeginTransformFeedback. .provokingVertexMode = SelectProvokingVertexMode( vkTopology, ProgramCapturesXfbFromGeometryStage(program)), .depthTestEnable = depthTestEnabled, .depthWriteEnable = depthTestEnabled && MGB_CTX->GetDepthMask(), .depthBiasEnable = polygonOffsetFillEnabled, .rasterizerDiscardEnable = rasterizerDiscardEnabled, .logicOpEnable = colorLogicOpEnabled, .stencilTestEnable = stencilTestEnabled, .depthCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(MGB_CTX->GetDepthFunc()), .logicOp = MG_Util::ConvertLogicOperationToVkEnum(MGB_CTX->GetLogicOp()), .frontStencilFailOp = MG_Util::ConvertStencilOperationToVkEnum(frontStencil.FailOp), .frontStencilPassOp = MG_Util::ConvertStencilOperationToVkEnum(frontStencil.PassDepthPassOp), .frontStencilDepthFailOp = MG_Util::ConvertStencilOperationToVkEnum(frontStencil.PassDepthFailOp), .frontStencilCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(frontStencil.Func), .backStencilFailOp = MG_Util::ConvertStencilOperationToVkEnum(backStencil.FailOp), .backStencilPassOp = MG_Util::ConvertStencilOperationToVkEnum(backStencil.PassDepthPassOp), .backStencilDepthFailOp = MG_Util::ConvertStencilOperationToVkEnum(backStencil.PassDepthFailOp), .backStencilCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(backStencil.Func), .fragmentReplacesDepth = programObj.fragmentReplacesDepth, .stages = &programObj.stages, .vertexInputState = pipelineVertexInputState, .stageSpirvDigests = &programObj.stageSpirvDigests }; // A program with a tessellation evaluation stage and no control stage relies on GL's // fixed-function pass-through (GL 4.6 core 11.2.2), which Vulkan does not have. Build the // stage GL describes for THIS draw's patch size - PATCH_VERTICES is draw state, not link // state, so it is only knowable here - and hand it to the pipeline. Where the // pass-through cannot stand in for what the evaluation stage actually reads, nothing is // attached and CreatePipeline refuses the pipeline, which skips the draw. // // Gated on the PATCH topology as well, and that gate is load-bearing rather than an // optimisation: patchControlPoints is only meaningful for a patch draw, and a pipeline // that carries tessellation stages while its topology is anything else violates // VUID-VkGraphicsPipelineCreateInfo-topology-00737 - the same class of invalid input as // the missing control stage, on the same driver. Such a draw is illegal in GL too (a // program with a tessellation stage may only be drawn with GL_PATCHES), so nothing legal // loses its pass-through here; what it does lose is the pipeline, because the refusal // below then sees an evaluation stage with no control stage and declines. // // The default tessellation levels (glPatchParameterfv) are draw state for the same reason // and are compiled into the same module, so they are read here too and their key is mixed // into the pipeline hash - without that a pipeline memoised at one set of levels would be // handed back after the application changed them. if (programObj.needsPassthroughTessControl && programObj.passthroughTessControlEmulatable && vkTopology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST) { const FloatVec4& defaultOuterLevel = MGB_CTX->GetPatchDefaultOuterLevel(); const FloatVec2& defaultInnerLevel = MGB_CTX->GetPatchDefaultInnerLevel(); payload.passthroughTessControlKey = ProgramFactory::ComputePassthroughTessControlKey( payload.patchControlPoints, defaultOuterLevel, defaultInnerLevel, programObj.passthroughPerVertexMembers); payload.passthroughTessControlStage = m_programFactory->GetOrCreatePassthroughTessControlStage( payload.patchControlPoints, defaultOuterLevel, defaultInnerLevel, programObj.passthroughPerVertexMembers); } if (!payload.stencilTestEnable) { payload.frontStencilFailOp = VK_STENCIL_OP_KEEP; payload.frontStencilPassOp = VK_STENCIL_OP_KEEP; payload.frontStencilDepthFailOp = VK_STENCIL_OP_KEEP; payload.frontStencilCompareOp = VK_COMPARE_OP_ALWAYS; payload.backStencilFailOp = VK_STENCIL_OP_KEEP; payload.backStencilPassOp = VK_STENCIL_OP_KEEP; payload.backStencilDepthFailOp = VK_STENCIL_OP_KEEP; payload.backStencilCompareOp = VK_COMPARE_OP_ALWAYS; } const Bool hasDepthStencilAttachment = renderPassEntry.hasDepthStencilAttachment; if (!hasDepthStencilAttachment && (payload.depthTestEnable || payload.depthWriteEnable || payload.stencilTestEnable)) { MGLOG_D("GetOrCreatePipeline: disabling depth/stencil tests for program=%u because render pass has no depth attachment (attachmentCount=%u colorAttachmentCount=%u)", program.GetExternalIndex(), renderPassEntry.attachmentCount, renderPassEntry.colorAttachmentCount); payload.depthTestEnable = false; payload.depthWriteEnable = false; payload.stencilTestEnable = false; payload.depthCompareOp = VK_COMPARE_OP_ALWAYS; payload.frontStencilFailOp = VK_STENCIL_OP_KEEP; payload.frontStencilPassOp = VK_STENCIL_OP_KEEP; payload.frontStencilDepthFailOp = VK_STENCIL_OP_KEEP; payload.frontStencilCompareOp = VK_COMPARE_OP_ALWAYS; payload.backStencilFailOp = VK_STENCIL_OP_KEEP; payload.backStencilPassOp = VK_STENCIL_OP_KEEP; payload.backStencilDepthFailOp = VK_STENCIL_OP_KEEP; payload.backStencilCompareOp = VK_COMPARE_OP_ALWAYS; } const Uint32 fragmentOutputMask = programObj.activeFragmentOutputLocationMask; // Outputs at locations past the render pass's trimmed colour span are // simply discarded - GL's semantic for a fragment output whose draw // buffer is GL_NONE (the trailing UNUSED slots no longer occupy // references, see GetOrCreateRenderPass). if ((fragmentOutputMask >> payload.colorAttachmentCount) != 0) { MGLOG_D("GetOrCreatePipeline: fragmentOutputMask=0x%x exceeds colorAttachmentCount=%u for program=%u; " "outputs past the span are discarded", fragmentOutputMask, payload.colorAttachmentCount, program.GetExternalIndex()); } MOBILEGL_ASSERT(payload.colorAttachmentCount <= PipelineFactory::PipelineCreatePayload::kMaxColorAttachments, "GetOrCreatePipeline: colorAttachmentCount=%u exceeds payload capacity", payload.colorAttachmentCount); #if MOBILEGL_BUILD_DISAGGREGATED const auto* wireFbo = MG_Config::Transport != MG_Config::TransportMode::Monolith ? MG_Pipe::MGPipeApplier().DrawFramebuffer() : nullptr; const auto drawFboBinding = wireFbo ? SharedPtr{} : MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); #else const auto& drawFboBinding = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); #endif #if MOBILEGL_BUILD_DISAGGREGATED MOBILEGL_ASSERT(wireFbo != nullptr || drawFboBinding != nullptr, "GetOrCreatePipeline: draw framebuffer is null"); #else MOBILEGL_ASSERT(drawFboBinding != nullptr, "GetOrCreatePipeline: draw framebuffer is null"); #endif #if MOBILEGL_BUILD_DISAGGREGATED const Bool isDefaultDrawFbo = wireFbo ? wireFbo->IsDefault : drawFboBinding->IsDefaultFramebuffer(); Array wireDrawBuffers{}; if (wireFbo) for (SizeT i = 0; i < wireDrawBuffers.size(); ++i) wireDrawBuffers[i] = wireFbo->DrawBuffers[i] < 0 ? FramebufferAttachmentType::None : static_cast(static_cast(FramebufferAttachmentType::Color0) + wireFbo->DrawBuffers[i]); const auto& drawBuffers = wireFbo ? wireDrawBuffers : drawFboBinding->GetDrawBuffers(); #else const Bool isDefaultDrawFbo = drawFboBinding->IsDefaultFramebuffer(); const auto& drawBuffers = drawFboBinding->GetDrawBuffers(); #endif auto resolveCompleteColorAttachmentTexture = [&](Uint32 drawBufferIndex) -> MG_State::GLState::ITextureObject* { #if MOBILEGL_BUILD_DISAGGREGATED if (wireFbo) return nullptr; #endif if (isDefaultDrawFbo || drawBufferIndex >= drawBuffers.size()) { return nullptr; } const auto drawBuffer = drawBuffers[drawBufferIndex]; if (drawBuffer == FramebufferAttachmentType::None) { return nullptr; } const auto& attachment = drawFboBinding->GetAttachment(drawBuffer); if (!attachment.IsTexture() || !attachment.IsComplete()) { return nullptr; } return attachment.GetTexture().get(); }; for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) { BlendFactor srcRGB = BlendFactor::One; BlendFactor dstRGB = BlendFactor::Zero; BlendFactor srcAlpha = BlendFactor::One; BlendFactor dstAlpha = BlendFactor::Zero; BlendEquation colorEquation = BlendEquation::Add; BlendEquation alphaEquation = BlendEquation::Add; MGB_CTX->GetBlendFuncIndexed(i, srcRGB, dstRGB, srcAlpha, dstAlpha); MGB_CTX->GetBlendEquationIndexed(i, colorEquation, alphaEquation); const Bool blendEnabled = MGB_CTX->IsCapabilityEnabledIndexed(CapabilityInput::Blend, i); // Per-draw-buffer color write mask (glColorMaski). Divergent per-attachment masks require // the independentBlend device feature; when it is absent, fall back to draw buffer 0's // mask for every attachment (matching the non-indexed glColorMask broadcast). const BoolVec4 bufferMask = MGB_CTX->GetColorMaskIndexed(m_independentBlendFeatureEnabled ? i : 0); VkColorComponentFlags attachmentColorWriteMask = static_cast( (bufferMask.r() ? VK_COLOR_COMPONENT_R_BIT : 0u) | (bufferMask.g() ? VK_COLOR_COMPONENT_G_BIT : 0u) | (bufferMask.b() ? VK_COLOR_COMPONENT_B_BIT : 0u) | (bufferMask.a() ? VK_COLOR_COMPONENT_A_BIT : 0u)); Bool effectiveBlendEnabled = blendEnabled; MG_State::GLState::ITextureObject* colorAttachmentTexture = nullptr; MG_State::GLState::RenderbufferObject* colorAttachmentRenderbuffer = nullptr; if (isDefaultDrawFbo && i < drawBuffers.size() && drawBuffers[i] == FramebufferAttachmentType::None) { // The default framebuffer spans the same MAX_DRAW_BUFFERS slots as an FBO // (slot 0 is the back buffer, or None after glDrawBuffer(GL_NONE); slots // 1+ are always None). Discard writes and blend state for the None slots // like the FBO path below does, so stale indexed blend state on phantom // slots cannot leak into the pipeline - most notably into the blended // depth-write quirk's accumulation scan. attachmentColorWriteMask = 0; effectiveBlendEnabled = false; } #if MOBILEGL_BUILD_DISAGGREGATED if (wireFbo && !isDefaultDrawFbo) { const Int32 slot = wireFbo->DrawBuffers[i]; if (slot < 0 || wireFbo->Color[slot].Kind == MG_Pipe::kMGPipeSurfaceKindNone) { attachmentColorWriteMask = 0; effectiveBlendEnabled = false; } } else #endif if (!isDefaultDrawFbo && i < drawBuffers.size()) { const auto drawBuffer = drawBuffers[i]; colorAttachmentTexture = resolveCompleteColorAttachmentTexture(i); if (colorAttachmentTexture == nullptr && drawBuffer != FramebufferAttachmentType::None) { const auto& attachment = drawFboBinding->GetAttachment(drawBuffer); if (attachment.IsRenderbuffer() && attachment.IsComplete()) { colorAttachmentRenderbuffer = attachment.GetRenderbuffer().get(); } } if (drawBuffer == FramebufferAttachmentType::None || (colorAttachmentTexture == nullptr && colorAttachmentRenderbuffer == nullptr)) { // GL ignores writes and per-target blend state for GL_NONE draw buffer slots. // Depth-only or otherwise unattached draw buffers should also discard color writes. attachmentColorWriteMask = 0; effectiveBlendEnabled = false; } if (colorAttachmentRenderbuffer != nullptr) { const SizeT componentCount = MG_Util::GetBaseInternalFormatComponentCount( colorAttachmentRenderbuffer->GetInternalFormat()); attachmentColorWriteMask &= GetSupportedColorWriteMaskForComponentCount(componentCount); } if (colorAttachmentTexture != nullptr) { auto* texture = colorAttachmentTexture; #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG const auto* textureResource = m_textureManager->SyncTextureAndGetDescriptor(*texture); MOBILEGL_ASSERT(textureResource != nullptr, "GetOrCreatePipeline: failed to sync color attachment textureId=%d", texture->GetExternalIndex()); VkFormatProperties attachmentFormatProperties{}; vkGetPhysicalDeviceFormatProperties( m_physicalDevice.handle, textureResource->format, &attachmentFormatProperties); MOBILEGL_ASSERT( (attachmentFormatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0, "GetOrCreatePipeline: color attachment %u format=%d textureId=%d lacks VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT (program=%u)", i, static_cast(textureResource->format), texture->GetExternalIndex(), program.GetExternalIndex()); #endif const SizeT componentCount = MG_Util::GetBaseInternalFormatComponentCount(texture->GetFormat()); #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG const NumericDomain attachmentNumericDomain = GetNumericDomainForTextureInternalFormat(texture->GetFormat()); for (Uint32 outputLocation = 0; outputLocation < ProgramFactory::VkProgramObject::kMaxVertexInputLocations; ++outputLocation) { if ((programObj.activeFragmentOutputLocationMask & (1u << outputLocation)) == 0 || outputLocation != i) { continue; } const GLenum fragmentOutputType = programObj.fragmentOutputTypes[outputLocation]; const NumericDomain fragmentOutputDomain = GetNumericDomainForShaderValueType(fragmentOutputType); // GL allows fragment outputs with more components than the bound color attachment; // excess components are discarded during conversion to the attachment format. MOBILEGL_ASSERT( attachmentNumericDomain == NumericDomain::Unknown || fragmentOutputDomain == NumericDomain::Unknown || attachmentNumericDomain == fragmentOutputDomain, "GetOrCreatePipeline: fragment output location=%d type=%u mismatches color attachment %u internalFormat=%d textureId=%d program=%u", static_cast(outputLocation), static_cast(fragmentOutputType), i, static_cast(texture->GetFormat()), texture->GetExternalIndex(), program.GetExternalIndex()); } #endif const VkColorComponentFlags supportedColorWriteMask = GetSupportedColorWriteMaskForComponentCount(componentCount); if ((attachmentColorWriteMask & ~supportedColorWriteMask) != 0) { MGLOG_W_ONCE( "GetOrCreatePipeline: clamping colorWriteMask=0x%x to 0x%x on color attachment %u (componentCount=%zu textureId=%d internalFormat=%d program=%u blendEnabled=%d)", static_cast(attachmentColorWriteMask), static_cast(attachmentColorWriteMask & supportedColorWriteMask), i, componentCount, texture->GetExternalIndex(), static_cast(texture->GetFormat()), program.GetExternalIndex(), effectiveBlendEnabled ? 1 : 0); attachmentColorWriteMask &= supportedColorWriteMask; } } } if (effectiveBlendEnabled) { MOBILEGL_ASSERT(i < drawBuffers.size(), "GetOrCreatePipeline: color attachment %u is out of draw buffer range %zu", i, drawBuffers.size()); VkFormat colorAttachmentFormat = VK_FORMAT_UNDEFINED; Int textureExternalIndex = -1; #if MOBILEGL_BUILD_DISAGGREGATED if (wireFbo && !isDefaultDrawFbo) { const Int32 slot = wireFbo->DrawBuffers[i]; colorAttachmentFormat = ResolveWireImage(*wireFbo, wireFbo->Color[slot], VK_IMAGE_ASPECT_COLOR_BIT).format; } else #endif if (isDefaultDrawFbo) { colorAttachmentFormat = m_swapchainObject.GetSurfaceFormat().format; } else if (colorAttachmentRenderbuffer != nullptr) { textureExternalIndex = static_cast(colorAttachmentRenderbuffer->GetExternalIndex()); // The SAME resolver GetOrCreateRenderbufferResource backs the image with, so the // probe cannot ask about a format the attachment does not have. The strict 1:1 // converter is the wrong question here and answered VK_FORMAT_UNDEFINED for // RGBA2/RGBA12/RGB10/RGB12/RGB16 and the packed 16-bit formats for RGBA4/RGB5_A1 // - and VkFormatProperties for UNDEFINED are all zero, so blending was // force-disabled forever on attachments that blend perfectly well. Every // three-channel colour renderbuffer was in that set too (R8G8B8_UNORM is rarely // supported), which is the more ordinary shape. // // Resolved rather than looked up: GetOrCreateRenderbufferResource creates images // and bumps epochs, which a pipeline-state query must not do as a side effect. // A renderbuffer has no device-fallback step after the resolver (unlike the // texture path's D24 -> D32 substitution), so the resolver IS its live format. colorAttachmentFormat = ResolveTextureFormatInfo(colorAttachmentRenderbuffer->GetInternalFormat()).format; } else { auto* texture = colorAttachmentTexture; MOBILEGL_ASSERT(texture != nullptr, "GetOrCreatePipeline: blend is enabled on draw buffer %u but no complete color attachment is bound", i); textureExternalIndex = texture->GetExternalIndex(); auto* textureResource = m_textureManager->SyncTextureAndGetDescriptor(*texture); MOBILEGL_ASSERT(textureResource != nullptr, "GetOrCreatePipeline: failed to sync blend color attachment textureId=%d", texture->GetExternalIndex()); colorAttachmentFormat = textureResource->format; } // Blending on an attachment whose format lacks // VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT is invalid pipeline state // (blend support is optional for e.g. 32-bit float formats on some GPUs); // force-disable it instead of baking undefined behavior into the pipeline. static UnorderedMap formatBlendSupport; auto blendSupportIt = formatBlendSupport.find(static_cast(colorAttachmentFormat)); if (blendSupportIt == formatBlendSupport.end()) { VkFormatProperties formatProperties{}; vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, colorAttachmentFormat, &formatProperties); const Bool blendable = (formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) != 0; blendSupportIt = formatBlendSupport.emplace(static_cast(colorAttachmentFormat), blendable).first; if (!blendable) { MGLOG_E_ONCE("GetOrCreatePipeline: format=%d lacks VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT; " "disabling blending on attachments with this format (first hit: attachment %u textureId=%d program=%u)", static_cast(colorAttachmentFormat), i, textureExternalIndex, program.GetExternalIndex()); if (PipelineFactory::IsSuppressBlendedDepthWriteEnabled()) { // With blending force-disabled the blended depth-write quirk can // never fire for pipelines on this format, so a depth-equality // chain that accumulates into it (MC 26.3 OIT depth_bounds on // RGBA32F) keeps its depth writes and may flicker on this driver. MGLOG_W_ONCE("GetOrCreatePipeline: format=%d is not blendable, so the blended " "depth-write quirk cannot apply to it; depth-equality chains " "accumulating into this format may flicker", static_cast(colorAttachmentFormat)); } } } if (!blendSupportIt->second) { effectiveBlendEnabled = false; } } // Dual-source blending (GL_SRC1_* factors from glBlendFunc paired with // glBindFragDataLocationIndexed) requires the dualSrcBlend device feature. It is detected // at device creation and surfaced in the POST; there is no fallback that BLENDS correctly, // so a draw that asks for a SRC1 factor on a device without the feature gets the blend // DECLINED - this attachment is baked with blending off and neutral One/Zero factors, and // the loss is logged once. Both the factors AND the enable have to be neutralised: // VUID-VkPipelineColorBlendAttachmentState-srcColorBlendFactor-00608 and its three // siblings forbid a VK_BLEND_FACTOR_SRC1_* in the struct without the feature whatever // blendEnable says, so clearing only the enable would still be invalid pipeline state. // The previous behaviour, throwing, took the whole process down over one unsupported // blend factor; this is defined, survivable and visible in the log, and it matches what // the non-blendable-format arm above already does. if (!m_dualSrcBlendFeatureEnabled && (IsDualSourceBlendFactor(srcRGB) || IsDualSourceBlendFactor(dstRGB) || IsDualSourceBlendFactor(srcAlpha) || IsDualSourceBlendFactor(dstAlpha))) { MGLOG_E_ONCE( "GetOrCreatePipeline: dual-source blending (GL_SRC1_* blend factor) was requested on " "color attachment %u, but the Vulkan device does not support the dualSrcBlend feature " "(see the dualSrcBlend row in the driver POST). Blending is DECLINED on that " "attachment - the fragment's first output is written unblended and the second source " "is dropped (program=%u)", i, program.GetExternalIndex()); effectiveBlendEnabled = false; srcRGB = BlendFactor::One; dstRGB = BlendFactor::Zero; srcAlpha = BlendFactor::One; dstAlpha = BlendFactor::Zero; } payload.colorBlendAttachments[i] = MakeColorBlendAttachmentState( effectiveBlendEnabled, MG_Util::ConvertBlendFactorToVkEnum(srcRGB), MG_Util::ConvertBlendFactorToVkEnum(dstRGB), MG_Util::ConvertBlendEquationToVkEnum(colorEquation), MG_Util::ConvertBlendFactorToVkEnum(srcAlpha), MG_Util::ConvertBlendFactorToVkEnum(dstAlpha), MG_Util::ConvertBlendEquationToVkEnum(alphaEquation), attachmentColorWriteMask); } #if MOBILEGL_BUILD_DISAGGREGATED // P7 wave 2-B: MOBILEGL_TEST_PIPELINE_CREATE_DELAY_MS. // // The Redmi's OpenRA divergence (exit gate 3) is timing-shaped and reproduces ONLY with a // cold driver pipeline cache: three cleared-cache runs give a bit-identical 0.976494 / // 14658 mismatched pixels, while a warm cache gives 1.000000 from the second run on. What // a cold cache changes is how long vkCreateGraphicsPipelines takes on the apply thread, // so this knob buys that time on a host whose cache is always warm. INERT unless set, and // compiled only into the disaggregated build - the monolith image, which G1 pins, never // sees these lines. The delay goes on the MISS path only: a memo or cache hit is not // what is slow on the device either. if (const Uint32 delayMs = MagmaTestPipelineCreateDelayMs(); delayMs != 0 && MG_Config::Transport != MG_Config::TransportMode::Monolith) { std::this_thread::sleep_for(std::chrono::milliseconds(delayMs)); } #endif VkPipeline pipeline = m_pipelineFactory->GetOrCreatePipeline(payload); if (pipeline != VK_NULL_HANDLE) { PipelineMemoEntry& entry = m_pipelineMemo[m_pipelineMemoNext]; entry.mode = mode; entry.programHash = programObj.hash; entry.vertexInputHash = vertexLayoutHash; entry.renderPassHash = renderPassHash; #if MOBILEGL_BUILD_DISAGGREGATED entry.wireRenderPassCompatibilityId = wireRenderPassCompatibilityId; #endif entry.pipelineStateHash = pipelineStateHash; #if MOBILEGL_PIPE_PUSH entry.renderStateCso = renderStateCso; #endif entry.primitiveRestartEnable = primitiveRestartEnable; entry.transformFlags = transformFlags; entry.pipeline = pipeline; m_pipelineMemoNext = (m_pipelineMemoNext + 1) % kPipelineMemoSize; m_pipelineMemoCount = std::min(m_pipelineMemoCount + 1, kPipelineMemoSize); } return pipeline; } Bool VulkanRenderer::PrepareStorageImageTextures( FrameContext::FrameData& frame, const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj) { if (!programObj.hasStorageImages) { return true; } auto& storageTextures = m_storageImageTexturesScratch; if (!m_uniformManager->CollectStorageImageTextures(program, programObj, storageTextures)) { MGLOG_E_ONCE("%s: failed to collect storage images for program=%u", __func__, program.GetExternalIndex()); return false; } if (storageTextures.empty()) { return true; } // Steady-state fast path: when every collected texture is already resident in GENERAL // with no pending clear and no dirty content, the loop below has nothing to record, so // keep the render pass alive instead of splitting it on every storage-image draw (on // tiled GPUs each split is a full tile load/store). GL makes cross-draw image-store // coherence the app's job (glMemoryBarrier), so no implicit barrier is owed here. // Record every image-unit binding before probing anything: a texture whose image was // created without STORAGE usage (the default - it costs UBWC compression on Adreno) // needs a recreate, and the probe below is what ends the render pass so that recreate // lands here rather than mid-pass. This cannot be folded into the probe loop, which // stops at the first texture that needs work and would leave the rest unmarked. for (auto* texture : storageTextures) { MOBILEGL_ASSERT(texture != nullptr, "%s: collected a null storage texture", __func__); m_textureManager->MarkStorageImageTexture(*texture); } Bool anyNeedsPreparation = false; for (auto* texture : storageTextures) { if (m_textureManager->NeedsStorageImagePreparation(*texture) || m_clearManager->HasPendingClear(texture)) { anyNeedsPreparation = true; break; } } if (!anyNeedsPreparation) { return true; } // A first-time storage-usage upgrade recreates the image and carries the old contents // forward with an out-of-band, immediately-submitted copy (PreserveTextureContentsOnRecreate). // Whatever this frame already recorded into the old image is still sitting unsubmitted in // this command buffer, so that copy would read pre-frame content and this frame's rendering // into the texture would be lost - precisely the render-target-then-image-unit case this // whole path exists for. Submit what is recorded first; the copy then queues behind it. Bool anyNeedsStorageUpgrade = false; for (auto* texture : storageTextures) { if (m_textureManager->NeedsStorageUsageUpgrade(*texture)) { anyNeedsStorageUpgrade = true; break; } } if (anyNeedsStorageUpgrade && HasPendingRecordedWork()) { if (FlushPendingCommands()) { // Fresh command buffer: the sampled-descriptor-set memo describes bindings that // only existed in the retired one. FlushPendingCommands drops the pipeline memo // itself; this is the other command-buffer-scoped cache. m_lastSampledSetValid = false; } else { // Best effort: the upgrade still produces a correct image, only its preserved // contents may predate this frame's writes. Dropping the draw would be worse. MGLOG_E_ONCE("%s: flush before a storage-usage image upgrade failed; preserved contents " "may be stale for one frame", __func__); } } if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); } // Image uploads, deferred-clear materialization, and layout barriers are illegal inside // a classic render pass. Do this before sampler preparation as well: a texture used by // both a sampler and an image must stay in GENERAL, and both descriptors must name that // same layout independent of SPIR-V reflection/binding order. if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } for (auto* texture : storageTextures) { if (!MaterializePendingClearForTexture(frame.commandBuffer, *texture)) { MGLOG_E_ONCE("%s: failed to materialize pending clear for storage textureId=%d", __func__, texture->GetExternalIndex()); return false; } if (!m_textureManager->TransitionTextureForStorageImage(frame.commandBuffer, *texture)) { MGLOG_E_ONCE("%s: failed to prepare storage textureId=%d", __func__, texture->GetExternalIndex()); return false; } } return true; } Bool VulkanRenderer::PrepareSamplerImageFeedbackSnapshots( FrameContext::FrameData& frame, const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, VkPipelineStageFlags consumerShaderStageMask) { auto& feedbackBindings = m_samplerImageFeedbackScratch; auto& overrides = m_samplerImageBindingOverridesScratch; overrides.clear(); if (!programObj.hasStorageImages) { feedbackBindings.clear(); return true; } if (!m_uniformManager->CollectSamplerImageFeedback(program, programObj, feedbackBindings)) { MGLOG_E_ONCE("%s: failed to collect sampler/image feedback for program=%u", __func__, program.GetExternalIndex()); return false; } if (feedbackBindings.empty()) { return true; } // Copy and layout barriers cannot be recorded inside a render pass. A graphics draw only // gets here after an actual sampled/writable-image mip overlap was found, so ordinary // graphics draws retain the active pass. if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } struct SnapshotCacheEntry { MG_State::GLState::ITextureObject* texture = nullptr; SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown; VkTextureManager::SampledTextureSnapshot snapshot{}; }; Vector snapshotCache; snapshotCache.reserve(feedbackBindings.size()); overrides.reserve(feedbackBindings.size()); for (const auto& feedback : feedbackBindings) { VkTextureManager::SampledTextureSnapshot snapshot{}; const auto existing = std::find_if( snapshotCache.begin(), snapshotCache.end(), [&feedback](const SnapshotCacheEntry& candidate) { return candidate.texture == feedback.texture && candidate.numericDomain == feedback.numericDomain; }); if (existing != snapshotCache.end()) { snapshot = existing->snapshot; } else { if (!m_textureManager->SnapshotTextureForSampling(frame.commandBuffer, *feedback.texture, feedback.numericDomain, consumerShaderStageMask, snapshot) || snapshot.imageView == VK_NULL_HANDLE) { MGLOG_E_ONCE("%s: failed to snapshot textureId=%d for sampler binding=%u element=%u", __func__, feedback.texture != nullptr ? feedback.texture->GetExternalIndex() : 0, feedback.samplerBinding, feedback.samplerElement); return false; } snapshotCache.push_back({.texture = feedback.texture, .numericDomain = feedback.numericDomain, .snapshot = snapshot}); } overrides.push_back({ .binding = feedback.samplerBinding, .element = feedback.samplerElement, .texture = feedback.texture, .sampler = feedback.sampler, .imageView = snapshot.imageView, .imageLayout = snapshot.layout, .forceNearestFiltering = feedback.numericDomain == SamplerNumericDomain::SignedInteger || feedback.numericDomain == SamplerNumericDomain::UnsignedInteger, }); if (program.GetExternalIndex() == 194 && feedback.texture->GetExternalIndex() == 75) { MGLOG_D_ONCE("sampler/image feedback snapshot: program=194 texture=75 binding=%u element=%u view=%p", feedback.samplerBinding, feedback.samplerElement, snapshot.imageView); } } return true; } // The scissor rectangle Vulkan needs for ARB_viewport_array index `index`. Vulkan has no // per-viewport scissor-test TOGGLE - a scissor rectangle always applies - so an index whose // GL scissor test is disabled gets the whole framebuffer, which is exactly "the test always // passes" (GL 4.6 core 17.3.2). VkRect2D VulkanRenderer::ComputeGLScissorRect(Uint32 index, const IntVec2& extent, VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo) const { const auto& parameters = MGB_CTX->GetRenderStateParameters(); if ((parameters.ScissorTestEnabledMask & (1u << index)) == 0) { VkRect2D full{}; full.offset = {0, 0}; full.extent = {static_cast(extent.x()), static_cast(extent.y())}; return full; } const IntVec4& scissorBox = parameters.ScissorBoxes[index]; return isDefaultFbo ? MakeDefaultFramebufferScissorRect(scissorBox, extent, preTransform) : MakeClampedScissorRect(scissorBox, extent); } // The wide half of ApplyDynamicDrawStateTail: a pipeline built for a gl_ViewportIndex-writing // program declares viewportCount > 1, and Vulkan then requires that many viewports AND that // many scissors to have been set before the draw // (VUID-vkCmdDraw-viewportCount-03417/-03418). Deliberately unmemoized: only conformance // shaders reach it, the single-element dynamic-state shadow cannot describe an array, and // leaving that shadow invalidated is what makes the next ordinary draw re-push its own // single viewport instead of believing the array's element 0 is already bound. void VulkanRenderer::ApplyMultiViewportDynamicState(VkCommandBuffer commandBuffer, Uint32 viewportCount, const IntVec2& extent, VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo) { MOBILEGL_ASSERT(viewportCount <= RenderStateParameters::MAX_VIEWPORTS, "ApplyMultiViewportDynamicState: viewportCount=%u exceeds the indexed state width", viewportCount); const Uint32 count = std::min(viewportCount, RenderStateParameters::MAX_VIEWPORTS); Array viewports{}; Array scissors{}; for (Uint32 i = 0; i < count; ++i) { viewports[i] = ComputeGLViewport(i, extent, preTransform, isDefaultFbo); scissors[i] = ComputeGLScissorRect(i, extent, preTransform, isDefaultFbo); } vkCmdSetViewport(commandBuffer, 0, count, viewports.data()); vkCmdSetScissor(commandBuffer, 0, count, scissors.data()); auto& shadow = g_dynamicStateShadow; shadow.viewportValid = false; shadow.scissorValid = false; shadow.dynamicTailValid = false; } void VulkanRenderer::ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo, Uint32 viewportCount) { auto& shadow = g_dynamicStateShadow; if (viewportCount > 1) { // The other five Apply* still run: blend constants, depth bias, line width and the // stencil masks are not per-viewport and a multi-viewport draw needs them just as // much. Only the viewport/scissor pair takes the array shape. ApplyBlendConstants(frame.commandBuffer); ApplyPolygonOffsetState(frame.commandBuffer); ApplyLineWidthState(frame.commandBuffer); ApplyStencilState(frame.commandBuffer); ApplyMultiViewportDynamicState(frame.commandBuffer, viewportCount, extent, m_swapchainObject.GetPreTransform(), isDefaultFbo); return; } // One compare for the whole tail: see the gate's declaration in // DynamicStateShadow for why (version, extent, default-FBO flag) pins every // input the six Apply* below read. // // P2 D12.3: this read is RE-SOURCED, not re-shaped. Under MOBILEGL_PIPE_PUSH the // accessor no longer walks into GLContext's RenderState - it returns // PipeInputs::m_renderStateParametersVersion, which the applier publishes from // MGPDynamicState::Version (set_dynamic_state) and MGPBindRenderState::Version // (bind_render_state). So the gate now reads what the client PUSHED. // // What it does NOT do, and the P2 brief expects it to, is stop moving on a // pipeline-only change. The tree settles that against the brief: bind_render_state // carries m_version too and the applier publishes it, and it has to - Espryt's // SyncRenderState uses the very same counter as its all-state change detector and G5 // forbids touching it, so a bind that rewrote the pipeline half while leaving the // counter still would make Espryt skip re-syncing the blend state it just changed. // The second-level DynamicTailKey compare below is therefore what actually absorbs a // pipeline-only change, exactly as it did before P2: one key build, no vkCmd*. const Uint paramsVersion = MGB_CTX->GetRenderStateParametersVersion(); 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 // exactly the tail's inputs (inventory in DynamicTailKey) out of one bulk // parameters fetch and compare; an equal key means every Apply* below would // re-derive the value its shadow already holds. DynamicStateShadow::DynamicTailKey key; { const RenderStateParameters& p = MGB_CTX->GetRenderStateParameters(); // Viewport 0 and its depth range: ApplyGLViewportState reads exactly those two // (per-index state for indices > 0 is keyed separately, see multiViewportKey below). key.viewport[0] = p.Viewports[0].x(); key.viewport[1] = p.Viewports[0].y(); key.viewport[2] = p.Viewports[0].z(); key.viewport[3] = p.Viewports[0].w(); key.depthRange[0] = p.DepthRanges[0].x(); key.depthRange[1] = p.DepthRanges[0].y(); key.blendColor[0] = p.BlendColor.x(); key.blendColor[1] = p.BlendColor.y(); key.blendColor[2] = p.BlendColor.z(); key.blendColor[3] = p.BlendColor.w(); key.polygonOffsetFactor = p.PolygonOffsetFactor; key.polygonOffsetUnits = p.PolygonOffsetUnits; key.lineWidth = p.LineWidth; // StencilStates[0] is Front, [1] is Back (RenderState::GetStencilFaceIndex), // the same order ApplyStencilState reads them in. for (Uint32 face = 0; face < 2; ++face) { key.stencilValueMask[face] = p.StencilStates[face].ValueMask; key.stencilWriteMask[face] = p.StencilStates[face].WriteMask; key.stencilRef[face] = p.StencilStates[face].Ref; } key.scissorEnabled = (p.ScissorTestEnabledMask & 1u) != 0; key.scissorBox[0] = p.ScissorBoxes[0].x(); key.scissorBox[1] = p.ScissorBoxes[0].y(); key.scissorBox[2] = p.ScissorBoxes[0].z(); key.scissorBox[3] = p.ScissorBoxes[0].w(); key.extentX = extent.x(); key.extentY = extent.y(); key.preTransform = static_cast(preTransform); key.isDefaultFbo = isDefaultFbo; } if (shadow.dynamicTailValid && shadow.dynamicTailKey == key) { // Re-arm the cheap version gate so an unchanged-parameters run of draws after // this one costs the four-integer compare again. shadow.dynamicTailParamsVersion = paramsVersion; return; } ApplyGLViewportState(frame.commandBuffer, extent, preTransform, isDefaultFbo); ApplyBlendConstants(frame.commandBuffer); ApplyPolygonOffsetState(frame.commandBuffer); ApplyLineWidthState(frame.commandBuffer); ApplyStencilState(frame.commandBuffer); VkRect2D scissor{}; if (key.scissorEnabled) { const IntVec4 scissorBox(key.scissorBox[0], key.scissorBox[1], key.scissorBox[2], key.scissorBox[3]); scissor = isDefaultFbo ? MakeDefaultFramebufferScissorRect(scissorBox, extent, preTransform) : MakeClampedScissorRect(scissorBox, extent); } else { scissor.offset = {0, 0}; scissor.extent = { (Uint)extent.x(), (Uint)extent.y() }; } ShadowedSetScissor(frame.commandBuffer, scissor); shadow.dynamicTailValid = true; shadow.dynamicTailParamsVersion = paramsVersion; shadow.dynamicTailExtentX = extent.x(); shadow.dynamicTailExtentY = extent.y(); shadow.dynamicTailIsDefaultFbo = isDefaultFbo; shadow.dynamicTailKey = key; } Uint32 VulkanRenderer::GetBaseTransformFlagsRaw(Bool isDefaultFbo) { // GetShaderTransformFlags is a function of the pre-transform AND of whether // the bound draw framebuffer is the default one (the Y-flip/rotation bits // apply only when presenting). Memo keyed on both; keying on the // pre-transform alone served an FBO pass's unflipped flags to the following // default-framebuffer pass and flipped the whole frame. // isDefaultFbo is supplied by the caller: every draw-path caller has already // resolved the bound draw framebuffer (and its default-ness) for its own // guards, and re-walking the binding slot + the virtual IsDefaultFramebuffer // per draw showed up in the profile. Callers MUST pass the value derived from // the SAME draw-framebuffer binding the draw uses - see the assert below. MOBILEGL_ASSERT( [&] { #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer(); return isDefaultFbo == (fbo != nullptr && fbo->IsDefault); } #endif const auto& fbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); return isDefaultFbo == (fbo != nullptr && fbo->IsDefaultFramebuffer()); }(), "GetBaseTransformFlagsRaw: isDefaultFbo does not match the bound draw framebuffer"); const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform(); if (!m_baseTransformFlagsKeyValid || preTransform != m_baseTransformFlagsPreTransform || isDefaultFbo != m_baseTransformFlagsIsDefaultFbo) { m_baseTransformFlagsCache = GetShaderTransformFlags(preTransform).GetRaw(); m_baseTransformFlagsPreTransform = preTransform; m_baseTransformFlagsIsDefaultFbo = isDefaultFbo; m_baseTransformFlagsKeyValid = true; } return m_baseTransformFlagsCache; } Bool VulkanRenderer::TrySetupDrawFastPath(FrameContext::FrameData& frame, GLenum mode, Flags aspects, const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView) { if (!frame.isCommandRecording) { return false; } // Entry select: by the draw program's lifetime id, MRU first (the id pins // the entry; every other fact is re-guarded below, so probing a stale // entry can only decline, never serve stale state). const auto& program = *MGB_CTX->GetProgramForDraw(); const Uint64 programLifetimeId = program.GetLifetimeId(); SetupDrawSnapshot* snapPtr = nullptr; { SetupDrawSnapshot& mru = m_setupDrawSnapshots[m_setupDrawSnapshotMru]; if (mru.valid && mru.programLifetimeId == programLifetimeId) { snapPtr = &mru; } else { for (Uint32 i = 0; i < kSetupDrawSnapshotCount; ++i) { SetupDrawSnapshot& candidate = m_setupDrawSnapshots[i]; if (candidate.valid && candidate.programLifetimeId == programLifetimeId) { snapPtr = &candidate; m_setupDrawSnapshotMru = i; break; } } } } if (snapPtr == nullptr) { return false; } SetupDrawSnapshot& snap = *snapPtr; // Resolved once for the whole function: it guards the snapshot, keys the pipeline memo // probe below, and is handed to GetOrCreatePipeline on a miss - all three must agree. const Bool drawPrimitiveRestartEnable = ResolvePrimitiveRestartEnable(aspects, pIndexBufferView); if (snap.aspects != aspects.GetRaw() || snap.mode != mode || snap.primitiveRestartEnable != drawPrimitiveRestartEnable) { return false; } if (m_clearManager->HasAnyPendingClears()) { return false; } const auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); if (activeRenderPass == nullptr || activeRenderPass->hash != snap.renderPassHash || snap.imageIndex != m_imageIndexAcquired) { return false; } if (program.GetBackendStateVersion() != snap.programVersion) { return false; } // glBegin/EndTransformFeedback moves no key this fast path otherwise observes // (the design makes capture a compile-option FLAG precisely because no version // bumps, VulkanRenderer.h's pipeline-memo note) - but the snapshot bakes that // flag into resolvedTransformFlags and the pipeline. Recompute the one dynamic // bit (the full path's exact predicate) and decline on a mismatch, or the first // captured draw after glBeginTransformFeedback would bind the undecorated // variant and silently capture nothing while the CPU bookkeeping advances. const Bool wantsXfbCapture = m_transformFeedbackFeatureEnabled && MGB_CTX->IsTransformFeedbackActive() && program.GetTransformFeedbackVaryingCount() > 0; const Bool snapHasXfbCapture = static_cast(ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags) & ProgramFactory::CompileOptionBit::XfbCapture); if (wantsXfbCapture != snapHasXfbCapture) { return false; } // A changed VAO does NOT decline: the VAO only feeds the pipeline's vertex // input state (re-resolved below through the layout-keyed memo, so N VAOs // sharing one attribute layout share one pipeline) and the vertex/index // buffer binds (re-run every draw anyway). Declining here would send every // draw of a VAO-cycling stream (Minecraft chunk rendering) through the full // path, re-resolving descriptors and texture layouts nothing invalidated. const auto& vao = *MGB_CTX->GetBoundVertexArray(); #if MOBILEGL_PIPE_PUSH // P2 D12.4: the handle replaces the (address, lifetime id) pair here too - one // compare instead of two, and the same identity the VAO draw memo is keyed on, so // the two cannot disagree about whether "the VAO moved". The config version stays: // it answers a different question (did this same object's layout change). const Bool vaoMoved = MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput) ? (!(ResolveVaoHandle(vao) == snap.vaoHandle) || vao.GetConfigVersion() != snap.vaoConfigVersion) : (static_cast(&vao) != snap.vao || vao.GetLifetimeId() != snap.vaoLifetimeId || vao.GetConfigVersion() != snap.vaoConfigVersion); #else const Bool vaoMoved = static_cast(&vao) != snap.vao || vao.GetLifetimeId() != snap.vaoLifetimeId || vao.GetConfigVersion() != snap.vaoConfigVersion; #endif const auto& drawFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); if (static_cast(drawFbo.get()) != snap.drawFbo || drawFbo->GetLifetimeId() != snap.drawFboLifetimeId || drawFbo->GetObjectVersion() != snap.fboVersion) { return false; } // The two monotonic counters get a shadow-compare rescue instead of an // unconditional decline: both bump on state writes whose VALUE often lands // back on what the snapshot already describes (a GL_BLEND toggle between // two draws, a redundant glBindSampler), and declining here sends every // such draw through the full SetupDraw. const Uint renderStateVersion = MGB_CTX->GetPipelineStateVersion(); const Uint64 bindGeneration = MGB_CTX->GetTextureBindGeneration(); const Bool renderStateMoved = renderStateVersion != snap.renderStateVersion; const Bool bindsMoved = bindGeneration != snap.bindGeneration; if (renderStateMoved) { // Only the pipeline depends on the moved state - except the render-pass // flavor input (depth/stencil participation); a flip of that must take // the full path's pass selection. One bulk parameters fetch instead of // two capability-accessor calls; both are pure reads of the same fields. const RenderStateParameters& rsp = MGB_CTX->GetRenderStateParameters(); const Bool drawUsesDepthStencil = rsp.DepthTestEnabled || rsp.StencilTestEnabled; if (drawUsesDepthStencil != snap.drawUsesDepthStencil) { return false; } } // The FBO identity+version compare above proved this draw's framebuffer is the // snapshotting draw's, so its default-ness is the snapshot's too - no second walk // of the binding slot and no virtual IsDefaultFramebuffer call. if (GetBaseTransformFlagsRaw(snap.drawFboIsDefault) != snap.baseTransformFlags) { return false; } if (m_textureManager->GetResourceEraseEpoch() != snap.textureEraseEpoch || m_textureManager->GetTextureImageEpoch() != snap.textureImageEpoch || m_renderPassManager->GetRenderbufferImageEpoch() != snap.renderbufferImageEpoch) { return false; } // Program entry: (lifetimeId, backend-state version, resolved flags) were proven // equal above, and those pin the factory hash - so the snapshot's memoised entry // pointer IS this draw's entry while the factory's open-addressing cache has not // moved entries (structure epoch). Bypassing GetOrCreateProgram skips its use // stamp, so re-stamp here or the idle sweep could evict a live entry. const ProgramFactory::VkProgramObject* programObjPtr = snap.programObj; if (programObjPtr != nullptr && snap.programFactoryEpoch == m_programFactory->GetCacheStructureEpoch()) { m_programFactory->StampProgramUse(*programObjPtr); } else { programObjPtr = &m_programFactory->GetOrCreateProgram( program, ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags)); snap.programObj = programObjPtr; snap.programFactoryEpoch = m_programFactory->GetCacheStructureEpoch(); } const auto& programObj = *programObjPtr; // Pinned for BeginXfbCaptureForDraw, which otherwise decides from GL state alone and has // no way to know the bound pipeline's last pre-rasterization module lost (or never got) // its Xfb execution mode. See VkProgramObject::xfbCaptureDeclined. m_currentDrawXfbCaptureDeclined = programObj.xfbCaptureDeclined; // A refused program cannot reach here today - the full path refuses before it ever // records a snapshot - but declining the fast path costs one compare and means the // refusal does not depend on that ordering staying true. if (programObj.pointSizeCapabilityUnsupported) { return false; } // The pipeline and the vertex-input pre-flight depend on the VAO only through // its resolved LAYOUT (layoutHash folds the attribute formats, bindings and the // unsupported mask; the masks below are functions of the same configuration), // never its identity. A VAO-cycling stream (Minecraft chunk rendering) swaps // hundreds of VAOs sharing one layout per frame: answer "same layout?" from the // VAO's aux memo - it sits next to the config-version word this compare chain // already loaded - instead of chasing the vertex-input factory's cold heap entry. Uint64 vaoLayoutHash = snap.vaoLayoutHash; Bool vaoLayoutMoved = false; if (vaoMoved) { // Read the layout facts through the flat per-VAO memo table, keyed by the // VAO's content-hash memo. The hash memo shares the cache line this compare // chain already loaded (the config version), and the table slot is compact // and hot - unlike the VAO's aux-memo words, which start a second cold line // of every object in a VAO-cycling frame. The slot only ever answers for // THIS object: LookupVaoDrawMemo matches (address, lifetime id), so a slot // a destroyed VAO left behind at a recycled address misses and the facts // are re-resolved. The contentHash compare is the second gate on top of // that identity check, catching a reconfiguration of the same live object. Uint64 auxMasks = 0; Bool factsKnown = false; Uint64 contentHash = 0; if (VaoContentHashIfKnown(vao, contentHash)) { const VaoDrawMemo* vaoMemo = LookupVaoDrawMemo(&vao); if (vaoMemo->layoutFactsValid && vaoMemo->contentHash == contentHash) { vaoLayoutHash = vaoMemo->layoutHash; auxMasks = vaoMemo->layoutAuxMasks; factsKnown = true; } } if (!factsKnown) { // First sight of this VAO configuration: resolve (which stamps the // VAO's hash memo) and read the same facts from the entry, then stamp // the table slot for every later draw. const auto& vertexInputState = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao); vaoLayoutHash = vertexInputState.layoutHash; auxMasks = VertexInputStateFactory::PackVertexInputAuxMasks( vertexInputState.unsupportedAttribMask, vertexInputState.attributeLocationMask); Uint64 stampedHash = 0; if (VaoContentHashIfKnown(vao, stampedHash)) { VaoDrawMemo* vaoMemo = LookupVaoDrawMemo(&vao); vaoMemo->contentHash = stampedHash; vaoMemo->layoutHash = vaoLayoutHash; vaoMemo->layoutAuxMasks = auxMasks; vaoMemo->layoutFactsValid = true; } } vaoLayoutMoved = vaoLayoutHash != snap.vaoLayoutHash; if (vaoLayoutMoved) { // Vertex-input pre-flight for the changed layout, mirroring the full // path: a bad attribute must never be baked into a cached VkPipeline, // and the current-value synthesis in UploadAndBindVertexBuffers must // never see an unsupported generic-attribute type. Declining routes the // draw through the full path's loud failure reporting. An UNMOVED layout // needs no pre-flight: the snapshotting draw passed it with identical // inputs (same program; masks pinned by the layout hash). const Uint32 unsupportedAttribMask = static_cast(auxMasks >> 32); const Uint32 attributeLocationMask = static_cast(auxMasks); const Uint32 activeAttribMask = programObj.activeVertexInputLocationMask; if ((unsupportedAttribMask & activeAttribMask) != 0) { return false; } const Uint32 missingAttribMask = activeAttribMask & ~attributeLocationMask; if (missingAttribMask != 0) { for (Uint32 location = 0; location < kMaxVertexAttribs; ++location) { if ((missingAttribMask & (1u << location)) == 0) { continue; } if (MG_State::GLState::ClassifyVertexAttribType(programObj.vertexInputTypes[location]) .baseType == MG_State::GLState::VertexAttribBaseType::Unsupported) { return false; } } } } } if (bindsMoved && !m_uniformManager->SampledBindingsUnchanged(program, programObj, snap.sampledBindingRecords)) { return false; } // Same sampled set as the snapshotting draw (program/bind keys above); // verify content and params are untouched and every layout is still // sampleable, then stamp recording use exactly as the full path would. // A feedback case (sampled texture written by the active pass) fails the // layout check and falls back to the full path's end-pass handling. // The ENTRY's copies, not the scratch vectors: with more than one entry // the scratch holds only the last full-path draw's set, which may belong // to a different program. const auto& sampledTextures = snap.sampledTextures; const auto& sampledResources = snap.sampledResources; if (sampledResources.size() != sampledTextures.size()) { return false; } Uint64 contentSum = 0; Uint64 paramsSum = 0; // The descriptor-reuse hint (see BindProgramUniformBuffers) additionally needs // every sampled resource still in the exact layout the cached descriptors hold. // A layout that moved to a different-but-sampleable one only clears the hint // (this draw re-resolves and re-caches) - the fast path itself stays valid. // bindsMoved does not clear the hint: reaching this point with a moved bind // generation means SampledBindingsUnchanged proved the per-binding (texture, // sampler) pairs identical, and the sums/generation checks below cover every // remaining descriptor input. const Bool layoutSnapshotUsable = snap.sampledLayouts.size() == sampledTextures.size(); Bool samplerDescriptorsUnchanged = layoutSnapshotUsable; for (SizeT i = 0; i < sampledTextures.size(); ++i) { const auto* sampledTexture = sampledTextures[i]; if (sampledTexture == nullptr) { continue; } auto* resource = sampledResources[i]; if (resource == nullptr || !IsValidSampledImageLayout(resource->layout)) { return false; } if (layoutSnapshotUsable && snap.sampledLayouts[i] != resource->layout) { snap.sampledLayouts[i] = resource->layout; samplerDescriptorsUnchanged = false; } contentSum += sampledTexture->GetContentVersion(); paramsSum += sampledTexture->GetTextureParamsVersion(); // Folded into this walk (was a second loop): the stamp is a plain recency // store. Stamping ahead of the sum compare below is benign - a declined // draw re-runs the full path, which stamps the same resources, and an // over-stamp only delays garbage collection by one generation. m_textureManager->StampResourceRecordingUse(*resource); } if (contentSum != snap.sampledContentSum || paramsSum != snap.sampledParamsSum) { return false; } const Uint64 samplingResolutionGeneration = MGB_CTX->GetSamplingResolutionGeneration(); if (samplingResolutionGeneration != snap.samplingResolutionGeneration) { // Decline, not re-arm: snap.resolvedTransformFlags bakes the // ExplicitLod0Sampling verdict, which reads the effective sampler's // filters/aniso/LOD range - exactly the state this counter tracks. // Re-arming the stamp here would rebuild the descriptors but keep the // stale SPIR-V variant forever (every later draw compares equal again). // Same shape as the erase-epoch declines above; costs one full-path draw // per sampler/shape change, and the full path's LOD memo re-probes. return false; } // Everything the full path would re-resolve is provably unchanged - or, for // a moved pipeline-state version or a changed vertex-input LAYOUT, reduces to // re-resolving just the pipeline through the value-keyed memo against the // still-active render pass. A changed VAO with the SAME layout keeps the // snapshot's pipeline outright (the layout is the pipeline's only VAO input). // Run only the per-draw tail. VkPipeline pipeline = snap.pipeline; if (renderStateMoved || vaoLayoutMoved) { pipeline = VK_NULL_HANDLE; // The render pass is provably the snapshot's (hash match above), so probe // the value-keyed pipeline memo directly - no render-pass-entry re-fetch // (whose pending-clear probes cost more than the whole probe below). For a // moved state version, first refresh the pipeline-state VALUE hash exactly // as GetOrCreatePipeline would (same inputs: the snapshot pins the pass, so // its color attachment count is the right hash input); the value hash is // what lets a per-draw GL_BLEND toggle alternate between two memo entries // instead of missing forever on a monotonic version. A miss falls through // to the full lookup. #if MOBILEGL_PIPE_PUSH // Same arm selector as GetOrCreatePipeline's probe (P2 D12.1); this site is the // fast path's copy of it, and the two must key identically or the fast path would // hand back a pipeline the full path would not have matched. const MG_Pipe::MGPipeHandle renderStateCso = ResolveBoundRenderStateCso(); const Uint64 pipelineStateHash = !MG_Pipe::MGPipeHandleIsNull(renderStateCso) ? 0 : ResolveFallbackPipelineStateHash(renderStateVersion, snap.renderPassColorCount, snap.renderPassSampleCount); #else // The pull build's text, statement for statement (see GetOrCreatePipeline). if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion || m_pipelineStateHashColorCount != snap.renderPassColorCount || m_pipelineStateHashSampleCount != snap.renderPassSampleCount) { m_pipelineStateHash = ComputePipelineStateHash(snap.renderPassColorCount, snap.renderPassSampleCount); m_pipelineStateHashVersion = renderStateVersion; m_pipelineStateHashColorCount = snap.renderPassColorCount; m_pipelineStateHashSampleCount = snap.renderPassSampleCount; m_pipelineStateHashValid = true; } const Uint64 pipelineStateHash = m_pipelineStateHash; #endif const auto memoTransformFlags = ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags); for (Uint32 i = 0; i < m_pipelineMemoCount; ++i) { const PipelineMemoEntry& entry = m_pipelineMemo[i]; if (entry.pipeline != VK_NULL_HANDLE && entry.mode == mode && entry.programHash == programObj.hash && entry.vertexInputHash == vaoLayoutHash && entry.renderPassHash == snap.renderPassHash && #if MOBILEGL_BUILD_DISAGGREGATED entry.wireRenderPassCompatibilityId == 0 && #endif entry.pipelineStateHash == pipelineStateHash && #if MOBILEGL_PIPE_PUSH entry.renderStateCso == renderStateCso && #endif entry.primitiveRestartEnable == drawPrimitiveRestartEnable && entry.transformFlags == memoTransformFlags) { pipeline = entry.pipeline; break; } } if (pipeline == VK_NULL_HANDLE) { // Same lookup the full path would do; every input (FBO + version, image // index, depth/stencil participation, image epochs, no pending clears) // was verified unchanged above, so this is a pure cache hit on the same // entry the snapshot's pipeline was built against. const RenderPassEntry* renderPassEntry = m_renderPassManager->GetOrCreateRenderPass( *drawFbo, m_imageIndexAcquired, snap.drawUsesDepthStencil); // A decline (nullptr) is an attachment DirectVulkan cannot represent; the builder // has already logged it. Fall out of the fast path the same way an incompatible // pass does - the full path re-resolves, declines again and drops the draw. if (renderPassEntry == nullptr || !activeRenderPass->CompatibleWith(*renderPassEntry)) { return false; } pipeline = GetOrCreatePipeline(mode, program, programObj, ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags), vao, *renderPassEntry, drawPrimitiveRestartEnable); if (pipeline == VK_NULL_HANDLE) { return false; } } } // Every decline is behind us: the snapshot again describes the current // counters, so the next draw's compare is two integer loads. snap.renderStateVersion = renderStateVersion; snap.bindGeneration = bindGeneration; snap.vao = static_cast(&vao); snap.vaoLifetimeId = vao.GetLifetimeId(); #if MOBILEGL_PIPE_PUSH // Guarded by the SUBSYSTEM, not only by the build switch: with bit 6 clear the field // is dead (vaoMoved takes the address/lifetime-id branch), and minting a handle for it // would put this package's cost inside MOBILEGL_PIPE_PUSH=0 - the all-pull control arm // D14 defines as reproducing P1 exactly, and the arm D.4.3's T2 is measured on. if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) { snap.vaoHandle = ResolveVaoHandle(vao); } #endif snap.vaoConfigVersion = vao.GetConfigVersion(); snap.vaoLayoutHash = vaoLayoutHash; snap.pipeline = pipeline; if (!g_dynamicStateShadow.graphicsPipelineValid || g_dynamicStateShadow.graphicsPipeline != pipeline) { vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); g_dynamicStateShadow.graphicsPipelineValid = true; g_dynamicStateShadow.graphicsPipeline = pipeline; } if (!m_uniformManager->BindProgramUniformBuffers(frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_GRAPHICS, nullptr, samplerDescriptorsUnchanged)) { return false; } if (!UploadAndBindVertexBuffers(frame.commandBuffer, vao, programObj, drawParams, pIndexBufferView)) { return false; } if (aspects & DrawSetupAspect::IndexBuffer) { const Bool idxUploadOk = UploadAndBindIndexBuffer(frame, vao, pIndexBufferView); 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; } Bool VulkanRenderer::SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags aspects, const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView) { #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { RewindWireDescriptorSetsIfDue(); // The wire route returns before the monolith branch's draw-gated // sweep. Dead wire texture/renderbuffer records otherwise live // until the next frame boundary, which a long trace may not reach. m_textureManager->CollectGarbage(); return SetupWireDraw(frame, mode, aspects, drawParams, pIndexBufferView); } #endif // Sync each sampled texture at most once across this whole draw: the layout // probe loop, the post-transition loop, and ResolveSamplerDescriptor would // otherwise each re-run the full SyncTexture path on the same textures. MakeXfbWritesVisible(); VkTextureManager::DrawSyncScope drawSyncScope(*m_textureManager); m_textureManager->CollectGarbage(); { // Mirror DirectGLES's SyncToBackend gate: a program whose phase-B job failed or // was cancelled has no usable optimized module - and on an in-place // SanitizeAndOptimizeBinary failure GetGeneratedSpirv() still holds the RAW // glslang words, which must never reach vkCreateShaderModule. Drop the draw. const auto& drawProgram = *MGB_CTX->GetProgramForDraw(); if (!drawProgram.GetLinkStatus() || !drawProgram.GetSpirvStatus()) { MGLOG_D("SetupDraw skipped: program=%u is linked=%d spirv=%d", drawProgram.GetExternalIndex(), static_cast(drawProgram.GetLinkStatus()), static_cast(drawProgram.GetSpirvStatus())); 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 = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); if (drawFbo != nullptr && IsUnsupportedFramebufferForDirectVulkan(*drawFbo)) { // Nothing was mutated: other entries' per-probe guards (FBO identity + // version among them) stay authoritative, so none need invalidating. RecordUnsupportedFramebufferError(__func__); return false; } const auto& vao = *MGB_CTX->GetBoundVertexArray(); const auto& program = *MGB_CTX->GetProgramForDraw(); // The fast path declined (or had no entry for this program): whatever THIS // program's entry saw may be stale, and the full path below mutates state as // it goes, so the entry must not stay matchable if that path fails mid-way. // Select it now - the program's own entry when one exists, else an invalid // slot, else a round-robin victim - and invalidate it until the successful // refill at the end. Other programs' entries keep their validity: every fact // they carry is re-guarded per probe (live pass hash, epochs, versions, // sums), so a full path run in between can only make them decline. SetupDrawSnapshot* fillSnap = nullptr; { Uint32 fillIndex = kSetupDrawSnapshotCount; const Uint64 fillProgramLifetimeId = program.GetLifetimeId(); for (Uint32 i = 0; i < kSetupDrawSnapshotCount; ++i) { if (m_setupDrawSnapshots[i].valid && m_setupDrawSnapshots[i].programLifetimeId == fillProgramLifetimeId) { fillIndex = i; break; } } if (fillIndex == kSetupDrawSnapshotCount) { for (Uint32 i = 0; i < kSetupDrawSnapshotCount; ++i) { if (!m_setupDrawSnapshots[i].valid) { fillIndex = i; break; } } } if (fillIndex == kSetupDrawSnapshotCount) { fillIndex = m_setupDrawSnapshotVictim; m_setupDrawSnapshotVictim = (m_setupDrawSnapshotVictim + 1) % kSetupDrawSnapshotCount; } fillSnap = &m_setupDrawSnapshots[fillIndex]; fillSnap->valid = false; m_setupDrawSnapshotMru = fillIndex; } const Bool drawFboIsDefault = drawFbo != nullptr && drawFbo->IsDefaultFramebuffer(); ProgramFactory::CompileOptionFlags transformFlags = ProgramFactory::CompileOptionFlags(GetBaseTransformFlagsRaw(drawFboIsDefault)); // Captured draws take the xfb-decorated program variant. if (m_transformFeedbackFeatureEnabled && MGB_CTX->IsTransformFeedbackActive() && program.GetTransformFeedbackVaryingCount() > 0) { transformFlags |= ProgramFactory::CompileOptionBit::XfbCapture; } // Sampling a colour render target through the driver's implicit-LOD path faults the GPU on // Adreno 650 (see ForceExplicitLod0SamplePass); ask for the explicit-LOD variant when doing // so cannot change a texel, i.e. when every sampler this program reads is pinned to a // single mip level. The probe walks every sampler binding, so its verdict is memoized // under the sampled-set memo's key plus the sampled textures' params-version sum (level // range and filter changes live there); the previous draw's texture list is valid for the // sum exactly when that key matches (same program, same binds). { const Uint64 lodProgramLifetimeId = program.GetLifetimeId(); const Uint32 lodProgramVersion = program.GetBackendStateVersion(); const Uint64 lodBindGeneration = MGB_CTX->GetTextureBindGeneration(); // The probe also reads the EFFECTIVE sampler's filters/aniso/LOD range // (ProgramSamplesOnlySingleLevelTextures), and those setters bump ONLY the // sampling-resolution generation - not the texture params version the sum // below covers. Without this key a filter/aniso change would keep serving // the stale verdict. const Uint64 lodSamplingGeneration = MGB_CTX->GetSamplingResolutionGeneration(); Bool lodMemoHit = false; if (m_lastLodDecisionValid && m_lastSampledSetValid && m_lastLodProgramLifetimeId == lodProgramLifetimeId && m_lastLodProgramVersion == lodProgramVersion && m_lastLodBindGeneration == lodBindGeneration && m_lastLodSamplingGeneration == lodSamplingGeneration && m_lastLodBaseFlags == transformFlags && m_lastSampledSetProgramLifetimeId == lodProgramLifetimeId && m_lastSampledSetProgramVersion == lodProgramVersion && m_lastSampledSetBindGeneration == lodBindGeneration) { Uint64 paramsSum = 0; for (const auto* sampledTexture : m_sampledTexturesScratch) { if (sampledTexture != nullptr) { paramsSum += sampledTexture->GetTextureParamsVersion(); } } if (paramsSum == m_lastLodParamsSum) { transformFlags = m_lastLodResultFlags; lodMemoHit = true; } } if (!lodMemoHit) { const ProgramFactory::CompileOptionFlags baseFlags = transformFlags; const auto& baseProgramObj = m_programFactory->GetOrCreateProgram(program, transformFlags); if (UniformManager::ProgramSamplesOnlySingleLevelTextures(program, baseProgramObj)) { transformFlags |= ProgramFactory::CompileOptionBit::ExplicitLod0Sampling; } m_lastLodDecisionValid = true; m_lastLodProgramLifetimeId = lodProgramLifetimeId; m_lastLodProgramVersion = lodProgramVersion; m_lastLodBindGeneration = lodBindGeneration; m_lastLodSamplingGeneration = lodSamplingGeneration; m_lastLodBaseFlags = baseFlags; m_lastLodResultFlags = transformFlags; m_lastLodParamsSum = 0; // filled below once the sampled set is known } } // GL's gl_BaseVertex is zero for every command without a baseVertex parameter, while // Vulkan's builtin reports the draw's firstVertex; a non-indexed draw therefore takes // the zeroed program variant. The question is about the program's SPIR-V, not about // this draw, so it is memoized on (program lifetime, backend-state version): only the // very first draw of a program pays the extra lookup, and a program used exclusively // with non-indexed draws never resolves - never compiles, never re-stamps - the // variant no draw of it would use. const Bool nonIndexedDraw = !(aspects & DrawSetupAspect::IndexBuffer); const Uint64 baseVertexProgramLifetimeId = program.GetLifetimeId(); const Uint32 baseVertexProgramVersion = program.GetBackendStateVersion(); const Bool baseVertexQueryKnown = m_lastBaseVertexQueryValid && m_lastBaseVertexProgramLifetimeId == baseVertexProgramLifetimeId && m_lastBaseVertexProgramVersion == baseVertexProgramVersion; if (baseVertexQueryKnown && nonIndexedDraw && m_lastBaseVertexReads) { transformFlags |= ProgramFactory::CompileOptionBit::ZeroBaseVertex; } const ProgramFactory::VkProgramObject* resolvedProgramObj = &m_programFactory->GetOrCreateProgram(program, transformFlags); if (!baseVertexQueryKnown) { // Read the answer out of the entry BEFORE any second lookup: that lookup may // insert and move every entry of the open-addressing cache, dangling the // reference. The zeroing pass leaves the variable declared, so the variant just // resolved answers the same as the base one either way. const Bool readsBaseVertex = resolvedProgramObj->readsBaseVertexBuiltin; m_lastBaseVertexQueryValid = true; m_lastBaseVertexProgramLifetimeId = baseVertexProgramLifetimeId; m_lastBaseVertexProgramVersion = baseVertexProgramVersion; m_lastBaseVertexReads = readsBaseVertex; if (nonIndexedDraw && readsBaseVertex) { transformFlags |= ProgramFactory::CompileOptionBit::ZeroBaseVertex; resolvedProgramObj = &m_programFactory->GetOrCreateProgram(program, transformFlags); } } const auto& programObj = *resolvedProgramObj; // Pinned for BeginXfbCaptureForDraw, which otherwise decides from GL state alone and has // no way to know the bound pipeline's last pre-rasterization module lost (or never got) // its Xfb execution mode. See VkProgramObject::xfbCaptureDeclined. m_currentDrawXfbCaptureDeclined = programObj.xfbCaptureDeclined; // The build already said why, once, naming the program and the stage. Refusing here - // before any pipeline is built from it - is what makes that message a decline rather // than a note attached to invalid usage the driver still receives. if (programObj.pointSizeCapabilityUnsupported) { return false; } // For the snapshot's memoised entry pointer: if anything below inserts into the // program cache (blit/aux program compiles), the epoch moves and the snapshot // stores no pointer for this draw - the fast path then re-looks-up once. const Uint64 programFactoryEpochAtResolve = m_programFactory->GetCacheStructureEpoch(); // Begin command recording if not yet if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); // New command buffer: a program/FBO address from a previous frame may have been // recycled, so start the sampled-set skip cache fresh this frame. m_lastSampledSetValid = false; } if (!PrepareStorageImageTextures(frame, program, programObj)) { MGLOG_E_ONCE("SetupDraw skipped: storage image preparation failed"); return false; } if (!PrepareSamplerImageFeedbackSnapshots(frame, program, programObj, VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT)) { MGLOG_E_ONCE("SetupDraw skipped: sampler/image feedback snapshot failed"); return false; } auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); // Check if any of the textures to sample have pending clears, // which probably indicates it's been gone through codepath like `fbo attach` -> `clear` -> `fbo detach`, and // without draws in between to give it a chance to materialize such clear. // Deal with this situation here. // Reuse the previous draw's sampled-texture list when the set is provably unchanged (same // program+state+transform and no bind/unbind/delete since), skipping the per-draw GL walk. // The layout/feedback/transition loops below still run on the list every draw, so this only // elides re-resolving *which* textures are sampled, never their layout handling. auto& sampledTextures = m_sampledTexturesScratch; { const Uint64 programLifetimeId = program.GetLifetimeId(); const Uint32 programVersion = program.GetBackendStateVersion(); const Uint64 bindGeneration = MGB_CTX->GetTextureBindGeneration(); // The bind generation alone stopped covering this set the moment ResolveSampledBinding // started asking SamplesAsIncompleteTexture: membership now depends on the effective // sampler PARAMETERS (MIN_FILTER decides whether the mip chain is read at all) and on // the texture SHAPE, and neither moves the bind generation. A texture that flips // incomplete -> complete under a fixed binding - one glTexParameteri, one // glSamplerParameteri, a BASE_LEVEL/MAX_LEVEL change, or an upload that fills the // chain - would keep replaying the FALLBACK out of this memo, so the real texture // never got its pre-pass sync, its pending-clear materialisation or its sampled-layout // transition, and the descriptor path would then transition it from INSIDE the open // render pass, which the subpass declares no self-dependency for. // // The sampling-resolution generation is exactly the counter for that family and is // deliberately coarse (any texture, any sampler), so this one term covers every input // the predicate reads that the bind generation does not: TextureObjectBase:: // BumpShapeVersion and SamplerObject::BumpVersion both bump it, while WHICH sampler // object a unit carries goes through TextureUnit::SetSamplerObject and moves the bind // generation instead. Same term the SetupDrawSnapshot fast path and the LOD memo // already carry. const Uint64 samplingGeneration = MGB_CTX->GetSamplingResolutionGeneration(); const Bool sampledSetUnchanged = m_lastSampledSetValid && m_lastSampledSetProgramLifetimeId == programLifetimeId && m_lastSampledSetProgramVersion == programVersion && m_lastSampledSetTransformFlags == transformFlags && m_lastSampledSetBindGeneration == bindGeneration && m_lastSampledSetSamplingGeneration == samplingGeneration; if (!sampledSetUnchanged) { const Bool hasSampledTextures = m_uniformManager->CollectSampledTextures( program, programObj, sampledTextures, &m_sampledBindingRecordsScratch); MOBILEGL_ASSERT(hasSampledTextures, "%s: CollectSampledTextures failed", __func__); m_lastSampledSetValid = true; m_lastSampledSetProgramLifetimeId = programLifetimeId; m_lastSampledSetProgramVersion = programVersion; m_lastSampledSetTransformFlags = transformFlags; m_lastSampledSetBindGeneration = bindGeneration; m_lastSampledSetSamplingGeneration = samplingGeneration; } // Complete a freshly-made LOD decision (see above): its params sum // can only be taken once the sampled set is known. A genuine // all-zero sum merely re-probes next draw. if (m_lastLodDecisionValid && m_lastLodParamsSum == 0) { Uint64 paramsSum = 0; for (const auto* sampledTexture : sampledTextures) { if (sampledTexture != nullptr) { paramsSum += sampledTexture->GetTextureParamsVersion(); } } m_lastLodParamsSum = paramsSum; } } MGLOG_D("SetupDraw: program=%u drawFbo=%u sampledTextureCount=%zu activeRenderPass=%s", program.GetExternalIndex(), drawFbo ? drawFbo->GetExternalIndex() : 0u, sampledTextures.size(), activeRenderPass ? "true" : "false"); Bool activeRenderPassUsesSampledTexture = false; if (activeRenderPass != nullptr) { for (auto* sampledTexture : sampledTextures) { if (sampledTexture == nullptr) { continue; } if (ActiveRenderPassUsesTexture(*activeRenderPass, *sampledTexture)) { MGLOG_D("SetupDraw: active render pass is still using sampled textureId=%d; ending render pass before descriptor preparation", sampledTexture->GetExternalIndex()); activeRenderPassUsesSampledTexture = true; break; } } } if (activeRenderPassUsesSampledTexture) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); activeRenderPass = nullptr; } Bool needSampledTextureTransitions = false; auto& sampledResources = m_sampledResourcesScratch; sampledResources.assign(sampledTextures.size(), nullptr); for (SizeT sampledIndex = 0; sampledIndex < sampledTextures.size(); ++sampledIndex) { auto* sampledTexture = sampledTextures[sampledIndex]; if (!sampledTexture) { continue; } auto* textureResource = m_textureManager->SyncTextureAndGetDescriptor(*sampledTexture); if (textureResource == nullptr) { // SyncTextureAndGetDescriptor has a real failure channel - an incomplete or // otherwise unbackable texture declines and returns nullptr with its own log // line - and the assert that used to be the only guard here is compiled out of // every build past DEBUG. The next line dereferenced it, so a sampler left // pointing at a texture GL calls incomplete was a SIGSEGV inside SetupDraw // rather than a degraded draw. Leave the slot null and carry on: the descriptor // resolve substitutes the fallback texture for exactly these bindings // (ResolveSamplerDescriptor's SamplesAsIncompleteTexture branch), and the fast // path at the top of SetupDraw already treats a null resource as "re-resolve". MGLOG_E_ONCE("SetupDraw: no texture resource for sampled textureId=%d; leaving the binding to the " "descriptor resolve's fallback", sampledTexture->GetExternalIndex()); continue; } sampledResources[sampledIndex] = textureResource; MGLOG_D("SetupDraw: sampled textureId=%d layout(before)=%s(%d)", sampledTexture->GetExternalIndex(), VkImageLayoutToString(textureResource->layout), static_cast(textureResource->layout)); if (m_clearManager->HasPendingClear(sampledTexture) || !IsValidSampledImageLayout(textureResource->layout)) { // Out-of-pass work is needed (deferred clear materialization or // a sampled-layout transition). When the open frame recording // has not referenced this image yet, that work can execute // ahead of the WHOLE recording - record it into the pre-pass // stream instead of splitting the active render pass (ANGLE's // outside-render-pass command stream, restricted to the // provably reorderable case). if (activeRenderPass != nullptr && !m_frameContext.GetCurrent().hasPreCommandBufferRecorded && !m_textureManager->WasTouchedThisRecording(*textureResource)) { VkCommandBuffer preCommandBuffer = m_frameContext.BeginPreCommandRecording(); const Bool preClearReady = MaterializePendingClearForTexture(preCommandBuffer, *sampledTexture); MOBILEGL_ASSERT(preClearReady, "%s: pre-pass MaterializePendingClearForTexture failed for textureId=%d", __func__, sampledTexture->GetExternalIndex()); const Bool preTransitionReady = m_textureManager->TransitionTextureForSampling(preCommandBuffer, *sampledTexture); MOBILEGL_ASSERT(preTransitionReady, "%s: pre-pass TransitionTextureForSampling failed for textureId=%d", __func__, sampledTexture->GetExternalIndex()); continue; } needSampledTextureTransitions = true; } } if (activeRenderPass && needSampledTextureTransitions) { MGLOG_D("SetupDraw: ending active render pass before sampled texture transitions"); VkRenderPassManager::EndRenderPass(frame.commandBuffer); activeRenderPass = nullptr; } for (SizeT sampledIndex = 0; sampledIndex < sampledTextures.size(); ++sampledIndex) { auto* sampledTexture = sampledTextures[sampledIndex]; if (!sampledTexture) { continue; } // Fast path: the first loop already resolved this texture, nothing // is pending against it, and its layout is still sampleable (the // layout re-check covers an EndRenderPass between the loops having // rewritten an attachment's layout). Skipping the materialize + // transition + re-resolve chain here is the difference between one // pointer read and three calls per sampled texture per draw. if (auto* fastResource = sampledResources[sampledIndex]; fastResource != nullptr && !m_clearManager->HasPendingClear(sampledTexture) && IsValidSampledImageLayout(fastResource->layout)) { m_textureManager->StampResourceRecordingUse(*fastResource); continue; } const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *sampledTexture); MOBILEGL_ASSERT(clearReady, "%s: MaterializePendingClearForTexture failed for textureId=%d", __func__, sampledTexture->GetExternalIndex()); const Bool ready = m_textureManager->TransitionTextureForSampling(frame.commandBuffer, *sampledTexture); MOBILEGL_ASSERT(ready, "%s: TransitionTextureForSampling failed for textureId=%d", __func__, sampledTexture->GetExternalIndex()); auto* transitionedResource = m_textureManager->SyncTextureAndGetDescriptor(*sampledTexture); if (transitionedResource == nullptr) { // Same declined-sync channel as the first loop, and the same reason not to // dereference it: StampResourceRecordingUse below takes a reference. MGLOG_E_ONCE("SetupDraw: no texture resource after transitioning sampled textureId=%d; leaving the " "binding to the descriptor resolve's fallback", sampledTexture->GetExternalIndex()); continue; } // Pre-pass stream bookkeeping: the draw about to be recorded reads // this image, so later out-of-pass work on it can no longer jump // ahead of the recording. m_textureManager->StampResourceRecordingUse(*transitionedResource); MGLOG_D("SetupDraw: sampled textureId=%d layout(after)=%s(%d)", sampledTexture->GetExternalIndex(), VkImageLayoutToString(transitionedResource->layout), static_cast(transitionedResource->layout)); } // Depth/stencil participation of THIS draw, for the default-FBO depth-less // pass flavor (GL: a disabled depth/stencil test neither reads nor writes // its buffer). const Bool drawUsesDepthStencil = MGB_CTX->IsCapabilityEnabled(CapabilityInput::DepthTest) || MGB_CTX->IsCapabilityEnabled(CapabilityInput::StencilTest); auto* renderPassEntry = m_renderPassManager->GetOrCreateRenderPass( *drawFbo, DefaultFramebufferWriteIndex(drawFbo->IsDefaultFramebuffer()), drawUsesDepthStencil); // nullptr: the framebuffer has an attachment DirectVulkan cannot represent (a texture the // texture manager declined to back, or a view it could not build). The builder logged which // one; drop the draw here, exactly as an unresolvable sampler descriptor drops one in // BindProgramUniformBuffers. Before this existed the same condition dereferenced a null // resource or handed VK_NULL_HANDLE to vkCreateFramebuffer and took the process down. if (renderPassEntry == nullptr) { return false; } if (activeRenderPass && !activeRenderPass->CompatibleWith(*renderPassEntry)) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); activeRenderPass = nullptr; renderPassEntry = m_renderPassManager->GetOrCreateRenderPass( *drawFbo, DefaultFramebufferWriteIndex(drawFbo->IsDefaultFramebuffer()), drawUsesDepthStencil); if (renderPassEntry == nullptr) { return false; } } if (renderPassEntry->attachmentCount == 0 || renderPassEntry->extent.x() <= 0 || renderPassEntry->extent.y() <= 0) { MGLOG_D("SetupDraw skipped: drawFbo=%u resolved to an empty render pass (attachmentCount=%u extent=%dx%d)", drawFbo->GetExternalIndex(), renderPassEntry->attachmentCount, renderPassEntry->extent.x(), renderPassEntry->extent.y()); return false; } // Vertex-input pre-flight, run before pipeline creation so that a bad attribute can never be // baked into a cached VkPipeline. { const auto& vertexInputState = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao); const Uint32 activeAttribMask = programObj.activeVertexInputLocationMask; // An enabled array whose GL type has no VkFormat mapping never reaches the vertex input // state, which makes it indistinguishable from a disabled array: the draw would treat it as // "missing" and silently feed the shader the current attribute value instead of the app's // vertex data. Fail loudly rather than render wrong pixels. const Uint32 brokenAttribMask = vertexInputState.unsupportedAttribMask & activeAttribMask; if (brokenAttribMask != 0) { MGLOG_E_ONCE("SetupDraw skipped: program=%u reads vertex attribute location mask 0x%x whose enabled " "array has no supported vertex format", program.GetExternalIndex(), brokenAttribMask); return false; } // Every genuinely disabled attribute the shader reads must have a current-value type we can // synthesize a binding for; otherwise the upload below would push a null payload. const Uint32 missingAttribMask = activeAttribMask & ~vertexInputState.attributeLocationMask; for (Uint32 location = 0; location < kMaxVertexAttribs; ++location) { if ((missingAttribMask & (1u << location)) == 0) continue; const GLenum glType = programObj.vertexInputTypes[location]; if (MG_State::GLState::ClassifyVertexAttribType(glType).baseType == MG_State::GLState::VertexAttribBaseType::Unsupported) { MGLOG_E_ONCE("SetupDraw skipped: program=%u location=%u has no enabled array and its shader input " "type 0x%x is not supported as a current generic vertex attribute", program.GetExternalIndex(), location, glType); return false; } } } auto pipeline = GetOrCreatePipeline(mode, program, programObj, transformFlags, vao, *renderPassEntry, ResolvePrimitiveRestartEnable(aspects, pIndexBufferView)); // GetOrCreatePipeline documents a VK_NULL_HANDLE return (empty stages, or a driver that // rejected vkCreateGraphicsPipelines). Binding it dereferences null inside the driver - // 9 of the 15 CTS process deaths were exactly this vkCmdBindPipeline. A draw that has no // pipeline is a skipped draw, which is what every other failure below already does. // MGLOG_E, latched: the condition is a property of the program, so an unlatched line // here is one per draw forever. Parked at MGLOG_I until the Log.h ordering was fixed. if (pipeline == VK_NULL_HANDLE) { MGLOG_E_ONCE("SetupDraw skipped: no graphics pipeline for program=%u (creation failed or the " "program has no shader stages)", program.GetExternalIndex()); return false; } activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); // Begin render pass, and handle clear if (activeRenderPass && activeRenderPass->CompatibleWith(*renderPassEntry)) { ClearAttachmentsOnActiveRenderPass(frame.commandBuffer, *renderPassEntry); } else { // No active render pass or active one not compatible. // Restart a new render pass Bool ok = VkRenderPassManager::BeginRenderPass(frame.commandBuffer, *renderPassEntry); MOBILEGL_ASSERT(ok, "%s: BeginRenderPass failed", __func__); } if (!g_dynamicStateShadow.graphicsPipelineValid || g_dynamicStateShadow.graphicsPipeline != pipeline) { vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); g_dynamicStateShadow.graphicsPipelineValid = true; g_dynamicStateShadow.graphicsPipeline = pipeline; } const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers( frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_GRAPHICS, nullptr, false, m_samplerImageBindingOverridesScratch.empty() ? nullptr : &m_samplerImageBindingOverridesScratch); if (!boundUniforms) { MGLOG_E_ONCE("SetupDraw skipped: BindProgramUniformBuffers failed"); return false; } auto vtxUploadOk = UploadAndBindVertexBuffers( frame.commandBuffer, vao, programObj, drawParams, pIndexBufferView); if (!vtxUploadOk) { MGLOG_E_ONCE("SetupDraw skipped: failed to upload vertex buffers"); return false; } if (aspects & DrawSetupAspect::IndexBuffer) { auto idxUploadOk = UploadAndBindIndexBuffer(frame, vao, pIndexBufferView); MOBILEGL_ASSERT(idxUploadOk, "SetupDraw skipped: failed to upload index buffer"); } ApplyDynamicDrawStateTail(frame, renderPassEntry->extent, drawFbo->IsDefaultFramebuffer(), ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin)); // Snapshot the fully resolved configuration for the consecutive-draw // fast path (see TrySetupDrawFastPath). { auto& snap = *fillSnap; const auto* nowActiveRenderPass = VkRenderPassManager::GetActiveRenderPass(); if (nowActiveRenderPass != nullptr && !programObj.hasStorageImages) { snap.valid = true; snap.aspects = aspects.GetRaw(); snap.primitiveRestartEnable = ResolvePrimitiveRestartEnable(aspects, pIndexBufferView); snap.mode = mode; snap.programLifetimeId = program.GetLifetimeId(); snap.programVersion = program.GetBackendStateVersion(); snap.vao = &vao; snap.vaoLifetimeId = vao.GetLifetimeId(); #if MOBILEGL_PIPE_PUSH // Subsystem-guarded for the same reason as the other stamping site: the field // is dead with bit 6 clear, and MOBILEGL_PIPE_PUSH=0 has to be P1 exactly. if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) { snap.vaoHandle = ResolveVaoHandle(vao); } #endif snap.vaoConfigVersion = vao.GetConfigVersion(); snap.drawFbo = drawFbo.get(); snap.drawFboLifetimeId = drawFbo->GetLifetimeId(); snap.fboVersion = drawFbo->GetObjectVersion(); snap.drawFboIsDefault = drawFboIsDefault; snap.viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin); snap.renderStateVersion = MGB_CTX->GetPipelineStateVersion(); snap.bindGeneration = MGB_CTX->GetTextureBindGeneration(); snap.baseTransformFlags = GetBaseTransformFlagsRaw(drawFboIsDefault); snap.resolvedTransformFlags = transformFlags.GetRaw(); snap.renderPassHash = nowActiveRenderPass->hash; snap.imageIndex = m_imageIndexAcquired; snap.textureEraseEpoch = m_textureManager->GetResourceEraseEpoch(); snap.textureImageEpoch = m_textureManager->GetTextureImageEpoch(); snap.renderbufferImageEpoch = m_renderPassManager->GetRenderbufferImageEpoch(); snap.drawUsesDepthStencil = drawUsesDepthStencil; snap.renderPassExtent = renderPassEntry->extent; snap.renderPassColorCount = renderPassEntry->colorAttachmentCount; snap.renderPassSampleCount = renderPassEntry->sampleCount; snap.pipeline = pipeline; // The layout identity the fast path's aux-memo compare answers against. // A memo hit here, not a rebuild: the pre-flight above resolved this // VAO's entry already, so this re-reads the VAO's stamped state memo. snap.vaoLayoutHash = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao).layoutHash; // Entry pointer memo: only when nothing since the resolve restructured // the factory cache (see programFactoryEpochAtResolve above). if (m_programFactory->GetCacheStructureEpoch() == programFactoryEpochAtResolve) { snap.programObj = &programObj; snap.programFactoryEpoch = programFactoryEpochAtResolve; } else { snap.programObj = nullptr; snap.programFactoryEpoch = 0; } snap.samplingResolutionGeneration = MGB_CTX->GetSamplingResolutionGeneration(); Uint64 snapContentSum = 0; Uint64 snapParamsSum = 0; // Per-entry copies of this draw's sampled set (the scratch vectors // will be overwritten by the next full-path draw of ANY program). // Record each resource's layout VALUE for the descriptor-reuse hint; // transitions above updated the resources in place, so this reads the // layouts the descriptors just resolved against. snap.sampledTextures = sampledTextures; snap.sampledResources = sampledResources; snap.sampledBindingRecords = m_sampledBindingRecordsScratch; snap.sampledLayouts.assign(sampledTextures.size(), VK_IMAGE_LAYOUT_UNDEFINED); for (SizeT i = 0; i < sampledTextures.size(); ++i) { const auto* sampledTexture = sampledTextures[i]; if (sampledTexture == nullptr) { continue; } snapContentSum += sampledTexture->GetContentVersion(); snapParamsSum += sampledTexture->GetTextureParamsVersion(); if (sampledResources[i] != nullptr) { snap.sampledLayouts[i] = sampledResources[i]->layout; } } snap.sampledContentSum = snapContentSum; snap.sampledParamsSum = snapParamsSum; } else { snap.valid = false; } } return true; } void VulkanRenderer::DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) { #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { RewindWireDescriptorSetsIfDue(); m_textureManager->CollectGarbage(); DispatchWireCompute(numGroupsX, numGroupsY, numGroupsZ); return; } #endif m_textureManager->CollectGarbage(); auto& frame = m_frameContext.GetCurrent(); // The DISPATCH accessor: with a pipeline bound this is its compute stage program // itself, never the graphics composite (which carries no compute stage at all). const auto& program = *MGB_CTX->GetProgramForDispatch(); if (!program.GetLinkStatus() || !program.GetSpirvStatus()) { MGLOG_E_ONCE("DispatchCompute skipped: program=%u has no optimized SPIR-V", program.GetExternalIndex()); return; } ProgramFactory::CompileOptionFlags transformFlags = 0; const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags); if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); } if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } if (!PrepareStorageImageTextures(frame, program, programObj)) { MGLOG_E_ONCE("DispatchCompute skipped: storage image preparation failed"); return; } if (!PrepareSamplerImageFeedbackSnapshots(frame, program, programObj, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT)) { MGLOG_E_ONCE("DispatchCompute skipped: sampler/image feedback snapshot failed"); return; } const VkPipeline pipeline = GetOrCreateComputePipeline(programObj); if (pipeline == VK_NULL_HANDLE) { MGLOG_E_ONCE("DispatchCompute skipped: compute pipeline creation failed for program=%u", program.GetExternalIndex()); return; } vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers( frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_COMPUTE, nullptr, false, m_samplerImageBindingOverridesScratch.empty() ? nullptr : &m_samplerImageBindingOverridesScratch); if (!boundUniforms) { MGLOG_E_ONCE("DispatchCompute skipped: BindProgramUniformBuffers failed"); return; } MGLOG_D("DirectVulkan: glDispatchCompute(%u, %u, %u)", numGroupsX, numGroupsY, numGroupsZ); vkCmdDispatch(frame.commandBuffer, numGroupsX, numGroupsY, numGroupsZ); } void VulkanRenderer::DispatchComputeIndirect(GLintptr indirect) { m_textureManager->CollectGarbage(); auto& frame = m_frameContext.GetCurrent(); // See DispatchCompute: the dispatch accessor, not the draw one. const auto& program = *MGB_CTX->GetProgramForDispatch(); if (!program.GetLinkStatus() || !program.GetSpirvStatus()) { MGLOG_E_ONCE("DispatchComputeIndirect skipped: program=%u has no optimized SPIR-V", program.GetExternalIndex()); return; } ProgramFactory::CompileOptionFlags transformFlags = 0; const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags); if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); } if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } if (!PrepareStorageImageTextures(frame, program, programObj)) { MGLOG_E_ONCE("DispatchComputeIndirect skipped: storage image preparation failed"); return; } if (!PrepareSamplerImageFeedbackSnapshots(frame, program, programObj, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT)) { MGLOG_E_ONCE("DispatchComputeIndirect skipped: sampler/image feedback snapshot failed"); return; } const VkPipeline pipeline = GetOrCreateComputePipeline(programObj); if (pipeline == VK_NULL_HANDLE) { MGLOG_E_ONCE("DispatchComputeIndirect skipped: compute pipeline creation failed for program=%u", program.GetExternalIndex()); return; } vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers( frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_COMPUTE, nullptr, false, m_samplerImageBindingOverridesScratch.empty() ? nullptr : &m_samplerImageBindingOverridesScratch); if (!boundUniforms) { MGLOG_E_ONCE("DispatchComputeIndirect skipped: BindProgramUniformBuffers failed"); return; } auto indirectBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DispatchIndirect).GetBoundObject(); if (!indirectBuffer) { MGLOG_E_ONCE("DispatchComputeIndirect skipped: GL_DISPATCH_INDIRECT_BUFFER is not bound"); return; } indirectBuffer->SyncPersistentMappedRange(); BufferSlice slice{}; if (!m_bufferManager.AcquireResidentSlice(BufferKind::Indirect, indirectBuffer, slice)) { MGLOG_E_ONCE("DispatchComputeIndirect skipped: failed to sync indirect dispatch buffer"); return; } MGLOG_D("DirectVulkan: glDispatchComputeIndirect(offset=%zu)", static_cast(indirect)); vkCmdDispatchIndirect(frame.commandBuffer, slice.buffer, slice.offset + static_cast(indirect)); } VkMemoryBarrier VulkanRenderer::BuildMemoryBarrierForGlBarriers(GLbitfield barriers) { VkMemoryBarrier memoryBarrier{}; memoryBarrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER; memoryBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_MEMORY_WRITE_BIT; memoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; if ((barriers & GL_COMMAND_BARRIER_BIT) != 0) { memoryBarrier.dstAccessMask |= VK_ACCESS_INDIRECT_COMMAND_READ_BIT; } return memoryBarrier; } void VulkanRenderer::MemoryBarrier(GLbitfield barriers) { auto& frame = m_frameContext.GetCurrent(); if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); } if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } VkMemoryBarrier memoryBarrier = BuildMemoryBarrierForGlBarriers(barriers); MGLOG_D("DirectVulkan: glMemoryBarrier(0x%x)", static_cast(barriers)); vkCmdPipelineBarrier(frame.commandBuffer, #if MOBILEGL_BUILD_DISAGGREGATED // ALL_COMMANDS does not include HOST. The barrier's // HOST_WRITE access must have a matching source stage. MG_Config::Transport != MG_Config::TransportMode::Monolith ? VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT : VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, #else VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, #endif VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memoryBarrier, 0, nullptr, 0, nullptr); } VulkanRenderer::ScissoredClearPrep VulkanRenderer::PrepareScissoredClear( const MG_State::GLState::FramebufferObject& framebuffer, VkClearRect& outClearRect) { auto& frame = m_frameContext.GetCurrent(); if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); } auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); auto* renderPassEntry = m_renderPassManager->GetOrCreateRenderPass( framebuffer, DefaultFramebufferWriteIndex(framebuffer.IsDefaultFramebuffer())); // A declined render pass is the same answer as an empty one for a clear: there is nothing // attached that can be cleared inside a pass. The builder has already logged the reason. if (renderPassEntry == nullptr || renderPassEntry->attachmentCount == 0 || renderPassEntry->extent.x() <= 0 || renderPassEntry->extent.y() <= 0) { return ScissoredClearPrep::NoOp; } VkClearRect clearRect{}; clearRect.rect = framebuffer.IsDefaultFramebuffer() ? MakeDefaultFramebufferScissorRect(MGB_CTX->GetScissorBox(), renderPassEntry->extent, m_swapchainObject.GetPreTransform()) : MakeClampedScissorRect(MGB_CTX->GetScissorBox(), renderPassEntry->extent); clearRect.baseArrayLayer = 0; // GL 3.3 ยง4.4.7: clearing a layered framebuffer clears every layer. clearRect.layerCount = renderPassEntry->layers; if (clearRect.rect.extent.width == 0 || clearRect.rect.extent.height == 0) { return ScissoredClearPrep::NoOp; } // A scissor that covers the whole target is a whole-surface clear; the deferred loadOp // path is equivalent and cheaper (no render pass churn, loadOp=CLEAR on tilers). if (clearRect.rect.offset.x == 0 && clearRect.rect.offset.y == 0 && clearRect.rect.extent.width == static_cast(renderPassEntry->extent.x()) && clearRect.rect.extent.height == static_cast(renderPassEntry->extent.y())) { return ScissoredClearPrep::NotNeeded; } if (activeRenderPass && !activeRenderPass->CompatibleWith(*renderPassEntry)) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); activeRenderPass = nullptr; // Re-resolve: ending the pass updates tracked attachment layouts, which feed the // entry's load ops and initial layouts. renderPassEntry = m_renderPassManager->GetOrCreateRenderPass( framebuffer, DefaultFramebufferWriteIndex(framebuffer.IsDefaultFramebuffer())); if (renderPassEntry == nullptr) { return ScissoredClearPrep::NoOp; } } // A still-active pass is necessarily compatible here: the block above ended any // incompatible one and nothing since can change the active pass. if (activeRenderPass) { // Materialize any older whole-attachment clear before applying this // ordered, scissored clear. ClearAttachmentsOnActiveRenderPass(frame.commandBuffer, *renderPassEntry); } else { const Bool began = VkRenderPassManager::BeginRenderPass(frame.commandBuffer, *renderPassEntry); MOBILEGL_ASSERT(began, "%s: BeginRenderPass failed", __func__); if (!began) { return ScissoredClearPrep::NoOp; } } outClearRect = clearRect; return ScissoredClearPrep::Ready; } // 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& 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(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(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(s) : d; Memcpy(dst, &value, sizeof(value)); break; } case GL_HALF_FLOAT: { const Uint16 value = MG_Util::EncodeFloatToHalfBits(stencilOnly ? static_cast(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(std::llround(std::clamp(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( std::llround(std::clamp(d, 0.0, 1.0) * static_cast(maximum))); Memcpy(dst, &value, size); break; } } } return true; } #include "WireFramebuffer.inc" #include "WireTextureReadback.inc" #include "WireColorBlit.inc" // AFTER WireColorBlit.inc: the depth mip reuses that file's window-Y convention and is // written to be read beside it. #include "WireDepthMipmap.inc" // AFTER WireDepthMipmap.inc for the same reason: the multisample depth/stencil resolve is // that file's pass with the box filter taken out and a sample index put in. #include "WireMultisampleResolve.inc" #include "WireDraw.inc" void VulkanRenderer::Clear(GLbitfield mask) { #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer(); if (!fbo) MagmaWireFatal("clear-framebuffer-record"); ClearAttachmentPayload payload{}; payload.mask = mask; payload.color = MGB_CTX->GetClearColor(); payload.depth = MGB_CTX->GetClearDepth(); payload.stencil = MGB_CTX->GetClearStencil(); ClearWireFramebuffer(*fbo, payload); return; } #endif m_clearManager->CollectGarbage(); if ((mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) == 0) { return; } // GL 3.3 ยง3.1: when RASTERIZER_DISCARD is enabled, Clear and ClearBuffer* are ignored. if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) { return; } auto* fbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get(); MOBILEGL_ASSERT(fbo, "VulkanRenderer::Clear: draw framebuffer not found (fbo == nullptr)"); if (IsUnsupportedFramebufferForDirectVulkan(*fbo)) { RecordUnsupportedFramebufferError(__func__); return; } ClearFramebufferPayload payload { .color = MGB_CTX->GetClearColor(), .depth = MGB_CTX->GetClearDepth(), .stencil = MGB_CTX->GetClearStencil() }; // A render-pass loadOp clear always covers the complete attachment, while // OpenGL glClear is clipped by GL_SCISSOR_TEST. Blaze3D relies on this for // GuiItemAtlas: animated items clear only their atlas slot before being // redrawn. Queueing that clear as a loadOp erases every cached static item. if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) { VkClearRect clearRect{}; switch (PrepareScissoredClear(*fbo, clearRect)) { case ScissoredClearPrep::NoOp: return; case ScissoredClearPrep::NotNeeded: break; // full-coverage scissor: the deferred whole-surface path below is equivalent case ScissoredClearPrep::Ready: { VkClearAttachment clearAttachments[MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS + 1]; Uint32 clearAttachmentCount = 0; if ((mask & GL_COLOR_BUFFER_BIT) != 0) { const auto& drawBuffers = fbo->GetDrawBuffers(); for (Uint32 drawBufferIndex = 0; drawBufferIndex < drawBuffers.size(); ++drawBufferIndex) { const auto attachmentType = drawBuffers[drawBufferIndex]; if (attachmentType == FramebufferAttachmentType::None) { continue; } const auto& attachment = fbo->GetAttachment(attachmentType); if (!attachment.IsComplete()) { continue; } const BoolVec4 colorMask = MGB_CTX->GetColorMaskIndexed(drawBufferIndex); if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) { continue; } if (!colorMask.r() || !colorMask.g() || !colorMask.b() || !colorMask.a()) { MGLOG_W_ONCE("DirectVulkan: scissored glClear with a partial color mask is not supported"); continue; } MG_State::GLState::ITextureObject* colorTexture = nullptr; if (attachment.IsTexture()) { colorTexture = attachment.GetTexture().get(); } VkClearAttachment clearAttachment{}; clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; clearAttachment.colorAttachment = drawBufferIndex; // glClear only ever supplies float values (ClearFramebufferPayload has no // other form), so the float member is always the right one here. clearAttachment.clearValue.color = { payload.color.x(), payload.color.y(), payload.color.z(), ColorFormatLacksAlpha(colorTexture) ? 1.0f : payload.color.w() }; clearAttachments[clearAttachmentCount++] = clearAttachment; } } VkImageAspectFlags depthStencilAspects = 0; if ((mask & GL_DEPTH_BUFFER_BIT) != 0 && MGB_CTX->GetDepthMask()) { const auto& depthAttachment = fbo->GetAttachment(FramebufferAttachmentType::Depth); if (depthAttachment.IsComplete()) { depthStencilAspects |= VK_IMAGE_ASPECT_DEPTH_BIT; } } if ((mask & GL_STENCIL_BUFFER_BIT) != 0) { const auto& stencilAttachment = fbo->GetAttachment(FramebufferAttachmentType::Stencil); if (stencilAttachment.IsComplete()) { // GL 3.3 ยง4.2.3: the clear is masked by the front stencil write mask. // vkCmdClearAttachments writes every bit, so only a full (8-bit stencil) or // zero mask can be expressed; treat a partial mask like a partial color mask. const Uint32 stencilWriteMask = MGB_CTX->GetStencilState(StencilFace::Front).WriteMask; if ((stencilWriteMask & 0xFFu) == 0xFFu) { depthStencilAspects |= VK_IMAGE_ASPECT_STENCIL_BIT; } else if (stencilWriteMask != 0) { MGLOG_W_ONCE("DirectVulkan: scissored glClear with a partial stencil write mask is not supported"); } } } if (depthStencilAspects != 0) { VkClearAttachment clearAttachment{}; clearAttachment.aspectMask = depthStencilAspects; clearAttachment.clearValue.depthStencil = {payload.depth, payload.stencil}; clearAttachments[clearAttachmentCount++] = clearAttachment; } if (clearAttachmentCount != 0) { vkCmdClearAttachments(m_frameContext.GetCurrent().commandBuffer, clearAttachmentCount, clearAttachments, 1, &clearRect); } return; } } } // GL 3.3 ยง4.2.3: glClear honors the write masks. Mirror the scissored path's // gating for the deferred path: drop fully-masked planes, warn on partial // masks vkCmdClear*/loadOp clears cannot express. GLbitfield deferredMask = mask; if ((deferredMask & GL_DEPTH_BUFFER_BIT) != 0 && !MGB_CTX->GetDepthMask()) { deferredMask &= ~static_cast(GL_DEPTH_BUFFER_BIT); } if ((deferredMask & GL_STENCIL_BUFFER_BIT) != 0) { const Uint32 stencilWriteMask = MGB_CTX->GetStencilState(StencilFace::Front).WriteMask; if ((stencilWriteMask & 0xFFu) != 0xFFu) { if (stencilWriteMask != 0) { MGLOG_W_ONCE("DirectVulkan: deferred glClear with a partial stencil write mask is not supported"); } deferredMask &= ~static_cast(GL_STENCIL_BUFFER_BIT); } } if ((deferredMask & GL_COLOR_BUFFER_BIT) != 0) { const auto& drawBuffers = fbo->GetDrawBuffers(); Bool anyFullMask = false; Bool anyRestrictedMask = false; for (Uint32 drawBufferIndex = 0; drawBufferIndex < drawBuffers.size(); ++drawBufferIndex) { if (drawBuffers[drawBufferIndex] == FramebufferAttachmentType::None) { continue; } const BoolVec4 colorMask = MGB_CTX->GetColorMaskIndexed(drawBufferIndex); const Bool full = colorMask.r() && colorMask.g() && colorMask.b() && colorMask.a(); if (full) { anyFullMask = true; } else { anyRestrictedMask = true; if (colorMask.r() || colorMask.g() || colorMask.b() || colorMask.a()) { MGLOG_W_ONCE("DirectVulkan: deferred glClear with a partial color mask is not supported"); } } } if (!anyFullMask) { deferredMask &= ~static_cast(GL_COLOR_BUFFER_BIT); } else if (anyRestrictedMask) { // Mixed per-buffer masks: queue only the fully-writable texture targets // individually and drop the framebuffer-level color clear. for (Uint32 drawBufferIndex = 0; drawBufferIndex < drawBuffers.size(); ++drawBufferIndex) { const auto attachmentType = drawBuffers[drawBufferIndex]; if (attachmentType == FramebufferAttachmentType::None) { continue; } const BoolVec4 colorMask = MGB_CTX->GetColorMaskIndexed(drawBufferIndex); if (!(colorMask.r() && colorMask.g() && colorMask.b() && colorMask.a())) { continue; } const auto& attachment = fbo->GetAttachment(attachmentType); if (attachment.IsRenderbuffer()) { m_renderPassManager->QueueRenderbufferClear( {.mask = GL_COLOR_BUFFER_BIT, .color = payload.color}, attachment); } else if (attachment.IsTexture()) { m_clearManager->QueueClear({.mask = GL_COLOR_BUFFER_BIT, .color = payload.color}, attachment); } } deferredMask &= ~static_cast(GL_COLOR_BUFFER_BIT); } } if (deferredMask == 0) { return; } m_clearManager->QueueClear(deferredMask, payload, *fbo); m_renderPassManager->QueueRenderbufferClear(deferredMask, payload, *fbo); } void VulkanRenderer::QueueClearBufferPayloadForFramebuffer( const MG_State::GLState::FramebufferObject& framebuffer, GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload) { m_clearManager->CollectGarbage(); // GL 3.3 ยง3.1: when RASTERIZER_DISCARD is enabled, Clear and ClearBuffer* are ignored. if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) { return; } if (IsUnsupportedFramebufferForDirectVulkan(framebuffer)) { RecordUnsupportedFramebufferError(__func__); return; } // Validate (buffer, drawbuffer) up front so GL errors fire regardless of which clear // path is taken below. switch (buffer) { case GL_COLOR: if (drawbuffer < 0 || drawbuffer >= static_cast(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS)) { RecordClearBufferError(__func__, ErrorCode::InvalidValue, "color drawbuffer index is out of range"); return; } break; case GL_DEPTH: if (drawbuffer != 0) { RecordClearBufferError(__func__, ErrorCode::InvalidValue, "depth clear requires drawbuffer 0"); return; } break; case GL_STENCIL: if (drawbuffer != 0) { RecordClearBufferError(__func__, ErrorCode::InvalidValue, "stencil clear requires drawbuffer 0"); return; } break; case GL_DEPTH_STENCIL: if (drawbuffer != 0) { RecordClearBufferError(__func__, ErrorCode::InvalidValue, "depth/stencil clear requires drawbuffer 0"); return; } break; default: RecordClearBufferError(__func__, ErrorCode::InvalidEnum, "unsupported clear buffer target"); return; } // GL 3.3 ยง4.2.3: ClearBuffer* is clipped by GL_SCISSOR_TEST exactly like Clear. if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) { VkClearRect clearRect{}; switch (PrepareScissoredClear(framebuffer, clearRect)) { case ScissoredClearPrep::NoOp: return; case ScissoredClearPrep::NotNeeded: break; // full-coverage scissor: the deferred whole-surface path below is equivalent case ScissoredClearPrep::Ready: RecordScissoredClearBuffer(framebuffer, buffer, drawbuffer, clearPayload, clearRect); return; } } auto queueAttachmentClear = [&](FramebufferAttachmentType attachmentType, const ClearAttachmentPayload& payload) { if (attachmentType == FramebufferAttachmentType::None || payload.mask == 0) { return; } const auto& attachment = framebuffer.GetAttachment(attachmentType); if (attachment.IsRenderbuffer()) { m_renderPassManager->QueueRenderbufferClear(payload, attachment); return; } if (!attachment.IsTexture()) { return; } m_clearManager->QueueClear(payload, attachment); }; // GL 3.3 ยง4.2.3: ClearBuffer* honors the write masks like Clear. Deferred // clears cannot express partial masks; warn and skip those. const auto depthClearAllowed = [&]() -> Bool { return MGB_CTX->GetDepthMask(); }; const auto stencilClearAllowed = [&]() -> Bool { const Uint32 stencilWriteMask = MGB_CTX->GetStencilState(StencilFace::Front).WriteMask; if ((stencilWriteMask & 0xFFu) == 0xFFu) { return true; } if (stencilWriteMask != 0) { MGLOG_W_ONCE("DirectVulkan: deferred glClearBuffer with a partial stencil write mask is not supported"); } return false; }; switch (buffer) { case GL_COLOR: { const BoolVec4 colorMask = MGB_CTX->GetColorMaskIndexed(static_cast(drawbuffer)); if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) { return; } if (!(colorMask.r() && colorMask.g() && colorMask.b() && colorMask.a())) { MGLOG_W_ONCE("DirectVulkan: deferred glClearBuffer with a partial color mask is not supported"); return; } queueAttachmentClear(framebuffer.GetDrawBuffers()[drawbuffer], clearPayload); return; } case GL_DEPTH: if (depthClearAllowed()) { queueAttachmentClear(FramebufferAttachmentType::Depth, clearPayload); } return; case GL_STENCIL: if (stencilClearAllowed()) { queueAttachmentClear(FramebufferAttachmentType::Stencil, clearPayload); } return; case GL_DEPTH_STENCIL: { ClearAttachmentPayload allowedPayload = clearPayload; if (!depthClearAllowed()) { allowedPayload.mask &= ~static_cast(GL_DEPTH_BUFFER_BIT); } if (!stencilClearAllowed()) { allowedPayload.mask &= ~static_cast(GL_STENCIL_BUFFER_BIT); } if ((allowedPayload.mask & GL_DEPTH_BUFFER_BIT) != 0) { queueAttachmentClear(FramebufferAttachmentType::Depth, allowedPayload); } if ((allowedPayload.mask & GL_STENCIL_BUFFER_BIT) != 0) { queueAttachmentClear(FramebufferAttachmentType::Stencil, allowedPayload); } return; } default: return; } } void VulkanRenderer::RecordScissoredClearBuffer(const MG_State::GLState::FramebufferObject& framebuffer, GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload, const VkClearRect& clearRect) { VkClearAttachment clearAttachment{}; if (buffer == GL_COLOR) { const auto attachmentType = framebuffer.GetDrawBuffers()[drawbuffer]; if (attachmentType == FramebufferAttachmentType::None) { return; } const auto& attachment = framebuffer.GetAttachment(attachmentType); if (!attachment.IsComplete()) { return; } const BoolVec4 colorMask = MGB_CTX->GetColorMaskIndexed(static_cast(drawbuffer)); if (!colorMask.r() && !colorMask.g() && !colorMask.b() && !colorMask.a()) { return; } if (!colorMask.r() || !colorMask.g() || !colorMask.b() || !colorMask.a()) { MGLOG_W_ONCE("DirectVulkan: scissored glClearBuffer with a partial color mask is not supported"); return; } MG_State::GLState::ITextureObject* colorTexture = nullptr; if (attachment.IsTexture()) { colorTexture = attachment.GetTexture().get(); } clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; clearAttachment.colorAttachment = static_cast(drawbuffer); clearAttachment.clearValue.color = MakeVkClearColorValue(clearPayload, ColorFormatLacksAlpha(colorTexture)); } else { VkImageAspectFlags aspects = 0; if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0 && MGB_CTX->GetDepthMask() && framebuffer.GetAttachment(FramebufferAttachmentType::Depth).IsComplete()) { aspects |= VK_IMAGE_ASPECT_DEPTH_BIT; } if ((clearPayload.mask & GL_STENCIL_BUFFER_BIT) != 0 && framebuffer.GetAttachment(FramebufferAttachmentType::Stencil).IsComplete()) { // GL 3.3 ยง4.2.3: the clear is masked by the front stencil write mask (see Clear). const Uint32 stencilWriteMask = MGB_CTX->GetStencilState(StencilFace::Front).WriteMask; if ((stencilWriteMask & 0xFFu) == 0xFFu) { aspects |= VK_IMAGE_ASPECT_STENCIL_BIT; } else if (stencilWriteMask != 0) { MGLOG_W_ONCE("DirectVulkan: scissored glClearBuffer with a partial stencil write mask is not supported"); } } if (aspects == 0) { return; } clearAttachment.aspectMask = aspects; clearAttachment.clearValue.depthStencil = {clearPayload.depth, clearPayload.stencil}; } vkCmdClearAttachments(m_frameContext.GetCurrent().commandBuffer, 1, &clearAttachment, 1, &clearRect); } void VulkanRenderer::QueueClearBufferPayload(GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload) { #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { const auto* fbo = MG_Pipe::MGPipeApplier().DrawFramebuffer(); if (!fbo) MagmaWireFatal("clear-buffer-framebuffer-record"); ClearWireFramebuffer(*fbo,clearPayload,buffer == GL_COLOR ? drawbuffer : -1); return; } #endif auto* fbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get(); if (!fbo) { return; } QueueClearBufferPayloadForFramebuffer(*fbo, buffer, drawbuffer, clearPayload); } void VulkanRenderer::ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { ClearAttachmentPayload payload{}; payload.mask = GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT; // Vulkan clear values require depth in [0,1] (VUID-VkClearDepthStencilValue-depth-00022). payload.depth = std::clamp(depth, 0.0f, 1.0f); payload.stencil = static_cast(stencil); QueueClearBufferPayload(buffer, drawbuffer, payload); } void VulkanRenderer::ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) { if (value == nullptr) { return; } ClearAttachmentPayload payload{}; switch (buffer) { case GL_COLOR: payload.mask = GL_COLOR_BUFFER_BIT; payload.color = FloatVec4(value[0], value[1], value[2], value[3]); break; case GL_DEPTH: payload.mask = GL_DEPTH_BUFFER_BIT; payload.depth = std::clamp(value[0], 0.0f, 1.0f); break; default: break; } QueueClearBufferPayload(buffer, drawbuffer, payload); } void VulkanRenderer::ClearNamedFramebufferfv( const SharedPtr& framebuffer, GLenum buffer, GLint drawbuffer, const GLfloat* value) { if (!framebuffer || value == nullptr) { return; } ClearAttachmentPayload payload{}; switch (buffer) { case GL_COLOR: payload.mask = GL_COLOR_BUFFER_BIT; payload.color = FloatVec4(value[0], value[1], value[2], value[3]); break; case GL_DEPTH: payload.mask = GL_DEPTH_BUFFER_BIT; payload.depth = std::clamp(value[0], 0.0f, 1.0f); break; default: break; } QueueClearBufferPayloadForFramebuffer(*framebuffer, buffer, drawbuffer, payload); } // The integer clears carry the same payload as their target-based siblings; only the // destination differs, so they queue against the named framebuffer rather than the bound one. void VulkanRenderer::ClearNamedFramebufferiv( const SharedPtr& framebuffer, GLenum buffer, GLint drawbuffer, const GLint* value) { if (!framebuffer || value == nullptr) { return; } ClearAttachmentPayload payload{}; switch (buffer) { case GL_COLOR: payload.mask = GL_COLOR_BUFFER_BIT; payload.colorEncoding = ClearColorEncoding::Int; payload.colorInt = IntVec4(value[0], value[1], value[2], value[3]); break; case GL_STENCIL: payload.mask = GL_STENCIL_BUFFER_BIT; payload.stencil = static_cast(std::max(value[0], 0)); break; default: break; } QueueClearBufferPayloadForFramebuffer(*framebuffer, buffer, drawbuffer, payload); } void VulkanRenderer::ClearNamedFramebufferuiv( const SharedPtr& framebuffer, GLenum buffer, GLint drawbuffer, const GLuint* value) { if (!framebuffer || value == nullptr) { return; } ClearAttachmentPayload payload{}; if (buffer == GL_COLOR) { payload.mask = GL_COLOR_BUFFER_BIT; payload.colorEncoding = ClearColorEncoding::Uint; payload.colorUint = UintVec4(value[0], value[1], value[2], value[3]); } QueueClearBufferPayloadForFramebuffer(*framebuffer, buffer, drawbuffer, payload); } void VulkanRenderer::ClearNamedFramebufferfi( const SharedPtr& framebuffer, GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { if (!framebuffer) { return; } ClearAttachmentPayload payload{}; payload.mask = GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT; // Vulkan clear values require depth in [0,1] (VUID-VkClearDepthStencilValue-depth-00022). payload.depth = std::clamp(depth, 0.0f, 1.0f); payload.stencil = static_cast(stencil); QueueClearBufferPayloadForFramebuffer(*framebuffer, buffer, drawbuffer, payload); } void VulkanRenderer::ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) { if (value == nullptr) { return; } ClearAttachmentPayload payload{}; switch (buffer) { case GL_COLOR: payload.mask = GL_COLOR_BUFFER_BIT; payload.colorEncoding = ClearColorEncoding::Uint; payload.colorUint = UintVec4(value[0], value[1], value[2], value[3]); break; case GL_STENCIL: payload.mask = GL_STENCIL_BUFFER_BIT; payload.stencil = value[0]; break; default: break; } QueueClearBufferPayload(buffer, drawbuffer, payload); } void VulkanRenderer::ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) { if (value == nullptr) { return; } ClearAttachmentPayload payload{}; switch (buffer) { case GL_COLOR: payload.mask = GL_COLOR_BUFFER_BIT; payload.colorEncoding = ClearColorEncoding::Int; payload.colorInt = IntVec4(value[0], value[1], value[2], value[3]); break; case GL_STENCIL: payload.mask = GL_STENCIL_BUFFER_BIT; payload.stencil = static_cast(std::max(value[0], 0)); break; default: break; } QueueClearBufferPayload(buffer, drawbuffer, payload); } Bool VulkanRenderer::ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture, Uint32 mipLevel, Uint32 depthSlice, const VkClearValue& clearValue, VkImageLayout finalLayout) { auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture); if (resource == nullptr || resource->image == VK_NULL_HANDLE) return false; if (m_frameContext.GetCurrentFrameIndex() >= m_deferredDepthMipmapCleanup.size()) return false; // A 2D view over one z slice. Returns VK_NULL_HANDLE when the image is not // 2D-array-compatible, which is the whole reason this can fail. const VkImageView sliceView = m_textureManager->GetOrCreateAttachmentViewAtMipLevel( texture, mipLevel, depthSlice, 1, VK_IMAGE_VIEW_TYPE_2D); if (sliceView == VK_NULL_HANDLE) return false; VkAttachmentDescription colorAttachment{}; colorAttachment.format = resource->format; // The image's own count, not a hardcoded one: a render-pass attachment must match the // image it is given (VUID-VkFramebufferCreateInfo-pAttachments-00880), and this helper is // now also the multisample path - a multisample image carries no TRANSFER_DST usage, so a // load-op clear is the only legal way to clear it at all. colorAttachment.samples = resource->sampleCount; colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; // Hand the slice back in the layout the caller already tracks for the whole image, so its // closing barrier stays truthful and resource->layout is never touched from in here. colorAttachment.finalLayout = finalLayout; VkAttachmentReference colorRef{}; colorRef.attachment = 0; colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &colorRef; VkRenderPassCreateInfo renderPassInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; renderPassInfo.attachmentCount = 1; renderPassInfo.pAttachments = &colorAttachment; renderPassInfo.subpassCount = 1; renderPassInfo.pSubpasses = &subpass; VkRenderPass renderPass = VK_NULL_HANDLE; if (vkCreateRenderPass(m_device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) return false; const Uint32 levelWidth = std::max(resource->extent.width >> mipLevel, 1u); const Uint32 levelHeight = std::max(resource->extent.height >> mipLevel, 1u); VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; framebufferInfo.renderPass = renderPass; framebufferInfo.attachmentCount = 1; framebufferInfo.pAttachments = &sliceView; framebufferInfo.width = levelWidth; framebufferInfo.height = levelHeight; framebufferInfo.layers = 1; VkFramebuffer framebuffer = VK_NULL_HANDLE; if (vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer) != VK_SUCCESS) { vkDestroyRenderPass(m_device, renderPass, nullptr); return false; } VkRenderPassBeginInfo beginInfo{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; beginInfo.renderPass = renderPass; beginInfo.framebuffer = framebuffer; beginInfo.renderArea.extent = {levelWidth, levelHeight}; beginInfo.clearValueCount = 1; beginInfo.pClearValues = &clearValue; // The load op is the whole operation: begin and end with nothing in between. vkCmdBeginRenderPass(commandBuffer, &beginInfo, VK_SUBPASS_CONTENTS_INLINE); vkCmdEndRenderPass(commandBuffer); // The image view is owned and memoised by the texture resource; only these two are throwaway. auto& deferredCleanup = m_deferredDepthMipmapCleanup[m_frameContext.GetCurrentFrameIndex()]; deferredCleanup.renderPasses.push_back(renderPass); deferredCleanup.framebuffers.push_back(framebuffer); return true; } Bool VulkanRenderer::MaterializePendingClearForTexture(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture) { Vector pendingClears; if (!m_clearManager->GetPendingClears(&texture, pendingClears)) { return true; } // A pass may stay open on the FRAME command buffer while this clear is // recorded into the pre-pass stream (a different command buffer that // executes strictly before the frame's commands). MOBILEGL_ASSERT(VkRenderPassManager::GetActiveRenderPass() == nullptr || commandBuffer != m_frameContext.GetCurrent().commandBuffer, "MaterializePendingClearForTexture requires no active render pass on the target buffer"); auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture); if (resource == nullptr) { // Declined sync (an incomplete texture, say). Nothing to clear into, and every line // below dereferences this - the assert that used to stand here is compiled out of // every build past DEBUG. MGLOG_E_ONCE("MaterializePendingClearForTexture: no texture resource for textureId=%d; the queued clears " "stay queued", texture.GetExternalIndex()); return false; } // A multisample image is not a transfer target: SyncTextureResource deliberately withholds // TRANSFER_DST/TRANSFER_SRC from every one of them, so the vkCmdClearColorImage below - // and the TRANSFER_DST transition ahead of it - are invalid usage // (VUID-vkCmdClearColorImage-image-00002) on exactly the shape a // glClearBufferfv-then-sample sequence produces. Clear it the one way that is legal at // any sample count instead: a throwaway render pass whose whole content is its load-op // clear, which is also what the 3D-slice case below already does. if (resource->sampleCount != VK_SAMPLE_COUNT_1_BIT) { return MaterializeMultisamplePendingClear(commandBuffer, texture, *resource, pendingClears); } VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; GetImageTransitionSourceState(resource->layout, srcStageMask, srcAccessMask); VkImageLayout clearLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; Bool ok = VkTextureManager::TransitionImageLayout( commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, resource->aspect, 0, resource->mipLevels); MOBILEGL_ASSERT(ok, "MaterializePendingClearForTexture: failed to transition textureId=%d to TRANSFER_DST", texture.GetExternalIndex()); VkImageLayout sampledLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; for (const auto& pendingClear : pendingClears) { MOBILEGL_ASSERT(pendingClear.key.mipLevel < resource->mipLevels, "MaterializePendingClearForTexture: textureId=%d pending clear mip=%u out of range %u", texture.GetExternalIndex(), pendingClear.key.mipLevel, resource->mipLevels); // FIXME: a layered clear of a GL_TEXTURE_3D texture still reads back wrong. // KHR-GL44/45/46.geometry_shader.layered_framebuffer.clear_call_support fails on // DirectVulkan: it attaches a 4-deep 3D texture with glFramebufferTexture (layered), // clears with glClearBufferiv, then reads each slice back through // glFramebufferTextureLayer and gets zeros. Those cases exist only in the GL44+ lists, // above the 4.0 this backend reports, so they are outside the current conformance // claim - but the feature (layered attachment, GL 3.2) is not, so an application can // reach this. // // Already ruled out by bisecting with temporary bypasses, so do not re-test these: // - the per-slice render-pass clear below (disabling it changes nothing) // - the per-target gate in FramebufferTextureLayer_State (it already permits // Texture3D here; bypassing it changes nothing) // - VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT on the 3D image (not requesting it // changes nothing) // What IS fixed here is the subresource range below: a layered GL clear queues // layerCount = depth, which is illegal for a VK_IMAGE_TYPE_3D image, and the old code // passed it straight through - running the case standalone against the previous build // trips MOBILEGL_ASSERT(baseArrayLayer + layerCount <= arrayLayers) as 0 + 4 <= 1. // // Note when picking this up: the case does not reproduce standalone the way it behaves // in a batch run (batch passed before this change, standalone asserted), so it depends // on state left by earlier cases. Reproduce it inside a chunk, not on its own. // // A 3D image keeps its GL layers on the z axis (arrayLayers == 1), so the pending // clear's "layer" is a slice index bounded by the mip level's depth. const Bool clearAddressesDepthSlices = resource->viewType == VK_IMAGE_VIEW_TYPE_3D; const Uint32 clearableLayers = clearAddressesDepthSlices ? std::max(resource->depth >> pendingClear.key.mipLevel, 1u) : resource->arrayLayers; MOBILEGL_ASSERT(pendingClear.key.baseArrayLayer + pendingClear.key.layerCount <= clearableLayers, "MaterializePendingClearForTexture: textureId=%d pending clear layer span [%u, %u) exceeds %u", texture.GetExternalIndex(), pendingClear.key.baseArrayLayer, pendingClear.key.baseArrayLayer + pendingClear.key.layerCount, clearableLayers); // Whether this clear names a strict SUBSET of the level. A layered attachment // (glFramebufferTexture) queues layerCount = the whole depth, a single-slice one // (glFramebufferTextureLayer) queues 1 - so the key already distinguishes them, and it is // the clear's span that decides, not the image's slice count. Reading the latter sent a // layered clear of a 3D texture down the per-slice path, where it cleared slice zero and // left the rest stale (geometry_shader.layered_framebuffer.clear_call_support). const Bool clearsWholeLevel = pendingClear.key.baseArrayLayer == 0 && pendingClear.key.layerCount >= clearableLayers; if (clearAddressesDepthSlices && clearableLayers > 1 && !clearsWholeLevel) { // vkCmdClearColorImage cannot clear a subset of a 3D image's slices: // VUID-vkCmdClearColorImage-baseArrayLayer-01472 pins baseArrayLayer to 0 and // layerCount to 1 for VK_IMAGE_TYPE_3D, i.e. the whole mip level. A render pass whose // only content is its LOAD_OP_CLEAR does address exactly one slice, because its // attachment is a 2D view over that slice. auto clearPayload3D = pendingClear.payload; PreCompensateSrgbClearColor(clearPayload3D, resource->format); VkClearValue sliceClearValue{}; sliceClearValue.color = MakeVkClearColorValue(clearPayload3D, ColorFormatLacksAlpha(&texture)); if (!ClearDepthSliceWithRenderPass(commandBuffer, texture, pendingClear.key.mipLevel, pendingClear.key.baseArrayLayer, sliceClearValue)) { // The device or the format refused VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT, so // there is no way to name this slice. Leaving it uncleared is wrong pixels; // asserting would abort a process that glFramebufferTextureLayer can reach at will. MGLOG_W_ONCE("MaterializePendingClearForTexture: textureId=%d slice %u could not be cleared " "(no 2D-array-compatible view)", texture.GetExternalIndex(), pendingClear.key.baseArrayLayer); } continue; } VkImageSubresourceRange subresourceRange{}; subresourceRange.baseMipLevel = pendingClear.key.mipLevel; subresourceRange.levelCount = 1; // VUID-vkCmdClearColorImage-baseArrayLayer-01472: for a VK_IMAGE_TYPE_3D image the range // must name baseArrayLayer 0 and layerCount 1, which Vulkan reads as "the whole mip // level" - the z extent is not an array dimension. A layered GL clear queues // layerCount = depth, which is the right GL answer and an illegal Vulkan one. subresourceRange.baseArrayLayer = clearAddressesDepthSlices ? 0u : pendingClear.key.baseArrayLayer; subresourceRange.layerCount = clearAddressesDepthSlices ? 1u : pendingClear.key.layerCount; auto clearPayload = pendingClear.payload; if ((resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) { subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; PreCompensateSrgbClearColor(clearPayload, resource->format); const VkClearColorValue clearValue = MakeVkClearColorValue(clearPayload, ColorFormatLacksAlpha(&texture)); vkCmdClearColorImage(commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearValue, 1, &subresourceRange); } else { VkImageAspectFlags clearAspectMask = 0; if ((resource->aspect & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 && (clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0) { clearAspectMask |= VK_IMAGE_ASPECT_DEPTH_BIT; } if ((resource->aspect & VK_IMAGE_ASPECT_STENCIL_BIT) != 0 && (clearPayload.mask & GL_STENCIL_BUFFER_BIT) != 0) { clearAspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT; } MOBILEGL_ASSERT(clearAspectMask != 0, "MaterializePendingClearForTexture: textureId=%d has no matching depth/stencil clear mask", texture.GetExternalIndex()); subresourceRange.aspectMask = clearAspectMask; VkClearDepthStencilValue clearValue{}; clearValue.depth = clearPayload.depth; clearValue.stencil = clearPayload.stencil; vkCmdClearDepthStencilImage(commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearValue, 1, &subresourceRange); sampledLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; } } ok = VkTextureManager::TransitionImageLayout( commandBuffer, resource->image, clearLayout, sampledLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels); MOBILEGL_ASSERT(ok, "MaterializePendingClearForTexture: failed to transition textureId=%d to sampled layout", texture.GetExternalIndex()); resource->layout = sampledLayout; m_clearManager->PopPendingClear(&texture); MGLOG_D("MaterializePendingClearForTexture: textureId=%d pending clear materialized", texture.GetExternalIndex()); return true; } Bool VulkanRenderer::MaterializeMultisamplePendingClear(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture, VkTextureManager::TextureResource& resource, const Vector& pendingClears) { // Colour only. GL can queue a depth/stencil clear on a multisample texture too, and the // load-op idiom would serve it just as well, but this helper attaches its view as a // COLOUR attachment; declining is honest and leaves the queue intact for a later path. if ((resource.aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) { MGLOG_E_ONCE("MaterializeMultisamplePendingClear: textureId=%d is a multisample depth/stencil texture; " "its queued clear cannot be materialised out of a render pass yet", texture.GetExternalIndex()); return false; } Bool allCleared = true; for (const auto& pendingClear : pendingClears) { if (pendingClear.key.mipLevel >= resource.mipLevels) { MGLOG_E_ONCE("MaterializeMultisamplePendingClear: textureId=%d pending clear mip=%u out of range %u", texture.GetExternalIndex(), pendingClear.key.mipLevel, resource.mipLevels); allCleared = false; continue; } auto clearPayload = pendingClear.payload; PreCompensateSrgbClearColor(clearPayload, resource.format); VkClearValue clearValue{}; clearValue.color = MakeVkClearColorValue(clearPayload, ColorFormatLacksAlpha(&texture)); // A multisample texture has exactly one level and, for the 2D target, one layer; the // array target's layers are cleared one at a time, which is what this helper's // per-layer view gives us. const Uint32 firstLayer = pendingClear.key.baseArrayLayer; const Uint32 layerCount = std::max(pendingClear.key.layerCount, 1u); for (Uint32 layer = firstLayer; layer < firstLayer + layerCount; ++layer) { if (layer >= resource.arrayLayers) break; // COLOR_ATTACHMENT_OPTIMAL, not TRANSFER_DST: the image never has transfer usage, // and a render target is where it came from and where it is going. if (!ClearDepthSliceWithRenderPass(commandBuffer, texture, pendingClear.key.mipLevel, layer, clearValue, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL)) { MGLOG_E_ONCE("MaterializeMultisamplePendingClear: textureId=%d layer %u could not be cleared", texture.GetExternalIndex(), layer); allCleared = false; } } } if (!allCleared) { return false; } // The load-op clear left every touched layer in COLOR_ATTACHMENT_OPTIMAL (each pass's // finalLayout), so that - not the tracked layout on entry - is what the closing barrier // has to start from. // TransitionImageLayout takes the tracked layout by reference and updates it, so seeding // it is both how the barrier learns its source and how resource->layout ends up right. resource.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; const Bool ok = VkTextureManager::TransitionImageLayout( commandBuffer, resource.image, resource.layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, resource.aspect, 0, resource.mipLevels); if (!ok) { MGLOG_E_ONCE("MaterializeMultisamplePendingClear: failed to transition textureId=%d to the sampled layout", texture.GetExternalIndex()); return false; } m_clearManager->PopPendingClear(&texture); MGLOG_D("MaterializeMultisamplePendingClear: textureId=%d pending clear materialised through a load-op pass", texture.GetExternalIndex()); return true; } Bool VulkanRenderer::MaterializePendingClearForRenderbuffer( VkCommandBuffer commandBuffer, const SharedPtr& renderbuffer) { if (renderbuffer == nullptr) { return true; } ClearAttachmentPayload clearPayload{}; if (!m_renderPassManager->GetPendingRenderbufferClear(renderbuffer.get(), clearPayload)) { return true; } MOBILEGL_ASSERT(VkRenderPassManager::GetActiveRenderPass() == nullptr, "MaterializePendingClearForRenderbuffer requires no active render pass"); auto* resource = m_renderPassManager->GetOrCreateRenderbufferResource(renderbuffer); if (resource == nullptr) { MGLOG_E_ONCE("MaterializePendingClearForRenderbuffer: no resource for renderbuffer %u", renderbuffer->GetExternalIndex()); return false; } VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; GetImageTransitionSourceState(resource->layout, srcStageMask, srcAccessMask); Bool ok = VkTextureManager::TransitionImageLayout( commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, resource->aspect, 0, 1); MOBILEGL_ASSERT(ok, "MaterializePendingClearForRenderbuffer: failed to transition renderbuffer %u to TRANSFER_DST", renderbuffer->GetExternalIndex()); VkImageSubresourceRange subresourceRange{}; subresourceRange.baseMipLevel = 0; subresourceRange.levelCount = 1; subresourceRange.baseArrayLayer = 0; subresourceRange.layerCount = 1; VkImageLayout steadyLayout; if ((resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) { subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; PreCompensateSrgbClearColor(clearPayload, resource->format); // RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1. const VkClearColorValue clearValue = MakeVkClearColorValue( clearPayload, MG_Util::GetBaseInternalFormatComponentCount(renderbuffer->GetInternalFormat()) == 3); vkCmdClearColorImage(commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearValue, 1, &subresourceRange); steadyLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; } else { VkImageAspectFlags clearAspectMask = 0; if ((resource->aspect & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 && (clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0) { clearAspectMask |= VK_IMAGE_ASPECT_DEPTH_BIT; } if ((resource->aspect & VK_IMAGE_ASPECT_STENCIL_BIT) != 0 && (clearPayload.mask & GL_STENCIL_BUFFER_BIT) != 0) { clearAspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT; } if (clearAspectMask != 0) { subresourceRange.aspectMask = clearAspectMask; VkClearDepthStencilValue clearValue{}; clearValue.depth = clearPayload.depth; clearValue.stencil = clearPayload.stencil; vkCmdClearDepthStencilImage(commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearValue, 1, &subresourceRange); } steadyLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; } VkImageLayout clearLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; ok = VkTextureManager::TransitionImageLayout( commandBuffer, resource->image, clearLayout, steadyLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT, resource->aspect, 0, 1); MOBILEGL_ASSERT(ok, "MaterializePendingClearForRenderbuffer: failed to transition renderbuffer %u to steady layout", renderbuffer->GetExternalIndex()); resource->layout = steadyLayout; m_renderPassManager->PopPendingRenderbufferClear(renderbuffer.get()); MGLOG_D("MaterializePendingClearForRenderbuffer: renderbuffer %u pending clear materialized", renderbuffer->GetExternalIndex()); return true; } void VulkanRenderer::DestroyMultisampleResolveScratchImage() { if (m_msResolveScratch.image != VK_NULL_HANDLE) { vmaDestroyImage(m_allocator, m_msResolveScratch.image, m_msResolveScratch.allocation); } m_msResolveScratch = {}; } Bool VulkanRenderer::AcquireMultisampleResolveScratchImage(VkCommandBuffer commandBuffer, VkFormat format, VkExtent2D extent) { if (extent.width == 0 || extent.height == 0 || format == VK_FORMAT_UNDEFINED) { return false; } // Grow-only, and never shrink: these blits repeat at one or two sizes, so the steady state // is one allocation for the whole process. if (m_msResolveScratch.image == VK_NULL_HANDLE || m_msResolveScratch.format != format || m_msResolveScratch.extent.width < extent.width || m_msResolveScratch.extent.height < extent.height) { const VkExtent2D grown = {std::max(extent.width, m_msResolveScratch.extent.width), std::max(extent.height, m_msResolveScratch.extent.height)}; DestroyMultisampleResolveScratchImage(); VkImageCreateInfo imageInfo{}; imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; imageInfo.imageType = VK_IMAGE_TYPE_2D; imageInfo.format = format; imageInfo.extent = {grown.width, grown.height, 1}; imageInfo.mipLevels = 1; imageInfo.arrayLayers = 1; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; VmaAllocationCreateInfo allocationInfo{}; allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; if (vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &m_msResolveScratch.image, &m_msResolveScratch.allocation, nullptr) != VK_SUCCESS) { // Soft failure: the caller keeps the direct resolve, which is what shipped before. MGLOG_E_ONCE("AcquireMultisampleResolveScratchImage: vmaCreateImage failed (format=%d %ux%u)", static_cast(format), grown.width, grown.height); m_msResolveScratch = {}; return false; } m_msResolveScratch.format = format; m_msResolveScratch.extent = grown; m_msResolveScratch.layout = VK_IMAGE_LAYOUT_UNDEFINED; } VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; GetImageTransitionSourceState(m_msResolveScratch.layout, srcStageMask, srcAccessMask); if (!VkTextureManager::TransitionImageLayout(commandBuffer, m_msResolveScratch.image, m_msResolveScratch.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_ASPECT_COLOR_BIT)) { return false; } return true; } // The aspects a depth/stencil format actually carries. VkTextureManager keeps its own copy of // this private, and the swapchain's depth/stencil image has no TextureResource to ask. static VkImageAspectFlags GetDepthStencilAspectMaskForFormat(VkFormat format) { switch (format) { case VK_FORMAT_D16_UNORM: case VK_FORMAT_X8_D24_UNORM_PACK32: case VK_FORMAT_D32_SFLOAT: return VK_IMAGE_ASPECT_DEPTH_BIT; case VK_FORMAT_D16_UNORM_S8_UINT: case VK_FORMAT_D24_UNORM_S8_UINT: case VK_FORMAT_D32_SFLOAT_S8_UINT: return VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; case VK_FORMAT_S8_UINT: return VK_IMAGE_ASPECT_STENCIL_BIT; default: return VK_IMAGE_ASPECT_NONE; } } // The depth/stencil half of MaterializePendingClearForDefaultFramebuffer. Separate only // because the image, the aspects and the clear value are all different from the colour one; // the reason it exists is the same - a readback with no intervening draw has no render pass // to fold the parked clear into. Bool VulkanRenderer::MaterializePendingDepthStencilClearForDefaultFramebuffer( VkCommandBuffer commandBuffer, const MG_State::GLState::FramebufferAttachmentObject& attachment, const ClearAttachmentPayload& payload) { // The image the readback that triggered this materialization resolves; a parked clear // must land where the read looks (see m_defaultFramebufferImageIndex). const Uint32 swapchainImageIndex = DefaultFramebufferReadIndex(true); const VkImage depthStencilImage = m_swapchainObject.GetDepthStencilImage(swapchainImageIndex); if (depthStencilImage == VK_NULL_HANDLE) { return false; } const VkImageAspectFlags imageAspects = GetDepthStencilAspectMaskForFormat(m_swapchainObject.GetDepthStencilFormat()); VkImageAspectFlags clearAspects = 0; if ((payload.mask & GL_DEPTH_BUFFER_BIT) != 0) clearAspects |= (imageAspects & VK_IMAGE_ASPECT_DEPTH_BIT); if ((payload.mask & GL_STENCIL_BUFFER_BIT) != 0) clearAspects |= (imageAspects & VK_IMAGE_ASPECT_STENCIL_BIT); if (clearAspects == 0) { // Nothing this image can express; drop the pending clear rather than leave it to a // later render pass that would load it against an aspect that does not exist. m_clearManager->PopPendingClear(attachment); return true; } VkImageLayout currentLayout = m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex); VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; GetImageTransitionSourceState(currentLayout, srcStageMask, srcAccessMask); VkImageLayout clearLayout = currentLayout; if (!VkTextureManager::TransitionImageLayout(commandBuffer, depthStencilImage, clearLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, imageAspects)) { return false; } VkClearDepthStencilValue clearValue{}; clearValue.depth = payload.depth; clearValue.stencil = payload.stencil; VkImageSubresourceRange range{}; range.aspectMask = clearAspects; range.baseMipLevel = 0; range.levelCount = 1; range.baseArrayLayer = 0; range.layerCount = 1; vkCmdClearDepthStencilImage(commandBuffer, depthStencilImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearValue, 1, &range); VkImageLayout settledLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags dstAccessMask = 0; GetImageTransitionDestinationState(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, dstStageMask, dstAccessMask); if (!VkTextureManager::TransitionImageLayout(commandBuffer, depthStencilImage, settledLayout, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, imageAspects)) { return false; } m_swapchainObject.SetDepthStencilImageLayout(swapchainImageIndex, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); // The image now holds real values, so the next render pass must LOAD them rather than // treat the attachment as undefined and discard the clear that just executed. m_swapchainObject.SetDepthStencilContentDefined(swapchainImageIndex, true); m_clearManager->PopPendingClear(attachment); MGLOG_D("MaterializePendingClearForDefaultFramebuffer: swapchain depth/stencil image %u pending clear " "materialized (aspects=0x%x)", swapchainImageIndex, static_cast(clearAspects)); return true; } // A glClear on the DEFAULT framebuffer is parked as a pending clear and folded into the next // render pass's loadOp. With no draw in between there is no render pass, so a readback that // followed such a clear blitted the untouched swapchain image and returned the PREVIOUS // frame's colour - which is exactly what the whole KHR-GL40.draw_indirect.negative-* family // sees (clear, an erroring draw that never executes, glReadPixels expecting zeroes). // // Materializing it means clearing the acquired swapchain image itself, which is why this // cannot reuse MaterializePendingClearForTexture: the default FBO's colour attachment is a // placeholder ITextureObject, and syncing it would allocate and clear an unrelated image. Bool VulkanRenderer::MaterializePendingClearForDefaultFramebuffer(VkCommandBuffer commandBuffer, MG_State::GLState::FramebufferObject& fbo, FramebufferAttachmentType attachmentType) { if (!fbo.IsDefaultFramebuffer() || attachmentType == FramebufferAttachmentType::None) { return true; } const auto& attachment = fbo.GetAttachment(attachmentType); if (!attachment.IsTexture() || attachment.IsRenderbuffer()) { return true; } ClearAttachmentPayload payload{}; if (!m_clearManager->GetPendingClear(attachment, payload)) { return true; } MOBILEGL_ASSERT(VkRenderPassManager::GetActiveRenderPass() == nullptr || commandBuffer != m_frameContext.GetCurrent().commandBuffer, "MaterializePendingClearForDefaultFramebuffer requires no active render pass"); if ((payload.mask & GL_COLOR_BUFFER_BIT) == 0) { return MaterializePendingDepthStencilClearForDefaultFramebuffer(commandBuffer, attachment, payload); } // Same rule as the depth/stencil twin below: the parked clear has to land on the image // the readback that reaches this resolves. const Uint32 swapchainImageIndex = DefaultFramebufferReadIndex(true); const VkImage swapchainImage = m_swapchainObject.GetImage(swapchainImageIndex); if (swapchainImage == VK_NULL_HANDLE) { return false; } VkImageLayout currentLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex); VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; GetImageTransitionSourceState(currentLayout, srcStageMask, srcAccessMask); VkImageLayout clearLayout = currentLayout; if (!VkTextureManager::TransitionImageLayout(commandBuffer, swapchainImage, clearLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_ASPECT_COLOR_BIT)) { return false; } // The clear colour goes in verbatim, alpha included. Forcing opaque alpha here is what // makes a glClear(0,0,0,0) read back as (0,0,0,1) - the default framebuffer's placeholder // attachment can describe an alpha-less format while the swapchain image it stands for // has a real alpha channel. VkClearColorValue clearColor{}; clearColor.float32[0] = payload.color.x(); clearColor.float32[1] = payload.color.y(); clearColor.float32[2] = payload.color.z(); clearColor.float32[3] = payload.color.w(); VkImageSubresourceRange range{}; range.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; range.baseMipLevel = 0; range.levelCount = 1; range.baseArrayLayer = 0; range.layerCount = 1; vkCmdClearColorImage(commandBuffer, swapchainImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearColor, 1, &range); VkImageLayout settledLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags dstAccessMask = 0; GetImageTransitionDestinationState(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, dstStageMask, dstAccessMask); if (!VkTextureManager::TransitionImageLayout(commandBuffer, swapchainImage, settledLayout, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, VK_IMAGE_ASPECT_COLOR_BIT)) { return false; } m_swapchainObject.SetImageLayout(swapchainImageIndex, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); // Popped, not left behind: the clear has executed, so letting the next render pass load // it again as a loadOp would erase whatever is drawn between here and there. m_clearManager->PopPendingClear(attachment); MGLOG_D("MaterializePendingClearForDefaultFramebuffer: swapchain image %u pending clear materialized", swapchainImageIndex); return true; } Bool VulkanRenderer::TryBlitToDefaultFramebufferWithShader(FrameContext::FrameData& frame, MG_State::GLState::FramebufferObject& readFbo, MG_State::GLState::FramebufferObject& drawFbo, GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLenum filter) { const Bool drawIsDefaultFbo = drawFbo.IsDefaultFramebuffer(); if (!drawIsDefaultFbo) { return false; } BlitImageBinding srcBinding{}; BlitImageBinding dstBinding{}; // The source reads the image the app last rendered into, the destination re-points the // default framebuffer at the acquired one - see m_defaultFramebufferImageIndex. The // source resolves first, so a default-FBO source still names the pre-present image. const Uint32 readDefaultImageIndex = DefaultFramebufferReadIndex(readFbo.IsDefaultFramebuffer()); const Uint32 drawDefaultImageIndex = DefaultFramebufferWriteIndex(drawIsDefaultFbo); if (!ResolveColorBlitBinding(readFbo, true, readDefaultImageIndex, m_swapchainObject, *m_textureManager, *m_renderPassManager, srcBinding) || !ResolveColorBlitBinding(drawFbo, false, drawDefaultImageIndex, m_swapchainObject, *m_textureManager, *m_renderPassManager, dstBinding)) { return false; } if (srcBinding.trackedLayout == nullptr) { MGLOG_E_ONCE("BlitFramebuffer skipped: shader blit to default framebuffer requires a texture-backed source framebuffer"); return false; } auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); if (activeRenderPass != nullptr) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } const auto& attachment = readFbo.GetAttachment(readFbo.GetReadBuffer()); auto sourceTexture = attachment.GetTexture(); MOBILEGL_ASSERT(sourceTexture != nullptr, "TryBlitToDefaultFramebufferWithShader: source texture is null"); const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *sourceTexture); MOBILEGL_ASSERT(clearReady, "TryBlitToDefaultFramebufferWithShader: failed to materialize pending clear for textureId=%d", sourceTexture->GetExternalIndex()); #if MOBILEGL_BUILD_DISAGGREGATED // P7 wave 2-B2 (CONTRACT-P7 ยง5.2, (B')): THE MONOLITH ARM ON THE BAKED MODULE. Below // this line the pull build binds a hidden GL program, writes three default-block // uniforms into its global UBO and hands the sampler to the application's descriptor // binder. A disaggregated build has no such program on either arm: the same pass, with // the same vertex shader (WireColorBlit.vert IS this one, ported), takes its rects on a // push constant and its sampler from its own descriptor set. // // The pre-amble above stays shared because it is not about the program: resolving the // two bindings, ending an open render pass and materializing a pending clear on the // source are what makes the blit legal, whichever pass performs it. What the baked path // does NOT need is the sampling transition, the descriptor sync and the cached sampled // view - it makes its own view and moves the layouts through TransitionWireImage, whose // tracker is the same `trackedLayout` these bindings carry. { const auto toWire = [](const BlitImageBinding& binding) { WireImage out{}; out.image = binding.image; out.format = binding.format; out.extent = {static_cast(binding.extent.x()), static_cast(binding.extent.y())}; out.samples = binding.sampleCount; out.aspect = binding.aspectMask; out.level = binding.mipLevel; out.levels = binding.mipLevelCount; out.layer = binding.baseArrayLayer; out.layers = binding.layerCount; out.viewType = binding.layerCount > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; out.trackedLayout = binding.trackedLayout; out.layout = binding.trackedLayout ? *binding.trackedLayout : VK_IMAGE_LAYOUT_UNDEFINED; return out; }; WireImage source = toWire(srcBinding); WireImage destination = toWire(dstBinding); // The default framebuffer's layout lives in the swapchain object, not in a // per-resource slot, which is what ResolveWireImage does for the same surface and // what TransitionWireImage writes back through. destination.isDefault = true; destination.swapchainImageIndex = drawDefaultImageIndex; destination.trackedLayout = nullptr; destination.layout = m_swapchainObject.GetImageLayout(drawDefaultImageIndex); // `false` here means "not serviced by the shader pass", and the caller falls // through to the native vkCmdBlitImage arm - the same contract the pull build's // early returns above have. return BlitWireColorToDefault(source, destination, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, filter); } #else const Bool ready = m_textureManager->TransitionTextureForSampling(frame.commandBuffer, *sourceTexture); if (!ready) { MGLOG_E_ONCE("BlitFramebuffer skipped: failed to transition source textureId=%d for sampling", sourceTexture->GetExternalIndex()); return false; } if (m_textureManager->SyncTextureAndGetDescriptor(*sourceTexture) == nullptr) { MGLOG_E_ONCE("BlitFramebuffer skipped: failed to resolve source textureId=%d after sampling transition", sourceTexture->GetExternalIndex()); return false; } const VkImageView sourceImageView = m_textureManager->GetOrCreateSampledViewAtMipLevel(*sourceTexture, srcBinding.mipLevel); MOBILEGL_ASSERT(sourceImageView != VK_NULL_HANDLE, "TryBlitToDefaultFramebufferWithShader: failed to create sampled view for textureId=%d mip=%u", sourceTexture->GetExternalIndex(), srcBinding.mipLevel); // A color-only blit never touches depth/stencil: let the default-FBO pass // it opens skip the depth attachment (depth-less flavor). auto* renderPassEntryPtr = m_renderPassManager->GetOrCreateRenderPass(drawFbo, drawDefaultImageIndex, /*drawUsesDepthStencil=*/false); if (renderPassEntryPtr == nullptr) { // Declined (the builder logged which attachment). The caller's contract for `false` is // "this blit was not serviced here", which is the honest answer. return false; } auto& renderPassEntry = *renderPassEntryPtr; const Bool ok = VkRenderPassManager::BeginRenderPass(frame.commandBuffer, renderPassEntry); MOBILEGL_ASSERT(ok, "%s: BeginRenderPass failed", __func__); ApplyGLViewportState(frame.commandBuffer, renderPassEntry.extent, m_swapchainObject.GetPreTransform(), drawIsDefaultFbo); VkRect2D scissor{}; scissor.offset = {0, 0}; scissor.extent = {static_cast(renderPassEntry.extent.x()), static_cast(renderPassEntry.extent.y())}; vkCmdSetScissor(frame.commandBuffer, 0, 1, &scissor); MOBILEGL_ASSERT(m_blitResources.program != nullptr, "TryBlitToDefaultFramebufferWithShader: blit program is null"); const VkPipeline pipeline = GetOrCreateBlitPipeline(renderPassEntry); MOBILEGL_ASSERT(pipeline != VK_NULL_HANDLE, "TryBlitToDefaultFramebufferWithShader: blit pipeline is null"); vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); // The blit pipeline's narrower dynamic set (viewport/scissor only) // leaves the other dynamic states undefined; its raw viewport/scissor // writes also bypass the shadow. ResetDynamicStateShadow(); auto* blitProgramData = static_cast(m_blitResources.program->MapUBO()); MOBILEGL_ASSERT(blitProgramData != nullptr, "TryBlitToDefaultFramebufferWithShader: blit UBO is null"); std::fill(blitProgramData, blitProgramData + m_blitResources.program->GetUBOSize(), Uint8{0}); BlitUniformData blitUniformData{}; const float srcWidth = static_cast(srcBinding.extent.x()); const float srcHeight = static_cast(srcBinding.extent.y()); const float dstWidth = static_cast(dstBinding.extent.x()); const float dstHeight = static_cast(dstBinding.extent.y()); float dstNormWidth = dstWidth; float dstNormHeight = dstHeight; switch (m_swapchainObject.GetPreTransform()) { case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR: case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR: dstNormWidth = dstHeight; dstNormHeight = dstWidth; break; default: break; } blitUniformData.srcRect[0] = static_cast(srcX0) / srcWidth; blitUniformData.srcRect[1] = static_cast(srcY0) / srcHeight; blitUniformData.srcRect[2] = static_cast(srcX1 - srcX0) / srcWidth; blitUniformData.srcRect[3] = static_cast(srcY1 - srcY0) / srcHeight; blitUniformData.dstRect[0] = static_cast(dstX0) / dstNormWidth; blitUniformData.dstRect[1] = static_cast(dstY0) / dstNormHeight; blitUniformData.dstRect[2] = static_cast(dstX1 - dstX0) / dstNormWidth; blitUniformData.dstRect[3] = static_cast(dstY1 - dstY0) / dstNormHeight; blitUniformData.surfaceTransform = static_cast(ToBlitSurfaceTransform(m_swapchainObject.GetPreTransform())); auto writeUniform = [&](Int location, const void* data, SizeT size) { MOBILEGL_ASSERT(location >= 0, "TryBlitToDefaultFramebufferWithShader: invalid uniform location"); const Uint offset = m_blitResources.program->GetUniformOffset(static_cast(location)); // A RETURN, not only an assert - see GenerateDepthMipmapWithShader's copy of this // guard: kInvalidUniformOffset must not reach the memcpy in a release build. if (offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset || offset + size > m_blitResources.program->GetUBOSize()) { MOBILEGL_ASSERT(false, "TryBlitToDefaultFramebufferWithShader: uniform write out of bounds"); return; } memcpy(blitProgramData + offset, data, size); }; writeUniform(m_blitResources.srcRectLocation, blitUniformData.srcRect, sizeof(blitUniformData.srcRect)); writeUniform(m_blitResources.dstRectLocation, blitUniformData.dstRect, sizeof(blitUniformData.dstRect)); writeUniform(m_blitResources.surfaceTransformLocation, &blitUniformData.surfaceTransform, sizeof(blitUniformData.surfaceTransform)); m_blitResources.program->MarkUBOContentDirty(); const auto samplerBindingOverride = UniformManager::SamplerBindingOverride{ .binding = m_blitResources.samplerBinding, .texture = sourceTexture.get(), .sampler = (filter == GL_LINEAR ? m_blitResources.linearSampler.get() : m_blitResources.nearestSampler.get()), .imageView = sourceImageView, }; ProgramFactory::CompileOptionFlags blitTransformFlags = 0; const auto& blitProgramObj = m_programFactory->GetOrCreateProgram(*m_blitResources.program, blitTransformFlags); const Bool bound = m_uniformManager->BindProgramUniformBuffers( frame.commandBuffer, *m_blitResources.program, blitProgramObj, m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_GRAPHICS, &samplerBindingOverride); MOBILEGL_ASSERT(bound, "TryBlitToDefaultFramebufferWithShader: BindProgramUniformBuffers failed"); vkCmdDraw(frame.commandBuffer, 3, 1, 0, 0); return true; #endif } // A single-aspect depth/stencil copy between two images of the same format. An image copy // moves the format's own native representation, and for a packed depth-stencil format that // representation is one word holding both aspects: a region that names only the depth aspect // still carries the source's stencil byte into the destination, and the other way round, so a // depth-only resolve silently replaces the destination's stencil (and a stencil-only resolve // its depth). Staging the requested aspect through a buffer makes the region's aspectMask // authoritative - a buffer copy names one aspect and moves exactly its texels - which is the // same reason the wire resolve (WireFramebuffer.inc, ResolveWireDepthStencil) copies its // resolved aspect through a buffer instead of patching the image copy. // // Both images are in a TRANSFER layout when this is called, so no transition is needed here. Bool VulkanRenderer::CopyDepthStencilAspectThroughBuffer( FrameContext::FrameData& frame, VkImage srcImage, VkImage dstImage, VkFormat format, Uint32 srcMipLevel, Uint32 srcBaseArrayLayer, Uint32 dstMipLevel, Uint32 dstBaseArrayLayer, GLint srcX, GLint srcY, GLint dstX, GLint dstY, GLsizei width, GLsizei height, VkImageAspectFlagBits aspect) { // The aspect's own tightly defined copy layout: a stencil texel is one byte whatever the // format, and the depth half of a packed format copies on its own at 2 or 4 bytes. const VkDeviceSize texelSize = aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? 1 : (format == VK_FORMAT_D16_UNORM || format == VK_FORMAT_D16_UNORM_S8_UINT ? 2 : 4); const VkDeviceSize bytes = static_cast(width) * static_cast(height) * texelSize; VkBufferObject staging; if (!staging.Create({.allocator = m_allocator, .size = bytes, .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memoryUsage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE})) { MGLOG_E_ONCE("CopyDepthStencilAspectThroughBuffer: failed to create the aspect staging buffer"); return false; } VkBufferImageCopy region{}; region.imageSubresource = {aspect, srcMipLevel, srcBaseArrayLayer, 1}; region.imageOffset = {srcX, srcY, 0}; region.imageExtent = {static_cast(width), static_cast(height), 1}; vkCmdCopyImageToBuffer(frame.commandBuffer, srcImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, staging.GetHandle(), 1, ®ion); VkBufferMemoryBarrier barrier{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.buffer = staging.GetHandle(); barrier.size = bytes; vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &barrier, 0, nullptr); region.imageSubresource = {aspect, dstMipLevel, dstBaseArrayLayer, 1}; region.imageOffset = {dstX, dstY, 0}; vkCmdCopyBufferToImage(frame.commandBuffer, staging.GetHandle(), dstImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); // The commands above still name this buffer and this frame's submission has not happened // yet, so the buffer retires with the frame rather than with this scope. m_bufferManager.DeferRelease(Move(staging)); return true; } void VulkanRenderer::BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { BlitWireFramebuffers(srcX0,srcY0,srcX1,srcY1,dstX0,dstY0,dstX1,dstY1,mask,filter); return; } #endif auto readFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(); auto drawFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); BlitNamedFramebuffer(readFbo, drawFbo, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); } void VulkanRenderer::BlitNamedFramebuffer(const SharedPtr& readFbo, const SharedPtr& drawFbo, GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { static constexpr GLbitfield kSupportedBlitMask = GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT; if ((mask & ~kSupportedBlitMask) != 0) { MGLOG_E_ONCE("BlitFramebuffer skipped: unsupported mask bits=0x%x", static_cast(mask)); return; } const Bool isColorBlit = (mask & GL_COLOR_BUFFER_BIT) != 0; const Bool isDepthBlit = (mask & GL_DEPTH_BUFFER_BIT) != 0; const Bool isStencilBlit = (mask & GL_STENCIL_BUFFER_BIT) != 0; if (!isColorBlit && !isDepthBlit && !isStencilBlit) { return; } if (filter != GL_NEAREST && filter != GL_LINEAR) { MGLOG_E_ONCE("BlitFramebuffer skipped: unsupported filter=0x%x", static_cast(filter)); return; } if ((isDepthBlit || isStencilBlit) && filter != GL_NEAREST) { MGLOG_E_ONCE("BlitFramebuffer skipped: depth/stencil blits require GL_NEAREST"); return; } // The scissor test clips blit writes: intersect the destination rectangle with // the scissor box and shrink the source proportionally. if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) { const IntVec4& scissor = MGB_CTX->GetScissorBox(); const auto clipAxis = [](GLint& d0, GLint& d1, GLint& s0, GLint& s1, GLint clipLo, GLint clipHi) -> Bool { const Bool dstFlipped = d1 < d0; GLint lo = dstFlipped ? d1 : d0; GLint hi = dstFlipped ? d0 : d1; const GLint newLo = std::max(lo, clipLo); const GLint newHi = std::min(hi, clipHi); if (newLo >= newHi) { return false; } const double srcSpan = static_cast(s1 - s0); const double dstSpan = static_cast(d1 - d0); const double scale = dstSpan != 0.0 ? srcSpan / dstSpan : 0.0; const GLint origD0 = d0; const GLint clippedD0 = dstFlipped ? newHi : newLo; const GLint clippedD1 = dstFlipped ? newLo : newHi; s0 = s0 + static_cast(std::lround((clippedD0 - origD0) * scale)); s1 = s0 + static_cast(std::lround((clippedD1 - clippedD0) * scale)); d0 = clippedD0; d1 = clippedD1; return true; }; if (!clipAxis(dstX0, dstX1, srcX0, srcX1, scissor.x(), scissor.x() + scissor.z()) || !clipAxis(dstY0, dstY1, srcY0, srcY1, scissor.y(), scissor.y() + scissor.w())) { return; // fully scissored out } } MOBILEGL_ASSERT(readFbo != nullptr, "VulkanRenderer::BlitFramebuffer: read framebuffer is null"); MOBILEGL_ASSERT(drawFbo != nullptr, "VulkanRenderer::BlitFramebuffer: draw framebuffer is null"); if (IsUnsupportedFramebufferForDirectVulkan(*readFbo) || IsUnsupportedFramebufferForDirectVulkan(*drawFbo)) { RecordUnsupportedFramebufferError(__func__); return; } auto& frame = m_frameContext.GetCurrent(); if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); } auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); if (activeRenderPass != nullptr) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } const Bool readIsDefaultFbo = readFbo->IsDefaultFramebuffer(); const Bool drawIsDefaultFbo = drawFbo->IsDefaultFramebuffer(); if (isColorBlit && drawIsDefaultFbo && RequiresShaderBlitToDefaultFramebuffer(m_swapchainObject.GetPreTransform())) { if (TryBlitToDefaultFramebufferWithShader(frame, *readFbo, *drawFbo, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, filter)) { return; } MGLOG_E_ONCE("BlitFramebuffer skipped: rotated blit to default framebuffer requires a texture-backed source framebuffer"); return; } Vector depthStencilAspects; if (isDepthBlit) depthStencilAspects.push_back(VK_IMAGE_ASPECT_DEPTH_BIT); if (isStencilBlit) depthStencilAspects.push_back(VK_IMAGE_ASPECT_STENCIL_BIT); for (const VkImageAspectFlagBits depthStencilAspect : depthStencilAspects) { BlitImageBinding srcBinding{}; BlitImageBinding dstBinding{}; // A default-FBO source resolves the image the app last rendered into; a default-FBO // destination re-points it at the image Present() acquired (see // m_defaultFramebufferImageIndex). The source's index is taken first, so a blit // whose two sides are both the default framebuffer still names one image. const Uint32 srcImageIndex = DefaultFramebufferReadIndex(readIsDefaultFbo); const Uint32 dstImageIndex = DefaultFramebufferWriteIndex(drawIsDefaultFbo); if (!ResolveFramebufferBlitBinding(*readFbo, true, srcImageIndex, m_swapchainObject, *m_textureManager, *m_renderPassManager, depthStencilAspect, srcBinding) || !ResolveFramebufferBlitBinding(*drawFbo, false, dstImageIndex, m_swapchainObject, *m_textureManager, *m_renderPassManager, depthStencilAspect, dstBinding)) { // A buffer named in the mask but absent from either framebuffer copies // nothing for that buffer; the other requested buffers still blit. continue; } if (srcX1 < srcX0 || srcY1 < srcY0 || dstX1 < dstX0 || dstY1 < dstY0) { MGLOG_E_ONCE("BlitFramebuffer skipped: depth blits with flipped rectangles are not supported yet"); continue; } const Int srcWidth = srcX1 - srcX0; const Int srcHeight = srcY1 - srcY0; const Int dstWidth = dstX1 - dstX0; const Int dstHeight = dstY1 - dstY0; if (srcWidth <= 0 || srcHeight <= 0 || dstWidth <= 0 || dstHeight <= 0) { MGLOG_E_ONCE("BlitFramebuffer skipped: degenerate depth blit rectangle"); continue; } // A scaling depth blit is legal GL and vkCmdBlitImage scales natively; only a same-size // pair can take the cheaper vkCmdCopyImage. const Bool depthBlitScales = srcWidth != dstWidth || srcHeight != dstHeight; if (!readIsDefaultFbo) { const auto sourceAttachmentType = ResolveFramebufferCopyAttachmentType(*readFbo, true, srcBinding.aspectMask); const auto& sourceAttachment = readFbo->GetAttachment(sourceAttachmentType); if (auto sourceTexture = sourceAttachment.GetTexture(); sourceTexture != nullptr) { const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *sourceTexture); MOBILEGL_ASSERT(clearReady, "BlitFramebuffer: failed to materialize pending clear for depth source textureId=%d", sourceTexture->GetExternalIndex()); } else if (sourceAttachment.IsRenderbuffer()) { const Bool clearReady = MaterializePendingClearForRenderbuffer(frame.commandBuffer, sourceAttachment.GetRenderbuffer()); MOBILEGL_ASSERT(clearReady, "BlitFramebuffer: failed to materialize pending clear for depth source renderbuffer %u", sourceAttachment.GetRenderbuffer()->GetExternalIndex()); } } const auto destAttachmentType = ResolveFramebufferCopyAttachmentType(*drawFbo, false, dstBinding.aspectMask); if (drawIsDefaultFbo) { // Same ordering rule for the default framebuffer's depth/stencil - see the // colour twin below. const Bool dstClearReady = MaterializePendingClearForDefaultFramebuffer( frame.commandBuffer, *drawFbo, destAttachmentType); MOBILEGL_ASSERT(dstClearReady, "BlitFramebuffer: failed to materialize the default framebuffer's pending " "depth/stencil clear"); } else { // A clear queued for the destination predates this blit in API order; // execute it now, or its deferred materialization would later stomp the // copied contents (MC 26.3 OIT clears cloud_depth, then blits the main // depth into it - the stale loadOp=CLEAR erased the copy). const auto& destAttachment = drawFbo->GetAttachment(destAttachmentType); if (auto destTexture = destAttachment.GetTexture(); destTexture != nullptr) { const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *destTexture); MOBILEGL_ASSERT(dstClearReady, "BlitFramebuffer: failed to materialize pending clear for depth destination textureId=%d", destTexture->GetExternalIndex()); } else if (destAttachment.IsRenderbuffer()) { const Bool dstClearReady = MaterializePendingClearForRenderbuffer(frame.commandBuffer, destAttachment.GetRenderbuffer()); MOBILEGL_ASSERT(dstClearReady, "BlitFramebuffer: failed to materialize pending clear for depth destination renderbuffer %u", destAttachment.GetRenderbuffer()->GetExternalIndex()); } } const VkImageLayout srcOriginalLayout = readIsDefaultFbo ? m_swapchainObject.GetDepthStencilImageLayout(srcImageIndex) : *srcBinding.trackedLayout; if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { MGLOG_E_ONCE("BlitFramebuffer skipped: depth source image layout is undefined"); continue; } const VkImageLayout dstOriginalLayout = drawIsDefaultFbo ? m_swapchainObject.GetDepthStencilImageLayout(dstImageIndex) : *dstBinding.trackedLayout; const VkImageLayout dstRestoreLayout = dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL : dstOriginalLayout; // vkCmdCopyImage requires identical depth formats; a mismatched pair (e.g. // a D24S8 renderbuffer into a DEPTH_COMPONENT24 texture backed by the // D32_SFLOAT fallback) round-trips the region through the host with a // per-texel re-encode instead. if (srcBinding.format != dstBinding.format) { if (readIsDefaultFbo || drawIsDefaultFbo) { MGLOG_E_ONCE("BlitFramebuffer skipped: cross-format depth/stencil blit with the default framebuffer"); continue; } if (!BlitDepthAcrossFormats(frame, srcBinding.image, srcBinding.format, srcBinding.trackedLayout, srcBinding.mipLevel, srcBinding.baseArrayLayer, dstBinding.image, dstBinding.format, dstBinding.trackedLayout, dstBinding.mipLevel, dstBinding.baseArrayLayer, srcX0, srcY0, dstX0, dstY0, srcX1 - srcX0, srcY1 - srcY0, srcOriginalLayout, dstRestoreLayout, depthStencilAspect == VK_IMAGE_ASPECT_STENCIL_BIT)) { continue; } continue; } VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; GetImageTransitionSourceState(srcOriginalLayout, srcStageMask, srcAccessMask); // Both blit regions below name `baseArrayLayer` from their binding, and a layered depth // attachment puts that above 0. These barriers carry a mip range only - their layer // range is every layer (see VkTextureManager::TransitionImageLayout). if (readIsDefaultFbo) { VkImageLayout srcTrackedLayout = srcOriginalLayout; Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, srcBinding.aspectMask, srcBinding.mipLevel, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to transition swapchain depth source image", __func__); m_swapchainObject.SetDepthStencilImageLayout(srcImageIndex, srcTrackedLayout); } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, *srcBinding.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, srcBinding.aspectMask, srcBinding.mipLevel, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to transition depth source image", __func__); } VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags dstAccessMask = 0; GetImageTransitionSourceState(dstOriginalLayout, dstStageMask, dstAccessMask); if (drawIsDefaultFbo) { VkImageLayout dstTrackedLayout = dstOriginalLayout; Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstBinding.image, dstTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstBinding.aspectMask, dstBinding.mipLevel, dstBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to transition swapchain depth destination image", __func__); m_swapchainObject.SetDepthStencilImageLayout(dstImageIndex, dstTrackedLayout); } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstBinding.image, *dstBinding.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstBinding.aspectMask, dstBinding.mipLevel, dstBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to transition depth destination image", __func__); } // The default framebuffer is stored display-side-up, so a rect aimed at it (or read // from it) has to be converted out of GL's bottom-origin space - the same conversion // the colour blit below applies. vkCmdCopyImage cannot express it (it has no second // offset to invert), so a default-framebuffer side forces the vkCmdBlitImage form even // at equal size. Without this a scissored depth blit into the default framebuffer // wrote the MIRRORED band: KHR-GL*.framebuffer_blit.scissor_blit clips to the lower // left quadrant, and the depth landed in the upper one. const Bool depthBlitNeedsOrientation = readIsDefaultFbo || drawIsDefaultFbo; if (depthBlitScales || depthBlitNeedsOrientation) { // vkCmdCopyImage cannot resize; NEAREST is the only filter Vulkan allows for a // depth/stencil blit anyway, and the GL front end already rejects the others. VkImageBlit blitRegion{}; blitRegion.srcSubresource.aspectMask = srcBinding.aspectMask; blitRegion.srcSubresource.mipLevel = srcBinding.mipLevel; blitRegion.srcSubresource.baseArrayLayer = srcBinding.baseArrayLayer; blitRegion.srcSubresource.layerCount = srcBinding.layerCount; blitRegion.srcOffsets[0] = {srcX0, srcY0, 0}; blitRegion.srcOffsets[1] = {srcX1, srcY1, 1}; blitRegion.dstSubresource.aspectMask = dstBinding.aspectMask; blitRegion.dstSubresource.mipLevel = dstBinding.mipLevel; blitRegion.dstSubresource.baseArrayLayer = dstBinding.baseArrayLayer; blitRegion.dstSubresource.layerCount = dstBinding.layerCount; blitRegion.dstOffsets[0] = {dstX0, dstY0, 0}; blitRegion.dstOffsets[1] = {dstX1, dstY1, 1}; if (readIsDefaultFbo) { ApplyNativeBlitDefaultFramebufferSourceTransform(m_swapchainObject.GetPreTransform(), srcBinding, blitRegion); } if (drawIsDefaultFbo) { ApplyNativeBlitDefaultFramebufferTransform(m_swapchainObject.GetPreTransform(), dstBinding, blitRegion); } vkCmdBlitImage(frame.commandBuffer, srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dstBinding.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blitRegion, VK_FILTER_NEAREST); } else if (isDepthBlit != isStencilBlit && srcBinding.layerCount == 1 && dstBinding.layerCount == 1 && (GetDepthStencilAspectMaskForFormat(srcBinding.format) & ~static_cast(depthStencilAspect)) != 0) { // Exactly one aspect of a packed depth-stencil format is being copied into a // destination that carries the other one as well. A whole-image copy moves the // packed native word and would replace that other aspect with the source's; see // CopyDepthStencilAspectThroughBuffer. Same format, same size, one layer - the // shapes the buffer round trip can express. A scaled or display-oriented copy // takes the blit above, and a multi-layer one keeps the image copy. (void)CopyDepthStencilAspectThroughBuffer(frame, srcBinding.image, dstBinding.image, srcBinding.format, srcBinding.mipLevel, srcBinding.baseArrayLayer, dstBinding.mipLevel, dstBinding.baseArrayLayer, srcX0, srcY0, dstX0, dstY0, srcWidth, srcHeight, depthStencilAspect); } else { VkImageCopy copyRegion{}; copyRegion.srcSubresource.aspectMask = srcBinding.aspectMask; copyRegion.srcSubresource.mipLevel = srcBinding.mipLevel; copyRegion.srcSubresource.baseArrayLayer = srcBinding.baseArrayLayer; copyRegion.srcSubresource.layerCount = srcBinding.layerCount; copyRegion.srcOffset = {srcX0, srcY0, 0}; copyRegion.dstSubresource.aspectMask = dstBinding.aspectMask; copyRegion.dstSubresource.mipLevel = dstBinding.mipLevel; copyRegion.dstSubresource.baseArrayLayer = dstBinding.baseArrayLayer; copyRegion.dstSubresource.layerCount = dstBinding.layerCount; copyRegion.dstOffset = {dstX0, dstY0, 0}; copyRegion.extent = {static_cast(srcWidth), static_cast(srcHeight), 1}; vkCmdCopyImage(frame.commandBuffer, srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dstBinding.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©Region); } VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcRestoreAccessMask = 0; GetImageTransitionDestinationState(srcOriginalLayout, srcRestoreStageMask, srcRestoreAccessMask); if (readIsDefaultFbo) { VkImageLayout srcTrackedLayout = m_swapchainObject.GetDepthStencilImageLayout(srcImageIndex); Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, srcTrackedLayout, srcOriginalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask, VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, srcBinding.aspectMask, srcBinding.mipLevel, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to restore swapchain depth source image layout", __func__); m_swapchainObject.SetDepthStencilImageLayout(srcImageIndex, srcTrackedLayout); } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, *srcBinding.trackedLayout, srcOriginalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask, VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, srcBinding.aspectMask, srcBinding.mipLevel, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to restore depth source image layout", __func__); } VkPipelineStageFlags dstRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags dstRestoreAccessMask = 0; GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStageMask, dstRestoreAccessMask); if (drawIsDefaultFbo) { VkImageLayout dstTrackedLayout = m_swapchainObject.GetDepthStencilImageLayout(dstImageIndex); Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstBinding.image, dstTrackedLayout, dstRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, dstBinding.aspectMask, dstBinding.mipLevel, dstBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to restore swapchain depth destination image layout", __func__); m_swapchainObject.SetDepthStencilImageLayout(dstImageIndex, dstTrackedLayout); } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstBinding.image, *dstBinding.trackedLayout, dstRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, dstBinding.aspectMask, dstBinding.mipLevel, dstBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to restore depth destination image layout", __func__); } } if (!isColorBlit) { return; } BlitImageBinding srcBinding{}; BlitImageBinding dstBinding{}; // Same split as the depth/stencil loop above, and the same ordering rule: the source's // index is read before the destination's write re-points it. const Uint32 srcImageIndex = DefaultFramebufferReadIndex(readIsDefaultFbo); const Uint32 dstImageIndex = DefaultFramebufferWriteIndex(drawIsDefaultFbo); if (!ResolveColorBlitBinding(*readFbo, true, srcImageIndex, m_swapchainObject, *m_textureManager, *m_renderPassManager, srcBinding) || !ResolveColorBlitBinding(*drawFbo, false, dstImageIndex, m_swapchainObject, *m_textureManager, *m_renderPassManager, dstBinding)) { return; } if (!readIsDefaultFbo) { const auto& sourceAttachment = readFbo->GetAttachment(readFbo->GetReadBuffer()); auto sourceTexture = sourceAttachment.GetTexture(); if (sourceTexture != nullptr) { const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *sourceTexture); MOBILEGL_ASSERT(clearReady, "BlitFramebuffer: failed to materialize pending clear for source textureId=%d", sourceTexture->GetExternalIndex()); } else if (sourceAttachment.IsRenderbuffer()) { const Bool clearReady = MaterializePendingClearForRenderbuffer(frame.commandBuffer, sourceAttachment.GetRenderbuffer()); MOBILEGL_ASSERT(clearReady, "BlitFramebuffer: failed to materialize pending clear for source renderbuffer %u", sourceAttachment.GetRenderbuffer()->GetExternalIndex()); } } if (drawIsDefaultFbo) { // The default framebuffer needs the same ordering, and needed it before anything // consumed its parked clear: Minecraft clears the default framebuffer, renders the // world into its own framebuffer and BLITS the result out, so nothing between the // clear and the blit ever opens a render pass on the default framebuffer to fold the // clear in as a loadOp. The clear therefore stayed pending across the whole frame, // and the first path that did materialize it - the readback - executed it AFTER the // blit and handed back a blank frame (every DirectVulkan retrace, ssim 0.000005). const Bool dstClearReady = MaterializePendingClearForDefaultFramebuffer( frame.commandBuffer, *drawFbo, drawFbo->GetDrawBuffers()[0]); MOBILEGL_ASSERT(dstClearReady, "BlitFramebuffer: failed to materialize the default framebuffer's pending clear"); } else { // A clear queued for the destination predates this blit in API order; execute // it now, or its deferred materialization would later stomp the blitted color. const auto& destAttachment = drawFbo->GetAttachment(drawFbo->GetDrawBuffers()[0]); auto destTexture = destAttachment.GetTexture(); if (destTexture != nullptr) { const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *destTexture); MOBILEGL_ASSERT(dstClearReady, "BlitFramebuffer: failed to materialize pending clear for destination textureId=%d", destTexture->GetExternalIndex()); } else if (destAttachment.IsRenderbuffer()) { const Bool dstClearReady = MaterializePendingClearForRenderbuffer(frame.commandBuffer, destAttachment.GetRenderbuffer()); MOBILEGL_ASSERT(dstClearReady, "BlitFramebuffer: failed to materialize pending clear for destination renderbuffer %u", destAttachment.GetRenderbuffer()->GetExternalIndex()); } } VkImageLayout srcLayout = readIsDefaultFbo ? m_swapchainObject.GetImageLayout(srcImageIndex) : *srcBinding.trackedLayout; VkImageLayout dstLayout = drawIsDefaultFbo ? m_swapchainObject.GetImageLayout(dstImageIndex) : *dstBinding.trackedLayout; const VkImageLayout srcOriginalLayout = srcLayout; const VkImageLayout dstOriginalLayout = dstLayout; const VkImageLayout dstRestoreLayout = dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : dstOriginalLayout; if (readIsDefaultFbo && srcLayout == VK_IMAGE_LAYOUT_UNDEFINED) { MGLOG_E_ONCE("BlitFramebuffer skipped: swapchain source image layout is undefined"); return; } if (srcLayout == VK_IMAGE_LAYOUT_UNDEFINED) { MGLOG_E_ONCE("BlitFramebuffer skipped: source image layout is undefined"); return; } VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; GetImageTransitionSourceState(srcLayout, srcStageMask, srcAccessMask); if (readIsDefaultFbo) { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, srcLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, srcBinding.aspectMask); MOBILEGL_ASSERT(ok, "%s: failed to transition swapchain source image", __func__); m_swapchainObject.SetImageLayout(srcImageIndex, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, *srcBinding.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, srcBinding.aspectMask, 0, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to transition source image", __func__); } VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags dstAccessMask = 0; GetImageTransitionSourceState(dstLayout, dstStageMask, dstAccessMask); if (drawIsDefaultFbo) { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstBinding.image, dstLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstBinding.aspectMask); MOBILEGL_ASSERT(ok, "%s: failed to transition swapchain destination image", __func__); m_swapchainObject.SetImageLayout(dstImageIndex, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstBinding.image, *dstBinding.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstBinding.aspectMask, 0, dstBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to transition destination image", __func__); } VkImageBlit blitRegion{}; blitRegion.srcSubresource.aspectMask = srcBinding.aspectMask; blitRegion.srcSubresource.mipLevel = srcBinding.mipLevel; blitRegion.srcSubresource.baseArrayLayer = srcBinding.baseArrayLayer; blitRegion.srcSubresource.layerCount = srcBinding.layerCount; blitRegion.srcOffsets[0] = {srcX0, srcY0, 0}; blitRegion.srcOffsets[1] = {srcX1, srcY1, 1}; blitRegion.dstSubresource.aspectMask = dstBinding.aspectMask; blitRegion.dstSubresource.mipLevel = dstBinding.mipLevel; blitRegion.dstSubresource.baseArrayLayer = dstBinding.baseArrayLayer; blitRegion.dstSubresource.layerCount = dstBinding.layerCount; blitRegion.dstOffsets[0] = {dstX0, dstY0, 0}; blitRegion.dstOffsets[1] = {dstX1, dstY1, 1}; if (readIsDefaultFbo) { ApplyNativeBlitDefaultFramebufferSourceTransform(m_swapchainObject.GetPreTransform(), srcBinding, blitRegion); } if (drawIsDefaultFbo) { ApplyNativeBlitDefaultFramebufferTransform(m_swapchainObject.GetPreTransform(), dstBinding, blitRegion); } if (srcBinding.sampleCount != VK_SAMPLE_COUNT_1_BIT && dstBinding.sampleCount == VK_SAMPLE_COUNT_1_BIT) { // GL multisample resolve blits are 1:1 by spec; vkCmdBlitImage cannot read a // multisampled source, so the samples have to come down through vkCmdResolveImage. const Uint32 resolveWidth = static_cast(std::abs(srcX1 - srcX0)); const Uint32 resolveHeight = static_cast(std::abs(srcY1 - srcY0)); // vkCmdResolveImage takes ONE offset per side, so it cannot express the axis inversion // that a default-framebuffer rect needs - it would land the mirrored band. When the // transforms above actually moved the region, split the operation: resolve into a // single-sample scratch image at raw offsets, then blit THAT into the destination with // the (already transformed) region, which vkCmdBlitImage can invert. const Bool regionWasTransformed = (readIsDefaultFbo || drawIsDefaultFbo) && (blitRegion.srcOffsets[0].x != srcX0 || blitRegion.srcOffsets[0].y != srcY0 || blitRegion.srcOffsets[1].x != srcX1 || blitRegion.srcOffsets[1].y != srcY1 || blitRegion.dstOffsets[0].x != dstX0 || blitRegion.dstOffsets[0].y != dstY0 || blitRegion.dstOffsets[1].x != dstX1 || blitRegion.dstOffsets[1].y != dstY1); const Bool useScratchResolve = regionWasTransformed && resolveWidth > 0 && resolveHeight > 0 && AcquireMultisampleResolveScratchImage(frame.commandBuffer, srcBinding.format, {resolveWidth, resolveHeight}); VkImageResolve resolveRegion{}; resolveRegion.srcSubresource = blitRegion.srcSubresource; resolveRegion.dstSubresource = blitRegion.dstSubresource; resolveRegion.extent = {resolveWidth, resolveHeight, 1}; if (useScratchResolve) { // The scratch copy is a plain single-layer colour image, and the resolve reads the // SOURCE band the (possibly inverted) transformed region names - taking its min so // an inverted pair still describes the same band. resolveRegion.srcOffset = {std::min(blitRegion.srcOffsets[0].x, blitRegion.srcOffsets[1].x), std::min(blitRegion.srcOffsets[0].y, blitRegion.srcOffsets[1].y), 0}; resolveRegion.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; resolveRegion.dstSubresource.mipLevel = 0; resolveRegion.dstSubresource.baseArrayLayer = 0; resolveRegion.dstSubresource.layerCount = 1; resolveRegion.dstOffset = {0, 0, 0}; vkCmdResolveImage(frame.commandBuffer, srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, m_msResolveScratch.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &resolveRegion); VkImageLayout scratchLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; const Bool scratchReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, m_msResolveScratch.image, scratchLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_ASPECT_COLOR_BIT); MOBILEGL_ASSERT(scratchReady, "%s: failed to transition the resolve scratch image", __func__); m_msResolveScratch.layout = scratchLayout; // Second leg: the scratch image holds the resolved band at its own origin, so the // source side of the region becomes the whole scratch rect and only the // destination keeps the transform. VkImageBlit scratchBlit = blitRegion; scratchBlit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; scratchBlit.srcSubresource.mipLevel = 0; scratchBlit.srcSubresource.baseArrayLayer = 0; scratchBlit.srcSubresource.layerCount = 1; scratchBlit.srcOffsets[0] = {0, 0, 0}; scratchBlit.srcOffsets[1] = {static_cast(resolveWidth), static_cast(resolveHeight), 1}; vkCmdBlitImage(frame.commandBuffer, m_msResolveScratch.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dstBinding.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &scratchBlit, filter == GL_LINEAR ? VK_FILTER_LINEAR : VK_FILTER_NEAREST); } else { resolveRegion.srcOffset = {std::min(srcX0, srcX1), std::min(srcY0, srcY1), 0}; resolveRegion.dstOffset = {std::min(dstX0, dstX1), std::min(dstY0, dstY1), 0}; vkCmdResolveImage(frame.commandBuffer, srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dstBinding.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &resolveRegion); } } else { vkCmdBlitImage(frame.commandBuffer, srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dstBinding.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blitRegion, filter == GL_LINEAR ? VK_FILTER_LINEAR : VK_FILTER_NEAREST); } VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcRestoreAccessMask = 0; GetImageTransitionDestinationState(srcOriginalLayout, srcRestoreStageMask, srcRestoreAccessMask); if (readIsDefaultFbo) { VkImageLayout srcTrackedLayout = m_swapchainObject.GetImageLayout(srcImageIndex); Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, srcTrackedLayout, srcOriginalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask, VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, srcBinding.aspectMask); MOBILEGL_ASSERT(ok, "%s: failed to restore swapchain source image layout", __func__); m_swapchainObject.SetImageLayout(srcImageIndex, srcTrackedLayout); } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, *srcBinding.trackedLayout, srcOriginalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask, VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, srcBinding.aspectMask, 0, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to restore source image layout", __func__); } VkPipelineStageFlags dstRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags dstRestoreAccessMask = 0; GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStageMask, dstRestoreAccessMask); if (drawIsDefaultFbo) { VkImageLayout dstTrackedLayout = m_swapchainObject.GetImageLayout(dstImageIndex); Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstBinding.image, dstTrackedLayout, dstRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, dstBinding.aspectMask); MOBILEGL_ASSERT(ok, "%s: failed to restore swapchain destination image layout", __func__); m_swapchainObject.SetImageLayout(dstImageIndex, dstTrackedLayout); } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstBinding.image, *dstBinding.trackedLayout, dstRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, dstBinding.aspectMask, 0, dstBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to restore destination image layout", __func__); } } void VulkanRenderer::CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) { #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { CopyWireFramebufferToTexture(target,level,xoffset,yoffset,x,y,width,height); return; } #endif if (width <= 0 || height <= 0) { return; } const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); if (textureTarget != TextureTarget::Texture2D) { RecordTextureCopyError(__func__, ErrorCode::InvalidOperation, "CopyTexSubImage2D currently only supports GL_TEXTURE_2D destinations."); return; } if (level < 0) { RecordTextureCopyError(__func__, ErrorCode::InvalidValue, "CopyTexSubImage2D level must be non-negative."); return; } auto& textureUnit = MGB_CTX->GetTextureUnitObject(MGB_CTX->GetActiveTextureUnit()); auto destinationTexture = textureUnit.GetBindingSlot(textureTarget).GetBoundObject(); if (destinationTexture == nullptr) { RecordTextureCopyError(__func__, ErrorCode::InvalidOperation, "CopyTexSubImage2D requires a bound destination texture."); return; } auto readFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(); if (readFbo == nullptr) { RecordTextureCopyError(__func__, ErrorCode::InvalidOperation, "CopyTexSubImage2D requires a framebuffer bound to GL_READ_FRAMEBUFFER."); return; } if (IsUnsupportedFramebufferForDirectVulkan(*readFbo)) { RecordTextureCopyError(__func__, ErrorCode::InvalidFramebufferOperation, "CopyTexSubImage2D does not support the current non-default read framebuffer configuration on DirectVulkan."); return; } auto& frame = m_frameContext.GetCurrent(); if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); } if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } const Bool readIsDefaultFbo = readFbo->IsDefaultFramebuffer(); // CopyTexSubImage2D only ever READS the default framebuffer, so it resolves the image // the app last rendered into (see m_defaultFramebufferImageIndex) - never the one // Present() has already acquired for the next frame. const Uint32 srcImageIndex = DefaultFramebufferReadIndex(readIsDefaultFbo); BlitImageBinding dstBinding{}; if (!ResolveTextureCopyDestinationBinding(*destinationTexture, static_cast(level), *m_textureManager, dstBinding)) { RecordTextureCopyError(__func__, ErrorCode::InvalidOperation, "CopyTexSubImage2D failed to resolve the destination texture."); return; } BlitImageBinding srcBinding{}; if (!ResolveTextureCopySourceBinding(*readFbo, srcImageIndex, m_swapchainObject, *m_textureManager, *m_renderPassManager, dstBinding.aspectMask, srcBinding)) { RecordTextureCopyError(__func__, ErrorCode::InvalidOperation, "CopyTexSubImage2D requires a complete read attachment compatible with the destination texture."); return; } if (!readIsDefaultFbo) { const auto sourceAttachmentType = ResolveFramebufferCopyAttachmentType(*readFbo, true, srcBinding.aspectMask); const auto& sourceAttachment = readFbo->GetAttachment(sourceAttachmentType); auto sourceTexture = sourceAttachment.GetTexture(); MOBILEGL_ASSERT(sourceTexture != nullptr, "CopyTexSubImage2D: source texture attachment is null"); const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *sourceTexture); MOBILEGL_ASSERT(clearReady, "CopyTexSubImage2D: failed to materialize pending clear for source textureId=%d", sourceTexture->GetExternalIndex()); } { // A clear queued for the destination predates this copy in API order; // execute it now so the deferred materialization cannot stomp the copy. const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *destinationTexture); MOBILEGL_ASSERT(dstClearReady, "CopyTexSubImage2D: failed to materialize pending clear for destination textureId=%d", destinationTexture->GetExternalIndex()); } const Bool srcUsesSwapchainDepth = readIsDefaultFbo && (srcBinding.aspectMask & VK_IMAGE_ASPECT_COLOR_BIT) == 0; const VkImageLayout srcOriginalLayout = readIsDefaultFbo ? (srcUsesSwapchainDepth ? m_swapchainObject.GetDepthStencilImageLayout(srcImageIndex) : m_swapchainObject.GetImageLayout(srcImageIndex)) : *srcBinding.trackedLayout; if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { RecordTextureCopyError(__func__, ErrorCode::InvalidOperation, "CopyTexSubImage2D source image has undefined layout."); return; } const VkImageLayout dstOriginalLayout = *dstBinding.trackedLayout; const VkImageLayout dstRestoreLayout = dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED ? ((dstBinding.aspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0 ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) : dstOriginalLayout; VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; GetImageTransitionSourceState(srcOriginalLayout, srcStageMask, srcAccessMask); if (readIsDefaultFbo) { VkImageLayout srcTrackedLayout = srcOriginalLayout; Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, srcBinding.aspectMask, srcBinding.mipLevel, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to transition swapchain source image", __func__); if (srcUsesSwapchainDepth) { m_swapchainObject.SetDepthStencilImageLayout(srcImageIndex, srcTrackedLayout); } else { m_swapchainObject.SetImageLayout(srcImageIndex, srcTrackedLayout); } } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, *srcBinding.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, srcBinding.aspectMask, srcBinding.mipLevel, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to transition source image", __func__); } VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags dstAccessMask = 0; GetImageTransitionSourceState(dstOriginalLayout, dstStageMask, dstAccessMask); Bool dstReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstBinding.image, *dstBinding.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstBinding.aspectMask, dstBinding.mipLevel, dstBinding.mipLevelCount); MOBILEGL_ASSERT(dstReady, "%s: failed to transition destination image", __func__); VkImageCopy copyRegion{}; copyRegion.srcSubresource.aspectMask = srcBinding.aspectMask; copyRegion.srcSubresource.mipLevel = srcBinding.mipLevel; copyRegion.srcSubresource.baseArrayLayer = srcBinding.baseArrayLayer; copyRegion.srcSubresource.layerCount = srcBinding.layerCount; // KNOWN GAP, deliberately not half-fixed here: when the read framebuffer is the default // one this samples GL rows [y, y+h) counted from the TOP of a display-oriented image, so // it takes the mirrored band AND writes it into the (GL-oriented) destination texture // upside down. Correcting only the offset would swap one wrong answer for another, // because vkCmdCopyImage cannot reverse rows: this path has to become a vkCmdBlitImage // with an inverted source Y pair, the way BlitFramebuffer above now does it. Tracked // separately; the four sites behind the 1,759-case orientation defect are the viewport, // the scissor, the ReadPixels copy offset and the readback remap. if (readIsDefaultFbo) { MGLOG_D("DirectVulkan::CopyTexSubImage2D: copying from the DEFAULT framebuffer still uses the raw GL " "Y origin (x=%d y=%d w=%d h=%d); the result is the mirrored band, stored flipped", x, y, width, height); } copyRegion.srcOffset = {x, y, 0}; copyRegion.dstSubresource.aspectMask = dstBinding.aspectMask; copyRegion.dstSubresource.mipLevel = dstBinding.mipLevel; copyRegion.dstSubresource.baseArrayLayer = dstBinding.baseArrayLayer; copyRegion.dstSubresource.layerCount = dstBinding.layerCount; copyRegion.dstOffset = {xoffset, yoffset, 0}; copyRegion.extent = {static_cast(width), static_cast(height), 1}; vkCmdCopyImage(frame.commandBuffer, srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dstBinding.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©Region); VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcRestoreAccessMask = 0; GetImageTransitionDestinationState(srcOriginalLayout, srcRestoreStageMask, srcRestoreAccessMask); if (readIsDefaultFbo) { VkImageLayout srcTrackedLayout = srcUsesSwapchainDepth ? m_swapchainObject.GetDepthStencilImageLayout(srcImageIndex) : m_swapchainObject.GetImageLayout(srcImageIndex); Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, srcTrackedLayout, srcOriginalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask, VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, srcBinding.aspectMask, srcBinding.mipLevel, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to restore swapchain source image layout", __func__); if (srcUsesSwapchainDepth) { m_swapchainObject.SetDepthStencilImageLayout(srcImageIndex, srcTrackedLayout); } else { m_swapchainObject.SetImageLayout(srcImageIndex, srcTrackedLayout); } } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, *srcBinding.trackedLayout, srcOriginalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask, VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, srcBinding.aspectMask, srcBinding.mipLevel, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to restore source image layout", __func__); } VkPipelineStageFlags dstRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags dstRestoreAccessMask = 0; GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStageMask, dstRestoreAccessMask); Bool dstRestored = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstBinding.image, *dstBinding.trackedLayout, dstRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, dstBinding.aspectMask, dstBinding.mipLevel, dstBinding.mipLevelCount); MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__); } namespace { // GL hands CopyImageSubData ONE z/depth pair and lets the texture target decide what it // means. Vulkan splits that meaning across two different fields of VkImageCopy, chosen by // the image type: // // VK_IMAGE_TYPE_3D - slices live on the z axis: srcOffset.z/dstOffset.z select them and // extent.depth counts them. The subresource layer range must stay // (0, 1): Vulkan reads a 3D image as a single layer whose depth is // the mip level's depth (VUID-VkImageCopy-apiVersion-07932/-07933). // everything else - slices live in the array dimension: baseArrayLayer selects them and // layerCount counts them, while offset.z stays 0 and (when neither // endpoint is 3D) extent.depth stays 1. // // A mixed 2D-array <-> 3D pair is legal because maintenance1 - core since Vulkan 1.1 - // relaxed the old "layerCounts must match" rule into "the 3D side's extent.depth must // equal the array side's layerCount". struct CopyImageSliceMapping { // True for a VK_IMAGE_TYPE_3D image, i.e. slices ride the z axis, not the layer axis. Bool slicesAreDepth = false; // The GL z offset, kept in whichever field this endpoint's image type reads it from. Uint32 baseSlice = 0; // Slices this endpoint can address at the selected mip level; the copy range check // needs the level's depth for a 3D image (3D mips shrink in z) and the image's array // size for a layered one (array layers do not shrink). Uint32 availableSlices = 1; Uint32 BaseArrayLayer() const { return slicesAreDepth ? 0u : baseSlice; } Int32 OffsetZ() const { return slicesAreDepth ? static_cast(baseSlice) : 0; } }; // The Vulkan image one glCopyImageSubData endpoint names, after the two object kinds GL // 4.6 core 18.3.2 allows have been collapsed onto the fields this copy reads. A // renderbuffer is a single-level, single-layer 2D image, so its shape answers are // constants rather than a mip walk. `trackedLayout` points AT the owning resource's own // layout field - both resource maps are node-based, so the pointer survives the further // lookups the clear materialization below makes. struct CopyImageVkImage { Bool isRenderbuffer = false; VkImage image = VK_NULL_HANDLE; VkImageLayout* trackedLayout = nullptr; VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE; Uint32 mipLevels = 1; VkExtent2D extent = {0, 0}; Uint32 depth = 1; Uint32 arrayLayers = 1; // Both resources carry a format; this copy used to decline to read it, which is why a // four-row drift between the texture and renderbuffer format tables turned into // corrupted texels with nothing in the log. vkCmdCopyImage requires size-compatible // formats whenever they differ (VUID-vkCmdCopyImage-srcImage-01548) and there is no // downstream check - a mismatched pair is a promise the driver takes at face value. VkFormat format = VK_FORMAT_UNDEFINED; }; Bool TryResolveCopyImageSliceMapping(TextureTarget target, const CopyImageVkImage& image, Uint32 mipLevel, GLint glZ, GLsizei glDepth, CopyImageSliceMapping& outMapping) { if (glZ < 0 || glDepth <= 0) { return false; } const Uint32 baseSlice = static_cast(glZ); if (image.isRenderbuffer) { // A renderbuffer holds one 2D image and nothing else; GL still requires the // z/depth pair and it can only name that one slice. outMapping = {}; return baseSlice == 0 && glDepth == 1; } switch (target) { case TextureTarget::Texture1D: case TextureTarget::Texture2D: case TextureTarget::TextureRectangle: case TextureTarget::Texture2DMultisample: // Not layered at all: GL still requires the z/depth pair, and it can only name the // one slice these targets have. outMapping = {}; return baseSlice == 0 && glDepth == 1; case TextureTarget::Texture3D: outMapping.slicesAreDepth = true; outMapping.baseSlice = baseSlice; outMapping.availableSlices = std::max(1u, image.depth >> mipLevel); return true; case TextureTarget::Texture1DArray: case TextureTarget::Texture2DArray: case TextureTarget::Texture2DMultisampleArray: case TextureTarget::TextureCubeMap: case TextureTarget::TextureCubeMapArray: // A cube map is an array of six faces here (see TryResolveTextureShapeInfo), and GL // numbers its faces on the same z axis an array texture numbers its layers, so both // arrive as a plain layer range. // // GL_TEXTURE_1D_ARRAY belongs here too, and needs no remap: this backend STORES it // as a VK_IMAGE_TYPE_1D image whose layers live in arrayLayers (ToVulkanLevelExtent // moves the count across), and GL 4.6 core 18.3.2 ADDRESSES it as a stack of slices // on z with an image height of 1 - so the frontend's y/height are already the 0/1 // Vulkan requires and the layer lands in baseArrayLayer either way. outMapping.slicesAreDepth = false; outMapping.baseSlice = baseSlice; outMapping.availableSlices = image.arrayLayers; return true; default: // GL_TEXTURE_BUFFER has no image at all. Declined rather than mis-addressed. return false; } } Uint CopyImageEndpointName(const CopyImageEndpoint& endpoint) { #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return endpoint.IsRenderbuffer() ? endpoint.RenderbufferHandle.Slot : endpoint.TextureHandle.Slot; #endif if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetExternalIndex(); return endpoint.Texture ? endpoint.Texture->GetExternalIndex() : 0u; } } // namespace void VulkanRenderer::CopyImageSubData(const CopyImageEndpoint& srcEndpoint, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, const CopyImageEndpoint& dstEndpoint, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { #if MOBILEGL_BUILD_DISAGGREGATED const Bool wire = MG_Config::Transport != MG_Config::TransportMode::Monolith; MG_Pipe::MGPipeHandle dstStorageHandle = dstEndpoint.TextureHandle; if (wire && HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("copy-image-flush"); #endif MOBILEGL_ASSERT(srcEndpoint.Exists() && dstEndpoint.Exists(), "CopyImageSubData requires valid source and destination images."); // The frontend already declines a zero or negative extent, so anything else here is a // caller MobileGL wrote - but it still reaches vkCmdCopyImage in a release build, and a // zero extent.depth is as invalid as a zero width. if (srcWidth <= 0 || srcHeight <= 0 || srcDepth <= 0) { MGLOG_E_ONCE("%s: non-positive copy extent %dx%dx%d; declining the copy", __func__, srcWidth, srcHeight, srcDepth); return; } const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget); const auto dstTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(dstTarget); // Both endpoints of a same-image copy would have to share one VkImageLayout, so the // TRANSFER_SRC/TRANSFER_DST pair below cannot express it (it needs VK_IMAGE_LAYOUT_GENERAL // and an overlap check). Refused outright, and refused for real rather than through an // assertion the release build drops: recording the pair anyway is a validation error and, // on a tiler, a copy whose source has already been overwritten. // Compared by STORAGE, not by GL object: a texture view and the texture it views are two // different objects over one VkImage (ARB_texture_view), and GL 4.6 core 8.18 explicitly // permits copying between them - so an object-identity test would let exactly the case // this guard exists for through. const auto* srcStorageTexture = srcEndpoint.Texture ? &VkTextureManager::StorageTextureOf(*srcEndpoint.Texture) : nullptr; const auto* dstStorageTexture = dstEndpoint.Texture ? &VkTextureManager::StorageTextureOf(*dstEndpoint.Texture) : nullptr; if ( #if MOBILEGL_BUILD_DISAGGREGATED !wire && #endif srcStorageTexture == dstStorageTexture && srcEndpoint.Renderbuffer == dstEndpoint.Renderbuffer) { MGLOG_E_ONCE("%s: in-place copy on objectId=%u is not supported; declining the copy", __func__, CopyImageEndpointName(srcEndpoint)); return; } auto& frame = m_frameContext.GetCurrent(); if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); } if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } // One resolver for both object kinds. The texture arm is the same // SyncTextureAndGetDescriptor the copy always used; the renderbuffer arm goes through the // render-pass manager, which is where a renderbuffer's VkImage lives. const auto resolveImage = [this](const CopyImageEndpoint& endpoint, CopyImageVkImage& out) { #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { const Bool renderbuffer = endpoint.IsRenderbuffer(); // The same handle-keyed allocation used by wire FBO clear/draw/read. // The legacy render-pass manager owns a different, frontend-keyed map. auto* resource = m_textureManager->SyncTextureResourceByHandle( renderbuffer ? endpoint.RenderbufferHandle : endpoint.TextureHandle, renderbuffer); if (!resource) MagmaWireFatal("copy-image-resource"); out.isRenderbuffer = renderbuffer; out.image = resource->image; out.trackedLayout = &resource->layout; out.aspect = resource->aspect; out.mipLevels = resource->mipLevels; out.extent = resource->extent; out.depth = resource->depth; out.arrayLayers = resource->arrayLayers; out.format = resource->format; return out.image != VK_NULL_HANDLE; } #endif if (endpoint.IsRenderbuffer()) { auto* resource = m_renderPassManager->GetOrCreateRenderbufferResource(endpoint.Renderbuffer); if (resource == nullptr) return false; out.isRenderbuffer = true; out.image = resource->image; out.trackedLayout = &resource->layout; out.aspect = resource->aspect; out.mipLevels = 1; out.extent = resource->extent; out.depth = 1; out.arrayLayers = 1; out.format = resource->format; return out.image != VK_NULL_HANDLE; } // An endpoint that named nothing is the frontend validator's INVALID_VALUE and never // reaches here - but the assertion that says so is compiled out of a release build. if (endpoint.Texture == nullptr) return false; auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*endpoint.Texture); if (resource == nullptr) return false; out.isRenderbuffer = false; out.image = resource->image; out.trackedLayout = &resource->layout; out.aspect = resource->aspect; out.mipLevels = resource->mipLevels; out.extent = resource->extent; out.depth = resource->depth; out.arrayLayers = resource->arrayLayers; out.format = resource->format; return true; }; CopyImageVkImage srcImage{}; CopyImageVkImage dstImage{}; const Bool srcResolved = resolveImage(srcEndpoint, srcImage); const Bool dstResolved = resolveImage(dstEndpoint, dstImage); #if MOBILEGL_BUILD_DISAGGREGATED // P7 wave 2-B, CONTRACT-P7 ยง3.2: `copy-image-in-place@P7` RETIRES. // // What the Fatal here used to say was true of the code below it and of nothing else: the // TRANSFER_SRC/TRANSFER_DST pair cannot describe one image, because both endpoints share // one VkImageLayout and the second transition undoes the first. VK_IMAGE_LAYOUT_GENERAL // can describe it - it is a legal layout for both sides of vkCmdCopyImage - so a // same-image copy takes ONE transition of the whole image to GENERAL, GENERAL on both // sides of the command, and ONE restore. The monolith arm declines this shape outright // (a few lines up, `!wire &&`); the wire arm is now the one that performs it, which is // what ยง3.2 asks for and more than parity would give. // // `inPlace` is constexpr false in the pull build so that every branch it guards folds // away: this function is in the monolith image too and G1 pins that image's .text. const Bool inPlace = wire && srcImage.image != VK_NULL_HANDLE && srcImage.image == dstImage.image; if (wire) m_textureManager->FlushPendingUploads(); #else constexpr Bool inPlace = false; #endif // Real checks, not MOBILEGL_ASSERT: the assertions this replaces compile to nothing in // a release build, which is where both observed failures happened - a null resource // dereferenced right below (lavapipe) and a mip level the VkImage does not have handed // to vkCmdCopyImage (Adreno, SIGSEGV inside the driver). Neither is caught downstream: // an out-of-range subresource is a promise the driver takes at face value. // // _ONCE, because the severity is right but the repetition is not: MGLOG_E is the level // the project logs failures at and it IS live at the default MOBILEGL_LOG_ACTIVE_LEVEL, // so an application that reissues the same rejected copy every frame would otherwise // print at ERROR every frame. Once per site says the same thing and says it in a log // somebody can still read. // // The frontend validator (ValidateTextureLevelExists) is what produces the // GL_INVALID_VALUE the application is actually owed. This guard exists so the next gap // up there declines a copy instead of taking the process down. if (!srcResolved || !dstResolved) { MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__); return; } // Storage space from here down. srcImage/dstImage are the STORAGE textures' resources // (SyncTextureAndGetDescriptor resolves a view to the texture it views), while srcLevel / // dstLevel and the z origins below arrived relative to whichever name the application // passed - so a view's level 0 has to become the parent level it opened onto before it // can index a subresource, exactly as at every other attachment boundary. #if MOBILEGL_BUILD_DISAGGREGATED if (wire) { const auto mapSubresource = [this](const CopyImageEndpoint& endpoint, GLint& mip, GLint& layer) { if (endpoint.IsRenderbuffer()) { if (mip != 0 || layer != 0) MagmaWireFatal("copy-image-renderbuffer-subresource"); return endpoint.RenderbufferHandle; } Uint32 mappedMip = static_cast(mip), mappedLayer = static_cast(layer); const auto storage = m_textureManager->ResolveWireTextureStorage( endpoint.TextureHandle, mappedMip, mappedLayer); if (MG_Pipe::MGPipeHandleIsNull(storage)) MagmaWireFatal("copy-image-view-record"); mip = static_cast(mappedMip); layer = static_cast(mappedLayer); return storage; }; (void)mapSubresource(srcEndpoint, srcLevel, srcZ); dstStorageHandle = mapSubresource(dstEndpoint, dstLevel, dstZ); } #endif srcLevel = static_cast(ToStorageMipLevel(srcEndpoint.Texture.get(), srcLevel)); dstLevel = static_cast(ToStorageMipLevel(dstEndpoint.Texture.get(), dstLevel)); srcZ = static_cast(ToStorageArrayLayer(srcEndpoint.Texture.get(), srcZ)); dstZ = static_cast(ToStorageArrayLayer(dstEndpoint.Texture.get(), dstZ)); if (srcLevel < 0 || dstLevel < 0 || static_cast(srcLevel) >= srcImage.mipLevels || static_cast(dstLevel) >= dstImage.mipLevels) { MGLOG_E_ONCE("%s: mip level out of range (src %d of %u, dst %d of %u); declining the copy", __func__, srcLevel, srcImage.mipLevels, dstLevel, dstImage.mipLevels); return; } // Size compatibility, the guard whose absence let a table drift two files away reach the // driver as a promise. glCopyImageSubData is a raw texel-block move (GL 4.6 core 18.3.2), and // Vulkan says as much: when the two formats differ they must be size-compatible - the same // texel block size - or vkCmdCopyImage is undefined (VUID-vkCmdCopyImage-srcImage-01548). // Nothing else on this path asks: the three checks around it cover the mip range, the region // bounds and the slice range, and none of them ever looked at a format. // // A decline rather than a MOBILEGL_ASSERT, for the reason the neighbouring guards spell out: // assertions compile out of the release build that the CTS and shipping both run, which is // exactly where the corruption was observed. if (srcImage.format != dstImage.format) { // Size-compatibility is the COLOUR rule. Vulkan makes each depth/stencil format compatible // only with ITSELF, and the texel block sizes cannot tell them apart: X8_D24_UNORM_PACK32, // D32_SFLOAT and D24_UNORM_S8_UINT are all 4 bytes and all in different compatibility // classes, so a raw block-size test waves through exactly the pairs Vulkan forbids. The // frontend cannot filter them either - its own texel-block resolver is byte-size only, so // glCopyImageSubData between a GL_DEPTH_COMPONENT24 texture and a GL_DEPTH_COMPONENT32F // one reaches here with two different depth formats and 4 == 4. const Bool eitherIsDepthStencil = ((srcImage.aspect | dstImage.aspect) & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0; if (eitherIsDepthStencil) { MGLOG_E_ONCE("%s: depth/stencil formats are compatible only with themselves, and source format " "%d differs from destination format %d; declining the copy", __func__, static_cast(srcImage.format), static_cast(dstImage.format)); return; } const Uint32 srcBlockSize = vkuGetFormatInfo(srcImage.format).texel_block_size; const Uint32 dstBlockSize = vkuGetFormatInfo(dstImage.format).texel_block_size; if (srcBlockSize == 0 || dstBlockSize == 0 || srcBlockSize != dstBlockSize) { MGLOG_E_ONCE("%s: source format %d and destination format %d are not size-compatible " "(%u vs %u bytes per texel block); declining the copy", __func__, static_cast(srcImage.format), static_cast(dstImage.format), srcBlockSize, dstBlockSize); return; } } const VkImageAspectFlags copyAspectMask = srcImage.aspect & dstImage.aspect & (VK_IMAGE_ASPECT_COLOR_BIT | VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT); MOBILEGL_ASSERT(copyAspectMask != 0 && (srcImage.aspect & copyAspectMask) == srcImage.aspect && (dstImage.aspect & copyAspectMask) == dstImage.aspect, "CopyImageSubData source and destination aspects are incompatible."); const Uint32 srcMipLevel = static_cast(srcLevel); const Uint32 dstMipLevel = static_cast(dstLevel); const Uint32 srcMipWidth = std::max(1u, srcImage.extent.width >> srcMipLevel); const Uint32 srcMipHeight = std::max(1u, srcImage.extent.height >> srcMipLevel); const Uint32 dstMipWidth = std::max(1u, dstImage.extent.width >> dstMipLevel); const Uint32 dstMipHeight = std::max(1u, dstImage.extent.height >> dstMipLevel); // Promoted for the same reason as the level range above, and it is the same bug class: // a VkImageCopy whose region runs past the image is an out-of-bounds promise to the // driver, and the frontend does not check the region at all (there is a CTS sibling, // copy_image.exceeding_boundaries, that asks for exactly this input). Nothing legal is // lost by declining - a copy that reads or writes outside the image was never going to // produce a correct result, it was going to produce whatever the driver did next. if (srcX < 0 || srcY < 0 || dstX < 0 || dstY < 0 || static_cast(srcX + srcWidth) > srcMipWidth || static_cast(srcY + srcHeight) > srcMipHeight || static_cast(dstX + srcWidth) > dstMipWidth || static_cast(dstY + srcHeight) > dstMipHeight) { MGLOG_E_ONCE("%s: region outside image bounds (src %dx%d+%d+%d of %ux%u, dst +%d+%d of %ux%u); " "declining the copy", __func__, srcWidth, srcHeight, srcX, srcY, srcMipWidth, srcMipHeight, dstX, dstY, dstMipWidth, dstMipHeight); return; } // The supported envelope, replacing the "GL_TEXTURE_2D only" assertion that used to stand // here: every target whose slices this function can address on one of the two Vulkan axes. // A refusal has to be a real decline, not an assertion - the assertion compiled to nothing // in a release build and the unsupported shape reached vkCmdCopyImage anyway. CopyImageSliceMapping srcSlices; CopyImageSliceMapping dstSlices; if (!TryResolveCopyImageSliceMapping(srcTextureTarget, srcImage, srcMipLevel, srcZ, srcDepth, srcSlices) || !TryResolveCopyImageSliceMapping(dstTextureTarget, dstImage, dstMipLevel, dstZ, srcDepth, dstSlices)) { MGLOG_E_ONCE("%s: unsupported target pair src=%s dst=%s (srcZ=%d dstZ=%d depth=%d); declining the copy", __func__, MG_Util::ConvertTextureTargetToString(srcTextureTarget).c_str(), MG_Util::ConvertTextureTargetToString(dstTextureTarget).c_str(), srcZ, dstZ, srcDepth); return; } // The slice half of the region-bounds guard above. A layered endpoint's bound is NOT the // mip-0 2D extent: an array texture is bounded by its layer count (which no mip level // shrinks) and a 3D texture by the selected level's depth (which every level halves), so // both come from the endpoint that resolved them. const Uint32 copySliceCount = static_cast(srcDepth); if (srcSlices.baseSlice + copySliceCount > srcSlices.availableSlices || dstSlices.baseSlice + copySliceCount > dstSlices.availableSlices) { MGLOG_E_ONCE("%s: slice range outside image bounds (srcZ=%d of %u, dstZ=%d of %u, depth=%d); " "declining the copy", __func__, srcZ, srcSlices.availableSlices, dstZ, dstSlices.availableSlices, srcDepth); return; } #if MOBILEGL_BUILD_DISAGGREGATED // THE ONE IN-PLACE SHAPE THAT STAYS A DECLINE (ยง3.2). GL 4.6 core 18.3.2: if the source // and destination name the same image AND the same level AND the same layers, the result // is UNDEFINED where the regions overlap. GENERAL makes the command legal to record, not // meaningful to execute - vkCmdCopyImage has no ordering between its own reads and writes // within one region, so what a tiler produces is whichever half of each texel it reached // first. Declining says that, once, instead of shipping it. // // The test is ALL THREE dimensions at once: same level, overlapping slice range, and // overlapping rectangle. A copy from level 0 to level 1, or between disjoint layers, or // between disjoint rectangles of one level, is perfectly defined and is performed. if (inPlace && srcMipLevel == dstMipLevel) { const Uint32 srcSliceBegin = srcSlices.baseSlice, dstSliceBegin = dstSlices.baseSlice; const Bool slicesOverlap = srcSliceBegin < dstSliceBegin + copySliceCount && dstSliceBegin < srcSliceBegin + copySliceCount; const Bool rectOverlaps = srcX < dstX + srcWidth && dstX < srcX + srcWidth && srcY < dstY + srcHeight && dstY < srcY + srcHeight; if (slicesOverlap && rectOverlaps) { MGLOG_E_ONCE("%s: in-place copy on objectId=%u overlaps itself at level %u " "(src %dx%d+%d+%d slice %u, dst +%d+%d slice %u, %u slice(s)); GL leaves " "the result undefined, so the copy is declined rather than recorded", __func__, CopyImageEndpointName(srcEndpoint), srcMipLevel, srcWidth, srcHeight, srcX, srcY, srcSliceBegin, dstX, dstY, dstSliceBegin, copySliceCount); return; } } #endif const auto materializeClear = [this, &frame](const CopyImageEndpoint& endpoint) { #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return true; #endif if (endpoint.IsRenderbuffer()) { return MaterializePendingClearForRenderbuffer(frame.commandBuffer, endpoint.Renderbuffer); } return MaterializePendingClearForTexture(frame.commandBuffer, *endpoint.Texture); }; const Bool clearReady = materializeClear(srcEndpoint); MOBILEGL_ASSERT(clearReady, "%s: failed to materialize pending clear for source objectId=%u", __func__, CopyImageEndpointName(srcEndpoint)); // A clear still parked on the destination would otherwise materialize AFTER this copy and // wipe the texels it just wrote. const Bool dstClearReady = materializeClear(dstEndpoint); MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination objectId=%u", __func__, CopyImageEndpointName(dstEndpoint)); const VkImageLayout srcOriginalLayout = *srcImage.trackedLayout; const VkImageLayout dstOriginalLayout = *dstImage.trackedLayout; // A layout of UNDEFINED means nothing has ever been written to the image, which on the // SOURCE side is glTexStorage without an upload: legal GL, and the texels it copies are // undefined by the same spec sentence that lets the application ask. Both sides therefore // take the same shape - transition the whole image out of UNDEFINED and settle it on a // real layout afterwards, since UNDEFINED is not a layout a barrier may transition BACK to. // A renderbuffer settles on its ATTACHMENT layout instead: it is never sampled, and that is // the layout MaterializePendingClearForRenderbuffer leaves it in. const auto resolveRestoreLayout = [copyAspectMask](VkImageLayout originalLayout, Bool isRenderbuffer) { if (originalLayout != VK_IMAGE_LAYOUT_UNDEFINED) { return originalLayout; } const Bool depthStencil = (copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0; if (isRenderbuffer) { return depthStencil ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; } return depthStencil ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; }; const VkImageLayout srcRestoreLayout = resolveRestoreLayout(srcOriginalLayout, srcImage.isRenderbuffer); const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout, dstImage.isRenderbuffer); // ONE IMAGE, ONE LAYOUT, ONE TRANSITION. For a same-image copy both endpoints are the same // VkImage behind the same tracked layout, so the two transitions below would describe one // subresource twice and the second would undo the first. GENERAL is the layout both sides // of vkCmdCopyImage accept at once, the barrier covers the WHOLE image (both the source // level and the destination level are in it), and the access mask carries both directions // because this one barrier is the edge for both. const VkImageLayout copySourceLayout = inPlace ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; const VkImageLayout copyDestinationLayout = inPlace ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; const VkAccessFlags copySourceAccess = inPlace ? (VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT) : VK_ACCESS_TRANSFER_READ_BIT; VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; GetImageTransitionSourceState(srcOriginalLayout, srcStageMask, srcAccessMask); VkImageLayout srcCopyLayout = srcOriginalLayout; // The barriers below name a MIP range only. Their layer range is not a parameter: // TransitionImageLayout always covers every layer of the image, which is a superset of the // [baseSlice, baseSlice + depth) the slice mapping above hands the copy. if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { Bool srcReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcImage.image, *srcImage.trackedLayout, copySourceLayout, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, copySourceAccess, srcImage.aspect, 0, srcImage.mipLevels); MOBILEGL_ASSERT(srcReady, "%s: failed to transition undefined source image", __func__); srcCopyLayout = *srcImage.trackedLayout; } else { Bool srcReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcImage.image, srcCopyLayout, copySourceLayout, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, copySourceAccess, inPlace ? srcImage.aspect : copyAspectMask, inPlace ? 0u : srcMipLevel, inPlace ? srcImage.mipLevels : 1u); MOBILEGL_ASSERT(srcReady, "%s: failed to transition source image", __func__); } VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags dstAccessMask = 0; GetImageTransitionSourceState(dstOriginalLayout, dstStageMask, dstAccessMask); VkImageLayout dstCopyLayout = dstOriginalLayout; if (inPlace) { // Already moved, by the barrier above: trackedLayout is the same field. dstCopyLayout = *dstImage.trackedLayout; } else if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { Bool dstReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstImage.image, *dstImage.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstImage.aspect, 0, dstImage.mipLevels); MOBILEGL_ASSERT(dstReady, "%s: failed to transition undefined destination image", __func__); dstCopyLayout = *dstImage.trackedLayout; } else { Bool dstReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstImage.image, dstCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, copyAspectMask, dstMipLevel, 1); MOBILEGL_ASSERT(dstReady, "%s: failed to transition destination image", __func__); } // The GL slice count reaches Vulkan on the layer axis of whichever endpoint is NOT 3D, and // on extent.depth as soon as either endpoint IS: a 3D image's subresource is always the // single layer (0, 1) and its slices are counted by the depth of the copy extent. With two // non-3D endpoints both layer counts carry it and extent.depth stays 1. const Bool copyCrossesDepthAxis = srcSlices.slicesAreDepth || dstSlices.slicesAreDepth; VkImageCopy copyRegion{}; copyRegion.srcSubresource.aspectMask = copyAspectMask; copyRegion.srcSubresource.mipLevel = srcMipLevel; copyRegion.srcSubresource.baseArrayLayer = srcSlices.BaseArrayLayer(); copyRegion.srcSubresource.layerCount = srcSlices.slicesAreDepth ? 1u : copySliceCount; copyRegion.srcOffset = {srcX, srcY, srcSlices.OffsetZ()}; copyRegion.dstSubresource.aspectMask = copyAspectMask; copyRegion.dstSubresource.mipLevel = dstMipLevel; copyRegion.dstSubresource.baseArrayLayer = dstSlices.BaseArrayLayer(); copyRegion.dstSubresource.layerCount = dstSlices.slicesAreDepth ? 1u : copySliceCount; copyRegion.dstOffset = {dstX, dstY, dstSlices.OffsetZ()}; copyRegion.extent = {static_cast(srcWidth), static_cast(srcHeight), copyCrossesDepthAxis ? copySliceCount : 1u}; MGLOG_D("CopyImageSubData: src(target=%s level=%u layer=%u+%u z=%d) -> dst(target=%s level=%u layer=%u+%u " "z=%d) extent=[%d x %d x %u]", MG_Util::ConvertTextureTargetToString(srcTextureTarget).c_str(), srcMipLevel, copyRegion.srcSubresource.baseArrayLayer, copyRegion.srcSubresource.layerCount, copyRegion.srcOffset.z, MG_Util::ConvertTextureTargetToString(dstTextureTarget).c_str(), dstMipLevel, copyRegion.dstSubresource.baseArrayLayer, copyRegion.dstSubresource.layerCount, copyRegion.dstOffset.z, srcWidth, srcHeight, copyRegion.extent.depth); vkCmdCopyImage(frame.commandBuffer, srcImage.image, copySourceLayout, dstImage.image, copyDestinationLayout, 1, ©Region); #if MOBILEGL_BUILD_DISAGGREGATED // The copy coordinates above are already storage-relative. Name that // storage here too, so a texture view's offsets are not applied twice. if (wire && !dstImage.isRenderbuffer) m_textureManager->MarkWireTextureGpuWritten(dstStorageHandle, dstMipLevel, dstSlices.BaseArrayLayer(), dstSlices.slicesAreDepth ? 1u : copySliceCount); #endif VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcRestoreAccessMask = 0; GetImageTransitionDestinationState(srcRestoreLayout, srcRestoreStageMask, srcRestoreAccessMask); if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { Bool srcRestored = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcImage.image, *srcImage.trackedLayout, srcRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask, copySourceAccess, srcRestoreAccessMask, srcImage.aspect, 0, srcImage.mipLevels); MOBILEGL_ASSERT(srcRestored, "%s: failed to restore undefined source image layout", __func__); } else { Bool srcRestored = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcImage.image, srcCopyLayout, srcRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask, copySourceAccess, srcRestoreAccessMask, inPlace ? srcImage.aspect : copyAspectMask, inPlace ? 0u : srcMipLevel, inPlace ? srcImage.mipLevels : 1u); MOBILEGL_ASSERT(srcRestored, "%s: failed to restore source image layout", __func__); } VkPipelineStageFlags dstRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags dstRestoreAccessMask = 0; GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStageMask, dstRestoreAccessMask); if (inPlace) { // The restore above already moved the whole image off GENERAL, and the destination // is that same image: a second restore would barrier a layout the tracker has // already left, which is the mirror of the bug the retired Fatal was guarding. } else if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { Bool dstRestored = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstImage.image, *dstImage.trackedLayout, dstRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, dstImage.aspect, 0, dstImage.mipLevels); MOBILEGL_ASSERT(dstRestored, "%s: failed to restore undefined destination image layout", __func__); } else { Bool dstRestored = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstImage.image, dstCopyLayout, dstRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1); MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__); } } Bool VulkanRenderer::FinishPendingGpuWork() { MakeXfbWritesVisible(); auto& frame = m_frameContext.GetCurrent(); if (!frame.isCommandRecording) { return true; } if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } return SubmitReadbackCommandsAndWait(frame); } Bool VulkanRenderer::SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame) { if (frame.isCommandRecording) { m_frameContext.EndCommandRecording(); frame.hasCommandBufferRecorded = true; InvalidatePipelineMemo(); // command-buffer boundary: drop the pipeline memo } // The pre-pass stream must never be submitted later than the recording // it was paired with (frame commands recorded after a pre-pass move // rely on the moved work having executed first). m_frameContext.EndPreCommandRecordingIfOpen(); if (!frame.hasCommandBufferRecorded && !frame.hasPreCommandBufferRecorded) { return true; } if (!SubmitPendingCommandBuffer(frame, frame.imageInFlightFence, /*pooledFence=*/false)) { return false; } // EVERY OUTSTANDING SUBMISSION, NOT ONLY THE ONE JUST MADE (P7 wave 2-B, exit gate 3). // // This used to wait one fence and then call OnSubmitsCompletedUpTo(frame.lastSubmitIndex), // and the comment below said the wait "proved every submission complete". On the monolith // arm that is true - nothing flushes before a readback, so the frame's draws and the copy // are one submission under one fence. On the WIRE arm it is not: ReadWirePixels calls // FlushPendingCommands first, which submits the draws under a POOLED FENCE OF ITS OWN and // returns without waiting, so the copy is a later, separately fenced submission. Two // submissions on one queue start in order; they do not finish in order unless something // makes them, and a fence says nothing about a submission it does not belong to. // // Measured, on this host, at the copy: submit=72 completed=71. The old code then called // OnSubmitsCompletedUpTo(73), which RECORDED 72 as complete and retired its fence - so // after the fact nothing could even observe that it had not been waited for. // // The cost is nothing a readback does not already pay: it is a full CPU stall by // construction, and the fences gathered here are exactly the ones already in flight. // // UNDER #if, although this function serves both arms of the disaggregated build: the PULL // build has no wire arm, cannot flush before a readback, and is the image G1 pins to // 0xa52203. Its single-fence wait stays the statement it always was. #if MOBILEGL_BUILD_DISAGGREGATED Vector readbackFences; readbackFences.reserve(m_inFlightSubmits.size() + 1); for (const auto& record : m_inFlightSubmits) { if (record.fence != VK_NULL_HANDLE && record.submitIndex <= frame.lastSubmitIndex) { readbackFences.push_back(record.fence); } } if (readbackFences.empty()) { readbackFences.push_back(frame.imageInFlightFence); } // Measured here on the host (OpenRA, DirectVulkan, inproc, at the frame readback): // fences=2 upTo=73 submitCounter=73 completed=71. Two fences, because submission 72 - // the frame's draws, flushed by ReadWirePixels under a pooled fence - was still // outstanding; the code this replaces waited on one. VkResult result = vkWaitForFences(m_device, static_cast(readbackFences.size()), readbackFences.data(), VK_TRUE, UINT64_MAX); #else VkResult result = vkWaitForFences(m_device, 1, &frame.imageInFlightFence, VK_TRUE, UINT64_MAX); #endif if (result != VK_SUCCESS) { MGLOG_E_ONCE("DirectVulkan readback: vkWaitForFences returned %d", result); return false; } OnSubmitsCompletedUpTo(frame.lastSubmitIndex); result = vkResetFences(m_device, 1, &frame.imageInFlightFence); if (result != VK_SUCCESS) { MGLOG_E_ONCE("DirectVulkan readback: vkResetFences returned %d", result); return false; } frame.hasCommandBufferRecorded = false; frame.isCommandRecording = false; // The wait above covered every submission at or below this one, so the full // frame-boundary drain applies: descriptor cursors, transient arenas, deferred // texture/buffer releases, retired command buffers and the converted // vertex-stream cache all rewind here, keeping present-less readback // loops bounded (Present is the only other drain point). TryDrainFrameTransients(); return true; } void VulkanRenderer::ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) { #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { ReadWirePixels(x,y,width,height,format,type,pixels); return; } #endif if (width <= 0 || height <= 0) { return; } auto readFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(); if (readFbo == nullptr) { MGLOG_E_ONCE("DirectVulkan::ReadPixels skipped: no read framebuffer is bound"); return; } if (format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL || format == GL_STENCIL_INDEX) { ReadDepthStencilPixels(*readFbo, x, y, width, height, format, type, pixels); return; } auto& frame = m_frameContext.GetCurrent(); if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); } if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } const Bool readIsDefaultFbo = readFbo->IsDefaultFramebuffer(); // Materialize any pending clear on the read-buffer attachment BEFORE resolving the // blit binding below: for a renderbuffer/texture that has never been part of any // render pass yet (e.g. a GL_NONE draw buffer slot whose attachment is only ever // touched via an explicit glReadBuffer), materializing lazily creates its backing // Vulkan resource for the first time. UnorderedMap is open-addressing and may // rehash on that insertion, invalidating any RenderbufferResource*/TextureResource* // obtained beforehand - so ResolveColorBlitBinding's cached `trackedLayout` pointer // must be taken AFTER this, never before it. // // The default framebuffer needs this just as much, and used to be excluded: its clear is // parked the same way, and with no draw between the clear and the readback no render // pass ever runs to fold it in, so the readback returned the previous frame's image // (KHR-GL40.draw_indirect.negative-*). It only takes a different materializer because the // image to clear is the acquired swapchain image, not the attachment's placeholder // texture. if (readIsDefaultFbo) { const Bool clearReady = MaterializePendingClearForDefaultFramebuffer(frame.commandBuffer, *readFbo, readFbo->GetReadBuffer()); MOBILEGL_ASSERT(clearReady, "ReadPixels: failed to materialize the default framebuffer's pending clear"); } else { const auto& sourceAttachment = readFbo->GetAttachment(readFbo->GetReadBuffer()); auto sourceTexture = sourceAttachment.GetTexture(); if (sourceTexture != nullptr) { const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *sourceTexture); MOBILEGL_ASSERT(clearReady, "ReadPixels: failed to materialize pending clear for source textureId=%d", sourceTexture->GetExternalIndex()); } else if (sourceAttachment.IsRenderbuffer()) { const Bool clearReady = MaterializePendingClearForRenderbuffer(frame.commandBuffer, sourceAttachment.GetRenderbuffer()); MOBILEGL_ASSERT(clearReady, "ReadPixels: failed to materialize pending clear for source renderbuffer %u", sourceAttachment.GetRenderbuffer()->GetExternalIndex()); } } BlitImageBinding srcBinding{}; // A read resolves the image the app last rendered into, not the one Present() has // already acquired for the frame that is starting (see m_defaultFramebufferImageIndex). // A retrace snapshot taken just before a swap is applied after it, and must still find // the colour the app drew. const Uint32 srcImageIndex = DefaultFramebufferReadIndex(readIsDefaultFbo); if (readIsDefaultFbo && !m_swapchainObject.IsImageContentDefined(srcImageIndex)) { // Named rather than silent: the readback hands back whatever the image holds. The // ordinary cause is a read of the default framebuffer applied after the Present that // consumed it, where the app's own colour is still what is expected. MGLOG_W_ONCE("DirectVulkan::ReadPixels: no write is recorded for swapchain image %u of the " "default framebuffer; the readback returns its raw pixels", srcImageIndex); } if (!ResolveColorBlitBinding(*readFbo, true, srcImageIndex, m_swapchainObject, *m_textureManager, *m_renderPassManager, srcBinding)) { return; } const VkImageLayout srcOriginalLayout = readIsDefaultFbo ? m_swapchainObject.GetImageLayout(srcImageIndex) : *srcBinding.trackedLayout; if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { MGLOG_E_ONCE("DirectVulkan::ReadPixels skipped: source image layout is undefined"); return; } const VkFormat srcFormat = srcBinding.format; const SizeT sourceTexelSize = GetReadbackTexelSize(srcFormat); if (sourceTexelSize == 0) { MGLOG_E_ONCE("DirectVulkan::ReadPixels skipped: unsupported source format=%d", static_cast(srcFormat)); return; } const VkDeviceSize readbackSize = static_cast(width) * static_cast(height) * sourceTexelSize; VkBufferObject readback; if (!readback.Create({ .allocator = m_allocator, .size = readbackSize, .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memoryUsage = VMA_MEMORY_USAGE_AUTO, .allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT, })) { MGLOG_E_ONCE("DirectVulkan::ReadPixels skipped: failed to create readback buffer"); return; } VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; GetImageTransitionSourceState(srcOriginalLayout, srcStageMask, srcAccessMask); // The copy below reads `srcBinding.baseArrayLayer`, which for a glFramebufferTextureLayer // attachment is any layer of the array - the barrier covers all of them (see // VkTextureManager::TransitionImageLayout), so the layer being read is one it moved. if (readIsDefaultFbo) { VkImageLayout trackedLayout = srcOriginalLayout; Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, srcBinding.aspectMask); MOBILEGL_ASSERT(ok, "%s: failed to transition swapchain source image", __func__); m_swapchainObject.SetImageLayout(srcImageIndex, trackedLayout); } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, *srcBinding.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, srcBinding.aspectMask, srcBinding.mipLevel, 1); MOBILEGL_ASSERT(ok, "%s: failed to transition source image", __func__); } VkBufferImageCopy copyRegion{}; copyRegion.imageSubresource.aspectMask = srcBinding.aspectMask; copyRegion.imageSubresource.mipLevel = srcBinding.mipLevel; copyRegion.imageSubresource.baseArrayLayer = srcBinding.baseArrayLayer; copyRegion.imageSubresource.layerCount = 1; // The GL rect, aimed at the default framebuffer's stored orientation. Using the GL y // verbatim copied rows [y, y+h) counted from the TOP of the image, i.e. the wrong band for // every read that was not full-height. VkOffset2D copyOffset{x, y}; VkExtent2D copyExtent{static_cast(width), static_cast(height)}; if (readIsDefaultFbo) { const VkExtent2D defaultFboExtent = m_swapchainObject.GetExtent(); const Bool mapped = MapDefaultFramebufferReadbackRect( x, y, width, height, defaultFboExtent, m_swapchainObject.GetPreTransform(), ©Offset, ©Extent); MOBILEGL_ASSERT(mapped, "ReadPixels: default framebuffer read rectangle is out of bounds"); if (!mapped) return; } copyRegion.imageOffset = {copyOffset.x, copyOffset.y, static_cast(srcBinding.depthOffset)}; copyRegion.imageExtent = {copyExtent.width, copyExtent.height, 1}; vkCmdCopyImageToBuffer(frame.commandBuffer, srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(), 1, ©Region); VkPipelineStageFlags restoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags restoreAccessMask = 0; GetImageTransitionDestinationState(srcOriginalLayout, restoreStageMask, restoreAccessMask); if (readIsDefaultFbo) { VkImageLayout trackedLayout = m_swapchainObject.GetImageLayout(srcImageIndex); Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, trackedLayout, srcOriginalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, restoreStageMask, VK_ACCESS_TRANSFER_READ_BIT, restoreAccessMask, srcBinding.aspectMask); MOBILEGL_ASSERT(ok, "%s: failed to restore swapchain source image layout", __func__); m_swapchainObject.SetImageLayout(srcImageIndex, trackedLayout); } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, *srcBinding.trackedLayout, srcOriginalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, restoreStageMask, VK_ACCESS_TRANSFER_READ_BIT, restoreAccessMask, srcBinding.aspectMask, srcBinding.mipLevel, 1); MOBILEGL_ASSERT(ok, "%s: failed to restore source image layout", __func__); } if (!SubmitReadbackCommandsAndWait(frame)) { return; } const auto* mapped = static_cast(readback.Map()); if (mapped == nullptr) { MGLOG_E_ONCE("DirectVulkan::ReadPixels skipped: failed to map readback buffer"); return; } if (!readback.Invalidate(readbackSize)) { MGLOG_E_ONCE("DirectVulkan::ReadPixels skipped: failed to invalidate readback buffer"); return; } if (readIsDefaultFbo) { const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform(); // No full-extent gate any more: the remap works on the copied rect, and the copy was // already aimed with the same mapping. The gate is exactly what made every partial // read of the default framebuffer come back in Vulkan row order. Vector remapped(static_cast(width) * static_cast(height) * sourceTexelSize); if (RemapDefaultFramebufferReadback(mapped, static_cast(width), static_cast(height), preTransform, sourceTexelSize, remapped.data())) { PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, 1, format, type, pixels, /*applyPackImageParams=*/false, /*applyReadColorClamp=*/true); return; } MGLOG_D("DirectVulkan::ReadPixels: default-FBO remap failed (w=%d h=%d preTransform=%d); falling back " "to raw readback", width, height, static_cast(preTransform)); } PackReadbackToClientOrPbo(mapped, srcFormat, width, height, 1, format, type, pixels, /*applyPackImageParams=*/false, /*applyReadColorClamp=*/true); } Bool VulkanRenderer::BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat, VkImageLayout* srcTrackedLayout, Uint32 srcMipLevel, Uint32 srcBaseArrayLayer, VkImage dstImage, VkFormat dstFormat, VkImageLayout* dstTrackedLayout, Uint32 dstMipLevel, Uint32 dstBaseArrayLayer, GLint srcX, GLint srcY, GLint dstX, GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout, VkImageLayout dstRestoreLayout, Bool stencilAspect) { const auto aspectMaskForFormat = [](VkFormat format) -> VkImageAspectFlags { switch (format) { case VK_FORMAT_D16_UNORM: case VK_FORMAT_X8_D24_UNORM_PACK32: case VK_FORMAT_D32_SFLOAT: return VK_IMAGE_ASPECT_DEPTH_BIT; case VK_FORMAT_D24_UNORM_S8_UINT: case VK_FORMAT_D32_SFLOAT_S8_UINT: return VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; default: return VK_IMAGE_ASPECT_COLOR_BIT; } }; const auto depthTexelSize = [](VkFormat format) -> SizeT { switch (format) { case VK_FORMAT_D16_UNORM: return 2; case VK_FORMAT_X8_D24_UNORM_PACK32: case VK_FORMAT_D24_UNORM_S8_UINT: case VK_FORMAT_D32_SFLOAT: case VK_FORMAT_D32_SFLOAT_S8_UINT: return 4; default: return 0; } }; // The stencil aspect of every supported format copies as one byte per texel, // so a cross-format stencil "blit" is a raw pass-through. const SizeT srcTexel = stencilAspect ? 1 : depthTexelSize(srcFormat); const SizeT dstTexel = stencilAspect ? 1 : depthTexelSize(dstFormat); if (srcTexel == 0 || dstTexel == 0 || width <= 0 || height <= 0) { MGLOG_E_ONCE("BlitDepthAcrossFormats skipped: unsupported formats src=%d dst=%d", static_cast(srcFormat), static_cast(dstFormat)); return false; } const SizeT pixelCount = static_cast(width) * static_cast(height); VkBufferObject readback; if (!readback.Create({ .allocator = m_allocator, .size = pixelCount * srcTexel, .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memoryUsage = VMA_MEMORY_USAGE_AUTO, .allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT, })) { return false; } VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; GetImageTransitionSourceState(*srcTrackedLayout, srcStageMask, srcAccessMask); Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcImage, *srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, aspectMaskForFormat(srcFormat), srcMipLevel, 1); MOBILEGL_ASSERT(ok, "BlitDepthAcrossFormats: source transition failed"); VkBufferImageCopy readRegion{}; readRegion.imageSubresource.aspectMask = stencilAspect ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT; readRegion.imageSubresource.mipLevel = srcMipLevel; readRegion.imageSubresource.baseArrayLayer = srcBaseArrayLayer; readRegion.imageSubresource.layerCount = 1; readRegion.imageOffset = {srcX, srcY, 0}; readRegion.imageExtent = {static_cast(width), static_cast(height), 1}; vkCmdCopyImageToBuffer(frame.commandBuffer, srcImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(), 1, &readRegion); VkPipelineStageFlags srcRestoreStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcRestoreAccess = 0; GetImageTransitionDestinationState(srcRestoreLayout, srcRestoreStage, srcRestoreAccess); ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcImage, *srcTrackedLayout, srcRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStage, VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccess, aspectMaskForFormat(srcFormat), srcMipLevel, 1); MOBILEGL_ASSERT(ok, "BlitDepthAcrossFormats: source restore failed"); if (!SubmitReadbackCommandsAndWait(frame)) { return false; } const auto* mapped = static_cast(readback.Map()); if (mapped == nullptr || !readback.Invalidate(pixelCount * srcTexel)) { return false; } // Decode source depths to float, re-encode into the destination texel layout. Vector encoded(pixelCount * dstTexel); if (stencilAspect) { Memcpy(encoded.data(), mapped, pixelCount); } for (SizeT i = 0; !stencilAspect && i < pixelCount; ++i) { Float depthValue = 0.0f; switch (srcFormat) { case VK_FORMAT_D16_UNORM: { Uint16 raw = 0; Memcpy(&raw, mapped + i * 2, sizeof(raw)); depthValue = static_cast(raw) / 65535.0f; break; } case VK_FORMAT_X8_D24_UNORM_PACK32: case VK_FORMAT_D24_UNORM_S8_UINT: { Uint32 raw = 0; Memcpy(&raw, mapped + i * 4, sizeof(raw)); depthValue = static_cast(raw & 0xFFFFFFu) / static_cast(0xFFFFFFu); break; } default: { Memcpy(&depthValue, mapped + i * 4, sizeof(depthValue)); break; } } Uint8* dst = encoded.data() + i * dstTexel; switch (dstFormat) { case VK_FORMAT_D16_UNORM: { const Uint16 value = static_cast(std::lround(static_cast(std::clamp(depthValue, 0.0f, 1.0f)) * 65535.0)); Memcpy(dst, &value, sizeof(value)); break; } case VK_FORMAT_X8_D24_UNORM_PACK32: case VK_FORMAT_D24_UNORM_S8_UINT: { const Uint32 value = static_cast( std::lround(static_cast(std::clamp(depthValue, 0.0f, 1.0f)) * 16777215.0)); Memcpy(dst, &value, sizeof(value)); break; } default: Memcpy(dst, &depthValue, sizeof(depthValue)); break; } } // Upload the converted region; recording restarted after the readback flush. if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); } BufferSlice slice{}; if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), encoded.data(), encoded.size(), 4, slice)) { MGLOG_E_ONCE("BlitDepthAcrossFormats: staging upload failed"); return false; } VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags dstAccessMask = 0; GetImageTransitionSourceState(*dstTrackedLayout, dstStageMask, dstAccessMask); ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstImage, *dstTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, aspectMaskForFormat(dstFormat), dstMipLevel, 1); MOBILEGL_ASSERT(ok, "BlitDepthAcrossFormats: destination transition failed"); VkBufferImageCopy writeRegion{}; writeRegion.bufferOffset = slice.offset; writeRegion.imageSubresource.aspectMask = stencilAspect ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT; writeRegion.imageSubresource.mipLevel = dstMipLevel; writeRegion.imageSubresource.baseArrayLayer = dstBaseArrayLayer; writeRegion.imageSubresource.layerCount = 1; writeRegion.imageOffset = {dstX, dstY, 0}; writeRegion.imageExtent = {static_cast(width), static_cast(height), 1}; vkCmdCopyBufferToImage(frame.commandBuffer, slice.buffer, dstImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &writeRegion); VkPipelineStageFlags dstRestoreStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags dstRestoreAccess = 0; GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStage, dstRestoreAccess); ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstImage, *dstTrackedLayout, dstRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStage, VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccess, aspectMaskForFormat(dstFormat), dstMipLevel, 1); MOBILEGL_ASSERT(ok, "BlitDepthAcrossFormats: destination restore failed"); return true; } void VulkanRenderer::ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) { if (width <= 0 || height <= 0) { return; } const Bool wantDepth = format != GL_STENCIL_INDEX; const Bool wantStencil = format != GL_DEPTH_COMPONENT; // GL_DEPTH_STENCIL requires both halves; the state layer already rejected // framebuffers lacking either, so resolving via the depth attachment is enough. const auto attachmentType = wantDepth ? MobileGL::FramebufferAttachmentType::Depth : MobileGL::FramebufferAttachmentType::Stencil; const Bool readIsDefaultFbo = readFbo.IsDefaultFramebuffer(); if (!readIsDefaultFbo) { const auto& attachment = readFbo.GetAttachment(attachmentType); if (!attachment.IsValid() || attachment.IsEmpty()) { MGLOG_E_ONCE("DirectVulkan::ReadDepthStencilPixels skipped: no depth/stencil attachment image"); return; } } auto& frame = m_frameContext.GetCurrent(); if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); } if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } // The default framebuffer's depth/stencil lives in the swapchain, not in an // attachment object: its placeholder ITextureObject describes the format but backs no // image, so the branches below would have synced (and read back) an unrelated one. // Declining outright is what made every glReadPixels(GL_DEPTH_COMPONENT/ // GL_STENCIL_INDEX) of the default framebuffer leave the caller's buffer untouched - // the whole KHR-GL*.framebuffer_blit family checks exactly that before it blits. if (readIsDefaultFbo) { // A read names the image the app last rendered into, not the one Present() has // already acquired (see m_defaultFramebufferImageIndex). const Uint32 srcImageIndex = DefaultFramebufferReadIndex(true); const VkImage swapchainDepthImage = m_swapchainObject.GetDepthStencilImage(srcImageIndex); if (swapchainDepthImage == VK_NULL_HANDLE) { MGLOG_E_ONCE("DirectVulkan::ReadDepthStencilPixels skipped: the default framebuffer has no " "depth/stencil image"); return; } // Per aspect, because the default framebuffer carries a SEPARATE placeholder // attachment for depth and for stencil (MG_Impl/Init.cpp) and each parks its own // pending clear; materializing only one would read the other back un-cleared. if (wantDepth) { const Bool clearReady = MaterializePendingClearForDefaultFramebuffer( frame.commandBuffer, readFbo, MobileGL::FramebufferAttachmentType::Depth); MOBILEGL_ASSERT(clearReady, "ReadDepthStencilPixels: failed to materialize the default framebuffer's pending " "depth clear"); } if (wantStencil) { const Bool clearReady = MaterializePendingClearForDefaultFramebuffer( frame.commandBuffer, readFbo, MobileGL::FramebufferAttachmentType::Stencil); MOBILEGL_ASSERT(clearReady, "ReadDepthStencilPixels: failed to materialize the default framebuffer's pending " "stencil clear"); } const VkFormat swapchainDepthFormat = m_swapchainObject.GetDepthStencilFormat(); VkImageLayout trackedLayout = m_swapchainObject.GetDepthStencilImageLayout(srcImageIndex); ReadDepthStencilImageToClient(swapchainDepthImage, swapchainDepthFormat, &trackedLayout, GetDepthStencilAspectMaskForFormat(swapchainDepthFormat), 0, 0, x, y, width, height, format, type, pixels, /*defaultFramebufferOrientation=*/true); m_swapchainObject.SetDepthStencilImageLayout(srcImageIndex, trackedLayout); return; } const auto& attachment = readFbo.GetAttachment(attachmentType); VkImage image = VK_NULL_HANDLE; VkFormat vkFormat = VK_FORMAT_UNDEFINED; VkImageLayout* trackedLayout = nullptr; VkImageAspectFlags imageAspect = VK_IMAGE_ASPECT_NONE; Uint32 mipLevel = 0; Uint32 baseArrayLayer = 0; if (attachment.IsTexture() && attachment.GetTexture()) { auto textureObject = attachment.GetTexture(); const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *textureObject); MOBILEGL_ASSERT(clearReady, "ReadDepthStencilPixels: failed to materialize pending clear for textureId=%d", textureObject->GetExternalIndex()); auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*textureObject); if (resource == nullptr || resource->image == VK_NULL_HANDLE) { MGLOG_E_ONCE("DirectVulkan::ReadDepthStencilPixels skipped: failed to sync depth textureId=%u", textureObject->GetExternalIndex()); return; } image = resource->image; vkFormat = resource->format; trackedLayout = &resource->layout; imageAspect = resource->aspect; mipLevel = ToStorageMipLevel(attachment.GetTexture().get(), attachment.GetTextureLevel()); baseArrayLayer = ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer()); } else if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) { const auto& renderbufferObject = attachment.GetRenderbuffer(); const Bool clearReady = MaterializePendingClearForRenderbuffer(frame.commandBuffer, renderbufferObject); MOBILEGL_ASSERT(clearReady, "ReadDepthStencilPixels: failed to materialize pending clear for renderbuffer %u", renderbufferObject->GetExternalIndex()); auto* resource = m_renderPassManager->GetOrCreateRenderbufferResource(renderbufferObject); if (resource == nullptr || resource->image == VK_NULL_HANDLE) { MGLOG_E_ONCE("DirectVulkan::ReadDepthStencilPixels skipped: failed to resolve renderbuffer %u", renderbufferObject->GetExternalIndex()); return; } image = resource->image; vkFormat = resource->format; trackedLayout = &resource->layout; imageAspect = resource->aspect; } else { return; } ReadDepthStencilImageToClient(image, vkFormat, trackedLayout, imageAspect, mipLevel, baseArrayLayer, x, y, width, height, format, type, pixels); } void VulkanRenderer::ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout, VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels, Bool defaultFramebufferOrientation, Uint32 sourceLayerCount) { const Bool wantDepth = format != GL_STENCIL_INDEX; const Bool wantStencil = format != GL_DEPTH_COMPONENT; auto& frame = m_frameContext.GetCurrent(); if (*trackedLayout == VK_IMAGE_LAYOUT_UNDEFINED) { MGLOG_E_ONCE("DirectVulkan::ReadDepthStencilPixels skipped: source layout is undefined"); return; } if (wantDepth && (imageAspect & VK_IMAGE_ASPECT_DEPTH_BIT) == 0) { MGLOG_E_ONCE("DirectVulkan::ReadDepthStencilPixels skipped: attachment has no depth aspect"); return; } if (wantStencil && (imageAspect & VK_IMAGE_ASPECT_STENCIL_BIT) == 0) { MGLOG_E_ONCE("DirectVulkan::ReadDepthStencilPixels skipped: attachment has no stencil aspect"); return; } // Per-aspect buffer-copy texel sizes (Vulkan defines the depth aspect of packed // formats to copy as its own tightly defined layout). SizeT depthCopyBytes = 0; switch (vkFormat) { case VK_FORMAT_D16_UNORM: depthCopyBytes = 2; break; case VK_FORMAT_X8_D24_UNORM_PACK32: case VK_FORMAT_D24_UNORM_S8_UINT: case VK_FORMAT_D32_SFLOAT: case VK_FORMAT_D32_SFLOAT_S8_UINT: depthCopyBytes = 4; break; case VK_FORMAT_S8_UINT: break; default: MGLOG_E_ONCE("DirectVulkan::ReadDepthStencilPixels skipped: unsupported source format=%d", static_cast(vkFormat)); return; } const SizeT pixelCount = static_cast(width) * static_cast(height); const VkDeviceSize depthBytes = wantDepth ? pixelCount * depthCopyBytes : 0; // Buffer offsets for depth/stencil copies must be 4-byte aligned. const VkDeviceSize stencilOffset = (depthBytes + 3) & ~VkDeviceSize{3}; const VkDeviceSize stencilBytes = wantStencil ? pixelCount : 0; VkBufferObject readback; if (!readback.Create({ .allocator = m_allocator, .size = stencilOffset + stencilBytes, .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memoryUsage = VMA_MEMORY_USAGE_AUTO, .allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT, })) { MGLOG_E_ONCE("DirectVulkan::ReadDepthStencilPixels skipped: failed to create readback buffer"); return; } const VkImageLayout originalLayout = *trackedLayout; VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; GetImageTransitionSourceState(originalLayout, srcStageMask, srcAccessMask); Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, image, *trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, imageAspect, mipLevel, 1); MOBILEGL_ASSERT(ok, "%s: failed to transition depth-stencil source image", __func__); // The swapchain's depth/stencil image is stored display-side-up like its colour twin, so // the GL rect has to be mapped into that space before the copy and the copied rows // re-oriented afterwards - the same two halves the colour ReadPixels path applies. VkOffset2D copyOffset{x, y}; VkExtent2D copyExtent{static_cast(width), static_cast(height)}; if (defaultFramebufferOrientation) { const VkExtent2D defaultFboExtent = m_swapchainObject.GetExtent(); const Bool mapped = MapDefaultFramebufferReadbackRect( x, y, width, height, defaultFboExtent, m_swapchainObject.GetPreTransform(), ©Offset, ©Extent); MOBILEGL_ASSERT(mapped, "ReadDepthStencilPixels: default framebuffer read rectangle is out of bounds"); if (!mapped) return; } // See the header: a stack of one-row layers and a single multi-row layer copy out to the // same tightly-packed bytes, so only the region's shape splits the two cases. const Uint32 copyLayerCount = std::max(sourceLayerCount, 1u); const Uint32 copyRowCount = copyLayerCount > 1u ? 1u : copyExtent.height; VkBufferImageCopy regions[2]{}; Uint32 regionCount = 0; if (wantDepth) { auto& region = regions[regionCount++]; region.bufferOffset = 0; region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; region.imageSubresource.mipLevel = mipLevel; region.imageSubresource.baseArrayLayer = baseArrayLayer; region.imageSubresource.layerCount = copyLayerCount; region.imageOffset = {copyOffset.x, copyOffset.y, 0}; region.imageExtent = {copyExtent.width, copyRowCount, 1}; } if (wantStencil) { auto& region = regions[regionCount++]; region.bufferOffset = stencilOffset; region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT; region.imageSubresource.mipLevel = mipLevel; region.imageSubresource.baseArrayLayer = baseArrayLayer; region.imageSubresource.layerCount = copyLayerCount; region.imageOffset = {copyOffset.x, copyOffset.y, 0}; region.imageExtent = {copyExtent.width, copyRowCount, 1}; } vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(), regionCount, regions); VkPipelineStageFlags restoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags restoreAccessMask = 0; GetImageTransitionDestinationState(originalLayout, restoreStageMask, restoreAccessMask); ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, image, *trackedLayout, originalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, restoreStageMask, VK_ACCESS_TRANSFER_READ_BIT, restoreAccessMask, imageAspect, mipLevel, 1); MOBILEGL_ASSERT(ok, "%s: failed to restore depth-stencil source image layout", __func__); if (!SubmitReadbackCommandsAndWait(frame)) { return; } const auto* mapped = static_cast(readback.Map()); if (mapped == nullptr || !readback.Invalidate(stencilOffset + stencilBytes)) { MGLOG_E_ONCE("DirectVulkan::ReadDepthStencilPixels skipped: failed to map readback buffer"); return; } const Uint8* depthSrc = mapped; const Uint8* stencilSrc = mapped + stencilOffset; // Re-orient the copied band per aspect, before any repacking reads it: the depth and // stencil aspects were copied into their own tightly packed sub-buffers, so each is a // plain width x height image of its own texel size. Vector remappedDepth; Vector remappedStencil; if (defaultFramebufferOrientation) { const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform(); Bool remapped = true; if (wantDepth && depthCopyBytes > 0) { remappedDepth.resize(pixelCount * depthCopyBytes); remapped = RemapDefaultFramebufferReadback(depthSrc, static_cast(width), static_cast(height), preTransform, depthCopyBytes, remappedDepth.data()); } if (remapped && wantStencil) { remappedStencil.resize(pixelCount); remapped = RemapDefaultFramebufferReadback(stencilSrc, static_cast(width), static_cast(height), preTransform, 1, remappedStencil.data()); } if (remapped) { if (!remappedDepth.empty()) depthSrc = remappedDepth.data(); if (!remappedStencil.empty()) stencilSrc = remappedStencil.data(); } else { MGLOG_D("DirectVulkan::ReadDepthStencilPixels: default-FBO remap failed (w=%d h=%d " "preTransform=%d); falling back to raw readback", width, height, static_cast(preTransform)); } } // The packing is the SHARED PackDepthStencilTight, so this arm and the wire arm cannot // disagree about the encoding again: one aspect or both, in the client's own width, with // the signed integer widths scaled into their own signed range and GL_HALF_FLOAT written // as the half word the caller asked for. Vector packed; if (!PackDepthStencilTight(wantDepth ? depthSrc : nullptr, wantStencil ? stencilSrc : nullptr, vkFormat, pixelCount, format, type, packed)) { MGLOG_E_ONCE("DirectVulkan::ReadDepthStencilPixels skipped: unsupported type=0x%x", type); return; } const SizeT dstPixelBytes = packed.size() / pixelCount; // Store honoring the client pack state (single slice). #if MOBILEGL_BUILD_DISAGGREGATED const SharedPtr wireReplyHasNoPackBuffer; #endif const auto& pixelPackBufferObject = #if MOBILEGL_BUILD_DISAGGREGATED MG_Config::Transport != MG_Config::TransportMode::Monolith ? wireReplyHasNoPackBuffer : #endif MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); const auto packParams = MGB_CTX->GetPixelStoreParameters(false); const SizeT rowPixels = static_cast(packParams.RowLength > 0 ? packParams.RowLength : width); const SizeT packAlignment = packParams.Alignment > 0 ? static_cast(packParams.Alignment) : 1; const SizeT dstRowStride = ((rowPixels * dstPixelBytes) + packAlignment - 1) / packAlignment * packAlignment; const SizeT dstSkipOffset = static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; const SizeT dstRowBytes = static_cast(width) * dstPixelBytes; const SizeT pboBaseOffset = reinterpret_cast(pixels); if (pixelPackBufferObject != nullptr) { const SizeT requiredSize = pboBaseOffset + dstSkipOffset + static_cast(height - 1) * dstRowStride + dstRowBytes; if (requiredSize > pixelPackBufferObject->GetSize()) { MGLOG_E_ONCE("DirectVulkan::ReadDepthStencilPixels skipped: pixel pack buffer is too small"); return; } } for (GLsizei row = 0; row < height; ++row) { Uint8* srcRow = packed.data() + static_cast(row) * dstRowBytes; const SizeT dstOffset = dstSkipOffset + static_cast(row) * dstRowStride; if (pixelPackBufferObject != nullptr) { pixelPackBufferObject->WritebackFromBackend({srcRow, dstRowBytes}, pboBaseOffset + dstOffset); } else { Memcpy(static_cast(pixels) + dstOffset, srcRow, dstRowBytes); } } } void VulkanRenderer::GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) { const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); auto& activeUnit = MGB_CTX->GetTextureUnitObject(MGB_CTX->GetActiveTextureUnit()); auto textureObject = activeUnit.GetBindingSlot(textureTarget).GetBoundObject(); GetTextureImage(textureObject, textureUploadTarget, level, format, type, -1, pixels); } void VulkanRenderer::GetTextureImage(const SharedPtr& textureObject, TextureUploadTarget textureUploadTarget, GLint level, GLenum format, GLenum type, GLsizei bufSize, GLvoid* pixels) { if (textureObject == nullptr || textureObject->GetStorageType() != TextureStorageType::Mipmap) { return; } auto* textureMipmapObject = static_cast(textureObject.get()); if (level < 0 || static_cast(level) >= textureMipmapObject->GetMipmapLevelCount()) { MGLOG_E_ONCE("DirectVulkan::GetTexImage skipped: level %d is out of range", level); return; } auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*textureObject); // Two shapes end up in the same place, and for the same reason: the GL level being read has // no GPU storage, so UploadDirtyMipLevels never wrote it and the CPU shadow is the ONLY copy // of its bytes - which makes the shadow both the safe answer and the correct one. // // (a) No VkImage at all. A mutable texture whose GL level 0 was never defined - // glTexImage2D(GL_TEXTURE_2D, 5, ...) and nothing else, exactly what the // clear_tex_image conformance cases build. VkTextureManager takes storage mip 0 as the // physical image extent (CheckMipmapCompleteness), so it refuses to back the texture. // (b) A VkImage with FEWER mip levels than the GL level count. GetUploadMipLevelCount // breaks at the first level with a zero extent, so "level 0 defined, a gap, level 3 // defined" produces a one-mip image while GL_TEXTURE_MAX_LEVEL-style state still // reports four levels. The same clamp also fires on a base level small enough that the // full chain is shorter than the levels the application defined. // // (b) is the dangerous one and is why the level is bounded against the RESOURCE and not only // against the GL-side count above: writing that level into imageSubresource.mipLevel is an // out-of-range subresource, which is the promise the driver takes at face value. The // glCopyImageSubData path two functions up carries the same guard for the same reason, added // after it SIGSEGV'd inside the Adreno driver; the readback never had one. const Bool hasImage = resource != nullptr && resource->image != VK_NULL_HANDLE; const Bool levelIsBacked = hasImage && ToStorageMipLevel(textureObject.get(), level) < resource->mipLevels; if (!levelIsBacked) { // Never gated on "syncing was inconvenient": a blanket shadow answer would silently // return stale bytes for every render-to-texture result. MGLOG_D("DirectVulkan::GetTexImage: textureId=%u level %d has no GPU storage (%s); answering " "from the CPU shadow", textureObject->GetExternalIndex(), level, hasImage ? "the image has fewer mip levels" : "the texture has no VkImage"); MG_Impl::GLImpl::CopyTextureImageToClientOrPBO_State(textureObject, textureUploadTarget, level, format, type, bufSize, pixels, "DirectVulkan::GetTextureImage"); return; } auto& frame = m_frameContext.GetCurrent(); if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); } if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *textureObject); MOBILEGL_ASSERT(clearReady, "GetTexImage: failed to materialize pending clear for textureId=%d", textureObject->GetExternalIndex()); // WHICH FACE the caller asked for. glGetTexImage names one face of a cube map through the // TARGET token (GL_TEXTURE_CUBE_MAP_NEGATIVE_X and friends, GL 4.6 core 8.11), and a cube // map's six faces are its VkImage's six ARRAY LAYERS - so unless the token is turned into a // baseArrayLayer, every face token reads layer 0 and the whole cube answers as +X. The // image's own target cannot supply this: a plain GL_TEXTURE_CUBE_MAP is not an array target, // so the layer arithmetic below leaves it at one layer starting at zero, which is precisely // the layer this face index has to displace. Same conversion, same reason, as // VkClearManager's / VkRenderPassManager's ResolveAttachmentBaseArrayLayer, which resolve an // ATTACHMENT's face; this is the readback's copy of it. Zero for every other target, // including a cube map ARRAY - that one arrives as TextureUploadTarget::CubeMapArray with // its layer-faces already counted in the level's z, not as a face token. const Bool isCubeFaceTarget = textureUploadTarget >= TextureUploadTarget::CubeMapPositiveX && textureUploadTarget <= TextureUploadTarget::CubeMapNegativeZ; const Int glCubeFaceLayer = isCubeFaceTarget ? static_cast(textureUploadTarget) - static_cast(TextureUploadTarget::CubeMapPositiveX) : 0; // The storage-image layer a copy starts at: the view's layer origin plus, for the one shape // that names a face, the face the target token chose. Every copy below asks here, so the // depth branch and the colour path cannot disagree about which layer was read. const Uint32 baseArrayLayer = ToStorageArrayLayer(textureObject.get(), glCubeFaceLayer); if ((resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) { if (format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL || format == GL_STENCIL_INDEX) { const auto levelSize = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, static_cast(level)); // Storage space: `resource` is the storage texture's, so a view's level and // layer have to be shifted into its numbering (see ToStorageMipLevel). const Uint32 arrayLayer = baseArrayLayer; const Uint32 storageLevel = ToStorageMipLevel(textureObject.get(), level); // A 1D array's levelSize.y() is its LAYER count, and those layers are the rows // GL wants back - but in Vulkan they are array layers of a one-row image, not // rows of layer 0, so the read has to be told which of the two it is looking at. const Uint32 sourceLayers = textureObject->GetTarget() == TextureTarget::Texture1DArray ? static_cast(std::max(levelSize.y(), 1)) : 1u; ReadDepthStencilImageToClient(resource->image, resource->format, &resource->layout, resource->aspect, storageLevel, arrayLayer, 0, 0, levelSize.x(), levelSize.y(), format, type, pixels, /*defaultFramebufferOrientation=*/false, sourceLayers); } else { MGLOG_E_ONCE("DirectVulkan::GetTexImage skipped: color query of a non-color texture"); } return; } const auto texelSize = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, static_cast(level)); const GLsizei width = texelSize.x(); const GLsizei height = texelSize.y(); if (width <= 0 || height <= 0) { return; } // GetTexImage returns every slice of a 3D level and every layer of an array // level; GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply to the 3D/array // destination layout (GL 3.3 section 6.1.4). const auto imageTextureTarget = textureObject->GetTarget(); const Bool is3dImage = imageTextureTarget == TextureTarget::Texture3D; const Bool is1dArrayImage = imageTextureTarget == TextureTarget::Texture1DArray; const Bool isArrayImage = is1dArrayImage || imageTextureTarget == TextureTarget::Texture2DArray || imageTextureTarget == TextureTarget::TextureCubeMapArray; const GLsizei depthSlices = is3dImage ? std::max(texelSize.z(), 1) : 1; // How many layers of the CALLER's texture a copy has to cover. `texelSize` above is already // measured in the caller's space - a view's layer axis is the VIEW's, and the destination // size the frontend validates a by-name readback against is built from the same extents - // so the copy has to cover exactly those layers. resource->arrayLayers is the STORAGE // owner's count: reading that many layers through a layer-windowed view covers layers the // view does not name and, because the copy starts at the view's layer origin, runs past the // last layer the image has. const Uint32 storageArrayLayers = static_cast(resource->arrayLayers); const Uint32 layersFromOrigin = baseArrayLayer < storageArrayLayers ? storageArrayLayers - baseArrayLayer : 0u; const GLsizei arrayLayers = static_cast( isArrayImage ? (textureObject->IsTextureView() ? std::min(static_cast(textureObject->GetViewNumLayers()), layersFromOrigin) : storageArrayLayers) : 1u); // A 1D array level comes back as ONE two-dimensional image whose rows are its layers // (GL 4.6 core 8.11.4), so its layers are already counted by `height` above and must not // multiply the slice count the way a 2D-array's or a cube-array's do. Vulkan still keeps // them in arrayLayers on a one-row image, which is what the copy region below says - the // two describe the same tightly-packed bytes. const GLsizei sliceCount = std::max(depthSlices * (is1dArrayImage ? 1 : arrayLayers), 1); if (bufSize >= 0) { const Int dstChannels = GetReadbackChannelCount(format); if ((type == GL_UNSIGNED_BYTE || type == GL_FLOAT) && dstChannels > 0) { const SizeT dstComponentSize = type == GL_FLOAT ? sizeof(Float) : sizeof(Uint8); const SizeT minSize = static_cast(width) * static_cast(height) * static_cast(dstChannels) * dstComponentSize; if (static_cast(bufSize) < minSize) { MGLOG_E_ONCE("DirectVulkan::GetTextureImage skipped: destination buffer is too small"); return; } } } const SizeT sourceTexelSize = GetReadbackTexelSize(resource->format); if (sourceTexelSize == 0) { MGLOG_E_ONCE("DirectVulkan::GetTexImage skipped: unsupported source format=%d", static_cast(resource->format)); return; } const VkDeviceSize readbackSize = static_cast(width) * static_cast(height) * static_cast(sliceCount) * sourceTexelSize; VkBufferObject readback; if (!readback.Create({ .allocator = m_allocator, .size = readbackSize, .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memoryUsage = VMA_MEMORY_USAGE_AUTO, .allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT, })) { MGLOG_E_ONCE("DirectVulkan::GetTexImage skipped: failed to create readback buffer"); return; } const VkImageLayout originalLayout = resource->layout; VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; GetImageTransitionSourceState(originalLayout, srcStageMask, srcAccessMask); // The copy below reads EVERY layer of the level, which is exactly the range // TransitionImageLayout barriers cover. Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, resource->aspect, ToStorageMipLevel(textureObject.get(), level), 1); MOBILEGL_ASSERT(ok, "%s: failed to transition texture image", __func__); VkBufferImageCopy copyRegion{}; copyRegion.imageSubresource.aspectMask = resource->aspect; // Storage space, as above: a texture view reads its own level 0 out of whichever level // and layer of the parent it opened onto. copyRegion.imageSubresource.mipLevel = ToStorageMipLevel(textureObject.get(), level); // baseArrayLayer, not 0: the cube face the target token named, plus the view's layer origin // (see above). The face is non-zero for exactly one shape - a plain cube map read one face // at a time - and layerCount is 1 there, so the copy stays inside the six layers the image // has. copyRegion.imageSubresource.baseArrayLayer = baseArrayLayer; copyRegion.imageSubresource.layerCount = static_cast(arrayLayers); copyRegion.imageExtent = {static_cast(width), is1dArrayImage ? 1u : static_cast(height), static_cast(depthSlices)}; vkCmdCopyImageToBuffer(frame.commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(), 1, ©Region); VkPipelineStageFlags restoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags restoreAccessMask = 0; GetImageTransitionDestinationState(originalLayout, restoreStageMask, restoreAccessMask); ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource->image, resource->layout, originalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, restoreStageMask, VK_ACCESS_TRANSFER_READ_BIT, restoreAccessMask, resource->aspect, ToStorageMipLevel(textureObject.get(), level), 1); MOBILEGL_ASSERT(ok, "%s: failed to restore texture image layout", __func__); if (!SubmitReadbackCommandsAndWait(frame)) { return; } const auto* mapped = static_cast(readback.Map()); if (mapped == nullptr) { MGLOG_E_ONCE("DirectVulkan::GetTextureImage skipped: failed to map readback buffer"); return; } if (!readback.Invalidate(readbackSize)) { MGLOG_E_ONCE("DirectVulkan::GetTextureImage skipped: failed to invalidate readback buffer"); return; } PackReadbackToClientOrPbo(mapped, resource->format, width, height, sliceCount, format, type, pixels, /*applyPackImageParams=*/is3dImage || isArrayImage); } void VulkanRenderer::GenerateMipmap(GLenum target) { #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { GenerateWireMipmap(); return; } #endif const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); // Whatever is left here is a coverage gap in this backend, not a broken invariant, so it // declines (leaving the mip chain unwritten) rather than asserting the process down. What // remains is the multisample targets, which GL 4.6 core 8.14.4 forbids to glGenerateMipmap // outright. // // Every ARRAY target - 1D array, 2D array, cube map array - needs no blit code of its own: // its layers live in the VkImage's arrayLayers, so resource->extent/depth already describe // one layer's image and the loop below already copies every layer of the window per level // via srcSubresource.layerCount. The one thing they DO need is that the GL-space storage // allocation not shrink the layer count down the chain, which MipShrinkingComponentCount // handles. if (textureTarget != TextureTarget::Texture2D && textureTarget != TextureTarget::Texture2DArray && textureTarget != TextureTarget::Texture3D && textureTarget != TextureTarget::TextureCubeMap && textureTarget != TextureTarget::TextureCubeMapArray && // A 1D texture needs nothing special: its storage extent is {width, 1, 1}, so the blit // loop below already emits the y and z offsets of 0 and 1 that a 1D image requires. textureTarget != TextureTarget::Texture1D && textureTarget != TextureTarget::Texture1DArray) { MGLOG_W_ONCE("GenerateMipmap: unsupported target %s", MG_Util::ConvertTextureTargetToString(textureTarget).c_str()); return; } auto& textureUnit = MGB_CTX->GetTextureUnitObject(MGB_CTX->GetActiveTextureUnit()); auto texture = textureUnit.GetBindingSlot(textureTarget).GetBoundObject(); MOBILEGL_ASSERT(texture != nullptr, "GenerateMipmap requires a bound texture."); MOBILEGL_ASSERT(texture->IsComplete(), "GenerateMipmap requires a complete texture."); auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(texture.get()); MOBILEGL_ASSERT(mipmapTexture != nullptr, "GenerateMipmap requires a mipmapped texture object."); const Uint32 currentMipLevelCount = static_cast(mipmapTexture->GetMipmapLevelCount()); MOBILEGL_ASSERT(currentMipLevelCount > 0, "GenerateMipmap requires level 0 storage."); const auto& uploadTargets = mipmapTexture->GetUploadTargets(); MOBILEGL_ASSERT(!uploadTargets.empty(), "GenerateMipmap requires at least one upload target."); // The generation window the call names, in the BOUND object's coordinates: BASE_LEVEL is // where generation starts, and it runs to the smaller of the base image's full chain and // MAX_LEVEL + 1 (GL 4.6 core 8.17). Two things make this the FRONTEND's plan rather than // an arithmetic of this function's own: that window is what the storage allocation below // has to cover, and through a view the numbers are the VIEW's own - BASE_LEVEL 1 of a view // whose MIN_LEVEL is 1 describes the STORAGE's level 2, and MAX_LEVEL 2 ends the chain at // the storage's level 3. const auto mipmapPlan = MG_Impl::GLImpl::ComputeMipmapGenerationRange(*mipmapTexture, uploadTargets.front()); const Uint32 baseMipLevel = std::min(mipmapPlan.Base, currentMipLevelCount - 1); // A base level the storage does not reach leaves nothing to generate; the storage window // below collapses to a single level, which every consumer reads as a no-op. const Uint32 endMipLevel = std::max(mipmapPlan.End, baseMipLevel + 1); // A texture that has only ever defined level 0 carries a single-level backing, so defining // the rest of the chain below recreates the image and carries the old contents over with a // copy that is submitted and waited on out of band. Anything this frame has already // recorded into the old image is not submitted yet, so that copy would read pre-flush // content and every generated level would descend from a stale level 0 - the same hazard // the storage-usage upgrade flushes for before its own preserve-copy. if (m_textureManager->NeedsMipChainGrowth(*texture) && HasPendingRecordedWork()) { if (FlushPendingCommands()) { // Fresh command buffer: the sampled-descriptor-set memo describes bindings that // only existed in the retired one. m_lastSampledSetValid = false; } } auto& frame = m_frameContext.GetCurrent(); if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); } if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *texture); MOBILEGL_ASSERT(clearReady, "GenerateMipmap: failed to materialize pending clear for textureId=%d", texture->GetExternalIndex()); auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*texture); MOBILEGL_ASSERT(resource != nullptr && resource->image != VK_NULL_HANDLE, "GenerateMipmap failed to sync the backend texture."); VkFormatProperties formatProperties{}; vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, resource->format, &formatProperties); const VkFormatFeatureFlags optimalTilingFeatures = formatProperties.optimalTilingFeatures; const Bool isDepthOrStencilTexture = (resource->aspect & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0; const Bool supportsNativeBlit = (optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) != 0 && (optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT) != 0; if (!isDepthOrStencilTexture && !supportsNativeBlit) { MGLOG_W_ONCE("GenerateMipmap skipped for textureId=%d because Vulkan format %d does not support blit-based mip generation", texture->GetExternalIndex(), static_cast(resource->format)); return; } if (isDepthOrStencilTexture) { MOBILEGL_ASSERT((resource->aspect & VK_IMAGE_ASPECT_STENCIL_BIT) == 0, "GenerateMipmap: depth-stencil mipmap generation is not supported yet."); } #if MOBILEGL_BUILD_DISAGGREGATED // P5c (T5 / tx): under an active transport the generated chain is defined on the // server's staged shadow (keyed by the synced TextureResource above) and the client's // level storage is never written; in monolith the client-object path runs unchanged. const Bool allocatedMipmapStorage = MG_Config::Transport != MG_Config::TransportMode::Monolith ? EnsureGenerateMipmapShadowAllocated(*resource, baseMipLevel, uploadTargets) : EnsureGenerateMipmapStorageAllocated(*mipmapTexture, baseMipLevel, endMipLevel); #else const Bool allocatedMipmapStorage = EnsureGenerateMipmapStorageAllocated(*mipmapTexture, baseMipLevel, endMipLevel); #endif MOBILEGL_ASSERT(allocatedMipmapStorage, "GenerateMipmap could not allocate a full mip chain for this texture."); resource = m_textureManager->SyncTextureAndGetDescriptor(*texture); MOBILEGL_ASSERT(resource != nullptr && resource->image != VK_NULL_HANDLE, "GenerateMipmap failed to resync the backend texture after allocating mip storage."); if (resource->layout == VK_IMAGE_LAYOUT_UNDEFINED) { const VkImageLayout finalLayout = ResolveGenerateMipmapFinalLayout(resource->aspect); Bool transitioned = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource->image, resource->layout, finalLayout, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels); MOBILEGL_ASSERT(transitioned, "GenerateMipmap: failed to transition uninitialized mip chain"); return; } // Every blit below runs in the STORAGE image's coordinates. A view owns no image of its // own (VkTextureManager::StorageTextureOf), so the window the application named through // the view - its BASE_LEVEL..MAX_LEVEL and its layer slice - has to land at the levels and // layers the view aliases, and everything outside that window belongs to the storage owner // and must not move (GL 4.6 core 8.18). const Uint32 storageBaseMipLevel = ToStorageMipLevel(texture.get(), baseMipLevel); const Uint32 storageEndMipLevel = ToStorageMipLevel(texture.get(), endMipLevel); // The chain may also stop where the base image's extent stops: levels past 1x1 are not // generatable, and a chain the storage does not carry cannot be written. const IntVec3 storageBaseTexelSize = { static_cast(resource->extent.width), static_cast(resource->extent.height), static_cast(resource->depth), }; const IntVec3 baseTexelSize = ComputeMipTexelSize(storageBaseTexelSize, storageBaseMipLevel); const Uint32 storageChainEndMipLevel = storageBaseMipLevel + ComputeFullMipLevelCount(baseTexelSize); const Uint32 generateMipLevelCount = std::min({storageEndMipLevel, storageChainEndMipLevel, static_cast(resource->mipLevels)}); if (generateMipLevelCount <= storageBaseMipLevel + 1) { resource->layout = ResolveGenerateMipmapFinalLayout(resource->aspect); return; } // A view writes ONLY the layers it names; the storage owner keeps every other one. A // texture that is not a view addresses the whole image, which is what layerCount // resource->arrayLayers already means. Uint32 baseArrayLayer = 0; Uint32 arrayLayerCount = static_cast(resource->arrayLayers); if (texture->IsTextureView()) { baseArrayLayer = std::min(ToStorageArrayLayer(texture.get(), 0), arrayLayerCount); arrayLayerCount = std::min(static_cast(texture->GetViewNumLayers()), static_cast(resource->arrayLayers) - baseArrayLayer); } MOBILEGL_ASSERT(arrayLayerCount > 0, "GenerateMipmap: empty layer window for textureId=%d", texture->GetExternalIndex()); const VkImageLayout originalLayout = resource->layout; const VkImageLayout finalLayout = ResolveGenerateMipmapFinalLayout(resource->aspect); if (isDepthOrStencilTexture && !supportsNativeBlit) { const Bool supportsShaderDepthMipmap = (optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0 && (optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0; MOBILEGL_ASSERT(resource->aspect == VK_IMAGE_ASPECT_DEPTH_BIT, "GenerateMipmap: shader fallback only supports depth-only textures."); MOBILEGL_ASSERT(textureTarget == TextureTarget::Texture2D && resource->depth == 1 && resource->arrayLayers == 1, "GenerateMipmap: shader fallback only supports single-layer GL_TEXTURE_2D depth textures."); MOBILEGL_ASSERT(supportsShaderDepthMipmap, "GenerateMipmap: depth texture format %d lacks sampled/depth-attachment support for shader fallback.", static_cast(resource->format)); const Bool depthReady = GenerateDepthMipmapWithShader(frame, *texture, *resource, storageBaseMipLevel, generateMipLevelCount, storageBaseTexelSize, originalLayout, finalLayout); MOBILEGL_ASSERT(depthReady, "GenerateMipmap: depth fallback failed for textureId=%d target=%d internalFormat=%d vkFormat=%d", texture->GetExternalIndex(), static_cast(texture->GetTarget()), static_cast(texture->GetFormat()), static_cast(resource->format)); return; } const VkFilter blitFilter = isDepthOrStencilTexture ? VK_FILTER_NEAREST : ((optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0 ? VK_FILTER_LINEAR : VK_FILTER_NEAREST); VkPipelineStageFlags originalSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags originalSrcAccessMask = 0; GetImageTransitionSourceState(originalLayout, originalSrcStageMask, originalSrcAccessMask); VkPipelineStageFlags finalDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags finalDstAccessMask = 0; GetImageTransitionDestinationState(finalLayout, finalDstStageMask, finalDstAccessMask); if (originalLayout != finalLayout) { if (storageBaseMipLevel > 0) { VkImageLayout lowerMipLayout = originalLayout; const Bool lowerReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource->image, lowerMipLayout, finalLayout, originalSrcStageMask, finalDstStageMask, originalSrcAccessMask, finalDstAccessMask, resource->aspect, 0, storageBaseMipLevel); MOBILEGL_ASSERT(lowerReady, "%s: failed to transition lower untouched mip levels", __func__); } if (generateMipLevelCount < resource->mipLevels) { VkImageLayout upperMipLayout = originalLayout; const Bool upperReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource->image, upperMipLayout, finalLayout, originalSrcStageMask, finalDstStageMask, originalSrcAccessMask, finalDstAccessMask, resource->aspect, generateMipLevelCount, resource->mipLevels - generateMipLevelCount); MOBILEGL_ASSERT(upperReady, "%s: failed to transition upper untouched mip levels", __func__); } } VkImageLayout srcMipLayout = originalLayout; Bool srcReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource->image, srcMipLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, originalSrcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, originalSrcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, resource->aspect, storageBaseMipLevel, 1); MOBILEGL_ASSERT(srcReady, "%s: failed to transition base mip level to transfer source", __func__); // Every generated level starts from originalLayout and ends up TRANSFER_DST_OPTIMAL, and // the loop below only ever moves a level OUT of that layout after it has been written - so // the whole range can be prepared in one barrier instead of one per level. That turns a // 12-level chain's 3(N-1)+1 barrier commands into 2(N-1)+2. Each level is still // individually transitioned to TRANSFER_SRC before it is read, so the write-then-read // dependency between consecutive levels is unchanged. if (generateMipLevelCount > storageBaseMipLevel + 1) { VkImageLayout dstRangeLayout = originalLayout; const Bool dstRangeReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource->image, dstRangeLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, originalSrcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, originalSrcAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, resource->aspect, storageBaseMipLevel + 1, generateMipLevelCount - (storageBaseMipLevel + 1)); MOBILEGL_ASSERT(dstRangeReady, "%s: failed to transition mip levels to transfer destination", __func__); } for (Uint32 level = storageBaseMipLevel + 1; level < generateMipLevelCount; ++level) { const IntVec3 srcTexelSize = ComputeMipTexelSize(storageBaseTexelSize, level - 1); const IntVec3 dstTexelSize = ComputeMipTexelSize(storageBaseTexelSize, level); VkImageBlit blitRegion{}; blitRegion.srcSubresource.aspectMask = resource->aspect; blitRegion.srcSubresource.mipLevel = level - 1; blitRegion.srcSubresource.baseArrayLayer = baseArrayLayer; blitRegion.srcSubresource.layerCount = arrayLayerCount; blitRegion.srcOffsets[0] = {0, 0, 0}; blitRegion.srcOffsets[1] = {srcTexelSize.x(), srcTexelSize.y(), srcTexelSize.z()}; blitRegion.dstSubresource.aspectMask = resource->aspect; blitRegion.dstSubresource.mipLevel = level; blitRegion.dstSubresource.baseArrayLayer = baseArrayLayer; blitRegion.dstSubresource.layerCount = arrayLayerCount; blitRegion.dstOffsets[0] = {0, 0, 0}; blitRegion.dstOffsets[1] = {dstTexelSize.x(), dstTexelSize.y(), dstTexelSize.z()}; vkCmdBlitImage(frame.commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, resource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blitRegion, blitFilter); VkImageLayout finishedSrcLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; Bool srcRestored = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource->image, finishedSrcLayout, finalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, finalDstStageMask, VK_ACCESS_TRANSFER_READ_BIT, finalDstAccessMask, resource->aspect, level - 1, 1); MOBILEGL_ASSERT(srcRestored, "%s: failed to transition mip level %u to final layout", __func__, level - 1); if (level + 1 < generateMipLevelCount) { VkImageLayout nextSrcLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; Bool nextSrcReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource->image, nextSrcLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, resource->aspect, level, 1); MOBILEGL_ASSERT(nextSrcReady, "%s: failed to prepare mip level %u as next transfer source", __func__, level); } else { VkImageLayout lastMipLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; Bool lastMipReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource->image, lastMipLayout, finalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, finalDstStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, finalDstAccessMask, resource->aspect, level, 1); MOBILEGL_ASSERT(lastMipReady, "%s: failed to transition last mip level to final layout", __func__); } } resource->layout = finalLayout; // The chain above is GPU work recorded into this frame's command buffer, which is not // submitted until the frame ends - but a texture upload goes out on a command buffer of // its own the moment it happens. A glTexSubImage2D into a level this just generated // would therefore reach the GPU FIRST and be overwritten by these blits, which is how // KHR-GL40.texture_gather.base-level lost the texels it wrote into level 1 right after // generating the chain. Submitting here is what orders the two. if (HasPendingRecordedWork() && FlushPendingCommands()) { // Fresh command buffer: the sampled-descriptor-set memo describes bindings that // only existed in the retired one. m_lastSampledSetValid = false; } } // 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(kXfbCounterObjectSlots); const Uint64 identity = MGB_CTX->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 (MGB_CTX->HasOpenTransformFeedbackSpan(m_xfbCounterSlotOwner[candidate])) { 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 || !MGB_CTX_LIVE || !MGB_CTX->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 (MGB_CTX->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; } #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return BeginWireXfbCaptureForDraw(frame); #endif const auto& program = MGB_CTX->GetTransformFeedbackProgram(); if (!program || program->GetTransformFeedbackVaryingCount() == 0) { return false; } const SizeT bufferCount = std::min(program->GetTransformFeedbackBufferCount(), 4); if (bufferCount == 0) { return false; } VkBuffer buffers[4] = {}; VkDeviceSize offsets[4] = {}; VkDeviceSize sizes[4] = {}; for (SizeT i = 0; i < bufferCount; ++i) { if (!program->GetTransformFeedbackStride(static_cast(i))) continue; auto& point = MGB_CTX->GetBufferBindingPoint(BufferTarget::TransformFeedback, static_cast(i)); const auto& bufferObject = point.GetBoundObject(); if (bufferObject == nullptr) { return false; } // Host-visible coherent GPU residency: the capture writes land where // MapBuffer/GetBufferSubData read. Coherence makes them visible once they // have happened, so the buffer is also flagged for the wait that a later CPU // read has to perform - the capture is a GPU write like any shader's. bufferObject->EnsureGpuResidentStorage(); #if MOBILEGL_PIPE_PUSH // P5c ev (R2, CONTRACT-P5C ยง4.2): through the reverse channel, not a direct poke // of the client object from the apply thread. MG_Pipe::MGPipeAnnounceBufferGpuWritten(bufferObject); #else bufferObject->MarkGpuWritten(); #endif BufferSlice slice{}; if (!m_bufferManager.AcquireResidentSlice(BufferKind::Vertex, bufferObject, slice)) { MGLOG_E_ONCE("BeginXfbCaptureForDraw: failed to acquire capture buffer %zu", i); return false; } const Range1D range = point.GetRange(); const VkDeviceSize rangeStart = static_cast(range.start); const VkDeviceSize rangeSize = range.end > range.start ? static_cast(range.end - range.start) : VK_WHOLE_SIZE; buffers[i] = slice.buffer; offsets[i] = slice.offset + rangeStart; sizes[i] = rangeSize; } return BeginXfbCaptureWithBuffers(frame, static_cast(bufferCount), buffers, offsets, sizes); } 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 = MGB_CTX->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(counterSlot) * 16 + static_cast(i) * 4; } if (resume) { s_vkCmdBeginTransformFeedbackEXT(frame.commandBuffer, 0, static_cast(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(counterSlot) * 16 + static_cast(i) * 4; } s_vkCmdEndTransformFeedbackEXT(frame.commandBuffer, 0, static_cast(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& outSlots) { outSlots = Move(m_occlusionActiveSlots); m_occlusionActiveSlots.clear(); m_occlusionCaptureActive = false; } Bool VulkanRenderer::ResolveOcclusionQueryResult(const Vector& 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& outSlots, Vector& 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& slots, const Vector& 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(payload.params.vertexOffset) : 0); vertexRange.instanceCount = payload.params.instanceCount; vertexRange.firstVertex = 0; vertexRange.firstInstance = static_cast(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 Vector 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(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(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 Vector 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(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(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(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(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 = MGB_CTX->GetRenderStateParameters(); if (rsp.PrimitiveRestartEnabled || rsp.PrimitiveRestartFixedIndexEnabled) { mergeGranularity = 0; } } const DrawIndexedCmdParam* pParams = payload.pParams; Uint32 drawCount = payload.drawCount; static Vector 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(mergedParams.size()); } EmitMultiDrawIndexed(frame.commandBuffer, pParams, drawCount); } // Byte size of the command structures GL defines for the indirect draws (GL 4.6 core // 10.3.10): four uint32 for DrawArraysIndirectCommand, five for DrawElementsIndirect- // Command. These bound the read out of GL_DRAW_INDIRECT_BUFFER and are the default // stride, so they must be GL's sizes and not this renderer's own draw-parameter // structs - DrawCmdParam carries two extra members and is 24 bytes, which made every // glDrawArraysIndirect on a tightly-sized indirect buffer look out of range and draw // nothing. constexpr SizeT kGLDrawArraysIndirectCommandBytes = 4 * sizeof(Uint32); constexpr SizeT kGLDrawElementsIndirectCommandBytes = 5 * sizeof(Uint32); void VulkanRenderer::MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) { auto& frame = m_frameContext.GetCurrent(); if (maxdrawcount <= 0) { return; } if (stride == 0) { stride = kGLDrawElementsIndirectCommandBytes; } if (stride < static_cast(kGLDrawElementsIndirectCommandBytes)) { MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: stride %d is smaller than command size %zu", stride, kGLDrawElementsIndirectCommandBytes); return; } const SizeT indexSize = MG_Util::GetGLTypeSize(type); if (indexSize == 0) { MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: unsupported index type 0x%x", type); return; } const auto& vao = *MGB_CTX->GetBoundVertexArray(); const auto* indexBuffer = vao.GetIndexBufferBindingSlot().GetBoundObject().get(); if (!indexBuffer) { MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: no element array buffer is bound"); return; } const SizeT commandOffset = reinterpret_cast(indirect); const SizeT commandBytes = commandOffset + static_cast(stride) * static_cast(maxdrawcount - 1) + kGLDrawElementsIndirectCommandBytes; auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); if (!drawBuffer || commandBytes > drawBuffer->GetSize()) { MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range"); return; } auto parameterBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject(); if (!parameterBuffer || static_cast(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) { MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range"); return; } DrawCmdParam vertexRange{}; vertexRange.vertexCount = static_cast(indexBuffer->GetSize() / indexSize); vertexRange.instanceCount = 1; IndexBufferView indexBufferView{}; indexBufferView.indexType = type; indexBufferView.indexByteOffset = 0; indexBufferView.indexByteSize = indexBuffer->GetSize(); if (!SetupDraw(frame, mode, DrawSetupAspect::IndexBuffer | DrawSetupAspect::IndirectDrawBuffer, vertexRange, &indexBufferView)) { return; } drawBuffer->SyncPersistentMappedRange(); parameterBuffer->SyncPersistentMappedRange(); BufferSlice drawSlice{}; if (!m_bufferManager.AcquireResidentSlice(BufferKind::Indirect, drawBuffer, drawSlice)) { MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: failed to sync draw indirect buffer"); return; } BufferSlice parameterSlice{}; if (!m_bufferManager.AcquireResidentSlice(BufferKind::Indirect, parameterBuffer, parameterSlice)) { MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: failed to sync parameter buffer"); return; } MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__); // vkCmdDrawIndexedIndirectCount with maxDrawCount > 1 additionally requires the // multiDrawIndirect device feature; fall back to the CPU readback loop otherwise. if (m_drawIndirectCountExtensionEnabled && s_vkCmdDrawIndexedIndirectCount && (m_multiDrawIndirectFeatureEnabled || maxdrawcount == 1)) { MGLOG_D("DirectVulkan: glMultiDrawElementsIndirectCountARB(max=%d stride=%d)", maxdrawcount, stride); s_vkCmdDrawIndexedIndirectCount(frame.commandBuffer, drawSlice.buffer, drawSlice.offset + static_cast(commandOffset), parameterSlice.buffer, parameterSlice.offset + static_cast(drawcount), static_cast(maxdrawcount), static_cast(stride)); return; } const Uint8* parameterData = parameterBuffer->MappedData(); const Uint8* drawData = drawBuffer->MappedData(); Uint32 actualDrawCount = 0; std::memcpy(&actualDrawCount, parameterData + drawcount, sizeof(actualDrawCount)); actualDrawCount = std::min(actualDrawCount, static_cast(maxdrawcount)); for (Uint32 idraw = 0; idraw < actualDrawCount; ++idraw) { DrawIndexedCmdParam cmd{}; std::memcpy(&cmd, drawData + commandOffset + static_cast(idraw) * stride, sizeof(cmd)); vkCmdDrawIndexed(frame.commandBuffer, cmd.indexCount, cmd.instanceCount, cmd.firstIndex, cmd.vertexOffset, cmd.firstInstance); } } void VulkanRenderer::MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) { auto& frame = m_frameContext.GetCurrent(); if (drawcount <= 0) { return; } if (stride == 0) { stride = kGLDrawElementsIndirectCommandBytes; } if (stride < static_cast(kGLDrawElementsIndirectCommandBytes)) { MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: stride %d is smaller than command size %zu", stride, kGLDrawElementsIndirectCommandBytes); return; } const SizeT indexSize = MG_Util::GetGLTypeSize(type); if (indexSize == 0) { MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: unsupported index type 0x%x", type); return; } const auto& vao = *MGB_CTX->GetBoundVertexArray(); const auto* indexBuffer = vao.GetIndexBufferBindingSlot().GetBoundObject().get(); if (!indexBuffer) { MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: no element array buffer is bound"); return; } const SizeT commandOffset = reinterpret_cast(indirect); const SizeT commandBytes = commandOffset + static_cast(stride) * static_cast(drawcount - 1) + kGLDrawElementsIndirectCommandBytes; auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); if (!drawBuffer || commandBytes > drawBuffer->GetSize()) { MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range"); return; } // The command parameters live on the GPU; the CPU-visible range that any single // command may address is the whole element array buffer. DrawCmdParam vertexRange{}; vertexRange.vertexCount = static_cast(indexBuffer->GetSize() / indexSize); vertexRange.instanceCount = 1; IndexBufferView indexBufferView{}; indexBufferView.indexType = type; indexBufferView.indexByteOffset = 0; indexBufferView.indexByteSize = indexBuffer->GetSize(); if (!SetupDraw(frame, mode, DrawSetupAspect::IndexBuffer | DrawSetupAspect::IndirectDrawBuffer, vertexRange, &indexBufferView)) { return; } BufferSlice drawSlice{}; if (!m_bufferManager.AcquireResidentSlice(BufferKind::Indirect, drawBuffer, drawSlice)) { MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: failed to sync draw indirect buffer"); return; } MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__); MGLOG_D("DirectVulkan: glMultiDrawElementsIndirect(drawcount=%d stride=%d)", drawcount, stride); if (drawcount == 1 || (!m_multiDrawForceUnrollIndirect && m_multiDrawIndirectFeatureEnabled && stride % 4 == 0)) { vkCmdDrawIndexedIndirect(frame.commandBuffer, drawSlice.buffer, drawSlice.offset + static_cast(commandOffset), static_cast(drawcount), static_cast(stride)); return; } // multiDrawIndirect device feature unavailable: one indirect draw per command is // valid without it and still consumes the GPU-written parameters. for (GLsizei idraw = 0; idraw < drawcount; ++idraw) { vkCmdDrawIndexedIndirect(frame.commandBuffer, drawSlice.buffer, drawSlice.offset + static_cast(commandOffset) + static_cast(idraw) * static_cast(stride), 1, 0); } } void VulkanRenderer::MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) { auto& frame = m_frameContext.GetCurrent(); if (drawcount <= 0) { return; } if (stride == 0) { stride = kGLDrawArraysIndirectCommandBytes; } if (stride < static_cast(kGLDrawArraysIndirectCommandBytes)) { MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu", stride, kGLDrawArraysIndirectCommandBytes); return; } const SizeT commandOffset = reinterpret_cast(indirect); const SizeT commandBytes = commandOffset + static_cast(stride) * static_cast(drawcount - 1) + kGLDrawArraysIndirectCommandBytes; auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); if (!drawBuffer || commandBytes > drawBuffer->GetSize()) { MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range"); return; } // The command parameters live on the GPU, so the vertex range is unknown here; // resident vertex buffers are uploaded in full regardless. A CLIENT-MEMORY ARRAY is the // one exception: its bytes have to be staged BEFORE the GPU reads the command, so for // exactly that case the words are resolved on the CPU and this range is what bounds the // staging. The draw itself stays native - the GPU reads the same commands. DrawCmdParam vertexRange{}; vertexRange.vertexCount = 0; vertexRange.instanceCount = 1; // MONOLITH ONLY: with a transport the frontend VAO is not this side's to read at all // (the client owns those bytes and stages them itself), and this runs on the apply thread. if (MG_Config::Transport == MG_Config::TransportMode::Monolith) { const auto& currentVAO = MGB_CTX->GetBoundVertexArray(); Bool clientArray = false; if (currentVAO) { for (const auto& attribute : currentVAO->GetAllAttributes()) { if (attribute.Enabled && !attribute.Buffer) { clientArray = true; break; } } } if (clientArray) { // The commands may be shader-written, and a stale shadow would size the upload // off words no shader ever wrote. drawBuffer->SyncGpuWrites(); const Uint8* commandHostBytes = drawBuffer->MappedData(); if (commandHostBytes == nullptr) { MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: a client-memory vertex array needs " "the commands on the CPU and the indirect buffer has no readable shadow"); return; } Uint64 lastElement = 0; for (GLsizei idraw = 0; idraw < drawcount; ++idraw) { VkDrawIndirectCommand command{}; Memcpy(&command, commandHostBytes + commandOffset + static_cast(idraw) * static_cast(stride), sizeof(command)); lastElement = std::max(lastElement, static_cast(command.firstVertex) + command.vertexCount); vertexRange.firstInstance = std::max(vertexRange.firstInstance, command.firstInstance); vertexRange.instanceCount = std::max(vertexRange.instanceCount, command.instanceCount); } vertexRange.firstVertex = 0; vertexRange.vertexCount = static_cast(lastElement); } } if (!SetupDraw(frame, mode, DrawSetupAspect::IndirectDrawBuffer, vertexRange)) { return; } BufferSlice drawSlice{}; if (!m_bufferManager.AcquireResidentSlice(BufferKind::Indirect, drawBuffer, drawSlice)) { MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: failed to sync draw indirect buffer"); return; } MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__); MGLOG_D("DirectVulkan: glMultiDrawArraysIndirect(drawcount=%d stride=%d)", drawcount, stride); if (drawcount == 1 || (!m_multiDrawForceUnrollIndirect && m_multiDrawIndirectFeatureEnabled && stride % 4 == 0)) { vkCmdDrawIndirect(frame.commandBuffer, drawSlice.buffer, drawSlice.offset + static_cast(commandOffset), static_cast(drawcount), static_cast(stride)); return; } for (GLsizei idraw = 0; idraw < drawcount; ++idraw) { vkCmdDrawIndirect(frame.commandBuffer, drawSlice.buffer, drawSlice.offset + static_cast(commandOffset) + static_cast(idraw) * static_cast(stride), 1, 0); } } 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) { #if MOBILEGL_BUILD_DISAGGREGATED return IsFrameSerialComplete(serial); #else return true; #endif } #if MOBILEGL_BUILD_DISAGGREGATED // 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; } #else // Pull build's historical one-fence wait stays byte-identical. for (const auto& record : m_inFlightSubmits) { if (record.frameSerial < serial || record.fence == VK_NULL_HANDLE) { continue; } if (vkWaitForFences(m_device, 1, &record.fence, VK_TRUE, UINT64_MAX) != VK_SUCCESS) { break; // fall through to the drain below } OnSubmitsCompletedUpTo(record.submitIndex); // Deliberately no NotifyDeviceIdle() here: that claims every submission has retired, // which is only true after a real queue drain. Work past this record may still run. TryDrainFrameTransients(); return true; } #endif // 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) { 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(); #if MOBILEGL_BUILD_DISAGGREGATED return IsFrameSerialComplete(serial); #else return true; #endif } 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(); #if MOBILEGL_BUILD_DISAGGREGATED return frame.isCommandRecording || frame.hasCommandBufferRecorded || frame.isPreCommandRecording || frame.hasPreCommandBufferRecorded; #else return frame.isCommandRecording || frame.hasCommandBufferRecorded; #endif } #if MOBILEGL_BUILD_DISAGGREGATED void VulkanRenderer::RewindWireDescriptorSetsIfDue() { if (!m_uniformManager || m_frameContext.GetFrameCount() == 0) return; const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex(); if (!m_uniformManager->WireDescriptorSetBudgetReached(frameIndex)) return; // 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()) 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 (vkQueueWaitIdle(m_graphicsQueue) != VK_SUCCESS) 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(through), UniformManager::kWireDescriptorSetBudget, cachedSets); } #endif Bool VulkanRenderer::IsSubmitIndexComplete(Uint64 submitIndex) { if (submitIndex <= m_completedSubmitCounter) { return true; } if (submitIndex > m_submitCounter) { return false; // not even submitted; no point polling fences } RefreshCompletedSubmits(); return submitIndex <= m_completedSubmitCounter; } 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); } } #if MOBILEGL_BUILD_DISAGGREGATED 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 fences; if (!CollectSubmitFencePrefix(m_inFlightSubmits, submitIndex, VkFence{}, fences)) return false; const VkResult result = vkWaitForFences(m_device, static_cast(fences.size()), fences.data(), VK_TRUE, timeoutNs); if (result == VK_SUCCESS) { OnSubmitsCompletedUpTo(submitIndex); return true; } if (result != VK_TIMEOUT) MGLOG_E_ONCE("WaitForSubmitsUpTo: vkWaitForFences returned %d", result); return false; } #endif void VulkanRenderer::OnSubmitsCompletedUpTo(Uint64 submitIndex) { m_completedSubmitCounter = std::max(m_completedSubmitCounter, submitIndex); #if MOBILEGL_BUILD_DISAGGREGATED // ---- 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; #endif while (!m_inFlightSubmits.empty() && m_inFlightSubmits.front().submitIndex <= submitIndex) { SubmitRecord record = m_inFlightSubmits.front(); m_inFlightSubmits.erase(m_inFlightSubmits.begin()); #if MOBILEGL_BUILD_DISAGGREGATED retiredAny = true; advanceTo = std::max(advanceTo, record.frameSerial); #else // Frame-serial completion piggybacks on submission completion. // NotifyFrameSerialComplete refuses the current (still-recording) // serial, so mid-frame flush records do not mark it early. m_bufferManager.NotifyFrameSerialComplete(record.frameSerial); #endif 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 MOBILEGL_BUILD_DISAGGREGATED 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(WireDeclineTally::UnsoundSerialCompleteEvents()), static_cast(advanceTo), static_cast(remaining.submitIndex), static_cast(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(submitIndex), static_cast(advanceTo), m_inFlightSubmits.size(), m_inFlightSubmits.empty() ? -1LL : static_cast(minRemaining), static_cast(m_bufferManager.GetFrameSerial())); } } m_bufferManager.NotifyFrameSerialComplete(advanceTo); } #endif // 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); #if MOBILEGL_BUILD_DISAGGREGATED CollectWireObjects(m_completedSubmitCounter); #endif } Bool VulkanRenderer::TryDrainFrameTransients() { #if MOBILEGL_BUILD_DISAGGREGATED if (m_wirePreparationDepth != 0) return false; #endif 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; } #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { // 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(); } #endif // 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; #if MOBILEGL_BUILD_DISAGGREGATED // 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(m_drainsSinceLastPresent), (m_drainsSinceLastPresent % 8) == 0 ? " ***FRAME-BOUNDARY-WORK***" : "", static_cast(m_submitCounter), static_cast(m_completedSubmitCounter)); } } #endif if ((m_drainsSinceLastPresent % 8) != 0) { return true; } if (m_textureManager) { m_textureManager->BeginFrame(frameIndex); } m_bufferManager.BeginFrame(frameIndex); // The cached conversion slices point into the transient arena the // BeginFrame above just rewound; drop them together. m_convertedVertexStreams.clear(); if (m_renderPassManager) { m_renderPassManager->OnPresent(); } // 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_vertexInputStateFactory) { m_vertexInputStateFactory->OnFrameBoundary(); } #if MOBILEGL_PIPE_PUSH // Reclaim {slot, gen} for objects that have not been drawn for a long time, on the same // cadence and the same retirement age as the entries those slots key. This is the // stand-in for the frontend death notification P2 has no hook for, and it is what keeps // the mint's footprint the LIVE working set rather than every object ever created // (review v2 MAJOR 1 / MAJOR 3). m_pipeIdentity.OnFrameBoundary(); #endif 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) { #if MOBILEGL_BUILD_DISAGGREGATED RetireWireDrawPass(); #endif // 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 = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; VkSemaphore waitSemaphore = frame.imageAvailableSemaphore; VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO}; if (!frame.imageAvailableSemaphoreConsumed) { 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; const VkResult result = vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, fence); if (result != VK_SUCCESS) { MGLOG_E_ONCE("SubmitPendingCommandBuffer: vkQueueSubmit returned %d", result); return false; } frame.imageAvailableSemaphoreConsumed = true; frame.hasCommandBufferRecorded = false; frame.hasPreCommandBufferRecorded = false; RegisterSubmit(fence, pooledFence); frame.lastSubmitIndex = m_submitCounter; 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 MOBILEGL_BUILD_DISAGGREGATED if (!frame.isCommandRecording && !frame.hasCommandBufferRecorded && !frame.isPreCommandRecording && !frame.hasPreCommandBufferRecorded) { #else if (!frame.isCommandRecording && !frame.hasCommandBufferRecorded) { #endif // 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 MOBILEGL_BUILD_DISAGGREGATED if (WaitForSubmitsUpTo(m_submitCounter, UINT64_MAX)) { // Every registered fence through this submission was waited. } else if (vkQueueWaitIdle(m_graphicsQueue) == VK_SUCCESS) { #else if (vkWaitForFences(m_device, 1, &fence, VK_TRUE, UINT64_MAX) == VK_SUCCESS) { OnSubmitsCompletedUpTo(m_submitCounter); } else if (vkQueueWaitIdle(m_graphicsQueue) == VK_SUCCESS) { #endif 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; } 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(); } 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 MOBILEGL_BUILD_DISAGGREGATED if (!WaitForSubmitsUpTo(submitIndex, timeoutNs)) return false; // A blocking wait can drain a present-less loop's frame transients. TryDrainFrameTransients(); return true; #else for (const auto& record : m_inFlightSubmits) { if (record.submitIndex >= submitIndex) { const VkResult result = vkWaitForFences(m_device, 1, &record.fence, VK_TRUE, timeoutNs); if (result == VK_SUCCESS) { OnSubmitsCompletedUpTo(record.submitIndex); // The wait already stalled the pipeline; if it happens to // have drained everything (present-less fence loops), take // the free frame-boundary drain. No-op otherwise. TryDrainFrameTransients(); return true; } if (result != VK_TIMEOUT) { MGLOG_E_ONCE("WaitForSubmitIndex: vkWaitForFences returned %d", result); } return false; } } // No in-flight record at or beyond the index: it was already observed // complete via a fence wait on a later submission. return true; #endif } void VulkanRenderer::OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) { // Dynamic state does not survive a command-buffer boundary. ResetDynamicStateShadow(); InvalidateSetupDrawSnapshots(); 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 (MGB_CTX_LIVE && MGB_CTX->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 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; } void VulkanRenderer::Present() { #if MOBILEGL_BUILD_DISAGGREGATED // 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(); // 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(); MGLOG_D("Present skipped: no usable swapchain (zero-area window)"); return; } m_presentSuspended = false; #if MOBILEGL_BUILD_DISAGGREGATED // 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)) MagmaWireFatal("deferred-acquire-submit-wait"); #endif 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"); m_renderPassManager->OnPresent(); // 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; InvalidateSetupDrawSnapshots(); } m_vertexInputStateFactory->OnFrameBoundary(); #if MOBILEGL_PIPE_PUSH m_pipeIdentity.OnFrameBoundary(); #endif m_samplerManager->OnFrameBoundary(); auto& frame = m_frameContext.GetCurrent(); auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); if (activeRenderPass) VkRenderPassManager::EndRenderPass(frame.commandBuffer); // 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); VK_VERIFY(vkQueueSubmit(m_graphicsQueue, 1, &submitPacket.submitInfo, frame.imageInFlightFence)); RegisterSubmit(frame.imageInFlightFence, /*pooledFence=*/false); frame.lastSubmitIndex = m_submitCounter; 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; } 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. 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; } if (m_swapchainResizeRequested) { MGLOG_D("Present, processing requested swapchain resize"); 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(); #if MOBILEGL_BUILD_DISAGGREGATED // 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)) MagmaWireFatal("present-slot-submit-wait"); #endif // 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); } 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()); #if MOBILEGL_BUILD_DISAGGREGATED // Its previous recording has completed. Drop cached attachment views // before the texture manager can release their retired images. ClearWireDrawPassCache(m_frameContext.GetCurrentFrameIndex()); #endif m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_convertedVertexStreams.clear(); // 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()); } void VulkanRenderer::CreateInstance() { #if defined(VK_USE_PLATFORM_METAL_EXT) // MoltenVK snapshots its configuration when the loader first discovers the ICD. Set // this before instance-extension enumeration, while preserving an explicit user value. if (std::getenv("MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS") == nullptr) { if (::setenv("MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS", "1", 0) == 0) { MGLOG_I("MoltenVK: enabling Metal argument buffers"); } else { MGLOG_W("MoltenVK: could not enable Metal argument buffers before ICD discovery"); } } #endif m_extensions = EnumerateInstanceExtensions(); MGLOG_I("Got %d Vulkan instance extensions: ", m_extensions.size()); for (auto& extension : m_extensions) { MGLOG_I(" %s (r.%u)", extension.extensionName, extension.specVersion); } Bool validationLayerAvailable = CheckValidationLayerSupport(); MGLOG_I("Validation layers %s.", validationLayerAvailable ? "available" : "not available"); MGLOG_I("Validation layers %s.", m_config.EnableValidationLayers ? "requested" : "not requested"); if (m_config.EnableValidationLayers && !validationLayerAvailable) { MGLOG_I("Validation layers not available! Disabling validation layers."); } m_validationLayersEnabled = m_config.EnableValidationLayers && validationLayerAvailable; // The debug messenger is a VK_EXT_debug_utils object, but a driver can ship // the validation layers while exposing only the older VK_EXT_debug_report // (Adreno 650 / Vulkan 1.1.128 does exactly that). Requesting the extension // unconditionally tripped the required-extension assert below, aborting every // validation-enabled build in CreateInstance. Keep the layers - they still // validate, and on Android they report to logcat on their own - and drop only // the messenger. const Bool debugUtilsAvailable = m_validationLayersEnabled && IsExtensionSupported(m_extensions, VK_EXT_DEBUG_UTILS_EXTENSION_NAME); // Without a reporting channel the layers validate but say nothing, so fall // back to VK_EXT_debug_report when debug_utils is missing. const Bool debugReportAvailable = m_validationLayersEnabled && !debugUtilsAvailable && IsExtensionSupported(m_extensions, VK_EXT_DEBUG_REPORT_EXTENSION_NAME); if (m_validationLayersEnabled && !debugUtilsAvailable) { MGLOG_I("%s not available; validation reports via %s instead.", VK_EXT_DEBUG_UTILS_EXTENSION_NAME, debugReportAvailable ? VK_EXT_DEBUG_REPORT_EXTENSION_NAME : "(no channel)"); } // ---------------- App info ------------------- VkApplicationInfo appInfo = {}; appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; appInfo.pApplicationName = m_config.AppName.c_str(); appInfo.applicationVersion = VK_MAKE_VERSION(m_config.CacheVersion, 0, 0); appInfo.pEngineName = "MobileGL"; appInfo.engineVersion = VK_MAKE_VERSION(m_config.Version.Major, m_config.Version.Minor, m_config.Version.Patch); #ifdef VK_USE_PLATFORM_WIN32_KHR appInfo.apiVersion = VK_API_VERSION_1_3; #else appInfo.apiVersion = VK_API_VERSION_1_1; #endif // ---------------- Instance info ------------------- VkInstanceCreateInfo instanceInfo = {}; instanceInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; instanceInfo.pApplicationInfo = &appInfo; // Extensions Vector exts = {VK_KHR_SURFACE_EXTENSION_NAME}; if (!m_window) { #ifdef VK_USE_PLATFORM_METAL_EXT exts.push_back(VK_EXT_METAL_SURFACE_EXTENSION_NAME); #elif defined VK_USE_PLATFORM_ANDROID_KHR m_headlessSurfaceSupported = IsExtensionSupported(m_extensions, VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME); if (m_headlessSurfaceSupported) { exts.push_back(VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME); } else { // No mobile ICD seen so far implements VK_EXT_headless_surface // (Mali r32p1 does not), and this used to abort the process the // moment an application asked for a pbuffer context. CreateSurface() // gives the WSI an AImageReader window instead, so request the // Android surface extension for it. MGLOG_I("%s not available; falling back to an AImageReader %s surface for the pbuffer context.", VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME); exts.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME); } #elif defined VK_USE_PLATFORM_XLIB_KHR // An offscreen surface has ZERO window-system dependence, by design and on // every machine - including ones that do have a display. There used to be a // fallback here that requested VK_KHR_xlib_surface and had CreateSurface() // open a hidden, never-mapped X window; it is gone. A pbuffer that quietly // needs an X server is a pbuffer that works on a workstation and dies on a // headless runner, which is exactly what it did: with no DISPLAY, XOpenDisplay // returned null and the next Xlib call segfaulted. If the loader genuinely has // no VK_EXT_headless_surface, that is an honest bring-up failure and is // reported as one below - never papered over with a window. m_headlessSurfaceSupported = IsExtensionSupported(m_extensions, VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME); if (!m_headlessSurfaceSupported) { MGLOG_F("%s is not available from this Vulkan loader, so an offscreen (pbuffer) DirectVulkan " "surface cannot be created. Refusing to substitute a window: offscreen surfaces must not " "depend on a window system. Install an ICD that implements it (lavapipe does).", VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME); throw RuntimeError("VK_EXT_headless_surface is unavailable for an offscreen DirectVulkan surface"); } exts.push_back(VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME); #else exts.push_back(VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME); #endif } else { #ifdef VK_USE_PLATFORM_ANDROID_KHR exts.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME); #elif defined VK_USE_PLATFORM_WIN32_KHR exts.push_back(VK_KHR_WIN32_SURFACE_EXTENSION_NAME); #elif defined VK_USE_PLATFORM_METAL_EXT exts.push_back(VK_EXT_METAL_SURFACE_EXTENSION_NAME); #elif defined VK_USE_PLATFORM_XLIB_KHR exts.push_back(VK_KHR_XLIB_SURFACE_EXTENSION_NAME); #else #warning "VulkanContext::CreateInstance: VK_KHR_*_surface extension not defined on this platform" #endif } // TODO: support more platforms #if defined(VK_USE_PLATFORM_METAL_EXT) if (IsExtensionSupported(m_extensions, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME)) { exts.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); instanceInfo.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; } else { MGLOG_I("Optional Vulkan instance extension not supported: %s", VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); } #endif if (debugUtilsAvailable) { exts.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); } else if (debugReportAvailable) { exts.push_back(VK_EXT_DEBUG_REPORT_EXTENSION_NAME); } MGLOG_I("Enabling %d Vulkan instance extensions:", exts.size()); for (const char* ext : exts) { MGLOG_I(" %s", ext); } for (const char* ext : exts) { if (!IsExtensionSupported(m_extensions, ext)) { MGLOG_E("Required Vulkan instance extension not found: %s", ext); } MOBILEGL_ASSERT(IsExtensionSupported(m_extensions, ext), "Required Vulkan instance extension not found: %s", ext); } instanceInfo.enabledExtensionCount = exts.size(); instanceInfo.ppEnabledExtensionNames = exts.data(); auto debugMessengerCreateInfo = PopulateDebugMessengerCreateInfo(); // Layers const void* instanceCreatePNext = nullptr; if (m_validationLayersEnabled) { MGLOG_I("Enabling validation layer..."); instanceInfo.enabledLayerCount = static_cast(std::size(s_validationLayerNames)); instanceInfo.ppEnabledLayerNames = s_validationLayerNames; // Chaining the messenger create-info is only legal with the extension on. instanceCreatePNext = debugUtilsAvailable ? &debugMessengerCreateInfo : nullptr; } else { instanceInfo.enabledLayerCount = 0; } instanceInfo.pNext = instanceCreatePNext; VK_VERIFY(vkCreateInstance(&instanceInfo, nullptr, &m_instance), "vkCreateInstance failed"); if (debugUtilsAvailable) { VK_VERIFY(SetupDebugMessenger()); } else if (debugReportAvailable) { VK_VERIFY(SetupDebugReportCallback()); } } static VKAPI_ATTR VkBool32 VKAPI_CALL DebugReportCallback(VkDebugReportFlagsEXT flags, VkDebugReportObjectTypeEXT, Uint64, size_t, Int32 messageCode, const char* pLayerPrefix, const char* pMessage, void*) { if ((flags & (VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT | VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT)) != 0) { // MGLOG_F, unlatched, on purpose: a validation-layer report means MobileGL fed // Vulkan something illegal, which is a broken invariant rather than an expected // failure mode. It stays loud and keeps repeating - the quietness rules that latch // W/E are for expected failures, not for this. The callback is only installed when // a build arms the debug report extension, so it costs shipping builds nothing. MGLOG_F("[Vulkan %s %d] %s", pLayerPrefix ? pLayerPrefix : "?", messageCode, pMessage ? pMessage : ""); } return VK_FALSE; } VkResult VulkanRenderer::SetupDebugReportCallback() { auto vkCreateDebugReportCallbackEXT = (PFN_vkCreateDebugReportCallbackEXT)vkGetInstanceProcAddr(m_instance, "vkCreateDebugReportCallbackEXT"); if (!vkCreateDebugReportCallbackEXT) return VK_ERROR_EXTENSION_NOT_PRESENT; VkDebugReportCallbackCreateInfoEXT createInfo{VK_STRUCTURE_TYPE_DEBUG_REPORT_CALLBACK_CREATE_INFO_EXT}; createInfo.flags = VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT | VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT; createInfo.pfnCallback = &DebugReportCallback; return vkCreateDebugReportCallbackEXT(m_instance, &createInfo, nullptr, &m_debugReportCallback); } void VulkanRenderer::DestroyDebugReportCallback() { if (m_debugReportCallback == VK_NULL_HANDLE) return; auto func = (PFN_vkDestroyDebugReportCallbackEXT)vkGetInstanceProcAddr(m_instance, "vkDestroyDebugReportCallbackEXT"); if (func != nullptr) func(m_instance, m_debugReportCallback, nullptr); m_debugReportCallback = VK_NULL_HANDLE; } VkResult VulkanRenderer::SetupDebugMessenger() { auto createInfo = PopulateDebugMessengerCreateInfo(); auto vkCreateDebugUtilsMessengerEXT = (PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(m_instance, "vkCreateDebugUtilsMessengerEXT"); if (!vkCreateDebugUtilsMessengerEXT) return VK_ERROR_EXTENSION_NOT_PRESENT; VK_VERIFY(vkCreateDebugUtilsMessengerEXT(m_instance, &createInfo, nullptr, &m_debugMessenger)); return VK_SUCCESS; } VkResult VulkanRenderer::DestroyDebugMessenger() { if (m_debugMessenger != VK_NULL_HANDLE) { auto func = (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(m_instance, "vkDestroyDebugUtilsMessengerEXT"); if (func != nullptr) { func(m_instance, m_debugMessenger, nullptr); } else { return VK_ERROR_EXTENSION_NOT_PRESENT; } } return VK_SUCCESS; } VkDebugUtilsMessengerCreateInfoEXT VulkanRenderer::PopulateDebugMessengerCreateInfo() { VkDebugUtilsMessengerCreateInfoEXT createInfo{}; createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; createInfo.pfnUserCallback = DebugCallback; createInfo.pUserData = this; return createInfo; } void VulkanRenderer::PickPhysicalDevice() { Uint32 deviceCount = 0; VK_VERIFY(vkEnumeratePhysicalDevices(m_instance, &deviceCount, nullptr)); if (deviceCount == 0) { // A real, reachable configuration, not a broken invariant: an instance can be // created from ICDs that load perfectly and then expose no device at all - a // GPU-less machine with the vendor ICDs installed (RADV/ANV/NVK on a CI runner) // is exactly that. It has to be a bring-up failure the caller can report. // // It used to be MGLOG_E + MOBILEGL_ASSERT, and back then BOTH were compiled out at // the INFO log level every shipping and CI build uses - the ordering bug that made // MGLOG_E dead at INFO was only fixed in 2026-08. The count-zero case therefore fell // through in silence to `devices[0]` on an EMPTY vector below and segfaulted in // vkGetPhysicalDeviceProperties. MGLOG_F stays: E is live now, but MOBILEGL_ASSERT // is still DEBUG-only and this is a genuine bring-up abort, not a recoverable error. MGLOG_F("No Vulkan physical devices found: the instance loaded ICDs but none of them exposes a " "device. Cannot bring up DirectVulkan. (A software ICD such as lavapipe provides one; " "pin it with VK_ICD_FILENAMES if the machine has no GPU.)"); throw RuntimeError("No Vulkan physical devices available for DirectVulkan"); } MGLOG_I("Found %d physical device(s).", deviceCount); Vector devices(deviceCount); // Same truncation hazard as the instance-extension enumeration: a VK_INCOMPLETE here // leaves the tail of `devices` default-constructed (VK_NULL_HANDLE), and every one of // those is a null handle waiting to be passed to the driver. Take only what was // actually written. const VkResult enumerateResult = vkEnumeratePhysicalDevices(m_instance, &deviceCount, devices.data()); if (enumerateResult != VK_SUCCESS && enumerateResult != VK_INCOMPLETE) { VK_VERIFY(enumerateResult, "vkEnumeratePhysicalDevices failed"); } devices.resize(deviceCount); if (devices.empty()) { MGLOG_F("vkEnumeratePhysicalDevices reported devices and then wrote none"); throw RuntimeError("No Vulkan physical devices available for DirectVulkan"); } for (Int i = 0; i < deviceCount; i++) { if (GetMoreCapablePhysicalDevice(devices[i], m_surface, m_physicalDevice, m_physicalDevice)) MGLOG_I("Picked physical device %d.", i); } if (m_physicalDevice.handle == VK_NULL_HANDLE) { m_physicalDevice.handle = devices[0]; vkGetPhysicalDeviceProperties(devices[0], &m_physicalDevice.properties); MGLOG_I("No suitable physical device picked yet, defaulting to device 0."); MGLOG_W("No graphics queue found on physical device. Picking a device that doesn't do graphics?"); } } Bool VulkanRenderer::GetMoreCapablePhysicalDevice(VkPhysicalDevice newVkDevice, VkSurfaceKHR surface, const PhysicalDevice& otherDevice, PhysicalDevice& outBetterDevice) { const auto deviceTypeToStr = [](VkPhysicalDeviceType type) { switch (type) { case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return "INTEGRATED_GPU"; case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return "DISCRETE_GPU"; case VK_PHYSICAL_DEVICE_TYPE_CPU: return "CPU"; case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return "VIRTUAL_GPU"; case VK_PHYSICAL_DEVICE_TYPE_OTHER: return "OTHER"; default: return "UNKNOWN"; } }; PhysicalDevice newDevice; newDevice.handle = newVkDevice; vkGetPhysicalDeviceProperties(newVkDevice, &newDevice.properties); const auto& deviceProperties = newDevice.properties; auto apiVersion = deviceProperties.apiVersion; MGLOG_I(" %s (Vulkan %d.%d.%d, %s)", deviceProperties.deviceName, VK_VERSION_MAJOR(apiVersion), VK_VERSION_MINOR(apiVersion), VK_VERSION_PATCH(apiVersion), deviceTypeToStr(deviceProperties.deviceType)); // Check device extensions (including swapchain extension) Bool deviceExtSupported = IsNecessaryDeviceExtensionSupported(newVkDevice); if (!deviceExtSupported) { outBetterDevice = otherDevice; MGLOG_I(" Ignored physical device. (Reason: Some of the required device extension not supported on this " "device)"); return false; } // Check swapchain capabilities auto swapchainCapabilities = SwapchainObject::GetSwapchainCapabilities(newVkDevice, surface); if (!swapchainCapabilities.IsComplete()) { outBetterDevice = otherDevice; MGLOG_I(" Ignored physical device. (Reason: Swapchain capabilities not met)"); return false; } // Check queue families Vector queueFamilies = GetQueueFamilyFromPhysicalDevice(newVkDevice); newDevice.queueFamilies.graphicsFamily = GetQueueFamilyIndex(queueFamilies, VK_QUEUE_GRAPHICS_BIT); if (newDevice.queueFamilies.graphicsFamily == -1) { outBetterDevice = otherDevice; MGLOG_I(" Ignored physical device. (Reason: No graphics queue family)"); return false; } newDevice.queueFamilies.presentFamily = GetPresentQueueFamilyIndex(newDevice, surface, queueFamilies, newDevice.queueFamilies.graphicsFamily); if (newDevice.queueFamilies.presentFamily == -1) { outBetterDevice = otherDevice; MGLOG_I(" Ignored physical device. (Reason: No present queue family)"); return false; } // Accept software/virtual/other devices when no discrete or integrated GPU // has been selected yet. This is important for Linux headless CI using lavapipe. if (!otherDevice.IsComplete()) { outBetterDevice = newDevice; MGLOG_I(" Picked physical device. (Reason: First suitable device)"); return true; } // Pick discrete GPU if (newDevice.properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU && otherDevice.properties.deviceType != VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { outBetterDevice = newDevice; MGLOG_I(" Picked physical device. (Reason: Discrete GPU)"); return true; } // Pick integrated GPU if no discrete GPU if (newDevice.properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU && otherDevice.properties.deviceType != VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { outBetterDevice = newDevice; MGLOG_I(" Picked physical device. (Reason: Integrated GPU and no discrete one found yet)"); return true; } // Ignore other GPU when discrete GPU found if (newDevice.properties.deviceType != VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU && otherDevice.properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { outBetterDevice = otherDevice; MGLOG_I(" Ignored physical device. (Reason: Already picked discrete GPU)"); return false; } return false; } Bool VulkanRenderer::IsNecessaryDeviceExtensionSupported(VkPhysicalDevice device) { const Vector availableExtensions = EnumerateDeviceExtensions(device); MGLOG_I("Got %u Vulkan device extensions: ", static_cast(availableExtensions.size())); for (auto& extension : availableExtensions) { MGLOG_I(" %s (r.%u)", extension.extensionName, extension.specVersion); } for (SizeT i = 0; i < std::size(s_deviceExtensionNames); ++i) { if (!IsExtensionSupported(availableExtensions, s_deviceExtensionNames[i])) { MGLOG_I("Required extension not found: %s", s_deviceExtensionNames[i]); return false; } MGLOG_I("Required extension found: %s", s_deviceExtensionNames[i]); } return true; } void VulkanRenderer::CreateLogicalDeviceAndQueues() { Float queuePriority = 1.0f; Vector queueCreateInfos; MOBILEGL_ASSERT(m_physicalDevice.queueFamilies.graphicsFamily != -1, "Graphics queue family not found."); VkDeviceQueueCreateInfo& gfxQueueCreateInfo = queueCreateInfos.emplace_back(); gfxQueueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; gfxQueueCreateInfo.queueFamilyIndex = m_physicalDevice.queueFamilies.graphicsFamily; gfxQueueCreateInfo.queueCount = 1; gfxQueueCreateInfo.pQueuePriorities = &queuePriority; if (m_physicalDevice.queueFamilies.graphicsFamily != m_physicalDevice.queueFamilies.presentFamily) { MOBILEGL_ASSERT(m_physicalDevice.queueFamilies.presentFamily != -1, "Present queue family not found."); VkDeviceQueueCreateInfo& presentQueueCreateInfo = queueCreateInfos.emplace_back(); presentQueueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; presentQueueCreateInfo.queueFamilyIndex = m_physicalDevice.queueFamilies.presentFamily; presentQueueCreateInfo.queueCount = 1; presentQueueCreateInfo.pQueuePriorities = &queuePriority; } VkPhysicalDeviceFeatures supportedDeviceFeatures{}; vkGetPhysicalDeviceFeatures(m_physicalDevice.handle, &supportedDeviceFeatures); VkPhysicalDeviceFeatures deviceFeatures{}; // Match GL's robust buffer-fetch behavior where the Vulkan device supports it. This covers // out-of-range fetches; arbitrary GL vertex strides/offsets still need the explicit tight // repack in VertexInputStateFactory when they violate Vulkan's address-alignment rules. // MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS leaves it off to measure or dodge its GPU cost. deviceFeatures.robustBufferAccess = MG_Config::Features.MagmaDisableRobustBufferAccess ? VK_FALSE : supportedDeviceFeatures.robustBufferAccess; deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader; deviceFeatures.tessellationShader = supportedDeviceFeatures.tessellationShader; // gl_PointSize is an ORDINARY per-vertex output in desktop GL - a tessellation // evaluation or geometry shader may write it, and a program may capture it by name - // but in Vulkan the PointSize built-in is only usable from those two stages when this // feature is on (VUID-RuntimeSpirv-PointSize-06439; SPIR-V spells the requirement as // the TessellationPointSize / GeometryPointSize capabilities, which glslang emits from // any such write). Left off, every one of those programs is invalid usage that a lenient // driver silently gives an undefined point size and a strict one faults on. Nothing here // asks for it speculatively: the feature is taken only where the device advertises it. deviceFeatures.shaderTessellationAndGeometryPointSize = supportedDeviceFeatures.shaderTessellationAndGeometryPointSize; m_tessellationAndGeometryPointSizeFeatureEnabled = deviceFeatures.shaderTessellationAndGeometryPointSize == VK_TRUE; // Sampled-read barriers may only name the shader stages whose device feature is // actually enabled (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), so the // mask is assembled here, next to the feature decision, and handed to consumers. m_sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT; if (deviceFeatures.geometryShader == VK_TRUE) { m_sampledReadStageMask |= VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT; } if (deviceFeatures.tessellationShader == VK_TRUE) { m_sampledReadStageMask |= VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT | VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT; } deviceFeatures.independentBlend = supportedDeviceFeatures.independentBlend; m_independentBlendFeatureEnabled = deviceFeatures.independentBlend == VK_TRUE; deviceFeatures.fillModeNonSolid = supportedDeviceFeatures.fillModeNonSolid; m_fillModeNonSolidFeatureEnabled = deviceFeatures.fillModeNonSolid == VK_TRUE; deviceFeatures.dualSrcBlend = supportedDeviceFeatures.dualSrcBlend; m_dualSrcBlendFeatureEnabled = deviceFeatures.dualSrcBlend == VK_TRUE; // ARB_sample_shading. Without this feature a pipeline may not set sampleShadingEnable // (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784), so the GL enable // has to be dropped rather than forwarded - which is what the flag below records. deviceFeatures.sampleRateShading = supportedDeviceFeatures.sampleRateShading; m_sampleRateShadingFeatureEnabled = deviceFeatures.sampleRateShading == VK_TRUE; // ARB_viewport_array rasterization. Without multiViewport a pipeline may declare exactly // one viewport (VUID-VkPipelineViewportStateCreateInfo-viewportCount-01216), so a shader's // gl_ViewportIndex can only ever select viewport 0 and the other fifteen rectangles are // state with nowhere to go. The GL state stays 16 wide either way - GL 4.3 core requires // MAX_VIEWPORTS >= 16 and that is a frontend promise, not a device one; this gate decides // only whether a DRAW can rasterize into more than one of them. deviceFeatures.multiViewport = supportedDeviceFeatures.multiViewport; m_multiViewportFeatureEnabled = deviceFeatures.multiViewport == VK_TRUE; m_maxRasterizableViewports = m_multiViewportFeatureEnabled ? std::min(RenderStateParameters::MAX_VIEWPORTS, std::max(m_physicalDevice.properties.limits.maxViewports, 1u)) : 1u; MGLOG_I("Vulkan: multiViewport %s; rasterizable viewports=%u (device limit %u, GL state width %u)", m_multiViewportFeatureEnabled ? "enabled" : "UNAVAILABLE", m_maxRasterizableViewports, m_physicalDevice.properties.limits.maxViewports, static_cast(RenderStateParameters::MAX_VIEWPORTS)); if (!m_multiViewportFeatureEnabled) { MGLOG_W("Vulkan: the device does not support the multiViewport feature; gl_ViewportIndex will always " "select viewport 0 and per-viewport scissor/depth-range state past index 0 cannot be " "rasterized (the state itself is still stored and queryable)"); } deviceFeatures.logicOp = supportedDeviceFeatures.logicOp; deviceFeatures.shaderClipDistance = supportedDeviceFeatures.shaderClipDistance; deviceFeatures.shaderCullDistance = supportedDeviceFeatures.shaderCullDistance; deviceFeatures.wideLines = supportedDeviceFeatures.wideLines; m_logicOpFeatureEnabled = deviceFeatures.logicOp == VK_TRUE; deviceFeatures.shaderInt64 = supportedDeviceFeatures.shaderInt64; // Required for any module that declares OpCapability Float64 - which is every shader with a // double in it, including the 64-bit vertex attribute path (the attribute itself arrives as // uint32 words, but the bitcast result and everything computed from it is Float64). Without // it vkCreateShaderModule is invalid usage (VUID-VkShaderModuleCreateInfo-pCode-08740), // which is why SupportsFloat64VertexAttributes gates the entry point on the same feature. deviceFeatures.shaderFloat64 = supportedDeviceFeatures.shaderFloat64; // Required before a VK_IMAGE_VIEW_TYPE_CUBE_ARRAY view may be created // (VUID-VkImageViewCreateInfo-viewType-01004). Without it a cube map array texture cannot // get its sampled or full view, so SyncTextureResource fails and the texture stays unbacked. deviceFeatures.imageCubeArray = supportedDeviceFeatures.imageCubeArray; // Required for desktop GL image load/store semantics. iterationRP writes storage // images from vertex and fragment stages and uses formats outside Vulkan's small // mandatory storage-image set. deviceFeatures.vertexPipelineStoresAndAtomics = supportedDeviceFeatures.vertexPipelineStoresAndAtomics; deviceFeatures.fragmentStoresAndAtomics = supportedDeviceFeatures.fragmentStoresAndAtomics; deviceFeatures.shaderStorageImageExtendedFormats = supportedDeviceFeatures.shaderStorageImageExtendedFormats; // The formatless float-storage compatibility path must be all-or-nothing: transformed // modules declare both capabilities and image bindings may be read, written, or both. m_unformattedFloatStorageImagesEnabled = supportedDeviceFeatures.shaderStorageImageReadWithoutFormat == VK_TRUE && supportedDeviceFeatures.shaderStorageImageWriteWithoutFormat == VK_TRUE; if (m_unformattedFloatStorageImagesEnabled) { deviceFeatures.shaderStorageImageReadWithoutFormat = VK_TRUE; deviceFeatures.shaderStorageImageWriteWithoutFormat = VK_TRUE; } else { // Surface the degradation instead of failing silently: shader packs that bind a // float storage image with a format different from its declaration (e.g. // iterationRP) will render incorrectly on this device. MGLOG_W("CreateLogicalDeviceAndQueues: shaderStorageImage*WithoutFormat unavailable " "(read=%d write=%d); float storage-image format reinterpretation is disabled " "and packs relying on it may misrender", supportedDeviceFeatures.shaderStorageImageReadWithoutFormat, supportedDeviceFeatures.shaderStorageImageWriteWithoutFormat); } deviceFeatures.drawIndirectFirstInstance = supportedDeviceFeatures.drawIndirectFirstInstance; m_drawIndirectFirstInstanceFeatureEnabled = deviceFeatures.drawIndirectFirstInstance == VK_TRUE; deviceFeatures.multiDrawIndirect = supportedDeviceFeatures.multiDrawIndirect; m_multiDrawIndirectFeatureEnabled = deviceFeatures.multiDrawIndirect == VK_TRUE; m_logicOpFeatureEnabled = deviceFeatures.logicOp == VK_TRUE; // Backs GL_TEXTURE_MAX_ANISOTROPY_EXT; optional in Vulkan, so the sampler manager falls back // to isotropic filtering (and the extension goes unadvertised) when the device lacks it. deviceFeatures.samplerAnisotropy = supportedDeviceFeatures.samplerAnisotropy; m_samplerAnisotropyFeatureEnabled = deviceFeatures.samplerAnisotropy == VK_TRUE; // GL_SAMPLES_PASSED needs exact sample counts; without the feature the boolean // occlusion result still satisfies any-samples-style consumers. deviceFeatures.occlusionQueryPrecise = supportedDeviceFeatures.occlusionQueryPrecise; m_occlusionQueryPreciseEnabled = deviceFeatures.occlusionQueryPrecise == VK_TRUE; m_tessellationShaderFeatureEnabled = deviceFeatures.tessellationShader == VK_TRUE; // Backs the GL_PRIMITIVES_GENERATED reroute's statistics tier (see the // m_primGenReroute* members): a VK_QUERY_TYPE_PIPELINE_STATISTICS pool may only // be created with this feature enabled. Enabled wherever the device has it - the // feature alone costs nothing; pools exist only where the reroute is armed. deviceFeatures.pipelineStatisticsQuery = supportedDeviceFeatures.pipelineStatisticsQuery; m_pipelineStatisticsQueryFeatureEnabled = deviceFeatures.pipelineStatisticsQuery == VK_TRUE; VkDeviceCreateInfo deviceCreateInfo{}; deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; deviceCreateInfo.pQueueCreateInfos = queueCreateInfos.data(); deviceCreateInfo.queueCreateInfoCount = queueCreateInfos.size(); deviceCreateInfo.pEnabledFeatures = &deviceFeatures; if (m_validationLayersEnabled) { deviceCreateInfo.enabledLayerCount = static_cast(std::size(s_validationLayerNames)); deviceCreateInfo.ppEnabledLayerNames = s_validationLayerNames; } else { deviceCreateInfo.enabledLayerCount = 0; } Vector enabledDeviceExtensions; enabledDeviceExtensions.reserve(std::size(s_deviceExtensionNames) + 2); for (const char* extensionName : s_deviceExtensionNames) { enabledDeviceExtensions.push_back(extensionName); } const Vector availableExtensions = EnumerateDeviceExtensions(m_physicalDevice.handle); ResolveOptionalDeviceExtensions(availableExtensions, enabledDeviceExtensions); // VK_KHR_image_format_list lets a MUTABLE_FORMAT image declare exactly which formats it // may be viewed as. Adreno drops UBWC bandwidth compression on a blindly-mutable image // (measured: 65 -> 80 fps in MC 26.2 once mutability is not requested); an explicit, // compression-compatible format list is the portable way to keep both. m_imageFormatListExtensionEnabled = IsExtensionSupported(availableExtensions, VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME); if (m_imageFormatListExtensionEnabled) { enabledDeviceExtensions.push_back(VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME); } MGLOG_I("VK_KHR_image_format_list enabled: %s", m_imageFormatListExtensionEnabled ? "true" : "false"); MGLOG_I("VK_KHR_draw_indirect_count enabled: %s", m_drawIndirectCountExtensionEnabled ? "true" : "false"); m_indexTypeUint8ExtensionEnabled = false; const char* indexTypeUint8ExtensionName = nullptr; if (IsExtensionSupported(availableExtensions, VK_KHR_INDEX_TYPE_UINT8_EXTENSION_NAME)) { indexTypeUint8ExtensionName = VK_KHR_INDEX_TYPE_UINT8_EXTENSION_NAME; } else if (IsExtensionSupported(availableExtensions, VK_EXT_INDEX_TYPE_UINT8_EXTENSION_NAME)) { indexTypeUint8ExtensionName = VK_EXT_INDEX_TYPE_UINT8_EXTENSION_NAME; } auto getPhysicalDeviceFeatures2 = reinterpret_cast( vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceFeatures2")); if (getPhysicalDeviceFeatures2 == nullptr) { getPhysicalDeviceFeatures2 = reinterpret_cast( vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceFeatures2KHR")); } m_updateAfterBindLimits = {}; VkPhysicalDeviceDescriptorIndexingFeatures descriptorIndexingFeatures{}; descriptorIndexingFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES; VkPhysicalDeviceDescriptorIndexingProperties descriptorIndexingProperties{}; descriptorIndexingProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_PROPERTIES; const Bool descriptorIndexingCore = #if MOBILEGL_BUILD_DISAGGREGATED && !defined(VK_USE_PLATFORM_WIN32_KHR) // CreateInstance requests Vulkan 1.1 on these platforms. A 1.2+ GPU // does not promote descriptor indexing into that application's core // API; the wire path must enable VK_EXT_descriptor_indexing instead. (MG_Config::Transport == MG_Config::TransportMode::Monolith) && #endif m_physicalDevice.properties.apiVersion >= VK_API_VERSION_1_2; const Bool descriptorIndexingExtension = IsExtensionSupported(availableExtensions, VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME); auto getPhysicalDeviceProperties2 = reinterpret_cast( vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2")); if (getPhysicalDeviceProperties2 == nullptr) { getPhysicalDeviceProperties2 = reinterpret_cast( vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2KHR")); } #if MOBILEGL_BUILD_DISAGGREGATED // Linux/Android request Vulkan 1.1: a 1.2 physical device alone does // not expose the promoted renderpass2/depth-resolve API to this app. #ifdef VK_USE_PLATFORM_WIN32_KHR const Bool wireDepthResolveCore = m_physicalDevice.properties.apiVersion >= VK_API_VERSION_1_2; #else const Bool wireDepthResolveCore = false; #endif const Bool wireDepthResolveExtensions = m_physicalDevice.properties.apiVersion >= VK_API_VERSION_1_1 && IsExtensionSupported(availableExtensions, VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME) && IsExtensionSupported(availableExtensions, VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME); const Bool wireDepthResolveEnabled = MG_Config::Transport != MG_Config::TransportMode::Monolith && getPhysicalDeviceProperties2 && (wireDepthResolveCore || wireDepthResolveExtensions); m_wireCreateRenderPass2 = nullptr; m_wireDepthResolveModes = m_wireStencilResolveModes = 0; if (wireDepthResolveEnabled) { if (!wireDepthResolveCore) { EnableOptionalDeviceExtension(availableExtensions, enabledDeviceExtensions, VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME); EnableOptionalDeviceExtension(availableExtensions, enabledDeviceExtensions, VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME); } VkPhysicalDeviceDepthStencilResolveProperties resolveProperties{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEPTH_STENCIL_RESOLVE_PROPERTIES}; VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2}; properties.pNext = &resolveProperties; getPhysicalDeviceProperties2(m_physicalDevice.handle, &properties); m_wireDepthResolveModes = resolveProperties.supportedDepthResolveModes; m_wireStencilResolveModes = resolveProperties.supportedStencilResolveModes; } // P7 wave 2-B2, CONTRACT-P7 ยง3.2 (`multisample-blit-aspect`): the shader resolve's // stencil half needs SPV_EXT_shader_stencil_export, because a fragment shader cannot // write the stencil aspect without it. Enabled on the same terms as the resolve // extensions above - wire arms only - and the arm declines stencil where it is absent // rather than producing an undefined aspect. m_wireShaderStencilExport = MG_Config::Transport != MG_Config::TransportMode::Monolith && IsExtensionSupported(availableExtensions, VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME); if (m_wireShaderStencilExport) EnableOptionalDeviceExtension(availableExtensions, enabledDeviceExtensions, VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME); // P7 gate 5 (g5-msrbo): the Adreno's no-draw depth/stencil resolve pass writes nothing, // so the shader resolve goes first there (WireDepthResolveArm.h says why and where). // // THE VENDOR THE POLICY READS CAN BE SUBSTITUTED, AND ONLY HERE (review round). No host // lane runs on a Qualcomm device, so the order this line picks for the Redmi ran nowhere // a lane could see it: dropping this assignment, or the `m_wirePreferShaderDepthResolve ||` // in ResolveWireDepthStencil, left every lane green and put // KHR-GL46.direct_state_access.renderbuffers_storage_multisample back at 54 Fail lines on // the device. MGITEST_MAGMA_DEPTH_RESOLVE_VENDOR_ID (strtoul base 0, so `0x5143`) replaces // the id handed to WirePrefersShaderDepthResolve and nothing else - no other vendor // check reads it - and the DirectVulkan.{Split,Spawn}.MsResolveQ./MsFlipQ. entries set it // to Qualcomm's id and read ResolveWireDepthStencil's arm line back from the server log. Uint32 depthResolvePolicyVendor = m_physicalDevice.properties.vendorID; if (const char* substitute = std::getenv("MGITEST_MAGMA_DEPTH_RESOLVE_VENDOR_ID"); substitute != nullptr && *substitute != '\0') depthResolvePolicyVendor = static_cast(std::strtoul(substitute, nullptr, 0)); m_wirePreferShaderDepthResolve = MG_Config::Transport != MG_Config::TransportMode::Monolith && WirePrefersShaderDepthResolve(depthResolvePolicyVendor); if (m_wirePreferShaderDepthResolve) MGLOG_I("DirectVulkan: vendor 0x%x (policy vendor 0x%x) - the wire arm resolves multisample " "depth/stencil with the shader pass first (the no-draw resolve render pass is the fallback)", m_physicalDevice.properties.vendorID, depthResolvePolicyVendor); #endif if ((descriptorIndexingCore || descriptorIndexingExtension) && getPhysicalDeviceFeatures2 != nullptr && getPhysicalDeviceProperties2 != nullptr) { VkPhysicalDeviceFeatures2 featureQuery{}; featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; featureQuery.pNext = &descriptorIndexingFeatures; getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery); VkPhysicalDeviceProperties2 propertyQuery{}; propertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; propertyQuery.pNext = &descriptorIndexingProperties; getPhysicalDeviceProperties2(m_physicalDevice.handle, &propertyQuery); // This renderer emits every descriptor category listed below, including // dynamic UBOs and combined image samplers. Do not enable a partial // descriptor-indexing contract: it would make a later reflected program // fail in the driver instead of choosing its ordinary descriptor layout. const Bool allUpdateAfterBindFeatures = descriptorIndexingFeatures.descriptorBindingUniformBufferUpdateAfterBind == VK_TRUE && descriptorIndexingFeatures.descriptorBindingSampledImageUpdateAfterBind == VK_TRUE && descriptorIndexingFeatures.descriptorBindingStorageImageUpdateAfterBind == VK_TRUE && descriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind == VK_TRUE && descriptorIndexingFeatures.descriptorBindingUniformTexelBufferUpdateAfterBind == VK_TRUE && descriptorIndexingFeatures.descriptorBindingStorageTexelBufferUpdateAfterBind == VK_TRUE && (!deviceFeatures.robustBufferAccess || descriptorIndexingProperties.robustBufferAccessUpdateAfterBind); if (allUpdateAfterBindFeatures) { if (!descriptorIndexingCore && !IsExtensionAlreadyEnabled( enabledDeviceExtensions, VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME)) { enabledDeviceExtensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME); } descriptorIndexingFeatures.pNext = const_cast(deviceCreateInfo.pNext); deviceCreateInfo.pNext = &descriptorIndexingFeatures; m_updateAfterBindLimits = { true, descriptorIndexingProperties.maxPerStageDescriptorUpdateAfterBindSamplers, descriptorIndexingProperties.maxPerStageDescriptorUpdateAfterBindUniformBuffers, descriptorIndexingProperties.maxPerStageDescriptorUpdateAfterBindStorageBuffers, descriptorIndexingProperties.maxPerStageDescriptorUpdateAfterBindSampledImages, descriptorIndexingProperties.maxPerStageDescriptorUpdateAfterBindStorageImages, descriptorIndexingProperties.maxPerStageUpdateAfterBindResources, descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindSamplers, descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindUniformBuffers, descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindUniformBuffersDynamic, descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindStorageBuffers, descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindStorageBuffersDynamic, descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindSampledImages, descriptorIndexingProperties.maxDescriptorSetUpdateAfterBindStorageImages}; MGLOG_I("Vulkan: update-after-bind descriptor layouts enabled"); } else { MGLOG_I("Vulkan: descriptor indexing is present but lacks the complete update-after-bind feature set; " "using ordinary descriptor layouts"); } } else { MGLOG_I("Vulkan: descriptor indexing unavailable; using ordinary descriptor layouts"); } VkPhysicalDeviceIndexTypeUint8Features indexTypeUint8Features{}; indexTypeUint8Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_INDEX_TYPE_UINT8_FEATURES; if (indexTypeUint8ExtensionName != nullptr) { VkPhysicalDeviceFeatures2 featureQuery{}; featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; featureQuery.pNext = &indexTypeUint8Features; MOBILEGL_ASSERT(getPhysicalDeviceFeatures2 != nullptr, "CreateLogicalDeviceAndQueues: vkGetPhysicalDeviceFeatures2 is unavailable"); getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery); if (indexTypeUint8Features.indexTypeUint8 == VK_TRUE) { if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, indexTypeUint8ExtensionName)) { enabledDeviceExtensions.push_back(indexTypeUint8ExtensionName); } m_indexTypeUint8ExtensionEnabled = true; indexTypeUint8Features.pNext = const_cast(deviceCreateInfo.pNext); deviceCreateInfo.pNext = &indexTypeUint8Features; MGLOG_I("Enabled optional device extension: %s", indexTypeUint8ExtensionName); } else { MGLOG_W("%s is advertised, but indexTypeUint8 feature is unavailable; uint8 index buffers will stay disabled", indexTypeUint8ExtensionName); } } else { MGLOG_W("VK_KHR_index_type_uint8 / VK_EXT_index_type_uint8 not supported; uint8 index buffers will stay disabled"); } m_shaderDrawParametersFeatureEnabled = false; VkPhysicalDeviceShaderDrawParametersFeatures shaderDrawParametersFeatures{}; shaderDrawParametersFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES; if (m_physicalDevice.properties.apiVersion >= VK_API_VERSION_1_1 && getPhysicalDeviceFeatures2 != nullptr) { VkPhysicalDeviceFeatures2 featureQuery{}; featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; featureQuery.pNext = &shaderDrawParametersFeatures; getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery); if (shaderDrawParametersFeatures.shaderDrawParameters == VK_TRUE) { shaderDrawParametersFeatures.pNext = const_cast(deviceCreateInfo.pNext); deviceCreateInfo.pNext = &shaderDrawParametersFeatures; m_shaderDrawParametersFeatureEnabled = true; } } else if (m_shaderDrawParametersExtensionEnabled) { // Vulkan 1.0 device: enabling VK_KHR_shader_draw_parameters alone exposes the SPIR-V // DrawParameters capability; the shaderDrawParameters feature struct only exists from 1.1. m_shaderDrawParametersFeatureEnabled = true; } if (!m_shaderDrawParametersFeatureEnabled) { MGLOG_W("shaderDrawParameters is unavailable; shaders using gl_DrawID/gl_BaseInstance will not work"); } // primitiveTopologyListRestart lets primitive restart work on *list* topologies (strip/fan // restart needs no feature). Optional; enabled via VK_EXT_primitive_topology_list_restart. m_primitiveTopologyListRestartFeatureEnabled = false; VkPhysicalDevicePrimitiveTopologyListRestartFeaturesEXT listRestartFeatures{}; listRestartFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVE_TOPOLOGY_LIST_RESTART_FEATURES_EXT; if (IsExtensionSupported(availableExtensions, VK_EXT_PRIMITIVE_TOPOLOGY_LIST_RESTART_EXTENSION_NAME) && getPhysicalDeviceFeatures2 != nullptr) { VkPhysicalDeviceFeatures2 featureQuery{}; featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; featureQuery.pNext = &listRestartFeatures; getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery); if (listRestartFeatures.primitiveTopologyListRestart == VK_TRUE) { if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, VK_EXT_PRIMITIVE_TOPOLOGY_LIST_RESTART_EXTENSION_NAME)) { enabledDeviceExtensions.push_back(VK_EXT_PRIMITIVE_TOPOLOGY_LIST_RESTART_EXTENSION_NAME); } listRestartFeatures.pNext = const_cast(deviceCreateInfo.pNext); deviceCreateInfo.pNext = &listRestartFeatures; m_primitiveTopologyListRestartFeatureEnabled = true; MGLOG_I("Enabled optional device extension: %s", VK_EXT_PRIMITIVE_TOPOLOGY_LIST_RESTART_EXTENSION_NAME); } } // VK_EXT_custom_border_color: an arbitrary GL_TEXTURE_BORDER_COLOR, in float or integer form, // instead of the four predefined VkBorderColor values. Without it a border outside // transparent black / opaque black / opaque white has to be snapped, which is what made every // border texel of a GL_RGBA8 texture with border (255,255,255,255) sample as 0 and what made // an integer border of -1 come back as 0. // // customBorderColorWithoutFormat is required alongside customBorderColors, not merely // preferred: a GL sampler object carries a border colour with no idea which texture it will // be paired with, so the VkSamplerCustomBorderColorCreateInfoEXT this backend builds has to // leave `format` VK_FORMAT_UNDEFINED. m_customBorderColorFeatureEnabled = false; m_maxCustomBorderColorSamplers = 0; VkPhysicalDeviceCustomBorderColorFeaturesEXT customBorderColorFeatures{}; customBorderColorFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CUSTOM_BORDER_COLOR_FEATURES_EXT; if (IsExtensionSupported(availableExtensions, VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME) && getPhysicalDeviceFeatures2 != nullptr) { VkPhysicalDeviceFeatures2 featureQuery{}; featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; featureQuery.pNext = &customBorderColorFeatures; getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery); if (customBorderColorFeatures.customBorderColors == VK_TRUE && customBorderColorFeatures.customBorderColorWithoutFormat == VK_TRUE) { if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME)) { enabledDeviceExtensions.push_back(VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME); } customBorderColorFeatures.pNext = const_cast(deviceCreateInfo.pNext); deviceCreateInfo.pNext = &customBorderColorFeatures; m_customBorderColorFeatureEnabled = true; if (getPhysicalDeviceProperties2 != nullptr) { VkPhysicalDeviceCustomBorderColorPropertiesEXT customBorderColorProperties{}; customBorderColorProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CUSTOM_BORDER_COLOR_PROPERTIES_EXT; VkPhysicalDeviceProperties2 propertyQuery{}; propertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; propertyQuery.pNext = &customBorderColorProperties; getPhysicalDeviceProperties2(m_physicalDevice.handle, &propertyQuery); m_maxCustomBorderColorSamplers = customBorderColorProperties.maxCustomBorderColorSamplers; } MGLOG_I("Enabled optional device extension: %s (maxCustomBorderColorSamplers=%u)", VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME, m_maxCustomBorderColorSamplers); } } if (!m_customBorderColorFeatureEnabled) { MGLOG_I("%s unavailable; GL_TEXTURE_BORDER_COLOR snaps to the nearest predefined VkBorderColor", VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME); } // Native subgroup topology, and VK_EXT_subgroup_size_control's // computeFullSubgroups feature. REQUIRE_FULL_SUBGROUPS on a compute stage is what // turns the derived gl_NumSubgroups (DeriveNumSubgroupsPass) from // encouraged-but-unspecified driver behaviour into a spec guarantee: with the bit // set and local_size_x a multiple of the subgroup size, every subgroup launches // full, so the subgroup count is exactly invocations / size ("Full Subgroups", // VUID-VkPipelineShaderStageCreateInfo-flags-02759/-02785). m_nativeSubgroupSize = 0; m_nativeSubgroupSupported = false; m_computeFullSubgroupsFeatureEnabled = false; if (getPhysicalDeviceProperties2 != nullptr) { VkPhysicalDeviceSubgroupProperties subgroupProperties{}; subgroupProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES; VkPhysicalDeviceProperties2 subgroupPropertyQuery{}; subgroupPropertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; subgroupPropertyQuery.pNext = &subgroupProperties; getPhysicalDeviceProperties2(m_physicalDevice.handle, &subgroupPropertyQuery); // Mirrors the loader's HasUsableShaderSubgroupSupport gate, including the // MOBILEGL_MAGMA_DISABLE_SUBGROUP escape hatch, so the module lowerings can never // disagree with the advertised capabilities. const Bool usableSubgroups = subgroupProperties.subgroupSize > 0 && (subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0 && (subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0; if (usableSubgroups && !MG_Config::Features.MagmaDisableSubgroup) { m_nativeSubgroupSize = subgroupProperties.subgroupSize; m_nativeSubgroupSupported = true; } } VkPhysicalDeviceSubgroupSizeControlFeaturesEXT subgroupSizeControlFeatures{}; subgroupSizeControlFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_FEATURES_EXT; m_maxComputeWorkgroupSubgroups = 0; if (m_nativeSubgroupSupported && IsExtensionSupported(availableExtensions, VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME) && getPhysicalDeviceFeatures2 != nullptr) { VkPhysicalDeviceFeatures2 featureQuery{}; featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; featureQuery.pNext = &subgroupSizeControlFeatures; getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery); if (getPhysicalDeviceProperties2 != nullptr) { VkPhysicalDeviceSubgroupSizeControlPropertiesEXT subgroupSizeControlProperties{}; subgroupSizeControlProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_PROPERTIES_EXT; VkPhysicalDeviceProperties2 propertyQuery{}; propertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; propertyQuery.pNext = &subgroupSizeControlProperties; getPhysicalDeviceProperties2(m_physicalDevice.handle, &propertyQuery); m_maxComputeWorkgroupSubgroups = subgroupSizeControlProperties.maxComputeWorkgroupSubgroups; } if (subgroupSizeControlFeatures.computeFullSubgroups == VK_TRUE) { if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME)) { enabledDeviceExtensions.push_back(VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME); } // Only the full-subgroups guarantee is wanted; required/varying subgroup // sizes stay unrequested. subgroupSizeControlFeatures.subgroupSizeControl = VK_FALSE; subgroupSizeControlFeatures.pNext = const_cast(deviceCreateInfo.pNext); deviceCreateInfo.pNext = &subgroupSizeControlFeatures; m_computeFullSubgroupsFeatureEnabled = true; MGLOG_I("Enabled optional device extension: %s (computeFullSubgroups)", VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME); } } // VK_EXT_transform_feedback backs GL transform feedback capture. m_transformFeedbackFeatureEnabled = false; VkPhysicalDeviceTransformFeedbackFeaturesEXT transformFeedbackFeatures{}; transformFeedbackFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_FEATURES_EXT; if (IsExtensionSupported(availableExtensions, VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME) && getPhysicalDeviceFeatures2 != nullptr) { VkPhysicalDeviceFeatures2 featureQuery{}; featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; featureQuery.pNext = &transformFeedbackFeatures; getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery); if (transformFeedbackFeatures.transformFeedback == VK_TRUE) { if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME)) { enabledDeviceExtensions.push_back(VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME); } transformFeedbackFeatures.geometryStreams = VK_FALSE; transformFeedbackFeatures.pNext = const_cast(deviceCreateInfo.pNext); deviceCreateInfo.pNext = &transformFeedbackFeatures; m_transformFeedbackFeatureEnabled = true; MGLOG_I("Enabled optional device extension: %s", VK_EXT_TRANSFORM_FEEDBACK_EXTENSION_NAME); } } // VK_EXT_primitives_generated_query - the query Vulkan defines for GL's // GL_PRIMITIVES_GENERATED precisely because the stream query above needs no // capture by spec but drivers disagree. Taken with BOTH the base feature and the // rasterizer-discard feature or not at all: without the latter, a discarding draw // inside the query is invalid usage, and GL applications toggle discard freely. // Only the PRIMITIVES_GENERATED reroute consumes it (see ArmPrimGenReroute). m_primitivesGeneratedQueryFeatureEnabled = false; m_primitivesGeneratedQueryDiscardFeatureEnabled = false; VkPhysicalDevicePrimitivesGeneratedQueryFeaturesEXT primitivesGeneratedQueryFeatures{}; primitivesGeneratedQueryFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVES_GENERATED_QUERY_FEATURES_EXT; if (IsExtensionSupported(availableExtensions, VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME) && getPhysicalDeviceFeatures2 != nullptr) { VkPhysicalDeviceFeatures2 featureQuery{}; featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; featureQuery.pNext = &primitivesGeneratedQueryFeatures; getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery); if (primitivesGeneratedQueryFeatures.primitivesGeneratedQuery == VK_TRUE && primitivesGeneratedQueryFeatures.primitivesGeneratedQueryWithRasterizerDiscard == VK_TRUE) { if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME)) { enabledDeviceExtensions.push_back(VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME); } primitivesGeneratedQueryFeatures.primitivesGeneratedQueryWithNonZeroStreams = VK_FALSE; primitivesGeneratedQueryFeatures.pNext = const_cast(deviceCreateInfo.pNext); deviceCreateInfo.pNext = &primitivesGeneratedQueryFeatures; m_primitivesGeneratedQueryFeatureEnabled = true; m_primitivesGeneratedQueryDiscardFeatureEnabled = true; MGLOG_I("Enabled optional device extension: %s", VK_EXT_PRIMITIVES_GENERATED_QUERY_EXTENSION_NAME); } } // VK_EXT_provoking_vertex. Two independent features live behind one extension: // provokingVertexLast -> flat varyings, gl_Layer/gl_ViewportIndex and // the input-assembler capture order. // transformFeedbackPreservesProvokingVertex -> spec-level guarantee for the capture order; // only legal when the transformFeedback // feature is also enabled, which is why this // block sits after the one above. // They are enabled independently on purpose: gating the first on the second would leave flat // shading GL-wrong on any device without VK_EXT_transform_feedback, for no legality reason. m_provokingVertexLastEnabled = false; m_provokingVertexXfbPreserveEnabled = false; m_provokingVertexModePerPipeline = false; m_provokingVertexFanPreserved = false; VkPhysicalDeviceProvokingVertexFeaturesEXT provokingVertexFeatures{}; provokingVertexFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_FEATURES_EXT; if (IsExtensionSupported(availableExtensions, VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME) && getPhysicalDeviceFeatures2 != nullptr) { VkPhysicalDeviceFeatures2 featureQuery{}; featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; featureQuery.pNext = &provokingVertexFeatures; getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery); VkPhysicalDeviceProvokingVertexPropertiesEXT provokingVertexProperties{}; provokingVertexProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_PROPERTIES_EXT; auto getPhysicalDeviceProperties2 = reinterpret_cast( vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2")); if (getPhysicalDeviceProperties2 == nullptr) { getPhysicalDeviceProperties2 = reinterpret_cast( vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2KHR")); } if (getPhysicalDeviceProperties2 != nullptr) { VkPhysicalDeviceProperties2 propertyQuery{}; propertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; propertyQuery.pNext = &provokingVertexProperties; getPhysicalDeviceProperties2(m_physicalDevice.handle, &propertyQuery); } m_provokingVertexModePerPipeline = provokingVertexProperties.provokingVertexModePerPipeline == VK_TRUE; m_provokingVertexFanPreserved = provokingVertexProperties.transformFeedbackPreservesTriangleFanProvokingVertex == VK_TRUE; if (provokingVertexFeatures.provokingVertexLast == VK_TRUE) { // transformFeedbackPreservesProvokingVertex is deliberately NOT requested. Measured: // asking for it regresses transform_feedback.geometry on GL33 through GL45. A // geometry shader emits its triangles already in GL's vertex order, and the pipeline // that captures them runs on FIRST (see SelectProvokingVertexMode); without the // guarantee the driver leaves that stream alone, but with it the capture is forced to // follow the pipeline's FIRST convention and comes back rotated. The guarantee buys // nothing here either - the input-assembler capture order that // direct_state_access.queries_functional needs comes from provokingVertexLast alone, // which was confirmed by measurement. Leaving it off also keeps VU 04884 disarmed, so // a TRIANGLE_FAN pipeline may take LAST on any device. const Bool wantXfbPreserve = false; if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME)) { enabledDeviceExtensions.push_back(VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME); } provokingVertexFeatures.provokingVertexLast = VK_TRUE; provokingVertexFeatures.transformFeedbackPreservesProvokingVertex = wantXfbPreserve ? VK_TRUE : VK_FALSE; provokingVertexFeatures.pNext = const_cast(deviceCreateInfo.pNext); deviceCreateInfo.pNext = &provokingVertexFeatures; m_provokingVertexLastEnabled = true; m_provokingVertexXfbPreserveEnabled = wantXfbPreserve; MGLOG_I("Enabled optional device extension: %s (transformFeedbackPreservesProvokingVertex=%s)", VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME, wantXfbPreserve ? "true" : "false"); } } if (!m_provokingVertexLastEnabled) { MGLOG_W("VK_EXT_provoking_vertex is unavailable; flat-shaded varyings take a primitive's first " "vertex instead of GL's last, and transform feedback records TRIANGLE_STRIP/TRIANGLE_FAN " "triangles rotated (0,1,2 / 1,3,2 instead of 0,1,2 / 2,1,3)"); } if (!m_transformFeedbackFeatureEnabled) { MGLOG_W("VK_EXT_transform_feedback is unavailable; transform feedback capture will not work"); } // VK_EXT_vertex_attribute_divisor. Vulkan's instance input rate advances an attribute // once per instance and nothing else, so without this every glVertexAttribDivisor value // collapses to 1 and an attribute meant to change every N instances changes every one. m_vertexAttributeDivisorEnabled = false; VkPhysicalDeviceVertexAttributeDivisorFeaturesEXT vertexAttributeDivisorFeatures{}; vertexAttributeDivisorFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VERTEX_ATTRIBUTE_DIVISOR_FEATURES_EXT; if (IsExtensionSupported(availableExtensions, VK_EXT_VERTEX_ATTRIBUTE_DIVISOR_EXTENSION_NAME) && getPhysicalDeviceFeatures2 != nullptr) { VkPhysicalDeviceFeatures2 featureQuery{}; featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; featureQuery.pNext = &vertexAttributeDivisorFeatures; getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery); if (vertexAttributeDivisorFeatures.vertexAttributeInstanceRateDivisor == VK_TRUE) { if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, VK_EXT_VERTEX_ATTRIBUTE_DIVISOR_EXTENSION_NAME)) { enabledDeviceExtensions.push_back(VK_EXT_VERTEX_ATTRIBUTE_DIVISOR_EXTENSION_NAME); } vertexAttributeDivisorFeatures.vertexAttributeInstanceRateZeroDivisor = VK_FALSE; vertexAttributeDivisorFeatures.pNext = const_cast(deviceCreateInfo.pNext); deviceCreateInfo.pNext = &vertexAttributeDivisorFeatures; m_vertexAttributeDivisorEnabled = true; MGLOG_I("Enabled optional device extension: %s", VK_EXT_VERTEX_ATTRIBUTE_DIVISOR_EXTENSION_NAME); } } if (!m_vertexAttributeDivisorEnabled) { MGLOG_W("VK_EXT_vertex_attribute_divisor is unavailable; a glVertexAttribDivisor other " "than 1 will advance its attribute once per instance"); } // Host query reset lets the occlusion-query ring recycle slots without a // command-buffer round trip. m_hostQueryResetEnabled = false; VkPhysicalDeviceHostQueryResetFeatures hostQueryResetFeatures{}; hostQueryResetFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_HOST_QUERY_RESET_FEATURES; if (IsExtensionSupported(availableExtensions, VK_EXT_HOST_QUERY_RESET_EXTENSION_NAME) && getPhysicalDeviceFeatures2 != nullptr) { VkPhysicalDeviceFeatures2 featureQuery{}; featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; featureQuery.pNext = &hostQueryResetFeatures; getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery); if (hostQueryResetFeatures.hostQueryReset == VK_TRUE) { if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, VK_EXT_HOST_QUERY_RESET_EXTENSION_NAME)) { enabledDeviceExtensions.push_back(VK_EXT_HOST_QUERY_RESET_EXTENSION_NAME); } hostQueryResetFeatures.pNext = const_cast(deviceCreateInfo.pNext); deviceCreateInfo.pNext = &hostQueryResetFeatures; m_hostQueryResetEnabled = true; } } // VK_EXT_multi_draw: tier 1 of the multi-draw dispatch - one vkCmdDrawMulti(Indexed)EXT // for a whole glMultiDraw* batch (VkMultiDrawIndexedInfoEXT carries per-draw // firstIndex/indexCount/vertexOffset, so glMultiDrawElementsBaseVertex fits natively). // Requested only when both the extension and its multiDraw feature are present; // absent it, the dispatch falls to the multiDrawIndirect tier or the unrolled loop. m_multiDrawExtensionEnabled = false; m_maxMultiDrawCount = 0; VkPhysicalDeviceMultiDrawFeaturesEXT multiDrawFeatures{}; multiDrawFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTI_DRAW_FEATURES_EXT; if (IsExtensionSupported(availableExtensions, VK_EXT_MULTI_DRAW_EXTENSION_NAME) && getPhysicalDeviceFeatures2 != nullptr) { VkPhysicalDeviceFeatures2 featureQuery{}; featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; featureQuery.pNext = &multiDrawFeatures; getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery); if (multiDrawFeatures.multiDraw == VK_TRUE) { if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, VK_EXT_MULTI_DRAW_EXTENSION_NAME)) { enabledDeviceExtensions.push_back(VK_EXT_MULTI_DRAW_EXTENSION_NAME); } multiDrawFeatures.pNext = const_cast(deviceCreateInfo.pNext); deviceCreateInfo.pNext = &multiDrawFeatures; m_multiDrawExtensionEnabled = true; VkPhysicalDeviceMultiDrawPropertiesEXT multiDrawProperties{}; multiDrawProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTI_DRAW_PROPERTIES_EXT; auto getPhysicalDeviceProperties2 = reinterpret_cast( vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2")); if (getPhysicalDeviceProperties2 == nullptr) { getPhysicalDeviceProperties2 = reinterpret_cast( vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2KHR")); } if (getPhysicalDeviceProperties2 != nullptr) { VkPhysicalDeviceProperties2 propertyQuery{}; propertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; propertyQuery.pNext = &multiDrawProperties; getPhysicalDeviceProperties2(m_physicalDevice.handle, &propertyQuery); } // Spec minimum is 1024; a driver reporting 0 through a failed query must not // zero out every batch, so fall back to the spec minimum. m_maxMultiDrawCount = multiDrawProperties.maxMultiDrawCount != 0 ? multiDrawProperties.maxMultiDrawCount : 1024; MGLOG_I("Enabled optional device extension: %s (maxMultiDrawCount=%u)", VK_EXT_MULTI_DRAW_EXTENSION_NAME, m_maxMultiDrawCount); } else { MGLOG_I("VK_EXT_multi_draw is advertised but its multiDraw feature is unavailable; " "multi-draw batches use the indirect or unrolled tier"); } } deviceCreateInfo.enabledExtensionCount = static_cast(enabledDeviceExtensions.size()); deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data(); MGLOG_I("Device feature support: robustBufferAccess=%s geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s " "shaderCullDistance=%s wideLines=%s shaderInt64=%s vertexStoresAtomics=%s " "fragmentStoresAtomics=%s storageImageExtendedFormats=%s storageImageReadWithoutFormat=%s " "storageImageWriteWithoutFormat=%s drawIndirectFirstInstance=%s " "multiDrawIndirect=%s", supportedDeviceFeatures.robustBufferAccess ? "true" : "false", supportedDeviceFeatures.geometryShader ? "true" : "false", supportedDeviceFeatures.independentBlend ? "true" : "false", supportedDeviceFeatures.logicOp ? "true" : "false", supportedDeviceFeatures.shaderClipDistance ? "true" : "false", supportedDeviceFeatures.shaderCullDistance ? "true" : "false", supportedDeviceFeatures.wideLines ? "true" : "false", supportedDeviceFeatures.shaderInt64 ? "true" : "false", supportedDeviceFeatures.vertexPipelineStoresAndAtomics ? "true" : "false", supportedDeviceFeatures.fragmentStoresAndAtomics ? "true" : "false", supportedDeviceFeatures.shaderStorageImageExtendedFormats ? "true" : "false", supportedDeviceFeatures.shaderStorageImageReadWithoutFormat ? "true" : "false", supportedDeviceFeatures.shaderStorageImageWriteWithoutFormat ? "true" : "false", supportedDeviceFeatures.drawIndirectFirstInstance ? "true" : "false", supportedDeviceFeatures.multiDrawIndirect ? "true" : "false"); MGLOG_I("Device feature enabled: robustBufferAccess=%s geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s " "shaderCullDistance=%s wideLines=%s shaderInt64=%s vertexStoresAtomics=%s " "fragmentStoresAtomics=%s storageImageExtendedFormats=%s storageImageReadWithoutFormat=%s " "storageImageWriteWithoutFormat=%s drawIndirectFirstInstance=%s " "multiDrawIndirect=%s shaderDrawParameters=%s", deviceFeatures.robustBufferAccess ? "true" : "false", deviceFeatures.geometryShader ? "true" : "false", deviceFeatures.independentBlend ? "true" : "false", deviceFeatures.logicOp ? "true" : "false", deviceFeatures.shaderClipDistance ? "true" : "false", deviceFeatures.shaderCullDistance ? "true" : "false", deviceFeatures.wideLines ? "true" : "false", deviceFeatures.shaderInt64 ? "true" : "false", deviceFeatures.vertexPipelineStoresAndAtomics ? "true" : "false", deviceFeatures.fragmentStoresAndAtomics ? "true" : "false", deviceFeatures.shaderStorageImageExtendedFormats ? "true" : "false", deviceFeatures.shaderStorageImageReadWithoutFormat ? "true" : "false", deviceFeatures.shaderStorageImageWriteWithoutFormat ? "true" : "false", deviceFeatures.drawIndirectFirstInstance ? "true" : "false", deviceFeatures.multiDrawIndirect ? "true" : "false", m_shaderDrawParametersFeatureEnabled ? "true" : "false"); VK_VERIFY(vkCreateDevice(m_physicalDevice.handle, &deviceCreateInfo, nullptr, &m_device), "vkCreateDevice"); #if MOBILEGL_BUILD_DISAGGREGATED if (wireDepthResolveEnabled) { m_wireCreateRenderPass2 = reinterpret_cast( vkGetDeviceProcAddr(m_device, wireDepthResolveCore ? "vkCreateRenderPass2" : "vkCreateRenderPass2KHR")); } #endif s_vkCmdDrawIndexedIndirectCount = reinterpret_cast( vkGetDeviceProcAddr(m_device, "vkCmdDrawIndexedIndirectCountKHR")); if (s_vkCmdDrawIndexedIndirectCount == nullptr) { s_vkCmdDrawIndexedIndirectCount = reinterpret_cast( vkGetDeviceProcAddr(m_device, "vkCmdDrawIndexedIndirectCount")); } if (m_drawIndirectCountExtensionEnabled && s_vkCmdDrawIndexedIndirectCount == nullptr) { MGLOG_W("VK_KHR_draw_indirect_count enabled but vkCmdDrawIndexedIndirectCount entry point is missing, will continue as if VK_KHR_draw_indirect_count is not supported!"); m_drawIndirectCountExtensionEnabled = false; } s_vkCmdDrawMultiEXT = nullptr; s_vkCmdDrawMultiIndexedEXT = nullptr; if (m_multiDrawExtensionEnabled) { s_vkCmdDrawMultiEXT = reinterpret_cast(vkGetDeviceProcAddr(m_device, "vkCmdDrawMultiEXT")); s_vkCmdDrawMultiIndexedEXT = reinterpret_cast( vkGetDeviceProcAddr(m_device, "vkCmdDrawMultiIndexedEXT")); if (s_vkCmdDrawMultiEXT == nullptr || s_vkCmdDrawMultiIndexedEXT == nullptr) { MGLOG_W("VK_EXT_multi_draw enabled but its entry points are missing, will continue as if " "VK_EXT_multi_draw is not supported!"); s_vkCmdDrawMultiEXT = nullptr; s_vkCmdDrawMultiIndexedEXT = nullptr; m_multiDrawExtensionEnabled = false; } } // Resolve the multi-draw dispatch tiers once: device support clamped by the // MOBILEGL_MAGMA_MULTIDRAW_MODE preference. Requesting an unavailable tier is // never an error - the dispatch falls down the chain ext -> indirect -> unroll. { using MG_Config::MultiDrawMode; const MultiDrawMode mode = MG_Config::Features.MagmaMultiDrawMode; m_multiDrawAllowExt = m_multiDrawExtensionEnabled && (mode == MultiDrawMode::Auto || mode == MultiDrawMode::Ext); m_multiDrawAllowIndirect = m_multiDrawIndirectFeatureEnabled && mode != MultiDrawMode::Unroll; m_multiDrawForceUnrollIndirect = mode == MultiDrawMode::Unroll; if (mode == MultiDrawMode::Ext && !m_multiDrawExtensionEnabled) { MGLOG_I("MOBILEGL_MAGMA_MULTIDRAW_MODE=ext requested but VK_EXT_multi_draw is unavailable; " "falling back to the %s tier", m_multiDrawAllowIndirect ? "indirect" : "unroll"); } if (mode == MultiDrawMode::Indirect && !m_multiDrawIndirectFeatureEnabled) { MGLOG_I("MOBILEGL_MAGMA_MULTIDRAW_MODE=indirect requested but the multiDrawIndirect device " "feature is unavailable; falling back to the unroll tier"); } MGLOG_I("Multi-draw dispatch tier: %s (VK_EXT_multi_draw=%s, multiDrawIndirect=%s, mode=%s)", m_multiDrawAllowExt ? "ext" : (m_multiDrawAllowIndirect ? "indirect" : "unroll"), m_multiDrawExtensionEnabled ? "true" : "false", m_multiDrawIndirectFeatureEnabled ? "true" : "false", mode == MultiDrawMode::Auto ? "auto" : mode == MultiDrawMode::Ext ? "ext" : mode == MultiDrawMode::Indirect ? "indirect" : "unroll"); } if (m_transformFeedbackFeatureEnabled) { s_vkCmdBindTransformFeedbackBuffersEXT = reinterpret_cast( vkGetDeviceProcAddr(m_device, "vkCmdBindTransformFeedbackBuffersEXT")); s_vkCmdBeginTransformFeedbackEXT = reinterpret_cast( vkGetDeviceProcAddr(m_device, "vkCmdBeginTransformFeedbackEXT")); s_vkCmdEndTransformFeedbackEXT = reinterpret_cast( vkGetDeviceProcAddr(m_device, "vkCmdEndTransformFeedbackEXT")); if (s_vkCmdBindTransformFeedbackBuffersEXT == nullptr || s_vkCmdBeginTransformFeedbackEXT == nullptr || s_vkCmdEndTransformFeedbackEXT == nullptr) { MGLOG_W("VK_EXT_transform_feedback entry points missing; transform feedback capture disabled"); m_transformFeedbackFeatureEnabled = false; } } if (m_hostQueryResetEnabled) { s_vkResetQueryPool = reinterpret_cast(vkGetDeviceProcAddr(m_device, "vkResetQueryPool")); if (s_vkResetQueryPool == nullptr) { s_vkResetQueryPool = reinterpret_cast(vkGetDeviceProcAddr(m_device, "vkResetQueryPoolEXT")); } if (s_vkResetQueryPool == nullptr) { m_hostQueryResetEnabled = false; } } if (m_transformFeedbackFeatureEnabled) { s_vkCmdBeginQueryIndexedEXT = reinterpret_cast( vkGetDeviceProcAddr(m_device, "vkCmdBeginQueryIndexedEXT")); s_vkCmdEndQueryIndexedEXT = reinterpret_cast( vkGetDeviceProcAddr(m_device, "vkCmdEndQueryIndexedEXT")); VkPhysicalDeviceTransformFeedbackPropertiesEXT xfbProperties{}; xfbProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_PROPERTIES_EXT; VkPhysicalDeviceProperties2 properties2{}; properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; properties2.pNext = &xfbProperties; // Resolved via proc addr: vkGetPhysicalDeviceProperties2 is Vulkan 1.1, and Android's // libvulkan.so only exports it from API 28 while minSdk is 26. auto getPhysicalDeviceProperties2 = reinterpret_cast( vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2")); if (getPhysicalDeviceProperties2 == nullptr) { getPhysicalDeviceProperties2 = reinterpret_cast( vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2KHR")); } if (getPhysicalDeviceProperties2 != nullptr) { getPhysicalDeviceProperties2(m_physicalDevice.handle, &properties2); } m_xfbQueriesSupported = xfbProperties.transformFeedbackQueries == VK_TRUE && s_vkCmdBeginQueryIndexedEXT != nullptr && s_vkCmdEndQueryIndexedEXT != nullptr; } MGLOG_I("index type uint8 enabled: %s", m_indexTypeUint8ExtensionEnabled ? "true" : "false"); MGLOG_I("Logical device created."); // Queues vkGetDeviceQueue(m_device, m_physicalDevice.queueFamilies.graphicsFamily, 0, &m_graphicsQueue); vkGetDeviceQueue(m_device, m_physicalDevice.queueFamilies.presentFamily, 0, &m_presentQueue); MGLOG_I("Queues got successfully."); // Timestamp (timer query) support: re-enumerate the graphics queue // family's properties for its timestampValidBits (0 means the queue // cannot write timestamps) and take timestampPeriod (ns per tick) from // the device limits. const auto timestampQueueFamilies = GetQueueFamilyFromPhysicalDevice(m_physicalDevice.handle); m_timestampValidBits = 0; const Int32 graphicsFamilyIndex = m_physicalDevice.queueFamilies.graphicsFamily; if (graphicsFamilyIndex >= 0 && static_cast(graphicsFamilyIndex) < timestampQueueFamilies.size()) { m_timestampValidBits = timestampQueueFamilies[graphicsFamilyIndex].timestampValidBits; } m_timestampPeriodNs = m_physicalDevice.properties.limits.timestampPeriod; m_timerQuerySupported = m_timestampValidBits > 0 && m_timestampPeriodNs > 0.0f; MGLOG_I("Timer queries %s (timestampValidBits=%u, timestampPeriod=%f ns/tick)", m_timerQuerySupported ? "supported" : "not supported", m_timestampValidBits, m_timestampPeriodNs); // Last, because it records on m_graphicsQueue: decide the PRIMITIVES_GENERATED // reroute for XFB-inactive draws. Nothing else has touched the queue yet. ArmPrimGenReroute(); } void VulkanRenderer::ArmPrimGenReroute() { using namespace MG_Util::SelfTest; m_primGenRerouteKind = PrimGenRerouteKind::None; const MG_Config::QuirkOverride overrideSetting = MG_Config::Features.MagmaPrimGenQueryReroute; // Without stream queries the GENERATED path never opens a slot at all, so // there is nothing to reroute - whatever the override says. if (!m_xfbQueriesSupported || !m_hostQueryResetEnabled) { return; } const Bool primitivesGeneratedQueryUsable = m_primitivesGeneratedQueryFeatureEnabled && m_primitivesGeneratedQueryDiscardFeatureEnabled; PrimitivesGeneratedNoXfbVerdict verdict = PrimitivesGeneratedNoXfbVerdict::Inconclusive; // The probe only matters under Auto (ForceOn bypasses the verdict, ForceOff // never asks), and the answer is a device property - so it is memoized per // process rather than re-paid on every renderer recreation. if (overrideSetting == MG_Config::QuirkOverride::Auto) { static const PrimitivesGeneratedNoXfbMeasurement s_measurement = [&]() { PrimitivesGeneratedNoXfbProbeContext probeContext; probeContext.device = m_device; probeContext.queue = m_graphicsQueue; probeContext.queueFamilyIndex = static_cast(m_physicalDevice.queueFamilies.graphicsFamily); probeContext.transformFeedbackQueriesUsable = m_xfbQueriesSupported; probeContext.primitivesGeneratedQueryUsable = primitivesGeneratedQueryUsable; probeContext.pipelineStatisticsEnabled = m_pipelineStatisticsQueryFeatureEnabled; probeContext.tessellationEnabled = m_tessellationShaderFeatureEnabled; auto& fns = probeContext.fns; fns.vkCreateCommandPool = vkCreateCommandPool; fns.vkDestroyCommandPool = vkDestroyCommandPool; fns.vkAllocateCommandBuffers = vkAllocateCommandBuffers; fns.vkBeginCommandBuffer = vkBeginCommandBuffer; fns.vkEndCommandBuffer = vkEndCommandBuffer; fns.vkCreateQueryPool = vkCreateQueryPool; fns.vkDestroyQueryPool = vkDestroyQueryPool; fns.vkCmdResetQueryPool = vkCmdResetQueryPool; fns.vkCmdBeginQuery = vkCmdBeginQuery; fns.vkCmdEndQuery = vkCmdEndQuery; fns.vkCmdBeginQueryIndexedEXT = s_vkCmdBeginQueryIndexedEXT; fns.vkCmdEndQueryIndexedEXT = s_vkCmdEndQueryIndexedEXT; fns.vkCreateRenderPass = vkCreateRenderPass; fns.vkDestroyRenderPass = vkDestroyRenderPass; fns.vkCreateFramebuffer = vkCreateFramebuffer; fns.vkDestroyFramebuffer = vkDestroyFramebuffer; fns.vkCmdBeginRenderPass = vkCmdBeginRenderPass; fns.vkCmdEndRenderPass = vkCmdEndRenderPass; fns.vkCreateShaderModule = vkCreateShaderModule; fns.vkDestroyShaderModule = vkDestroyShaderModule; fns.vkCreatePipelineLayout = vkCreatePipelineLayout; fns.vkDestroyPipelineLayout = vkDestroyPipelineLayout; fns.vkCreateGraphicsPipelines = vkCreateGraphicsPipelines; fns.vkDestroyPipeline = vkDestroyPipeline; fns.vkCmdBindPipeline = vkCmdBindPipeline; fns.vkCmdDraw = vkCmdDraw; fns.vkCreateFence = vkCreateFence; fns.vkDestroyFence = vkDestroyFence; fns.vkQueueSubmit = vkQueueSubmit; fns.vkWaitForFences = vkWaitForFences; fns.vkGetQueryPoolResults = vkGetQueryPoolResults; fns.vkDeviceWaitIdle = vkDeviceWaitIdle; return RunPrimitivesGeneratedNoXfbProbe(probeContext); }(); verdict = EvaluatePrimitivesGeneratedNoXfbVerdict(s_measurement); if (s_measurement.fenceWaitTimedOut) { // The probe's submission never signaled within its bound, so it left its // command pool, query pools, render pass, framebuffer, shader modules, // pipeline layout, pipelines and fence alive on purpose. This device is the // renderer's own and outlives them, so nothing here may destroy them or // wait the device idle - the queue may still be executing that submission, // and an idle wait is the hang the bound exists to prevent. They leak for // the process's life; a device this sick has bigger problems. MGLOG_W("PRIMITIVES_GENERATED probe timed out waiting on its own submission (%s); its " "Vulkan objects are deliberately leaked and XFB-inactive draws keep the stream " "query", s_measurement.failureReason.c_str()); } else if (!s_measurement.ran) { MGLOG_W("PRIMITIVES_GENERATED probe did not run (%s); XFB-inactive draws keep the " "stream query", s_measurement.failureReason.c_str()); } else { const auto logShape = [](const char* name, const MG_Util::SelfTest::PrimitivesGeneratedNoXfbShapeMeasurement& shape) { MGLOG_I("PRIMITIVES_GENERATED probe %s: drawn=%d stream=%llu/%llu pgq=%llu(%d) " "stat=%llu(%d)", name, shape.drawn ? 1 : 0, static_cast(shape.streamGenerated), static_cast(shape.expectedPrimitives), static_cast(shape.primitivesGeneratedExt), shape.primitivesGeneratedExtMeasured ? 1 : 0, static_cast(shape.statisticsClippingInput), shape.statisticsMeasured ? 1 : 0); }; logShape("triangles", s_measurement.trianglesPlain); logShape("triangles+discard", s_measurement.trianglesDiscard); logShape("patches+discard", s_measurement.patchesDiscard); } } // A driver whose stream query counts capture-less draws counts a PAUSED span's // draws through the stream slot they take, so that span's result must not have // the frontend's CPU paused counter added on top of it either (the pre-reroute // accounting did exactly that, double counting every paused draw the CPU could // price). Measured, not assumed: the forced arms never ask the probe and leave // this false. m_primGenStreamCountsXfbInactiveDraws = verdict == PrimitivesGeneratedNoXfbVerdict::StreamCounts; m_primGenRerouteKind = ChoosePrimitivesGeneratedReroute( overrideSetting, verdict, primitivesGeneratedQueryUsable, m_pipelineStatisticsQueryFeatureEnabled); if (m_primGenRerouteKind != PrimGenRerouteKind::None) { MGLOG_I("PRIMITIVES_GENERATED for XFB-inactive draws will accumulate through a %s pool%s", m_primGenRerouteKind == PrimGenRerouteKind::PrimitivesGeneratedExt ? "VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT" : "clipping-invocations pipeline-statistics", overrideSetting == MG_Config::QuirkOverride::ForceOn ? " (forced on)" : ""); } } void VulkanRenderer::CreateAllocator() { MOBILEGL_ASSERT(m_instance != VK_NULL_HANDLE, "CreateAllocator requires valid VkInstance"); MOBILEGL_ASSERT(m_physicalDevice.handle != VK_NULL_HANDLE, "CreateAllocator requires valid physical device"); MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "CreateAllocator requires valid VkDevice"); if (m_allocator != nullptr) { return; } VmaAllocatorCreateInfo allocatorInfo{}; VmaVulkanFunctions vulkanFunctions{}; vulkanFunctions.vkGetInstanceProcAddr = vkGetInstanceProcAddr; vulkanFunctions.vkGetDeviceProcAddr = vkGetDeviceProcAddr; allocatorInfo.instance = m_instance; allocatorInfo.physicalDevice = m_physicalDevice.handle; allocatorInfo.device = m_device; allocatorInfo.pVulkanFunctions = &vulkanFunctions; allocatorInfo.vulkanApiVersion = VK_API_VERSION_1_0; VK_VERIFY(vmaCreateAllocator(&allocatorInfo, &m_allocator), "vmaCreateAllocator"); } void VulkanRenderer::DestroyAllocator() { if (m_allocator != nullptr) { vmaDestroyAllocator(m_allocator); m_allocator = nullptr; } } void VulkanRenderer::CreateSwapchain() { const VkExtent2D desiredExtent = { std::max(m_config.SurfaceWidth, 1), std::max(m_config.SurfaceHeight, 1), }; m_swapchainObject.Create(m_device, m_physicalDevice.handle, m_surface, static_cast(m_physicalDevice.queueFamilies.graphicsFamily), static_cast(m_physicalDevice.queueFamilies.presentFamily), m_config.MaxFramesInFlight, desiredExtent); // The FragCoordYFlip variants bake this height in; it is the only input to a shader // module that lives outside the GL program, so the factory has to learn it here (and on // every recreation, which is the only way it can change). if (m_programFactory) { m_programFactory->SetDefaultFramebufferHeight(m_swapchainObject.GetExtent().height); } } void VulkanRenderer::CreateCommandPool() { VkCommandPoolCreateInfo createInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; createInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; createInfo.queueFamilyIndex = m_physicalDevice.queueFamilies.graphicsFamily; VK_VERIFY(vkCreateCommandPool(m_device, &createInfo, nullptr, &m_commandPool)); MGLOG_I("Command pool created"); } void VulkanRenderer::CreateSurface() { if (!m_window) { #if defined VK_USE_PLATFORM_METAL_EXT m_window = reinterpret_cast( CreateInternalMetalLayer(m_config.SurfaceWidth, m_config.SurfaceHeight, &m_platformDisplay)); m_platformLibrary = reinterpret_cast(m_window); #elif defined VK_USE_PLATFORM_ANDROID_KHR if (m_headlessSurfaceSupported) { auto* createHeadlessSurface = reinterpret_cast( vkGetInstanceProcAddr(m_instance, "vkCreateHeadlessSurfaceEXT")); MOBILEGL_ASSERT(createHeadlessSurface != nullptr, "VK_EXT_headless_surface is not available for DirectVulkan pbuffer surface"); VkHeadlessSurfaceCreateInfoEXT sci{VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT}; VK_VERIFY(createHeadlessSurface(m_instance, &sci, nullptr, &m_surface), "vkCreateHeadlessSurfaceEXT failed"); return; } // Windowless context on a driver without VK_EXT_headless_surface: give // the WSI an AImageReader's ANativeWindow. It is a real, valid producer // surface that is attached to no display and whose images this code never // acquires, which is exactly the "drawable nobody sees" the Xlib fallback // below builds out of an unmapped window. libmediandk is dlopen'd rather // than linked so a device without it degrades to the old error instead of // failing to load the library at all. { void* mediaLib = dlopen("libmediandk.so", RTLD_NOW | RTLD_LOCAL); MOBILEGL_ASSERT(mediaLib != nullptr, "VK_EXT_headless_surface is unavailable and libmediandk.so could not be loaded " "for the pbuffer surface fallback"); using AImageReaderNewFn = int (*)(int32_t, int32_t, int32_t, int32_t, void**); using AImageReaderGetWindowFn = int (*)(void*, void**); auto* imageReaderNew = reinterpret_cast(dlsym(mediaLib, "AImageReader_new")); auto* imageReaderGetWindow = reinterpret_cast(dlsym(mediaLib, "AImageReader_getWindow")); MOBILEGL_ASSERT(imageReaderNew != nullptr && imageReaderGetWindow != nullptr, "libmediandk.so is missing AImageReader_new/AImageReader_getWindow"); constexpr int32_t kAndroidFormatRgba8888 = 0x1; // AIMAGE_FORMAT_RGBA_8888 const int32_t width = static_cast(std::max(m_config.SurfaceWidth, 1)); const int32_t height = static_cast(std::max(m_config.SurfaceHeight, 1)); void* reader = nullptr; // maxImages must cover the swapchain's images; the reader never // acquires any, so this only sizes its buffer queue. const int status = imageReaderNew(width, height, kAndroidFormatRgba8888, 8, &reader); MOBILEGL_ASSERT(status == 0 && reader != nullptr, "AImageReader_new failed (%d) for the pbuffer surface fallback", status); void* nativeWindow = nullptr; const int windowStatus = imageReaderGetWindow(reader, &nativeWindow); MOBILEGL_ASSERT(windowStatus == 0 && nativeWindow != nullptr, "AImageReader_getWindow failed (%d) for the pbuffer surface fallback", windowStatus); m_fallbackImageReader = reader; m_platformLibrary = mediaLib; m_window = reinterpret_cast(nativeWindow); } #elif defined VK_USE_PLATFORM_XLIB_KHR // No fall-through to Xlib: an offscreen surface never touches a window // system. CreateInstance() has already refused the bring-up if the loader // lacks the extension, so reaching here without it is a broken invariant // rather than a platform limitation - report it and fail, do not continue. auto* createHeadlessSurface = reinterpret_cast( vkGetInstanceProcAddr(m_instance, "vkCreateHeadlessSurfaceEXT")); if (!m_headlessSurfaceSupported || createHeadlessSurface == nullptr) { MGLOG_F("vkCreateHeadlessSurfaceEXT is unavailable (%s reported as %s) while creating an " "offscreen DirectVulkan surface", VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME, m_headlessSurfaceSupported ? "supported" : "unsupported"); throw RuntimeError("vkCreateHeadlessSurfaceEXT is unavailable for an offscreen DirectVulkan surface"); } VkHeadlessSurfaceCreateInfoEXT sci{VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT}; VK_VERIFY(createHeadlessSurface(m_instance, &sci, nullptr, &m_surface), "vkCreateHeadlessSurfaceEXT failed"); return; #else auto* createHeadlessSurface = reinterpret_cast( vkGetInstanceProcAddr(m_instance, "vkCreateHeadlessSurfaceEXT")); if (createHeadlessSurface == nullptr) { // Same class as the Xlib branch above: a null entry point behind // MOBILEGL_ASSERT is a segv on the next line in every INFO-level build. MGLOG_F("vkCreateHeadlessSurfaceEXT is unavailable while creating an offscreen DirectVulkan " "surface (%s missing from this loader)", VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME); throw RuntimeError("vkCreateHeadlessSurfaceEXT is unavailable for an offscreen DirectVulkan surface"); } VkHeadlessSurfaceCreateInfoEXT sci{VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT}; VK_VERIFY(createHeadlessSurface(m_instance, &sci, nullptr, &m_surface), "vkCreateHeadlessSurfaceEXT failed"); return; #endif } #if defined VK_USE_PLATFORM_ANDROID_KHR auto* nativeWindow = static_cast(m_window); if (!nativeWindow) throw RuntimeError("ANativeWindowType is null"); VkAndroidSurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR}; sci.window = nativeWindow; VK_VERIFY(vkCreateAndroidSurfaceKHR(m_instance, &sci, nullptr, &m_surface), "vkCreateAndroidSurfaceKHR failed"); #elif defined VK_USE_PLATFORM_WIN32_KHR auto hwnd = static_cast(m_window); MOBILEGL_ASSERT(hwnd, "HWND is null"); VkWin32SurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR}; sci.hinstance = GetModuleHandleW(nullptr); sci.hwnd = hwnd; VK_VERIFY(vkCreateWin32SurfaceKHR(m_instance, &sci, nullptr, &m_surface), "vkCreateWin32SurfaceKHR failed"); #elif defined VK_USE_PLATFORM_METAL_EXT MOBILEGL_ASSERT(m_window, "CAMetalLayer is null"); VkMetalSurfaceCreateInfoEXT sci{VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT}; sci.pLayer = reinterpret_cast(m_window); VK_VERIFY(vkCreateMetalSurfaceEXT(m_instance, &sci, nullptr, &m_surface), "vkCreateMetalSurfaceEXT failed"); #elif defined VK_USE_PLATFORM_XLIB_KHR // Reached only for a REAL on-screen window surface (a windowed desktop app, // retrace in window mode). Presentation to a window legitimately needs a // window system; offscreen requests returned above and never come here, so // there is no longer any path that opens a display on a caller's behalf. // // Every failure below is a real error return, not MOBILEGL_ASSERT: that macro // is compiled out at the INFO log level every shipping and CI build uses, so // asserting here meant a null Display sailed straight into the next Xlib call // and segfaulted - which is exactly how this presented in CI. if (!m_window) { MGLOG_F("CreateSurface: a window surface was requested with no native window"); throw RuntimeError("CreateSurface: no native window for the Vulkan Xlib surface"); } void* x11Lib = dlopen("libX11.so.6", RTLD_LOCAL | RTLD_NOW); if (!x11Lib) { x11Lib = dlopen("libX11.so", RTLD_LOCAL | RTLD_NOW); } if (x11Lib == nullptr) { MGLOG_F("Failed to open libX11 (.so.6 and .so) while creating a Vulkan Xlib window surface: %s", dlerror()); throw RuntimeError("libX11 is unavailable for the Vulkan Xlib window surface"); } using XOpenDisplayFn = Display* (*)(const char*); using XCloseDisplayFn = int (*)(Display*); auto* xOpenDisplay = reinterpret_cast(dlsym(x11Lib, "XOpenDisplay")); auto* xCloseDisplay = reinterpret_cast(dlsym(x11Lib, "XCloseDisplay")); if (xOpenDisplay == nullptr || xCloseDisplay == nullptr) { MGLOG_F("Failed to resolve XOpenDisplay/XCloseDisplay while creating a Vulkan Xlib window surface"); dlclose(x11Lib); throw RuntimeError("libX11 is missing XOpenDisplay/XCloseDisplay"); } const char* displayName = std::getenv("DISPLAY"); auto* display = xOpenDisplay(displayName); if (display == nullptr) { MGLOG_F("XOpenDisplay(%s) failed while creating a Vulkan Xlib window surface; there is no usable X " "display for the requested window surface", displayName != nullptr ? displayName : ""); dlclose(x11Lib); throw RuntimeError("XOpenDisplay failed for the Vulkan Xlib window surface"); } m_platformDisplay = display; m_platformLibrary = x11Lib; m_platformCloseDisplay = reinterpret_cast(xCloseDisplay); VkXlibSurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR}; sci.dpy = display; sci.window = static_cast(m_window); VK_VERIFY(vkCreateXlibSurfaceKHR(m_instance, &sci, nullptr, &m_surface), "vkCreateXlibSurfaceKHR failed"); #else // #warning "VulkanRenderer::Initialize called on a platform which is not supported yet" MGLOG_W("VulkanRenderer::Initialize called on a platform which is not supported yet"); // TODO: support more // platforms #endif } Vector VulkanRenderer::GetQueueFamilyFromPhysicalDevice(VkPhysicalDevice device) { Uint32 queueFamilyCount = 0; vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr); Vector queueFamilies(queueFamilyCount); vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data()); return queueFamilies; } Int VulkanRenderer::GetQueueFamilyIndex(const Vector& queueFamilies, VkQueueFlagBits flag) { for (Uint32 i = 0; i < queueFamilies.size(); i++) { if (queueFamilies[i].queueFlags & flag) { return i; } } return -1; } Int VulkanRenderer::GetPresentQueueFamilyIndex(const PhysicalDevice& physicalDevice, VkSurfaceKHR surface, const Vector& queueFamilies, Int preferredFamilyIndex) { if (preferredFamilyIndex != -1) { VkBool32 supportsPresent = false; vkGetPhysicalDeviceSurfaceSupportKHR(physicalDevice.handle, preferredFamilyIndex, surface, &supportsPresent); if (supportsPresent) return preferredFamilyIndex; } for (Uint32 i = 0; i < queueFamilies.size(); i++) { VkBool32 supportsPresent = false; vkGetPhysicalDeviceSurfaceSupportKHR(physicalDevice.handle, i, surface, &supportsPresent); if (supportsPresent) return i; } return -1; } Vector VulkanRenderer::EnumerateInstanceExtensions() { // The two-call idiom has a race the spec explicitly allows for: the loader // re-scans ICDs, so the property count can GROW between the sizing call and // the fill call, and the fill then returns VK_INCOMPLETE having written only // as many entries as the caller asked for. The result is a silently TRUNCATED // extension list - and which extensions fall off the end is exactly as stable // as the loader's scan order, i.e. not at all. That is how a headless CI // runner could decide VK_EXT_headless_surface did not exist on one run and // did on the next, sending the pbuffer path into the Xlib fallback with no // X server to open. The sibling EnumerateDeviceExtensions below already // checked its second call; this one dropped the result on the floor. // Loop until a fill call agrees with its own sizing call. Vector extensions; for (Uint32 attempt = 0; attempt < 8; ++attempt) { Uint32 extensionCount = 0; VK_VERIFY(vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr)); extensions.resize(extensionCount); if (extensionCount == 0) { return extensions; } const VkResult result = vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data()); if (result == VK_SUCCESS) { extensions.resize(extensionCount); return extensions; } if (result != VK_INCOMPLETE) { VK_VERIFY(result, "vkEnumerateInstanceExtensionProperties failed"); return extensions; } MGLOG_I("vkEnumerateInstanceExtensionProperties returned VK_INCOMPLETE (the loader's list grew " "mid-enumeration); re-enumerating"); } MGLOG_F("vkEnumerateInstanceExtensionProperties never settled; the instance extension list may be " "truncated and surface-extension selection is about to be made on incomplete information"); return extensions; } Vector VulkanRenderer::EnumerateDeviceExtensions(VkPhysicalDevice device) { Uint32 extensionCount = 0; VK_VERIFY(vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr)); Vector extensions(extensionCount); VK_VERIFY(vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, extensions.data())); return extensions; } Bool VulkanRenderer::IsExtensionSupported(const Vector& availableExtensions, const char* extensionName) { for (const auto& extension : availableExtensions) { if (strcmp(extension.extensionName, extensionName) == 0) { return true; } } return false; } Bool VulkanRenderer::IsExtensionAlreadyEnabled(const Vector& enabledExtensions, const char* extensionName) { return std::any_of(enabledExtensions.begin(), enabledExtensions.end(), [&extensionName](const String& name) { return name == extensionName; }); } Bool VulkanRenderer::EnableOptionalDeviceExtension(const Vector& availableExtensions, Vector& inOutEnabledExtensions, const char* extensionName) { if (!IsExtensionSupported(availableExtensions, extensionName)) { MGLOG_I("Optional device extension not supported: %s", extensionName); return false; } if (!IsExtensionAlreadyEnabled(inOutEnabledExtensions, extensionName)) { inOutEnabledExtensions.push_back(extensionName); } MGLOG_I("Enabled optional device extension: %s", extensionName); return true; } void VulkanRenderer::ResolveOptionalDeviceExtensions(const Vector& availableExtensions, Vector& inOutEnabledExtensions) { m_drawIndirectCountExtensionEnabled = EnableOptionalDeviceExtension(availableExtensions, inOutEnabledExtensions, VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME); m_shaderDrawParametersExtensionEnabled = EnableOptionalDeviceExtension(availableExtensions, inOutEnabledExtensions, VK_KHR_SHADER_DRAW_PARAMETERS_EXTENSION_NAME); #ifdef VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME EnableOptionalDeviceExtension(availableExtensions, inOutEnabledExtensions, VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME); #endif } Bool VulkanRenderer::CheckValidationLayerSupport() { Uint32 layerCount = 0; VK_VERIFY(vkEnumerateInstanceLayerProperties(&layerCount, nullptr)); Vector layers(layerCount); VK_VERIFY(vkEnumerateInstanceLayerProperties(&layerCount, layers.data())); for (const char* layerName : s_validationLayerNames) { for (const auto& layerProperties : layers) { if (strcmp(layerName, layerProperties.layerName) == 0) { return true; } } } return false; } 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() { // 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); #if MOBILEGL_BUILD_DISAGGREGATED // 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(); #endif 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(), {}); ShutdownSwapchain(); CreateSwapchain(); // 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(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; InvalidateSetupDrawSnapshots(); 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()); m_convertedVertexStreams.clear(); } 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(m_swapchainObject.GetPreTransform()), surfaceCaps.currentExtent.width, surfaceCaps.currentExtent.height, static_cast(surfaceCaps.currentTransform)); return true; } void VulkanRenderer::RequestSwapchainResize(Uint32 width, Uint32 height) { width = std::max(width, 1); height = std::max(height, 1); if (m_config.SurfaceWidth == width && m_config.SurfaceHeight == height) { return; } m_config.SurfaceWidth = width; m_config.SurfaceHeight = height; m_swapchainResizeRequested = true; } VkInstance VulkanRenderer::GetInstance() const { return m_instance; } Bool VulkanRenderer::IsDrawIndirectCountExtensionEnabled() const { return m_drawIndirectCountExtensionEnabled; } void VulkanRenderer::ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer, const RenderPassEntry &compatibleRenderPassEntry) { auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); MOBILEGL_ASSERT(activeRenderPass, "No render pass active"); VkClearRect clearRect{}; clearRect.rect.offset = {0, 0}; clearRect.rect.extent = { static_cast(activeRenderPass->extent.x()), static_cast(activeRenderPass->extent.y()) }; clearRect.baseArrayLayer = 0; // Compatible entries share the framebuffer layer count; layered attachments clear every layer. clearRect.layerCount = compatibleRenderPassEntry.layers; for (const auto& pending : compatibleRenderPassEntry.pendingClearAttachments) { if (!pending.hasInlinePayload && pending.key.texture == nullptr) { continue; } ClearAttachmentPayload clearPayload{}; SharedPtr liveTexture; if (pending.hasInlinePayload) { // The inline payload is baked into the cached RenderPassEntry and outlives // its consumption at pass begin (loadOp CLEAR). Replaying it here would // wipe every draw already recorded in the pass, so only clear while the // renderbuffer's clear is still actually pending, and take the live // payload (a newer glClear may carry different values). if (!m_renderPassManager->GetPendingRenderbufferClear(pending.renderbuffer, clearPayload)) { continue; } if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0 && pending.renderbuffer != nullptr && MG_Util::GetBaseInternalFormatComponentCount(pending.renderbuffer->GetInternalFormat()) == 3) { // RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1. clearPayload.color = FloatVec4(clearPayload.color.x(), clearPayload.color.y(), clearPayload.color.z(), 1.0f); } } else { if (!m_clearManager->GetPendingClear(pending.key, clearPayload, liveTexture)) { continue; } } VkClearAttachment clearAttachment{}; clearAttachment.clearValue.depthStencil = {1.0f, 0}; if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0) { clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; // VkClearAttachment::colorAttachment indexes the subpass pColorAttachments (draw-buffer // slot space, with UNUSED holes), not the compacted attachment descriptions. clearAttachment.colorAttachment = pending.colorAttachmentSlot; clearAttachment.clearValue.color = MakeVkClearColorValue(clearPayload, ColorFormatLacksAlpha(liveTexture.get())); } else { if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0) { clearAttachment.aspectMask |= VK_IMAGE_ASPECT_DEPTH_BIT; clearAttachment.clearValue.depthStencil.depth = clearPayload.depth; } if ((clearPayload.mask & GL_STENCIL_BUFFER_BIT) != 0) { clearAttachment.aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT; clearAttachment.clearValue.depthStencil.stencil = clearPayload.stencil; } if (clearAttachment.aspectMask == 0) { continue; } } vkCmdClearAttachments(commandBuffer, 1, &clearAttachment, 1, &clearRect); if (pending.hasInlinePayload) { m_renderPassManager->PopPendingRenderbufferClear(pending.renderbuffer); } else { m_clearManager->PopPendingClear(pending.key); } } } void VulkanRenderer::DestroyComputePipelines() { if (m_device != VK_NULL_HANDLE) { for (const auto& [hash, pipeline] : m_computePipelines) { (void)hash; if (pipeline != VK_NULL_HANDLE) { vkDestroyPipeline(m_device, pipeline, nullptr); } } } m_computePipelines.clear(); } void VulkanRenderer::OnRenderPassesDestroyed(const Vector& renderPasses) { if (m_pipelineFactory == nullptr) { return; } // The render-pass sweep's >1024-boundary idle guarantee covers these pipelines // too (they are only bound by draws that hit the dying entries), so the factory // destroys them immediately. The memo must drop as well: it can hand out a // cached handle without touching the factory. if (m_pipelineFactory->EvictByRenderPasses(renderPasses) > 0) { InvalidatePipelineMemo(); } } void VulkanRenderer::OnProgramEvicted(ProgramFactory::HashType programHash, VkDescriptorSetLayout descriptorSetLayout) { // Same >1024-boundary idleness as the program entry: its compute pipeline is // only dispatched, and its graphics pipelines only bound, through paths that // stamp the entry, so immediate destruction is GPU-safe. (The graphics memo // never holds compute pipelines; it only needs invalidating for the factory // eviction below.) const auto computeIt = m_computePipelines.find(programHash); if (computeIt != m_computePipelines.end()) { if (computeIt->second != VK_NULL_HANDLE && m_device != VK_NULL_HANDLE) { vkDestroyPipeline(m_device, computeIt->second, nullptr); } m_computePipelines.erase(computeIt); } if (m_pipelineFactory != nullptr && m_pipelineFactory->EvictByProgramHash(programHash) > 0) { InvalidatePipelineMemo(); } if (m_uniformManager != nullptr) { m_uniformManager->OnDescriptorSetLayoutDestroyed(descriptorSetLayout); } } VkPipeline VulkanRenderer::GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj) { const auto it = m_computePipelines.find(programObj.hash); if (it != m_computePipelines.end()) { return it->second; } const auto stageIt = std::find_if(programObj.stages.begin(), programObj.stages.end(), [](const VkPipelineShaderStageCreateInfo& stage) { return stage.stage == VK_SHADER_STAGE_COMPUTE_BIT; }); MOBILEGL_ASSERT(stageIt != programObj.stages.end(), "GetOrCreateComputePipeline: program has no compute stage"); if (stageIt == programObj.stages.end()) { return VK_NULL_HANDLE; } VkComputePipelineCreateInfo pipelineInfo{}; pipelineInfo.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; pipelineInfo.stage = *stageIt; pipelineInfo.layout = programObj.pipelineLayout; VkPipeline pipeline = VK_NULL_HANDLE; VK_VERIFY(vkCreateComputePipelines(m_device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline), "GetOrCreateComputePipeline, vkCreateComputePipelines"); // A failed creation must never be memoized - same contract as // PipelineFactory::GetOrCreatePipeline: caching the null would serve it back // for the rest of the process and every dispatch of this program would be // silently skipped. Retrying costs one failed vkCreateComputePipelines per // dispatch, which is the correct price. if (pipeline == VK_NULL_HANDLE) { MGLOG_E("GetOrCreateComputePipeline: vkCreateComputePipelines failed; not caching the failure"); return VK_NULL_HANDLE; } m_computePipelines.emplace(programObj.hash, pipeline); return pipeline; } } // namespace MobileGL::MG_Backend::DirectVulkan