Repository navigation
Do not present the root surface when an external view embedder owns it - #193000
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a render_to_surface flag to GPUSurfaceVulkanImpeller to prevent acquiring or presenting an image when an external view embedder is responsible for presentation. It also moves the disposal of thread-local cached resources to the beginning of AcquireFrame. The review feedback suggests moving the IsValid() check to the very beginning of AcquireFrame to prevent resource disposal or dummy frame creation on an invalid surface.
2d840f2 to
984e119
Compare
|
Force-pushed a fix. CI was red because this did not build standalone: hoisting My mistake was only ever building the whole series together, where the next PR happens to supply what this one dropped. Each PR has to build on its own, which is what CI builds. |
GPUSurfaceVulkan and GPUSurfaceGLImpeller both take render_to_surface and hand back a no-op SurfaceFrame when it is false. GPUSurfaceVulkanImpeller has no such flag, so with a compositor the engine acquires an embedder image and presents it after every composited frame. The compositor has already presented that frame's layers. The root present puts an unrendered image on top of them. An embedder holding one image sees nothing; one cycling a ring of scanout buffers sees its frame replaced by whatever the ring handed back -- one good frame, then black, at a flip rate that still reads 60fps. Reachable once the embedder can build an Impeller render target from a Vulkan backing store (flutter#187525); the gate is the groundwork for it. Dispose of the thread-local pools before the gate rather than after. It is per-frame upkeep, not part of presenting, and skipping it on the compositor path would leave the raster thread holding its command and descriptor pools for the life of the engine.
984e119 to
c80c4f0
Compare
| // holding those pools for the life of the engine. | ||
| if (impeller_context_) { | ||
| impeller::ContextVK::Cast(*impeller_context_) | ||
| .DisposeThreadLocalCachedResources(); |
There was a problem hiding this comment.
Several presubmit CI tests (linux_host_engine_test, linux_unopt, and framework_tests_impeller) are failing with what looks like a real crash:
system_error was thrown in -fno-exceptions mode with error 22 and message "mutex lock failed"
TestDeviceException(Shell subprocess crashed with SIGABRT (-6).)
I suspect this failure is triggered by the downcast in GPUSurfaceVulkanImpeller::AcquireFrame:
if (impeller_context_) {
impeller::ContextVK::Cast(*impeller_context_)
.DisposeThreadLocalCachedResources();
}impeller_context_ is often an instance of SurfaceContextVK, which inherits directly from impeller::Context rather than impeller::ContextVK. The ContextVK::Cast unchecked downcast causes undefined behavior. In ContextVK::DisposeThreadLocalCachedResources(), attempting to lock desc_pool_mutex_ at an invalid offset causes pthread_mutex_lock to return EINVAL (error 22) and abort the process.
Because DisposeThreadLocalCachedResources() is already declared as a virtual method on the impeller::Context base class (and implemented by both ContextVK and SurfaceContextVK), you can call it directly on impeller_context_ without casting:
if (impeller_context_) {
impeller_context_->DisposeThreadLocalCachedResources();
}impeller_context_ is a shared_ptr<impeller::Context> handed in by the caller, and it is often a SurfaceContextVK, which derives from Context rather than ContextVK. ContextVK::Cast on it is an unchecked downcast: DisposeThreadLocalCachedResources then locks desc_pool_mutex_ at the wrong offset, pthread_mutex_lock returns EINVAL, and the process aborts. The same member is cast to SurfaceContextVK a few lines below, so the two casts could not both be right. DisposeThreadLocalCachedResources, CreateCommandBuffer and GetCommandQueue are all virtual on Context and SurfaceContextVK overrides or delegates them, so call through the base and drop the casts.
andywolff
left a comment
There was a problem hiding this comment.
dashboard checks shows the previously failing tests are passing now. It had an unrelated flake, so I hit retry. LGTM
| // holding those pools for the life of the engine. | ||
| if (impeller_context_) { | ||
| impeller::ContextVK::Cast(*impeller_context_) | ||
| .DisposeThreadLocalCachedResources(); |
587a346 also changed the submit callback. That was unnecessary: the callback is built inside the `delegate_ != nullptr` branch, the embedder backing-store path, where impeller_context_ is a ContextVK -- the same branch as the GetDevice cast below it. ContextVK::Cast is sound there. Only DisposeThreadLocalCachedResources runs ahead of that branch, on both paths, so it is the one place the context may be a SurfaceContextVK and the cast undefined. That is the line andywolff flagged, and it stays fixed. Restores the callback to ContextVK::Cast so the change is the single line.
andywolff
left a comment
There was a problem hiding this comment.
LGTM. FYI I pushed a merge to move past a tree breakage so CI can pass for this PR
GPUSurfaceVulkanandGPUSurfaceGLImpellerboth takerender_to_surfaceand return a no-opSurfaceFramewhen it is false.GPUSurfaceVulkanImpellerhas no such flag, so with a compositor the engine acquires an embedder image and presents it after every composited frame.The compositor has already presented that frame's layers. The root present puts an unrendered image on top of them. An embedder holding a single image sees nothing; one cycling a ring of scanout buffers sees its frame replaced by whatever the ring handed back — one good frame, then black, at a flip rate that still reads 60fps. That is what it looked like on hardware before I found this.
Nothing can reach it today: with a compositor and Impeller on Vulkan the embedder cannot build a render target at all, so the frame fails earlier (#187525). It becomes reachable with that fix, so the gate is groundwork for it.
Also moves
DisposeThreadLocalCachedResourcesahead of the gate. It is per-frame upkeep, not part of presenting, and leaving it after would hand the compositor path a raster thread that holds its command and descriptor pools for the life of the engine.Tests
Two in
gpu_surface_vulkan_impeller_unittests.cc:DoesNotPresentWhenNotRenderingToSurface— no image acquired, none presented, and the thread-local pools still released. Fails without the gate; fails again if the dispose is moved back after it ("thread-local pools survived a frame the compositor presented").PresentsWhenRenderingToSurface— the default still acquires, which is what a compositor-less embedder relies on.Verified against the failing behavior, not just green: reverting each half of the change fails the matching assertion.
Second of six, from #187525. Independent of the others; conflicts textually with the third (both touch
gpu_surface_vulkan_impeller.cc), so whichever lands second wants a rebase.