Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 134 additions & 3 deletions cli/XCWH264Encoder.m
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
static const NSUInteger XCWRealtimeHardwareHealthyFrameWindow = 6;
static const NSUInteger XCWMaximumRealtimeInFlightFrames = 3;
static const int32_t XCWRealtimeKeyFrameIntervalSeconds = 5;
static const double XCWEncoderLatencyEWMAAlpha = 0.2;
static const double XCWEncoderStrainedLoadPercent = 85.0;
static const double XCWEncoderOverloadedLoadPercent = 105.0;
static const NSUInteger XCWEncoderConsecutiveOverBudgetFrameThreshold = 3;

typedef NS_ENUM(NSUInteger, XCWVideoEncoderMode) {
XCWVideoEncoderModeAuto,
Expand Down Expand Up @@ -477,11 +481,17 @@ @interface XCWH264Encoder ()
- (nullable CVPixelBufferRef)copySoftwareScaledPixelBuffer:(CVPixelBufferRef)pixelBuffer
targetWidth:(int32_t)targetWidth
targetHeight:(int32_t)targetHeight;
- (BOOL)shouldUseSoftwareScalerForSourceWidth:(int32_t)sourceWidth
sourceHeight:(int32_t)sourceHeight
targetWidth:(int32_t)targetWidth
targetHeight:(int32_t)targetHeight
pixelFormat:(OSType)pixelFormat;
- (nullable CVPixelBufferRef)copyPixelBufferFromScalingPoolWithWidth:(int32_t)targetWidth
height:(int32_t)targetHeight
pixelFormat:(OSType)pixelFormat;
- (void)handleCompressionOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
submittedAtUs:(uint64_t)submittedAtUs;
- (uint64_t)activeFrameIntervalUsLocked;

@end

Expand Down Expand Up @@ -515,6 +525,13 @@ @implementation XCWH264Encoder {
NSUInteger _keyFrameOutputCount;
NSUInteger _maxInFlightFrameCount;
uint64_t _latestEncodeLatencyUs;
double _averageEncodeLatencyUs;
uint64_t _peakEncodeLatencyUs;
NSUInteger _overBudgetFrameCount;
NSUInteger _consecutiveOverBudgetFrameCount;
NSUInteger _consecutiveStrainedFrameCount;
NSUInteger _overloadEventCount;
BOOL _wasOverloaded;
uint64_t _softwareFrameIntervalUs;
uint64_t _lastSoftwareSubmissionUs;
NSUInteger _softwarePacedFrameCount;
Expand Down Expand Up @@ -616,6 +633,33 @@ - (NSDictionary *)statsRepresentation {

__block NSDictionary *stats = nil;
dispatch_sync(_queue, ^{
uint64_t encoderBudgetUs = [self activeFrameIntervalUsLocked];
double latestLoadPercent = encoderBudgetUs > 0
? ((double)self->_latestEncodeLatencyUs * 100.0) / (double)encoderBudgetUs
: 0.0;
double averageLoadPercent = encoderBudgetUs > 0
? (self->_averageEncodeLatencyUs * 100.0) / (double)encoderBudgetUs
: 0.0;
BOOL overloaded = averageLoadPercent >= XCWEncoderOverloadedLoadPercent ||
self->_consecutiveOverBudgetFrameCount >= XCWEncoderConsecutiveOverBudgetFrameThreshold;
BOOL strained = overloaded ||
averageLoadPercent >= XCWEncoderStrainedLoadPercent ||
self->_consecutiveStrainedFrameCount >= XCWEncoderConsecutiveOverBudgetFrameThreshold;
NSString *overloadState = overloaded
? @"overloaded"
: strained
? @"strained"
: @"nominal";
NSString *overloadReason = @"within-budget";
if (overloaded) {
overloadReason = self->_consecutiveOverBudgetFrameCount >= XCWEncoderConsecutiveOverBudgetFrameThreshold
? @"consecutive-frames-over-budget"
: @"average-latency-over-budget";
} else if (strained) {
overloadReason = averageLoadPercent >= XCWEncoderStrainedLoadPercent
? @"average-latency-near-budget"
: @"consecutive-frames-near-budget";
}
stats = @{
@"inputFrames": @(inputFrameCount),
@"pendingReplacements": @(pendingReplacementCount),
Expand All @@ -626,6 +670,18 @@ - (NSDictionary *)statsRepresentation {
@"inFlightFrames": @(self->_inFlightFrameCount),
@"maxInFlightFrames": @(self->_maxInFlightFrameCount),
@"latestEncodeLatencyUs": @(self->_latestEncodeLatencyUs),
@"averageEncodeLatencyUs": @(self->_averageEncodeLatencyUs),
@"peakEncodeLatencyUs": @(self->_peakEncodeLatencyUs),
@"encoderBudgetUs": @(encoderBudgetUs),
@"encoderLoadPercent": @(latestLoadPercent),
@"averageEncoderLoadPercent": @(averageLoadPercent),
@"overloadState": overloadState,
@"overloaded": @(overloaded),
@"overloadReason": overloadReason,
@"overBudgetFrames": @(self->_overBudgetFrameCount),
@"consecutiveOverBudgetFrames": @(self->_consecutiveOverBudgetFrameCount),
@"consecutiveStrainedFrames": @(self->_consecutiveStrainedFrameCount),
@"overloadEvents": @(self->_overloadEventCount),
@"softwareFrameIntervalUs": @(self->_softwareFrameIntervalUs),
@"softwareTargetFps": @(self->_softwareFrameIntervalUs > 0 ? (1000000.0 / (double)self->_softwareFrameIntervalUs) : 0.0),
@"softwarePacedFrames": @(self->_softwarePacedFrameCount),
Expand Down Expand Up @@ -715,7 +771,22 @@ - (uint64_t)initialHardwareFrameIntervalUsLocked {
}

- (uint64_t)maximumHardwareFrameIntervalUsLocked {
return _realtimeStreamMode ? XCWRealtimeMaximumFrameIntervalUs() : XCWLocalStreamMaximumFrameIntervalUs();
if (_realtimeStreamMode) {
uint64_t minimumFpsIntervalUs = (uint64_t)llround(1000000.0 / (double)XCWMinimumLocalStreamFrameRate);
return MAX(XCWRealtimeMaximumFrameIntervalUs(), minimumFpsIntervalUs);
}
return XCWLocalStreamMaximumFrameIntervalUs();
}

- (uint64_t)activeFrameIntervalUsLocked {
if (_encoderMode == XCWVideoEncoderModeH264Software) {
return _softwareFrameIntervalUs > 0 ? _softwareFrameIntervalUs : [self initialSoftwareFrameIntervalUsLocked];
}
if (_encoderMode == XCWVideoEncoderModeAuto || _encoderMode == XCWVideoEncoderModeH264Hardware) {
return _hardwareFrameIntervalUs > 0 ? _hardwareFrameIntervalUs : [self initialHardwareFrameIntervalUsLocked];
}
int32_t expectedFrameRate = MAX(1, [self expectedFrameRateLocked]);
return (uint64_t)llround(1000000.0 / (double)expectedFrameRate);
}

- (int32_t)expectedFrameRateLocked {
Expand Down Expand Up @@ -807,7 +878,7 @@ - (void)adaptSoftwarePacingForLatencyUs:(uint64_t)latencyUs {
}

- (void)adaptHardwarePacingForLatencyUs:(uint64_t)latencyUs {
if ((_encoderMode != XCWVideoEncoderModeAuto && _encoderMode != XCWVideoEncoderModeH264Hardware) || !_lowLatencyMode || latencyUs == 0) {
if ((_encoderMode != XCWVideoEncoderModeAuto && _encoderMode != XCWVideoEncoderModeH264Hardware) || !_realtimeStreamMode || latencyUs == 0) {
return;
}
if (_hardwareFrameIntervalUs == 0) {
Expand Down Expand Up @@ -1066,6 +1137,12 @@ - (void)invalidateCompressionSessionLocked {
_inFlightFrameCount = 0;
_lastSoftwareSubmissionUs = 0;
_lastHardwareSubmissionUs = 0;
_latestEncodeLatencyUs = 0;
_averageEncodeLatencyUs = 0;
_peakEncodeLatencyUs = 0;
_consecutiveOverBudgetFrameCount = 0;
_consecutiveStrainedFrameCount = 0;
_wasOverloaded = NO;
_hardwareAccelerated = NO;
_selectedEncoderID = nil;
_scalingActive = NO;
Expand All @@ -1077,11 +1154,24 @@ - (nullable CVPixelBufferRef)copyScaledPixelBufferIfNeeded:(CVPixelBufferRef)pix
targetHeight:(int32_t)targetHeight {
int32_t sourceWidth = (int32_t)CVPixelBufferGetWidth(pixelBuffer);
int32_t sourceHeight = (int32_t)CVPixelBufferGetHeight(pixelBuffer);
OSType sourcePixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);
BOOL shouldCopyStableRealtimeBuffer = _realtimeStreamMode || _lowLatencyMode;
if (sourceWidth == targetWidth && sourceHeight == targetHeight && !shouldCopyStableRealtimeBuffer) {
CVPixelBufferRetain(pixelBuffer);
return pixelBuffer;
}
if ([self shouldUseSoftwareScalerForSourceWidth:sourceWidth
sourceHeight:sourceHeight
targetWidth:targetWidth
targetHeight:targetHeight
pixelFormat:sourcePixelFormat]) {
CVPixelBufferRef scaledPixelBuffer = [self copySoftwareScaledPixelBuffer:pixelBuffer
targetWidth:targetWidth
targetHeight:targetHeight];
if (scaledPixelBuffer != NULL) {
return scaledPixelBuffer;
}
}

if (_pixelTransferSession == NULL) {
OSStatus sessionStatus = VTPixelTransferSessionCreate(kCFAllocatorDefault, &_pixelTransferSession);
Expand All @@ -1105,7 +1195,6 @@ - (nullable CVPixelBufferRef)copyScaledPixelBufferIfNeeded:(CVPixelBufferRef)pix
kVTScalingMode_Normal);
}

OSType sourcePixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);
CVPixelBufferRef scaledPixelBuffer = [self copyPixelBufferFromScalingPoolWithWidth:targetWidth
height:targetHeight
pixelFormat:sourcePixelFormat];
Expand All @@ -1130,6 +1219,20 @@ - (nullable CVPixelBufferRef)copyScaledPixelBufferIfNeeded:(CVPixelBufferRef)pix
return scaledPixelBuffer;
}

- (BOOL)shouldUseSoftwareScalerForSourceWidth:(int32_t)sourceWidth
sourceHeight:(int32_t)sourceHeight
targetWidth:(int32_t)targetWidth
targetHeight:(int32_t)targetHeight
pixelFormat:(OSType)pixelFormat {
if (!_realtimeStreamMode || !XCWPixelFormatSupportsSoftwareScaling(pixelFormat)) {
return NO;
}
if (sourceWidth == targetWidth && sourceHeight == targetHeight) {
return NO;
}
return _encoderMode == XCWVideoEncoderModeAuto || _encoderMode == XCWVideoEncoderModeH264Hardware;
}

- (nullable CVPixelBufferRef)copySoftwareScaledPixelBuffer:(CVPixelBufferRef)pixelBuffer
targetWidth:(int32_t)targetWidth
targetHeight:(int32_t)targetHeight {
Expand Down Expand Up @@ -1268,6 +1371,34 @@ - (void)handleEncodedSampleBuffer:(CMSampleBufferRef)sampleBuffer
if (submittedAtUs > 0) {
uint64_t nowUs = (uint64_t)(CACurrentMediaTime() * 1000000.0);
_latestEncodeLatencyUs = nowUs >= submittedAtUs ? nowUs - submittedAtUs : 0;
_peakEncodeLatencyUs = MAX(_peakEncodeLatencyUs, _latestEncodeLatencyUs);
_averageEncodeLatencyUs = _averageEncodeLatencyUs <= 0.0
? (double)_latestEncodeLatencyUs
: (_averageEncodeLatencyUs * (1.0 - XCWEncoderLatencyEWMAAlpha)) + ((double)_latestEncodeLatencyUs * XCWEncoderLatencyEWMAAlpha);
uint64_t encoderBudgetUs = [self activeFrameIntervalUsLocked];
double averageLoadPercent = encoderBudgetUs > 0
? (_averageEncodeLatencyUs * 100.0) / (double)encoderBudgetUs
: 0.0;
double latestLoadPercent = encoderBudgetUs > 0
? ((double)_latestEncodeLatencyUs * 100.0) / (double)encoderBudgetUs
: 0.0;
if (encoderBudgetUs > 0 && _latestEncodeLatencyUs > encoderBudgetUs) {
_overBudgetFrameCount += 1;
_consecutiveOverBudgetFrameCount += 1;
} else {
_consecutiveOverBudgetFrameCount = 0;
}
if (latestLoadPercent >= XCWEncoderStrainedLoadPercent) {
_consecutiveStrainedFrameCount += 1;
} else {
_consecutiveStrainedFrameCount = 0;
}
BOOL overloaded = averageLoadPercent >= XCWEncoderOverloadedLoadPercent ||
_consecutiveOverBudgetFrameCount >= XCWEncoderConsecutiveOverBudgetFrameThreshold;
if (overloaded && !_wasOverloaded) {
_overloadEventCount += 1;
}
_wasOverloaded = overloaded;
[self adaptSoftwarePacingForLatencyUs:_latestEncodeLatencyUs];
[self adaptHardwarePacingForLatencyUs:_latestEncodeLatencyUs];
}
Expand Down
18 changes: 18 additions & 0 deletions client/src/api/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
export interface EncoderStats {
averageEncodeLatencyUs?: number;
averageEncoderLoadPercent?: number;
consecutiveOverBudgetFrames?: number;
encoderBudgetUs?: number;
encoderLoadPercent?: number;
encoderMode?: string;
hardwareAccelerated?: boolean;
latestEncodeLatencyUs?: number;
overloadEvents?: number;
overloaded?: boolean;
overloadReason?: string;
overloadState?: "nominal" | "strained" | "overloaded" | string;
peakEncodeLatencyUs?: number;
selectedEncoderId?: string | null;
}

export interface PrivateDisplayInfo {
displayReady: boolean;
displayStatus: string;
displayWidth: number;
displayHeight: number;
encoder?: EncoderStats;
frameSequence: number;
rotationQuarterTurns?: number;
}
Expand Down
7 changes: 7 additions & 0 deletions client/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1669,6 +1669,7 @@ export function AppShell({
debugPanel={
debugVisible ? (
<DebugPanel
encoder={selectedSimulator.privateDisplay?.encoder}
fps={fps}
inline
onClose={() => setDebugVisible(false)}
Expand Down Expand Up @@ -1827,6 +1828,12 @@ function normalizeStreamQuality(
if (normalized === "economy" || normalized === "ci-software") {
return "economy";
}
if (normalized === "low") {
return "low";
}
if (normalized === "tiny") {
return "tiny";
}
return fallback;
}

Expand Down
8 changes: 5 additions & 3 deletions client/src/features/simulators/SimulatorMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,11 @@ const STREAM_QUALITY_OPTIONS: Array<{
label: string;
value: StreamQualityPreset;
}> = [
{ label: "Quality", value: "quality" },
{ label: "Balanced", value: "balanced" },
{ label: "Economy", value: "economy" },
{ label: "Full", value: "quality" },
{ label: "1280", value: "balanced" },
{ label: "1080", value: "economy" },
{ label: "720", value: "low" },
{ label: "540", value: "tiny" },
];

function MenuIcon() {
Expand Down
4 changes: 3 additions & 1 deletion client/src/features/stream/streamTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ export type StreamQualityPreset =
| "ci-software"
| "economy"
| "fast"
| "low"
| "quality"
| "smooth";
| "smooth"
| "tiny";

export interface StreamConfig {
encoder: StreamEncoder;
Expand Down
37 changes: 37 additions & 0 deletions client/src/features/toolbar/DebugPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { EncoderStats } from "../../api/types";
import type {
StreamRuntimeInfo,
StreamStats,
StreamStatus,
} from "../stream/streamTypes";

interface DebugPanelProps {
encoder?: EncoderStats | null;
fps: number;
inline?: boolean;
onClose?: () => void;
Expand All @@ -27,6 +29,20 @@ function formatMs(value: number): string {
return `${value.toFixed(1)} ms`;
}

function formatUsAsMs(value: number | undefined): string {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
return "—";
}
return `${(value / 1000).toFixed(1)} ms`;
}

function formatPercent(value: number | undefined): string {
if (typeof value !== "number" || !Number.isFinite(value)) {
return "—";
}
return `${value.toFixed(0)}%`;
}

function formatResolution(stats: StreamStats): string {
if (!stats.width || !stats.height) {
return "—";
Expand All @@ -35,6 +51,7 @@ function formatResolution(stats: StreamStats): string {
}

export function DebugPanel({
encoder,
fps,
inline = false,
onClose,
Expand All @@ -60,6 +77,26 @@ export function DebugPanel({
{ label: "Frame Gap", value: formatMs(stats.latestFrameGapMs) },
{ label: "Path", value: runtimeInfo.streamBackend },
];
if (encoder) {
rows.push(
{ label: "Encoder", value: encoder.encoderMode ?? "—" },
{ label: "Encoder State", value: encoder.overloadState ?? "—" },
{
label: "Encoder Load",
value: formatPercent(encoder.averageEncoderLoadPercent),
},
{
label: "Encode Latency",
value: formatUsAsMs(encoder.averageEncodeLatencyUs),
},
{ label: "Encode Budget", value: formatUsAsMs(encoder.encoderBudgetUs) },
{ label: "Encoder Reason", value: encoder.overloadReason ?? "—" },
{
label: "Overload Events",
value: String(encoder.overloadEvents ?? 0),
},
);
}

return (
<section
Expand Down
Loading
Loading