2525
2626#include < algorithm>
2727#include < cassert>
28+ #include < cstdlib>
2829#include < cstring>
2930#include < iostream>
3031#include < vector>
@@ -190,6 +191,19 @@ namespace Rv
190191 using namespace TwkApp ;
191192 using namespace IPCore ;
192193
194+ namespace
195+ {
196+ // Forces the CPU pack-and-upload present path regardless of GPU/driver.
197+ // Useful for exercising the fallback, which the NVIDIA optimal-tiling fix
198+ // otherwise makes rare. Read once. Platform-neutral (unlike the Windows-only
199+ // GL-interop entry-point helpers above).
200+ bool forceCpuPresentation ()
201+ {
202+ static const bool forced = getenv (" RV_VULKAN_FORCE_CPU_PRESENT" ) != nullptr ;
203+ return forced;
204+ }
205+ } // namespace
206+
193207 QTVulkanVideoDevice::QTVulkanVideoDevice (VideoModule* module , const string& name, VulkanView* view, QWidget* eventWidget)
194208 : TwkGLF::GLVideoDevice(module , name, VideoDevice::ImageOutput | VideoDevice::ProvidesSync | VideoDevice::SubWindow)
195209 , m_view(view)
@@ -202,7 +216,7 @@ namespace Rv
202216 QTVulkanVideoDevice::~QTVulkanVideoDevice ()
203217 {
204218 // Delete the FBO and its colour texture while the GL context is current.
205- if (m_glContext && (m_fbo || m_fboColorTex || m_glMemoryObject[0 ]))
219+ if (m_glContext && (m_fbo || m_fboColorTex || m_glMemoryObject[0 ] || m_cpuFlipFbo ))
206220 {
207221 m_glContext->makeCurrent (m_offscreenSurface);
208222 delete m_fbo;
@@ -214,6 +228,7 @@ namespace Rv
214228 }
215229 for (uint32_t i = 0 ; i < VulkanView::FRAMES_IN_FLIGHT ; ++i)
216230 cleanupSharedGLObjects (i);
231+ cleanupCpuFallbackTarget ();
217232 m_glContext->doneCurrent ();
218233 }
219234
@@ -461,6 +476,78 @@ namespace Rv
461476 m_sharedHeight[slot] = 0 ;
462477 }
463478
479+ void QTVulkanVideoDevice::ensureCpuFallbackTarget (int w, int h) const
480+ {
481+ if (m_cpuFlipFbo && m_cpuFlipWidth == w && m_cpuFlipHeight == h)
482+ return ;
483+
484+ cleanupCpuFallbackTarget ();
485+
486+ // RGB10_A2 color target so the Y-flip blit quantizes to 10-bit and the
487+ // packed readback below is a direct copy (no conversion in glReadPixels).
488+ glGenTextures (1 , &m_cpuFlipTex);
489+ glBindTexture (GL_TEXTURE_2D , m_cpuFlipTex);
490+ glTexImage2D (GL_TEXTURE_2D , 0 , GL_RGB10_A2 , w, h, 0 , GL_RGBA , GL_UNSIGNED_INT_2_10_10_10_REV , nullptr );
491+ glTexParameteri (GL_TEXTURE_2D , GL_TEXTURE_MIN_FILTER , GL_NEAREST );
492+ glTexParameteri (GL_TEXTURE_2D , GL_TEXTURE_MAG_FILTER , GL_NEAREST );
493+ glBindTexture (GL_TEXTURE_2D , 0 );
494+
495+ glGenFramebuffersEXT (1 , &m_cpuFlipFbo);
496+ glBindFramebufferEXT (GL_FRAMEBUFFER_EXT , m_cpuFlipFbo);
497+ glFramebufferTexture2DEXT (GL_FRAMEBUFFER_EXT , GL_COLOR_ATTACHMENT0_EXT , GL_TEXTURE_2D , m_cpuFlipTex, 0 );
498+ glBindFramebufferEXT (GL_FRAMEBUFFER_EXT , 0 );
499+
500+ m_cpuFlipWidth = w;
501+ m_cpuFlipHeight = h;
502+ }
503+
504+ void QTVulkanVideoDevice::cleanupCpuFallbackTarget () const
505+ {
506+ if (m_cpuFlipFbo)
507+ {
508+ glDeleteFramebuffersEXT (1 , &m_cpuFlipFbo);
509+ m_cpuFlipFbo = 0 ;
510+ }
511+ if (m_cpuFlipTex)
512+ {
513+ glDeleteTextures (1 , &m_cpuFlipTex);
514+ m_cpuFlipTex = 0 ;
515+ }
516+ m_cpuFlipWidth = 0 ;
517+ m_cpuFlipHeight = 0 ;
518+ }
519+
520+ void QTVulkanVideoDevice::presentCpuFallback (int w, int h) const
521+ {
522+ TwkGLF::GLFBO * fbo = m_fbo;
523+
524+ // Pack in the swapchain's channel order. glReadPixels with
525+ // GL_UNSIGNED_INT_2_10_10_10_REV packs A2B10G10R10 (R low) for GL_RGBA and
526+ // A2R10G10B10 (R high) for GL_BGRA, so the read format selects the layout
527+ // directly with no CPU conversion. Linux/RADV surfaces commonly offer only
528+ // A2R10G10B10.
529+ const VkFormat scFmt = m_view ? m_view->swapchainFormat () : VK_FORMAT_A2B10G10R10_UNORM_PACK32 ;
530+ const GLenum readFormat = (scFmt == VK_FORMAT_A2R10G10B10_UNORM_PACK32 ) ? GL_BGRA : GL_RGBA ;
531+
532+ ensureCpuFallbackTarget (w, h);
533+
534+ // Y-flip blit (GL bottom-left -> Vulkan top-left) into the RGB10_A2 target,
535+ // so glReadPixels below reads top-down and packs to the swapchain layout.
536+ glBindFramebufferEXT (GL_READ_FRAMEBUFFER_EXT , fbo->fboID ());
537+ glBindFramebufferEXT (GL_DRAW_FRAMEBUFFER_EXT , m_cpuFlipFbo);
538+ glBlitFramebufferEXT (0 , 0 , w, h, 0 , h, w, 0 , GL_COLOR_BUFFER_BIT , GL_NEAREST );
539+ glBindFramebufferEXT (GL_READ_FRAMEBUFFER_EXT , m_cpuFlipFbo);
540+
541+ // GL-packed readback: glReadPixels stalls until the flip blit finishes, but
542+ // the driver packs directly to the swapchain bit layout, so there is no
543+ // per-pixel CPU pack loop.
544+ m_cpuPackedScratch.resize (static_cast <size_t >(w) * h);
545+ glReadPixels (0 , 0 , w, h, readFormat, GL_UNSIGNED_INT_2_10_10_10_REV , m_cpuPackedScratch.data ());
546+ m_view->presentPixelData (m_cpuPackedScratch.data (), w, h);
547+
548+ glBindFramebufferEXT (GL_FRAMEBUFFER_EXT , fbo->fboID ()); // restore
549+ }
550+
464551 // --------------------------------------------------------------------------
465552 // syncBuffers
466553 // --------------------------------------------------------------------------
@@ -489,16 +576,19 @@ namespace Rv
489576 if (!m_glContext->makeCurrent (m_offscreenSurface))
490577 return ;
491578
492- // Get shared image info from VulkanView
579+ // Get shared image info from VulkanView. RV_VULKAN_FORCE_CPU_PRESENT skips
580+ // interop entirely so getSharedImageInfo() never allocates a shared image
581+ // and the CPU fallback below runs.
493582#ifdef PLATFORM_WINDOWS
494583 // Probe the EXT_memory_object/EXT_semaphore (+ _win32) entry points
495584 // while the GL context is current. If the driver does not expose them,
496585 // skip the Vulkan-side export work entirely and fall through to the
497586 // CPU pack-and-upload path below.
498- const bool glInteropAvailable = loadGLInteropExtensions ();
587+ const bool glInteropAvailable = ! forceCpuPresentation () && loadGLInteropExtensions ();
499588 const VulkanView::SharedImageInfo* sharedInfo = glInteropAvailable ? m_view->getSharedImageInfo (w, h) : nullptr ;
500589#else
501- const bool glInteropAvailable = GLEW_EXT_memory_object && GLEW_EXT_semaphore && GLEW_EXT_memory_object_fd && GLEW_EXT_semaphore_fd;
590+ const bool glInteropAvailable =
591+ !forceCpuPresentation () && GLEW_EXT_memory_object && GLEW_EXT_semaphore && GLEW_EXT_memory_object_fd && GLEW_EXT_semaphore_fd;
502592 const VulkanView::SharedImageInfo* sharedInfo = glInteropAvailable ? m_view->getSharedImageInfo (w, h) : nullptr ;
503593#endif
504594
@@ -521,38 +611,10 @@ namespace Rv
521611
522612 if (!sharedInfo)
523613 {
524- // Fallback to CPU readback if GPU interop fails. The packed 32-bit
525- // words are copied unconverted into the swapchain image, so pack in
526- // the swapchain's own component order: A2B10G10R10 (R in low bits,
527- // == GL_RGB10_A2) or A2R10G10B10 (R in high bits). Linux/RADV
528- // surfaces commonly offer only A2R10G10B10.
529- const VkFormat scFmt = m_view ? m_view->swapchainFormat () : VK_FORMAT_A2B10G10R10_UNORM_PACK32 ;
530- const bool rgbOrder = (scFmt == VK_FORMAT_A2R10G10B10_UNORM_PACK32 );
531- fbo->bind ();
532- glFinish ();
533- const size_t floatCount = static_cast <size_t >(w) * h * 4 ;
534- std::vector<float > floatPx (floatCount);
535- glReadPixels (0 , 0 , w, h, GL_RGBA , GL_FLOAT , floatPx.data ());
536- fbo->unbind ();
537-
538- std::vector<uint32_t > packed (static_cast <size_t >(w) * h);
539- for (int y = 0 ; y < h; ++y)
540- {
541- const float * src = floatPx.data () + (size_t )(h - 1 - y) * w * 4 ;
542- uint32_t * dst = packed.data () + (size_t )y * w;
543- for (int x = 0 ; x < w; ++x, src += 4 )
544- {
545- const float r = std::max (0 .f , std::min (1 .f , src[0 ]));
546- const float g = std::max (0 .f , std::min (1 .f , src[1 ]));
547- const float b = std::max (0 .f , std::min (1 .f , src[2 ]));
548- const uint32_t ri = static_cast <uint32_t >(r * 1023 .f + 0 .5f ) & 0x3FF ;
549- const uint32_t gi = static_cast <uint32_t >(g * 1023 .f + 0 .5f ) & 0x3FF ;
550- const uint32_t bi = static_cast <uint32_t >(b * 1023 .f + 0 .5f ) & 0x3FF ;
551- // A2R10G10B10: A|R|G|B (R high). A2B10G10R10: A|B|G|R (R low).
552- dst[x] = rgbOrder ? ((3u << 30 ) | (ri << 20 ) | (gi << 10 ) | bi) : ((3u << 30 ) | (bi << 20 ) | (gi << 10 ) | ri);
553- }
554- }
555- m_view->presentPixelData (packed.data (), w, h);
614+ // No zero-copy interop this frame: pack + present via the CPU fallback.
615+ // The GL-packed RGB10_A2 readback handles the Y flip and the swapchain
616+ // channel order (A2B10G10R10 / A2R10G10B10) without a per-pixel loop.
617+ presentCpuFallback (w, h);
556618 return ;
557619 }
558620
0 commit comments