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
3 changes: 3 additions & 0 deletions docs/en/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ Regular Windows builds download an available GUI release bundle. For testing an

## Verify the change

For configuration API mode-list semantics and XML failure handling, see
[saving legacy VDD settings](vdd-settings-persistence.md).

| Change | What to confirm |
| --- | --- |
| Capture or encoding | Actual capture method, encoder, format, and fallback during a real connection; frame rate and latency when relevant |
Expand Down
41 changes: 41 additions & 0 deletions docs/en/vdd-settings-persistence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Saving legacy VDD settings

[Development](development.md) · [简体中文](../zh-CN/vdd-settings-persistence.md)

The configuration API saves advertised resolutions, refresh rates and the GPU
selection in Sunshine's configuration and the existing VDD mode/GPU XML.
Previously an XML failure could be ignored, or a later host-file failure could
leave only the XML updated. Concurrent requests could interleave those writes.

The save now stages XML and writes the host file under the existing configuration
mutex. It reports an XML failure to the caller without committing the host file.
If the host write fails, it restores the original XML bytes before releasing the
mutex. Unrelated XML settings are retained. Missing XML is a no-op: the API still
saves Sunshine settings without creating a driver configuration file.

This is best-effort error recovery, not a crash-safe transaction across two files.
Rollback failures are logged. Inspect the host log if the API reports a save
failure; resolve the file-access problem and retry. The change does not restart
the host, reinstall a driver or recreate a display.

## Mode-list values

For `resolutions` and `fps`, an omitted field, an empty string, JSON null, or the
string `"null"` retains the saved list. If that list is also absent/empty/null on
disk, the host's current advertised runtime list is used. Both configuration
files receive the same effective values. An explicit bracketed empty list,
`"[]"`, clears a list and leaves the driver to use its existing fallback modes.
Later omitted/blank/null values preserve that intentionally empty list.

These list fields remain strings, for example:

```json
{"resolutions":"[1920x1080,3840x2160]","fps":"[60,120]"}
```

This is a field fragment, not a complete API request. `POST /api/config` still
replaces other unprotected settings; send the complete configuration to retain
those fields. Existing protected/client-field rules remain unchanged.

This save-path change has no EDID preference, monitor-creation protocol or signed
driver dependency. It can be reviewed independently of the EDID-native-size option.
2 changes: 2 additions & 0 deletions docs/zh-CN/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ npm run dev

## 如何验证修改

配置 API 的模式列表语义和 XML 失败处理见[保存旧版 VDD 设置](vdd-settings-persistence.md)。

| 修改类型 | 应确认的结果 |
| --- | --- |
| 捕获或编码 | 实际连接的捕获方法、编码器、格式和异常回退;必要时比较帧率与延迟 |
Expand Down
33 changes: 33 additions & 0 deletions docs/zh-CN/vdd-settings-persistence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 保存旧版 VDD 设置

[开发](development.md) · [English](../en/vdd-settings-persistence.md)

配置 API 会将公布的分辨率、刷新率和 GPU 选择保存到 Sunshine 配置和现有 VDD 模式/GPU XML。
以前 XML 写入失败可能被忽略,或后续主机配置写入失败导致只有 XML 更新。并发请求也可能交错写入。

现在保存操作在现有配置互斥锁内暂存 XML 并写入主机配置。
XML 写入失败会返回给调用方,且不会提交主机文件。
主机写入失败时,会在释放锁之前恢复原始 XML 字节。无关 XML 设置会保留。
XML 不存在时不执行 XML 保存:API 仍保存 Sunshine 设置,不创建驱动配置文件。

这是尽力而为的错误恢复,不是可应对崩溃的跨文件事务。回滚失败会记录日志。
API 报告保存失败时,请检查主机日志,解决文件访问问题后重试。
此变更不会重启主机、重装驱动或重新创建显示器。

## 模式列表值

对于 `resolutions` 和 `fps`,省略字段、空字符串、JSON null 或字符串 `"null"` 都会保留已保存的列表。
磁盘上的列表也不存在、为空或 null 时,使用主机当前公布的运行时列表。两个配置文件会保存相同的有效值。
明确提交带方括号的空列表 `"[]"` 会清空列表,由驱动使用现有后备模式。
后续省略、空或 null 值会保留这个明确清空的列表。

列表字段仍为字符串,例如:

```json
{"resolutions":"[1920x1080,3840x2160]","fps":"[60,120]"}
```

这只是字段片段,不是完整 API 请求。`POST /api/config` 仍会替换其他非受保护设置;请提交完整配置以保留这些字段。
现有受保护字段和客户端字段规则保持不变。

此保存路径变更不包含 EDID 首选项、显示器创建协议或签名驱动依赖,可以独立于 EDID 原生尺寸选项审查。
64 changes: 63 additions & 1 deletion src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <functional>
#include <iostream>
#include <iterator>
#include <memory>
#include <mutex>
#include <system_error>
#include <thread>
Expand All @@ -21,6 +22,9 @@
#include <boost/property_tree/ptree.hpp>

#include "config.h"
#ifdef SUNSHINE_TESTS
#include "config_test_support.h"
#endif
#include "entry_handler.h"
#include "file_handler.h"
#include "logging.h"
Expand All @@ -31,6 +35,8 @@
#include "hdr/calibration.h"

#include "display_device/parsed_config.h"
#include "display_device/vdd_mode_lists.h"
#include "display_device/vdd_settings.h"
#include "platform/common.h"

#ifdef _WIN32
Expand All @@ -55,6 +61,10 @@ namespace config {

namespace {
std::mutex config_file_mutex;
#ifdef SUNSHINE_TESTS
std::mutex save_hook_mutex;
std::function<void()> save_lock_contended_hook;
#endif
std::unordered_map<std::string, std::string> command_line_config_overrides;

std::optional<std::string>
Expand Down Expand Up @@ -1977,9 +1987,36 @@ namespace config {
return snapshot;
}

#ifdef SUNSHINE_TESTS
namespace test_support {
/** @brief Install a callback invoked after a full-config update encounters the held save mutex. */
void
set_save_lock_contended_hook(std::function<void()> hook) {
std::lock_guard lock { save_hook_mutex };
save_lock_contended_hook = std::move(hook);
}

} // namespace test_support
#endif

/** Validate, stage related XML, and commit while excluding all configuration writers. */
bool
update_full_config(const std::map<std::string, std::string> &fullConfig) {
update_full_config(const std::map<std::string, std::string> &fullConfig, bool save_vdd) {
#ifdef SUNSHINE_TESTS
// Signal actual contention, not merely that the worker thread was scheduled.
std::unique_lock lock { config_file_mutex, std::defer_lock };
if (!lock.try_lock()) {
std::function<void()> hook;
{
std::lock_guard hook_lock { save_hook_mutex };
hook = save_lock_contended_hook;
}
if (hook) hook();
lock.lock();
}
#else
std::lock_guard lock { config_file_mutex };
#endif
try {
// 不需要保存到配置文件的只读字段(API响应字段,不是配置项)
const std::set<std::string> readonlyFields = {
Expand Down Expand Up @@ -2011,6 +2048,7 @@ namespace config {

// 使用 std::map 保证按字母顺序保存
std::map<std::string, std::string> resultMap;
const auto existing_modes = display_device::vdd_modes::with_mode_list_defaults(originalMap, nvhttp.resolutions, nvhttp.fps);

// 收集传入的配置(跳过只读字段和受保护字段)
for (const auto &[key, value] : fullConfig) {
Expand Down Expand Up @@ -2043,6 +2081,16 @@ namespace config {
}
}

// Omitted/empty/null mode lists retain saved values, using advertised
// runtime defaults when absent from disk. Explicit lists, including [],
// are never replaced.
for (const auto key : { "resolutions", "fps" }) {
resultMap.try_emplace(key, existing_modes.at(key));
}
const auto value = [&](const char *key, const char *fallback) {
const auto found = resultMap.find(key);
return found == resultMap.end() ? std::string { fallback } : found->second;
};
// 添加受保护字段(使用本地配置文件中的原有值)
for (const auto &field : protectedFields) {
auto it = originalMap.find(field);
Expand All @@ -2051,6 +2099,18 @@ namespace config {
}
}

// Stage XML under the same lock as the host write. Roll back before
// releasing the lock so an error cannot overwrite a later successful save.
std::unique_ptr<display_device::vdd_settings::transaction> vdd_update;
if (save_vdd) {
vdd_update = std::make_unique<display_device::vdd_settings::transaction>();
if (vdd_update->save(value("resolutions", "[]"), value("fps", "[]"), value("adapter_name", "")) ==
display_device::vdd_settings::save_result::failed) {
BOOST_LOG(warning) << "Failed to save virtual display settings; Sunshine configuration was not saved";
return false;
}
}

// 对比新配置与原始配置,相同则跳过写入
if (resultMap != originalMap) {
// 按字母顺序写入配置文件
Expand All @@ -2068,6 +2128,8 @@ namespace config {
BOOST_LOG(info) << "Config unchanged, skip writing";
}

if (vdd_update) vdd_update->commit();

if (fullConfig.contains("clients")) sync_clients_config_locked(resultMap);
const auto gamepad = resultMap.find("gamepad");
platf::set_global_gamepad_mode(gamepad == resultMap.end() ? "auto"sv : std::string_view {gamepad->second});
Expand Down
6 changes: 5 additions & 1 deletion src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,12 @@ namespace config {
std::optional<std::map<std::string, std::string>>
get_effective_config_snapshot();

/**
* @brief Persist host configuration, optionally staging legacy VDD modes/GPU under the same lock.
* @param save_vdd Restore original XML bytes if host persistence fails; missing XML is a no-op.
*/
bool
update_full_config(const std::map<std::string, std::string> &fullConfig);
update_full_config(const std::map<std::string, std::string> &fullConfig, bool save_vdd = false);

/**
* Return a synchronized snapshot of the serialized per-client settings.
Expand Down
16 changes: 16 additions & 0 deletions src/config_test_support.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @file src/config_test_support.h
* @brief Test-only controls for configuration-save contention.
*/
#pragma once

#ifdef SUNSHINE_TESTS
#include <functional>

namespace config::test_support {
/** Observe an update only after its attempt to acquire the save mutex failed. */
void
set_save_lock_contended_hook(std::function<void()> hook);

} // namespace config::test_support
#endif
10 changes: 3 additions & 7 deletions src/confighttp/configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
#include "src/confighttp/common.h"
#include "src/display_device/session.h"
#include "src/display_device/to_string.h"
#include "src/display_device/vdd_settings.h"
#include "src/file_handler.h"
#include "src/logging.h"
#include "src/nvenc/frame_budget.h"
Expand Down Expand Up @@ -263,6 +262,7 @@ namespace confighttp::configuration {
}
}

/** @brief Save legacy VDD and host settings under one lock, reporting persistence failures. */
void
saveConfig(resp_https_t response, req_https_t request) {
if (!check_content_type(response, request, "application/json")) return;
Expand Down Expand Up @@ -295,8 +295,6 @@ namespace confighttp::configuration {
if (const auto upnp = inputTree.get_optional<std::string>("upnp"); upnp && *upnp != "enabled" && *upnp != "disabled" && *upnp != "true" && *upnp != "false") {
throw std::invalid_argument("invalid upnp value");
}
std::string resArray = inputTree.get<std::string>("resolutions", "[]");
std::string fpsArray = inputTree.get<std::string>("fps", "[]");
std::string gpu_name = inputTree.get<std::string>("adapter_name", "");

// Validate config field lengths to prevent abuse
Expand All @@ -312,8 +310,6 @@ namespace confighttp::configuration {
return;
}

display_device::vdd_settings::save(resArray, fpsArray, gpu_name);

// 将 inputTree 转换为 std::map(保证有序)
std::map<std::string, std::string> fullConfig;
for (const auto &kv : inputTree) {
Expand All @@ -323,9 +319,9 @@ namespace confighttp::configuration {
}

// 更新配置
if (!config::update_full_config(fullConfig)) {
if (!config::update_full_config(fullConfig, true)) {
outputTree.put("status", "false");
outputTree.put("error", "failed to persist configuration");
outputTree.put("error", "Failed to persist Sunshine or virtual display settings; see host log");
return;
}
}
Expand Down
38 changes: 38 additions & 0 deletions src/display_device/vdd_mode_lists.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#pragma once

#include <map>
#include <string>
#include <string_view>
#include <vector>

namespace display_device::vdd_modes {
/** @brief Treat missing, empty and null mode-list values as omitted; an explicit [] replaces the list. */
inline std::string_view
mode_list_or(const std::map<std::string, std::string> &values, const char *key, std::string_view fallback = "[]") {
const auto found = values.find(key);
return found == values.end() || found->second.empty() || found->second == "null" ? fallback : std::string_view { found->second };
}

/**
* @brief Fill omitted saved mode lists from the currently advertised runtime lists.
* @details Nonempty/non-null values, including explicit empty lists, retain their values.
* Locked persistence uses the same fallback for host and legacy XML lists.
*/
inline std::map<std::string, std::string>
with_mode_list_defaults(std::map<std::string, std::string> saved,
const std::vector<std::string> &runtime_resolutions, const std::vector<std::string> &runtime_fps) {
const auto fill = [&](const char *key, const auto &items) {
if (!mode_list_or(saved, key, "").empty()) return;
std::string list = "[";
for (const auto &item : items) {
if (list.size() > 1) list += ',';
list += item;
}
saved.insert_or_assign(key, list + ']');
};
fill("resolutions", runtime_resolutions);
fill("fps", runtime_fps);
return saved;
}

} // namespace display_device::vdd_modes
Loading
Loading