Skip to content

Commit 6e23177

Browse files
committed
Fix blocking during rendering rather than in waitToRender() on MoltenVK
MoltenVK does a lazy fetch of the drawable only when it's being modified, so waitToRender() is completely ineffective as is. Rendering a small empty overlay in waitToRender() forces the next drawable wait to happen in waitToRender(), before we pick the current frame to render. This removes one display refresh period of latency when using V-sync.
1 parent d997a0d commit 6e23177

2 files changed

Lines changed: 142 additions & 59 deletions

File tree

app/streaming/video/ffmpeg-renderers/plvk.cpp

Lines changed: 127 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
843887
void 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);

app/streaming/video/ffmpeg-renderers/plvk.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ class MetalVulkanTextureFactory {
2828
// from getting a new drawable while the current one is getting scanned out.
2929
#define PLVK_USE_DYNAMIC_SWAPCHAIN_DEPTH 1
3030

31+
// MoltenVK will block for the next drawable when we render the next frame
32+
// rather than inside pl_swapchain_start_frame(), so we will force it to wait
33+
// by rendering some no-op work right after we get the new swapchain frame.
34+
#define PLVK_USE_EARLY_RENDER_TO_WAIT 1
35+
3136
#endif
3237

3338
class PlVkRenderer : public IFFmpegRenderer {
@@ -54,7 +59,11 @@ class PlVkRenderer : public IFFmpegRenderer {
5459
static void unlockQueue(AVHWDeviceContext *dev_ctx, uint32_t queue_family, uint32_t index);
5560
static void overlayUploadComplete(void* opaque);
5661

62+
void beginRenderTiming();
63+
void endRenderTiming();
64+
5765
bool createSwapchain(int depth);
66+
bool createOverlay(pl_overlay* overlay, SDL_Surface* surface);
5867
bool mapAvFrameToPlacebo(const AVFrame *frame, pl_frame* mappedFrame);
5968
void unmapAvFrameFromPlacebo(const AVFrame *frame, pl_frame* mappedFrame);
6069
bool populateQueues(int videoFormat);
@@ -76,6 +85,7 @@ class PlVkRenderer : public IFFmpegRenderer {
7685

7786
#ifdef PLVK_USE_DYNAMIC_SWAPCHAIN_DEPTH
7887
int m_DelayedPresents = 0;
88+
Uint32 m_RenderStartTime = 0;
7989
#endif
8090

8191
// SDL state
@@ -96,6 +106,11 @@ class PlVkRenderer : public IFFmpegRenderer {
96106
pl_tex m_Textures[PL_MAX_PLANES] = {};
97107
pl_color_space m_LastColorspace = {};
98108

109+
#ifdef PLVK_USE_EARLY_RENDER_TO_WAIT
110+
pl_overlay m_EmptyOverlay = {};
111+
pl_overlay_part m_EmptyOverlayPart = {};
112+
#endif
113+
99114
// Pending swapchain state shared between waitToRender(), renderFrame(), and cleanupRenderContext()
100115
pl_swapchain_frame m_SwapchainFrame = {};
101116
bool m_HasPendingSwapchainFrame = false;

0 commit comments

Comments
 (0)