Fix thread sync

Reformat Metal files

Rewrite Metal render loop to not use DisplayLink so we can use the amazing afterMinimumDuration API call

Improve frame drop algorithm

Misc fixes to HDR <-> SDR switching, deprecation warning cleanup

Fix AVSB mode

Fix renderingBackend selected value

Add Rendering Backend setting to iPhone

Let users choose which renderer to use. Refactor metrics into a singleton

Fix tvOS build

Port moonlight-qt's Metal renderer

Bump imgui to latest

Misc tweaks and refactoring

Switch iOS to use Opus float32 audio, in line with other clients.  It was already being sent from the server as float32, so this is simply decoding it with less loss of dynamic range.

Give FrameQueue a faster data structure and full test coverage. Remove pts frame pacing, it's not a fit for iOS.

Don't allow very transparent opacity for middle stats overlay box

Change wording to select button instead of remote button

Add tvOS gesture for double-clicking remote button to show/hide stats

Remove kVTDecodeFrame_1xRealTimePlayback, this was found to seriously impact decode times

Move decode time stat out of async block

Enabe FQ logging

Misc cleanup/fixes

iPad: 26 will require UIRequiresFullScreen to be false, and for all orientations to be supported

Updated iPhone and iPad settings panels

Add setting for graph opacity. Enable stats and graphs to be toggled via two finger swipe

Fix wait time in displaylink, fix settings spacing, stats fixes

Settings for frame queue size, graphs. Misc refactoring and cleanup

Bump moonlight-common-c

Improve logging readability

Fix host frametime and frame queue stats to be recorded during input not output. Show min/max/avg decode time

Add bytes per frame graph

Started on settings changes, both modes working very well

Misc refactoring, move graphs around, both pacing functions working well

Set frameDropTarget to 2 always

Use raw RTP timestamps without converting to floating point for even more accurate playback

Standard frame pacing: remove +3 logic

Switch to NSRunLoopCommonModes for displayLink

Add frame pacing algorithm based on moonlight-qt's Pacer class as a simpler baseline

Switch to os_unfair_lock, misc tweaks

Large refactor that now uses VTDecompressionSession based on PR535 from felipejfc. This now allows more control over queue size and allows dropping arbitrary frames, but introduces a lot of new problems

bump moonlight-common-c to fix 32-bit rollover issue

LiWait timing graph

Add displaylink interval graph

Graphs checkpoint

ImGui graph working checkpoint

ImGui working checkpoint

Add pending frames to stats, add a define for verbose logging

DisplayLink WIP

Add imgui & implot modules

Frame pacing: alternate method based on thieman's pullFrames thread

Frame pacing: simplify displayLink, support passing targetTimestamp down to the sample buffer

Point to my common-c with some timing changes.

Add stats gestures: 2-finger swipe down to show stats, and 2-finger swipe up to hide

Limit refresh rate to 60 when in low power mode

Display screen refresh rate in stats overlay, remove decode time and explain why it can't be measured

Adjust the refresh rate of the display to match the stream framerate and improve battery life

Don't use presentationTimeMs when creating CMSampleTimingInfo

Fix overlayView build for iOS

Refactor CADisplayLink callback for improved frame delivery. Support a user-configurable buffered frame count.

[opt] Use UILabel for stats overlay
This commit is contained in:
Travis Thieman authored and Acaki committed 2025-07-11 21:02:13 +08:00
1 parent 51e915b598
commit df2b67ac99
61 files changed
+6080 -458

No files matched your search

+58 -21
View File
@@ -7,6 +7,7 @@
//
#import "Connection.h"
#import "Plot.h"
#import "Utils.h"
#import <VideoToolbox/VideoToolbox.h>
@@ -45,24 +46,28 @@ static int audioFrameSize;
static VideoDecoderRenderer* renderer;
static BandwidthTracker *bwTracker;
int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags)
{
[renderer setupWithVideoFormat:videoFormat width:width height:height frameRate:redrawRate];
lastFrameNumber = 0;
activeVideoFormat = videoFormat;
Log(LOG_I, @"Active video format: 0x%x", activeVideoFormat);
memset(&currentVideoStats, 0, sizeof(currentVideoStats));
memset(&lastVideoStats, 0, sizeof(lastVideoStats));
bwTracker = [[BandwidthTracker alloc] initWithWindowSeconds:10 bucketIntervalMs:250];
return 0;
}
void DrStart(void)
void DrCleanup(void)
{
[renderer start];
[renderer cleanup];
}
void DrStop(void)
-(BandwidthTracker *) getBwTracker
{
[renderer stop];
return bwTracker;
}
-(BOOL) getVideoStats:(video_stats_t*)stats
@@ -72,10 +77,12 @@ void DrStop(void)
if (lastVideoStats.endTime != 0) {
memcpy(stats, &lastVideoStats, sizeof(*stats));
[videoStatsLock unlock];
// Pull in the separately-collected renderer stats
[renderer getAllStats:stats];
return YES;
}
// No stats yet
[videoStatsLock unlock];
return NO;
}
@@ -133,6 +140,8 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
{
int offset = 0;
int ret;
CFTimeInterval decodeStartTime = CACurrentMediaTime();
unsigned char* data = (unsigned char*) malloc(decodeUnit->fullLength);
if (data == NULL) {
// A frame was lost due to OOM condition
@@ -158,8 +167,13 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
}
// Any frame number greater than m_LastFrameNumber + 1 represents a dropped frame
currentVideoStats.networkDroppedFrames += decodeUnit->frameNumber - (lastFrameNumber + 1);
currentVideoStats.totalFrames += decodeUnit->frameNumber - (lastFrameNumber + 1);
int droppedFrames = decodeUnit->frameNumber - (lastFrameNumber + 1);
if (droppedFrames > 0) {
currentVideoStats.networkDroppedFrames += droppedFrames;
currentVideoStats.totalFrames += droppedFrames;
Log(LOG_W, @"Network dropped %d frame(s): %d - %d", droppedFrames, lastFrameNumber + 1, decodeUnit->frameNumber - 1);
}
lastFrameNumber = decodeUnit->frameNumber;
}
@@ -179,6 +193,8 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
currentVideoStats.receivedFrames++;
currentVideoStats.totalFrames++;
[bwTracker addBytes:decodeUnit->fullLength];
PLENTRY entry = decodeUnit->bufferList;
while (entry != NULL) {
// Submit parameter set NALUs directly since no copy is required by the decoder
@@ -186,7 +202,8 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
ret = [renderer submitDecodeBuffer:(unsigned char*)entry->data
length:entry->length
bufferType:entry->bufferType
decodeUnit:decodeUnit];
decodeUnit:decodeUnit
decodeStartTime:decodeStartTime];
if (ret != DR_OK) {
free(data);
return ret;
@@ -204,7 +221,8 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
return [renderer submitDecodeBuffer:data
length:offset
bufferType:BUFFER_TYPE_PICDATA
decodeUnit:decodeUnit];
decodeUnit:decodeUnit
decodeStartTime:decodeStartTime];
}
int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags)
@@ -219,7 +237,7 @@ int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, v
SDL_zero(want);
want.freq = opusConfig->sampleRate;
want.format = AUDIO_S16;
want.format = AUDIO_F32;
want.channels = opusConfig->channelCount;
want.samples = opusConfig->samplesPerFrame;
@@ -231,7 +249,7 @@ int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, v
}
audioConfig = *opusConfig;
audioFrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
audioFrameSize = opusConfig->samplesPerFrame * sizeof(float) * opusConfig->channelCount;
audioBuffer = SDL_malloc(audioFrameSize);
if (audioBuffer == NULL) {
Log(LOG_E, @"Failed to allocate audio frame buffer");
@@ -289,19 +307,23 @@ void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
if (LiGetPendingAudioDuration() > 30) {
return;
}
decodeLen = opus_multistream_decode(opusDecoder, (unsigned char *)sampleData, sampleLength,
(short*)audioBuffer, audioConfig.samplesPerFrame, 0);
decodeLen = opus_multistream_decode_float(opusDecoder,
(unsigned char*)sampleData,
sampleLength,
(float*)audioBuffer,
audioConfig.samplesPerFrame,
0);
if (decodeLen > 0) {
// Provide backpressure on the queue to ensure too many frames don't build up
// in SDL's audio queue.
while (SDL_GetQueuedAudioSize(audioDevice) / audioFrameSize > 10) {
SDL_Delay(1);
[NSThread sleepForTimeInterval:0.001f];
}
if (SDL_QueueAudio(audioDevice,
audioBuffer,
sizeof(short) * decodeLen * audioConfig.channelCount) < 0) {
sizeof(float) * decodeLen * audioConfig.channelCount) < 0) {
Log(LOG_E, @"Failed to queue audio sample: %s\n", SDL_GetError());
}
}
@@ -435,6 +457,19 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
renderer = myRenderer;
_callbacks = callbacks;
// Check for low power mode which limits framerate to 60
if (config.frameRate > 60 && [[NSProcessInfo processInfo] isLowPowerModeEnabled]) {
Log(LOG_W, @"Limiting stream to 60fps because device is in low power mode");
config.frameRate = 60;
}
// Lower to 90fps on Vision Pro
NSInteger deviceFps = UIScreen.mainScreen.maximumFramesPerSecond;
if (deviceFps < config.frameRate) {
Log(LOG_W, @"Limiting stream to %dfps due to max refresh rate", deviceFps);
config.frameRate = (int)deviceFps;
}
LiInitializeStreamConfiguration(&_streamConfig);
_streamConfig.width = config.width;
_streamConfig.height = config.height;
@@ -442,7 +477,7 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
_streamConfig.bitrate = config.bitRate;
_streamConfig.supportedVideoFormats = config.supportedVideoFormats;
_streamConfig.audioConfiguration = config.audioConfiguration;
// Since we require iOS 12 or above, we're guaranteed to be running
// on a 64-bit device with ARMv8 crypto instructions, so we don't
// need to check for that here.
@@ -466,9 +501,9 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
LiInitializeVideoCallbacks(&_drCallbacks);
_drCallbacks.setup = DrDecoderSetup;
_drCallbacks.start = DrStart;
_drCallbacks.stop = DrStop;
_drCallbacks.capabilities = CAPABILITY_PULL_RENDERER |
_drCallbacks.cleanup = DrCleanup;
_drCallbacks.submitDecodeUnit = DrSubmitDecodeUnit;
_drCallbacks.capabilities = CAPABILITY_DIRECT_SUBMIT |
CAPABILITY_REFERENCE_FRAME_INVALIDATION_HEVC |
CAPABILITY_REFERENCE_FRAME_INVALIDATION_AV1;
@@ -484,7 +519,9 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
_clCallbacks.stageFailed = ClStageFailed;
_clCallbacks.connectionStarted = ClConnectionStarted;
_clCallbacks.connectionTerminated = ClConnectionTerminated;
#ifdef DEBUG
_clCallbacks.logMessage = ClLogMessage;
#endif
_clCallbacks.rumble = ClRumble;
_clCallbacks.connectionStatusUpdate = ClConnectionStatusUpdate;
_clCallbacks.setHdrMode = ClSetHdrMode;