Skip to content

Commit 652bcfb

Browse files
committed
Add auto encoder fallback to software under load
1 parent 4778e42 commit 652bcfb

4 files changed

Lines changed: 121 additions & 29 deletions

File tree

cli/XCWH264Encoder.m

Lines changed: 102 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
static const double XCWEncoderStrainedLoadPercent = 85.0;
4343
static const double XCWEncoderOverloadedLoadPercent = 105.0;
4444
static const NSUInteger XCWEncoderConsecutiveOverBudgetFrameThreshold = 3;
45+
static const uint64_t XCWAutoHardwareRetryIntervalUs = 10000000;
4546

4647
typedef NS_ENUM(NSUInteger, XCWVideoEncoderMode) {
4748
XCWVideoEncoderModeAuto,
@@ -507,6 +508,7 @@ @implementation XCWH264Encoder {
507508
OSType _scaledPixelFormat;
508509
BOOL _scalingActive;
509510
XCWVideoEncoderMode _encoderMode;
511+
XCWVideoEncoderMode _activeEncoderMode;
510512
BOOL _lowLatencyMode;
511513
BOOL _realtimeStreamMode;
512514
CMVideoCodecType _codecType;
@@ -533,6 +535,9 @@ @implementation XCWH264Encoder {
533535
uint64_t _hardwareFrameIntervalUs;
534536
uint64_t _lastHardwareSubmissionUs;
535537
NSUInteger _hardwarePacedFrameCount;
538+
uint64_t _autoSoftwareFallbackUntilUs;
539+
NSUInteger _autoSoftwareFallbackCount;
540+
NSUInteger _autoHardwareRetryCount;
536541
NSString *_selectedEncoderID;
537542
NSInteger _lastSessionStatus;
538543
NSInteger _lastPrepareStatus;
@@ -553,9 +558,10 @@ - (instancetype)initWithOutputHandler:(XCWH264EncoderOutputHandler)outputHandler
553558
_pendingLock = OS_UNFAIR_LOCK_INIT;
554559
_needsKeyFrame = YES;
555560
_encoderMode = XCWVideoEncoderModeFromEnvironment();
561+
_activeEncoderMode = _encoderMode;
556562
_lowLatencyMode = (_encoderMode == XCWVideoEncoderModeH264Software) && XCWLowLatencyModeFromEnvironment();
557563
_realtimeStreamMode = XCWRealtimeStreamModeFromEnvironment() || _lowLatencyMode;
558-
_codecType = XCWVideoCodecTypeForMode(_encoderMode);
564+
_codecType = XCWVideoCodecTypeForMode(_activeEncoderMode);
559565
_softwareFrameIntervalUs = [self initialSoftwareFrameIntervalUsLocked];
560566
_hardwareFrameIntervalUs = [self initialHardwareFrameIntervalUsLocked];
561567
return self;
@@ -604,15 +610,17 @@ - (void)reconfigureForStreamQualityChange {
604610
dispatch_async(_queue, ^{
605611
[self invalidateCompressionSessionLocked];
606612
self->_encoderMode = XCWVideoEncoderModeFromEnvironment();
613+
self->_activeEncoderMode = self->_encoderMode;
607614
self->_lowLatencyMode = (self->_encoderMode == XCWVideoEncoderModeH264Software) && XCWLowLatencyModeFromEnvironment();
608615
self->_realtimeStreamMode = XCWRealtimeStreamModeFromEnvironment() || self->_lowLatencyMode;
609-
self->_codecType = XCWVideoCodecTypeForMode(self->_encoderMode);
616+
self->_codecType = XCWVideoCodecTypeForMode(self->_activeEncoderMode);
610617
self->_needsKeyFrame = YES;
611618
self->_softwareFrameIntervalUs = [self initialSoftwareFrameIntervalUsLocked];
612619
self->_softwarePacedFrameCount = 0;
613620
self->_softwareHealthyFrameCount = 0;
614621
self->_hardwareFrameIntervalUs = [self initialHardwareFrameIntervalUsLocked];
615622
self->_hardwarePacedFrameCount = 0;
623+
self->_autoSoftwareFallbackUntilUs = 0;
616624
});
617625
}
618626

@@ -653,6 +661,12 @@ - (NSDictionary *)statsRepresentation {
653661
? @"average-latency-near-budget"
654662
: @"consecutive-frames-near-budget";
655663
}
664+
uint64_t nowUs = (uint64_t)(CACurrentMediaTime() * 1000000.0);
665+
BOOL autoSoftwareFallbackActive = [self isAutoSoftwareFallbackActiveLocked];
666+
uint64_t autoSoftwareFallbackRemainingUs = autoSoftwareFallbackActive &&
667+
self->_autoSoftwareFallbackUntilUs > nowUs
668+
? self->_autoSoftwareFallbackUntilUs - nowUs
669+
: 0;
656670
stats = @{
657671
@"inputFrames": @(inputFrameCount),
658672
@"pendingReplacements": @(pendingReplacementCount),
@@ -684,9 +698,14 @@ - (NSDictionary *)statsRepresentation {
684698
@"hardwarePacedFrames": @(self->_hardwarePacedFrameCount),
685699
@"transportCodec": XCWCodecName(self->_codecType),
686700
@"encoderMode": XCWVideoEncoderModeName(self->_encoderMode),
701+
@"activeEncoderMode": XCWVideoEncoderModeName(self->_activeEncoderMode),
702+
@"autoSoftwareFallbackActive": @(autoSoftwareFallbackActive),
703+
@"autoSoftwareFallbackRemainingUs": @(autoSoftwareFallbackRemainingUs),
704+
@"autoSoftwareFallbacks": @(self->_autoSoftwareFallbackCount),
705+
@"autoHardwareRetries": @(self->_autoHardwareRetryCount),
687706
@"lowLatencyMode": @(self->_lowLatencyMode),
688707
@"realtimeStreamMode": @(self->_realtimeStreamMode),
689-
@"encoderId": XCWVideoEncoderIDForMode(self->_encoderMode) ?: @"automatic",
708+
@"encoderId": XCWVideoEncoderIDForMode(self->_activeEncoderMode) ?: @"automatic",
690709
@"selectedEncoderId": self->_selectedEncoderID ?: NSNull.null,
691710
@"hardwareAccelerated": @(self->_hardwareAccelerated),
692711
@"lastSessionStatus": @(self->_lastSessionStatus),
@@ -760,19 +779,67 @@ - (uint64_t)initialHardwareFrameIntervalUsLocked {
760779
return _realtimeStreamMode ? XCWRealtimeFrameIntervalUs() : XCWLocalStreamFrameIntervalUs();
761780
}
762781

782+
- (BOOL)isAutoSoftwareFallbackActiveLocked {
783+
return _encoderMode == XCWVideoEncoderModeAuto &&
784+
_activeEncoderMode == XCWVideoEncoderModeH264Software;
785+
}
786+
787+
- (void)resetAutoFallbackLatencyStateLocked {
788+
_latestEncodeLatencyUs = 0;
789+
_averageEncodeLatencyUs = 0;
790+
_peakEncodeLatencyUs = 0;
791+
_consecutiveOverBudgetFrameCount = 0;
792+
_consecutiveStrainedFrameCount = 0;
793+
_wasOverloaded = NO;
794+
}
795+
796+
- (void)enterAutoSoftwareFallbackLockedAtTimeUs:(uint64_t)nowUs {
797+
if (_encoderMode != XCWVideoEncoderModeAuto ||
798+
_activeEncoderMode == XCWVideoEncoderModeH264Software) {
799+
return;
800+
}
801+
_activeEncoderMode = XCWVideoEncoderModeH264Software;
802+
_codecType = XCWVideoCodecTypeForMode(_activeEncoderMode);
803+
_autoSoftwareFallbackUntilUs = nowUs + XCWAutoHardwareRetryIntervalUs;
804+
_autoSoftwareFallbackCount += 1;
805+
_softwareFrameIntervalUs = [self initialSoftwareFrameIntervalUsLocked];
806+
_softwareHealthyFrameCount = 0;
807+
_softwarePacedFrameCount = 0;
808+
[self invalidateCompressionSessionLocked];
809+
[self resetAutoFallbackLatencyStateLocked];
810+
_needsKeyFrame = YES;
811+
}
812+
813+
- (void)retryAutoHardwareIfNeededLockedAtTimeUs:(uint64_t)nowUs {
814+
if (![self isAutoSoftwareFallbackActiveLocked] ||
815+
_autoSoftwareFallbackUntilUs == 0 ||
816+
nowUs < _autoSoftwareFallbackUntilUs) {
817+
return;
818+
}
819+
_activeEncoderMode = XCWVideoEncoderModeAuto;
820+
_codecType = XCWVideoCodecTypeForMode(_activeEncoderMode);
821+
_autoSoftwareFallbackUntilUs = 0;
822+
_autoHardwareRetryCount += 1;
823+
_hardwareFrameIntervalUs = [self initialHardwareFrameIntervalUsLocked];
824+
_hardwarePacedFrameCount = 0;
825+
[self invalidateCompressionSessionLocked];
826+
[self resetAutoFallbackLatencyStateLocked];
827+
_needsKeyFrame = YES;
828+
}
829+
763830
- (uint64_t)activeFrameIntervalUsLocked {
764-
if (_encoderMode == XCWVideoEncoderModeH264Software) {
831+
if (_activeEncoderMode == XCWVideoEncoderModeH264Software) {
765832
return _softwareFrameIntervalUs > 0 ? _softwareFrameIntervalUs : [self initialSoftwareFrameIntervalUsLocked];
766833
}
767-
if (_encoderMode == XCWVideoEncoderModeAuto || _encoderMode == XCWVideoEncoderModeH264Hardware) {
834+
if (_activeEncoderMode == XCWVideoEncoderModeAuto || _activeEncoderMode == XCWVideoEncoderModeH264Hardware) {
768835
return _hardwareFrameIntervalUs > 0 ? _hardwareFrameIntervalUs : [self initialHardwareFrameIntervalUsLocked];
769836
}
770837
int32_t expectedFrameRate = MAX(1, [self expectedFrameRateLocked]);
771838
return (uint64_t)llround(1000000.0 / (double)expectedFrameRate);
772839
}
773840

774841
- (int32_t)expectedFrameRateLocked {
775-
if (_encoderMode == XCWVideoEncoderModeH264Software) {
842+
if (_activeEncoderMode == XCWVideoEncoderModeH264Software) {
776843
if (_lowLatencyMode) {
777844
return XCWTargetLowLatencySoftwareFrameRate;
778845
}
@@ -785,7 +852,7 @@ - (int32_t)expectedFrameRateLocked {
785852
}
786853

787854
- (BOOL)shouldPaceHardwareFrameAtTimeUs:(uint64_t)nowUs {
788-
if ((_encoderMode != XCWVideoEncoderModeAuto && _encoderMode != XCWVideoEncoderModeH264Hardware) || _needsKeyFrame) {
855+
if ((_activeEncoderMode != XCWVideoEncoderModeAuto && _activeEncoderMode != XCWVideoEncoderModeH264Hardware) || _needsKeyFrame) {
789856
return NO;
790857
}
791858
if (_realtimeStreamMode) {
@@ -806,7 +873,7 @@ - (BOOL)shouldPaceHardwareFrameAtTimeUs:(uint64_t)nowUs {
806873
}
807874

808875
- (BOOL)shouldPaceSoftwareFrameAtTimeUs:(uint64_t)nowUs {
809-
if (_encoderMode != XCWVideoEncoderModeH264Software || _needsKeyFrame) {
876+
if (_activeEncoderMode != XCWVideoEncoderModeH264Software || _needsKeyFrame) {
810877
return NO;
811878
}
812879
if (_softwareFrameIntervalUs == 0) {
@@ -824,7 +891,7 @@ - (BOOL)shouldPaceSoftwareFrameAtTimeUs:(uint64_t)nowUs {
824891
}
825892

826893
- (void)adaptSoftwarePacingForLatencyUs:(uint64_t)latencyUs {
827-
if (_encoderMode != XCWVideoEncoderModeH264Software || !_lowLatencyMode || latencyUs == 0) {
894+
if (_activeEncoderMode != XCWVideoEncoderModeH264Software || !_lowLatencyMode || latencyUs == 0) {
828895
return;
829896
}
830897
if (_softwareFrameIntervalUs == 0) {
@@ -892,15 +959,17 @@ - (BOOL)encodePixelBufferLocked:(CVPixelBufferRef)pixelBuffer {
892959
return NO;
893960
}
894961

895-
CGSize targetSize = XCWScaledDimensionsForSourceSize(sourceWidth, sourceHeight, _encoderMode, _lowLatencyMode, _realtimeStreamMode);
962+
uint64_t nowUs = (uint64_t)(CACurrentMediaTime() * 1000000.0);
963+
[self retryAutoHardwareIfNeededLockedAtTimeUs:nowUs];
964+
965+
CGSize targetSize = XCWScaledDimensionsForSourceSize(sourceWidth, sourceHeight, _activeEncoderMode, _lowLatencyMode, _realtimeStreamMode);
896966
int32_t targetWidth = (int32_t)targetSize.width;
897967
int32_t targetHeight = (int32_t)targetSize.height;
898968
if (targetWidth <= 0 || targetHeight <= 0) {
899969
return NO;
900970
}
901971
_scalingActive = sourceWidth != targetWidth || sourceHeight != targetHeight;
902972

903-
uint64_t nowUs = (uint64_t)(CACurrentMediaTime() * 1000000.0);
904973
if ([self shouldPaceSoftwareFrameAtTimeUs:nowUs] || [self shouldPaceHardwareFrameAtTimeUs:nowUs]) {
905974
return YES;
906975
}
@@ -946,13 +1015,13 @@ - (BOOL)encodePixelBufferLocked:(CVPixelBufferRef)pixelBuffer {
9461015

9471016
_inFlightFrameCount += 1;
9481017
_submittedFrameCount += 1;
949-
if (_encoderMode == XCWVideoEncoderModeH264Software) {
1018+
if (_activeEncoderMode == XCWVideoEncoderModeH264Software) {
9501019
_lastSoftwareSubmissionUs = nowUs;
951-
} else if (_encoderMode == XCWVideoEncoderModeAuto || _encoderMode == XCWVideoEncoderModeH264Hardware) {
1020+
} else if (_activeEncoderMode == XCWVideoEncoderModeAuto || _activeEncoderMode == XCWVideoEncoderModeH264Hardware) {
9521021
_lastHardwareSubmissionUs = nowUs;
9531022
}
9541023
_maxInFlightFrameCount = MAX(_maxInFlightFrameCount, _inFlightFrameCount);
955-
if (_encoderMode == XCWVideoEncoderModeH264Software || !_realtimeStreamMode) {
1024+
if (_activeEncoderMode == XCWVideoEncoderModeH264Software || !_realtimeStreamMode) {
9561025
VTCompressionSessionCompleteFrames(_compressionSession, presentationTime);
9571026
}
9581027
return YES;
@@ -966,20 +1035,20 @@ - (BOOL)ensureCompressionSessionWithWidth:(int32_t)width height:(int32_t)height
9661035
[self invalidateCompressionSessionLocked];
9671036

9681037
NSMutableDictionary *encoderSpecification = [NSMutableDictionary dictionary];
969-
NSString *encoderID = XCWVideoEncoderIDForMode(_encoderMode);
1038+
NSString *encoderID = XCWVideoEncoderIDForMode(_activeEncoderMode);
9701039
if (encoderID.length > 0) {
9711040
encoderSpecification[(__bridge NSString *)kVTVideoEncoderSpecification_EncoderID] = encoderID;
9721041
}
973-
if (_encoderMode != XCWVideoEncoderModeH264Software && (_lowLatencyMode || _realtimeStreamMode)) {
1042+
if (_activeEncoderMode != XCWVideoEncoderModeH264Software && (_lowLatencyMode || _realtimeStreamMode)) {
9741043
if (@available(macOS 11.3, *)) {
9751044
encoderSpecification[(__bridge NSString *)kVTVideoEncoderSpecification_EnableLowLatencyRateControl] = @YES;
9761045
}
9771046
}
978-
if (_encoderMode == XCWVideoEncoderModeH264Software) {
1047+
if (_activeEncoderMode == XCWVideoEncoderModeH264Software) {
9791048
encoderSpecification[(__bridge NSString *)kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder] = @NO;
980-
} else if (_encoderMode == XCWVideoEncoderModeH264Hardware) {
1049+
} else if (_activeEncoderMode == XCWVideoEncoderModeH264Hardware) {
9811050
encoderSpecification[(__bridge NSString *)kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder] = @YES;
982-
} else if (_encoderMode == XCWVideoEncoderModeAuto && _realtimeStreamMode) {
1051+
} else if (_activeEncoderMode == XCWVideoEncoderModeAuto && _realtimeStreamMode) {
9831052
encoderSpecification[(__bridge NSString *)kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder] = @YES;
9841053
}
9851054

@@ -1006,7 +1075,7 @@ - (BOOL)ensureCompressionSessionWithWidth:(int32_t)width height:(int32_t)height
10061075
_needsKeyFrame = YES;
10071076

10081077
int expectedFrameRate = [self expectedFrameRateLocked];
1009-
int averageBitRate = XCWAverageBitRateForDimensions(width, height, _encoderMode, _lowLatencyMode, _realtimeStreamMode);
1078+
int averageBitRate = XCWAverageBitRateForDimensions(width, height, _activeEncoderMode, _lowLatencyMode, _realtimeStreamMode);
10101079

10111080
VTSessionSetProperty(session, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
10121081
if (@available(macOS 10.14, *)) {
@@ -1123,7 +1192,7 @@ - (nullable CVPixelBufferRef)copyScaledPixelBufferIfNeeded:(CVPixelBufferRef)pix
11231192
if (_pixelTransferSession == NULL) {
11241193
OSStatus sessionStatus = VTPixelTransferSessionCreate(kCFAllocatorDefault, &_pixelTransferSession);
11251194
if (sessionStatus != noErr || _pixelTransferSession == NULL) {
1126-
if (_encoderMode == XCWVideoEncoderModeH264Software) {
1195+
if (_activeEncoderMode == XCWVideoEncoderModeH264Software) {
11271196
return [self copySoftwareScaledPixelBuffer:pixelBuffer
11281197
targetWidth:targetWidth
11291198
targetHeight:targetHeight];
@@ -1155,7 +1224,7 @@ - (nullable CVPixelBufferRef)copyScaledPixelBufferIfNeeded:(CVPixelBufferRef)pix
11551224
_lastScaleStatus = transferStatus;
11561225
if (transferStatus != noErr) {
11571226
CVPixelBufferRelease(scaledPixelBuffer);
1158-
if (_encoderMode == XCWVideoEncoderModeH264Software) {
1227+
if (_activeEncoderMode == XCWVideoEncoderModeH264Software) {
11591228
return [self copySoftwareScaledPixelBuffer:pixelBuffer
11601229
targetWidth:targetWidth
11611230
targetHeight:targetHeight];
@@ -1177,7 +1246,7 @@ - (BOOL)shouldUseSoftwareScalerForSourceWidth:(int32_t)sourceWidth
11771246
if (sourceWidth == targetWidth && sourceHeight == targetHeight) {
11781247
return NO;
11791248
}
1180-
return _encoderMode == XCWVideoEncoderModeAuto || _encoderMode == XCWVideoEncoderModeH264Hardware;
1249+
return _activeEncoderMode == XCWVideoEncoderModeAuto || _activeEncoderMode == XCWVideoEncoderModeH264Hardware;
11811250
}
11821251

11831252
- (nullable CVPixelBufferRef)copySoftwareScaledPixelBuffer:(CVPixelBufferRef)pixelBuffer
@@ -1315,8 +1384,11 @@ - (void)handleEncodedSampleBuffer:(CMSampleBufferRef)sampleBuffer
13151384
if (isKeyFrame) {
13161385
_keyFrameOutputCount += 1;
13171386
}
1387+
BOOL shouldEnterAutoSoftwareFallback = NO;
1388+
uint64_t measurementTimeUs = 0;
13181389
if (submittedAtUs > 0) {
13191390
uint64_t nowUs = (uint64_t)(CACurrentMediaTime() * 1000000.0);
1391+
measurementTimeUs = nowUs;
13201392
_latestEncodeLatencyUs = nowUs >= submittedAtUs ? nowUs - submittedAtUs : 0;
13211393
_peakEncodeLatencyUs = MAX(_peakEncodeLatencyUs, _latestEncodeLatencyUs);
13221394
_averageEncodeLatencyUs = _averageEncodeLatencyUs <= 0.0
@@ -1346,6 +1418,10 @@ - (void)handleEncodedSampleBuffer:(CMSampleBufferRef)sampleBuffer
13461418
_overloadEventCount += 1;
13471419
}
13481420
_wasOverloaded = overloaded;
1421+
shouldEnterAutoSoftwareFallback = overloaded &&
1422+
_encoderMode == XCWVideoEncoderModeAuto &&
1423+
_activeEncoderMode != XCWVideoEncoderModeH264Software &&
1424+
_hardwareAccelerated;
13491425
[self adaptSoftwarePacingForLatencyUs:_latestEncodeLatencyUs];
13501426
}
13511427
NSString *codec = nil;
@@ -1400,6 +1476,9 @@ - (void)handleEncodedSampleBuffer:(CMSampleBufferRef)sampleBuffer
14001476

14011477
CGSize dimensions = CGSizeMake(_width, _height);
14021478
self.outputHandler(sampleData, timestampUs, isKeyFrame, codec, decoderConfig, dimensions);
1479+
if (shouldEnterAutoSoftwareFallback) {
1480+
[self enterAutoSoftwareFallbackLockedAtTimeUs:measurementTimeUs];
1481+
}
14031482
}
14041483

14051484
- (void)completeInFlightFrame {

docs/api/health.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ Returns a snapshot of every server-side counter and the rolling buffer of client
5656
{
5757
"udid": "9D7E5BB7-...",
5858
"encoder": {
59-
"encoderMode": "hardware",
59+
"encoderMode": "auto",
60+
"activeEncoderMode": "hardware",
61+
"autoSoftwareFallbackActive": false,
6062
"hardwareAccelerated": true,
6163
"overloadState": "nominal",
6264
"averageEncoderLoadPercent": 42.1,
@@ -113,7 +115,11 @@ is derived from native VideoToolbox submit-to-output latency:
113115

114116
This is an inferred pressure signal rather than a private macOS hardware queue
115117
counter. It is useful for deciding when to lower stream resolution/FPS or switch
116-
from hardware to software encoding.
118+
from hardware to software encoding. When `encoderMode` is `auto`, SimDeck uses
119+
this signal to temporarily rebuild the active session with software H.264 if the
120+
hardware encoder is overloaded, then retries hardware after a cooldown. The
121+
`activeEncoderMode`, `autoSoftwareFallbackActive`, `autoSoftwareFallbacks`, and
122+
`autoHardwareRetries` fields expose that state.
117123

118124
### Client stream stats
119125

docs/guide/troubleshooting.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ sudo xcode-select -s /Applications/Xcode.app
7979

8080
The encoder did not produce a keyframe within 3 seconds. The most common causes:
8181

82-
- **VideoToolbox is busy.** macOS screen recording can starve the hardware H.264 encoder. Switch to software H.264:
82+
- **VideoToolbox is busy.** macOS screen recording can starve the hardware H.264 encoder. Auto mode detects sustained hardware encode overload and temporarily falls back to software H.264. For a fully software-only run, start with:
8383

8484
```sh
8585
simdeck daemon stop

0 commit comments

Comments
 (0)