diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 5333ea17..3db8f941 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -596,6 +596,11 @@ namespace MobileGL { // native state is process-global by construction gives this object its own copy and binds // it to the calling thread. The default has nothing per process to separate. virtual void BindSessionStateToThisThread() {} + // A served session's ring moved to another of its client's contexts (bind_context). A + // backend whose surface follows the native context it binds needs nothing; one that keeps + // a single native surface binding re-binds the surface that context was last made current + // with. `token` is the client's context token (the EGLContext MakeEGLCurrent was given). + virtual void OnClientContextBound(Uint64 token) { (void)token; } virtual Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor); virtual Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle); diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 45859757..8acd10cf 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -373,14 +373,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { auto nativeWindow = reinterpret_cast(m_windowHandle.Handle); + VulkanRendererConfig config; + config.SwapInterval = GetRequestedSwapInterval(); + if (UsesSurfaceTargets() && pVulkanRenderer) { + pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(m_activatingSurface), nativeWindow, config); + return true; + } // Any renderer instance this assignment replaces is destroyed here; // fence/timer-query handles stamped with the old generation go stale. BumpRendererGeneration(); - VulkanRendererConfig config; - config.SwapInterval = GetRequestedSwapInterval(); pVulkanRenderer = MakeUnique(nativeWindow, config); MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitWindowSurface: VulkanRenderer creation failed"); pVulkanRenderer->Initialize(); + pVulkanRenderer->SetActiveSurfaceTargetKey(SurfaceTargetKey(m_activatingSurface)); return true; } @@ -388,12 +393,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { VulkanRendererConfig config; config.SurfaceWidth = static_cast(std::max(width, 1)); config.SurfaceHeight = static_cast(std::max(height, 1)); + if (UsesSurfaceTargets() && pVulkanRenderer) { + pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(m_activatingSurface), NativeWindowType{}, config); + return true; + } // Any renderer instance this assignment replaces is destroyed here; // fence/timer-query handles stamped with the old generation go stale. BumpRendererGeneration(); pVulkanRenderer = MakeUnique(NativeWindowType{}, config); MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitPbufferSurface: VulkanRenderer creation failed"); pVulkanRenderer->Initialize(); + pVulkanRenderer->SetActiveSurfaceTargetKey(SurfaceTargetKey(m_activatingSurface)); return true; } @@ -496,7 +506,33 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool BackendObject_DirectVulkan::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) { const std::lock_guard lock(m_eglStateMutex); - return BackendObject::MakeEGLCurrent(dpy, draw, read, ctx); + const Bool ok = BackendObject::MakeEGLCurrent(dpy, draw, read, ctx); + if (ok && UsesSurfaceTargets() && ctx != EGL_NO_CONTEXT && draw != EGL_NO_SURFACE) { + m_contextBindings[ctx] = EGLCurrentState{.Display = dpy, .DrawSurface = draw, .ReadSurface = read, + .Context = ctx}; + } + return ok; + } + + void BackendObject_DirectVulkan::OnClientContextBound(Uint64 token) { + const std::lock_guard lock(m_eglStateMutex); + if (!UsesSurfaceTargets() || token == 0) return; + // Each of a multi-window client's contexts draws to the surface IT was made current with + // (Qt renders every window on its own thread and context); the ring interleaves them with + // a bind_context between, and no make-current. Follow the context back to its surface. + const auto found = m_contextBindings.find(reinterpret_cast(token)); + if (found == m_contextBindings.end()) return; + const EGLCurrentState binding = found->second; + if (!GetRegisteredEGLSurface(binding.DrawSurface)) { + m_contextBindings.erase(found); + return; + } + const auto current = m_eglCurrentThreads.find(std::this_thread::get_id()); + if (current != m_eglCurrentThreads.end() && current->second.Context == binding.Context && + current->second.DrawSurface == binding.DrawSurface && m_eglSurface == binding.DrawSurface) { + return; + } + (void)BackendObject::MakeEGLCurrent(binding.Display, binding.DrawSurface, binding.ReadSurface, binding.Context); } Bool BackendObject_DirectVulkan::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) { @@ -531,8 +567,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { BackendObject::ReleaseEGLResources(); } + Bool BackendObject_DirectVulkan::UsesSurfaceTargets() const { + return MG_Config::Transport != MG_Config::TransportMode::Monolith; + } + + void BackendObject_DirectVulkan::OnEGLSurfaceForgotten(EGLSurface surface) { + if (UsesSurfaceTargets() && pVulkanRenderer) pVulkanRenderer->DestroySurfaceTarget(SurfaceTargetKey(surface)); + } + void BackendObject_DirectVulkan::OnEGLSurfaceReleased(EGLSurface surface) { - (void)surface; + // The renderer outlives its surfaces here: only this surface's target goes (in + // OnEGLSurfaceForgotten, which follows), and every fence stays valid. + if (UsesSurfaceTargets() && pVulkanRenderer) return; // Outstanding fence/timer-query handles now refer to a dead renderer; // treat them as signaled/available with zero results from here on. BumpRendererGeneration(); diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h index eb739b2e..94f86b7a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h @@ -28,6 +28,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { void Initialize() override; void BindSessionStateToThisThread() override; + void OnClientContextBound(Uint64 token) override; Bool InitWindowSurface() override; Bool InitCapabilities() override; Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) override; @@ -50,6 +51,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { private: Bool InitPbufferSurface(EGLint width, EGLint height) override; void OnEGLSurfaceReleased(EGLSurface surface) override; + void OnEGLSurfaceForgotten(EGLSurface surface) override; + // A served session keeps one renderer across all its client's surfaces and switches the + // renderer's surface target instead (VulkanRenderer::ActivateSurfaceTarget). + Bool UsesSurfaceTargets() const; + static Uint64 SurfaceTargetKey(EGLSurface surface) { return reinterpret_cast(surface); } void UpdateAdvertisedExtensions(); void UpdateDynamicBackendParameters(); @@ -60,6 +66,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { // A served session's own copy of Magma's per-process state (MagmaSession.h), bound to its // apply thread. Null in the monolith and every other single-session shape. UniquePtr m_magmaSession; + // The binding each client context was last made current with, for OnClientContextBound. + UnorderedMap m_contextBindings; }; // Single-source-of-truth helpers shared with the driver POST diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp index 04c6da55..bbf0dfbd 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp @@ -300,6 +300,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { return packet; } + VkResult FrameContext::AcquireNextImageInCurrentSlot(VkDevice device, VkSwapchainKHR swapchain, + Uint32& outImageIndex) { + auto& frame = GetCurrent(); + const VkResult result = vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, frame.imageAvailableSemaphore, + VK_NULL_HANDLE, &outImageIndex); + if (result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) frame.imageAvailableSemaphoreConsumed = false; + return result; + } + VkResult FrameContext::WaitAndAcquireNextImage(VkDevice device, VkSwapchainKHR swapchain, Uint32& outImageIndex, Uint64 timeout, VkFence acquireFence) { auto& frame = GetCurrent(); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.h index c12f30bc..50793027 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.h @@ -101,6 +101,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkImageLayout presentLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); SubmitInfoPacket GetSubmitInfo(Bool shouldSubmitCommandBuffer, Uint32 swapchainImageIndex) const; PresentInfoPacket GetPresentInfo(VkSwapchainKHR swapchain, Uint32 imageIndex) const; + // Acquires into the current slot's semaphore WITHOUT the slot-fence wait: for a surface + // brought up mid-frame, whose slot fence this frame has already waited and reset. The + // slot's semaphore must be unsignaled with no pending signal (consumed). + VkResult AcquireNextImageInCurrentSlot(VkDevice device, VkSwapchainKHR swapchain, Uint32& outImageIndex); + // The per-swapchain-image render-finished semaphores belong to one swapchain; a renderer + // that parks a surface parks them with it. + void ExchangeSwapchainSemaphores(Vector& other) { std::swap(m_swapchainImageRenderFinishedSemaphores, other); } VkResult WaitAndAcquireNextImage(VkDevice device, VkSwapchainKHR swapchain, Uint32& outImageIndex, Uint64 timeout = UINT64_MAX, VkFence acquireFence = VK_NULL_HANDLE); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp index 8da8e632..b30ab13c 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp @@ -299,6 +299,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { MGLOG_I("Swapchain created, extent = %dx%d, swapchain imageCount = %d", m_extent.width, m_extent.height, imageCount); + PublishDefaultFramebufferInfo(); + } + + void SwapchainObject::PublishDefaultFramebufferInfo() const { + const VkExtent2D defaultFramebufferExtent = m_surfaceExtent; // Properly initialize Default FBO here auto& defaultFBOInfo = MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo; const Int extentWidth = static_cast(defaultFramebufferExtent.width); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h index 4c36f43e..97ebf6a2 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.h @@ -38,6 +38,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 presentQueueFamily, Uint32 minImageCountHint, VkExtent2D desiredExtent, Optional swapInterval); void Shutdown(VkDevice device); + // Tells the frontend (or, under a transport, the client) the default framebuffer's size + // and formats. Create does this; a renderer switching between surfaces repeats it. + void PublishDefaultFramebufferInfo() const; VkSwapchainKHR GetHandle() const { return m_swapchain; } VkPresentModeKHR GetPresentMode() const { return m_presentMode; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index cd371dbe..0051a166 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -234,6 +234,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } + namespace { + // Diagnostic memory for DescribeWireBufferHistory: the last handles created and destroyed. + struct WireBufferHistory { + static constexpr SizeT kDepth = 256; + MG_Pipe::MGPipeHandle created[kDepth]{}; + MG_Pipe::MGPipeHandle destroyed[kDepth]{}; + SizeT createdCount = 0; + SizeT destroyedCount = 0; + }; + SessionLocal g_wireBufferHistory; + } // namespace + VkBufferManager::WireBufferResource* VkBufferManager::FindWireBuffer(MG_Pipe::MGPipeHandle res) { const auto found = m_wireBuffers.find(WireBufferKey(res)); return found == m_wireBuffers.end() ? nullptr : &found->second; @@ -246,6 +258,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { std::abort(); } m_wireBuffers.try_emplace(WireBufferKey(res)); + { + auto& history = *g_wireBufferHistory; + history.created[history.createdCount++ % 256] = res; + } RespecifyWireBuffer(res, desc, nullptr); } @@ -972,7 +988,29 @@ namespace MobileGL::MG_Backend::DirectVulkan { #endif } + + void VkBufferManager::DescribeWireBufferHistory(MG_Pipe::MGPipeHandle res) const { + const auto& history = *g_wireBufferHistory; + Int createdAgo = -1, destroyedAgo = -1; + for (SizeT i = 0; i < std::min(history.createdCount, WireBufferHistory::kDepth); ++i) { + const auto& h = history.created[(history.createdCount - 1 - i) % WireBufferHistory::kDepth]; + if (h.Slot == res.Slot && h.Gen == res.Gen) { createdAgo = static_cast(i); break; } + } + for (SizeT i = 0; i < std::min(history.destroyedCount, WireBufferHistory::kDepth); ++i) { + const auto& h = history.destroyed[(history.destroyedCount - 1 - i) % WireBufferHistory::kDepth]; + if (h.Slot == res.Slot && h.Gen == res.Gen) { destroyedAgo = static_cast(i); break; } + } + MGLOG_E("Magma wire buffer {slot=%u, gen=%u}: live stores %zu; created %d creates ago, destroyed %d " + "destroys ago (-1 = not in the last %zu); %zu creates, %zu destroys in this session", + res.Slot, res.Gen, m_wireBuffers.size(), createdAgo, destroyedAgo, WireBufferHistory::kDepth, + history.createdCount, history.destroyedCount); + } + void VkBufferManager::DestroyWireBuffer(MG_Pipe::MGPipeHandle res) { + { + auto& history = *g_wireBufferHistory; + history.destroyed[history.destroyedCount++ % WireBufferHistory::kDepth] = res; + } const auto found = m_wireBuffers.find(WireBufferKey(res)); if (found == m_wireBuffers.end()) return; DeferWireRelease(std::move(found->second.buffer), @@ -1135,7 +1173,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (g_activeBufferManager.Get() == this) { g_activeBufferManager.Get() = nullptr; #if MOBILEGL_BUILD_DISAGGREGATED - if (MG_Pipe::MGPipeGetResourceOps() == &g_vulkanWireResourceOps) { + // Under a transport the table is the PROCESS's and serves every session: it dispatches + // to the calling session's own manager (WireManager), so one session's renderer going + // away must not take it from the others - their buffer creates would be dropped from + // then on, and every buffer born after that would be a handle the server never saw. + if (MG_Config::Transport == MG_Config::TransportMode::Monolith && + MG_Pipe::MGPipeGetResourceOps() == &g_vulkanWireResourceOps) { MG_Pipe::MGPipeSetResourceOps(nullptr); } #endif diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index 7266b46a..e6ac279a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -110,6 +110,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { // frontend BufferObject. Acquires expose the full GPU store, without a CPU // pointer: CPU consumers must use ReadWireBuffer for ordered, current bytes. Bool AcquireWireSlice(BufferKind kind, MG_Pipe::MGPipeHandle res, BufferSlice& outSlice); + // The handle names a store this session created (whatever its size, zero included). + Bool IsKnownWireBuffer(MG_Pipe::MGPipeHandle res) { return FindWireBuffer(res) != nullptr; } + // Names, for a Fatal about an unknown handle, whether this manager ever saw it. + void DescribeWireBufferHistory(MG_Pipe::MGPipeHandle res) const; Bool ReadWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, void* dst); Bool CopyWireBufferRangeToSlice(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, const BufferSlice& dst); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp index 88e2eee7..8cf98785 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp @@ -607,6 +607,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { const Bool isDefaultFbo = fbo.IsDefaultFramebuffer(); if (isDefaultFbo) { XXHASH_VERIFY(XXH64_update(m_hashState, &swapchainImageIndex, sizeof(swapchainImageIndex))); + XXHASH_VERIFY(XXH64_update(m_hashState, &m_defaultFramebufferTarget, sizeof(m_defaultFramebufferTarget))); } // sRGB attachments switch between their sRGB and UNORM-twin views with this // capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats. @@ -1528,6 +1529,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { return &insertedIt->second; } + void VkRenderPassManager::PurgeRenderPasses() { + Vector destroyedRenderPasses; + destroyedRenderPasses.reserve(m_renderPasses.size()); + for (auto& [_, entry] : m_renderPasses) destroyedRenderPasses.push_back(entry.renderPass); + m_renderPasses.clear(); + m_rpFastValid = false; + if (!destroyedRenderPasses.empty() && m_evictionObserver != nullptr) { + m_evictionObserver->OnRenderPassesDestroyed(destroyedRenderPasses); + } + } + void VkRenderPassManager::OnPresent() { ++m_frameCounter; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h index b18ab881..708649a0 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h @@ -227,6 +227,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool Initialize(); void Shutdown(); + // The surface target the default framebuffer currently names (VulkanRenderer's surface + // targets): a default-framebuffer pass of one surface is not another's. + void SetDefaultFramebufferTarget(Uint64 serial) { + m_defaultFramebufferTarget = serial; + m_rpFastValid = false; + } + // Destroys every cached pass and framebuffer (not the renderbuffers). The device must be + // idle and no pass may be open: a surface target's image views are going away. + void PurgeRenderPasses(); HashType ComputeHash( const MG_State::GLState::FramebufferObject& fbo, @@ -303,6 +312,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // is reused WITHOUT recomputing the expensive per-draw hash. Invalidated by FBO switch / // version change, swapchain rotation, any attachment image recreation (the two epochs), // or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed). + Uint64 m_defaultFramebufferTarget = 0; Bool m_rpFastValid = false; const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr; // The FBO's never-reused lifetime id joins the raw pointer + Uint16 version: diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 20a40706..aafdca28 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -3478,6 +3478,9 @@ void main() { if (m_device != VK_NULL_HANDLE) { VK_VERIFY(vkDeviceWaitIdle(m_device)); } + // Parked surface targets (ActivateSurfaceTarget) go before the device and instance. + for (auto& [_, target] : m_parkedTargets) DestroyParkedTarget(target); + m_parkedTargets.clear(); #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). @@ -16658,6 +16661,8 @@ void main() { } Bool VulkanRenderer::RecreateSwapchain() { + // No surface target is active (its surface was released): nothing to build on. + if (m_surface == VK_NULL_HANDLE) return false; // Handle cases like minimize on Windows, where swapchain could return a 0x0 extent const auto swapchainCapabilities = SwapchainObject::GetSwapchainCapabilities(m_physicalDevice.handle, m_surface); @@ -16781,6 +16786,159 @@ void main() { } } + void VulkanRenderer::ExchangeActiveTarget(SurfaceTarget& target) { + std::swap(m_activeTargetSerial, target.serial); + std::swap(m_window, target.window); + std::swap(m_platformDisplay, target.platformDisplay); + std::swap(m_platformLibrary, target.platformLibrary); + std::swap(m_platformCloseDisplay, target.platformCloseDisplay); + std::swap(m_fallbackImageReader, target.fallbackImageReader); + std::swap(m_config.SurfaceWidth, target.surfaceWidth); + std::swap(m_config.SurfaceHeight, target.surfaceHeight); + std::swap(m_config.SwapInterval, target.swapInterval); + std::swap(m_presentsToAppWindow, target.presentsToAppWindow); + std::swap(m_swapchainSwapInterval, target.swapchainSwapInterval); + std::swap(m_swapchainResizeRequested, target.swapchainResizeRequested); + std::swap(m_presentSuspended, target.presentSuspended); + std::swap(m_surface, target.surface); + std::swap(m_swapchainObject, target.swapchain); + m_frameContext.ExchangeSwapchainSemaphores(target.renderFinishedSemaphores); + std::swap(m_imageIndexAcquired, target.imageIndexAcquired); + std::swap(m_defaultFramebufferImageIndex, target.defaultFramebufferImageIndex); + } + + void VulkanRenderer::ParkActiveTarget() { + if (m_surface == VK_NULL_HANDLE) return; + // The active surface's recorded work goes to the queue now, while its images are still + // the default framebuffer's. + FlushPendingCommands(); + auto& frame = m_frameContext.GetCurrent(); + // Its acquired image keeps its acquire for when the surface comes back, but the signal + // lives on THIS slot's semaphore, which the next surface acquires with. Wait it out with + // an empty submission; the image is then simply held, and its next submit waits nothing. + if (m_swapchainObject.GetHandle() != VK_NULL_HANDLE && !m_presentSuspended && + !frame.imageAvailableSemaphoreConsumed) { + const VkFence fence = AcquirePooledSubmitFence(); + if (fence == VK_NULL_HANDLE || !SubmitPendingCommandBuffer(frame, fence, /*pooledFence=*/true)) { + MGLOG_E("DirectVulkan: a parked surface's pending acquire could not be waited out"); + } + } + SurfaceTarget parked; + ExchangeActiveTarget(parked); + m_parkedTargets[m_activeTargetKey] = std::move(parked); + m_activeTargetKey = 0; + } + + void VulkanRenderer::OnActiveTargetChanged() { + if (m_swapchainObject.GetHandle() != VK_NULL_HANDLE) { + m_swapchainObject.PublishDefaultFramebufferInfo(); + if (m_programFactory) m_programFactory->SetDefaultFramebufferHeight(m_swapchainObject.GetExtent().height); + } + if (m_renderPassManager) m_renderPassManager->SetDefaultFramebufferTarget(m_activeTargetSerial); + InvalidatePipelineMemo(); + ResetDynamicStateShadow(); + InvalidateSetupDrawSnapshots(); + } + + void VulkanRenderer::ActivateSurfaceTarget(Uint64 key, NativeWindowType window, + const VulkanRendererConfig& surfaceConfig) { + if (key == m_activeTargetKey && m_surface != VK_NULL_HANDLE) return; + ParkActiveTarget(); + auto parked = m_parkedTargets.find(key); + if (parked != m_parkedTargets.end()) { + ExchangeActiveTarget(parked->second); + m_parkedTargets.erase(parked); + // Its acquire was waited when it was parked (ParkActiveTarget). + m_frameContext.GetCurrent().imageAvailableSemaphoreConsumed = true; + } else { + m_activeTargetSerial = m_nextTargetSerial++; + m_window = window; + m_presentsToAppWindow = window != NativeWindowType{}; + m_config.SurfaceWidth = surfaceConfig.SurfaceWidth; + m_config.SurfaceHeight = surfaceConfig.SurfaceHeight; + m_config.SwapInterval = m_presentsToAppWindow ? surfaceConfig.SwapInterval : Nullopt; + m_swapchainSwapInterval.reset(); + m_swapchainResizeRequested = false; + m_presentSuspended = false; + CreateSurface(); + CreateSwapchain(); + m_defaultFramebufferImageIndex = 0; + VK_VERIFY(m_frameContext.InitializeSwapchainSemaphores( + m_device, static_cast(m_swapchainObject.GetImageCount())), + "ActivateSurfaceTarget, InitializeSwapchainSemaphores"); + if (m_swapchainObject.GetHandle() != VK_NULL_HANDLE) { + VkResult result = m_frameContext.AcquireNextImageInCurrentSlot( + m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); + if (result == VK_SUBOPTIMAL_KHR) result = VK_SUCCESS; + VK_VERIFY(result, "ActivateSurfaceTarget, AcquireNextImageInCurrentSlot"); + m_defaultFramebufferImageIndex = m_imageIndexAcquired; + } else { + m_presentSuspended = true; + } + MGLOG_I("DirectVulkan: surface target %llu built (%ux%u, %s)", + static_cast(m_activeTargetSerial), m_swapchainObject.GetExtent().width, + m_swapchainObject.GetExtent().height, m_presentsToAppWindow ? "window" : "pbuffer"); + } + m_activeTargetKey = key; + OnActiveTargetChanged(); + } + + void VulkanRenderer::DestroyParkedTarget(SurfaceTarget& target) { + target.swapchain.Shutdown(m_device); + for (VkSemaphore semaphore : target.renderFinishedSemaphores) { + if (semaphore != VK_NULL_HANDLE) vkDestroySemaphore(m_device, semaphore, nullptr); + } + target.renderFinishedSemaphores.clear(); + if (target.surface != VK_NULL_HANDLE) { + vkDestroySurfaceKHR(m_instance, target.surface, nullptr); + target.surface = VK_NULL_HANDLE; + } +#if defined(VK_USE_PLATFORM_ANDROID_KHR) + if (target.fallbackImageReader != nullptr && target.platformLibrary != nullptr) { + using AImageReaderDeleteFn = void (*)(void*); + auto* imageReaderDelete = + reinterpret_cast(dlsym(target.platformLibrary, "AImageReader_delete")); + if (imageReaderDelete) imageReaderDelete(target.fallbackImageReader); + target.fallbackImageReader = nullptr; + dlclose(target.platformLibrary); + target.platformLibrary = nullptr; + } +#elif defined(VK_USE_PLATFORM_XLIB_KHR) + if (target.platformDisplay != nullptr) { + using XCloseDisplayFn = int (*)(Display*); + auto* closeDisplay = reinterpret_cast(target.platformCloseDisplay); + if (closeDisplay) closeDisplay(static_cast(target.platformDisplay)); + target.platformDisplay = nullptr; + } + if (target.platformLibrary != nullptr) { + dlclose(target.platformLibrary); + target.platformLibrary = nullptr; + } +#endif + } + + void VulkanRenderer::DestroySurfaceTarget(Uint64 key) { + if (key == m_activeTargetKey && m_surface != VK_NULL_HANDLE) ParkActiveTarget(); + auto parked = m_parkedTargets.find(key); + if (parked == m_parkedTargets.end()) return; + // Whatever is recorded may name the target's images (a blit out of it, say): submit it, + // then let the GPU finish with everything before the views go. + FlushPendingCommands(); + VK_VERIFY(vkDeviceWaitIdle(m_device)); + OnSubmitsCompletedUpTo(m_submitCounter); +#if MOBILEGL_BUILD_DISAGGREGATED + DestroyWireDrawPass(); + CollectWireObjects(m_submitCounter, true); + ClearAllWireDrawPassCaches(); +#endif + if (m_renderPassManager) m_renderPassManager->PurgeRenderPasses(); + InvalidatePipelineMemo(); + ResetDynamicStateShadow(); + InvalidateSetupDrawSnapshots(); + DestroyParkedTarget(parked->second); + m_parkedTargets.erase(parked); + } + VkInstance VulkanRenderer::GetInstance() const { return m_instance; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index c6b2304f..0fb85b79 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -427,7 +427,52 @@ namespace MobileGL::MG_Backend::DirectVulkan { // no new swapchain is installed and presentation must stay suspended. Bool RecreateSwapchain(); + // SURFACE TARGETS. One renderer - one device and every GL object's resources - serves + // every EGL surface of its context's client; only the presentation side (VkSurfaceKHR, + // swapchain, its render-finished semaphores, the acquired image, the pbuffer's image + // reader) belongs to a surface. `key` names the client surface. The renderer starts with + // the surface it was constructed for as its active target (SetActiveSurfaceTargetKey). + // Activating another key parks the active target - its recorded work submitted, its + // pending acquire consumed - and brings the other back, or builds it on first use. + void SetActiveSurfaceTargetKey(Uint64 key) { m_activeTargetKey = key; } + void ActivateSurfaceTarget(Uint64 key, NativeWindowType window, const VulkanRendererConfig& surfaceConfig); + // Drops a surface's target, active or parked. A no-op for a key that has none. + void DestroySurfaceTarget(Uint64 key); + private: + // The presentation half of the renderer, parked while another surface is active. Its + // fields mirror the renderer members ExchangeActiveTarget swaps them with. + struct SurfaceTarget { + Uint64 serial = 0; + NativeWindowType window = 0; + void* platformDisplay = nullptr; + void* platformLibrary = nullptr; + void* platformCloseDisplay = nullptr; + void* fallbackImageReader = nullptr; + Uint32 surfaceWidth = 0; + Uint32 surfaceHeight = 0; + Optional swapInterval; + Bool presentsToAppWindow = false; + Optional swapchainSwapInterval; + Bool swapchainResizeRequested = false; + Bool presentSuspended = false; + VkSurfaceKHR surface = VK_NULL_HANDLE; + SwapchainObject swapchain; + Vector renderFinishedSemaphores; + Uint imageIndexAcquired = 0; + Uint defaultFramebufferImageIndex = 0; + }; + void ExchangeActiveTarget(SurfaceTarget& target); + // Submits the active target's work, consumes its pending acquire and moves it into + // m_parkedTargets. Leaves the renderer with no target. + void ParkActiveTarget(); + void OnActiveTargetChanged(); + void DestroyParkedTarget(SurfaceTarget& target); + UnorderedMap m_parkedTargets; + Uint64 m_activeTargetKey = 0; + Uint64 m_activeTargetSerial = 1; + Uint64 m_nextTargetSerial = 2; + // Tiered emission for an already-set-up multi-draw batch (state bound, index // buffer bound for the indexed form). Tier 1: VK_EXT_multi_draw. Tier 2: one // vkCmdDraw(Indexed)Indirect over a transient command array. Tier 3: unrolled diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index e4e016aa..ef0afd8f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -306,8 +306,20 @@ "MGPipe: Fatal{ProtocolCorruption, \"Magma:client-vertex-array\"} location=%u: an " "attribute the program reads carries no buffer handle", location); BufferSlice slice{}; - if (!m_bufferManager.AcquireWireSlice(BufferKind::Vertex, resource.Res, slice)) - MagmaWireFatal("vertex-buffer-resource"); + if (!m_bufferManager.AcquireWireSlice(BufferKind::Vertex, resource.Res, slice)) { + // A buffer with no data store is legal GL to bind (glGenBuffers + glBindBuffer, no + // glBufferData), and the client mints its handle at construction while the create + // only crosses with the first respecify (PipeFill.cpp's M-1) - so the handle may be + // one this server never saw, or one it holds with zero bytes. Either way the draw + // reads no storage: it is declined, as the GL draws nothing defined from it. + static Bool s_described = false; + if (!s_described && !m_bufferManager.IsKnownWireBuffer(resource.Res)) { + s_described = true; + m_bufferManager.DescribeWireBufferHistory(resource.Res); + } + MGL_WIRE_DECLINE_AT(VertexAttributeWindow, "an enabled attribute's buffer has no storage"); + return false; + } const VkDeviceSize offset = input.bindingBaseOffsets[binding]; const auto type = static_cast(attr.Type); const SizeT elementSize = VertexInputStateFactory::GetAttributeByteSize(type, attr.Size, attr.IsBgra); @@ -387,8 +399,16 @@ const void* bytes = nullptr; if (cpuIndices) bytes = reinterpret_cast(indices->indexByteOffset); else { - if (!m_bufferManager.AcquireWireSlice(BufferKind::Index, state.IndexBuffer.Res, indexSlice)) - MagmaWireFatal("index-buffer-resource"); + if (!m_bufferManager.AcquireWireSlice(BufferKind::Index, state.IndexBuffer.Res, indexSlice)) { + // As for a vertex buffer: a bound element buffer with no data store yet. + static Bool s_described = false; + if (!s_described && !m_bufferManager.IsKnownWireBuffer(state.IndexBuffer.Res)) { + s_described = true; + m_bufferManager.DescribeWireBufferHistory(state.IndexBuffer.Res); + } + MGL_WIRE_DECLINE_AT(IndexWindow, "the element buffer has no storage"); + return false; + } const Uint64 offset = state.IndexBuffer.Offset + indices->indexByteOffset; if (offset < state.IndexBuffer.Offset || offset > indexSlice.size || indices->indexByteSize > indexSlice.size - offset) { diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index 70b0b5ec..20bfe2dc 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -12,6 +12,7 @@ #include #include "ServerSession.h" +#include "ServerLoop.h" #include "../Transport/ReplySlot.h" #include "StagedTextureStore.h" @@ -1504,6 +1505,10 @@ namespace MobileGL::MG_Remote::Server { // without a driver call when the context it resolves to is already the one bound here. if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { (void)MG_Backend::DirectGLES::MakeNativeContextCurrentForBoundToken(); + // The backend half for one that keeps a single native surface binding (Magma). + if (MG_Backend::BackendObject* backend = ServerLoopInstance().Backend()) { + backend->OnClientContextBound(bind.ClientContextToken); + } } #endif return true;