From 59b1dcfd13f6f04a74569fe0ceca572f477fb092 Mon Sep 17 00:00:00 2001 From: TrueZhuanjia Date: Mon, 25 Aug 2025 16:38:23 +0800 Subject: [PATCH] feat: add back legacy pacing --- VoidLink/Base.lproj/iPad.storyboard | 27 +- VoidLink/Base.lproj/iPhone.storyboard | 33 +- VoidLink/Database/DataManager.h | 5 + VoidLink/Database/DataManager.m | 2 + VoidLink/Database/TemporarySettings.h | 1 + VoidLink/Database/TemporarySettings.m | 2 + .../VoidLink v1.0.xcdatamodel/contents | 1 + VoidLink/Localization/Localizable.xcstrings | 506 +++++++++--------- VoidLink/Stream/VideoDecoderRenderer.m | 183 +++++-- VoidLink/Utility/Plot.h | 5 - .../ViewControllers/MainFrameViewController.m | 4 + .../ViewControllers/SettingsViewController.h | 2 + .../ViewControllers/SettingsViewController.m | 56 +- VoidLink/mul.lproj/iPad.xcstrings | 90 ++++ VoidLink/mul.lproj/iPhone.xcstrings | 90 ++++ 15 files changed, 689 insertions(+), 318 deletions(-) diff --git a/VoidLink/Base.lproj/iPad.storyboard b/VoidLink/Base.lproj/iPad.storyboard index 4bc7d608..a86229d8 100644 --- a/VoidLink/Base.lproj/iPad.storyboard +++ b/VoidLink/Base.lproj/iPad.storyboard @@ -986,6 +986,29 @@ + + + + + + + + + + + + + + + + + + @@ -1014,9 +1037,11 @@ + + - + diff --git a/VoidLink/Base.lproj/iPhone.storyboard b/VoidLink/Base.lproj/iPhone.storyboard index e1e24874..53088bc5 100644 --- a/VoidLink/Base.lproj/iPhone.storyboard +++ b/VoidLink/Base.lproj/iPhone.storyboard @@ -1685,6 +1685,29 @@ + + + + + + + + + + + + + + + + + + @@ -1713,6 +1736,8 @@ + + @@ -1798,19 +1823,19 @@ - + - + - + - + diff --git a/VoidLink/Database/DataManager.h b/VoidLink/Database/DataManager.h index 6111327d..af86acc2 100644 --- a/VoidLink/Database/DataManager.h +++ b/VoidLink/Database/DataManager.h @@ -42,6 +42,10 @@ typedef NS_ENUM(NSInteger, GyroMode) { AlwaysController }; +typedef NS_ENUM(NSInteger, FramePacingMode) { + FramePacingModeLegacy, + FramePacingModeQueue +}; typedef NS_ENUM(NSInteger, SettingsMenuMode) { AllSettings, @@ -91,6 +95,7 @@ typedef NS_ENUM(NSInteger, SettingsMenuMode) { enableGraphs:(BOOL)enableGraphs graphOpacity:(NSInteger)graphOpacity renderingBackend:(NSInteger)renderingBackend + framePacingMode:(NSInteger)framePacingMode backgroundSessionTimer:(NSInteger)backgroundSessionTimer; - (NSArray*) getHosts; diff --git a/VoidLink/Database/DataManager.m b/VoidLink/Database/DataManager.m index 488f2904..6140fd17 100644 --- a/VoidLink/Database/DataManager.m +++ b/VoidLink/Database/DataManager.m @@ -97,6 +97,7 @@ enableGraphs:(BOOL)enableGraphs graphOpacity:(NSInteger)graphOpacity renderingBackend:(NSInteger)renderingBackend + framePacingMode:(NSInteger)framePacingMode backgroundSessionTimer:(NSInteger)backgroundSessionTimer{ [_managedObjectContext performBlockAndWait:^{ @@ -145,6 +146,7 @@ settingsToSave.enableGraphs = enableGraphs; settingsToSave.graphOpacity = [NSNumber numberWithInteger:graphOpacity]; settingsToSave.renderingBackend = [NSNumber numberWithInteger:renderingBackend]; + settingsToSave.framePacingMode = [NSNumber numberWithInteger:framePacingMode]; [self saveData]; }]; } diff --git a/VoidLink/Database/TemporarySettings.h b/VoidLink/Database/TemporarySettings.h index a44c0e57..bfa557b2 100644 --- a/VoidLink/Database/TemporarySettings.h +++ b/VoidLink/Database/TemporarySettings.h @@ -68,6 +68,7 @@ @property (nonatomic, retain) NSNumber * frameQueueSize; @property (nonatomic, retain) NSNumber * graphOpacity; @property (nonatomic, retain) NSNumber * renderingBackend; +@property (nonatomic, retain) NSNumber * framePacingMode; - (id) initFromSettings:(Settings*)settings; diff --git a/VoidLink/Database/TemporarySettings.m b/VoidLink/Database/TemporarySettings.m index 160549ac..499608c0 100644 --- a/VoidLink/Database/TemporarySettings.m +++ b/VoidLink/Database/TemporarySettings.m @@ -61,6 +61,7 @@ // iOS < 17 must use Balanced mode (AVSB renderer) self.renderingBackend = [NSNumber numberWithInteger:RENDER_AVSB]; } + self.framePacingMode = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"framePacingMode"]]; NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"]; switch (_screenSize) { @@ -129,6 +130,7 @@ self.enableGraphs = settings.enableGraphs; self.graphOpacity = settings.graphOpacity; self.renderingBackend = settings.renderingBackend; + self.framePacingMode = settings.framePacingMode; self.backgroundSessionTimer = settings.backroundSessionTimer; #endif self.uniqueId = settings.uniqueId; diff --git a/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents b/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents index b582424a..ac857603 100644 --- a/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents +++ b/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents @@ -34,6 +34,7 @@ + diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings index fbddb762..d8dbd8f1 100644 --- a/VoidLink/Localization/Localizable.xcstrings +++ b/VoidLink/Localization/Localizable.xcstrings @@ -306,29 +306,6 @@ } } }, - "[ Toggle stats overlay & graphs ]" : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "[ 切换统计覆盖层和图表 ]" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "[ 切換統計覆蓋層和圖表 ]" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "[ 切換統計覆蓋層和圖表 ]" - } - } - } - }, "[ On-screen widget tool ]" : { "extractionState" : "manual", "localizations" : { @@ -375,6 +352,29 @@ } } }, + "[ Toggle stats overlay & graphs ]" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "[ 切换统计覆盖层和图表 ]" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "[ 切換統計覆蓋層和圖表 ]" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "[ 切換統計覆蓋層和圖表 ]" + } + } + } + }, "%@ is currently running" : { "extractionState" : "manual", "localizations" : { @@ -789,6 +789,29 @@ } } }, + "Balanced" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "平衡" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "平衡" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "平衡" + } + } + } + }, "Ball: assumed touch point. Cross: indicator location" : { "extractionState" : "manual", "localizations" : { @@ -2010,6 +2033,52 @@ } } }, + "Frames to Buffer" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "缓冲帧数" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "緩衝影格數" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "緩衝影格數" + } + } + } + }, + "Frames to buffer: %ld" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "缓冲帧数:%ld" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "緩衝影格數:%ld" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "緩衝影格數:%ld" + } + } + } + }, "From the player community, to the player community." : { "extractionState" : "manual", "localizations" : { @@ -2419,6 +2488,29 @@ } } }, + "Later" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "稍后" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "稍後" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "稍後" + } + } + } + }, "Launch" : { "extractionState" : "manual", "localizations" : { @@ -3393,6 +3485,52 @@ } } }, + "Performance" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "性能" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "效能" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "效能" + } + } + } + }, + "Performance Graphs - Opacity: %ld%%" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "性能图表 - 不透明度:%ld%%" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "效能圖表 - 不透明度:%ld%%" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "效能圖表 - 不透明度:%ld%%" + } + } + } + }, "Peripherals" : { "extractionState" : "manual", "localizations" : { @@ -3745,6 +3883,29 @@ } } }, + "Quit Now" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "立即退出" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "立即退出" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "立即退出" + } + } + } + }, "Quit Running App" : { "extractionState" : "manual", "localizations" : { @@ -3837,98 +3998,6 @@ } } }, - "Quit Now" : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "立即退出" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "立即退出" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "立即退出" - } - } - } - }, - "Rendering mode change requires app restart" : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "更改渲染模式需要重启应用以确保干净启动。您是否要立即退出应用?" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "更改渲染模式需要重新啟動應用程式以確保乾淨啟動。您是否要立即退出應用程式?" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "更改渲染模式需要重新啟動應用程式以確保乾淨啟動。您是否要立即退出應用程式?" - } - } - } - }, - "Restart Required" : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "需要重启" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "需要重新啟動" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "需要重新啟動" - } - } - } - }, - "Later" : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "稍后" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "稍後" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "稍後" - } - } - } - }, "Read Widget Instruction" : { "extractionState" : "manual", "localizations" : { @@ -4044,6 +4113,52 @@ } } }, + "Rendering Mode (Experimental)" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "渲染模式(实验性)" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "渲染模式(實驗性)" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "渲染模式(實驗性)" + } + } + } + }, + "Rendering mode change requires app restart" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更改渲染模式需要重启应用以确保干净启动。您是否要立即退出应用?" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "更改渲染模式需要重新啟動應用程式以確保乾淨啟動。您是否要立即退出應用程式?" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "更改渲染模式需要重新啟動應用程式以確保乾淨啟動。您是否要立即退出應用程式?" + } + } + } + }, "renderingBackendStackTip" : { "extractionState" : "manual", "localizations" : { @@ -4073,6 +4188,29 @@ } } }, + "Restart Required" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要重启" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要重新啟動" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要重新啟動" + } + } + } + }, "Resume App" : { "extractionState" : "manual", "localizations" : { @@ -5384,144 +5522,6 @@ } } }, - "Rendering Mode (Experimental)" : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "渲染模式(实验性)" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "渲染模式(實驗性)" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "渲染模式(實驗性)" - } - } - } - }, - "Frames to Buffer" : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "缓冲帧数" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "緩衝影格數" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "緩衝影格數" - } - } - } - }, - "Frames to buffer: %ld" : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "缓冲帧数:%ld" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "緩衝影格數:%ld" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "緩衝影格數:%ld" - } - } - } - }, - "Performance Graphs - Opacity: %ld%%" : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "性能图表 - 不透明度:%ld%%" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "效能圖表 - 不透明度:%ld%%" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "效能圖表 - 不透明度:%ld%%" - } - } - } - }, - "Balanced" : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "平衡" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "平衡" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "平衡" - } - } - } - }, - "Performance" : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "性能" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "效能" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "效能" - } - } - } - }, "闽ICP备17012590号-3A\n主办单位:福州创图信息技术有限公司" : { } diff --git a/VoidLink/Stream/VideoDecoderRenderer.m b/VoidLink/Stream/VideoDecoderRenderer.m index 2a71cae6..48384b07 100644 --- a/VoidLink/Stream/VideoDecoderRenderer.m +++ b/VoidLink/Stream/VideoDecoderRenderer.m @@ -55,6 +55,13 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size, NSInteger _maxRefreshRate; RenderingBackend _renderingBackend; + // Frame pacing mode + BOOL _useLegacyPacing; + + // Legacy pacing frame counter and queue management + NSInteger _simplePacingFrameCount; + NSInteger _enqueuedSampleBuffers; + } - (void)reinitializeDisplayLayer @@ -129,9 +136,15 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size, DataManager* dataMan = [[DataManager alloc] init]; - _frameQueue = [FrameQueue sharedInstance]; - [_frameQueue start]; - [_frameQueue setHighWaterMark:(int)[[dataMan getSettings].frameQueueSize integerValue]]; + // Check if we should use legacy pacing mode + _useLegacyPacing = [[dataMan getSettings].framePacingMode integerValue] == FramePacingModeLegacy; + + if (!_useLegacyPacing) { + // Queue pacing mode uses FrameQueue + _frameQueue = [FrameQueue sharedInstance]; + [_frameQueue start]; + [_frameQueue setHighWaterMark:(int)[[dataMan getSettings].frameQueueSize integerValue]]; + } [self reinitializeDisplayLayer]; @@ -155,11 +168,17 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size, DataManager* dataMan = [[DataManager alloc] init]; if ([[dataMan getSettings].renderingBackend integerValue] == RENDER_AVSB) { - // PACING_MODE_VSYNC: - // Deliver 1 frame at each vsync interval. Ignores server pts timestamps. - // Drop frames intelligently to maintain chosen queue size. _renderingBackend = RENDER_AVSB; - _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(renderModeAVSB:)]; + + if (_useLegacyPacing) { + // Legacy pacing mode + _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(renderModeLegacy:)]; + } else { + // PACING_MODE_VSYNC: + // Deliver 1 frame at each vsync interval. Ignores server pts timestamps. + // Drop frames intelligently to maintain chosen queue size. + _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(renderModeAVSB:)]; + } if (@available(iOS 15.0, tvOS 15.0, *)) { _displayLink.preferredFrameRateRange = CAFrameRateRangeMake(self->_frameRate, self->_frameRate, self->_frameRate); @@ -176,7 +195,6 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size, - (void)setupDecompressionSessionWithAttributes:(NSDictionary *)destinationPixelBufferAttributes { - // This method is called from within synchronized block, so no additional sync needed here if (_decompressionSession != NULL) { VTDecompressionSessionInvalidate(_decompressionSession); CFRelease(_decompressionSession); @@ -242,7 +260,33 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size, int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit); -#pragma mark DisplayLink - Frame Pacing - Vsync with FrameQueue +#pragma mark DisplayLink - Legacy Frame Pacing + +// Legacy frame pacing method from upstream/Integration - direct polling without FrameQueue +- (void)renderModeLegacy:(CADisplayLink *)link { + VIDEO_FRAME_HANDLE handle; + PDECODE_UNIT du; + + while (LiPollNextVideoFrame(&handle, &du)) { + LiCompleteVideoFrame(handle, DrSubmitDecodeUnit(du)); + + // Calculate the actual display refresh rate + double displayRefreshRate = 1 / (link.targetTimestamp - link.timestamp); + + // Only pace frames if the display refresh rate is >= 90% of our stream frame rate. + // Battery saver, accessibility settings, or device thermals can cause the actual + // refresh rate of the display to drop below the physical maximum. + if (displayRefreshRate >= _frameRate * 0.9f) { + // Keep one pending frame to smooth out gaps due to + // network jitter at the cost of 1 frame of latency + if (LiGetPendingVideoFrames() == 1) { + break; + } + } + } +} + +#pragma mark DisplayLink - Frame Pacing - Vsync with FrameQueue (Queue Pacing) // This frame pacing method was inspired by the behavior of moonlight-qt's Pacer class, although it has evolved // a few additional features. Incoming frames from Sunshine are asynchronously processed into a queue by the VideoRecv thread. @@ -294,9 +338,11 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit); // we missed a callback // Log(LOG_W, @"*** slow frametime %.3f ms", frametime * 1000.0); } - if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) { - [[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:frametime * 1000.0]; - } + dispatch_async(dispatch_get_main_queue(), ^{ + if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) { + [[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:frametime * 1000.0]; + } + }); } lastTargetLocal = targetLocal; @@ -364,7 +410,9 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit); } - (void)cleanup { - [_frameQueue stop]; + if (_frameQueue) { + [_frameQueue stop]; + } if (_renderingBackend == RENDER_AVSB) { [_displayLink invalidate]; @@ -925,34 +973,82 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit); // Dispatch onto our higher priority queue dispatch_async(self->_vtq, ^{ - Frame *frame = nil; - if (self->_renderingBackend == RENDER_AVSB) { - frame = [[Frame alloc] initWithSampleBuffer:sampleBufferOut frameNumber:frameNumber frameType:frameType]; + if (self->_useLegacyPacing && self->_renderingBackend == RENDER_AVSB) { + // Legacy pacing: Process all frames normally, but with immediate display timing + + // Set presentation time to current time for immediate display + CMTime targetTime = CMTimeMakeWithSeconds(CACurrentMediaTime(), NSEC_PER_SEC); + CMSampleBufferSetOutputPresentationTimeStamp(sampleBufferOut, targetTime); + + // Dispatch display layer operations to main thread to avoid CATransaction warnings + dispatch_async(dispatch_get_main_queue(), ^{ + if (frameType == FRAME_TYPE_IDR) { + // Ensure the layer is visible now + self->_displayLayer.hidden = NO; + + // Tell our parent VC to hide the progress indicator + [self->_callbacks videoContentShown]; + } + + if ([self->_displayLayer controlTimebase] == NULL) { + // On first frame, set timebase + CMTimebaseRef timebase = NULL; + CMTimebaseCreateWithSourceClock(CFAllocatorGetDefault(), CMClockGetHostTimeClock(), &timebase); + + CMTime pts = CMSampleBufferGetOutputPresentationTimeStamp(sampleBufferOut); + CMTimebaseSetTime(timebase, pts); + CMTimebaseSetRate(timebase, 1.0); + + [self->_displayLayer setControlTimebase:timebase]; + if (timebase) { + CFRelease(timebase); + } + Log(LOG_I, @"Setting timebase for legacy pacing to %d / %d", pts.value, pts.timescale); + } + + [self->_displayLayer enqueueSampleBuffer:sampleBufferOut]; + CFRelease(sampleBufferOut); + }); } else { - frame = [[Frame alloc] initWithPixelBufffer:pixelBuffer frameNumber:frameNumber frameType:frameType pts:presentationTimestamp]; - [frame setFormatDesc:self->_formatDesc]; - } - int framesDropped = [self->_frameQueue enqueue:frame withSlackSize:3]; - - if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) { - static PlotMetrics frameQueueMetrics = {}; - [[ImGuiPlots sharedInstance] observeFloatReturnMetrics:PLOT_QUEUED_FRAMES value:[self->_frameQueue count] plotMetrics:&frameQueueMetrics]; - [self safeCopyMetricsTo:&self->_frameQueueMetrics from:&frameQueueMetrics]; - - [[ImGuiPlots sharedInstance] observeFloat:PLOT_DROPPED value:framesDropped]; - - // It's important we capture host metrics on the incoming thread, as this frame object - // may have been dropped by the above enqueue - static CFTimeInterval lastHostFrame = 0.0f; - if (lastHostFrame != 0) { - [[ImGuiPlots sharedInstance] observeFloat:PLOT_HOST_FRAMETIME value:(frame.pts - lastHostFrame) * 1000.0]; + // Queue pacing: use FrameQueue + Frame *frame = nil; + if (self->_renderingBackend == RENDER_AVSB) { + frame = [[Frame alloc] initWithSampleBuffer:sampleBufferOut frameNumber:frameNumber frameType:frameType]; + } else { + frame = [[Frame alloc] initWithPixelBufffer:pixelBuffer frameNumber:frameNumber frameType:frameType pts:presentationTimestamp]; + [frame setFormatDesc:self->_formatDesc]; } - lastHostFrame = frame.pts; + int framesDropped = [self->_frameQueue enqueue:frame withSlackSize:3]; - // Decode time is not graphed because it is marked as hidden, but we can use the same mechanism for the value used by stats - static PlotMetrics decodeMetrics = {}; - [[ImGuiPlots sharedInstance] observeFloatReturnMetrics:PLOT_DECODE value:(CACurrentMediaTime() - decodeStartTime) * 1000.0 plotMetrics:&decodeMetrics]; - [self safeCopyMetricsTo:&self->_decodeMetrics from:&decodeMetrics]; + // Capture metrics on main thread to avoid UIApplication thread warning + int frameQueueCount = [self->_frameQueue count]; + CFTimeInterval capturedDecodeTime = CACurrentMediaTime() - decodeStartTime; + CFTimeInterval hostFramePts = frame.pts; + + dispatch_async(dispatch_get_main_queue(), ^{ + if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) { + static PlotMetrics frameQueueMetrics = {}; + [[ImGuiPlots sharedInstance] observeFloatReturnMetrics:PLOT_QUEUED_FRAMES value:frameQueueCount plotMetrics:&frameQueueMetrics]; + [self safeCopyMetricsTo:&self->_frameQueueMetrics from:&frameQueueMetrics]; + + [[ImGuiPlots sharedInstance] observeFloat:PLOT_DROPPED value:framesDropped]; + + // It's important we capture host metrics on the incoming thread, as this frame object + // may have been dropped by the above enqueue + static CFTimeInterval lastHostFrame = 0.0f; + if (lastHostFrame != 0) { + [[ImGuiPlots sharedInstance] observeFloat:PLOT_HOST_FRAMETIME value:(hostFramePts - lastHostFrame) * 1000.0]; + } + lastHostFrame = hostFramePts; + + // Decode time is not graphed because it is marked as hidden, but we can use the same mechanism for the value used by stats + static PlotMetrics decodeMetrics = {}; + [[ImGuiPlots sharedInstance] observeFloatReturnMetrics:PLOT_DECODE + value:capturedDecodeTime * 1000.0 + plotMetrics:&decodeMetrics]; + [self safeCopyMetricsTo:&self->_decodeMetrics from:&decodeMetrics]; + } + }); } }); }); @@ -1051,13 +1147,16 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit); if (_renderingBackend == RENDER_METAL) { stats->renderingBackendString = [NSString stringWithFormat:@"Metal, colorspace: %@", [MetalVideoRenderer currentColorSpace]]; } else { - stats->renderingBackendString = @"AVSampleBuffer"; + NSString *pacingMode = _useLegacyPacing ? @"Legacy" : @"Queue"; + stats->renderingBackendString = [NSString stringWithFormat:@"AVSampleBuffer (%@ pacing)", pacingMode]; } dispatch_sync(_sq, ^{ memcpy(&stats->decodeMetrics, &_decodeMetrics, sizeof(PlotMetrics)); - memcpy(&stats->frameQueueMetrics, &_frameQueueMetrics, sizeof(PlotMetrics)); - [_frameQueue.frameDropMetrics copyMetrics:&stats->frameDropMetrics]; + if (_frameQueue) { + memcpy(&stats->frameQueueMetrics, &_frameQueueMetrics, sizeof(PlotMetrics)); + [_frameQueue.frameDropMetrics copyMetrics:&stats->frameDropMetrics]; + } }); } @@ -1069,7 +1168,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit); static int lastTargetRate = 0; int targetRate = (int)_maxRefreshRate; - if (_maxRefreshRate <= 60 || _maxRefreshRate == 90) { + if (_maxRefreshRate <= 60 || _maxRefreshRate == 90 || !_frameQueue) { return; } diff --git a/VoidLink/Utility/Plot.h b/VoidLink/Utility/Plot.h index 2605233a..de78bf03 100644 --- a/VoidLink/Utility/Plot.h +++ b/VoidLink/Utility/Plot.h @@ -1,8 +1,3 @@ -typedef enum { - PACING_MODE_VSYNC, - PACING_MODE_PTS -} FramePacingMode; - typedef enum { PLOT_FRAMETIME = 0, PLOT_HOST_FRAMETIME, diff --git a/VoidLink/ViewControllers/MainFrameViewController.m b/VoidLink/ViewControllers/MainFrameViewController.m index 52dbe30c..84488125 100644 --- a/VoidLink/ViewControllers/MainFrameViewController.m +++ b/VoidLink/ViewControllers/MainFrameViewController.m @@ -28,6 +28,7 @@ #import "IdManager.h" #import "ConnectionHelper.h" #import "LocalizationHelper.h" +#import "Plot.h" #import "CustomEdgeSlideGestureRecognizer.h" #import "DataManager.h" #import "ThemeManager.h" @@ -1180,6 +1181,9 @@ static NSMutableSet* hostList; [settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.audioConfigStack]; [settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.pipStack]; [settingsViewController.renderingBackendSelector setEnabled:!_settingsExpandedInStreamView]; + // Enable frame pacing mode selector only if not in stream view AND not in performance mode + BOOL shouldEnableFramePacing = !_settingsExpandedInStreamView && (settingsViewController.renderingBackendSelector.selectedSegmentIndex != RENDER_METAL); + [settingsViewController.framePacingModeSelector setEnabled:shouldEnableFramePacing]; } - (void)revealController:(SWRevealViewController *)revealController didMoveToPosition:(FrontViewPosition)position { diff --git a/VoidLink/ViewControllers/SettingsViewController.h b/VoidLink/ViewControllers/SettingsViewController.h index 13860d64..696792e3 100644 --- a/VoidLink/ViewControllers/SettingsViewController.h +++ b/VoidLink/ViewControllers/SettingsViewController.h @@ -57,6 +57,7 @@ @property (strong, nonatomic) IBOutlet UIStackView *externalDisplayModeStack; @property (strong, nonatomic) IBOutlet UIStackView *localMousePointerModeStack; @property (strong, nonatomic) IBOutlet UIStackView *renderingBackendStack; +@property (strong, nonatomic) IBOutlet UIStackView *framePacingStack; @property (strong, nonatomic) IBOutlet UIStackView *frameQueueSizeStack; @property (strong, nonatomic) IBOutlet UIStackView *performanceGraphStack; @@ -115,6 +116,7 @@ @property (strong, nonatomic) IBOutlet UISwitch *enableGraphsSwitch; @property (strong, nonatomic) IBOutlet UIStepper *graphOpacityStepper; @property (strong, nonatomic) IBOutlet UISegmentedControl *renderingBackendSelector; +@property (strong, nonatomic) IBOutlet UISegmentedControl *framePacingModeSelector; @property (strong, nonatomic) IBOutlet UIStackView *backgroundSessionTimerStack; @property (strong, nonatomic) IBOutlet UISlider *backgroundSessionTimerSlider; diff --git a/VoidLink/ViewControllers/SettingsViewController.m b/VoidLink/ViewControllers/SettingsViewController.m index 9262ceef..cb90ab5e 100644 --- a/VoidLink/ViewControllers/SettingsViewController.m +++ b/VoidLink/ViewControllers/SettingsViewController.m @@ -560,6 +560,7 @@ BOOL isCustomResolution(int resolutionSelected) { [self addSetting:self.pipStack ofId:@"pipStack" withInfoTag:YES withDynamicLabel:NO to:videoSection]; [self addSetting:self.pipStack ofId:@"pipStack" withInfoTag:YES withDynamicLabel:NO to:videoSection]; [self addSetting:self.frameQueueSizeStack ofId:@"frameQueueSizeStack" withInfoTag:NO withDynamicLabel:NO to:videoSection]; + [self addSetting:self.framePacingStack ofId:@"framePacingStack" withInfoTag:YES withDynamicLabel:NO to:videoSection]; // Only show Metal renderer option on iOS 17+ where CAMetalDisplayLink is available if (@available(iOS 17.0, *)) { @@ -1431,7 +1432,13 @@ BOOL isCustomResolution(int resolutionSelected) { NSInteger renderingBackend = [currentSettings.renderingBackend integerValue]; [self.renderingBackendSelector setSelectedSegmentIndex:renderingBackend]; [self.renderingBackendSelector addTarget:self action:@selector(renderingBackendChanged:) forControlEvents:UIControlEventValueChanged]; - [self renderingBackendChanged:self.renderingBackendSelector]; // Update PiP state based on current selection + + NSInteger framePacingMode = [currentSettings.framePacingMode integerValue]; + [self.framePacingModeSelector setSelectedSegmentIndex:framePacingMode]; + [self.framePacingModeSelector addTarget:self action:@selector(framePacingModeChanged:) forControlEvents:UIControlEventValueChanged]; + [self framePacingModeChanged:self.framePacingModeSelector]; + + [self renderingBackendChanged:self.renderingBackendSelector]; // Update PiP and frame pacing state based on current selection [self.citrixX1MouseSwitch setOn:currentSettings.btMouseSupport]; [self.optimizeGamesSwitch setOn: currentSettings.optimizeGames]; @@ -1665,6 +1672,24 @@ BOOL isCustomResolution(int resolutionSelected) { } - (void)renderingBackendChanged:(UISegmentedControl *)sender { + // Disable PiP toggle when Metal renderer is selected + if (sender.selectedSegmentIndex == RENDER_METAL) { + // Performance mode (Metal renderer) selected - disable PiP + [self.pipSwitch setOn:NO animated:YES]; + [self.pipSwitch setEnabled:NO]; + // Set pacing method to Queue and disable selector + [self.framePacingModeSelector setSelectedSegmentIndex:FramePacingModeQueue]; + [self.framePacingModeSelector setEnabled:NO]; + } else { + // Balanced mode (AVSB renderer) - enable PiP toggle if iOS 15+ + if (@available(iOS 15.0, *)) { + [self.pipSwitch setEnabled:YES]; + } else { + [self.pipSwitch setOn:NO]; + [self.pipSwitch setEnabled:NO]; + } + [self.framePacingModeSelector setEnabled:YES]; + } // Get the current settings to compare with the new selection DataManager* dataMan = [[DataManager alloc] init]; @@ -1706,19 +1731,22 @@ BOOL isCustomResolution(int resolutionSelected) { [alertController addAction:quitAction]; [self presentViewController:alertController animated:YES completion:nil]; } - - // Disable PiP toggle when Metal renderer is selected - if (sender.selectedSegmentIndex == RENDER_METAL) { - // Performance mode (Metal renderer) selected - disable PiP - [self.pipSwitch setOn:NO animated:YES]; - [self.pipSwitch setEnabled:NO]; +} + +- (void)framePacingModeChanged:(UISegmentedControl *)sender { + + if (sender.selectedSegmentIndex == FramePacingModeLegacy) { + // Legacy mode selected - disable frames to buffer and graph settings + [self.frameQueueSizeSlider setEnabled:NO]; + [self.enableGraphsSwitch setOn:NO animated:YES]; + [self.enableGraphsSwitch setEnabled:NO]; + [self.graphOpacityStepper setEnabled:NO]; } else { - // Balanced mode (AVSB renderer) - enable PiP toggle if iOS 15+ - if (@available(iOS 15.0, *)) { - [self.pipSwitch setEnabled:YES]; - } else { - [self.pipSwitch setOn:NO]; - [self.pipSwitch setEnabled:NO]; + // Queue mode selected - enable frames to buffer and graph settings + [self.frameQueueSizeSlider setEnabled:YES]; + [self.enableGraphsSwitch setEnabled:YES]; + if (self.enableGraphsSwitch.isOn) { + [self.graphOpacityStepper setEnabled:YES]; } } } @@ -2265,6 +2293,7 @@ BOOL isCustomResolution(int resolutionSelected) { NSInteger audioConfig = [@[@2, @6, @8][[self.audioConfigSelector selectedSegmentIndex]] integerValue]; NSInteger renderingBackend = [self.renderingBackendSelector selectedSegmentIndex]; + NSInteger framePacingMode = [self.framePacingModeSelector selectedSegmentIndex]; NSInteger onscreenControls = [self.onScreenWidgetSelector selectedSegmentIndex]; NSInteger keyboardToggleFingers = self.softKeyboardGestureSelector.selectedSegmentIndex == 3 ? 20 : self.softKeyboardGestureSelector.selectedSegmentIndex+3; NSInteger oscLayoutToolFingers = (uint16_t)self->oswLayoutFingers; @@ -2348,6 +2377,7 @@ BOOL isCustomResolution(int resolutionSelected) { enableGraphs:enableGraphs graphOpacity:_graphOpacity renderingBackend:renderingBackend + framePacingMode:framePacingMode backgroundSessionTimer:backgroundSessionTimer]; } diff --git a/VoidLink/mul.lproj/iPad.xcstrings b/VoidLink/mul.lproj/iPad.xcstrings index 3ecdccf7..fe5c2086 100644 --- a/VoidLink/mul.lproj/iPad.xcstrings +++ b/VoidLink/mul.lproj/iPad.xcstrings @@ -31,6 +31,66 @@ } } }, + "2Dp-Qg-chi.segmentTitles[0]" : { + "comment" : "Class = \"UISegmentedControl\"; 2Dp-Qg-chi.segmentTitles[0] = \"Legacy\"; ObjectID = \"2Dp-Qg-chi\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Legacy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "旧版" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "舊版" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "舊版" + } + } + } + }, + "2Dp-Qg-chi.segmentTitles[1]" : { + "comment" : "Class = \"UISegmentedControl\"; 2Dp-Qg-chi.segmentTitles[1] = \"Queue\"; ObjectID = \"2Dp-Qg-chi\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Queue" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "佇列" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "佇列" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "佇列" + } + } + } + }, "2w1-Vw-GVy.text" : { "comment" : "Class = \"UILabel\"; text = \"HDR\"; ObjectID = \"2w1-Vw-GVy\";", "extractionState" : "extracted_with_value", @@ -2311,6 +2371,36 @@ } } }, + "MaN-f7-I3g.text" : { + "comment" : "Class = \"UILabel\"; text = \"Frame Pacing Mode\"; ObjectID = \"MaN-f7-I3g\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Frame Pacing Mode" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "帧缓冲模式" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "幀緩衝模式" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "幀緩衝模式" + } + } + } + }, "Mm0-xf-9eh.segmentTitles[0]" : { "comment" : "Class = \"UISegmentedControl\"; Mm0-xf-9eh.segmentTitles[0] = \"Slide From Left Edge\"; ObjectID = \"Mm0-xf-9eh\";", "extractionState" : "extracted_with_value", diff --git a/VoidLink/mul.lproj/iPhone.xcstrings b/VoidLink/mul.lproj/iPhone.xcstrings index a6c78979..9f4fb8d9 100644 --- a/VoidLink/mul.lproj/iPhone.xcstrings +++ b/VoidLink/mul.lproj/iPhone.xcstrings @@ -1087,6 +1087,66 @@ } } }, + "CC6-pu-Uoh.segmentTitles[0]" : { + "comment" : "Class = \"UISegmentedControl\"; CC6-pu-Uoh.segmentTitles[0] = \"Legacy\"; ObjectID = \"CC6-pu-Uoh\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Legacy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "旧版" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "舊版" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "舊版" + } + } + } + }, + "CC6-pu-Uoh.segmentTitles[1]" : { + "comment" : "Class = \"UISegmentedControl\"; CC6-pu-Uoh.segmentTitles[1] = \"Queue\"; ObjectID = \"CC6-pu-Uoh\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Queue" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "佇列" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "佇列" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "佇列" + } + } + } + }, "ci6-ak-aCa.text" : { "comment" : "Class = \"UILabel\"; text = \"Custom Resolution\"; ObjectID = \"ci6-ak-aCa\";", "extractionState" : "extracted_with_value", @@ -1177,6 +1237,36 @@ } } }, + "Cvx-kQ-L3H.text" : { + "comment" : "Class = \"UILabel\"; text = \"Frame Pacing Mode\"; ObjectID = \"Cvx-kQ-L3H\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Frame Pacing Mode" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "帧缓冲模式" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "幀緩衝模式" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "幀緩衝模式" + } + } + } + }, "D9X-7p-9oc.title" : { "comment" : "Class = \"UIBarButtonItem\"; title = \"ExportAll\"; ObjectID = \"D9X-7p-9oc\";", "extractionState" : "extracted_with_value",