Skip to content
Open
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
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ if (BUILD_TESTING)
target_compile_options(controller-handoff-queue-tests PRIVATE -UNDEBUG)
endif()
add_test(NAME controller-handoff-queue-tests COMMAND controller-handoff-queue-tests)
# Includes ControlStream.c to drive LiSendResolutionChangeRequest() over loopback ENet.
add_executable(resolution-change-request-tests tests/ResolutionChangeRequestTest.c)
target_include_directories(resolution-change-request-tests PRIVATE
enet/include $<TARGET_PROPERTY:moonlight-common-c,INCLUDE_DIRECTORIES>)
target_compile_definitions(resolution-change-request-tests PRIVATE
$<TARGET_PROPERTY:moonlight-common-c,COMPILE_DEFINITIONS>)
target_compile_options(resolution-change-request-tests PRIVATE
$<TARGET_PROPERTY:moonlight-common-c,COMPILE_OPTIONS>)
target_link_libraries(resolution-change-request-tests PRIVATE moonlight-common-c)
add_test(NAME resolution-change-request-tests COMMAND resolution-change-request-tests)
add_executable(pyrowave-decode-unit-tests tests/PyrowaveDecodeUnitTest.c)
target_include_directories(pyrowave-decode-unit-tests PRIVATE
enet/include $<TARGET_PROPERTY:moonlight-common-c,INCLUDE_DIRECTORIES>)
Expand Down
30 changes: 30 additions & 0 deletions src/ControlStream.c
Original file line number Diff line number Diff line change
Expand Up @@ -2248,6 +2248,36 @@ int LiSendClientSdrWhiteNits(float nits) {
return LI_DYNAMIC_SDR_WHITE_OK;
}

int LiSendResolutionChangeRequest(unsigned int width, unsigned int height) {
uint32_t payload[3];

// Encoders need even dimensions, so round down rather than reject odd sizes.
width &= ~1u;
height &= ~1u;
if (width < 2 || width > 16384 || height < 2 || height > 16384) {
return LI_RESOLUTION_CHANGE_ERR_INVALID;
}
if ((SunshineFeatureFlags & LI_FF_CLIENT_RESOLUTION_CHANGE) == 0 ||
AppVersionQuad[0] < 5 || packetTypes == NULL ||
packetTypes[IDX_DYNAMIC_PARAM_CHANGE] == -1) {
return LI_RESOLUTION_CHANGE_ERR_UNSUPPORTED;
}
if (peer == NULL || peer->state != ENET_PEER_STATE_CONNECTED) {
return LI_RESOLUTION_CHANGE_ERR_NOT_CONNECTED;
}

// Same layout the receive path parses for an inbound 0x5506 RESOLUTION message.
payload[0] = LE32(SS_DYNAMIC_PARAM_TYPE_RESOLUTION);
payload[1] = LE32(width);
payload[2] = LE32(height);
if (!sendMessageAndForget(packetTypes[IDX_DYNAMIC_PARAM_CHANGE], sizeof(payload), payload,
CTRL_CHANNEL_GENERIC, ENET_PACKET_FLAG_RELIABLE, false)) {
return LI_RESOLUTION_CHANGE_ERR_SEND_FAILED;
}

return LI_RESOLUTION_CHANGE_OK;
}

int LiSetCursorMode(int cursorMode) {
uint8_t payload[4] = { CURSOR_STREAM_PROTOCOL_VERSION, 0, 0, 0 };

Expand Down
15 changes: 15 additions & 0 deletions src/Limelight.h
Original file line number Diff line number Diff line change
Expand Up @@ -1327,6 +1327,7 @@ void LiRequestIdrFrame(void);
#define LI_FF_DS5_HAPTICS_PCM 0x80 // Host can stream authored DualSense stereo PCM
#define LI_FF_REMOTE_TEXT_CONTEXT 0x200 // Host can send InputPane/UIA text context updates
#define LI_FF_DYNAMIC_SDR_WHITE 0x100 // Host accepts runtime client SDR reference white updates
#define LI_FF_CLIENT_RESOLUTION_CHANGE 0x400 // LiSendResolutionChangeRequest() supported
uint32_t LiGetHostFeatureFlags(void);

// Update the client's SDR reference white (50-1000 nits inclusive) during an
Expand All @@ -1340,6 +1341,20 @@ uint32_t LiGetHostFeatureFlags(void);
#define LI_DYNAMIC_SDR_WHITE_ERR_SEND_FAILED -4
int LiSendClientSdrWhiteNits(float nits);

// Ask the host to switch the stream to width x height physical pixels (Sunshine
// dynamic parameter change 0x5506, RESOLUTION). This is only supported when the
// host advertises LI_FF_CLIENT_RESOLUTION_CHANGE in LiGetHostFeatureFlags().
// Odd dimensions are rounded down to even. The host validates and applies the
// mode, then reports the resolution it actually applied via the resolutionChanged
// listener callback. Debounce calls (e.g. during a live window resize) to avoid
// flooding the host with mode changes.
#define LI_RESOLUTION_CHANGE_OK 0
#define LI_RESOLUTION_CHANGE_ERR_INVALID -1 // < 2 or > 16384 after rounding
#define LI_RESOLUTION_CHANGE_ERR_UNSUPPORTED -2 // Host lacks LI_FF_CLIENT_RESOLUTION_CHANGE
#define LI_RESOLUTION_CHANGE_ERR_NOT_CONNECTED -3
#define LI_RESOLUTION_CHANGE_ERR_SEND_FAILED -4
int LiSendResolutionChangeRequest(unsigned int width, unsigned int height);

// Select whether Sunshine composites the cursor into the video frame or sends
// cursor shape updates for local rendering. This is only supported when the
// host advertises LI_FF_CURSOR_SHAPE.
Expand Down
127 changes: 127 additions & 0 deletions tests/ResolutionChangeRequestTest.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Drives the production sender over a loopback ENet connection and checks the
// exact bytes that reach the host.
#include "../src/ControlStream.c"

#include <stdio.h>

static int failures;

#define CHECK(condition) \
do { \
if (!(condition)) { \
fprintf(stderr, "%s:%d: check failed: %s\n", __FILE__, __LINE__, #condition); \
failures++; \
} \
} while (0)

static ENetHost* host;

static bool connectLoopback(void) {
struct sockaddr_in loopback = { 0 };
ENetAddress bindAddress, hostAddress;
ENetEvent event;

loopback.sin_family = AF_INET;
loopback.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
enet_address_set_address(&bindAddress, (struct sockaddr*)&loopback, (socklen_t)sizeof(loopback));
host = enet_host_create(AF_INET, &bindAddress, 1, CTRL_CHANNEL_COUNT, 0, 0);
if (host == NULL || enet_socket_get_address(host->socket, &hostAddress) < 0) {
return false;
}

client = enet_host_create(AF_INET, NULL, 1, CTRL_CHANNEL_COUNT, 0, 0);
if (client == NULL) {
return false;
}
peer = enet_host_connect(client, &hostAddress, CTRL_CHANNEL_COUNT, 0);
if (peer == NULL) {
return false;
}

for (int i = 0; i < 400 && peer->state != ENET_PEER_STATE_CONNECTED; i++) {
enet_host_service(client, &event, 5);
enet_host_service(host, &event, 5);
}
return peer->state == ENET_PEER_STATE_CONNECTED;
}

static ENetPacket* receiveOnHost(void) {
ENetEvent event;

for (int i = 0; i < 400; i++) {
enet_host_service(client, NULL, 0);
if (enet_host_service(host, &event, 5) > 0 && event.type == ENET_EVENT_TYPE_RECEIVE) {
return event.packet;
}
}
return NULL;
}

int main(void) {
static const unsigned char expected[] = {
0x06, 0x55, // Packet type 0x5506
0x00, 0x00, 0x00, 0x00, // SS_DYNAMIC_PARAM_TYPE_RESOLUTION
0x80, 0x07, 0x00, 0x00, // 1920
0x38, 0x04, 0x00, 0x00, // 1080
};
ENetPacket* packet;

CHECK(enet_initialize() == 0);

// A Sunshine host new enough to negotiate the Gen7Enc packet table, which
// advertised the feature during the RTSP handshake.
AppVersionQuad[0] = 7;
AppVersionQuad[1] = 1;
AppVersionQuad[2] = 431;
AppVersionQuad[3] = -1;
CHECK(initializeControlStream() == 0);
CHECK(packetTypes == packetTypesGen7Enc);
CHECK(LI_FF_CLIENT_RESOLUTION_CHANGE == 0x400); // Must match the host's bit
SunshineFeatureFlags = LI_FF_CLIENT_RESOLUTION_CHANGE;

// Plaintext framing lets the host side read the payload without a session key.
encryptedControlStream = false;

CHECK(LiSendResolutionChangeRequest(1920, 1080) == LI_RESOLUTION_CHANGE_ERR_NOT_CONNECTED);
CHECK(connectLoopback());

// None of these may reach the wire: the host must see the valid request first.
CHECK(LiSendResolutionChangeRequest(0, 720) == LI_RESOLUTION_CHANGE_ERR_INVALID);
CHECK(LiSendResolutionChangeRequest(1280, 1) == LI_RESOLUTION_CHANGE_ERR_INVALID);
CHECK(LiSendResolutionChangeRequest(16386, 720) == LI_RESOLUTION_CHANGE_ERR_INVALID);
CHECK(LiSendResolutionChangeRequest(1280, 16386) == LI_RESOLUTION_CHANGE_ERR_INVALID);

SunshineFeatureFlags = ~(uint32_t)LI_FF_CLIENT_RESOLUTION_CHANGE;
CHECK(LiSendResolutionChangeRequest(1280, 720) == LI_RESOLUTION_CHANGE_ERR_UNSUPPORTED);
SunshineFeatureFlags = LI_FF_CLIENT_RESOLUTION_CHANGE;

packetTypes = (short*)packetTypesGen7; // No 0x5506 without the encrypted control stream
CHECK(LiSendResolutionChangeRequest(1280, 720) == LI_RESOLUTION_CHANGE_ERR_UNSUPPORTED);
packetTypes = (short*)packetTypesGen7Enc;

// Odd sizes are rounded down to even ones.
CHECK(LiSendResolutionChangeRequest(1921, 1081) == LI_RESOLUTION_CHANGE_OK);
packet = receiveOnHost();
CHECK(packet != NULL);
if (packet != NULL) {
CHECK(packet->dataLength == sizeof(expected));
CHECK(packet->dataLength == sizeof(expected) && memcmp(packet->data, expected, sizeof(expected)) == 0);
enet_packet_destroy(packet);
}

enet_host_destroy(client);
client = NULL;
peer = NULL;
enet_host_destroy(host);
stopping = true;
destroyControlStream();
enet_deinitialize();

if (failures != 0) {
fprintf(stderr, "%d resolution change request checks failed\n", failures);
return 1;
}

puts("Resolution change request tests passed");
return 0;
}