Skip to content

Commit 3384afc

Browse files
feat(vulkan): implement CPU fallback for Vulkan presentation
- Introduced a CPU fallback mechanism in QTVulkanVideoDevice to handle scenarios where GPU interop is unavailable. - Added methods to ensure and clean up CPU fallback targets, allowing for efficient readback of pixel data. - Enhanced the present function to utilize the CPU fallback, ensuring compatibility across platforms when Vulkan interop fails. These changes improve the robustness of the Vulkan integration, providing a reliable alternative for rendering when GPU resources are not accessible. Signed-off-by: Cédrik Fuoco <cedrik.fuoco@autodesk.com>
1 parent d92df96 commit 3384afc

2 files changed

Lines changed: 119 additions & 36 deletions

File tree

src/lib/app/RvCommon/QTVulkanVideoDevice.cpp

Lines changed: 98 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
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

src/lib/app/RvCommon/RvCommon/QTVulkanVideoDevice.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <cstdint>
1313
#include <string>
1414
#include <memory>
15+
#include <vector>
1516

1617
QT_BEGIN_NAMESPACE
1718
class QOpenGLContext;
@@ -110,6 +111,26 @@ namespace Rv
110111
mutable std::array<int, VulkanView::FRAMES_IN_FLIGHT> m_sharedHeight{};
111112

112113
void cleanupSharedGLObjects(uint32_t slot) const;
114+
115+
// CPU-fallback GL state (used only when GPU interop is unavailable or
116+
// refused). A flipped RGB10_A2 blit target lets GL pack the 10-bit pixels
117+
// directly with glReadPixels(GL_UNSIGNED_INT_2_10_10_10_REV) and handle the
118+
// Y flip, eliminating the per-pixel CPU pack loop. The readback format
119+
// (GL_RGBA vs GL_BGRA) selects the swapchain's channel order
120+
// (A2B10G10R10 / A2R10G10B10). Not ringed: the fallback is a synchronous
121+
// readback, so a single reused target is sufficient.
122+
mutable GLuint m_cpuFlipFbo{0};
123+
mutable GLuint m_cpuFlipTex{0};
124+
mutable int m_cpuFlipWidth{0};
125+
mutable int m_cpuFlipHeight{0};
126+
mutable std::vector<uint32_t> m_cpuPackedScratch;
127+
128+
void ensureCpuFallbackTarget(int w, int h) const;
129+
void cleanupCpuFallbackTarget() const;
130+
131+
// Pack + present the framebuffer via the CPU fallback (GL-packed RGB10_A2
132+
// readback). Used when no zero-copy interop path is available.
133+
void presentCpuFallback(int w, int h) const;
113134
};
114135

115136
} // namespace Rv

0 commit comments

Comments
 (0)