@@ -161,6 +161,10 @@ PlVkRenderer::~PlVkRenderer()
161161 SDL_assert (!m_HasPendingSwapchainFrame);
162162
163163 if (m_Vulkan != nullptr ) {
164+ #ifdef PLVK_USE_EARLY_RENDER_TO_WAIT
165+ pl_tex_destroy (m_Vulkan->gpu , &m_EmptyOverlay.tex );
166+ #endif
167+
164168 for (int i = 0 ; i < (int )SDL_arraysize (m_Overlays); i++) {
165169 pl_tex_destroy (m_Vulkan->gpu , &m_Overlays[i].overlay .tex );
166170 pl_tex_destroy (m_Vulkan->gpu , &m_Overlays[i].stagingOverlay .tex );
@@ -538,6 +542,25 @@ bool PlVkRenderer::initialize(PDECODER_PARAMETERS params)
538542 return false ;
539543 }
540544
545+ #ifdef PLVK_USE_EARLY_RENDER_TO_WAIT
546+ SDL_Surface *emptySurface = SDL_CreateRGBSurfaceWithFormat (0 , 1 , 1 , 0 , SDL_PIXELFORMAT_ARGB8888 );
547+ if (emptySurface == nullptr ) {
548+ SDL_LogError (SDL_LOG_CATEGORY_APPLICATION ,
549+ " SDL_CreateRGBSurfaceWithFormat() failed: %s" , SDL_GetError ());
550+ return false ;
551+ }
552+
553+ // emptySurface is owned now by the overlay upload code (even on failure)
554+ if (!createOverlay (&m_EmptyOverlay, emptySurface)) {
555+ return false ;
556+ }
557+
558+ m_EmptyOverlayPart.src = { 0 .0f , 0 .0f , 1 .0f , 1 .0f };
559+ m_EmptyOverlayPart.dst = { 0 .0f , 0 .0f , 1 .0f , 1 .0f };
560+ m_EmptyOverlay.num_parts = 1 ;
561+ m_EmptyOverlay.parts = &m_EmptyOverlayPart;
562+ #endif
563+
541564 // We only need an hwaccel device context if we're going to act as the backend renderer too
542565 if (m_HwDeviceType == AV_HWDEVICE_TYPE_VULKAN ) {
543566 m_HwDeviceCtx = av_hwdevice_ctx_alloc (AV_HWDEVICE_TYPE_VULKAN );
@@ -840,6 +863,27 @@ bool PlVkRenderer::isSurfacePresentationSupportedByPhysicalDevice(VkPhysicalDevi
840863 return false ;
841864}
842865
866+ void PlVkRenderer::beginRenderTiming ()
867+ {
868+ #ifdef PLVK_USE_DYNAMIC_SWAPCHAIN_DEPTH
869+ m_RenderStartTime = SDL_GetTicks ();
870+ #endif
871+ }
872+
873+ void PlVkRenderer::endRenderTiming ()
874+ {
875+ #ifdef PLVK_USE_DYNAMIC_SWAPCHAIN_DEPTH
876+ // Trigger a switch to triple-buffered mode if our frame presentation time
877+ // exceeds 110% of the frame interval for half a second of frames.
878+ if (SDL_GetTicks () - m_RenderStartTime > (1100U / m_MaxVideoFps)) {
879+ m_DelayedPresents++;
880+ }
881+ else if (m_DelayedPresents > 0 ) {
882+ m_DelayedPresents--;
883+ }
884+ #endif
885+ }
886+
843887void PlVkRenderer::waitToRender ()
844888{
845889 // Check if the GPU has failed before doing anything else
@@ -879,6 +923,25 @@ void PlVkRenderer::waitToRender()
879923 // renderFrame() wasn't called after waitToRender().
880924 if (pl_swapchain_start_frame (m_Swapchain, &m_SwapchainFrame)) {
881925 m_HasPendingSwapchainFrame = true ;
926+
927+ #ifdef PLVK_USE_EARLY_RENDER_TO_WAIT
928+ // This is a workaround for MoltenVK which lazily fetches a drawable when the
929+ // swapchain frame is first modified (rather than in pl_swapchain_start_frame()).
930+ // By rendering an empty overlay on the swapchain here, we will trigger this wait
931+ // in the desired context (before we've latched the next frame to present), rather
932+ // than in the renderFrame() path where delays directly increase video latency.
933+ pl_frame targetFrame;
934+ pl_frame_from_swapchain (&targetFrame, &m_SwapchainFrame);
935+ targetFrame.num_overlays = 1 ;
936+ targetFrame.overlays = &m_EmptyOverlay;
937+
938+ beginRenderTiming ();
939+ if (!pl_render_image (m_Renderer, nullptr , &targetFrame, &pl_render_fast_params)) {
940+ SDL_LogWarn (SDL_LOG_CATEGORY_APPLICATION ,
941+ " pl_render_image() failed during render wait" );
942+ }
943+ endRenderTiming ();
944+ #endif
882945 }
883946}
884947
@@ -1005,8 +1068,9 @@ void PlVkRenderer::renderFrame(AVFrame *frame)
10051068 targetFrame.crop .x1 = dst.x + dst.w ;
10061069 targetFrame.crop .y1 = dst.y + dst.h ;
10071070
1008- #ifdef PLVK_USE_DYNAMIC_SWAPCHAIN_DEPTH
1009- Uint32 renderStartTime = SDL_GetTicks ();
1071+ #ifndef PLVK_USE_EARLY_RENDER_TO_WAIT
1072+ // For PLVK_USE_EARLY_RENDER_TO_WAIT, we already timed our early render in waitToRender()
1073+ beginRenderTiming ();
10101074#endif
10111075
10121076 // Render the video image and overlays into the swapchain buffer
@@ -1031,16 +1095,11 @@ void PlVkRenderer::renderFrame(AVFrame *frame)
10311095 goto UnmapExit;
10321096 }
10331097
1034- #ifdef PLVK_USE_DYNAMIC_SWAPCHAIN_DEPTH
1035- // Trigger a switch to triple-buffered mode if our frame presentation time
1036- // exceeds 110% of the frame interval for half a second of frames.
1037- if (SDL_GetTicks () - renderStartTime > (1100U / m_MaxVideoFps)) {
1038- m_DelayedPresents++;
1039- }
1040- else if (m_DelayedPresents > 0 ) {
1041- m_DelayedPresents--;
1042- }
1098+ #ifndef PLVK_USE_EARLY_RENDER_TO_WAIT
1099+ endRenderTiming ();
1100+ #endif
10431101
1102+ #ifdef PLVK_USE_DYNAMIC_SWAPCHAIN_DEPTH
10441103 if (m_DelayedPresents == m_MaxVideoFps / 2 && m_SwapchainDepth < 2 ) {
10451104 SDL_LogWarn (SDL_LOG_CATEGORY_APPLICATION ,
10461105 " Switching to triple-buffered swapchain after delayed presentations" );
@@ -1097,85 +1156,94 @@ bool PlVkRenderer::testRenderFrame(AVFrame *frame)
10971156 return true ;
10981157}
10991158
1100- void PlVkRenderer::notifyOverlayUpdated (Overlay::OverlayType type)
1159+ // Takes ownership of surface in all cases!
1160+ bool PlVkRenderer::createOverlay (pl_overlay* overlay, SDL_Surface* surface)
11011161{
1102- SDL_Surface* newSurface = Session::get ()->getOverlayManager ().getUpdatedOverlaySurface (type);
1103- if (newSurface == nullptr && Session::get ()->getOverlayManager ().isOverlayEnabled (type)) {
1104- // The overlay is enabled and there is no new surface. Leave the old texture alone.
1105- return ;
1106- }
1107-
1108- SDL_AtomicLock (&m_OverlayLock);
1109- // We want to clear the staging overlay flag even if a staging overlay is still present,
1110- // since this ensures the render thread will not read from a partially initialized pl_tex
1111- // as we modify or recreate the staging overlay texture outside the overlay lock.
1112- m_Overlays[type].hasStagingOverlay = false ;
1113- SDL_AtomicUnlock (&m_OverlayLock);
1114-
1115- // If there's no new staging overlay, free the old staging overlay texture.
1116- // NB: This is safe to do outside the overlay lock because we're guaranteed
1117- // to not have racing readers/writers if hasStagingOverlay is false.
1118- if (newSurface == nullptr ) {
1119- pl_tex_destroy (m_Vulkan->gpu , &m_Overlays[type].stagingOverlay .tex );
1120- SDL_zero (m_Overlays[type].stagingOverlay );
1121- return ;
1122- }
1123-
11241162 // Find a compatible texture format
1125- SDL_assert (newSurface ->format ->format == SDL_PIXELFORMAT_ARGB8888 );
1163+ SDL_assert (surface ->format ->format == SDL_PIXELFORMAT_ARGB8888 );
11261164 pl_fmt texFormat = pl_find_named_fmt (m_Vulkan->gpu , " bgra8" );
11271165 if (!texFormat) {
1128- SDL_FreeSurface (newSurface);
11291166 SDL_LogError (SDL_LOG_CATEGORY_APPLICATION ,
11301167 " pl_find_named_fmt(bgra8) failed" );
1131- return ;
1168+ SDL_FreeSurface (surface);
1169+ return false ;
11321170 }
11331171
11341172 // Create a new texture for this overlay if necessary, otherwise reuse the existing texture.
11351173 // NB: We're guaranteed that the render thread won't be reading this concurrently because
11361174 // we set hasStagingOverlay to false above.
11371175 pl_tex_params texParams = {};
1138- texParams.w = newSurface ->w ;
1139- texParams.h = newSurface ->h ;
1176+ texParams.w = surface ->w ;
1177+ texParams.h = surface ->h ;
11401178 texParams.format = texFormat;
11411179 texParams.sampleable = true ;
11421180 texParams.host_writable = true ;
11431181 texParams.blit_src = !!(texFormat->caps & PL_FMT_CAP_BLITTABLE );
11441182 texParams.debug_tag = PL_DEBUG_TAG ;
1145- if (!pl_tex_recreate (m_Vulkan->gpu , &m_Overlays[type]. stagingOverlay . tex , &texParams)) {
1146- pl_tex_destroy (m_Vulkan->gpu , &m_Overlays[type]. stagingOverlay . tex );
1147- SDL_zero (m_Overlays[type]. stagingOverlay );
1148- SDL_FreeSurface (newSurface );
1183+ if (!pl_tex_recreate (m_Vulkan->gpu , &overlay-> tex , &texParams)) {
1184+ pl_tex_destroy (m_Vulkan->gpu , &overlay-> tex );
1185+ SDL_zerop (overlay );
1186+ SDL_FreeSurface (surface );
11491187 SDL_LogError (SDL_LOG_CATEGORY_APPLICATION ,
11501188 " pl_tex_recreate() failed" );
1151- return ;
1189+ return false ;
11521190 }
11531191
11541192 // Upload the surface data to the new texture
1155- SDL_assert (!SDL_MUSTLOCK (newSurface ));
1193+ SDL_assert (!SDL_MUSTLOCK (surface ));
11561194 pl_tex_transfer_params xferParams = {};
1157- xferParams.tex = m_Overlays[type]. stagingOverlay . tex ;
1158- xferParams.row_pitch = (size_t )newSurface ->pitch ;
1159- xferParams.ptr = newSurface ->pixels ;
1195+ xferParams.tex = overlay-> tex ;
1196+ xferParams.row_pitch = (size_t )surface ->pitch ;
1197+ xferParams.ptr = surface ->pixels ;
11601198 xferParams.callback = overlayUploadComplete;
1161- xferParams.priv = newSurface ;
1199+ xferParams.priv = surface ;
11621200 if (!pl_tex_upload (m_Vulkan->gpu , &xferParams)) {
1163- pl_tex_destroy (m_Vulkan->gpu , &m_Overlays[type]. stagingOverlay . tex );
1164- SDL_zero (m_Overlays[type]. stagingOverlay );
1165- SDL_FreeSurface (newSurface );
1201+ pl_tex_destroy (m_Vulkan->gpu , &overlay-> tex );
1202+ SDL_zerop (overlay );
1203+ SDL_FreeSurface (surface );
11661204 SDL_LogError (SDL_LOG_CATEGORY_APPLICATION ,
11671205 " pl_tex_upload() failed" );
1168- return ;
1206+ return false ;
11691207 }
11701208
1171- // newSurface is now owned by the texture upload process. It will be freed in overlayUploadComplete()
1172- newSurface = nullptr ;
1209+ // surface is now owned by the texture upload process. It will be freed in overlayUploadComplete()
11731210
11741211 // Initialize the rest of the overlay params
1175- m_Overlays[type].stagingOverlay .mode = PL_OVERLAY_NORMAL ;
1176- m_Overlays[type].stagingOverlay .coords = PL_OVERLAY_COORDS_DST_FRAME ;
1177- m_Overlays[type].stagingOverlay .repr = pl_color_repr_rgb;
1178- m_Overlays[type].stagingOverlay .color = pl_color_space_srgb;
1212+ overlay->mode = PL_OVERLAY_NORMAL ;
1213+ overlay->coords = PL_OVERLAY_COORDS_DST_FRAME ;
1214+ overlay->repr = pl_color_repr_rgb;
1215+ overlay->color = pl_color_space_srgb;
1216+ return true ;
1217+ }
1218+
1219+ void PlVkRenderer::notifyOverlayUpdated (Overlay::OverlayType type)
1220+ {
1221+ SDL_Surface* newSurface = Session::get ()->getOverlayManager ().getUpdatedOverlaySurface (type);
1222+ if (newSurface == nullptr && Session::get ()->getOverlayManager ().isOverlayEnabled (type)) {
1223+ // The overlay is enabled and there is no new surface. Leave the old texture alone.
1224+ return ;
1225+ }
1226+
1227+ SDL_AtomicLock (&m_OverlayLock);
1228+ // We want to clear the staging overlay flag even if a staging overlay is still present,
1229+ // since this ensures the render thread will not read from a partially initialized pl_tex
1230+ // as we modify or recreate the staging overlay texture outside the overlay lock.
1231+ m_Overlays[type].hasStagingOverlay = false ;
1232+ SDL_AtomicUnlock (&m_OverlayLock);
1233+
1234+ // If there's no new staging overlay, free the old staging overlay texture.
1235+ // NB: This is safe to do outside the overlay lock because we're guaranteed
1236+ // to not have racing readers/writers if hasStagingOverlay is false.
1237+ if (newSurface == nullptr ) {
1238+ pl_tex_destroy (m_Vulkan->gpu , &m_Overlays[type].stagingOverlay .tex );
1239+ SDL_zero (m_Overlays[type].stagingOverlay );
1240+ return ;
1241+ }
1242+
1243+ // newSurface is now owned by the texture upload process
1244+ if (!createOverlay (&m_Overlays[type].stagingOverlay , newSurface)) {
1245+ return ;
1246+ }
11791247
11801248 // Make this staging overlay visible to the render thread
11811249 SDL_AtomicLock (&m_OverlayLock);
0 commit comments