Skip to content

Commit

Permalink
Improve support for high-resolution stats
Browse files Browse the repository at this point in the history
NOTE: Users of this library may need to make changes. I've also submitted a sister patch for
moonlight-qt that supports these changes and implements a few new features like an audio overlay.

* This patch adds a new microsecond-resolution function call, LiGetMicroseconds(), to complement
the existing LiGetMillis(). Many variables used by stats have been updated to work at this
higher resolution and now provide better results when displaying e.g. sub-millisecond frametime stats.
To try and avoid confusion, variables that now contain microseconds have been renamed with a suffix
of 'Us', and those ending in 'Ms' contain milliseconds. I originally experimented with nanoseconds but it
felt like overkill for our needs.

* Since this library is designed to be mostly standalone, I reorganized Platform.c a bit to make it compatible with
SDL's GetTicks64(), which starts its ticker at program start. A lot of the stats here are used with those in
moonlight-qt so I tried to simplify the functions as much as possible. Each platform now has its own few smaller
functions, instead of trying to fit a complex set of ifdef's inside the same function.

* I added a simple gtest suite for the Platform.c changes, and this test suite should be easy to extend to
other areas of the code.

Internal API:
void Plt_TicksInit(void);          // store initial timestamp
uint64_t Plt_GetTicks64_us(void);  // The most precision, in microseconds
uint64_t Plt_GetTicks64_ms(void);  // Plt_GetTicks64_ms returns the value in milliseconds
uint64_t Plt_GetTicks64(void);     // Alias to _ms() and compatible with SDL_GetTicks64 (ms since program start)
uint64_t PltGetMillis(void);       // replaced by Plt_GetTicks64_ms()

Public API in Limelight.h:
uint64_t LiGetMicroseconds(void);
uint64_t LiGetMillis(void);
PRTP_AUDIO_STATS LiGetRTPAudioStats(void);	// provides access to RTP data for the overlay stats
PRTP_VIDEO_STATS LiGetRTPVideoStats(void);
  • Loading branch information
andygrundman committed Sep 15, 2024
1 parent 8599b60 commit 8795c6c
Show file tree
Hide file tree
Showing 19 changed files with 495 additions and 126 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.vscode/
limelight-common/ARM/
limelight-common/Debug/
Build/
Expand Down
47 changes: 44 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
cmake_minimum_required(VERSION 3.1)
project(moonlight-common-c LANGUAGES C)

string(TOUPPER "x${CMAKE_BUILD_TYPE}" BUILD_TYPE)
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)

option(USE_MBEDTLS "Use MbedTLS instead of OpenSSL" OFF)
Expand All @@ -24,7 +25,13 @@ if (NOT DEFINED BUILD_SHARED_LIBS)
set(BUILD_SHARED_LIBS_OVERRIDE ON)
endif()

add_library(moonlight-common-c ${SRC_LIST})
# XXX There is probably a better way to do this, but build a static library in debug mode on
# Windows, so the tests can link to it. Release gets a DLL.
if((MSVC OR MINGW) AND "${BUILD_TYPE}" STREQUAL "XDEBUG")
add_library(moonlight-common-c STATIC ${SRC_LIST})
else()
add_library(moonlight-common-c ${SRC_LIST})
endif()

if (BUILD_SHARED_LIBS_OVERRIDE)
unset(BUILD_SHARED_LIBS)
Expand Down Expand Up @@ -61,7 +68,6 @@ else()
target_include_directories(moonlight-common-c SYSTEM PRIVATE ${OPENSSL_INCLUDE_DIR})
endif()

string(TOUPPER "x${CMAKE_BUILD_TYPE}" BUILD_TYPE)
if("${BUILD_TYPE}" STREQUAL "XDEBUG")
target_compile_definitions(moonlight-common-c PRIVATE LC_DEBUG)
else()
Expand All @@ -74,10 +80,45 @@ else()
endif()
endif()

if (NOT(MSVC OR APPLE))
include(CheckLibraryExists)
CHECK_LIBRARY_EXISTS(rt clock_gettime "" HAVE_CLOCK_GETTIME)

if (NOT HAVE_CLOCK_GETTIME)
set(CMAKE_EXTRA_INCLUDE_FILES time.h)
CHECK_FUNCTION_EXISTS(clock_gettime HAVE_CLOCK_GETTIME)
SET(CMAKE_EXTRA_INCLUDE_FILES)
endif()

foreach(clock CLOCK_MONOTONIC CLOCK_MONOTONIC_RAW)
message(STATUS "Testing whether ${clock} can be used")
CHECK_CXX_SOURCE_COMPILES(
"#define _POSIX_C_SOURCE 200112L
#include <time.h>
int main ()
{
struct timespec ts[1];
clock_gettime (${clock}, ts);
return 0;
}" HAVE_${clock})
if(HAVE_${clock})
message(STATUS "Testing whether ${clock} can be used -- Success")
else()
message(STATUS "Testing whether ${clock} can be used -- Failed")
endif()
endforeach()

endif()

target_include_directories(moonlight-common-c SYSTEM PUBLIC src)

target_include_directories(moonlight-common-c PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/reedsolomon
)

target_compile_definitions(moonlight-common-c PRIVATE HAS_SOCKLEN_T)
target_compile_definitions(moonlight-common-c PRIVATE HAS_SOCKLEN_T)

if("${BUILD_TYPE}" STREQUAL "XDEBUG")
enable_testing()
add_subdirectory(tests)
endif()
2 changes: 2 additions & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ build_script:
- cmd: 'cmake %CMAKE_ARGS% -DCMAKE_BUILD_TYPE=Debug ..'
- sh: 'cmake --build .'
- cmd: 'cmake --build . --config Debug'
- sh: 'ctest --output-on-failure --repeat until-pass:1000'
- cmd: 'ctest --output-on-failure --repeat until-pass:1000'
- 'cd ..'
- 'mkdir build_release'
- 'cd build_release'
Expand Down
18 changes: 12 additions & 6 deletions src/AudioStream.c
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ static void AudioReceiveThreadProc(void* context) {
}
else if (packet->header.size == 0) {
// Receive timed out; try again

if (!receivedDataFromPeer) {
waitingForAudioMs += UDP_RECV_POLL_TIMEOUT_MS;
}
Expand All @@ -299,7 +299,9 @@ static void AudioReceiveThreadProc(void* context) {
Limelog("Received first audio packet after %d ms\n", waitingForAudioMs);

if (firstReceiveTime != 0) {
packetsToDrop += (uint32_t)(PltGetMillis() - firstReceiveTime) / AudioPacketDuration;
// XXX firstReceiveTime is never set here...
// We're already dropping 500ms of audio so this probably doesn't matter
packetsToDrop += (uint32_t)(Plt_GetTicks64_ms() - firstReceiveTime) / AudioPacketDuration;
}

Limelog("Initial audio resync period: %d milliseconds\n", packetsToDrop * AudioPacketDuration);
Expand Down Expand Up @@ -366,15 +368,15 @@ static void AudioReceiveThreadProc(void* context) {
free(queuedPacket);
}
}

// Break on exit
if (queuedPacket != NULL) {
break;
}
}
}
}

if (packet != NULL) {
free(packet);
}
Expand Down Expand Up @@ -405,12 +407,12 @@ void stopAudioStream(void) {
AudioCallbacks.stop();

PltInterruptThread(&receiveThread);
if ((AudioCallbacks.capabilities & CAPABILITY_DIRECT_SUBMIT) == 0) {
if ((AudioCallbacks.capabilities & CAPABILITY_DIRECT_SUBMIT) == 0) {
// Signal threads waiting on the LBQ
LbqSignalQueueShutdown(&packetQueue);
PltInterruptThread(&decoderThread);
}

PltJoinThread(&receiveThread);
if ((AudioCallbacks.capabilities & CAPABILITY_DIRECT_SUBMIT) == 0) {
PltJoinThread(&decoderThread);
Expand Down Expand Up @@ -474,3 +476,7 @@ int LiGetPendingAudioFrames(void) {
int LiGetPendingAudioDuration(void) {
return LiGetPendingAudioFrames() * AudioPacketDuration;
}

PRTP_AUDIO_STATS LiGetRTPAudioStats(void) {
return &rtpAudioQueue.stats;
}
2 changes: 1 addition & 1 deletion src/ControlStream.c
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ void connectionSendFrameFecStatus(PSS_FRAME_FEC_STATUS fecStatus) {
void connectionSawFrame(uint32_t frameIndex) {
LC_ASSERT_VT(!isBefore16(frameIndex, lastSeenFrame));

uint64_t now = PltGetMillis();
uint64_t now = Plt_GetTicks64_ms();

// Suppress connection status warnings for the first sampling period
// to allow the network and host to settle.
Expand Down
22 changes: 11 additions & 11 deletions src/InputStream.c
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ typedef struct _PACKET_HOLDER {
// Initializes the input stream
int initializeInputStream(void) {
memcpy(currentAesIv, StreamConfig.remoteInputAesIv, sizeof(currentAesIv));

// Set a high maximum queue size limit to ensure input isn't dropped
// while the input send thread is blocked for short periods.
LbqInitializeLinkedBlockingQueue(&packetQueue, MAX_QUEUED_INPUT_PACKETS);
Expand Down Expand Up @@ -129,7 +129,7 @@ int initializeInputStream(void) {
// Destroys and cleans up the input stream
void destroyInputStream(void) {
PLINKED_BLOCKING_QUEUE_ENTRY entry, nextEntry;

PltDestroyCryptoContext(cryptoContext);

entry = LbqDestroyLinkedBlockingQueue(&packetQueue);
Expand Down Expand Up @@ -350,15 +350,15 @@ static void inputSendThreadProc(void* context) {
PPACKET_HOLDER controllerBatchHolder;
PNV_MULTI_CONTROLLER_PACKET origPkt;
short controllerNumber = LE16(holder->packet.multiController.controllerNumber);
uint64_t now = PltGetMillis();
uint64_t now = Plt_GetTicks64_ms();

LC_ASSERT(controllerNumber < MAX_GAMEPADS);

// Delay for batching if required
if (now < lastControllerPacketTime[controllerNumber] + CONTROLLER_BATCHING_INTERVAL_MS) {
flushInputOnControlStream();
PltSleepMs((int)(lastControllerPacketTime[controllerNumber] + CONTROLLER_BATCHING_INTERVAL_MS - now));
now = PltGetMillis();
now = Plt_GetTicks64_ms();
}

origPkt = &holder->packet.multiController;
Expand Down Expand Up @@ -410,13 +410,13 @@ static void inputSendThreadProc(void* context) {
}
// If it's a relative mouse move packet, we can also do batching
else if (holder->packet.header.magic == relMouseMagicLE) {
uint64_t now = PltGetMillis();
uint64_t now = Plt_GetTicks64_ms();

// Delay for batching if required
if (now < lastMousePacketTime + MOUSE_BATCHING_INTERVAL_MS) {
flushInputOnControlStream();
PltSleepMs((int)(lastMousePacketTime + MOUSE_BATCHING_INTERVAL_MS - now));
now = PltGetMillis();
now = Plt_GetTicks64_ms();
}

PltLockMutex(&batchedInputMutex);
Expand Down Expand Up @@ -481,13 +481,13 @@ static void inputSendThreadProc(void* context) {
}
// If it's an absolute mouse move packet, we should only send the latest
else if (holder->packet.header.magic == LE32(MOUSE_MOVE_ABS_MAGIC)) {
uint64_t now = PltGetMillis();
uint64_t now = Plt_GetTicks64_ms();

// Delay for batching if required
if (now < lastMousePacketTime + MOUSE_BATCHING_INTERVAL_MS) {
flushInputOnControlStream();
PltSleepMs((int)(lastMousePacketTime + MOUSE_BATCHING_INTERVAL_MS - now));
now = PltGetMillis();
now = Plt_GetTicks64_ms();
}

PltLockMutex(&batchedInputMutex);
Expand All @@ -513,13 +513,13 @@ static void inputSendThreadProc(void* context) {
}
// If it's a pen packet, we should only send the latest move or hover events
else if (holder->packet.header.magic == LE32(SS_PEN_MAGIC) && TOUCH_EVENT_IS_BATCHABLE(holder->packet.pen.eventType)) {
uint64_t now = PltGetMillis();
uint64_t now = Plt_GetTicks64_ms();

// Delay for batching if required
if (now < lastPenPacketTime + PEN_BATCHING_INTERVAL_MS) {
flushInputOnControlStream();
PltSleepMs((int)(lastPenPacketTime + PEN_BATCHING_INTERVAL_MS - now));
now = PltGetMillis();
now = Plt_GetTicks64_ms();
}

for (;;) {
Expand Down Expand Up @@ -740,7 +740,7 @@ int stopInputStream(void) {
if (inputSock != INVALID_SOCKET) {
shutdownTcpSocket(inputSock);
}

if (inputSock != INVALID_SOCKET) {
closeSocket(inputSock);
inputSock = INVALID_SOCKET;
Expand Down
57 changes: 45 additions & 12 deletions src/Limelight.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ typedef struct _STREAM_CONFIGURATION {
// Specifies the channel configuration of the audio stream.
// See AUDIO_CONFIGURATION constants and MAKE_AUDIO_CONFIGURATION() below.
int audioConfiguration;

// Specifies the mask of supported video formats.
// See VIDEO_FORMAT constants below.
int supportedVideoFormats;
Expand Down Expand Up @@ -154,21 +154,20 @@ typedef struct _DECODE_UNIT {
// (happens when the frame is repeated).
uint16_t frameHostProcessingLatency;

// Receive time of first buffer. This value uses an implementation-defined epoch,
// but the same epoch as enqueueTimeMs and LiGetMillis().
uint64_t receiveTimeMs;
// Receive time of first buffer in microseconds.
uint64_t receiveTimeUs;

// Time the frame was fully assembled and queued for the video decoder to process.
// This is also approximately the same time as the final packet was received, so
// enqueueTimeMs - receiveTimeMs is the time taken to receive the frame. At the
// enqueueTimeUs - receiveTimeUs is the time taken to receive the frame. At the
// time the decode unit is passed to submitDecodeUnit(), the total queue delay
// can be calculated by LiGetMillis() - enqueueTimeMs.
uint64_t enqueueTimeMs;
// can be calculated. This value is in microseconds.
uint64_t enqueueTimeUs;

// Presentation time in milliseconds with the epoch at the first captured frame.
// This can be used to aid frame pacing or to drop old frames that were queued too
// long prior to display.
unsigned int presentationTimeMs;
uint64_t presentationTimeMs;

// Length of the entire buffer chain in bytes
int fullLength;
Expand Down Expand Up @@ -512,10 +511,10 @@ void LiInitializeConnectionCallbacks(PCONNECTION_LISTENER_CALLBACKS clCallbacks)
typedef struct _SERVER_INFORMATION {
// Server host name or IP address in text form
const char* address;

// Text inside 'appversion' tag in /serverinfo
const char* serverInfoAppVersion;

// Text inside 'GfeVersion' tag in /serverinfo (if present)
const char* serverInfoGfeVersion;

Expand Down Expand Up @@ -825,7 +824,11 @@ int LiSendHighResScrollEvent(short scrollAmount);
int LiSendHScrollEvent(signed char scrollClicks);
int LiSendHighResHScrollEvent(short scrollAmount);

// This function returns a time in milliseconds with an implementation-defined epoch.
// This function returns the time in microseconds since the program started running.
uint64_t LiGetMicroseconds(void);

// This function returns the time in milliseconds since the program started running.
// This value will be the same as that returned by SDL_GetTicks64().
uint64_t LiGetMillis(void);

// This is a simplistic STUN function that can assist clients in getting the WAN address
Expand All @@ -848,6 +851,36 @@ int LiGetPendingAudioFrames(void);
// negotiated audio frame duration.
int LiGetPendingAudioDuration(void);

// Returns a pointer to a struct containing various statistics about the RTP audio stream.
// The data should be considered read-only and must not be modified.
typedef struct _RTP_AUDIO_STATS {
uint32_t packetCountAudio; // total audio packets
uint32_t packetCountFec; // total packets of type FEC
uint32_t packetCountFecRecovered; // a packet was saved
uint32_t packetCountFecFailed; // tried to recover but too much was lost
uint32_t packetCountOOS; // out-of-sequence packets
uint32_t packetCountInvalid; // corrupted packets, etc
uint32_t packetCountFecInvalid; // invalid FEC packet
} RTP_AUDIO_STATS, *PRTP_AUDIO_STATS;

PRTP_AUDIO_STATS LiGetRTPAudioStats(void);

// Returns a pointer to a struct containing various statistics about the RTP video stream.
// The data should be considered read-only and must not be modified.
// Right now this is mainly used to track total video and FEC packets, as there are
// many video stats already implemented at a higher level in moonlight-qt.
typedef struct _RTP_VIDEO_STATS {
uint32_t packetCountVideo; // total video packets
uint32_t packetCountFec; // total packets of type FEC
uint32_t packetCountFecRecovered; // a packet was saved
uint32_t packetCountFecFailed; // tried to recover but too much was lost
uint32_t packetCountOOS; // out-of-sequence packets
uint32_t packetCountInvalid; // corrupted packets, etc
uint32_t packetCountFecInvalid; // invalid FEC packet
} RTP_VIDEO_STATS, *PRTP_VIDEO_STATS;

PRTP_VIDEO_STATS LiGetRTPVideoStats(void);

// Port index flags for use with LiGetPortFromPortFlagIndex() and LiGetProtocolFromPortFlagIndex()
#define ML_PORT_INDEX_TCP_47984 0
#define ML_PORT_INDEX_TCP_47989 1
Expand Down Expand Up @@ -875,7 +908,7 @@ int LiGetPendingAudioDuration(void);
unsigned int LiGetPortFlagsFromStage(int stage);
unsigned int LiGetPortFlagsFromTerminationErrorCode(int errorCode);

// Returns the IPPROTO_* value for the specified port index
// Returns the IPPROTO_* value for the specified port index
int LiGetProtocolFromPortFlagIndex(int portFlagIndex);

// Returns the port number for the specified port index
Expand Down
8 changes: 6 additions & 2 deletions src/Misc.c
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ int extractVersionQuadFromString(const char* string, int* quad) {
nextNumber++;
}
}

return 0;
}

Expand Down Expand Up @@ -145,7 +145,11 @@ void LiInitializeServerInformation(PSERVER_INFORMATION serverInfo) {
}

uint64_t LiGetMillis(void) {
return PltGetMillis();
return Plt_GetTicks64_ms();
}

uint64_t LiGetMicroseconds(void) {
return Plt_GetTicks64_us();
}

uint32_t LiGetHostFeatureFlags(void) {
Expand Down
Loading

0 comments on commit 8795c6c

Please sign in to comment.