Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions cmake/compile_definitions/common.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ set(SUNSHINE_TARGET_FILES
"${CMAKE_SOURCE_DIR}/src/network.cpp"
"${CMAKE_SOURCE_DIR}/src/network.h"
"${CMAKE_SOURCE_DIR}/src/move_by_copy.h"
"${CMAKE_SOURCE_DIR}/src/vdd_control.h"
"${CMAKE_SOURCE_DIR}/src/vdd_control.cpp"
"${CMAKE_SOURCE_DIR}/src/system_tray.cpp"
"${CMAKE_SOURCE_DIR}/src/system_tray.h"
"${CMAKE_SOURCE_DIR}/src/task_pool.h"
Expand Down Expand Up @@ -139,6 +141,7 @@ include_directories(
BEFORE
SYSTEM
"${CMAKE_SOURCE_DIR}/third-party"
"${CMAKE_SOURCE_DIR}/third-party/parsec-vdd"
"${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/enet/include"
"${CMAKE_SOURCE_DIR}/third-party/nanors"
"${CMAKE_SOURCE_DIR}/third-party/nanors/deps/obl"
Expand Down
71 changes: 71 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,77 @@ editing the `conf` file in a text editor. Use the examples as reference.
</tr>
</table>

<div class="alert alert-info" role="alert">
<strong>Acknowledgement:</strong> The virtual display feature is powered by the
<a href="https://github.com/nomi-san/parsec-vdd" target="_blank">Parsec Virtual Display Driver</a>
created by <a href="https://github.com/nomi-san" target="_blank">nomi-san</a>.
</div>
Comment thread
fatebugs marked this conversation as resolved.
Outdated

### vdd_enabled

<table>
<tr>
<td>Description</td>
<td colspan="2">Automatically create a virtual display when no physical display is detected.</td>
</tr>
<tr>
<td>Default</td>
<td colspan="2">@code{}disabled@endcode</td>
</tr>
</table>

### vdd_width

<table>
<tr>
<td>Description</td>
<td colspan="2">Width of the virtual display in pixels.</td>
</tr>
<tr>
<td>Default</td>
<td colspan="2">@code{}1920@endcode</td>
</tr>
</table>

### vdd_height

<table>
<tr>
<td>Description</td>
<td colspan="2">Height of the virtual display in pixels.</td>
</tr>
<tr>
<td>Default</td>
<td colspan="2">@code{}1080@endcode</td>
</tr>
</table>

### vdd_refresh_rate

<table>
<tr>
<td>Description</td>
<td colspan="2">Refresh rate of the virtual display in Hz.</td>
</tr>
<tr>
<td>Default</td>
<td colspan="2">@code{}144@endcode</td>
</tr>
</table>

### vdd_display_count

<table>
<tr>
<td>Description</td>
<td colspan="2">Number of virtual displays to restore on startup. This value is set automatically when adding or removing displays via the tray or web UI.</td>
</tr>
<tr>
<td>Default</td>
<td colspan="2">@code{}0@endcode</td>
</tr>
</table>

### max_bitrate

<table>
Expand Down
14 changes: 14 additions & 0 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,14 @@ namespace config {
{} // wa
}, // display_device

{
false, // virtual_display_enabled
1920, // virtual_display_width
1080, // virtual_display_height
144, // virtual_display_refresh_rate
0, // virtual_display_count
Comment thread
fatebugs marked this conversation as resolved.
}, // vdd

0, // max_bitrate
0 // minimum_fps_target (0 = framerate)
};
Expand Down Expand Up @@ -1190,6 +1198,12 @@ namespace config {
}
bool_f(vars, "dd_config_revert_on_disconnect", video.dd.config_revert_on_disconnect);
generic_f(vars, "dd_mode_remapping", video.dd.mode_remapping, dd::mode_remapping_from_view);

bool_f(vars, "vdd_enabled", video.vdd.virtual_display_enabled);
int_f(vars, "vdd_width", video.vdd.virtual_display_width);
int_f(vars, "vdd_height", video.vdd.virtual_display_height);
int_f(vars, "vdd_refresh_rate", video.vdd.virtual_display_refresh_rate);
int_f(vars, "vdd_display_count", video.vdd.virtual_display_count);
{
int value = 0;
int_between_f(vars, "dd_wa_hdr_toggle_delay", value, {0, 3000});
Expand Down
8 changes: 8 additions & 0 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@ namespace config {
workarounds_t wa;
} dd;

struct {
bool virtual_display_enabled; ///< Enable virtual display creation when no display detected
int virtual_display_width; ///< Virtual display width
int virtual_display_height; ///< Virtual display height
int virtual_display_refresh_rate; ///< Virtual display refresh rate
int virtual_display_count; ///< Number of persisted virtual displays to restore on startup
} vdd;

int max_bitrate; // Maximum bitrate, sets ceiling in kbps for bitrate requested from client
double minimum_fps_target; ///< Lowest framerate that will be used when streaming. Range 0-1000, 0 = half of client's requested framerate.
};
Expand Down
220 changes: 220 additions & 0 deletions src/confighttp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
#include "httpcommon.h"
#include "logging.h"
#include "network.h"

#ifdef _WIN32
#include "vdd_control.h"
#endif
#include "nvhttp.h"
#include "platform/common.h"
#include "process.h"
Expand Down Expand Up @@ -1531,6 +1535,215 @@
send_response(response, output_tree);
}

#ifdef _WIN32
/**
* @brief Get the VDD virtual display status.
* @param response The HTTP response object.
* @param request The HTTP request object.
*
* @api_examples{/api/vdd/status| GET| null}
*/
void getVddStatus(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}

print_req(request);

// Auto-initialize if needed to report accurate driver status
if (!vdd::is_initialized()) {
vdd::init();
}

nlohmann::json output_tree;
output_tree["status"] = true;
output_tree["initialized"] = vdd::is_initialized();
output_tree["driver_ok"] = vdd::get_driver_status() == vdd::DriverStatus::OK;
output_tree["driver_version"] = vdd::get_driver_version();

auto displays = vdd::get_displays();
auto display_list = nlohmann::json::array();
for (const auto &d : displays) {
display_list.push_back({
{"index", d.index},
{"identifier", d.identifier},
{"width", d.width},
{"height", d.height},
{"hz", d.hz},
{"device_name", d.device_name}
});
}
output_tree["displays"] = display_list;
output_tree["display_count"] = display_list.size();

send_response(response, output_tree);
}

/**
* @brief Add a virtual display.
* @param response The HTTP response object.
* @param request The HTTP request object.
*
* @api_examples{/api/vdd/add| POST| {"width": 1920, "height": 1080, "hz": 144}}
*/
void addVddDisplay(const resp_https_t &response, const req_https_t &request) {
if (!check_content_type(response, request, "application/json")) {
return;
}
if (!authenticate(response, request)) {
return;
}

print_req(request);

std::string client_id = get_client_id(request);
if (!validate_csrf_token(response, request, client_id)) {
return;
}

nlohmann::json output_tree;

if (!vdd::is_initialized()) {
if (!vdd::init()) {
output_tree["status"] = false;
output_tree["error"] = "VDD driver not available";
send_response(response, output_tree);
return;
}
}

try {
auto input = nlohmann::json::parse(request->content);
int width = input.value("width", 1920);
int height = input.value("height", 1080);
int hz = input.value("hz", 144);

// Validate ranges
if (width < 320 || width > 7680) {
output_tree["status"] = false;
output_tree["error"] = "Width must be between 320 and 7680";
send_response(response, output_tree);
return;
}
if (height < 240 || height > 4320) {
output_tree["status"] = false;
output_tree["error"] = "Height must be between 240 and 4320";
send_response(response, output_tree);
return;
}
if (hz < 30 || hz > 240) {
output_tree["status"] = false;
output_tree["error"] = "Refresh rate must be between 30 and 240";
send_response(response, output_tree);
return;
}

int idx = vdd::add_display(width, height, hz);
output_tree["status"] = idx >= 0;
if (idx >= 0) {
output_tree["success"] = true;
output_tree["index"] = idx;
} else {
output_tree["success"] = false;
output_tree["error"] = "VDD driver returned error (idx=" + std::to_string(idx) + "). Check Sunshine logs for details.";

Check warning on line 1648 in src/confighttp.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use std::format instead of concatenating pieces manually.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ4mZuEGLIw9boGyH7VQ&open=AZ4mZuEGLIw9boGyH7VQ&pullRequest=5114
}
} catch (const std::exception &e) {

Check warning on line 1650 in src/confighttp.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Catch a more specific exception instead of a generic one.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ4mZuEGLIw9boGyH7VP&open=AZ4mZuEGLIw9boGyH7VP&pullRequest=5114
output_tree["status"] = false;
output_tree["error"] = e.what();
}

send_response(response, output_tree);
}

/**
* @brief Remove a virtual display.
* @param response The HTTP response object.
* @param request The HTTP request object.
*
* @api_examples{/api/vdd/remove| POST| {"index": 0}}
*/
void removeVddDisplay(const resp_https_t &response, const req_https_t &request) {
if (!check_content_type(response, request, "application/json")) {
return;
}
if (!authenticate(response, request)) {
return;
}

print_req(request);

std::string client_id = get_client_id(request);
if (!validate_csrf_token(response, request, client_id)) {
return;
}

nlohmann::json output_tree;

// Auto-initialize if needed
if (!vdd::is_initialized()) {
vdd::init();
}

try {
auto input = nlohmann::json::parse(request->content);
int index = input.value("index", -1);

bool result = false;
if (index >= 0) {
result = vdd::remove_display(index);
} else {
result = vdd::remove_last_display();
}

output_tree["status"] = result;
output_tree["success"] = result;
} catch (const std::exception &e) {

Check warning on line 1700 in src/confighttp.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Catch a more specific exception instead of a generic one.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ4mZuEGLIw9boGyH7VR&open=AZ4mZuEGLIw9boGyH7VR&pullRequest=5114
output_tree["status"] = false;
output_tree["error"] = e.what();
}

send_response(response, output_tree);
}

/**
* @brief Remove all virtual displays.
* @param response The HTTP response object.
* @param request The HTTP request object.
*
* @api_examples{/api/vdd/remove-all| POST| null}
*/
void removeAllVddDisplays(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}

print_req(request);

std::string client_id = get_client_id(request);
if (!validate_csrf_token(response, request, client_id)) {
return;
}

nlohmann::json output_tree;

// Auto-initialize if needed
if (!vdd::is_initialized()) {
vdd::init();
}

if (!vdd::is_initialized()) {
output_tree["status"] = false;
output_tree["error"] = "VDD driver not available";
} else {
vdd::remove_all_displays();
output_tree["status"] = true;
output_tree["success"] = true;
}

send_response(response, output_tree);
}
#endif

/**
* @brief Checks whether a directory entry qualifies as an executable file.
* @param entry The directory entry to check.
Expand Down Expand Up @@ -1784,6 +1997,13 @@
server.resource["^/api/vigembus/status$"]["GET"] = getViGEmBusStatus;
server.resource["^/api/vigembus/install$"]["POST"] = installViGEmBus;

#ifdef _WIN32
server.resource["^/api/vdd/status$"]["GET"] = getVddStatus;
server.resource["^/api/vdd/add$"]["POST"] = addVddDisplay;
server.resource["^/api/vdd/remove$"]["POST"] = removeVddDisplay;
server.resource["^/api/vdd/remove-all$"]["POST"] = removeAllVddDisplays;
#endif
Comment thread
fatebugs marked this conversation as resolved.
Outdated

// static/dynamic resources
server.resource["^/images/sunshine.ico$"]["GET"] = getFaviconImage;
server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage;
Expand Down
Loading
Loading