From 20c05eda6a0d410d84fe97a3a79d04fb70609d48 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Tue, 25 Nov 2025 17:26:01 -0600 Subject: [PATCH 01/10] Don't lock the ENet mutex when querying for RTT information --- src/ControlStream.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ControlStream.c b/src/ControlStream.c index 4280963..b7e3685 100644 --- a/src/ControlStream.c +++ b/src/ControlStream.c @@ -1670,7 +1670,10 @@ bool isControlDataInTransit(void) { bool LiGetEstimatedRttInfo(uint32_t* estimatedRtt, uint32_t* estimatedRttVariance) { bool ret = false; - PltLockMutex(&enetMutex); + // We do not acquire enetMutex here because we're just reading metrics + // and observing a torn write every once in a while is totally fine. + // The peer pointer points to memory reserved inside the client object, + // so it's guaranteed that it will never go away underneath us. if (peer != NULL && peer->state == ENET_PEER_STATE_CONNECTED) { if (estimatedRtt != NULL) { *estimatedRtt = peer->roundTripTime; @@ -1682,7 +1685,6 @@ bool LiGetEstimatedRttInfo(uint32_t* estimatedRtt, uint32_t* estimatedRttVarianc ret = true; } - PltUnlockMutex(&enetMutex); return ret; } From b126e481a195fdc7152d211def17190e3434bcce Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Tue, 25 Nov 2025 17:30:01 -0600 Subject: [PATCH 02/10] Improve locking for batched mouse and gamepad sensor events By unlocking the mutex before we enqueue the new entry, we can avoid the input thread immediately contending on the mutex after the new item wakes it up. --- src/InputStream.c | 54 +++++++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/src/InputStream.c b/src/InputStream.c index 06913ff..e8e60a4 100644 --- a/src/InputStream.c +++ b/src/InputStream.c @@ -724,9 +724,14 @@ int LiSendMouseMoveEvent(short deltaX, short deltaY) { // Queue a packet holder if this is the only pending relative mouse event if (!currentRelativeMouseState.dirty) { + // Set the dirty flag to claim ownership of inserting the packet holder + // and unlock to allow other threads to enqueue or process input. + currentRelativeMouseState.dirty = true; + PltUnlockMutex(&batchedInputMutex); + holder = allocatePacketHolder(0); if (holder == NULL) { - PltUnlockMutex(&batchedInputMutex); + currentRelativeMouseState.dirty = false; return -1; } @@ -747,22 +752,21 @@ int LiSendMouseMoveEvent(short deltaX, short deltaY) { // Remaining fields are set in the input thread based on the latest currentRelativeMouseState values err = LbqOfferQueueItem(&packetQueue, holder, &holder->entry); - if (err == LBQ_SUCCESS) { - currentRelativeMouseState.dirty = true; - } - else { + if (err != LBQ_SUCCESS) { LC_ASSERT(err == LBQ_BOUND_EXCEEDED); Limelog("Input queue reached maximum size limit\n"); freePacketHolder(holder); + + // We weren't able to insert the entry, so let the next call try again + currentRelativeMouseState.dirty = false; } } else { // There's already a packet holder queued to send this event + PltUnlockMutex(&batchedInputMutex); err = 0; } - PltUnlockMutex(&batchedInputMutex); - return err; } @@ -785,9 +789,14 @@ int LiSendMousePositionEvent(short x, short y, short referenceWidth, short refer // Queue a packet holder if this is the only pending absolute mouse event if (!currentAbsoluteMouseState.dirty) { + // Set the dirty flag to claim ownership of inserting the packet holder + // and unlock to allow other threads to enqueue or process input. + currentAbsoluteMouseState.dirty = true; + PltUnlockMutex(&batchedInputMutex); + holder = allocatePacketHolder(0); if (holder == NULL) { - PltUnlockMutex(&batchedInputMutex); + currentAbsoluteMouseState.dirty = false; return -1; } @@ -803,22 +812,21 @@ int LiSendMousePositionEvent(short x, short y, short referenceWidth, short refer // Remaining fields are set in the input thread based on the latest currentAbsoluteMouseState values err = LbqOfferQueueItem(&packetQueue, holder, &holder->entry); - if (err == LBQ_SUCCESS) { - currentAbsoluteMouseState.dirty = true; - } - else { + if (err != LBQ_SUCCESS) { LC_ASSERT(err == LBQ_BOUND_EXCEEDED); Limelog("Input queue reached maximum size limit\n"); freePacketHolder(holder); + + // We weren't able to insert the entry, so let the next call try again + currentAbsoluteMouseState.dirty = false; } } else { // There's already a packet holder queued to send this event + PltUnlockMutex(&batchedInputMutex); err = 0; } - PltUnlockMutex(&batchedInputMutex); - // This is not thread safe, but it's not a big deal because callers that want to // use LiSendRelativeMotionAsMousePositionEvent() must not mix these function // without synchronization (otherwise the state of the cursor on the host is @@ -1536,9 +1544,14 @@ int LiSendControllerMotionEvent(uint8_t controllerNumber, uint8_t motionType, fl // Queue a packet holder if this is the only pending sensor event if (!currentGamepadSensorState[controllerNumber][motionType - 1].dirty) { + // Set the dirty flag to claim ownership of inserting the packet holder + // and unlock to allow other threads to enqueue or process input. + currentGamepadSensorState[controllerNumber][motionType - 1].dirty = true; + PltUnlockMutex(&batchedInputMutex); + holder = allocatePacketHolder(0); if (holder == NULL) { - PltUnlockMutex(&batchedInputMutex); + currentGamepadSensorState[controllerNumber][motionType - 1].dirty = false; return -1; } @@ -1554,22 +1567,21 @@ int LiSendControllerMotionEvent(uint8_t controllerNumber, uint8_t motionType, fl // Remaining fields are set in the input thread based on the latest currentGamepadSensorState values err = LbqOfferQueueItem(&packetQueue, holder, &holder->entry); - if (err == LBQ_SUCCESS) { - currentGamepadSensorState[controllerNumber][motionType - 1].dirty = true; - } - else { + if (err != LBQ_SUCCESS) { LC_ASSERT(err == LBQ_BOUND_EXCEEDED); Limelog("Input queue reached maximum size limit\n"); freePacketHolder(holder); + + // We weren't able to insert the entry, so let the next call try again + currentGamepadSensorState[controllerNumber][motionType - 1].dirty = false; } } else { // There's already a packet holder queued to send this event + PltUnlockMutex(&batchedInputMutex); err = 0; } - PltUnlockMutex(&batchedInputMutex); - return err; } From 0586f3d65f0f7cec20a58d555f3d40debd7c6865 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 5 Jan 2026 00:21:35 -0600 Subject: [PATCH 03/10] Fix thread context leak on non-Vita platforms --- src/Platform.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Platform.c b/src/Platform.c index 30ae48b..4d123dc 100644 --- a/src/Platform.c +++ b/src/Platform.c @@ -86,9 +86,7 @@ void* ThreadProc(void* context) { ctx->entry(ctx->context); -#if defined(__vita__) -free(ctx); -#endif + free(ctx); #if defined(LC_WINDOWS) || defined(__vita__) || defined(__WIIU__) || defined(__3DS__) return 0; From 3a377e7d7be7776d68a57828ae22283144285f90 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 5 Jan 2026 00:23:31 -0600 Subject: [PATCH 04/10] Only set an explicit pthread stack size on Vita --- src/Platform.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Platform.c b/src/Platform.c index 4d123dc..0473f13 100644 --- a/src/Platform.c +++ b/src/Platform.c @@ -286,7 +286,9 @@ int PltCreateThread(const char* name, ThreadEntry entry, void* context, PLT_THRE pthread_attr_init(&attr); +#ifdef __vita__ pthread_attr_setstacksize(&attr, 0x100000); +#endif ctx->name = name; From 435bc6a5a4852c90cfb037de1378c0334ed36d8e Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 11 Jan 2026 01:46:30 -0600 Subject: [PATCH 05/10] Fix pthread_attr leak --- src/Platform.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Platform.c b/src/Platform.c index 0473f13..d68e692 100644 --- a/src/Platform.c +++ b/src/Platform.c @@ -293,6 +293,7 @@ int PltCreateThread(const char* name, ThreadEntry entry, void* context, PLT_THRE ctx->name = name; int err = pthread_create(&thread->thread, &attr, ThreadProc, ctx); + pthread_attr_destroy(&attr); if (err != 0) { free(ctx); return err; From 2a5a1f3e8a57cbbb316ed7dfff3a3965c2e77d25 Mon Sep 17 00:00:00 2001 From: ns6089 <61738816+ns6089@users.noreply.github.com> Date: Wed, 21 Jan 2026 06:47:38 +0300 Subject: [PATCH 06/10] Add support for LTR ACK control messages (#122) * Add support for LTR ACK control messages --- src/ControlStream.c | 131 ++++++++++++++++++++++++++++----------- src/Limelight-internal.h | 4 +- src/RtpVideoQueue.c | 2 +- src/Video.h | 20 +++++- src/VideoDepacketizer.c | 8 ++- 5 files changed, 121 insertions(+), 44 deletions(-) diff --git a/src/ControlStream.c b/src/ControlStream.c index b7e3685..2b5182e 100644 --- a/src/ControlStream.c +++ b/src/ControlStream.c @@ -31,11 +31,12 @@ typedef struct _NVCTL_ENCRYPTED_PACKET_HEADER { // encrypted NVCTL_ENET_PACKET_HEADER_V2 and payload data follow } NVCTL_ENCRYPTED_PACKET_HEADER, *PNVCTL_ENCRYPTED_PACKET_HEADER; -typedef struct _QUEUED_FRAME_INVALIDATION_TUPLE { +typedef struct _QUEUED_REFERENCE_FRAME_CONTROL { uint32_t startFrame; uint32_t endFrame; + bool invalidate; // true: RFI(startFrame, endFrame); false: LTR_ACK(startFrame) LINKED_BLOCKING_QUEUE_ENTRY entry; -} QUEUED_FRAME_INVALIDATION_TUPLE, *PQUEUED_FRAME_INVALIDATION_TUPLE; +} QUEUED_REFERENCE_FRAME_CONTROL, *PQUEUED_REFERENCE_FRAME_CONTROL; typedef struct _QUEUED_FRAME_FEC_STATUS { SS_FRAME_FEC_STATUS fecStatus; @@ -113,7 +114,7 @@ static int lastConnectionStatusUpdate; static uint32_t currentEnetSequenceNumber; static uint64_t firstFrameTimeMs; -static LINKED_BLOCKING_QUEUE invalidReferenceFrameTuples; +static LINKED_BLOCKING_QUEUE referenceFrameControlQueue; static LINKED_BLOCKING_QUEUE frameFecStatusQueue; static LINKED_BLOCKING_QUEUE asyncCallbackQueue; static PLT_EVENT idrFrameRequiredEvent; @@ -300,7 +301,7 @@ static bool supportsIdrFrameRequest; int initializeControlStream(void) { stopping = false; PltCreateEvent(&idrFrameRequiredEvent); - LbqInitializeLinkedBlockingQueue(&invalidReferenceFrameTuples, 20); + LbqInitializeLinkedBlockingQueue(&referenceFrameControlQueue, 20); LbqInitializeLinkedBlockingQueue(&frameFecStatusQueue, 8); // Limits number of frame status reports per periodic ping interval LbqInitializeLinkedBlockingQueue(&asyncCallbackQueue, 30); PltCreateMutex(&enetMutex); @@ -375,7 +376,7 @@ void destroyControlStream(void) { PltDestroyCryptoContext(encryptionCtx); PltDestroyCryptoContext(decryptionCtx); PltCloseEvent(&idrFrameRequiredEvent); - freeBasicLbqList(LbqDestroyLinkedBlockingQueue(&invalidReferenceFrameTuples)); + freeBasicLbqList(LbqDestroyLinkedBlockingQueue(&referenceFrameControlQueue)); freeBasicLbqList(LbqDestroyLinkedBlockingQueue(&frameFecStatusQueue)); freeBasicLbqList(LbqDestroyLinkedBlockingQueue(&asyncCallbackQueue)); @@ -386,12 +387,15 @@ static void queueFrameInvalidationTuple(uint32_t startFrame, uint32_t endFrame) LC_ASSERT(startFrame <= endFrame); if (isReferenceFrameInvalidationEnabled()) { - PQUEUED_FRAME_INVALIDATION_TUPLE qfit; + PQUEUED_REFERENCE_FRAME_CONTROL qfit; qfit = malloc(sizeof(*qfit)); if (qfit != NULL) { - qfit->startFrame = startFrame; - qfit->endFrame = endFrame; - if (LbqOfferQueueItem(&invalidReferenceFrameTuples, qfit, &qfit->entry) == LBQ_BOUND_EXCEEDED) { + *qfit = (QUEUED_REFERENCE_FRAME_CONTROL){ + .startFrame = startFrame, + .endFrame = endFrame, + .invalidate = true, + }; + if (LbqOfferQueueItem(&referenceFrameControlQueue, qfit, &qfit->entry) == LBQ_BOUND_EXCEEDED) { // Too many invalidation tuples, so we need an IDR frame now Limelog("RFI range list reached maximum size limit\n"); free(qfit); @@ -411,7 +415,7 @@ static void queueFrameInvalidationTuple(uint32_t startFrame, uint32_t endFrame) void LiRequestIdrFrame(void) { // Any reference frame invalidation requests should be dropped now. // We require a full IDR frame to recover. - freeBasicLbqList(LbqFlushQueueItems(&invalidReferenceFrameTuples)); + freeBasicLbqList(LbqFlushQueueItems(&referenceFrameControlQueue)); // Request the IDR frame PltSetEvent(&idrFrameRequiredEvent); @@ -423,9 +427,29 @@ void connectionDetectedFrameLoss(uint32_t startFrame, uint32_t endFrame) { } // When we receive a frame, update the number of our current frame -void connectionReceivedCompleteFrame(uint32_t frameIndex) { +// and send ACK control message if the frame is LTR +void connectionReceivedCompleteFrame(uint32_t frameIndex, bool frameIsLTR) { lastGoodFrame = frameIndex; intervalGoodFrameCount++; + + if (frameIsLTR && IS_SUNSHINE() && isReferenceFrameInvalidationEnabled()) { + // Queue LTR frame ACK control message + PQUEUED_REFERENCE_FRAME_CONTROL qfit; + qfit = malloc(sizeof(*qfit)); + if (qfit != NULL) { + *qfit = (QUEUED_REFERENCE_FRAME_CONTROL){ + .startFrame = frameIndex, + .invalidate = false, + }; + if (LbqOfferQueueItem(&referenceFrameControlQueue, qfit, &qfit->entry) == LBQ_BOUND_EXCEEDED) { + // This shouldn't happen and indicates that something has gone wrong with the queue + LC_ASSERT(false); + Limelog("Couldn't queue LTR ACK because the list has reached maximum size limit\n"); + free(qfit); + LiRequestIdrFrame(); + } + } + } } void connectionSendFrameFecStatus(PSS_FRAME_FEC_STATUS fecStatus) { @@ -1512,22 +1536,22 @@ static void requestIdrFrame(void) { } static void requestInvalidateReferenceFrames(uint32_t startFrame, uint32_t endFrame) { - int64_t payload[3]; - LC_ASSERT(startFrame <= endFrame); LC_ASSERT(isReferenceFrameInvalidationEnabled()); - payload[0] = LE64(startFrame); - payload[1] = LE64(endFrame); - payload[2] = 0; + SS_RFI_REQUEST payload = { + .firstFrameIndex = LE32(startFrame), + .lastFrameIndex = LE32(endFrame), + }; // Send the reference frame invalidation request and read the response if (!sendMessageAndDiscardReply(packetTypes[IDX_INVALIDATE_REF_FRAMES], sizeof(payload), - payload, CTRL_CHANNEL_URGENT, + &payload, + CTRL_CHANNEL_URGENT, ENET_PACKET_FLAG_RELIABLE, false)) { - Limelog("Request Invaldiate Reference Frames: Transaction failed: %d\n", (int)LastSocketError()); + Limelog("Request Invalidate Reference Frames: Transaction failed: %d\n", (int)LastSocketError()); ListenerCallbacks.connectionTerminated(LastSocketFail()); return; } @@ -1535,32 +1559,65 @@ static void requestInvalidateReferenceFrames(uint32_t startFrame, uint32_t endFr Limelog("Invalidate reference frame request sent (%d to %d)\n", startFrame, endFrame); } -static void invalidateRefFramesFunc(void* context) { +static void confirmLongtermReferenceFrame(uint32_t frameIndex) { + LC_ASSERT(isReferenceFrameInvalidationEnabled()); + + SS_LTR_FRAME_ACK payload = { + .frameIndex = LE32(frameIndex), + }; + + // Send LTR frame ACK and don't wait for response + if (!sendMessageAndForget(SS_LTR_FRAME_ACK_PTYPE, + sizeof(payload), + &payload, + CTRL_CHANNEL_URGENT, + ENET_PACKET_FLAG_RELIABLE, + false)) { + Limelog("LTR frame ACK: Transaction failed: %d\n", (int)LastSocketError()); + ListenerCallbacks.connectionTerminated(LastSocketFail()); + return; + } +} + +static void referenceFrameControlFunc(void* context) { LC_ASSERT(isReferenceFrameInvalidationEnabled()); while (!PltIsThreadInterrupted(&invalidateRefFramesThread)) { - PQUEUED_FRAME_INVALIDATION_TUPLE qfit; - uint32_t startFrame; - uint32_t endFrame; + PQUEUED_REFERENCE_FRAME_CONTROL qfit; + uint32_t invalidateStartFrame; + uint32_t invalidateEndFrame; + bool invalidate = false; - // Wait for a reference frame invalidation request or a request to shutdown - if (LbqWaitForQueueElement(&invalidReferenceFrameTuples, (void**)&qfit) != LBQ_SUCCESS) { + // Wait for a reference frame control message or a request to shutdown + if (LbqWaitForQueueElement(&referenceFrameControlQueue, (void**)&qfit) != LBQ_SUCCESS) { // Bail if we're stopping return; } - startFrame = qfit->startFrame; - endFrame = qfit->endFrame; - - // Aggregate all lost frames into one range do { - LC_ASSERT(qfit->endFrame >= endFrame); - endFrame = qfit->endFrame; + if (qfit->invalidate) { + if (!invalidate) { + invalidateStartFrame = qfit->startFrame; + invalidateEndFrame = qfit->endFrame; + invalidate = true; + } + else { + // Aggregate all lost frames into one range + LC_ASSERT(qfit->endFrame >= invalidateEndFrame); + invalidateEndFrame = qfit->endFrame; + } + } + else { + // Send LTR frame ACK + confirmLongtermReferenceFrame(qfit->startFrame); + } free(qfit); - } while (LbqPollQueueElement(&invalidReferenceFrameTuples, (void**)&qfit) == LBQ_SUCCESS); + } while (LbqPollQueueElement(&referenceFrameControlQueue, (void**)&qfit) == LBQ_SUCCESS); - // Send the reference frame invalidation request - requestInvalidateReferenceFrames(startFrame, endFrame); + if (invalidate) { + // Send the reference frame invalidation request + requestInvalidateReferenceFrames(invalidateStartFrame, invalidateEndFrame); + } } } @@ -1574,8 +1631,8 @@ static void requestIdrFrameFunc(void* context) { return; } - // Any pending reference frame invalidation requests are now redundant - freeBasicLbqList(LbqFlushQueueItems(&invalidReferenceFrameTuples)); + // Any pending RFI requests and LTR frame ACK messages are now redundant + freeBasicLbqList(LbqFlushQueueItems(&referenceFrameControlQueue)); // Request the IDR frame requestIdrFrame(); @@ -1585,7 +1642,7 @@ static void requestIdrFrameFunc(void* context) { // Stops the control stream int stopControlStream(void) { stopping = true; - LbqSignalQueueShutdown(&invalidReferenceFrameTuples); + LbqSignalQueueShutdown(&referenceFrameControlQueue); LbqSignalQueueShutdown(&frameFecStatusQueue); LbqSignalQueueDrain(&asyncCallbackQueue); PltSetEvent(&idrFrameRequiredEvent); @@ -1972,7 +2029,7 @@ int startControlStream(void) { // Only create the reference frame invalidation thread if RFI is enabled if (isReferenceFrameInvalidationEnabled()) { - err = PltCreateThread("InvRefFrames", invalidateRefFramesFunc, NULL, &invalidateRefFramesThread); + err = PltCreateThread("InvRefFrames", referenceFrameControlFunc, NULL, &invalidateRefFramesThread); if (err != 0) { stopping = true; PltSetEvent(&idrFrameRequiredEvent); diff --git a/src/Limelight-internal.h b/src/Limelight-internal.h index dfd4758..e1314c5 100644 --- a/src/Limelight-internal.h +++ b/src/Limelight-internal.h @@ -55,7 +55,7 @@ extern uint32_t EncryptionFeaturesEnabled; // ENet channel ID values #define CTRL_CHANNEL_GENERIC 0x00 -#define CTRL_CHANNEL_URGENT 0x01 // IDR and reference frame invalidation requests +#define CTRL_CHANNEL_URGENT 0x01 // IDR, LTR ACK and RFI #define CTRL_CHANNEL_KEYBOARD 0x02 #define CTRL_CHANNEL_MOUSE 0x03 #define CTRL_CHANNEL_PEN 0x04 @@ -119,7 +119,7 @@ int startControlStream(void); int stopControlStream(void); void destroyControlStream(void); void connectionDetectedFrameLoss(uint32_t startFrame, uint32_t endFrame); -void connectionReceivedCompleteFrame(uint32_t frameIndex); +void connectionReceivedCompleteFrame(uint32_t frameIndex, bool frameIsLTR); void connectionSawFrame(uint32_t frameIndex); void connectionSendFrameFecStatus(PSS_FRAME_FEC_STATUS fecStatus); int sendInputPacketOnControlStream(unsigned char* data, int length, uint8_t channelId, uint32_t flags, bool moreData); diff --git a/src/RtpVideoQueue.c b/src/RtpVideoQueue.c index c34d2a1..dfbed48 100644 --- a/src/RtpVideoQueue.c +++ b/src/RtpVideoQueue.c @@ -382,9 +382,9 @@ cleanup_packets: // Check all NV_VIDEO_PACKET fields except FEC stuff which differs in the recovered packet LC_ASSERT_VT(nvPacket->flags == droppedNvPacket->flags); + LC_ASSERT_VT(nvPacket->extraFlags == droppedNvPacket->extraFlags); LC_ASSERT_VT(nvPacket->frameIndex == droppedNvPacket->frameIndex); LC_ASSERT_VT(nvPacket->streamPacketIndex == droppedNvPacket->streamPacketIndex); - LC_ASSERT_VT(nvPacket->reserved == droppedNvPacket->reserved); LC_ASSERT_VT(!queue->multiFecCapable || nvPacket->multiFecBlocks == droppedNvPacket->multiFecBlocks); // Check the data itself - use memcmp() and only loop if an error is detected diff --git a/src/Video.h b/src/Video.h index 3df2eae..737d23c 100644 --- a/src/Video.h +++ b/src/Video.h @@ -22,11 +22,13 @@ typedef struct _ENC_VIDEO_HEADER { #define FLAG_EOF 0x2 #define FLAG_SOF 0x4 +#define NV_VIDEO_PACKET_EXTRA_FLAG_LTR_FRAME 0x1 + typedef struct _NV_VIDEO_PACKET { uint32_t streamPacketIndex; uint32_t frameIndex; uint8_t flags; - uint8_t reserved; + uint8_t extraFlags; uint8_t multiFecFlags; uint8_t multiFecBlocks; uint32_t fecInfo; @@ -67,4 +69,20 @@ typedef struct _SS_FRAME_FEC_STATUS { uint8_t multiFecBlockCount; } SS_FRAME_FEC_STATUS, *PSS_FRAME_FEC_STATUS; +// Fields are little-endian +#define SS_LTR_FRAME_ACK_PTYPE 0x0350 +typedef struct _SS_LTR_FRAME_ACK { + uint32_t frameIndex; + uint32_t reserved; +} SS_LTR_FRAME_ACK, *PSS_LTR_FRAME_ACK; + +// Fields are little-endian +#define SS_RFI_REQUEST_PTYPE 0x0301 +typedef struct _SS_RFI_REQUEST { + uint32_t firstFrameIndex; + uint32_t reserved1; + uint32_t lastFrameIndex; + uint32_t reserved2[3]; +} SS_RFI_REQUEST, *PSS_RFI_REQUEST; + #pragma pack(pop) diff --git a/src/VideoDepacketizer.c b/src/VideoDepacketizer.c index 364dcec..43f2df9 100644 --- a/src/VideoDepacketizer.c +++ b/src/VideoDepacketizer.c @@ -466,7 +466,7 @@ static bool isIdrFrameStart(PBUFFER_DESC buffer) { } // Reassemble the frame with the given frame number -static void reassembleFrame(int frameNumber) { +static void reassembleFrame(int frameNumber, bool frameIsLTR) { if (nalChainHead != NULL) { QUEUED_DECODE_UNIT qduDS; PQUEUED_DECODE_UNIT qdu; @@ -539,7 +539,7 @@ static void reassembleFrame(int frameNumber) { } // Notify the control connection - connectionReceivedCompleteFrame(frameNumber); + connectionReceivedCompleteFrame(frameNumber, frameIsLTR); // Clear frame drops consecutiveFrameDrops = 0; @@ -748,6 +748,7 @@ static void processRtpPayload(PNV_VIDEO_PACKET videoPacket, int length, BUFFER_DESC currentPos; uint32_t frameIndex; uint8_t flags; + uint8_t extraFlags; bool firstPacket, lastPacket; uint32_t streamPacketIndex; uint8_t fecCurrentBlockNumber; @@ -765,6 +766,7 @@ static void processRtpPayload(PNV_VIDEO_PACKET videoPacket, int length, fecLastBlockNumber = (videoPacket->multiFecBlocks >> 6) & 0x3; frameIndex = videoPacket->frameIndex; flags = videoPacket->flags; + extraFlags = videoPacket->extraFlags; firstPacket = isFirstPacket(flags, fecCurrentBlockNumber); lastPacket = (flags & FLAG_EOF) && fecCurrentBlockNumber == fecLastBlockNumber; @@ -1119,7 +1121,7 @@ static void processRtpPayload(PNV_VIDEO_PACKET videoPacket, int length, } } - reassembleFrame(frameIndex); + reassembleFrame(frameIndex, extraFlags & NV_VIDEO_PACKET_EXTRA_FLAG_LTR_FRAME); } } From 305993b01322aeb7710a5443960774ecd391c55c Mon Sep 17 00:00:00 2001 From: 7mile Date: Fri, 30 Jan 2026 08:52:06 +0800 Subject: [PATCH 07/10] fix: use UDP connect to probe local address (#121) --- src/Connection.c | 13 +++++++++++++ src/PlatformSockets.c | 28 ++++++++++++++++++++++++++++ src/PlatformSockets.h | 2 ++ src/RtspConnection.c | 12 ------------ 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/Connection.c b/src/Connection.c index d2e8292..fd5776b 100644 --- a/src/Connection.c +++ b/src/Connection.c @@ -377,6 +377,19 @@ int LiStartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION stre ListenerCallbacks.stageFailed(STAGE_NAME_RESOLUTION, err); goto Cleanup; } + + // Resolve LocalAddr by RemoteAddr. + { + SOCKADDR_LEN localAddrLen; + err = getLocalAddressByUdpConnect(&RemoteAddr, AddrLen, &LocalAddr, &localAddrLen); + if (err != 0) { + Limelog("failed to resolve local addr: %d\n", err); + ListenerCallbacks.stageFailed(STAGE_NAME_RESOLUTION, err); + goto Cleanup; + } + LC_ASSERT(localAddrLen == AddrLen); + } + stage++; LC_ASSERT(stage == STAGE_NAME_RESOLUTION); ListenerCallbacks.stageComplete(STAGE_NAME_RESOLUTION); diff --git a/src/PlatformSockets.c b/src/PlatformSockets.c index 56f9b9c..8468b82 100644 --- a/src/PlatformSockets.c +++ b/src/PlatformSockets.c @@ -568,6 +568,34 @@ Exit: return s; } +int getLocalAddressByUdpConnect(const struct sockaddr_storage* targetAddr, SOCKADDR_LEN targetAddrLen, + struct sockaddr_storage* localAddr, SOCKADDR_LEN* localAddrLen) { + SOCKET udpSocket; + + udpSocket = createSocket(targetAddr->ss_family, SOCK_DGRAM, IPPROTO_UDP, false); + if (udpSocket == INVALID_SOCKET) { + return LastSocketError(); + } + + if (connect(udpSocket, (struct sockaddr*)targetAddr, targetAddrLen) < 0) { + int err = LastSocketError(); + Limelog("UDP connect() failed: %d\n", err); + closeSocket(udpSocket); + return err; + } + + *localAddrLen = sizeof(*localAddr); + if (getsockname(udpSocket, (struct sockaddr*)localAddr, localAddrLen) < 0) { + int err = LastSocketError(); + Limelog("getsockname() failed: %d\n", err); + closeSocket(udpSocket); + return err; + } + + closeSocket(udpSocket); + return 0; +} + // See TCP_MAXSEG note in connectTcpSocket() above for more information. // TCP_NODELAY must be enabled on the socket for this function to work! int sendMtuSafe(SOCKET s, char* buffer, int size) { diff --git a/src/PlatformSockets.h b/src/PlatformSockets.h index 78ae4be..930f461 100644 --- a/src/PlatformSockets.h +++ b/src/PlatformSockets.h @@ -107,6 +107,8 @@ void addrToUrlSafeString(struct sockaddr_storage* addr, char* string, size_t str SOCKET createSocket(int addressFamily, int socketType, int protocol, bool nonBlocking); SOCKET connectTcpSocket(struct sockaddr_storage* dstaddr, SOCKADDR_LEN addrlen, unsigned short port, int timeoutSec); +int getLocalAddressByUdpConnect(const struct sockaddr_storage* targetAddr, SOCKADDR_LEN targetAddrLen, + struct sockaddr_storage* localAddr, SOCKADDR_LEN* localAddrLen); int sendMtuSafe(SOCKET s, char* buffer, int size); SOCKET bindUdpSocket(int addressFamily, struct sockaddr_storage* localAddr, SOCKADDR_LEN addrLen, int bufferSize, int socketQosType); int enableNoDelay(SOCKET s); diff --git a/src/RtspConnection.c b/src/RtspConnection.c index d8f1b35..52ce2b1 100644 --- a/src/RtspConnection.c +++ b/src/RtspConnection.c @@ -477,18 +477,6 @@ static bool transactRtspMessageTcp(PRTSP_MESSAGE request, PRTSP_MESSAGE response // Decrypt (if necessary) and deserialize the RTSP response ret = unsealRtspMessage(responseBuffer, offset, response); - // Fetch the local address for this socket if it's not populated yet - if (LocalAddr.ss_family == 0) { - SOCKADDR_LEN addrLen = (SOCKADDR_LEN)sizeof(LocalAddr); - if (getsockname(sock, (struct sockaddr*)&LocalAddr, &addrLen) < 0) { - Limelog("Failed to get local address: %d\n", LastSocketError()); - memset(&LocalAddr, 0, sizeof(LocalAddr)); - } - else { - LC_ASSERT(addrLen == AddrLen); - } - } - Exit: if (serializedMessage != NULL) { free(serializedMessage); From 07c32c80f98bb0d7214c577bd080eea3ce64a856 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 6 Feb 2026 22:53:54 -0600 Subject: [PATCH 08/10] Use a valid port when calling connect() for local address detection BSDs (including Darwin) don't like using port 0. --- src/Connection.c | 2 +- src/PlatformSockets.c | 12 +++++++++--- src/PlatformSockets.h | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Connection.c b/src/Connection.c index fd5776b..bac9a6e 100644 --- a/src/Connection.c +++ b/src/Connection.c @@ -381,7 +381,7 @@ int LiStartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION stre // Resolve LocalAddr by RemoteAddr. { SOCKADDR_LEN localAddrLen; - err = getLocalAddressByUdpConnect(&RemoteAddr, AddrLen, &LocalAddr, &localAddrLen); + err = getLocalAddressByUdpConnect(&RemoteAddr, AddrLen, RtspPortNumber, &LocalAddr, &localAddrLen); if (err != 0) { Limelog("failed to resolve local addr: %d\n", err); ListenerCallbacks.stageFailed(STAGE_NAME_RESOLUTION, err); diff --git a/src/PlatformSockets.c b/src/PlatformSockets.c index 8468b82..41e7dd8 100644 --- a/src/PlatformSockets.c +++ b/src/PlatformSockets.c @@ -568,16 +568,22 @@ Exit: return s; } -int getLocalAddressByUdpConnect(const struct sockaddr_storage* targetAddr, SOCKADDR_LEN targetAddrLen, - struct sockaddr_storage* localAddr, SOCKADDR_LEN* localAddrLen) { +int getLocalAddressByUdpConnect(const struct sockaddr_storage* targetAddr, SOCKADDR_LEN targetAddrLen, unsigned short targetPort, + struct sockaddr_storage* localAddr, SOCKADDR_LEN* localAddrLen) { SOCKET udpSocket; + LC_SOCKADDR connAddr; + + LC_ASSERT(targetPort != 0); udpSocket = createSocket(targetAddr->ss_family, SOCK_DGRAM, IPPROTO_UDP, false); if (udpSocket == INVALID_SOCKET) { return LastSocketError(); } - if (connect(udpSocket, (struct sockaddr*)targetAddr, targetAddrLen) < 0) { + memcpy(&connAddr, targetAddr, targetAddrLen); + SET_PORT(&connAddr, RtspPortNumber); + + if (connect(udpSocket, (struct sockaddr*)&connAddr, targetAddrLen) < 0) { int err = LastSocketError(); Limelog("UDP connect() failed: %d\n", err); closeSocket(udpSocket); diff --git a/src/PlatformSockets.h b/src/PlatformSockets.h index 930f461..db47d83 100644 --- a/src/PlatformSockets.h +++ b/src/PlatformSockets.h @@ -107,8 +107,8 @@ void addrToUrlSafeString(struct sockaddr_storage* addr, char* string, size_t str SOCKET createSocket(int addressFamily, int socketType, int protocol, bool nonBlocking); SOCKET connectTcpSocket(struct sockaddr_storage* dstaddr, SOCKADDR_LEN addrlen, unsigned short port, int timeoutSec); -int getLocalAddressByUdpConnect(const struct sockaddr_storage* targetAddr, SOCKADDR_LEN targetAddrLen, - struct sockaddr_storage* localAddr, SOCKADDR_LEN* localAddrLen); +int getLocalAddressByUdpConnect(const struct sockaddr_storage* targetAddr, SOCKADDR_LEN targetAddrLen, unsigned short targetPort, + struct sockaddr_storage* localAddr, SOCKADDR_LEN* localAddrLen); int sendMtuSafe(SOCKET s, char* buffer, int size); SOCKET bindUdpSocket(int addressFamily, struct sockaddr_storage* localAddr, SOCKADDR_LEN addrLen, int bufferSize, int socketQosType); int enableNoDelay(SOCKET s); From 6250fa29ee87873716045e3b64f1f229374324e8 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 6 Feb 2026 23:24:42 -0600 Subject: [PATCH 09/10] Update ENet for FreeBSD compatibility --- enet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enet b/enet index dea6fb5..78cc9b4 160000 --- a/enet +++ b/enet @@ -1 +1 @@ -Subproject commit dea6fb5414b180908b58c0293c831105b5d124dd +Subproject commit 78cc9b4b03bdd4f95c277391cac6e0951407072b From 611a2e7f8f6583d6d6aad30f0c8a02d6c07ab085 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 00:37:54 -0700 Subject: [PATCH 10/10] Add session input policy control stream support --- src/ControlStream.c | 56 ++++++++++++++++++++++++++++++++++++++++++++- src/FakeCallbacks.c | 5 ++++ src/Limelight.h | 9 ++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/ControlStream.c b/src/ControlStream.c index 2b5182e..fd8e7a7 100644 --- a/src/ControlStream.c +++ b/src/ControlStream.c @@ -83,6 +83,12 @@ typedef struct _QUEUED_ASYNC_CALLBACK { uint8_t left[DS_EFFECT_PAYLOAD_SIZE]; uint8_t right[DS_EFFECT_PAYLOAD_SIZE]; } dsAdaptiveTrigger; + struct { + uint8_t allowKeyboard; + uint8_t allowMouse; + uint8_t allowGamepad; + uint8_t reason; + } setInputPolicy; } data; LINKED_BLOCKING_QUEUE_ENTRY entry; } QUEUED_ASYNC_CALLBACK, *PQUEUED_ASYNC_CALLBACK; @@ -140,6 +146,7 @@ static PPLT_CRYPTO_CONTEXT decryptionCtx; #define IDX_SET_MOTION_EVENT 10 #define IDX_SET_RGB_LED 11 #define IDX_DS_ADAPTIVE_TRIGGERS 12 +#define IDX_SET_INPUT_POLICY 13 #define CONTROL_STREAM_TIMEOUT_SEC 10 #define CONTROL_STREAM_LINGER_TIMEOUT_SEC 2 @@ -157,6 +164,7 @@ static const short packetTypesGen3[] = { -1, // Rumble triggers (unused) -1, // Set motion event (unused) -1, // Set RGB LED (unused) + -1, }; static const short packetTypesGen4[] = { 0x0606, // Request IDR frame @@ -171,6 +179,7 @@ static const short packetTypesGen4[] = { -1, // Rumble triggers (unused) -1, // Set motion event (unused) -1, // Set RGB LED (unused) + -1, }; static const short packetTypesGen5[] = { 0x0305, // Start A @@ -185,6 +194,7 @@ static const short packetTypesGen5[] = { -1, // Rumble triggers (unused) -1, // Set motion event (unused) -1, // Set RGB LED (unused) + -1, }; static const short packetTypesGen7[] = { 0x0305, // Start A @@ -199,6 +209,7 @@ static const short packetTypesGen7[] = { -1, // Rumble triggers (unused) -1, // Set motion event (unused) -1, // Set RGB LED (unused) + -1, }; static const short packetTypesGen7Enc[] = { 0x0302, // Request IDR frame @@ -214,6 +225,7 @@ static const short packetTypesGen7Enc[] = { 0x5501, // Set motion event (Sunshine protocol extension) 0x5502, // Set RGB LED (Sunshine protocol extension) 0x5503, // Set Adaptive Triggers (Sunshine protocol extension) + 0x5504, }; static const char requestIdrFrameGen3[] = { 0, 0 }; @@ -1010,6 +1022,12 @@ static void asyncCallbackThreadFunc(void* context) { queuedCb->data.dsAdaptiveTrigger.left, queuedCb->data.dsAdaptiveTrigger.right); break; + case IDX_SET_INPUT_POLICY: + ListenerCallbacks.setInputPolicy(queuedCb->data.setInputPolicy.allowKeyboard, + queuedCb->data.setInputPolicy.allowMouse, + queuedCb->data.setInputPolicy.allowGamepad, + queuedCb->data.setInputPolicy.reason); + break; default: // Unhandled packet type from queueAsyncCallback() LC_ASSERT(false); @@ -1026,7 +1044,8 @@ static bool needsAsyncCallback(unsigned short packetType) { packetType == packetTypes[IDX_SET_MOTION_EVENT] || packetType == packetTypes[IDX_SET_RGB_LED] || packetType == packetTypes[IDX_HDR_INFO] || - packetType == packetTypes[IDX_DS_ADAPTIVE_TRIGGERS]; + packetType == packetTypes[IDX_DS_ADAPTIVE_TRIGGERS] || + packetType == packetTypes[IDX_SET_INPUT_POLICY]; } static void queueAsyncCallback(PNVCTL_ENET_PACKET_HEADER_V1 ctlHdr, int packetLength) { @@ -1087,6 +1106,13 @@ static void queueAsyncCallback(PNVCTL_ENET_PACKET_HEADER_V1 ctlHdr, int packetLe BbGetBytes(&bb, queuedCb->data.dsAdaptiveTrigger.right, DS_EFFECT_PAYLOAD_SIZE); queuedCb->typeIndex = IDX_DS_ADAPTIVE_TRIGGERS; } + else if (ctlHdr->type == packetTypes[IDX_SET_INPUT_POLICY]) { + BbGet8(&bb, &queuedCb->data.setInputPolicy.allowKeyboard); + BbGet8(&bb, &queuedCb->data.setInputPolicy.allowMouse); + BbGet8(&bb, &queuedCb->data.setInputPolicy.allowGamepad); + BbGet8(&bb, &queuedCb->data.setInputPolicy.reason); + queuedCb->typeIndex = IDX_SET_INPUT_POLICY; + } else { // Unhandled packet type from needsAsyncCallback() LC_ASSERT(false); @@ -1701,6 +1727,34 @@ int sendInputPacketOnControlStream(unsigned char* data, int length, uint8_t chan return 0; } +int LiSendSessionInputPolicy(bool allowKeyboard, bool allowMouse, bool allowGamepad, uint8_t reason) { + struct { + uint8_t allowKeyboard; + uint8_t allowMouse; + uint8_t allowGamepad; + uint8_t reason; + } payload; + + if (!IS_SUNSHINE() || AppVersionQuad[0] < 7) { + return LI_ERR_UNSUPPORTED; + } + + if (client == NULL || peer == NULL || stopping) { + return -2; + } + + payload.allowKeyboard = allowKeyboard ? 1 : 0; + payload.allowMouse = allowMouse ? 1 : 0; + payload.allowGamepad = allowGamepad ? 1 : 0; + payload.reason = reason; + + if (sendMessageAndForget(0x5504, sizeof(payload), &payload, CTRL_CHANNEL_GENERIC, ENET_PACKET_FLAG_RELIABLE, false) == 0) { + return -1; + } + + return 0; +} + // Called by the input stream to flush queued packets before a batching wait void flushInputOnControlStream(void) { if (AppVersionQuad[0] >= 5) { diff --git a/src/FakeCallbacks.c b/src/FakeCallbacks.c index 29fdf18..50095a0 100644 --- a/src/FakeCallbacks.c +++ b/src/FakeCallbacks.c @@ -41,6 +41,7 @@ static void fakeClRumbleTriggers(uint16_t controllerNumber, uint16_t leftTrigger static void fakeClSetMotionEventState(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) {} static void fakeClSetAdaptiveTriggers(uint16_t controllerNumber, uint8_t eventFlags, uint8_t typeLeft, uint8_t typeRight, uint8_t *left, uint8_t *right) {}; static void fakeClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) {} +static void fakeClSetInputPolicy(uint8_t allowKeyboard, uint8_t allowMouse, uint8_t allowGamepad, uint8_t reason) {} static CONNECTION_LISTENER_CALLBACKS fakeClCallbacks = { .stageStarting = fakeClStageStarting, @@ -56,6 +57,7 @@ static CONNECTION_LISTENER_CALLBACKS fakeClCallbacks = { .setMotionEventState = fakeClSetMotionEventState, .setControllerLED = fakeClSetControllerLED, .setAdaptiveTriggers = fakeClSetAdaptiveTriggers, + .setInputPolicy = fakeClSetInputPolicy, }; void fixupMissingCallbacks(PDECODER_RENDERER_CALLBACKS* drCallbacks, PAUDIO_RENDERER_CALLBACKS* arCallbacks, @@ -146,5 +148,8 @@ void fixupMissingCallbacks(PDECODER_RENDERER_CALLBACKS* drCallbacks, PAUDIO_REND if ((*clCallbacks)->setAdaptiveTriggers == NULL) { (*clCallbacks)->setAdaptiveTriggers = fakeClSetAdaptiveTriggers; } + if ((*clCallbacks)->setInputPolicy == NULL) { + (*clCallbacks)->setInputPolicy = fakeClSetInputPolicy; + } } } diff --git a/src/Limelight.h b/src/Limelight.h index ac87a4b..ba4e5c5 100644 --- a/src/Limelight.h +++ b/src/Limelight.h @@ -483,6 +483,8 @@ typedef void(*ConnListenerSetAdaptiveTriggers)(uint16_t controllerNumber, uint8_ // This callback is invoked to set a controller's RGB LED (if present). typedef void(*ConnListenerSetControllerLED)(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); +typedef void(*ConnListenerSetInputPolicy)(uint8_t allowKeyboard, uint8_t allowMouse, uint8_t allowGamepad, uint8_t reason); + typedef struct _CONNECTION_LISTENER_CALLBACKS { ConnListenerStageStarting stageStarting; ConnListenerStageComplete stageComplete; @@ -497,6 +499,7 @@ typedef struct _CONNECTION_LISTENER_CALLBACKS { ConnListenerSetMotionEventState setMotionEventState; ConnListenerSetControllerLED setControllerLED; ConnListenerSetAdaptiveTriggers setAdaptiveTriggers; + ConnListenerSetInputPolicy setInputPolicy; } CONNECTION_LISTENER_CALLBACKS, *PCONNECTION_LISTENER_CALLBACKS; // Use this function to zero the connection callbacks when allocated on the stack or heap @@ -837,6 +840,12 @@ int LiSendHighResScrollEvent(short scrollAmount); int LiSendHScrollEvent(signed char scrollClicks); int LiSendHighResHScrollEvent(short scrollAmount); +#define LI_SESSION_INPUT_POLICY_REASON_STREAM_START 0x00 +#define LI_SESSION_INPUT_POLICY_REASON_USER_TOGGLE 0x01 +#define LI_SESSION_INPUT_POLICY_REASON_HOST_ACK 0x02 +#define LI_SESSION_INPUT_POLICY_REASON_HOST_OVERRIDE 0x03 +int LiSendSessionInputPolicy(bool allowKeyboard, bool allowMouse, bool allowGamepad, uint8_t reason); + // This function returns a time in microseconds with an implementation-defined epoch. // It should only ever be compared with the return value from a previous call to itself. uint64_t LiGetMicroseconds(void);