ref:a991a9622cd01f06217d32e1a129b6601b8683e6

docs(doxygen): enforce warn if undocumented (#5337)

SHA: a991a9622cd01f06217d32e1a129b6601b8683e6
Author: Dave Lane <42013603+ReenigneArcher@users.noreply.github.com>
Date: 2026-06-26 02:43
Parents: be7c1bc
141 files changed +11803 -1523
Type
.github/workflows/ci-freebsd.yml +2 −5
@@ -143,11 +143,8 @@
release: ${{ matrix.bsd_release }}
run: |
set -e
uv sync --locked --only-group glad --python "/usr/local/bin/python${PYTHON_VERSION}" \
uv sync --locked --group glad --python "/usr/local/bin/python${PYTHON_VERSION}" \
--no-python-downloads --no-install-project
uv sync --project third-party/lizardbyte-common --locked --only-group test-c \
--python "/usr/local/bin/python${PYTHON_VERSION}" \
--no-python-downloads --no-install-project
# fix git safe.directory issues
git config --global --add safe.directory "*"
@@ -259,7 +256,7 @@
shell: freebsd {0}
run: |
cd "${GITHUB_WORKSPACE}/build"
uv run --project .. --locked --no-sync gcovr . -r ../src \
uv run --project ../third-party/lizardbyte-common --locked --no-sync gcovr . -r ../src \
--exclude-noncode-lines \
--exclude-throw-branches \
--exclude-unreachable-branches \
.github/workflows/ci-linux.yml +2 −2
@@ -175,6 +175,6 @@
- name: Sync Python tools
run: |
uv sync --project third-party/lizardbyte-common --locked --only-group test-c \
uv sync --locked \
--python "${PYTHON_VERSION}" \
--no-python-downloads --no-install-project
@@ -197,7 +197,7 @@
(steps.test.outcome == 'success' || steps.test.outcome == 'failure')
working-directory: build
run: |
uv run --project ../third-party/lizardbyte-common --locked --no-sync gcovr \
uv run --project .. --locked --no-sync gcovr \
--gcov-executable "gcov-${GCC_VERSION}" . -r ../src \
--exclude-noncode-lines \
--exclude-throw-branches \
.github/workflows/ci-macos.yml +2 −2
@@ -100,6 +100,6 @@
- name: Sync Python tools
run: |
uv sync --project third-party/lizardbyte-common --locked --only-group test-c \
uv sync --locked \
--python "${PYTHON_VERSION}" \
--no-python-downloads --no-install-project
@@ -184,7 +184,7 @@
(steps.test.outcome == 'success' || steps.test.outcome == 'failure')
working-directory: build
run: |
uv run --project ../third-party/lizardbyte-common --locked --no-sync gcovr . -r ../src \
uv run --project .. --locked --no-sync gcovr . -r ../src \
--exclude-noncode-lines \
--exclude-throw-branches \
--exclude-unreachable-branches \
.github/workflows/ci-windows.yml +2 −2
@@ -214,6 +214,6 @@
MSYS2_PATH_TYPE: inherit
UV_PYTHON: ${{ steps.setup-python.outputs.python-path }}
run: |
uv sync --project third-party/lizardbyte-common --locked --only-group test-c \
uv sync --locked \
--no-python-downloads \
--no-install-project
@@ -369,7 +369,7 @@
env:
MSYS2_PATH_TYPE: inherit
run: |
uv run --project ../third-party/lizardbyte-common --locked --no-sync gcovr . -r ../src \
uv run --project .. --locked --no-sync gcovr . -r ../src \
--exclude-noncode-lines \
--exclude-throw-branches \
--exclude-unreachable-branches \
.github/workflows/ci.yml +1 −0
@@ -140,6 +140,7 @@
COPR_BETA_WEBHOOK_TOKEN: ${{ secrets.COPR_BETA_WEBHOOK_TOKEN }}
COPR_STABLE_WEBHOOK_TOKEN: ${{ secrets.COPR_STABLE_WEBHOOK_TOKEN }}
COPR_CLI_CONFIG: ${{ secrets.COPR_CLI_CONFIG }}
VIRUSTOTAL_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }}
build-linux-flatpak:
name: Linux Flatpak
.run/docs.run.xml +7 −0
@@ -1,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="docs" type="CMakeRunConfiguration" factoryName="Application" PROGRAM_PARAMS="&quot;file:\\$CMakeCurrentBuildDir$\build\html\index.html&quot;" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="Sunshine" TARGET_NAME="docs" CONFIG_NAME="Release" RUN_PATH="$PROJECT_DIR$/../../../../../Windows/explorer.exe">
<method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method>
</configuration>
</component>
AGENTS.md +24 −1
@@ -8,6 +8,29 @@
The project uses gtest as a test framework.
When adding localization do not update any language other than `en`. This also means to exclude en-US or other variants.
Always add or update doxygen documentation.
The project requires that everything be documented in doxygen or the build will fail.
Primary doxygen comments should be done like so:
```cpp
/**
* @brief Describe the function, structure, etc.
*
* @param my_param Describe the parameter.
* @return Describe the return.
*/
```
Inline doxygen comments should use `///< ...` instead of `/**< ... */`.
Always follow the style guidelines defined in .clang-format for c/c++ code.
When adding localization do not update any language other than `en`. This also means to exclude en-US or other variants.
Do not ever create issues or pull requests.
If asked to create an issue or pull request, do so in their fork instead of the LizardByte GitHub organization.
Never create an issue or pull request in the LizardByte GitHub organization.
Add or update tests for new or modified methods and code. Target 100% coverage on changed code.
docs/contributing.md +10 −10
@@ -128,25 +128,23 @@
```
When testing locally, it may be desirable to manually extract, initialize, update, and compile strings. Python and
uv are required for this, along with the Python dependencies in the `third-party/lizardbyte-common/pyproject.toml`
uv are required for this, along with the Python dependencies in the Sunshine `pyproject.toml`. From the repository
root, install these with the following command.
file. You can install these with the following command.
```bash
uv sync --project third-party/lizardbyte-common --locked --only-group locale --no-install-project
uv sync --locked
```
Additionally, [xgettext](https://www.gnu.org/software/gettext) must be installed.
* Extract, initialize, and update
```bash
uv run --locked --no-sync lb-localize --root-dir . --extract --init --update
uv run --project third-party/lizardbyte-common --locked --no-sync \
python third-party/lizardbyte-common/scripts/localize.py --root-dir . --extract --init --update
```
* Compile
```bash
uv run --project third-party/lizardbyte-common --locked --no-sync \
python third-party/lizardbyte-common/scripts/localize.py --root-dir . --compile
uv run --locked --no-sync lb-localize --root-dir . --compile
```
> [!IMPORTANT]
@@ -160,10 +158,12 @@
#### Clang Format
Source code is tested against the `.clang-format` file for linting errors.
From the repository root, apply clang-format locally with the installed lizardbyte-common script. This will modify
To apply clang-format locally (will modify files):
files in place.
```bash
uv sync --locked
uv run --locked --no-sync lb-update-clang-format
uv run --project third-party/lizardbyte-common --locked --only-group lint-c \
python third-party/lizardbyte-common/scripts/update_clang_format.py
```
#### Unit Testing
docs/Doxyfile +0 −3
@@ -35,9 +35,6 @@
PREDEFINED += SUNSHINE_BUILD_WAYLAND
PREDEFINED += SUNSHINE_TRAY=1
# TODO: Enable this when we have complete documentation
WARN_IF_UNDOCUMENTED = NO
# files and directories to process
USE_MDFILE_AS_MAINPAGE = ../README.md
INPUT = ../README.md \
packaging/sunshine.rb +1 −1
@@ -221,7 +221,7 @@
either install icu4c or use brew install sunshine --with-static-boost instead
EOS
end
ENV.append "CXXFLAGS", "-I#{Formula["icu4c"].opt_include}"
ENV.append "CXXFLAGS", "-I#{formula_opt_include("icu4c")}"
icu4c_lib_path = formula_opt_lib("icu4c").to_s
ENV.append "LDFLAGS", "-L#{icu4c_lib_path}"
ENV["LIBRARY_PATH"] = icu4c_lib_path
pyproject.toml +4 −1
@@ -12,7 +12,9 @@
{name = "LizardByte", email = "lizardbyte@users.noreply.github.com"}
]
dependencies = [
"lizardbyte-common[c]",
]
dependencies = []
[dependency-groups]
# glad2 declares Jinja2>=2.7,<4.0 as a dependency, so installing it pulls in jinja2 transitively.
@@ -41,3 +43,4 @@
flatpak_node_generator = { path = "packaging/linux/flatpak/deps/flatpak-builder-tools/node" }
flatpak_pip_generator = { path = "packaging/linux/flatpak/deps/flatpak-builder-tools/pip" }
glad2 = { path = "third-party/glad" }
lizardbyte-common = { path = "third-party/lizardbyte-common" }
README.md +1 −0
@@ -17,6 +17,7 @@
<a href="https://github.com/LizardByte/Sunshine/actions/workflows/localize.yml?query=branch%3Amaster"><img src="https://img.shields.io/github/actions/workflow/status/lizardbyte/sunshine/localize.yml.svg?branch=master&label=localize%20build&logo=github&style=for-the-badge" alt="GitHub Workflow Status (localize)"></a>
<a href="https://docs.lizardbyte.dev/projects/sunshine"><img src="https://img.shields.io/readthedocs/sunshinestream.svg?label=Docs&style=for-the-badge&logo=readthedocs" alt="Read the Docs"></a>
<a href="https://codecov.io/gh/LizardByte/Sunshine"><img src="https://img.shields.io/codecov/c/gh/LizardByte/Sunshine?token=SMGXQ5NVMJ&style=for-the-badge&logo=codecov&label=codecov" alt="Codecov"></a>
<a href="https://sonarcloud.io/project/overview?id=LizardByte_Sunshine"><img src="https://img.shields.io/sonar/quality_gate/LizardByte_Sunshine?server=https%3A%2F%2Fsonarcloud.io&style=for-the-badge&logo=sonarqubecloud&label=sonarcloud" alt="SonarCloud"></a>
</div>
## ℹ️ About
src/audio.cpp +30 −1
@@ -19,19 +19,35 @@
namespace audio {
using namespace std::literals;
/**
* @brief Owning pointer for an Opus multistream encoder.
*/
using opus_t = util::safe_ptr<OpusMSEncoder, opus_multistream_encoder_destroy>;
/**
* @brief Shared queue carrying captured PCM sample buffers to the encoder thread.
*/
using sample_queue_t = std::shared_ptr<safe::queue_t<std::vector<float>>>;
static int start_audio_control(audio_ctx_t &ctx);
static void stop_audio_control(audio_ctx_t &);
static void apply_surround_params(opus_stream_config_t &stream, const stream_params_t &params);
/**
* @brief Select the Opus stream configuration for a channel count and quality tier.
*
* @param channels Number of audio channels in the stream.
* @param quality Whether the high-quality Opus layout should be selected.
* @return Index into `stream_configs` for the requested layout.
*/
int map_stream(int channels, bool quality);
constexpr auto SAMPLE_RATE = 48000;
constexpr auto SAMPLE_RATE = 48000; ///< Audio sample rate in hertz required by Opus.
// NOTE: If you adjust the bitrates listed here, make sure to update the
// corresponding bitrate adjustment logic in rtsp_stream::cmd_announce()
/**
* @brief Opus stream layouts and bitrates advertised to clients.
*/
opus_stream_config_t stream_configs[MAX_STREAM_CONFIG] {
{
SAMPLE_RATE,
@@ -83,6 +99,13 @@
},
};
/**
* @brief Encode captured PCM samples into Opus packets on the audio worker thread.
*
* @param samples Queue of captured PCM sample buffers to encode.
* @param config Audio stream settings negotiated with the client.
* @param channel_data Platform-specific audio capture context passed to packet metadata.
*/
void encodeThread(sample_queue_t samples, config_t config, void *channel_data) {
auto packets = mail::man->queue<packet_t>(mail::audio_packets);
auto stream = stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])];
@@ -128,6 +151,9 @@
}
}
/**
* @brief Run the capture loop for this backend.
*/
void capture(safe::mail_t mail, config_t config, void *channel_data) {
auto shutdown_event = mail->event<bool>(mail::shutdown);
if (!config::audio.stream) {
@@ -265,6 +291,9 @@
return ctx.control->is_sink_available(sink);
}
/**
* @brief Select the Opus stream configuration for a channel count and quality tier.
*/
int map_stream(int channels, bool quality) {
int shift = quality ? 1 : 0;
switch (channels) {
src/audio.h +54 −19
@@ -12,6 +12,9 @@
#include <bitset>
namespace audio {
/**
* @brief Supported Opus channel layouts advertised to Moonlight clients.
*/
enum stream_config_e : int {
STEREO, ///< Stereo
HIGH_STEREO, ///< High stereo
@@ -22,25 +25,37 @@
MAX_STREAM_CONFIG ///< Maximum audio stream configuration
};
/**
* @brief Static Opus encoder layout for one advertised audio mode.
*/
struct opus_stream_config_t {
std::int32_t sampleRate; ///< Opus sample rate in hertz.
int channelCount; ///< Number of audio channels in the Opus layout.
int streams; ///< Number of Opus streams in the layout.
int coupledStreams; ///< Number of stereo-coupled Opus streams.
std::int32_t sampleRate;
int channelCount;
int streams;
const std::uint8_t *mapping; ///< Channel mapping table passed to the Opus encoder.
int bitrate; ///< Target bitrate in bits per second.
int coupledStreams;
const std::uint8_t *mapping;
int bitrate;
};
/**
* @brief Custom Opus channel layout supplied by configuration.
*/
struct stream_params_t {
int channelCount; ///< Number of audio channels in the Opus layout.
int streams; ///< Number of Opus streams in the custom layout.
int coupledStreams; ///< Number of stereo-coupled Opus streams.
int channelCount;
int streams;
int coupledStreams;
std::uint8_t mapping[8];
std::uint8_t mapping[8]; ///< Channel mapping table for up to eight speakers.
};
extern opus_stream_config_t stream_configs[MAX_STREAM_CONFIG];
/**
* @brief Audio capture and encoder settings for a stream.
*/
struct config_t {
/**
* @brief Boolean audio feature flags.
*/
enum flags_e : int {
HIGH_QUALITY, ///< High quality audio
HOST_AUDIO, ///< Host audio
@@ -49,29 +64,48 @@
MAX_FLAGS ///< Maximum number of flags
};
int packetDuration; ///< Packet duration in milliseconds requested by the client.
int channels; ///< Number of audio channels requested by the client.
int packetDuration;
int channels;
int mask; ///< Speaker mask describing the requested channel layout.
int mask;
stream_params_t customStreamParams; ///< Custom Opus layout used when CUSTOM_SURROUND_PARAMS is enabled.
stream_params_t customStreamParams;
std::bitset<MAX_FLAGS> flags;
std::bitset<MAX_FLAGS> flags; ///< Enabled audio feature flags.
};
/**
* @brief Shared audio device state used while streams are active.
*/
struct audio_ctx_t {
// We want to change the sink for the first stream only
std::unique_ptr<std::atomic_bool> sink_flag; ///< Tracks whether the capture sink was already switched.
std::unique_ptr<std::atomic_bool> sink_flag;
std::unique_ptr<platf::audio_control_t> control; ///< Platform audio-control implementation.
std::unique_ptr<platf::audio_control_t> control;
bool restore_sink;
platf::sink_t sink;
bool restore_sink; ///< Whether Sunshine should restore the original sink when capture ends.
platf::sink_t sink; ///< Original sink captured before Sunshine switched devices.
};
/**
* @brief Byte buffer used for encoded audio packet payloads.
*/
using buffer_t = util::buffer_t<std::uint8_t>;
/**
* @brief Encoded audio packet paired with platform channel metadata.
*/
using packet_t = std::pair<void *, buffer_t>;
/**
* @brief Shared mailbox reference to the global audio context.
*/
using audio_ctx_ref_t = safe::shared_t<audio_ctx_t>::ptr_t;
/**
* @brief Capture, encode, and publish audio packets for a stream.
*
* @param mail Mailbox used to publish encoded audio packets.
* @param config Audio capture and encoder settings.
* @param channel_data Platform-specific capture channel pointer.
*/
void capture(safe::mail_t mail, config_t config, void *channel_data);
/**
@@ -99,6 +133,7 @@
* }
* return false;
* @examples_end
* @param ctx Native context object used by the operation or callback.
*/
bool is_audio_ctx_sink_available(const audio_ctx_t &ctx);
} // namespace audio
src/cbs.cpp +50 −3
@@ -19,13 +19,29 @@
using namespace std::literals;
namespace cbs {
/**
* @brief Release an FFmpeg coded bitstream context.
*
* @param c Context pointer owned by the safe pointer wrapper.
*/
void close(CodedBitstreamContext *c) {
ff_cbs_close(&c);
}
/**
* @brief Owning coded bitstream context pointer that calls ff_cbs_close.
*/
using ctx_t = util::safe_ptr<CodedBitstreamContext, close>;
/**
* @brief Owns an FFmpeg coded bitstream fragment and frees fragment buffers on destruction.
*/
class frag_t: public CodedBitstreamFragment {
public:
/**
* @brief Move-construct a coded bitstream fragment and transfer FFmpeg-owned buffers.
*
* @param o Fragment whose allocated buffers are transferred to this instance.
*/
frag_t(frag_t &&o) {
std::copy((std::uint8_t *) &o, (std::uint8_t *) (&o + 1), (std::uint8_t *) this);
@@ -38,5 +54,11 @@
std::fill_n((std::uint8_t *) this, sizeof(*this), 0);
}
/**
* @brief Move-assign a coded bitstream fragment and transfer FFmpeg-owned buffers.
*
* @param o Fragment whose allocated buffers are transferred to this instance.
* @return Reference to this fragment.
*/
frag_t &operator=(frag_t &&o) {
std::copy((std::uint8_t *) &o, (std::uint8_t *) (&o + 1), (std::uint8_t *) this);
@@ -54,6 +76,15 @@
}
};
/**
* @brief Serialize a prepared coded bitstream unit with an existing context.
*
* @param cbs_ctx Coded bitstream context initialized for the codec.
* @param nal NAL unit type to insert into the fragment.
* @param uh Pointer to the FFmpeg raw unit header/content structure.
* @param codec_id FFmpeg codec identifier used by the context.
* @return Serialized NAL unit bytes, or an empty buffer if FFmpeg rejects the fragment.
*/
util::buffer_t<std::uint8_t> write(cbs::ctx_t &cbs_ctx, std::uint8_t nal, void *uh, AVCodecID codec_id) {
cbs::frag_t frag;
auto err = ff_cbs_insert_unit_content(&frag, -1, nal, uh, nullptr);
@@ -79,6 +110,14 @@
return data;
}
/**
* @brief Serialize a prepared coded bitstream unit with a temporary context.
*
* @param nal NAL unit type to insert into the fragment.
* @param uh Pointer to the FFmpeg raw unit header/content structure.
* @param codec_id FFmpeg codec identifier used to initialize the temporary context.
* @return Serialized NAL unit bytes, or an empty buffer if FFmpeg rejects the fragment.
*/
util::buffer_t<std::uint8_t> write(std::uint8_t nal, void *uh, AVCodecID codec_id) {
cbs::ctx_t cbs_ctx;
ff_cbs_init(&cbs_ctx, codec_id, nullptr);
@@ -86,6 +125,9 @@
return write(cbs_ctx, nal, uh, codec_id);
}
/**
* @brief Build replacement H.264 SPS bytes with Sunshine-required VUI fields.
*/
h264_t make_sps_h264(const AVCodecContext *avctx, const AVPacket *packet) {
cbs::ctx_t ctx;
if (ff_cbs_init(&ctx, AV_CODEC_ID_H264, nullptr)) {
@@ -142,6 +184,9 @@
};
}
/**
* @brief Build replacement HEVC VPS/SPS bytes with Sunshine-required VUI fields.
*/
hevc_t make_sps_hevc(const AVCodecContext *avctx, const AVPacket *packet) {
cbs::ctx_t ctx;
if (ff_cbs_init(&ctx, AV_CODEC_ID_H265, nullptr)) {
@@ -215,9 +260,11 @@
}
/**
* @brief Check whether an encoded H.264 or HEVC packet contains active SPS VUI metadata.
*
* This function initializes a Coded Bitstream Context and reads the packet into a Coded Bitstream Fragment.
* It then checks if the SPS->VUI (Video Usability Information) is present in the active SPS of the packet.
* This is done for both H264 and H265 codecs.
* @param packet Encoded packet to parse with FFmpeg's coded bitstream reader.
* @param codec_id FFmpeg codec identifier; expected to be AV_CODEC_ID_H264 or AV_CODEC_ID_H265.
* @return `true` when the packet's active SPS advertises VUI parameters.
*/
bool validate_sps(const AVPacket *packet, int codec_id) {
cbs::ctx_t ctx;
src/cbs.h +33 −9
@@ -12,28 +12,52 @@
namespace cbs {
/**
* @brief Original and rewritten bytes for one codec NAL unit.
*/
struct nal_t {
util::buffer_t<std::uint8_t> _new;
util::buffer_t<std::uint8_t> _new; ///< Replacement bytes generated by Sunshine.
util::buffer_t<std::uint8_t> old; ///< Original bytes copied from the source packet.
util::buffer_t<std::uint8_t> old;
};
/**
* @brief HEVC parameter sets before and after VUI rewriting.
*/
struct hevc_t {
nal_t vps; ///< Video parameter set bytes.
nal_t vps;
nal_t sps;
nal_t sps; ///< Sequence parameter set bytes.
};
/**
* @brief H.264 parameter set before and after VUI rewriting.
*/
struct h264_t {
nal_t sps; ///< Sequence parameter set bytes.
nal_t sps;
};
/**
* @brief Build HEVC replacement parameter sets for a packet.
*
* @param ctx FFmpeg codec context that provides colorimetry and reference-frame metadata.
* @param packet Encoded packet containing the original VPS and SPS.
* @return Original and rewritten VPS/SPS bytes.
*/
hevc_t make_sps_hevc(const AVCodecContext *ctx, const AVPacket *packet);
/**
* @brief Build an H.264 replacement SPS for a packet.
*
* @param ctx FFmpeg codec context that provides colorimetry and reference-frame metadata.
* @param packet Encoded packet containing the original SPS.
* @return Original and rewritten SPS bytes.
*/
h264_t make_sps_h264(const AVCodecContext *ctx, const AVPacket *packet);
/**
* @brief Validates the Sequence Parameter Set (SPS) of a given packet.
* @param packet The packet to validate.
* @param codec_id The ID of the codec used (either AV_CODEC_ID_H264 or AV_CODEC_ID_H265).
* @return True if the SPS->VUI is present in the active SPS of the packet, false otherwise.
* @brief Check whether an encoded H.264 or HEVC packet contains active SPS VUI metadata.
*
* @param packet Encoded packet to parse with FFmpeg's coded bitstream reader.
* @param codec_id FFmpeg codec identifier; expected to be AV_CODEC_ID_H264 or AV_CODEC_ID_H265.
* @return `true` when the packet's active SPS advertises VUI parameters.
*/
bool validate_sps(const AVPacket *packet, int codec_id);
} // namespace cbs
src/config.cpp +461 −73
@@ -41,15 +41,21 @@
namespace fs = std::filesystem;
using namespace std::literals;
constexpr auto CA_DIR = "credentials";
constexpr auto CA_DIR = "credentials"; ///< Subdirectory under app data that stores Sunshine credentials.
const std::string PRIVATE_KEY_FILE = std::string(CA_DIR) + "/cakey.pem"; ///< Relative path to the persisted private key PEM file.
const std::string CERTIFICATE_FILE = std::string(CA_DIR) + "/cacert.pem"; ///< Relative path to the persisted certificate PEM file.
const std::string APPS_JSON_PATH = platf::appdata().string() + "/apps.json"; ///< Default path to the applications JSON file.
const std::string PRIVATE_KEY_FILE = std::string(CA_DIR) + "/cakey.pem";
const std::string CERTIFICATE_FILE = std::string(CA_DIR) + "/cacert.pem";
const std::string APPS_JSON_PATH = platf::appdata().string() + "/apps.json";
namespace config {
namespace nv {
/**
* @brief Parse the `nvenc_twopass` configuration value.
*
* @param preset Encoder preset value supplied by the configuration.
* @return Parsed enum value, or the setting-specific default when the text is unknown.
*/
nvenc::nvenc_two_pass twopass_from_view(const std::string_view &preset) {
if (preset == "disabled") {
return nvenc::nvenc_two_pass::disabled;
@@ -64,6 +70,12 @@
return nvenc::nvenc_two_pass::quarter_resolution;
}
/**
* @brief Parse the `nvenc_split_encode` configuration value.
*
* @param preset Encoder preset value supplied by the configuration.
* @return Parsed enum value, or the setting-specific default when the text is unknown.
*/
nvenc::nvenc_split_frame_encoding split_encode_from_view(const std::string_view &preset) {
using enum nvenc::nvenc_split_frame_encoding;
if (preset == "disabled") {
@@ -84,45 +96,45 @@
namespace amd {
#if !defined(_WIN32) || defined(DOXYGEN)
// values accurate as of 27/12/2022, but aren't strictly necessary for MacOS build
constexpr int AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_SPEED = 100; ///< Fallback AMF enum value for av1 quality preset speed.
constexpr int AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_QUALITY = 30; ///< Fallback AMF enum value for av1 quality preset quality.
constexpr int AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_BALANCED = 70; ///< Fallback AMF enum value for av1 quality preset balanced.
constexpr int AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_SPEED = 10; ///< Fallback AMF enum value for hevc quality preset speed.
constexpr int AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_QUALITY = 0; ///< Fallback AMF enum value for hevc quality preset quality.
constexpr int AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_BALANCED = 5; ///< Fallback AMF enum value for hevc quality preset balanced.
constexpr int AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_SPEED = 100;
constexpr int AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_QUALITY = 30;
constexpr int AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_BALANCED = 70;
constexpr int AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_SPEED = 10;
constexpr int AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_QUALITY = 0;
constexpr int AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_BALANCED = 5;
constexpr int AMF_VIDEO_ENCODER_QUALITY_PRESET_SPEED = 1; ///< Fallback AMF enum value for quality preset speed.
constexpr int AMF_VIDEO_ENCODER_QUALITY_PRESET_QUALITY = 2; ///< Fallback AMF enum value for quality preset quality.
constexpr int AMF_VIDEO_ENCODER_QUALITY_PRESET_SPEED = 1;
constexpr int AMF_VIDEO_ENCODER_QUALITY_PRESET_QUALITY = 2;
constexpr int AMF_VIDEO_ENCODER_QUALITY_PRESET_BALANCED = 0;
constexpr int AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CONSTANT_QP = 0;
constexpr int AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CBR = 3;
constexpr int AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR = 2;
constexpr int AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR = 1;
constexpr int AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CONSTANT_QP = 0;
constexpr int AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CBR = 3;
constexpr int AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR = 2;
constexpr int AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR = 1;
constexpr int AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CONSTANT_QP = 0;
constexpr int AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR = 1;
constexpr int AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR = 2;
constexpr int AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR = 3;
constexpr int AMF_VIDEO_ENCODER_AV1_USAGE_TRANSCODING = 0;
constexpr int AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY = 1;
constexpr int AMF_VIDEO_ENCODER_AV1_USAGE_ULTRA_LOW_LATENCY = 2;
constexpr int AMF_VIDEO_ENCODER_AV1_USAGE_WEBCAM = 3;
constexpr int AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY_HIGH_QUALITY = 5;
constexpr int AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCODING = 0;
constexpr int AMF_VIDEO_ENCODER_HEVC_USAGE_ULTRA_LOW_LATENCY = 1;
constexpr int AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY = 2;
constexpr int AMF_VIDEO_ENCODER_HEVC_USAGE_WEBCAM = 3;
constexpr int AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY_HIGH_QUALITY = 5;
constexpr int AMF_VIDEO_ENCODER_USAGE_TRANSCODING = 0;
constexpr int AMF_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY = 1;
constexpr int AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY = 2;
constexpr int AMF_VIDEO_ENCODER_USAGE_WEBCAM = 3;
constexpr int AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY_HIGH_QUALITY = 5;
constexpr int AMF_VIDEO_ENCODER_UNDEFINED = 0;
constexpr int AMF_VIDEO_ENCODER_CABAC = 1;
constexpr int AMF_VIDEO_ENCODER_CALV = 2;
constexpr int AMF_VIDEO_ENCODER_QUALITY_PRESET_BALANCED = 0; ///< Fallback AMF enum value for quality preset balanced.
constexpr int AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CONSTANT_QP = 0; ///< Fallback AMF enum value for av1 rate control method constant qp.
constexpr int AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CBR = 3; ///< Fallback AMF enum value for av1 rate control method cbr.
constexpr int AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR = 2; ///< Fallback AMF enum value for av1 rate control method peak constrained vbr.
constexpr int AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR = 1; ///< Fallback AMF enum value for av1 rate control method latency constrained vbr.
constexpr int AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CONSTANT_QP = 0; ///< Fallback AMF enum value for hevc rate control method constant qp.
constexpr int AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CBR = 3; ///< Fallback AMF enum value for hevc rate control method cbr.
constexpr int AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR = 2; ///< Fallback AMF enum value for hevc rate control method peak constrained vbr.
constexpr int AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR = 1; ///< Fallback AMF enum value for hevc rate control method latency constrained vbr.
constexpr int AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CONSTANT_QP = 0; ///< Fallback AMF enum value for rate control method constant qp.
constexpr int AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR = 1; ///< Fallback AMF enum value for rate control method cbr.
constexpr int AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR = 2; ///< Fallback AMF enum value for rate control method peak constrained vbr.
constexpr int AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR = 3; ///< Fallback AMF enum value for rate control method latency constrained vbr.
constexpr int AMF_VIDEO_ENCODER_AV1_USAGE_TRANSCODING = 0; ///< Fallback AMF enum value for av1 usage transcoding.
constexpr int AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY = 1; ///< Fallback AMF enum value for av1 usage low latency.
constexpr int AMF_VIDEO_ENCODER_AV1_USAGE_ULTRA_LOW_LATENCY = 2; ///< Fallback AMF enum value for av1 usage ultra low latency.
constexpr int AMF_VIDEO_ENCODER_AV1_USAGE_WEBCAM = 3; ///< Fallback AMF enum value for av1 usage webcam.
constexpr int AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY_HIGH_QUALITY = 5; ///< Fallback AMF enum value for av1 usage low latency high quality.
constexpr int AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCODING = 0; ///< Fallback AMF enum value for hevc usage transcoding.
constexpr int AMF_VIDEO_ENCODER_HEVC_USAGE_ULTRA_LOW_LATENCY = 1; ///< Fallback AMF enum value for hevc usage ultra low latency.
constexpr int AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY = 2; ///< Fallback AMF enum value for hevc usage low latency.
constexpr int AMF_VIDEO_ENCODER_HEVC_USAGE_WEBCAM = 3; ///< Fallback AMF enum value for hevc usage webcam.
constexpr int AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY_HIGH_QUALITY = 5; ///< Fallback AMF enum value for hevc usage low latency high quality.
constexpr int AMF_VIDEO_ENCODER_USAGE_TRANSCODING = 0; ///< Fallback AMF enum value for usage transcoding.
constexpr int AMF_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY = 1; ///< Fallback AMF enum value for usage ultra low latency.
constexpr int AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY = 2; ///< Fallback AMF enum value for usage low latency.
constexpr int AMF_VIDEO_ENCODER_USAGE_WEBCAM = 3; ///< Fallback AMF enum value for usage webcam.
constexpr int AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY_HIGH_QUALITY = 5; ///< Fallback AMF enum value for usage low latency high quality.
constexpr int AMF_VIDEO_ENCODER_UNDEFINED = 0; ///< Fallback AMF enum value for undefined.
constexpr int AMF_VIDEO_ENCODER_CABAC = 1; ///< Fallback AMF enum value for cabac.
constexpr int AMF_VIDEO_ENCODER_CALV = 2; ///< Fallback AMF enum value for calv.
#else
#ifdef _GLIBCXX_USE_C99_INTTYPES
#undef _GLIBCXX_USE_C99_INTTYPES
@@ -132,24 +144,36 @@
#include <AMF/components/VideoEncoderVCE.h>
#endif
/**
* @brief Enumerates supported quality AV1 options.
*/
enum class quality_av1_e : int {
speed = AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_SPEED, ///< Speed preset
quality = AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_QUALITY, ///< Quality preset
balanced = AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_BALANCED ///< Balanced preset
};
/**
* @brief Enumerates supported quality HEVC options.
*/
enum class quality_hevc_e : int {
speed = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_SPEED, ///< Speed preset
quality = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_QUALITY, ///< Quality preset
balanced = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_BALANCED ///< Balanced preset
};
/**
* @brief Enumerates supported quality h264 options.
*/
enum class quality_h264_e : int {
speed = AMF_VIDEO_ENCODER_QUALITY_PRESET_SPEED, ///< Speed preset
quality = AMF_VIDEO_ENCODER_QUALITY_PRESET_QUALITY, ///< Quality preset
balanced = AMF_VIDEO_ENCODER_QUALITY_PRESET_BALANCED ///< Balanced preset
};
/**
* @brief Enumerates supported rc AV1 options.
*/
enum class rc_av1_e : int {
cbr = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CBR, ///< CBR
cqp = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CONSTANT_QP, ///< CQP
@@ -157,6 +181,9 @@
vbr_peak = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR ///< VBR with peak constraints
};
/**
* @brief Enumerates supported rc HEVC options.
*/
enum class rc_hevc_e : int {
cbr = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CBR, ///< CBR
cqp = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CONSTANT_QP, ///< CQP
@@ -164,6 +191,9 @@
vbr_peak = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR ///< VBR with peak constraints
};
/**
* @brief Enumerates supported rc h264 options.
*/
enum class rc_h264_e : int {
cbr = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR, ///< CBR
cqp = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CONSTANT_QP, ///< CQP
@@ -171,6 +201,9 @@
vbr_peak = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR ///< VBR with peak constraints
};
/**
* @brief Enumerates supported usage AV1 options.
*/
enum class usage_av1_e : int {
transcoding = AMF_VIDEO_ENCODER_AV1_USAGE_TRANSCODING, ///< Transcoding preset
webcam = AMF_VIDEO_ENCODER_AV1_USAGE_WEBCAM, ///< Webcam preset
@@ -179,6 +212,9 @@
ultralowlatency = AMF_VIDEO_ENCODER_AV1_USAGE_ULTRA_LOW_LATENCY ///< Ultra low latency preset
};
/**
* @brief Enumerates supported usage HEVC options.
*/
enum class usage_hevc_e : int {
transcoding = AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCODING, ///< Transcoding preset
webcam = AMF_VIDEO_ENCODER_HEVC_USAGE_WEBCAM, ///< Webcam preset
@@ -187,6 +223,9 @@
ultralowlatency = AMF_VIDEO_ENCODER_HEVC_USAGE_ULTRA_LOW_LATENCY ///< Ultra low latency preset
};
/**
* @brief Enumerates supported usage h264 options.
*/
enum class usage_h264_e : int {
transcoding = AMF_VIDEO_ENCODER_USAGE_TRANSCODING, ///< Transcoding preset
webcam = AMF_VIDEO_ENCODER_USAGE_WEBCAM, ///< Webcam preset
@@ -195,17 +234,29 @@
ultralowlatency = AMF_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY ///< Ultra low latency preset
};
/**
* @brief Enumerates supported coder options.
*/
enum coder_e : int {
_auto = AMF_VIDEO_ENCODER_UNDEFINED, ///< Auto
cabac = AMF_VIDEO_ENCODER_CABAC, ///< CABAC
cavlc = AMF_VIDEO_ENCODER_CALV ///< CAVLC
};
/**
* @brief Parse an AMD quality preset while preserving the current value on invalid input.
*
* @param quality_type Configuration text naming the AMD quality preset.
* @param original Original text value used when reporting a parsing failure.
* @return Parsed enum value, or the setting-specific default when the text is unknown.
*/
template<class T>
::std::optional<int> quality_from_view(const ::std::string_view &quality_type, const ::std::optional<int>(&original)) {
#ifndef DOXYGEN
#define _CONVERT_(x) \
if (quality_type == #x##sv) \
return (int) T::x
#define _CONVERT_(x) \
if (quality_type == #x##sv) \
return (int) T::x
#endif
_CONVERT_(balanced);
_CONVERT_(quality);
_CONVERT_(speed);
@@ -213,11 +264,20 @@
return original;
}
/**
* @brief Parse an AMD rate-control mode while preserving the current value on invalid input.
*
* @param rc Rate-control mode selected in the configuration.
* @param original Original text value used when reporting a parsing failure.
* @return Parsed enum value, or the setting-specific default when the text is unknown.
*/
template<class T>
::std::optional<int> rc_from_view(const ::std::string_view &rc, const ::std::optional<int>(&original)) {
#define _CONVERT_(x) \
#ifndef DOXYGEN
#define _CONVERT_(x) \
if (rc == #x##sv) \
return (int) T::x
#endif
if (rc == #x##sv) \
return (int) T::x
_CONVERT_(cbr);
_CONVERT_(cqp);
_CONVERT_(vbr_latency);
@@ -226,11 +286,20 @@
return original;
}
/**
* @brief Parse an AMD encoder usage mode while preserving the current value on invalid input.
*
* @param usage Encoder usage mode selected in the configuration.
* @param original Original text value used when reporting a parsing failure.
* @return Parsed enum value, or the setting-specific default when the text is unknown.
*/
template<class T>
::std::optional<int> usage_from_view(const ::std::string_view &usage, const ::std::optional<int>(&original)) {
#ifndef DOXYGEN
#define _CONVERT_(x) \
if (usage == #x##sv) \
#define _CONVERT_(x) \
if (usage == #x##sv) \
return (int) T::x
return (int) T::x
#endif
_CONVERT_(lowlatency);
_CONVERT_(lowlatency_high_quality);
_CONVERT_(transcoding);
@@ -240,6 +309,12 @@
return original;
}
/**
* @brief Parse an entropy-coder mode from configuration text.
*
* @param coder Entropy-coder mode selected in the configuration.
* @return Parsed enum value, or the setting-specific default when the text is unknown.
*/
int coder_from_view(const ::std::string_view &coder) {
if (coder == "auto"sv) {
return _auto;
@@ -256,6 +331,9 @@
} // namespace amd
namespace qsv {
/**
* @brief Enumerates supported preset options.
*/
enum preset_e : int {
veryslow = 1, ///< veryslow preset
slower = 2, ///< slower preset
@@ -266,16 +344,27 @@
veryfast = 7 ///< veryfast preset
};
/**
* @brief Enumerates supported cavlc options.
*/
enum cavlc_e : int {
_auto = false, ///< Auto
enabled = true, ///< Enabled
disabled = false ///< Disabled
};
/**
* @brief Parse a QSV encoder preset from configuration text.
*
* @param preset Encoder preset value supplied by the configuration.
* @return Parsed enum value, or the setting-specific default when the text is unknown.
*/
std::optional<int> preset_from_view(const std::string_view &preset) {
#define _CONVERT_(x) \
if (preset == #x##sv) \
return x
#ifndef DOXYGEN
#define _CONVERT_(x) \
if (preset == #x##sv) \
return x
#endif
_CONVERT_(veryslow);
_CONVERT_(slower);
_CONVERT_(slow);
@@ -287,6 +376,12 @@
return std::nullopt;
}
/**
* @brief Parse an entropy-coder mode from configuration text.
*
* @param coder Entropy-coder mode selected in the configuration.
* @return Parsed enum value, or the setting-specific default when the text is unknown.
*/
std::optional<int> coder_from_view(const std::string_view &coder) {
if (coder == "auto"sv) {
return _auto;
@@ -304,12 +399,21 @@
namespace vt {
/**
* @brief Enumerates supported coder options.
*/
enum coder_e : int {
_auto = 0, ///< Auto
cabac, ///< CABAC
cavlc ///< CAVLC
};
/**
* @brief Parse an entropy-coder mode from configuration text.
*
* @param coder Entropy-coder mode selected in the configuration.
* @return Parsed enum value, or the setting-specific default when the text is unknown.
*/
int coder_from_view(const std::string_view &coder) {
if (coder == "auto"sv) {
return _auto;
@@ -324,6 +428,12 @@
return -1;
}
/**
* @brief Parse whether VideoToolbox software encoding is allowed.
*
* @param software Whether the software encoder path is being configured.
* @return Parsed enum value, or the setting-specific default when the text is unknown.
*/
int allow_software_from_view(const std::string_view &software) {
if (software == "allowed"sv || software == "forced") {
return 1;
@@ -332,6 +442,12 @@
return 0;
}
/**
* @brief Parse whether VideoToolbox software encoding is forced.
*
* @param software Whether the software encoder path is being configured.
* @return Parsed enum value, or the setting-specific default when the text is unknown.
*/
int force_software_from_view(const std::string_view &software) {
if (software == "forced") {
return 1;
@@ -340,6 +456,12 @@
return 0;
}
/**
* @brief Parse the VideoToolbox realtime encoder flag.
*
* @param rt Real-time encoder usage selector.
* @return Parsed enum value, or the setting-specific default when the text is unknown.
*/
int rt_from_view(const std::string_view &rt) {
if (rt == "disabled" || rt == "off" || rt == "0") {
return 0;
@@ -351,10 +473,18 @@
} // namespace vt
namespace sw {
/**
* @brief Parse an SVT-AV1 speed preset from configuration text.
*
* @param preset Encoder preset value supplied by the configuration.
* @return Parsed enum value, or the setting-specific default when the text is unknown.
*/
int svtav1_preset_from_view(const std::string_view &preset) {
#ifndef DOXYGEN
#define _CONVERT_(x, y) \
if (preset == #x##sv) \
return y
#endif
#define _CONVERT_(x, y) \
if (preset == #x##sv) \
return y
_CONVERT_(veryslow, 1);
_CONVERT_(slower, 2);
_CONVERT_(slow, 4);
@@ -370,10 +500,18 @@
} // namespace sw
namespace dd {
/**
* @brief Parse display-device preparation mode from configuration text.
*
* @param value Configuration text from the display-device preparation setting.
* @return Parsed enum value, or the setting-specific default when the text is unknown.
*/
video_t::dd_t::config_option_e config_option_from_view(const std::string_view value) {
#define _CONVERT_(x) \
if (value == #x##sv) \
return video_t::dd_t::config_option_e::x
#ifndef DOXYGEN
#define _CONVERT_(x) \
if (value == #x##sv) \
return video_t::dd_t::config_option_e::x
#endif
_CONVERT_(disabled);
_CONVERT_(verify_only);
_CONVERT_(ensure_active);
@@ -383,11 +521,19 @@
return video_t::dd_t::config_option_e::disabled; // Default to this if value is invalid
}
/**
* @brief Parse display-device resolution mode from configuration text.
*
* @param value Configuration text from the display-device resolution setting.
* @return Parsed enum value, or the setting-specific default when the text is unknown.
*/
video_t::dd_t::resolution_option_e resolution_option_from_view(const std::string_view value) {
#define _CONVERT_2_ARG_(str, val) \
if (value == #str##sv) \
return video_t::dd_t::resolution_option_e::val
#define _CONVERT_(x) _CONVERT_2_ARG_(x, x)
#ifndef DOXYGEN
#define _CONVERT_2_ARG_(str, val) \
if (value == #str##sv) \
return video_t::dd_t::resolution_option_e::val
#define _CONVERT_(x) _CONVERT_2_ARG_(x, x)
#endif
_CONVERT_(disabled);
_CONVERT_2_ARG_(auto, automatic);
_CONVERT_(manual);
@@ -396,11 +542,19 @@
return video_t::dd_t::resolution_option_e::disabled; // Default to this if value is invalid
}
/**
* @brief Parse display-device refresh-rate mode from configuration text.
*
* @param value Configuration text from the display-device refresh-rate setting.
* @return Parsed enum value, or the setting-specific default when the text is unknown.
*/
video_t::dd_t::refresh_rate_option_e refresh_rate_option_from_view(const std::string_view value) {
#define _CONVERT_2_ARG_(str, val) \
if (value == #str##sv) \
#ifndef DOXYGEN
#define _CONVERT_2_ARG_(str, val) \
if (value == #str##sv) \
return video_t::dd_t::refresh_rate_option_e::val
#define _CONVERT_(x) _CONVERT_2_ARG_(x, x)
return video_t::dd_t::refresh_rate_option_e::val
#define _CONVERT_(x) _CONVERT_2_ARG_(x, x)
#endif
_CONVERT_(disabled);
_CONVERT_2_ARG_(auto, automatic);
_CONVERT_(manual);
@@ -409,11 +563,19 @@
return video_t::dd_t::refresh_rate_option_e::disabled; // Default to this if value is invalid
}
/**
* @brief Parse display-device HDR mode from configuration text.
*
* @param value Configuration text from the display-device HDR setting.
* @return Parsed enum value, or the setting-specific default when the text is unknown.
*/
video_t::dd_t::hdr_option_e hdr_option_from_view(const std::string_view value) {
#ifndef DOXYGEN
#define _CONVERT_2_ARG_(str, val) \
if (value == #str##sv) \
#define _CONVERT_2_ARG_(str, val) \
if (value == #str##sv) \
return video_t::dd_t::hdr_option_e::val
#define _CONVERT_(x) _CONVERT_2_ARG_(x, x)
#endif
return video_t::dd_t::hdr_option_e::val
#define _CONVERT_(x) _CONVERT_2_ARG_(x, x)
_CONVERT_(disabled);
_CONVERT_2_ARG_(auto, automatic);
#undef _CONVERT_
@@ -421,6 +583,12 @@
return video_t::dd_t::hdr_option_e::disabled; // Default to this if value is invalid
}
/**
* @brief Parse display-mode remapping rules from JSON configuration text.
*
* @param value JSON array text from the display-device mode-remapping setting.
* @return Parsed enum value, or the setting-specific default when the text is unknown.
*/
video_t::dd_t::mode_remapping_t mode_remapping_from_view(const std::string_view value) {
const auto parse_entry_list {[](const auto &entry_list, auto &output_field) {
for (auto &[_, entry] : entry_list) {
@@ -449,5 +617,8 @@
}
} // namespace dd
/**
* @brief Default video configuration values used before file and CLI overrides.
*/
video_t video {
28, // qp
@@ -528,6 +699,9 @@
0 // minimum_fps_target (0 = framerate)
};
/**
* @brief Default audio configuration values used before file and CLI overrides.
*/
audio_t audio {
{}, // audio_sink
{}, // virtual_sink
@@ -535,5 +709,8 @@
true, // install_steam_drivers
};
/**
* @brief Default stream configuration values used before file and CLI overrides.
*/
stream_t stream {
10s, // ping_timeout
@@ -547,5 +724,8 @@
0, // packetsize
};
/**
* @brief Default NVHTTP server configuration values used before file and CLI overrides.
*/
nvhttp_t nvhttp {
"lan", // origin web manager
@@ -558,6 +738,9 @@
{}, // external_ip
};
/**
* @brief Default input configuration values used before file and CLI overrides.
*/
input_t input {
{
{0x10, 0xA0},
@@ -585,6 +768,9 @@
true, // native pen/touch support
};
/**
* @brief Default top-level Sunshine configuration values used before file and CLI overrides.
*/
sunshine_t sunshine {
"en", // locale
2, // min_log_level
@@ -604,17 +790,42 @@
{}, // prep commands
};
/**
* @brief Return whether a character terminates a configuration line.
*
* @param ch Character currently being classified by the parser.
* @return True when the tested parser condition is met.
*/
bool endline(char ch) {
return ch == '\r' || ch == '\n';
}
/**
* @brief Return whether a character is horizontal parser whitespace.
*
* @param ch Character currently being classified by the parser.
* @return True when the tested parser condition is met.
*/
bool space_tab(char ch) {
return ch == ' ' || ch == '\t';
}
/**
* @brief Return whether a character should be treated as parser whitespace.
*
* @param ch Character currently being classified by the parser.
* @return True when the tested parser condition is met.
*/
bool whitespace(char ch) {
return space_tab(ch) || endline(ch);
}
/**
* @brief Copy a configuration text range while stripping inline comments.
*
* @param begin Iterator or pointer marking the start of the input range.
* @param end Iterator or pointer marking the end of the input range.
* @return Value converted to string.
*/
std::string to_string(const char *begin, const char *end) {
std::string result;
@@ -631,6 +842,13 @@
return result;
}
/**
* @brief Advance over a bracketed list while honoring nested brackets.
*
* @param skipper Function used to skip characters while parsing.
* @param end Iterator or pointer marking the end of the input range.
* @return Iterator positioned after the matching closing bracket or at the end.
*/
template<class It>
It skip_list(It skipper, It end) {
int stack = 1;
@@ -651,6 +869,13 @@
std::pair<
std::string_view::const_iterator,
std::optional<std::pair<std::string, std::string>>>
/**
* @brief Parse one `name = value` configuration entry.
*
* @param begin Iterator or pointer marking the start of the input range.
* @param end Iterator or pointer marking the end of the input range.
* @return Iterator for the next line and the parsed key-value pair when one was found.
*/
parse_option(std::string_view::const_iterator begin, std::string_view::const_iterator end) {
begin = std::find_if_not(begin, end, whitespace);
auto endl = std::find_if(begin, end, endline);
@@ -689,5 +914,8 @@
);
}
/**
* @brief Parse Sunshine configuration text into key-value entries.
*/
std::unordered_map<std::string, std::string> parse_config(const std::string_view &file_content) {
std::unordered_map<std::string, std::string> vars;
@@ -714,6 +942,13 @@
return vars;
}
/**
* @brief Consume a string setting from the parsed configuration map.
*
* @param vars Parsed configuration entries; consumed keys are erased.
* @param name Configuration key to consume.
* @param input Destination field updated when the setting exists and parses successfully.
*/
void string_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::string &input) {
auto it = vars.find(name);
if (it == std::end(vars)) {
@@ -725,6 +960,14 @@
vars.erase(it);
}
/**
* @brief Consume a setting and convert it with a caller-provided parser.
*
* @param vars Parsed configuration entries; consumed keys are erased.
* @param name Configuration key to consume.
* @param input Destination field updated when the setting exists and parses successfully.
* @param f Converter applied to the raw configuration string.
*/
template<typename T, typename F>
void generic_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, T &input, F &&f) {
std::string tmp;
@@ -734,6 +977,14 @@
}
}
/**
* @brief Consume a string setting only when it matches an allowed value.
*
* @param vars Parsed configuration entries; consumed keys are erased.
* @param name Configuration key to consume.
* @param input Destination field updated when the setting exists and parses successfully.
* @param allowed_vals Accepted string values for this setting.
*/
void string_restricted_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::string &input, const std::vector<std::string_view> &allowed_vals) {
std::string temp;
string_f(vars, name, temp);
@@ -746,6 +997,13 @@
}
}
/**
* @brief Parse a comma-separated string setting into a list.
*
* @param vars Configuration key-value map.
* @param name Setting name.
* @param output Parsed string list.
*/
void string_list_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::vector<std::string> &output) { // NOSONAR(cpp:S6045) - transparent hasher not available for unordered_map in this codebase
std::string temp;
string_f(vars, name, temp);
@@ -767,6 +1025,13 @@
}
}
/**
* @brief Consume a path setting and normalize it under the app data directory when relative.
*
* @param vars Parsed configuration entries; consumed keys are erased.
* @param name Configuration key to consume.
* @param input Destination field updated when the setting exists and parses successfully.
*/
void path_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, fs::path &input) {
// appdata needs to be retrieved once only
static auto appdata = platf::appdata();
@@ -791,5 +1056,12 @@
}
}
/**
* @brief Consume a path setting and normalize it under the app data directory when relative.
*
* @param vars Parsed configuration entries; consumed keys are erased.
* @param name Configuration key to consume.
* @param input Destination field updated when the setting exists and parses successfully.
*/
void path_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::string &input) {
fs::path temp = input;
@@ -799,5 +1071,12 @@
input = temp.string();
}
/**
* @brief Consume an integer setting from decimal or hexadecimal configuration text.
*
* @param vars Parsed configuration entries; consumed keys are erased.
* @param name Configuration key to consume.
* @param input Destination field updated when the setting exists and parses successfully.
*/
void int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input) {
auto it = vars.find(name);
@@ -823,5 +1102,12 @@
vars.erase(it);
}
/**
* @brief Consume an integer setting from decimal or hexadecimal configuration text.
*
* @param vars Parsed configuration entries; consumed keys are erased.
* @param name Configuration key to consume.
* @param input Destination field updated when the setting exists and parses successfully.
*/
void int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::optional<int> &input) {
auto it = vars.find(name);
@@ -847,6 +1133,14 @@
vars.erase(it);
}
/**
* @brief Consume an integer setting from decimal or hexadecimal configuration text.
*
* @param vars Parsed configuration entries; consumed keys are erased.
* @param name Configuration key to consume.
* @param input Destination field updated when the setting exists and parses successfully.
* @param f Converter applied to the raw configuration string.
*/
template<class F>
void int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input, F &&f) {
std::string tmp;
@@ -856,6 +1150,14 @@
}
}
/**
* @brief Consume an integer setting from decimal or hexadecimal configuration text.
*
* @param vars Parsed configuration entries; consumed keys are erased.
* @param name Configuration key to consume.
* @param input Destination field updated when the setting exists and parses successfully.
* @param f Converter applied to the raw configuration string.
*/
template<class F>
void int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::optional<int> &input, F &&f) {
std::string tmp;
@@ -865,5 +1167,13 @@
}
}
/**
* @brief Consume an integer setting only when it falls inside an inclusive range.
*
* @param vars Parsed configuration entries; consumed keys are erased.
* @param name Configuration key to consume.
* @param input Destination field updated when the setting exists and parses successfully.
* @param range Inclusive range accepted for the parsed value.
*/
void int_between_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input, const std::pair<int, int> &range) {
int temp = input;
@@ -876,6 +1186,12 @@
}
}
/**
* @brief Convert common textual boolean forms to a boolean value.
*
* @param boolean Configuration string to classify as enabled or disabled.
* @return True when the tested parser condition is met.
*/
bool to_bool(std::string &boolean) {
std::for_each(std::begin(boolean), std::end(boolean), [](char ch) {
return (char) std::tolower(ch);
@@ -889,6 +1205,13 @@
(std::find(std::begin(boolean), std::end(boolean), '1') != std::end(boolean));
}
/**
* @brief Consume a boolean setting from the parsed configuration map.
*
* @param vars Parsed configuration entries; consumed keys are erased.
* @param name Configuration key to consume.
* @param input Destination field updated when the setting exists and parses successfully.
*/
void bool_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, bool &input) {
std::string tmp;
string_f(vars, name, tmp);
@@ -900,6 +1223,13 @@
input = to_bool(tmp);
}
/**
* @brief Consume a floating-point setting from the parsed configuration map.
*
* @param vars Parsed configuration entries; consumed keys are erased.
* @param name Configuration key to consume.
* @param input Destination field updated when the setting exists and parses successfully.
*/
void double_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, double &input) {
std::string tmp;
string_f(vars, name, tmp);
@@ -918,5 +1248,13 @@
input = val;
}
/**
* @brief Consume a floating-point setting only when it falls inside an inclusive range.
*
* @param vars Parsed configuration entries; consumed keys are erased.
* @param name Configuration key to consume.
* @param input Destination field updated when the setting exists and parses successfully.
* @param range Inclusive range accepted for the parsed value.
*/
void double_between_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, double &input, const std::pair<double, double> &range) {
double temp = input;
@@ -929,6 +1267,13 @@
}
}
/**
* @brief Consume a comma-separated or bracketed string list setting.
*
* @param vars Parsed configuration entries; consumed keys are erased.
* @param name Configuration key to consume.
* @param input Destination field updated when the setting exists and parses successfully.
*/
void list_string_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::vector<std::string> &input) {
std::string string;
string_f(vars, name, string);
@@ -968,6 +1313,13 @@
}
}
/**
* @brief Consume the JSON preparation-command list setting.
*
* @param vars Parsed configuration entries; consumed keys are erased.
* @param name Configuration key to consume.
* @param input Destination field updated when the setting exists and parses successfully.
*/
void list_prep_cmd_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::vector<prep_cmd_t> &input) {
std::string string;
string_f(vars, name, string);
@@ -994,6 +1346,13 @@
}
}
/**
* @brief Consume an integer list setting from decimal or hexadecimal configuration text.
*
* @param vars Parsed configuration entries; consumed keys are erased.
* @param name Configuration key to consume.
* @param input Destination field updated when the setting exists and parses successfully.
*/
void list_int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::vector<int> &input) {
std::vector<std::string> list;
list_string_f(vars, name, list);
@@ -1027,6 +1386,13 @@
}
}
/**
* @brief Consume an integer-pair list into a mapping table.
*
* @param vars Parsed configuration entries; consumed keys are erased.
* @param name Configuration key to consume.
* @param input Destination field updated when the setting exists and parses successfully.
*/
void map_int_int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::unordered_map<int, int> &input) {
std::vector<int> list;
list_int_f(vars, name, list);
@@ -1046,6 +1412,12 @@
}
}
/**
* @brief Apply single-character command-line flags to the global Sunshine flags bitset.
*
* @param line Configuration line being parsed.
* @return 0 when all flags are recognized; -1 when at least one flag is unknown.
*/
int apply_flags(const char *line) {
int ret = 0;
while (*line != '\0') {
@@ -1073,6 +1445,11 @@
return ret;
}
/**
* @brief Get supported gamepad options.
*
* @return Platform-supported gamepad backend names accepted by configuration.
*/
std::vector<std::string_view> &get_supported_gamepad_options() {
const auto options = platf::supported_gamepads(nullptr);
static std::vector<std::string_view> opts {};
@@ -1083,6 +1460,9 @@
return opts;
}
/**
* @brief Log parsed configuration entries and optionally record them as modified.
*/
void log_config_settings(const std::unordered_map<std::string, std::string> &vars, bool save) {
for (auto &[name, val] : vars) {
bool is_redacted = std::ranges::find(config::redacted_config, name) != config::redacted_config.end();
@@ -1095,5 +1475,10 @@
}
}
/**
* @brief Apply parsed configuration entries to the global runtime configuration.
*
* @param vars Parsed configuration entries; consumed keys are erased.
*/
void apply_config(std::unordered_map<std::string, std::string> &&vars) {
log_config_settings(vars, true);
@@ -1406,6 +1791,9 @@
}
}
/**
* @brief Parse serialized text into the corresponding runtime representation.
*/
int parse(int argc, char *argv[]) {
std::unordered_map<std::string, std::string> cmd_vars;
#ifdef _WIN32
src/config.h +184 −86
@@ -17,40 +17,55 @@
namespace config {
// Valid range for the packetsize limit
constexpr int PACKETSIZE_MIN = 200;
constexpr int PACKETSIZE_MIN = 200; ///< Lowest accepted configured packet size in bytes.
constexpr int PACKETSIZE_MAX = 65535; ///< Highest accepted configured packet size in bytes.
constexpr int PACKETSIZE_SMALL = 500; ///< Conservative packet size used for low-MTU links.
constexpr int PACKETSIZE_LARGE = 1456; ///< Default large packet size that avoids common MTU fragmentation.
constexpr int PACKETSIZE_MAX = 65535;
constexpr int PACKETSIZE_SMALL = 500;
constexpr int PACKETSIZE_LARGE = 1456;
// track modified config options
inline std::unordered_map<std::string, std::string> modified_config_settings; ///< Configuration keys changed during the current parse or UI update.
inline std::unordered_map<std::string, std::string> modified_config_settings;
// sensitive values that should be redacted from logging
/**
* @brief Configuration keys whose values must be hidden in logs.
*/
inline constexpr std::array redacted_config = {
"csrf_allowed_origins"
};
/**
* @brief Log configuration entries and optionally mark them for persistence.
*
* @param vars Parsed configuration entries to log.
* @param save Whether modified configuration values should be written back to disk.
*/
void log_config_settings(const std::unordered_map<std::string, std::string> &vars, bool save);
/**
* @brief Video encoder, capture, and color settings loaded from configuration.
*/
struct video_t {
// ffmpeg params
/**
* @brief Quantization parameter used by encoders where higher values trade quality for compression.
*/
int qp; // higher == more compression and less quality
int hevc_mode; ///< HEVC support mode advertised to clients.
int hevc_mode;
int av1_mode;
int av1_mode; ///< AV1 support mode advertised to clients.
int min_threads; // Minimum number of threads/slices for CPU encoding
int min_threads; ///< Minimum number of threads or slices for CPU encoding.
struct {
std::string sw_preset;
std::string sw_tune;
std::optional<int> svtav1_preset;
} sw;
} sw; ///< Software encoder options.
nvenc::nvenc_config nv;
bool nv_realtime_hags;
nvenc::nvenc_config nv; ///< NVIDIA NVENC encoder settings.
bool nv_realtime_hags; ///< Enable the NVIDIA realtime HAGS workaround.
bool nv_opengl_vulkan_on_dxgi; ///< Prefer NVIDIA OpenGL/Vulkan-on-DXGI interop.
bool nv_sunshine_high_power_mode; ///< Request NVIDIA high-power mode for Sunshine.
bool nv_opengl_vulkan_on_dxgi;
bool nv_sunshine_high_power_mode;
struct {
int preset;
@@ -58,13 +73,13 @@
int h264_coder;
int aq;
int vbv_percentage_increase;
} nv_legacy;
} nv_legacy; ///< Legacy NVIDIA encoder options kept for config compatibility.
struct {
std::optional<int> qsv_preset;
std::optional<int> qsv_cavlc;
bool qsv_slow_hevc;
} qsv;
} qsv; ///< Intel Quick Sync encoder options.
struct {
std::optional<int> amd_usage_h264;
@@ -80,34 +95,43 @@
std::optional<int> amd_preanalysis;
std::optional<int> amd_vbaq;
int amd_coder;
} amd;
} amd; ///< AMD AMF encoder options.
struct {
int vt_allow_sw;
int vt_require_sw;
int vt_realtime;
int vt_coder;
} vt;
} vt; ///< VideoToolbox encoder options.
struct {
bool strict_rc_buffer;
} vaapi;
} vaapi; ///< VA-API encoder options.
struct {
int tune; // 0=default, 1=hq, 2=ll, 3=ull, 4=lossless
int rc_mode; // 0=driver, 1=cqp, 2=cbr, 4=vbr
} vk;
} vk; ///< Vulkan encoder options.
std::string capture; ///< Capture backend name selected by configuration.
std::string encoder; ///< Encoder backend name selected by configuration.
std::string adapter_name; ///< Display adapter name selected in configuration.
std::string capture;
std::string encoder;
std::string adapter_name;
std::string output_name; ///< Display output name selected in configuration.
std::string output_name;
/**
* @brief Display-device integration settings.
*/
struct dd_t {
/**
* @brief Compatibility workarounds for display-device control.
*/
struct workarounds_t {
std::chrono::milliseconds hdr_toggle_delay; ///< Specify whether to apply HDR high-contrast color workaround and what delay to use.
};
/**
* @brief Selects how Sunshine prepares the active display before streaming.
*/
enum class config_option_e {
disabled, ///< Disable the configuration for the device.
verify_only, ///< @seealso{display_device::SingleDisplayConfiguration::DevicePreparation}
@@ -116,52 +140,70 @@
ensure_only_display ///< @seealso{display_device::SingleDisplayConfiguration::DevicePreparation}
};
/**
* @brief Selects how Sunshine chooses the stream display resolution.
*/
enum class resolution_option_e {
disabled, ///< Do not change resolution.
automatic, ///< Change resolution and use the one received from Moonlight.
manual ///< Change resolution and use the manually provided one.
};
/**
* @brief Selects how Sunshine chooses the stream display refresh rate.
*/
enum class refresh_rate_option_e {
disabled, ///< Do not change refresh rate.
automatic, ///< Change refresh rate and use the one received from Moonlight.
manual ///< Change refresh rate and use the manually provided one.
};
/**
* @brief Selects how Sunshine handles HDR state for the stream display.
*/
enum class hdr_option_e {
disabled, ///< Do not change HDR settings.
automatic ///< Change HDR settings and use the state requested by Moonlight.
};
/**
* @brief Single display mode remapping rule from configuration.
*/
struct mode_remapping_entry_t {
std::string requested_resolution;
std::string requested_fps;
std::string final_resolution;
std::string final_refresh_rate;
std::string requested_resolution; ///< Resolution string requested by the client.
std::string requested_fps; ///< Refresh-rate string requested by the client.
std::string final_resolution; ///< Resolution string to apply after remapping.
std::string final_refresh_rate; ///< Refresh-rate string to apply after remapping.
};
/**
* @brief Collection of display mode remapping rules.
*/
struct mode_remapping_t {
std::vector<mode_remapping_entry_t> mixed; ///< To be used when `resolution_option` and `refresh_rate_option` is set to `automatic`.
std::vector<mode_remapping_entry_t> resolution_only; ///< To be use when only `resolution_option` is set to `automatic`.
std::vector<mode_remapping_entry_t> refresh_rate_only; ///< To be use when only `refresh_rate_option` is set to `automatic`.
};
config_option_e configuration_option; ///< Display-preparation mode selected by configuration.
config_option_e configuration_option;
resolution_option_e resolution_option;
resolution_option_e resolution_option; ///< Resolution-selection mode selected by configuration.
std::string manual_resolution; ///< Manual resolution in case `resolution_option == resolution_option_e::manual`.
refresh_rate_option_e refresh_rate_option;
refresh_rate_option_e refresh_rate_option; ///< Refresh-rate selection mode selected by configuration.
std::string manual_refresh_rate; ///< Manual refresh rate in case `refresh_rate_option == refresh_rate_option_e::manual`.
hdr_option_e hdr_option;
hdr_option_e hdr_option; ///< HDR-selection mode selected by configuration.
std::chrono::milliseconds config_revert_delay; ///< Time to wait until settings are reverted (after stream ends/app exists).
bool config_revert_on_disconnect; ///< Specify whether to revert display configuration on client disconnect.
mode_remapping_t mode_remapping; ///< Display mode remapping rules grouped by automatic selection mode.
mode_remapping_t mode_remapping;
workarounds_t wa;
} dd;
workarounds_t wa; ///< Display-device compatibility workarounds.
} dd; ///< Display-device integration settings.
int max_bitrate; // Maximum bitrate, sets ceiling in kbps for bitrate requested from client
int max_bitrate; ///< Maximum bitrate ceiling in kbps for bitrate requested from the client.
double minimum_fps_target; ///< Lowest framerate that will be used when streaming. Range 0-1000, 0 = half of client's requested framerate.
};
/**
* @brief Audio capture and encoder settings loaded from configuration.
*/
struct audio_t {
std::string sink; ///< Audio output device/sink to use for audio capture
std::string virtual_sink; ///< Virtual audio sink for audio routing
@@ -169,65 +211,86 @@
bool install_steam_drivers; ///< Install Steam audio drivers for enhanced compatibility
};
/**
* @brief Encryption policy that always sends unencrypted video.
*/
constexpr int ENCRYPTION_MODE_NEVER = 0; // Never use video encryption, even if the client supports it
/**
* @brief Encryption policy that uses encrypted video only when the client supports it.
*/
constexpr int ENCRYPTION_MODE_OPPORTUNISTIC = 1; // Use video encryption if available, but stream without it if not supported
/**
* @brief Encryption policy that rejects clients without video encryption support.
*/
constexpr int ENCRYPTION_MODE_MANDATORY = 2; // Always use video encryption and refuse clients that can't encrypt
/**
* @brief Network stream settings shared by audio, video, and control channels.
*/
struct stream_t {
std::chrono::milliseconds ping_timeout;
std::chrono::milliseconds ping_timeout; ///< Timeout used when waiting for client ping responses.
std::string file_apps;
std::string file_apps; ///< Path to the configured applications file.
int fec_percentage;
int fec_percentage; ///< Percentage of forward-error-correction packets to add to the stream.
// Video encryption settings for LAN and WAN streams
int lan_encryption_mode;
int wan_encryption_mode;
int lan_encryption_mode; ///< Video encryption policy for LAN clients.
int wan_encryption_mode; ///< Video encryption policy for WAN clients.
// Limit the packetsize to avoid fragmentation on a low MTU link
int packetsize;
int packetsize; ///< Maximum payload size for network packets.
};
/**
* @brief HTTP and HTTPS settings used by the GameStream pairing server.
*/
struct nvhttp_t {
// Could be any of the following values:
// pc|lan|wan
std::string origin_web_ui_allowed; ///< Origin policy used for Web UI access checks.
std::string origin_web_ui_allowed;
std::string pkey;
std::string pkey; ///< Private key PEM string or path.
std::string cert; ///< Certificate PEM string or path.
std::string cert;
std::string sunshine_name;
std::string sunshine_name; ///< Host name advertised to Moonlight clients.
std::string file_state;
std::string file_state; ///< Path to the persisted Sunshine state file.
std::string external_ip;
std::string external_ip; ///< External address advertised to clients when configured.
};
/**
* @brief Input emulation settings loaded from configuration.
*/
struct input_t {
std::unordered_map<int, int> keybindings; ///< Client keycode to platform keycode bindings.
std::unordered_map<int, int> keybindings;
std::chrono::milliseconds back_button_timeout; ///< Hold duration that turns a controller Back button into a special action.
std::chrono::milliseconds back_button_timeout;
std::chrono::milliseconds key_repeat_delay;
std::chrono::milliseconds key_repeat_delay; ///< Delay before repeating a held keyboard key.
std::chrono::duration<double> key_repeat_period; ///< Interval between repeated keyboard key events.
std::chrono::duration<double> key_repeat_period;
std::string gamepad;
bool ds4_back_as_touchpad_click;
bool motion_as_ds4;
bool touchpad_as_ds4;
bool ds5_inputtino_randomize_mac;
std::string gamepad; ///< Virtual controller backend selected by configuration.
bool ds4_back_as_touchpad_click; ///< Map the DS4 Back button to a touchpad click.
bool motion_as_ds4; ///< Expose motion controls through the DS4 protocol.
bool touchpad_as_ds4; ///< Expose touchpad input through the DS4 protocol.
bool ds5_inputtino_randomize_mac; ///< Randomize the inputtino DualSense MAC address.
bool keyboard; ///< Enable keyboard input from clients.
bool key_rightalt_to_key_win; ///< Map the client Right Alt key to the Windows key.
bool keyboard;
bool key_rightalt_to_key_win;
bool mouse; ///< Enable mouse input from clients.
bool controller; ///< Enable controller input from clients.
bool mouse;
bool controller;
bool always_send_scancodes;
bool always_send_scancodes; ///< Always send keyboard scancodes when available.
bool high_resolution_scrolling;
bool native_pen_touch;
bool high_resolution_scrolling; ///< Enable high-resolution mouse-wheel events.
bool native_pen_touch; ///< Enable native pen and touch injection.
};
namespace flag {
/**
* @brief Enumerates supported flag options.
*/
enum flag_e : std::size_t {
PIN_STDIN = 0, ///< Read PIN from stdin instead of http
FRESH_STATE, ///< Do not load or save state
@@ -238,53 +301,75 @@
};
} // namespace flag
/**
* @brief External preparation command plus its privilege requirement.
*/
struct prep_cmd_t {
/**
* @brief Build a preparation command entry from parsed configuration data.
*
* @param do_cmd Command to run before the application starts.
* @param undo_cmd Command to run after the application exits.
* @param elevated Whether the command should run with elevated privileges.
*/
prep_cmd_t(std::string &&do_cmd, std::string &&undo_cmd, bool &&elevated):
do_cmd(std::move(do_cmd)),
undo_cmd(std::move(undo_cmd)),
elevated(std::move(elevated)) {
}
/**
* @brief Build a preparation command entry from parsed configuration data.
*
* @param do_cmd Command to run before the application starts.
* @param elevated Whether the command should run with elevated privileges.
*/
explicit prep_cmd_t(std::string &&do_cmd, bool &&elevated):
do_cmd(std::move(do_cmd)),
elevated(std::move(elevated)) {
}
std::string do_cmd; ///< Command to run before the application starts.
std::string undo_cmd; ///< Command to run after the application exits.
bool elevated; ///< Whether the process should be launched elevated.
std::string do_cmd;
std::string undo_cmd;
bool elevated;
};
/**
* @brief Top-level Sunshine configuration and credential state.
*/
struct sunshine_t {
std::string locale;
int min_log_level;
std::bitset<flag::FLAG_SIZE> flags;
std::string credentials_file;
std::string locale; ///< Locale selected for Sunshine UI and log messages.
int min_log_level; ///< Minimum severity level written to the configured log sink.
std::bitset<flag::FLAG_SIZE> flags; ///< Runtime flags parsed from command-line options.
std::string credentials_file; ///< Path to the stored pairing credentials file.
std::string username; ///< Username for the local Web UI account.
std::string password; ///< Password hash or secret for the local Web UI account.
std::string salt; ///< Salt used when hashing the Web UI password.
std::string username;
std::string password;
std::string salt;
std::string config_file; ///< Path to the active Sunshine configuration file.
std::string config_file;
/**
* @brief Command-line options parsed before configuration loading.
*/
struct cmd_t {
std::string name; ///< Executable name from the command line.
int argc; ///< Number of command-line arguments.
char **argv; ///< Command-line argument vector.
} cmd; ///< Command line used to launch the application.
std::string name;
int argc;
char **argv;
} cmd;
std::uint16_t port; ///< TCP port used by Sunshine services.
std::uint16_t port;
std::string address_family;
std::string address_family; ///< Address family requested for listening sockets.
std::string bind_address; ///< Local address Sunshine should bind to.
std::string bind_address;
std::string log_file;
bool notify_pre_releases;
bool system_tray;
std::string log_file; ///< Path to the configured log file.
bool notify_pre_releases; ///< Notify users about pre-release updates.
bool system_tray; ///< Enable the system tray integration.
std::vector<prep_cmd_t> prep_cmds; ///< Preparation commands executed around application launch.
std::vector<prep_cmd_t> prep_cmds;
// List of allowed origins for CSRF protection (e.g., "https://example.com,https://app.example.com")
// Comma-separated list of additional origins. Default includes localhost variants and web UI port.
std::vector<std::string> csrf_allowed_origins;
std::vector<std::string> csrf_allowed_origins; ///< Additional origins allowed by CSRF validation.
};
extern video_t video;
@@ -294,6 +379,19 @@
extern input_t input;
extern sunshine_t sunshine;
/**
* @brief Parse serialized text into the corresponding runtime representation.
*
* @param argc Number of command-line arguments.
* @param argv Command-line argument vector.
* @return 0 on success; nonzero when command-line or configuration parsing fails.
*/
int parse(int argc, char *argv[]);
/**
* @brief Parse Sunshine configuration text into key-value entries.
*
* @param file_content Raw configuration file contents to parse.
* @return Parsed configuration key-value entries.
*/
std::unordered_map<std::string, std::string> parse_config(const std::string_view &file_content);
} // namespace config
src/confighttp.cpp +41 −14
@@ -50,30 +50,56 @@
namespace confighttp {
namespace fs = std::filesystem;
/**
* @brief HTTPS server type used for Sunshine's configuration UI.
*/
using https_server_t = SimpleWeb::Server<SimpleWeb::HTTPS>;
/**
* @brief Case-insensitive map used for HTTP headers and query parameters.
*/
using args_t = SimpleWeb::CaseInsensitiveMultimap;
/**
* @brief Shared HTTPS response object passed to configuration handlers.
*/
using resp_https_t = std::shared_ptr<SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
/**
* @brief Shared HTTPS request object received by configuration handlers.
*/
using req_https_t = std::shared_ptr<SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>;
/**
* @brief Handler signature for configuration UI HTTPS routes.
*/
using https_handler_t = std::function<void(resp_https_t, req_https_t)>;
/**
* @brief Client certificate operations accepted by the configuration API.
*/
enum class op_e {
ADD, ///< Add client
REMOVE ///< Remove client
};
// CSRF token management
/**
* @brief CSRF token value and its expiration deadline.
*/
struct csrf_token_t {
std::string token;
std::chrono::steady_clock::time_point expiration;
std::string token; ///< Random token value that must be echoed by the client.
std::chrono::steady_clock::time_point expiration; ///< Monotonic deadline after which the token is rejected.
};
// Store CSRF tokens with thread safety
std::map<std::string, csrf_token_t, std::less<>> csrf_tokens; ///< CSRF tokens by client identifier. NOSONAR(cpp:S5421) - intentionally mutable global
std::mutex csrf_tokens_mutex; ///< Mutex protecting CSRF token storage. NOSONAR(cpp:S5421) - intentionally mutable global
std::map<std::string, csrf_token_t, std::less<>> csrf_tokens; // NOSONAR(cpp:S5421) - intentionally mutable global
std::mutex csrf_tokens_mutex; // NOSONAR(cpp:S5421) - intentionally mutable global
// CSRF token configuration
/**
* @brief Number of random bytes used when generating a CSRF token.
*/
constexpr auto CSRF_TOKEN_SIZE = 32; // 32 bytes = 256 bits
/**
* @brief Amount of time a generated CSRF token remains valid.
*/
constexpr auto CSRF_TOKEN_LIFETIME = std::chrono::hours(1); // Tokens valid for 1 hour
/**
@@ -247,9 +273,6 @@
/**
* @brief Validate the request content type and send a bad request when mismatched.
* @param response The HTTP response object.
* @param request The HTTP request object.
* @param contentType The expected content type
*/
bool check_content_type(const resp_https_t &response, const req_https_t &request, const std::string_view &contentType) {
const auto requestContentType = request->header.find("content-type");
@@ -360,6 +383,9 @@
return true;
}
/**
* @brief Validate CSRF token.
*/
bool validate_csrf_token(const resp_https_t &response, const req_https_t &request, const std::string &client_id) {
// Helper function to check if a URL starts with any allowed origin
auto is_allowed_origin = [](const std::string_view url) {
@@ -421,9 +447,6 @@
/**
* @brief Validates the application index and sends an error response if invalid.
* @param response The HTTP response object.
* @param request The HTTP request object.
* @param index The application index/id.
*/
bool check_app_index(const resp_https_t &response, const req_https_t &request, int index) {
std::string file = file_handler::read_file(config::stream.file_apps.c_str());
@@ -1384,9 +1407,10 @@
}
/**
* @brief Authenticate a Web UI request and restart the Sunshine process.
*
* @param response HTTP response used for authentication or CSRF failures.
* @param request HTTP request carrying the client identity and CSRF token.
* @brief Restart Sunshine.
* @param response The HTTP response object.
* @param request The HTTP request object.
*
* @api_examples{/api/restart| POST| null}
*/
@@ -1721,6 +1745,9 @@
}
}
/**
* @brief Start the HTTPS configuration server.
*/
void start() {
platf::set_thread_name("confighttp");
const auto shutdown_event = mail::man->event<bool>(mail::shutdown);
src/confighttp.h +32 −1
@@ -16,10 +16,14 @@
// local includes
#include "thread_safe.h"
/**
* @def WEB_DIR
* @brief Macro for WEB DIR.
*/
#define WEB_DIR SUNSHINE_ASSETS_DIR "/web/"
namespace confighttp {
constexpr auto PORT_HTTPS = 1; ///< GameStream port offset for port https.
constexpr auto PORT_HTTPS = 1;
// Type aliases for HTTPS server components
using https_server_t = SimpleWeb::Server<SimpleWeb::HTTPS>;
@@ -36,10 +40,34 @@
bool authenticate(const resp_https_t &response, const req_https_t &request);
void not_found(const resp_https_t &response, const req_https_t &request, const std::string &error_message = "Not Found");
void bad_request(const resp_https_t &response, const req_https_t &request, const std::string &error_message = "Bad Request");
/**
* @brief Check content type.
*
* @param response HTTP response object to populate.
* @param request HTTP request data from the client.
* @param contentType Expected HTTP content type.
* @return True when the request passes validation and processing may continue.
*/
bool check_content_type(const resp_https_t &response, const req_https_t &request, const std::string_view &contentType);
std::string generate_csrf_token(const std::string &client_id);
/**
* @brief Validate CSRF token.
*
* @param response HTTP response object to populate.
* @param request HTTP request data from the client.
* @param client_id Client identifier used to look up the CSRF token.
* @return True when the request passes validation and processing may continue.
*/
bool validate_csrf_token(const resp_https_t &response, const req_https_t &request, const std::string &client_id);
std::string get_client_id(const req_https_t &request);
/**
* @brief Check app index.
*
* @param response HTTP response object to populate.
* @param request HTTP request data from the client.
* @param index Zero-based index of the item being addressed.
* @return True when the request passes validation and processing may continue.
*/
bool check_app_index(const resp_https_t &response, const req_https_t &request, int index);
void getPage(const resp_https_t &response, const req_https_t &request, const char *html_file, bool require_auth = true, bool redirect_if_username = false);
void getAsset(const resp_https_t &response, const req_https_t &request);
@@ -74,6 +102,9 @@
} // namespace confighttp
// mime types map
/**
* @brief File-extension to MIME-type mapping used when serving the Web UI.
*/
const std::map<std::string, std::string> mime_types = {
{"css", "text/css"},
{"gif", "image/gif"},
src/crypto.cpp +59 −0
@@ -10,7 +10,13 @@
#include "crypto.h"
namespace crypto {
/**
* @brief OpenSSL ASN.1 string pointer with automatic release.
*/
using asn1_string_t = util::safe_ptr<ASN1_STRING, ASN1_STRING_free>;
/**
* @brief OpenSSL X.509 subject/issuer name pointer with automatic release.
*/
using x509_name_t = util::safe_ptr<X509_NAME, &X509_NAME_free>;
cert_chain_t::cert_chain_t():
@@ -322,5 +328,8 @@
} // namespace cipher
/**
* @brief Derive the AES key used by the pairing protocol.
*/
aes_t gen_aes_key(const std::array<uint8_t, 16> &salt, const std::string_view &pin) {
aes_t key(16);
@@ -344,5 +353,8 @@
return hsh;
}
/**
* @brief Parse PEM text into an X.509 certificate object.
*/
x509_t x509(const std::string_view &x) {
bio_t io {BIO_new(BIO_s_mem())};
@@ -355,5 +367,8 @@
return p;
}
/**
* @brief Parse PEM text into an OpenSSL private key object.
*/
pkey_t pkey(const std::string_view &k) {
bio_t io {BIO_new(BIO_s_mem())};
@@ -366,5 +381,8 @@
return p;
}
/**
* @brief Serialize an OpenSSL object to PEM text.
*/
std::string pem(x509_t &x509) {
bio_t bio {BIO_new(BIO_s_mem())};
@@ -376,5 +394,8 @@
return {mem_ptr->data, mem_ptr->length};
}
/**
* @brief Serialize an OpenSSL object to PEM text.
*/
std::string pem(pkey_t &pkey) {
bio_t bio {BIO_new(BIO_s_mem())};
@@ -386,5 +407,8 @@
return {mem_ptr->data, mem_ptr->length};
}
/**
* @brief Return the certificate signature bytes.
*/
std::string_view signature(const x509_t &x) {
// X509_ALGOR *_ = nullptr;
@@ -398,6 +422,9 @@
};
}
/**
* @brief Generate cryptographically secure random bytes.
*/
std::string rand(std::size_t bytes) {
std::string r;
r.resize(bytes);
@@ -407,5 +434,13 @@
return r;
}
/**
* @brief Sign data with the requested OpenSSL digest.
*
* @param pkey Private key PEM data or private key file path.
* @param data Payload or state data to serialize, deserialize, or forward.
* @param md OpenSSL message digest algorithm used for signing or verification.
* @return Number of bytes written, signature bytes, or an error status depending on the overload.
*/
std::vector<uint8_t> sign(const pkey_t &pkey, const std::string_view &data, const EVP_MD *md) {
md_ctx_t ctx {EVP_MD_CTX_create()};
@@ -431,6 +466,9 @@
return digest;
}
/**
* @brief Generate a self-signed certificate and private key for Sunshine pairing.
*/
creds_t gen_creds(const std::string_view &cn, std::uint32_t key_bits) {
x509_t x509 {X509_new()};
pkey_ctx_t ctx {EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr)};
@@ -475,9 +513,21 @@
return {pem(x509), pem(pkey)};
}
/**
* @brief Sign data with SHA-256.
*/
std::vector<uint8_t> sign256(const pkey_t &pkey, const std::string_view &data) {
return sign(pkey, data, EVP_sha256());
}
/**
* @brief Verify a signature against certificate public key data.
*
* @param x509 X.509 certificate object or PEM data.
* @param data Payload or state data to serialize, deserialize, or forward.
* @param signature Signature bytes to verify or encode.
* @param md OpenSSL message digest algorithm used for signing or verification.
* @return True when OpenSSL verifies the signature with the supplied digest.
*/
bool verify(const x509_t &x509, const std::string_view &data, const std::string_view &signature, const EVP_MD *md) {
auto pkey = X509_get0_pubkey(x509.get());
@@ -499,13 +549,22 @@
return true;
}
/**
* @brief Verify a SHA-256 signature with the certificate public key.
*/
bool verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature) {
return verify(x509, data, signature, EVP_sha256());
}
/**
* @brief Destroy an OpenSSL message digest context.
*/
void md_ctx_destroy(EVP_MD_CTX *ctx) {
EVP_MD_CTX_destroy(ctx);
}
/**
* @brief Generate random text from the supplied alphabet.
*/
std::string rand_alphabet(std::size_t bytes, const std::string_view &alphabet) {
auto value = rand(bytes);
src/crypto.h +214 −8
@@ -17,24 +17,65 @@
#include "utility.h"
namespace crypto {
/**
* @brief PEM-encoded certificate and private key pair.
*/
struct creds_t {
std::string x509; ///< PEM-encoded X.509 certificate.
std::string x509;
std::string pkey;
std::string pkey; ///< Private key PEM string or path.
};
void md_ctx_destroy(EVP_MD_CTX *);
/**
* @brief Destroy an OpenSSL message digest context.
*
* @param ctx Message digest context.
*/
void md_ctx_destroy(EVP_MD_CTX *ctx);
/**
* @brief Fixed-size SHA-256 digest byte array.
*/
using sha256_t = std::array<std::uint8_t, SHA256_DIGEST_LENGTH>;
/**
* @brief Byte buffer containing AES key material.
*/
using aes_t = std::vector<std::uint8_t>;
/**
* @brief Owning pointer for an OpenSSL X.509 certificate.
*/
using x509_t = util::safe_ptr<X509, X509_free>;
/**
* @brief Owning pointer for an OpenSSL certificate store.
*/
using x509_store_t = util::safe_ptr<X509_STORE, X509_STORE_free>;
/**
* @brief Owning pointer for an OpenSSL certificate verification context.
*/
using x509_store_ctx_t = util::safe_ptr<X509_STORE_CTX, X509_STORE_CTX_free>;
/**
* @brief Owning pointer for an OpenSSL cipher context.
*/
using cipher_ctx_t = util::safe_ptr<EVP_CIPHER_CTX, EVP_CIPHER_CTX_free>;
/**
* @brief Owning pointer for an OpenSSL message digest context.
*/
using md_ctx_t = util::safe_ptr<EVP_MD_CTX, md_ctx_destroy>;
/**
* @brief Owning pointer for an OpenSSL BIO chain.
*/
using bio_t = util::safe_ptr<BIO, BIO_free_all>;
/**
* @brief Owning pointer for an OpenSSL public or private key.
*/
using pkey_t = util::safe_ptr<EVP_PKEY, EVP_PKEY_free>;
/**
* @brief Owning pointer for an OpenSSL key-generation context.
*/
using pkey_ctx_t = util::safe_ptr<EVP_PKEY_CTX, EVP_PKEY_CTX_free>;
/**
* @brief Owning pointer for an OpenSSL BIGNUM.
*/
using bignum_t = util::safe_ptr<BIGNUM, BN_free>;
/**
@@ -44,28 +85,111 @@
*/
sha256_t hash(const std::string_view &plaintext);
/**
* @brief Derive the AES key used by the pairing protocol.
*
* @param salt Random salt used when deriving the pairing secret.
* @param pin PIN supplied by the client during pairing.
* @return Parsed or generated OpenSSL object or PEM data.
*/
aes_t gen_aes_key(const std::array<uint8_t, 16> &salt, const std::string_view &pin);
/**
* @brief Parse PEM text into an X.509 certificate object.
*
* @param x Certificate object or PEM text, depending on the overload.
* @return Parsed or generated OpenSSL object or PEM data.
*/
x509_t x509(const std::string_view &x);
/**
* @brief Parse PEM text into an OpenSSL private key object.
*
* @param k PEM text containing a private key.
* @return Parsed or generated OpenSSL object or PEM data.
*/
pkey_t pkey(const std::string_view &k);
/**
* @brief Serialize an OpenSSL object to PEM text.
*
* @param x509 X.509 certificate object or PEM data.
* @return Parsed or generated OpenSSL object or PEM data.
*/
std::string pem(x509_t &x509);
/**
* @brief Serialize an OpenSSL object to PEM text.
*
* @param pkey Private key PEM data or private key file path.
* @return Parsed or generated OpenSSL object or PEM data.
*/
std::string pem(pkey_t &pkey);
/**
* @brief Sign data with SHA-256.
*
* @param pkey Private key PEM data or private key file path.
* @param data Payload or state data to serialize, deserialize, or forward.
* @return Number of bytes written, signature bytes, or an error status depending on the overload.
*/
std::vector<uint8_t> sign256(const pkey_t &pkey, const std::string_view &data);
/**
* @brief Verify a SHA-256 signature with the certificate public key.
*
* @param x509 X.509 certificate object or PEM data.
* @param data Payload or state data to serialize, deserialize, or forward.
* @param signature Signature bytes to verify or encode.
* @return True when the signature is valid for the supplied data.
*/
bool verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature);
/**
* @brief Generate a self-signed certificate and private key for Sunshine pairing.
*
* @param cn Common name to place in the generated certificate.
* @param key_bits Size in bits of the generated RSA key.
* @return Parsed or generated OpenSSL object or PEM data.
*/
creds_t gen_creds(const std::string_view &cn, std::uint32_t key_bits);
/**
* @brief Return the certificate signature bytes.
*
* @param x Certificate object or PEM text, depending on the overload.
* @return Parsed or generated OpenSSL object or PEM data.
*/
std::string_view signature(const x509_t &x);
/**
* @brief Generate cryptographically secure random bytes.
*
* @param bytes Number of random bytes to generate.
* @return Random bytes or text generated by OpenSSL.
*/
std::string rand(std::size_t bytes);
/**
* @brief Generate random text from the supplied alphabet.
*
* @param bytes Number of random bytes to generate.
* @param alphabet Allowed characters used for random string generation.
* @return Random bytes or text generated by OpenSSL.
*/
std::string rand_alphabet(std::size_t bytes, const std::string_view &alphabet = std::string_view {"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!%&()=-"});
/**
* @brief Owns the certificate chain returned by Sunshine's TLS certificate loader.
*/
class cert_chain_t {
public:
KITTY_DECL_CONSTR(cert_chain_t)
/**
* @brief Add a certificate to the verification chain.
*
* @param cert Certificate data or object used by the operation.
*/
void add(x509_t &&cert);
/**
* @brief Remove all certificates from the verification chain.
*/
void clear();
const char *verify(x509_t::element_type *cert);
@@ -76,40 +200,97 @@
};
namespace cipher {
constexpr std::size_t tag_size = 16; ///< Tag size.
constexpr std::size_t tag_size = 16;
/**
* @brief Round a byte count up to the next PKCS#7 padding boundary.
*
* @param size Number of bytes or elements requested.
* @return `size` rounded up to the next PKCS#7 block boundary.
*/
constexpr std::size_t round_to_pkcs7_padded(std::size_t size) {
return ((size + 15) / 16) * 16;
}
/**
* @brief AES-GCM encrypt/decrypt context pair used for GameStream messages.
*/
class cipher_t {
public:
cipher_ctx_t decrypt_ctx; ///< Decrypt ctx.
cipher_ctx_t encrypt_ctx; ///< Encrypt ctx.
cipher_ctx_t decrypt_ctx;
cipher_ctx_t encrypt_ctx;
aes_t key;
aes_t key; ///< AES key used by the cipher context.
bool padding;
bool padding; ///< Enables block padding for the cipher.
};
/**
* @brief AES-ECB cipher helper used by pairing and stream encryption code.
*/
class ecb_t: public cipher_t {
public:
ecb_t() = default;
/**
* @brief Construct an AES-ECB cipher helper with key material.
*/
ecb_t(ecb_t &&) noexcept = default;
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @return Reference or value produced by the operator.
*/
ecb_t &operator=(ecb_t &&) noexcept = default;
/**
* @brief Construct an AES-ECB cipher helper with key material.
*
* @param key AES key material used to initialize the cipher.
* @param padding Whether the cipher should use block padding.
*/
ecb_t(const aes_t &key, bool padding = true);
/**
* @brief Encrypt plaintext into the supplied ciphertext buffer.
*
* @param plaintext Plaintext bytes to encrypt.
* @param cipher Ciphertext bytes to decrypt or output buffer for encryption.
* @return Number of bytes written, signature bytes, or an error status depending on the overload.
*/
int encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher);
/**
* @brief Decrypt ciphertext into the supplied plaintext buffer.
*
* @param cipher Ciphertext bytes to decrypt or output buffer for encryption.
* @param plaintext Plaintext bytes to encrypt.
* @return Number of bytes written, signature bytes, or an error status depending on the overload.
*/
int decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext);
};
/**
* @brief AES-GCM cipher helper that encrypts and authenticates payloads.
*/
class gcm_t: public cipher_t {
public:
gcm_t() = default;
/**
* @brief Construct an AES-GCM cipher helper with key material and IV state.
*/
gcm_t(gcm_t &&) noexcept = default;
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @return Reference or value produced by the operator.
*/
gcm_t &operator=(gcm_t &&) noexcept = default;
/**
* @brief Construct an AES-GCM cipher helper with key material and IV state.
*
* @param key AES key material used to initialize the cipher.
* @param padding Whether the cipher should use block padding.
*/
gcm_t(const crypto::aes_t &key, bool padding = true);
/**
@@ -132,15 +313,40 @@
*/
int encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv);
/**
* @brief Decrypt ciphertext into the supplied plaintext buffer.
*
* @param cipher Ciphertext bytes to decrypt or output buffer for encryption.
* @param plaintext Plaintext bytes to encrypt.
* @param iv Initialization vector for the cipher operation.
* @return Number of bytes written, signature bytes, or an error status depending on the overload.
*/
int decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext, aes_t *iv);
};
/**
* @brief AES-CBC cipher helper used for block-mode encryption and decryption.
*/
class cbc_t: public cipher_t {
public:
cbc_t() = default;
/**
* @brief Construct an AES-CBC cipher helper with key material and IV state.
*/
cbc_t(cbc_t &&) noexcept = default;
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @return Reference or value produced by the operator.
*/
cbc_t &operator=(cbc_t &&) noexcept = default;
/**
* @brief Construct an AES-CBC cipher helper with key material and IV state.
*
* @param key AES key material used to initialize the cipher.
* @param padding Whether the cipher should use block padding.
*/
cbc_t(const crypto::aes_t &key, bool padding = true);
/**
src/display_device.cpp +1 −1
@@ -456,7 +456,7 @@
}
/**
* @brief Contains remapping data parsed from the string values.
* @brief Enumerates supported contains remapping data parsed from the string options.
*/
struct parsed_remapping_entry_t {
std::optional<Resolution> requested_resolution;
src/display_device.h +1 −1
@@ -99,7 +99,7 @@
void revert_configuration();
/**
* @brief Reset the persistence and currently held initial display state.
* @brief Reset persisted display state and the captured initial state.
*
* This is normally used to get out of the "broken" state where the algorithm wants
* to restore the initial display state, but it is no longer possible.
src/entry_handler.cpp +11 −3
@@ -67,8 +67,8 @@
} // namespace args
namespace lifetime {
char **argv;
std::atomic_int desired_exit_code;
char **argv; ///< Command-line argument vector.
std::atomic_int desired_exit_code; ///< Desired exit code.
void exit_sunshine(int exit_code, bool async) {
// Store the exit code of the first exit_sunshine() call
@@ -121,10 +121,14 @@
}
namespace service_ctrl {
/**
* @brief Owns Windows service-manager handles for the Sunshine service.
*/
class service_controller {
public:
/**
* @brief Constructor for service_controller class.
* @brief Open the Windows service manager and Sunshine service handle.
*
* @param service_desired_access SERVICE_* desired access flags.
*/
service_controller(DWORD service_desired_access) {
@@ -155,6 +159,8 @@
/**
* @brief Asynchronously starts the Sunshine service.
*
* @return True when the Windows service API call succeeds.
*/
bool start_service() {
if (!service_handle) {
@@ -175,6 +181,8 @@
/**
* @brief Query the service status.
* @param status The SERVICE_STATUS struct to populate.
*
* @return True when the Windows service API call succeeds.
*/
bool query_service_status(SERVICE_STATUS &status) {
if (!service_handle) {
src/entry_handler.h +16 −0
@@ -34,5 +34,7 @@
* @examples
* creds("sunshine", 2, {"new_username", "new_password"});
* @examples_end
*
* @return Process exit code from updating the stored credentials.
*/
int creds(const char *name, int argc, char *argv[]);
@@ -43,5 +45,7 @@
* @examples
* help("sunshine");
* @examples_end
*
* @return Process exit code after printing command usage.
*/
int help(const char *name);
@@ -51,5 +55,7 @@
* @examples
* version();
* @examples_end
*
* @return Process exit code after printing the Sunshine version.
*/
int version();
@@ -63,6 +69,8 @@
* @examples
* restore_nvprefs_undo();
* @examples_end
*
* @return Process exit code from restoring NVIDIA profile preferences.
*/
int restore_nvprefs_undo();
#endif
@@ -89,6 +97,8 @@
/**
* @brief Get the argv array passed to main().
*
* @return Original argument vector captured from main().
*/
char **get_argv();
} // namespace lifetime
@@ -114,5 +124,7 @@
* @examples
* is_service_running();
* @examples_end
*
* @return True when the requested service or UI readiness condition is satisfied.
*/
bool is_service_running();
@@ -122,5 +134,7 @@
* @examples
* start_service();
* @examples_end
*
* @return True when the requested service or UI readiness condition is satisfied.
*/
bool start_service();
@@ -130,6 +144,8 @@
* @examples
* wait_for_ui_ready();
* @examples_end
*
* @return True when the requested service or UI readiness condition is satisfied.
*/
bool wait_for_ui_ready();
} // namespace service_ctrl
src/globals.h +14 −10
@@ -32,6 +32,10 @@
* @brief Handles process-wide communication.
*/
namespace mail {
/**
* @def MAIL(x)
* @brief Macro for MAIL.
*/
#define MAIL(x) \
constexpr auto x = std::string_view { \
#x \
@@ -43,18 +47,18 @@
extern safe::mail_t man;
// Global mail
MAIL(shutdown);
MAIL(broadcast_shutdown);
MAIL(video_packets);
MAIL(audio_packets);
MAIL(switch_display);
MAIL(shutdown); ///< Shutdown.
MAIL(broadcast_shutdown); ///< Broadcast shutdown.
MAIL(video_packets); ///< Video packets.
MAIL(audio_packets); ///< Audio packets.
MAIL(switch_display); ///< Switch display.
// Local mail
MAIL(touch_port);
MAIL(idr);
MAIL(invalidate_ref_frames);
MAIL(gamepad_feedback);
MAIL(hdr);
MAIL(touch_port); ///< Touch port.
MAIL(idr); ///< IDR.
MAIL(invalidate_ref_frames); ///< Invalidate ref frames.
MAIL(gamepad_feedback); ///< Gamepad feedback.
MAIL(hdr); ///< HDR.
#undef MAIL
} // namespace mail
src/httpcommon.cpp +38 −2
@@ -38,11 +38,20 @@
namespace pt = boost::property_tree;
int reload_user_creds(const std::string &file);
/**
* @brief Check whether the Web UI credentials file exists and is readable.
*
* @param file Path to the credentials file.
* @return True when the credentials file is present.
*/
bool user_creds_exist(const std::string &file);
std::string unique_id; ///< Unique ID.
net::net_e origin_web_ui_allowed; ///< Origin web ui allowed.
std::string unique_id;
net::net_e origin_web_ui_allowed;
/**
* @brief Load persisted HTTP credentials and initialize shared request state.
*/
int init() {
bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE];
origin_web_ui_allowed = net::from_enum_string(config::nvhttp.origin_web_ui_allowed);
@@ -66,5 +75,14 @@
return 0;
}
/**
* @brief Save user creds.
*
* @param file Credentials file path.
* @param username Username to save.
* @param password Password to save.
* @param run_our_mouth Whether to log user-facing status messages.
* @return 0 on success, non-zero on failure.
*/
int save_user_creds(const std::string &file, const std::string &username, const std::string &password, bool run_our_mouth) {
pt::ptree outputTree;
@@ -93,6 +111,9 @@
return 0;
}
/**
* @brief Check whether the Web UI credentials file exists and is readable.
*/
bool user_creds_exist(const std::string &file) {
if (!fs::exists(file)) {
return false;
@@ -111,6 +132,9 @@
return false;
}
/**
* @brief Reload the Web UI credentials from disk.
*/
int reload_user_creds(const std::string &file) {
pt::ptree inputTree;
try {
@@ -125,6 +149,9 @@
return 0;
}
/**
* @brief Generate HTTPS credential files from the provided key and certificate paths.
*/
int create_creds(const std::string &pkey, const std::string &cert) {
fs::path pkey_path = pkey;
fs::path cert_path = cert;
@@ -176,6 +203,9 @@
return 0;
}
/**
* @brief Send a static file response for a Web UI request.
*/
bool download_file(const std::string &url, const std::string &file, long ssl_version) {
// sonar complains about weak ssl and tls versions; however sonar cannot detect the fix
CURL *curl = curl_easy_init(); // NOSONAR
@@ -212,6 +242,9 @@
return result == CURLE_OK;
}
/**
* @brief Percent-encode URL data for use in HTTP query strings.
*/
std::string url_escape(const std::string &url) {
char *string = curl_easy_escape(nullptr, url.c_str(), static_cast<int>(url.length()));
std::string result(string);
@@ -219,6 +252,9 @@
return result;
}
/**
* @brief Extract the host component from a URL string.
*/
std::string url_get_host(const std::string &url) {
CURLU *curlu = curl_url();
curl_url_set(curlu, CURLUPART_URL, url.c_str(), static_cast<unsigned int>(url.length()));
src/httpcommon.h +38 −0
@@ -13,7 +13,19 @@
namespace http {
/**
* @brief Initialize shared HTTP client state.
*
* @return 0 when HTTP state initializes successfully; nonzero on failure.
*/
int init();
/**
* @brief Generate HTTPS credential files from the provided key and certificate paths.
*
* @param pkey Private key PEM data or private key file path.
* @param cert Certificate data or object used by the operation.
* @return Created creds object or status.
*/
int create_creds(const std::string &pkey, const std::string &cert);
int save_user_creds(
const std::string &file,
@@ -22,9 +34,35 @@
bool run_our_mouth = false
);
/**
* @brief Reload Web UI user credentials from disk.
*
* @param file Destination path for the downloaded content.
* @return 0 when credentials reload successfully; nonzero on failure.
*/
int reload_user_creds(const std::string &file);
/**
* @brief Download a URL to a local file using libcurl.
*
* @param url URL used for the HTTP request.
* @param file Destination path for the downloaded content.
* @param ssl_version libcurl TLS version selector for the request.
* @return True when the file is downloaded successfully.
*/
bool download_file(const std::string &url, const std::string &file, long ssl_version = CURL_SSLVERSION_TLSv1_2);
/**
* @brief Percent-encode a string for safe inclusion in a URL.
*
* @param url URL used for the HTTP request.
* @return Percent-encoded URL component.
*/
std::string url_escape(const std::string &url);
/**
* @brief Extract the host component from a URL.
*
* @param url URL used for the HTTP request.
* @return Host name parsed from the URL, or an empty string when none is present.
*/
std::string url_get_host(const std::string &url);
extern std::string unique_id;
src/input.cpp +229 −33
@@ -30,33 +30,50 @@
// Win32 WHEEL_DELTA constant
#ifndef WHEEL_DELTA
constexpr int WHEEL_DELTA = 120;
constexpr int WHEEL_DELTA = 120; ///< Standard Windows wheel delta used to normalize scroll events.
#endif
using namespace std::literals;
namespace input {
constexpr auto MAX_GAMEPADS = std::min((std::size_t) platf::MAX_GAMEPADS, sizeof(std::int16_t) * 8); ///< Maximum gamepads representable by the active gamepad mask.
/**
* @def DISABLE_LEFT_BUTTON_DELAY
constexpr auto MAX_GAMEPADS = std::min((std::size_t) platf::MAX_GAMEPADS, sizeof(std::int16_t) * 8);
* @brief Macro for DISABLE LEFT BUTTON DELAY.
*/
#define DISABLE_LEFT_BUTTON_DELAY ((thread_pool_util::ThreadPool::task_id_t) 0x01)
/**
* @def ENABLE_LEFT_BUTTON_DELAY
* @brief Macro for ENABLE LEFT BUTTON DELAY.
*/
#define ENABLE_LEFT_BUTTON_DELAY nullptr
constexpr auto VKEY_SHIFT = 0x10; ///< Windows virtual-key code for shift.
constexpr auto VKEY_LSHIFT = 0xA0; ///< Windows virtual-key code for lshift.
constexpr auto VKEY_RSHIFT = 0xA1; ///< Windows virtual-key code for rshift.
constexpr auto VKEY_CONTROL = 0x11; ///< Windows virtual-key code for control.
constexpr auto VKEY_LCONTROL = 0xA2; ///< Windows virtual-key code for lcontrol.
constexpr auto VKEY_SHIFT = 0x10;
constexpr auto VKEY_LSHIFT = 0xA0;
constexpr auto VKEY_RSHIFT = 0xA1;
constexpr auto VKEY_CONTROL = 0x11;
constexpr auto VKEY_LCONTROL = 0xA2;
constexpr auto VKEY_RCONTROL = 0xA3;
constexpr auto VKEY_MENU = 0x12;
constexpr auto VKEY_LMENU = 0xA4;
constexpr auto VKEY_RMENU = 0xA5;
constexpr auto VKEY_RCONTROL = 0xA3; ///< Windows virtual-key code for rcontrol.
constexpr auto VKEY_MENU = 0x12; ///< Windows virtual-key code for menu.
constexpr auto VKEY_LMENU = 0xA4; ///< Windows virtual-key code for lmenu.
constexpr auto VKEY_RMENU = 0xA5; ///< Windows virtual-key code for rmenu.
/**
* @brief Enumerates supported button state options.
*/
enum class button_state_e {
NONE, ///< No button state
DOWN, ///< Button is down
UP ///< Button is up
};
/**
* @brief Allocate an available input slot identifier.
*
* @param gamepad_mask Gamepad mask.
* @return Allocated ID object, or null when unavailable.
*/
template<std::size_t N>
int alloc_id(std::bitset<N> &gamepad_mask) {
for (int x = 0; x < gamepad_mask.size(); ++x) {
@@ -69,40 +86,68 @@
return -1;
}
/**
* @brief Release ID resources.
*
* @param gamepad_mask Gamepad mask.
* @param id Identifier for the controller, session, display, or resource.
*/
template<std::size_t N>
void free_id(std::bitset<N> &gamepad_mask, int id) {
gamepad_mask[id] = false;
}
/**
* @brief Packed identifier for a pressed key and its modifier flags.
*/
typedef uint32_t key_press_id_t;
/**
* @brief Create a key-press identifier from the virtual-key code and flags.
*
* @param vk Virtual-key code from the client input packet.
* @param flags Bit flags that modify the requested operation.
* @return Constructed kpid object.
*/
key_press_id_t make_kpid(uint16_t vk, uint8_t flags) {
return (key_press_id_t) vk << 8 | flags;
}
/**
* @brief Extract the virtual-key code from a packed key-press identifier.
*
* @param kpid Key-press identifier containing the virtual-key code and flags.
* @return Virtual-key code stored in the high byte.
*/
uint16_t vk_from_kpid(key_press_id_t kpid) {
return kpid >> 8;
}
/**
* @brief Extract the modifier flags from a packed key-press identifier.
*
* @param kpid Key-press identifier containing the virtual-key code and flags.
* @return Modifier flags stored in the low byte.
*/
uint8_t flags_from_kpid(key_press_id_t kpid) {
return kpid & 0xFF;
}
/**
* @brief Convert a little-endian netfloat to a native endianness float.
* @param f Little-endian network float bytes.
* @return Floating-point value decoded for the host CPU.
* @param f Netfloat value.
* @return The native endianness float value.
*/
float from_netfloat(netfloat f) {
return boost::endian::endian_load<float, sizeof(float), boost::endian::order::little>(f);
}
/**
* @brief Convert a little-endian netfloat to a native endianness float and clamps it.
* @param f Netfloat value.
* @brief Convert a little-endian netfloat to a native float and clamp it to a range.
* @param f Little-endian network float bytes.
* @param min The minimium value for clamping.
* @param max The maximum value for clamping.
* @return Clamped native endianess float value.
* @return Decoded floating-point value clamped between min and max.
*/
float from_clamped_netfloat(netfloat f, float min, float max) {
return std::clamp(from_netfloat(f), min, max);
@@ -115,6 +160,12 @@
static platf::input_t platf_input;
static std::bitset<platf::MAX_GAMEPADS> gamepadMask {};
/**
* @brief Release all platform resources associated with a virtual gamepad.
*
* @param platf_input Platf input.
* @param id Identifier for the controller, session, display, or resource.
*/
void free_gamepad(platf::input_t &platf_input, int id) {
platf::gamepad_update(platf_input, id, platf::gamepad_state_t {});
platf::free_gamepad(platf_input, id);
@@ -122,6 +173,9 @@
free_id(gamepadMask, id);
}
/**
* @brief Per-client gamepad slot and feedback state.
*/
struct gamepad_t {
gamepad_t():
gamepad_state {},
@@ -138,21 +192,27 @@
}
}
platf::gamepad_state_t gamepad_state;
platf::gamepad_state_t gamepad_state; ///< Gamepad state.
thread_pool_util::ThreadPool::task_id_t back_timeout_id;
thread_pool_util::ThreadPool::task_id_t back_timeout_id; ///< Back timeout ID.
int id;
int id; ///< Global gamepad slot assigned to this client controller.
// When emulating the HOME button, we may need to artificially release the back button.
// Afterwards, the gamepad state on sunshine won't match the state on Moonlight.
// To prevent Sunshine from sending erroneous input data to the active application,
// Sunshine forces the button to be in a specific state until the gamepad state matches that of
// Moonlight once more.
button_state_e back_button_state;
button_state_e back_button_state; ///< Back button state.
};
/**
* @brief Input emulation settings loaded from configuration.
*/
struct input_t {
/**
* @brief Enumerates supported shortkey options.
*/
enum shortkey_e {
CTRL = 0x1, ///< Control key
ALT = 0x2, ///< Alt key
@@ -160,6 +220,12 @@
SHORTCUT = CTRL | ALT | SHIFT ///< Shortcut combination
};
/**
* @brief Construct input state from the mailbox and platform backend.
*
* @param touch_port_event Event carrying the active touch port.
* @param feedback_queue Queue used for controller feedback.
*/
input_t(
safe::mail_raw_t::event_t<input::touch_port_t> touch_port_event,
platf::feedback_queue_t feedback_queue
@@ -176,26 +242,26 @@
}
// Keep track of alt+ctrl+shift key combo
int shortcutFlags;
int shortcutFlags; ///< Shortcut flags.
bool left_alt_pressed = false;
bool right_alt_pressed = false;
bool left_alt_pressed = false; ///< Tracks whether the left Alt key is currently pressed.
bool right_alt_pressed = false; ///< Tracks whether the right Alt key is currently pressed.
std::vector<gamepad_t> gamepads;
std::unique_ptr<platf::client_input_t> client_context;
std::vector<gamepad_t> gamepads; ///< Virtual gamepad slots tracked for the stream.
std::unique_ptr<platf::client_input_t> client_context; ///< Client context.
safe::mail_raw_t::event_t<input::touch_port_t> touch_port_event;
platf::feedback_queue_t feedback_queue;
safe::mail_raw_t::event_t<input::touch_port_t> touch_port_event; ///< Touch port event.
platf::feedback_queue_t feedback_queue; ///< Queue used to deliver controller feedback to the platform backend.
std::list<std::vector<uint8_t>> input_queue;
std::mutex input_queue_lock;
std::list<std::vector<uint8_t>> input_queue; ///< Pending raw input packets waiting for processing.
std::mutex input_queue_lock; ///< Input queue lock.
thread_pool_util::ThreadPool::task_id_t mouse_left_button_timeout;
thread_pool_util::ThreadPool::task_id_t mouse_left_button_timeout; ///< Mouse left button timeout.
input::touch_port_t touch_port;
input::touch_port_t touch_port; ///< Touch coordinate bounds for the current stream.
int32_t accumulated_vscroll_delta;
int32_t accumulated_hscroll_delta;
int32_t accumulated_vscroll_delta; ///< Accumulated vscroll delta.
int32_t accumulated_hscroll_delta; ///< Accumulated hscroll delta.
};
/**
@@ -223,6 +289,11 @@
return 0;
}
/**
* @brief Write a debug log representation of the input packet.
*
* @param packet Protocol packet being processed.
*/
void print(PNV_REL_MOUSE_MOVE_PACKET packet) {
BOOST_LOG(debug)
<< "--begin relative mouse move packet--"sv << std::endl
@@ -231,6 +302,11 @@
<< "--end relative mouse move packet--"sv;
}
/**
* @brief Write a debug log representation of the input packet.
*
* @param packet Protocol packet being processed.
*/
void print(PNV_ABS_MOUSE_MOVE_PACKET packet) {
BOOST_LOG(debug)
<< "--begin absolute mouse move packet--"sv << std::endl
@@ -241,6 +317,11 @@
<< "--end absolute mouse move packet--"sv;
}
/**
* @brief Write a debug log representation of the input packet.
*
* @param packet Protocol packet being processed.
*/
void print(PNV_MOUSE_BUTTON_PACKET packet) {
BOOST_LOG(debug)
<< "--begin mouse button packet--"sv << std::endl
@@ -249,6 +330,11 @@
<< "--end mouse button packet--"sv;
}
/**
* @brief Write a debug log representation of the input packet.
*
* @param packet Protocol packet being processed.
*/
void print(PNV_SCROLL_PACKET packet) {
BOOST_LOG(debug)
<< "--begin mouse scroll packet--"sv << std::endl
@@ -256,6 +342,11 @@
<< "--end mouse scroll packet--"sv;
}
/**
* @brief Write a debug log representation of the input packet.
*
* @param packet Protocol packet being processed.
*/
void print(PSS_HSCROLL_PACKET packet) {
BOOST_LOG(debug)
<< "--begin mouse hscroll packet--"sv << std::endl
@@ -263,6 +354,11 @@
<< "--end mouse hscroll packet--"sv;
}
/**
* @brief Write a debug log representation of the input packet.
*
* @param packet Protocol packet being processed.
*/
void print(PNV_KEYBOARD_PACKET packet) {
BOOST_LOG(debug)
<< "--begin keyboard packet--"sv << std::endl
@@ -273,6 +369,11 @@
<< "--end keyboard packet--"sv;
}
/**
* @brief Write a debug log representation of the input packet.
*
* @param packet Protocol packet being processed.
*/
void print(PNV_UNICODE_PACKET packet) {
std::string text(packet->text, util::endian::big(packet->header.size) - sizeof(packet->header.magic));
BOOST_LOG(debug)
@@ -281,6 +382,11 @@
<< "--end unicode packet--"sv;
}
/**
* @brief Write a debug log representation of the input packet.
*
* @param packet Protocol packet being processed.
*/
void print(PNV_MULTI_CONTROLLER_PACKET packet) {
// Moonlight spams controller packet even when not necessary
BOOST_LOG(verbose)
@@ -393,5 +499,8 @@
<< "--end controller battery packet--"sv;
}
/**
* @brief Write a debug log representation of the input packet.
*/
void print(void *payload) {
auto header = (PNV_INPUT_HEADER) payload;
@@ -444,6 +553,12 @@
}
}
/**
* @brief Forward a client input packet directly to the platform backend.
*
* @param input Platform input backend that receives the event.
* @param packet Protocol packet being processed.
*/
void passthrough(std::shared_ptr<input_t> &input, PNV_REL_MOUSE_MOVE_PACKET packet) {
if (!config::input.mouse) {
return;
@@ -541,6 +656,12 @@
return {multiply_polar_by_cartesian_scalar(major, angle, scalar), multiply_polar_by_cartesian_scalar(minor, angle + (M_PI / 2), scalar)};
}
/**
* @brief Forward a client input packet directly to the platform backend.
*
* @param input Platform input backend that receives the event.
* @param packet Protocol packet being processed.
*/
void passthrough(std::shared_ptr<input_t> &input, PNV_ABS_MOUSE_MOVE_PACKET packet) {
if (!config::input.mouse) {
return;
@@ -591,6 +712,12 @@
platf::abs_mouse(platf_input, abs_port, tpcoords->first, tpcoords->second);
}
/**
* @brief Called to pass a mouse button message to the platform backend.
*
* @param input The input context pointer.
* @param packet The mouse button packet.
*/
void passthrough(std::shared_ptr<input_t> &input, PNV_MOUSE_BUTTON_PACKET packet) {
if (!config::input.mouse) {
return;
@@ -652,6 +779,12 @@
platf::button_mouse(platf_input, button, release);
}
/**
* @brief Apply configured keybinding remaps to a platform keycode.
*
* @param keycode Platform keycode being translated or emitted.
* @return Remapped keycode when configured, otherwise the original keycode.
*/
short map_keycode(short keycode) {
auto it = config::input.keybindings.find(keycode);
if (it != std::end(config::input.keybindings)) {
@@ -663,6 +796,10 @@
/**
* @brief Update flags for keyboard shortcut combo's
*
* @param flags Bit flags that modify the requested operation.
* @param keyCode Moonlight keyboard packet key code.
* @param release Whether the key or button event is a release.
*/
inline void update_shortcutFlags(int *flags, short keyCode, bool release) {
switch (keyCode) {
@@ -696,6 +833,12 @@
}
}
/**
* @brief Check whether modifier.
*
* @param keyCode Moonlight keyboard packet key code.
* @return True when the key code is a keyboard modifier.
*/
bool is_modifier(uint16_t keyCode) {
switch (keyCode) {
case VKEY_SHIFT:
@@ -713,6 +856,14 @@
}
}
/**
* @brief Send key and modifiers.
*
* @param key_code Moonlight keyboard packet key code.
* @param release Whether the key or button event is a release.
* @param flags Bit flags that modify the requested operation.
* @param synthetic_modifiers Synthetic modifiers.
*/
void send_key_and_modifiers(uint16_t key_code, bool release, uint8_t flags, uint8_t synthetic_modifiers) {
if (!release) {
// Press any synthetic modifiers required for this key
@@ -743,6 +894,13 @@
}
}
/**
* @brief Re-emit a held key until its repeat task is cancelled.
*
* @param key_code Moonlight keyboard packet key code.
* @param flags Bit flags that modify the requested operation.
* @param synthetic_modifiers Synthetic modifiers.
*/
void repeat_key(uint16_t key_code, uint8_t flags, uint8_t synthetic_modifiers) {
// If key no longer pressed, stop repeating
if (!key_press[make_kpid(key_code, flags)]) {
@@ -755,6 +913,12 @@
key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_period, key_code, flags, synthetic_modifiers).task_id;
}
/**
* @brief Forward a client input packet directly to the platform backend.
*
* @param input Platform input backend that receives the event.
* @param packet Protocol packet being processed.
*/
void passthrough(std::shared_ptr<input_t> &input, PNV_KEYBOARD_PACKET packet) {
if (!config::input.keyboard) {
return;
@@ -869,6 +1033,11 @@
}
}
/**
* @brief Forward a client input packet directly to the platform backend.
*
* @param packet Protocol packet being processed.
*/
void passthrough(PNV_UNICODE_PACKET packet) {
if (!config::input.keyboard) {
return;
@@ -1142,6 +1311,12 @@
platf::gamepad_battery(platf_input, battery);
}
/**
* @brief Forward a client input packet directly to the platform backend.
*
* @param input Platform input backend that receives the event.
* @param packet Protocol packet being processed.
*/
void passthrough(std::shared_ptr<input_t> &input, PNV_MULTI_CONTROLLER_PACKET packet) {
if (!config::input.controller) {
return;
@@ -1257,6 +1432,9 @@
gamepad.gamepad_state = gamepad_state;
}
/**
* @brief Enumerates supported batch result options.
*/
enum class batch_result_e {
batched, ///< This entry was batched with the source entry
not_batchable, ///< Not eligible to batch but continue attempts to batch
@@ -1649,6 +1827,9 @@
task_pool.push(passthrough_next_message, input);
}
/**
* @brief Reset the object to its initial empty state.
*/
void reset(std::shared_ptr<input_t> &input) {
task_pool.cancel(key_press_repeat_id);
task_pool.cancel(input->mouse_left_button_timeout);
@@ -1673,19 +1854,31 @@
});
}
/**
* @brief RAII helper that runs shutdown cleanup when destroyed.
*/
class deinit_t: public platf::deinit_t {
public:
/**
* @brief Destroy the input subsystem deinitializer.
*/
~deinit_t() override {
platf_input.reset();
}
};
/**
* @brief Initialize the platform input backend.
*/
[[nodiscard]] std::unique_ptr<platf::deinit_t> init() {
platf_input = platf::input();
return std::make_unique<deinit_t>();
}
/**
* @brief Probe connected gamepads and update input capability state.
*/
bool probe_gamepads() {
auto input = static_cast<platf::input_t *>(platf_input.get());
const auto gamepads = platf::supported_gamepads(input);
@@ -1697,6 +1890,9 @@
return true;
}
/**
* @brief Allocate and initialize platform input state for a stream.
*/
std::shared_ptr<input_t> alloc(safe::mail_t mail) {
auto input = std::make_shared<input_t>(
mail->event<input::touch_port_t>(mail::touch_port),
src/input.h +44 −8
@@ -14,30 +14,66 @@
namespace input {
struct input_t;
/**
* @brief Write a debug log representation of the input packet.
*
* @param input Raw input packet to format for logging.
*/
void print(void *input);
/**
* @brief Reset stream input state after a client disconnect or shutdown.
*
* @param input Shared stream input state to reset.
*/
void reset(std::shared_ptr<input_t> &input);
/**
* @brief Queue a raw input message for platform passthrough.
*/
void passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data);
/**
* @brief Initialize global input resources and platform backends.
*
* @return Cleanup handle for initialized input resources, or null if none are required.
*/
[[nodiscard]] std::unique_ptr<platf::deinit_t> init();
/**
* @brief Probe whether the platform can create virtual gamepads.
*
* @return True when at least one configured gamepad backend is available.
*/
bool probe_gamepads();
/**
* @brief Allocate and initialize platform input state for a stream.
*
* @param mail Mailbox used to exchange messages with worker threads.
* @return Shared input state bound to the stream mailbox.
*/
std::shared_ptr<input_t> alloc(safe::mail_t mail);
/**
* @brief Touchscreen coordinate bounds used to scale absolute input.
*/
struct touch_port_t: public platf::touch_port_t {
int env_width;
int env_height;
int env_width; ///< Width of the full capture environment in physical pixels.
int env_height; ///< Height of the full capture environment in physical pixels.
// Offset x and y coordinates of the client
float client_offsetX;
float client_offsetY;
float client_offsetX; ///< Horizontal client viewport offset used when scaling touch input.
float client_offsetY; ///< Vertical client viewport offset used when scaling touch input.
float scalar_inv; ///< Inverse scale factor from client coordinates to display coordinates.
float scalar_tpcoords; ///< Scale factor from client coordinates to touch-port coordinates.
float scalar_inv;
float scalar_tpcoords;
int env_logical_width; ///< Width of the full capture environment after display scaling.
int env_logical_width;
int env_logical_height;
int env_logical_height; ///< Height of the full capture environment after display scaling.
/**
* @brief Check whether the touch-port bounds are initialized.
*/
explicit operator bool() const {
return width != 0 && height != 0 && env_width != 0 && env_height != 0;
}
src/logging.cpp +11 −8
@@ -35,16 +35,16 @@
namespace bl = boost::log;
boost::shared_ptr<boost::log::sinks::asynchronous_sink<boost::log::sinks::text_ostream_backend>> sink;
boost::shared_ptr<boost::log::sinks::asynchronous_sink<boost::log::sinks::text_ostream_backend>> sink; ///< Sink.
bl::sources::severity_logger<int> verbose {0}; ///< Dominating output.
bl::sources::severity_logger<int> verbose(0); // Dominating output
bl::sources::severity_logger<int> debug(1); // Follow what is happening
bl::sources::severity_logger<int> debug {1}; ///< Follow what is happening.
bl::sources::severity_logger<int> info {2}; ///< Should be informed about.
bl::sources::severity_logger<int> warning {3}; ///< Strange events.
bl::sources::severity_logger<int> error {4}; ///< Recoverable errors.
bl::sources::severity_logger<int> fatal {5}; ///< Unrecoverable errors.
bl::sources::severity_logger<int> info(2); // Should be informed about
bl::sources::severity_logger<int> warning(3); // Strange events
bl::sources::severity_logger<int> error(4); // Recoverable errors
bl::sources::severity_logger<int> fatal(5); // Unrecoverable errors
#ifdef SUNSHINE_TESTS
bl::sources::severity_logger<int> tests {10}; ///< Automatic tests output.
bl::sources::severity_logger<int> tests(10); // Automatic tests output
#endif
BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", int)
@@ -60,6 +60,9 @@
sink.reset();
}
/**
* @brief Format a Boost.Log record for Sunshine log output.
*/
void formatter(const boost::log::record_view &view, boost::log::formatting_ostream &os) {
constexpr const char *message = "Message";
constexpr const char *severity = "Severity";
src/logging.h +76 −7
@@ -8,14 +8,17 @@
#include <boost/log/common.hpp>
#include <boost/log/sinks.hpp>
/**
* @brief Boost.Log asynchronous text sink used by Sunshine logging.
*/
using text_sink = boost::log::sinks::asynchronous_sink<boost::log::sinks::text_ostream_backend>;
extern boost::log::sources::severity_logger<int> verbose; ///< Verbose.
extern boost::log::sources::severity_logger<int> debug; ///< Debug.
extern boost::log::sources::severity_logger<int> info; ///< Info.
extern boost::log::sources::severity_logger<int> verbose;
extern boost::log::sources::severity_logger<int> debug;
extern boost::log::sources::severity_logger<int> warning; ///< Warning.
extern boost::log::sources::severity_logger<int> error; ///< Error.
extern boost::log::sources::severity_logger<int> fatal; ///< Fatal.
extern boost::log::sources::severity_logger<int> info;
extern boost::log::sources::severity_logger<int> warning;
extern boost::log::sources::severity_logger<int> error;
extern boost::log::sources::severity_logger<int> fatal;
#ifdef SUNSHINE_TESTS
extern boost::log::sources::severity_logger<int> tests;
#endif
@@ -27,10 +30,13 @@
* @brief Handles the initialization and deinitialization of the logging system.
*/
namespace logging {
/**
* @brief RAII helper that runs shutdown cleanup when destroyed.
*/
class deinit_t {
public:
/**
* @brief Restores logging state when the logging subsystem shuts down.
* @brief A destructor that restores the initial state.
*/
~deinit_t();
};
@@ -43,6 +49,12 @@
*/
void deinit();
/**
* @brief Format a Boost.Log record for Sunshine log output.
*
* @param view Boost.Log record view being formatted.
* @param os Boost.Log output stream receiving formatted text.
*/
void formatter(const boost::log::record_view &view, boost::log::formatting_ostream &os);
/**
@@ -101,6 +113,14 @@
template<typename T>
class min_max_avg_periodic_logger {
public:
/**
* @brief Construct a periodic logger that reports min, max, and average samples.
*
* @param severity Severity level associated with the log message.
* @param message Message text to log or report.
* @param units Unit label appended to tracked numeric values.
* @param interval_in_seconds Interval in seconds.
*/
min_max_avg_periodic_logger(boost::log::sources::severity_logger<int> &severity, std::string_view message, std::string_view units, std::chrono::seconds interval_in_seconds = std::chrono::seconds(20)):
severity(severity),
message(message),
@@ -109,6 +129,11 @@
enabled(config::sunshine.min_log_level <= severity.default_severity()) {
}
/**
* @brief Collect a metric sample and write it to the periodic log when due.
*
* @param value Numeric sample to include in the next periodic aggregate.
*/
void collect_and_log(const T &value) {
if (enabled) {
auto print_info = [&](const T &min_value, const T &max_value, double avg_value) {
@@ -123,18 +148,31 @@
}
}
/**
* @brief Collect a metric sample and write it to the periodic log when due.
*
* @param func Callable used to calculate the tracked sample value.
*/
void collect_and_log(std::function<T()> func) {
if (enabled) {
collect_and_log(func());
}
}
/**
* @brief Reset the object to its initial empty state.
*/
void reset() {
if (enabled) {
tracker.reset();
}
}
/**
* @brief Check whether this periodic logger should emit at the configured severity.
*
* @return True when the logger severity is enabled and sampling should continue.
*/
bool is_enabled() const {
return enabled;
}
@@ -165,40 +203,71 @@
*/
class time_delta_periodic_logger {
public:
/**
* @brief Construct a periodic logger for elapsed-time samples.
*
* @param severity Severity level associated with the log message.
* @param message Message text to log or report.
* @param interval_in_seconds Interval in seconds.
*/
time_delta_periodic_logger(boost::log::sources::severity_logger<int> &severity, std::string_view message, std::chrono::seconds interval_in_seconds = std::chrono::seconds(20)):
logger(severity, message, "ms", interval_in_seconds) {
}
/**
* @brief Store the first timestamp for a measured interval.
*
* @param point Time point used for elapsed-time calculations.
*/
void first_point(const std::chrono::steady_clock::time_point &point) {
if (logger.is_enabled()) {
point1 = point;
}
}
/**
* @brief Store the current time as the first timestamp.
*/
void first_point_now() {
if (logger.is_enabled()) {
first_point(std::chrono::steady_clock::now());
}
}
/**
* @brief Store the second timestamp and log the elapsed interval.
*
* @param point Time point used for elapsed-time calculations.
*/
void second_point_and_log(const std::chrono::steady_clock::time_point &point) {
if (logger.is_enabled()) {
logger.collect_and_log(std::chrono::duration<double, std::milli>(point - point1).count());
}
}
/**
* @brief Store the current time as the second timestamp and log the elapsed interval.
*/
void second_point_now_and_log() {
if (logger.is_enabled()) {
second_point_and_log(std::chrono::steady_clock::now());
}
}
/**
* @brief Reset the object to its initial empty state.
*/
void reset() {
if (logger.is_enabled()) {
logger.reset();
}
}
/**
* @brief Check whether this periodic logger should emit at the configured severity.
*
* @return True when the logger severity is enabled and sampling should continue.
*/
bool is_enabled() const {
return logger.is_enabled();
}
src/main.cpp +43 −2
@@ -33,12 +33,23 @@
using namespace std::literals;
std::map<int, std::function<void()>> signal_handlers;
std::map<int, std::function<void()>> signal_handlers; ///< Signal handlers.
/**
* @brief Forward a POSIX signal to the registered Sunshine handler.
*
* @param sig Native signal number being handled.
*/
void on_signal_forwarder(int sig) {
signal_handlers.at(sig)();
}
/**
* @brief Register the handler invoked for a POSIX signal.
*
* @param sig Native signal number being handled.
* @param fn Signal handler function to install.
*/
template<class FN>
void on_signal(int sig, FN &&fn) {
signal_handlers.emplace(sig, std::forward<FN>(fn));
@@ -46,6 +57,9 @@
std::signal(sig, on_signal_forwarder);
}
/**
* @brief Cmd to func.
*/
std::map<std::string_view, std::function<int(const char *name, int argc, char **argv)>> cmd_to_func {
{"creds"sv, [](const char *name, int argc, char **argv) {
return args::creds(name, argc, argv);
@@ -64,6 +78,15 @@
};
#ifdef _WIN32
/**
* @brief Handle Windows session-change messages for the monitor window.
*
* @param hwnd Window handle receiving the Windows control event.
* @param uMsg U msg.
* @param wParam W param.
* @param lParam L param.
* @return Process or platform callback exit code.
*/
LRESULT CALLBACK SessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_CLOSE:
@@ -84,6 +107,12 @@
}
}
/**
* @brief Handle Windows console control events for shutdown.
*
* @param type Protocol, message, or resource type selector.
* @return Process or platform callback exit code.
*/
WINAPI BOOL ConsoleCtrlHandler(DWORD type) {
if (type == CTRL_CLOSE_EVENT) {
BOOST_LOG(info) << "Console closed handler called";
@@ -94,10 +123,15 @@
#endif
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
constexpr bool tray_is_enabled = true; ///< Compile-time flag indicating tray support is enabled.
constexpr bool tray_is_enabled = true;
#else
constexpr bool tray_is_enabled = false;
#endif
/**
* @brief Run the main event loop until Sunshine is asked to exit.
*
* @param shutdown_event Shutdown event.
*/
void mainThreadLoop(const std::shared_ptr<safe::event_t<bool>> &shutdown_event) {
bool run_loop = false;
@@ -122,6 +156,13 @@
BOOST_LOG(info) << "Main loop has exited"sv;
}
/**
* @brief Run the main application or worker loop.
*
* @param argc The number of arguments.
* @param argv The arguments.
* @return Process or platform callback exit code.
*/
int main(int argc, char *argv[]) {
#ifdef __APPLE__
// Bundle assets are referenced relative to the executable
src/main.h +2 −2
@@ -6,10 +6,10 @@
/**
* @brief Main application entry point.
* @param argc The number of arguments.
* @param argv The arguments.
* @examples
* main(1, const char* args[] = {"sunshine", nullptr});
* @examples_end
*
* @return Process or platform callback exit code.
*/
int main(int argc, char *argv[]);
src/move_by_copy.h +45 −0
@@ -18,41 +18,86 @@
template<class T>
class MoveByCopy {
public:
/**
* @brief Wrapped type moved through copy-shaped APIs.
*/
typedef T move_type;
private:
move_type _to_move;
public:
/**
* @brief Store a move-only value for transfer through copy-only call sites.
*
* @param to_move Object whose ownership is moved into this wrapper.
*/
explicit MoveByCopy(move_type &&to_move):
_to_move(std::move(to_move)) {
}
/**
* @brief Move the stored object from another wrapper.
*
* @param other Wrapper whose stored object is moved into this object.
*/
MoveByCopy(MoveByCopy &&other) = default;
/**
* @brief Copy by moving the stored object out of another wrapper.
*
* @param other Wrapper whose stored object will be moved despite the copy signature.
*/
MoveByCopy(const MoveByCopy &other) {
*this = other;
}
/**
* @brief Move-assign the wrapped object from another wrapper.
*
* @param other Source object whose state is copied or moved into this object.
* @return Reference to this wrapper.
*/
MoveByCopy &operator=(MoveByCopy &&other) = default;
/**
* @brief Copy-assign by moving the wrapped object out of the source wrapper.
*
* @param other Source object whose state is copied or moved into this object.
* @return Reference to this wrapper.
*/
MoveByCopy &operator=(const MoveByCopy &other) {
this->_to_move = std::move(const_cast<MoveByCopy &>(other)._to_move);
return *this;
}
/**
* @brief Move the wrapped object out of this helper.
*/
operator move_type() {
return std::move(_to_move);
}
};
/**
* @brief Copy a move-only value by moving from the source reference.
*
* @param movable Move-only object to transfer through a copy-shaped API.
* @return Wrapper that moves the object when copied.
*/
template<class T>
MoveByCopy<T> cmove(T &movable) {
return MoveByCopy<T>(std::move(movable));
}
// Do NOT use this unless you are absolutely certain the object to be moved is no longer used by the caller
/**
* @brief Copy-shape wrapper for moving from a const reference.
*
* @param movable Move-only object to transfer through a copy-shaped API.
* @return Wrapper that moves the object when copied.
*/
template<class T>
MoveByCopy<T> const_cmove(const T &movable) {
return MoveByCopy<T>(std::move(const_cast<T &>(movable)));
src/network.cpp +27 −0
@@ -17,9 +17,15 @@
namespace ip = boost::asio::ip;
namespace net {
/**
* @brief Pc ips v4.
*/
std::vector<ip::network_v4> pc_ips_v4 {
ip::make_network_v4("127.0.0.0/8"sv),
};
/**
* @brief Lan ips v4.
*/
std::vector<ip::network_v4> lan_ips_v4 {
ip::make_network_v4("192.168.0.0/16"sv),
ip::make_network_v4("172.16.0.0/12"sv),
@@ -28,14 +34,23 @@
ip::make_network_v4("169.254.0.0/16"sv),
};
/**
* @brief Pc ips v6.
*/
std::vector<ip::network_v6> pc_ips_v6 {
ip::make_network_v6("::1/128"sv),
};
/**
* @brief Lan ips v6.
*/
std::vector<ip::network_v6> lan_ips_v6 {
ip::make_network_v6("fc00::/7"sv),
ip::make_network_v6("fe80::/64"sv),
};
/**
* @brief Convert configuration text to a network enum value.
*/
net_e from_enum_string(const std::string_view &view) {
if (view == "wan") {
return WAN;
@@ -47,5 +62,8 @@
return PC;
}
/**
* @brief Convert a Boost address family to Sunshine network enum value.
*/
net_e from_address(const std::string_view &view) {
auto addr = normalize_address(ip::make_address(view));
@@ -79,6 +97,9 @@
return WAN;
}
/**
* @brief Convert a network enum value to configuration text.
*/
std::string_view to_enum_string(net_e net) {
switch (net) {
case PC:
@@ -163,6 +184,9 @@
}
}
/**
* @brief Create an ENet host with the requested address family.
*/
host_t host_create(af_e af, ENetAddress &addr, std::uint16_t port) {
static std::once_flag enet_init_flag;
std::call_once(enet_init_flag, []() {
@@ -182,6 +206,9 @@
return host;
}
/**
* @brief Destroy an ENet host allocated by host_create().
*/
void free_host(ENetHost *host) {
std::for_each(host->peers, host->peers + host->peerCount, [](ENetPeer &peer_ref) {
ENetPeer *peer = &peer_ref;
src/network.h +48 −2
@@ -16,6 +16,11 @@
#include "utility.h"
namespace net {
/**
* @brief Destroy an ENet host allocated by host_create().
*
* @param host Host name or address to resolve.
*/
void free_host(ENetHost *host);
/**
@@ -29,31 +34,72 @@
*/
std::uint16_t map_port(int port);
/**
* @brief Owning ENet host pointer released with `enet_host_destroy`.
*/
using host_t = util::safe_ptr<ENetHost, free_host>;
/**
* @brief Raw ENet peer handle owned by an ENet host.
*/
using peer_t = ENetPeer *;
/**
* @brief Owning ENet packet pointer released with `enet_packet_destroy`.
*/
using packet_t = util::safe_ptr<ENetPacket, enet_packet_destroy>;
/**
* @brief Enumerates supported net options.
*/
enum net_e : int {
PC, ///< PC
LAN, ///< LAN
WAN ///< WAN
};
/**
* @brief Enumerates supported af options.
*/
enum af_e : int {
IPV4, ///< IPv4 only
BOTH ///< IPv4 and IPv6
};
/**
* @brief Convert configuration text to a network enum value.
*
* @param view Boost.Log record view being formatted.
* @return Value converted from enum string.
*/
net_e from_enum_string(const std::string_view &view);
/**
* @brief Convert a network enum value to configuration text.
*
* @param net Network scope to convert or format.
* @return Value converted to enum string.
*/
std::string_view to_enum_string(net_e net);
/**
* @brief Convert a Boost address family to Sunshine network enum value.
*
* @param view Boost.Log record view being formatted.
* @return Value converted from address.
*/
net_e from_address(const std::string_view &view);
/**
* @brief Create an ENet host with the requested address family.
*
* @param af Address family used for socket creation or binding.
* @param addr Network address to bind, parse, or format.
* @param port TCP or UDP port number.
* @return ENet host bound to the requested address and port, or an empty handle on failure.
*/
host_t host_create(af_e af, ENetAddress &addr, std::uint16_t port);
/**
* @brief Get the address family enum value from a string.
* @brief Convert a config address-family string to the matching enum.
* @param view The config option value.
* @return Address-family enum represented by the string.
* @return The address family enum value.
*/
af_e af_from_enum_string(const std::string_view &view);
src/nvenc/nvenc_base.cpp +9 −3
@@ -13,6 +13,10 @@
#include "src/logging.h"
#include "src/utility.h"
/**
* @def MAKE_NVENC_VER(major, minor)
* @brief Macro for MAKE NVENC VER.
*/
#define MAKE_NVENC_VER(major, minor) ((major) | ((minor) << 24))
// Make sure we check backwards compatibility when bumping the Video Codec SDK version
@@ -659,9 +663,11 @@
bool nvenc_base::nvenc_failed(NVENCSTATUS status) {
auto status_string = [](NVENCSTATUS status) -> std::string {
switch (status) {
#define nvenc_status_case(x) \
case x: \
return #x;
#ifndef DOXYGEN
#define nvenc_status_case(x) \
case x: \
return #x;
#endif
nvenc_status_case(NV_ENC_SUCCESS);
nvenc_status_case(NV_ENC_ERR_NO_ENCODE_DEVICE);
nvenc_status_case(NV_ENC_ERR_UNSUPPORTED_DEVICE);
src/nvenc/nvenc_base.h +10 −4
@@ -104,11 +104,17 @@
return false;
}
/**
* @brief Check whether an NVENC API status represents failure.
*
* @param status Native status code returned by the platform API.
* @return True when the status is an NVENC error code.
*/
bool nvenc_failed(NVENCSTATUS status);
const NV_ENC_DEVICE_TYPE device_type;
const NV_ENC_DEVICE_TYPE device_type; ///< NVENC device backend used by this encoder instance.
void *encoder = nullptr;
void *encoder = nullptr; ///< Opaque NVENC encoder session handle returned by the driver.
struct {
uint32_t width = 0;
@@ -116,9 +122,9 @@
NV_ENC_BUFFER_FORMAT buffer_format = NV_ENC_BUFFER_FORMAT_UNDEFINED;
uint32_t ref_frames_in_dpb = 0;
bool rfi = false;
} encoder_params; ///< Current encoder dimensions, pixel format, and reference-frame settings.
} encoder_params;
std::string last_nvenc_error_string; ///< Last NVENC error string.
std::string last_nvenc_error_string;
// Derived classes set these variables
void *device = nullptr; ///< Platform-specific handle of encoding device.
src/nvenc/nvenc_colorspace.h +4 −4
@@ -13,10 +13,10 @@
* @brief YUV colorspace and color range.
*/
struct nvenc_colorspace_t {
NV_ENC_VUI_COLOR_PRIMARIES primaries;
NV_ENC_VUI_TRANSFER_CHARACTERISTIC tranfer_function;
NV_ENC_VUI_MATRIX_COEFFS matrix;
bool full_range;
NV_ENC_VUI_COLOR_PRIMARIES primaries; ///< Color primaries written into NVENC VUI metadata.
NV_ENC_VUI_TRANSFER_CHARACTERISTIC tranfer_function; ///< Transfer function written into NVENC VUI metadata.
NV_ENC_VUI_MATRIX_COEFFS matrix; ///< YUV matrix coefficients written into NVENC VUI metadata.
bool full_range; ///< Whether the video range is full-range instead of limited.
};
} // namespace nvenc
src/nvenc/nvenc_config.h +18 −12
@@ -6,12 +6,18 @@
namespace nvenc {
/**
* @brief Enumerates supported nVENC two pass options.
*/
enum class nvenc_two_pass {
disabled, ///< Single pass, the fastest and no extra vram
quarter_resolution, ///< Larger motion vectors being caught, faster and uses less extra vram
full_resolution, ///< Better overall statistics, slower and uses more extra vram
};
/**
* @brief Enumerates supported nVENC split frame encoding options.
*/
enum class nvenc_split_frame_encoding {
disabled, ///< Disable
driver_decides, ///< Let driver decide
@@ -23,40 +29,40 @@
*/
struct nvenc_config {
// Quality preset from 1 to 7, higher is slower
int quality_preset = 1;
int quality_preset = 1; ///< Quality preset.
// Use optional preliminary pass for better motion vectors, bitrate distribution and stricter VBV(HRD), uses CUDA cores
nvenc_two_pass two_pass = nvenc_two_pass::quarter_resolution;
nvenc_two_pass two_pass = nvenc_two_pass::quarter_resolution; ///< Two pass.
// Percentage increase of VBV/HRD from the default single frame, allows low-latency variable bitrate
int vbv_percentage_increase = 0;
int vbv_percentage_increase = 0; ///< Vbv percentage increase.
// Improves fades compression, uses CUDA cores
bool weighted_prediction = false;
bool weighted_prediction = false; ///< Enable weighted prediction for NVENC.
// Allocate more bitrate to flat regions since they're visually more perceptible, uses CUDA cores
bool adaptive_quantization = false;
bool adaptive_quantization = false; ///< Enable adaptive quantization for NVENC.
// Don't use QP below certain value, limits peak image quality to save bitrate
bool enable_min_qp = false;
bool enable_min_qp = false; ///< Enable minimum QP limits for NVENC.
// Min QP value for H.264 when enable_min_qp is selected
unsigned min_qp_h264 = 19;
unsigned min_qp_h264 = 19; ///< Min qp h264.
// Min QP value for HEVC when enable_min_qp is selected
unsigned min_qp_hevc = 23;
unsigned min_qp_hevc = 23; ///< Min qp HEVC.
// Min QP value for AV1 when enable_min_qp is selected
unsigned min_qp_av1 = 23; ///< Min qp AV1.
unsigned min_qp_av1 = 23;
// Use CAVLC entropy coding in H.264 instead of CABAC, not relevant and here for historical reasons
bool h264_cavlc = false;
bool h264_cavlc = false; ///< Use CAVLC entropy coding for H.264.
// Add filler data to encoded frames to stay at target bitrate, mainly for testing
bool insert_filler_data = false; ///< Insert filler data to maintain bitrate constraints.
bool insert_filler_data = false;
// Enable split-frame encoding if the gpu has multiple NVENC hardware clusters
nvenc_split_frame_encoding split_frame_encoding = nvenc_split_frame_encoding::driver_decides; ///< Split frame encoding.
nvenc_split_frame_encoding split_frame_encoding = nvenc_split_frame_encoding::driver_decides;
};
} // namespace nvenc
src/nvenc/nvenc_d3d11.h +24 −0
@@ -14,10 +14,29 @@
namespace nvenc {
#ifdef DOXYGEN
/**
* @brief COM smart pointer for ID3D11Device.
*/
using ID3D11DevicePtr = ID3D11Device *;
/**
* @brief COM smart pointer for ID3D11Texture2D.
*/
using ID3D11Texture2DPtr = ID3D11Texture2D *;
/**
* @brief COM smart pointer for IDXGIDevice.
*/
using IDXGIDevicePtr = IDXGIDevice *;
/**
* @brief COM smart pointer for IDXGIAdapter.
*/
using IDXGIAdapterPtr = IDXGIAdapter *;
#else
_COM_SMARTPTR_TYPEDEF(ID3D11Device, IID_ID3D11Device);
_COM_SMARTPTR_TYPEDEF(ID3D11Texture2D, IID_ID3D11Texture2D);
_COM_SMARTPTR_TYPEDEF(IDXGIDevice, IID_IDXGIDevice);
_COM_SMARTPTR_TYPEDEF(IDXGIAdapter, IID_IDXGIAdapter);
#endif
/**
* @brief Abstract Direct3D11 NVENC encoder.
@@ -25,5 +44,10 @@
*/
class nvenc_d3d11: public nvenc_base {
public:
/**
* @brief Initialize an NVENC session wrapper for D3D11 input textures.
*
* @param device_type NVENC device type used by the encoder session.
*/
explicit nvenc_d3d11(NV_ENC_DEVICE_TYPE device_type);
~nvenc_d3d11();
src/nvenc/nvenc_encoded_frame.h +5 −5
@@ -11,13 +11,13 @@
namespace nvenc {
/**
* @brief Encoded frame.
* @brief Encoded NVENC output frame and metadata needed by the packetizer.
*/
struct nvenc_encoded_frame {
std::vector<uint8_t> data;
uint64_t frame_index = 0;
bool idr = false;
bool after_ref_frame_invalidation = false;
std::vector<uint8_t> data; ///< Encoded bitstream bytes returned by NVENC.
uint64_t frame_index = 0; ///< Capture-frame index associated with the encoded data.
bool idr = false; ///< Whether the encoded frame is an IDR frame.
bool after_ref_frame_invalidation = false; ///< Whether the frame follows reference-frame invalidation.
};
} // namespace nvenc
src/nvenc/nvenc_utils.cpp +9 −0
@@ -11,6 +11,9 @@
namespace nvenc {
#ifdef _WIN32
/**
* @brief Convert an NVENC buffer format to the matching DXGI format.
*/
DXGI_FORMAT dxgi_format_from_nvenc_format(NV_ENC_BUFFER_FORMAT format) {
switch (format) {
case NV_ENC_BUFFER_FORMAT_YUV420_10BIT:
@@ -31,6 +34,9 @@
}
#endif
/**
* @brief Convert a Sunshine pixel format to the matching NVENC buffer format.
*/
NV_ENC_BUFFER_FORMAT nvenc_format_from_sunshine_format(platf::pix_fmt_e format) {
switch (format) {
case platf::pix_fmt_e::nv12:
@@ -53,5 +59,8 @@
}
}
/**
* @brief Convert Sunshine colorspace metadata to NVENC VUI metadata.
*/
nvenc_colorspace_t nvenc_colorspace_from_sunshine_colorspace(const video::sunshine_colorspace_t &sunshine_colorspace) {
nvenc_colorspace_t colorspace;
src/nvenc/nvenc_utils.h +18 −0
@@ -20,11 +20,29 @@
namespace nvenc {
#ifdef _WIN32
/**
* @brief Convert an NVENC buffer format to the matching DXGI format.
*
* @param format Pixel, audio, or protocol format being converted.
* @return DXGI texture format that matches the NVENC buffer format.
*/
DXGI_FORMAT dxgi_format_from_nvenc_format(NV_ENC_BUFFER_FORMAT format);
#endif
/**
* @brief Convert a Sunshine pixel format to the matching NVENC buffer format.
*
* @param format Pixel, audio, or protocol format being converted.
* @return NVENC buffer format compatible with the Sunshine pixel format.
*/
NV_ENC_BUFFER_FORMAT nvenc_format_from_sunshine_format(platf::pix_fmt_e format);
/**
* @brief Convert Sunshine colorspace metadata to NVENC VUI metadata.
*
* @param sunshine_colorspace Sunshine colorspace.
* @return NVENC colorspace and VUI fields for the Sunshine metadata.
*/
nvenc_colorspace_t nvenc_colorspace_from_sunshine_colorspace(const video::sunshine_colorspace_t &sunshine_colorspace);
} // namespace nvenc
src/nvhttp.cpp +208 −18
@@ -45,10 +45,19 @@
namespace fs = std::filesystem;
namespace pt = boost::property_tree;
crypto::cert_chain_t cert_chain;
crypto::cert_chain_t cert_chain; ///< Certificate chain presented by Sunshine's GameStream HTTPS server.
/**
* @brief HTTPS server backend that adds Sunshine's client-certificate verification.
*/
class SunshineHTTPSServer: public SimpleWeb::ServerBase<SunshineHTTPS> {
public:
/**
* @brief Initialize the HTTPS server with Sunshine's certificate and key files.
*
* @param certification_file Path to the server certificate file.
* @param private_key_file Path to the matching private key file.
*/
SunshineHTTPSServer(const std::string &certification_file, const std::string &private_key_file):
ServerBase<SunshineHTTPS>::ServerBase(443),
context(boost::asio::ssl::context::tls_server) {
@@ -59,12 +68,15 @@
context.use_private_key_file(private_key_file, boost::asio::ssl::context::pem);
}
std::function<int(SSL *)> verify; ///< Callback that validates a client's TLS certificate after handshake.
std::function<int(SSL *)> verify;
std::function<void(std::shared_ptr<Response>, std::shared_ptr<Request>)> on_verify_failed;
std::function<void(std::shared_ptr<Response>, std::shared_ptr<Request>)> on_verify_failed; ///< Handler used to return the pairing challenge when client verification fails.
protected:
boost::asio::ssl::context context;
boost::asio::ssl::context context; ///< TLS server context configured with Sunshine's certificate and protocol policy.
/**
* @brief Enable client-certificate verification after the listening socket is bound.
*/
void after_bind() override {
if (verify) {
context.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once);
@@ -76,5 +88,8 @@
}
// This is Server<HTTPS>::accept() with SSL validation support added
/**
* @brief Accept a pending connection and arm the server for the next client.
*/
void accept() override {
auto connection = create_connection(*io_service, context);
@@ -120,44 +135,85 @@
}
};
/**
* @brief HTTPS server type used for GameStream endpoints requiring TLS.
*/
using https_server_t = SunshineHTTPSServer;
/**
* @brief Plain HTTP server type used for GameStream endpoints without TLS.
*/
using http_server_t = SimpleWeb::Server<SimpleWeb::HTTP>;
/**
* @brief Internal HTTPS credential paths for the configuration server.
*/
struct conf_intern_t {
std::string servercert; ///< Server certificate PEM string.
std::string servercert;
std::string pkey;
} conf_intern;
std::string pkey; ///< Private key PEM string or path.
} conf_intern; ///< TLS credential paths loaded from Sunshine's runtime configuration.
/**
* @brief Certificate entry associated with a client name and UUID.
*/
struct named_cert_t {
std::string name;
std::string uuid;
std::string name; ///< Human-readable name for this item.
std::string uuid; ///< Persistent Moonlight client UUID associated with the certificate.
std::string cert; ///< Certificate PEM string or path.
bool enabled = true; ///< Whether this persisted client entry may connect.
std::string cert;
bool enabled = true;
};
/**
* @brief Persisted pairing data for one Moonlight client.
*/
struct client_t {
std::vector<named_cert_t> named_devices;
std::vector<named_cert_t> named_devices; ///< Persisted Moonlight clients allowed to pair or reconnect.
};
// uniqueID, session
std::unordered_map<std::string, pair_session_t> map_id_sess; ///< Pairing sessions keyed by temporary unique ID.
client_t client_root; ///< In-memory representation of the paired-client database.
std::atomic<uint32_t> session_id_counter; ///< Monotonic counter used to allocate GameStream session IDs.
std::unordered_map<std::string, pair_session_t> map_id_sess;
client_t client_root;
std::atomic<uint32_t> session_id_counter;
// Set by TLS verify callback, read by launch/resume handler (single-threaded HTTPS server)
std::string last_verified_client_cert; ///< Last client certificate accepted by the TLS verify callback. // NOSONAR(cpp:S5421) - intentionally mutable global
std::string last_verified_client_cert; // NOSONAR(cpp:S5421) - intentionally mutable global
/**
* @brief Case-insensitive map used for HTTP headers and query parameters.
*/
using args_t = SimpleWeb::CaseInsensitiveMultimap;
/**
* @brief Shared HTTPS response object passed to GameStream handlers.
*/
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Response>;
/**
* @brief Shared HTTPS request object received by GameStream handlers.
*/
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Request>;
/**
* @brief Shared HTTP response object passed to redirect and discovery handlers.
*/
using resp_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>;
/**
* @brief Shared HTTP request object received by redirect and discovery handlers.
*/
using req_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Request>;
/**
* @brief Certificate operations supported by the pairing API.
*/
enum class op_e {
ADD, ///< Add certificate
REMOVE ///< Remove certificate
};
/**
* @brief Read a named query argument from the HTTP request map.
*
* @param args Parsed query-string argument map.
* @param name Query parameter name to read.
* @param default_value Value returned when the parameter is absent.
* @return Query parameter value, default value, or an empty string.
*/
std::string get_arg(const args_t &args, const char *name, const char *default_value = nullptr) {
auto it = args.find(name);
if (it == std::end(args)) {
@@ -170,5 +226,8 @@
return it->second;
}
/**
* @brief Persist the current state to its backing store.
*/
void save_state() {
pt::ptree root;
@@ -207,6 +266,9 @@
}
}
/**
* @brief Load state from its backing store.
*/
void load_state() {
if (!fs::exists(config::nvhttp.file_state)) {
BOOST_LOG(info) << "File "sv << config::nvhttp.file_state << " doesn't exist"sv;
@@ -272,6 +334,12 @@
client_root = client;
}
/**
* @brief Add authorized client data.
*
* @param name Human-readable name to assign.
* @param cert Certificate data or object used by the operation.
*/
void add_authorized_client(const std::string &name, std::string &&cert) {
client_t &client = client_root;
named_cert_t named_cert;
@@ -285,5 +353,12 @@
}
}
/**
* @brief Create launch session.
*
* @param host_audio Host audio.
* @param args Arguments forwarded to the callable or parser.
* @return Constructed launch session object.
*/
std::shared_ptr<rtsp_stream::launch_session_t> make_launch_session(bool host_audio, const args_t &args) {
auto launch_session = std::make_shared<rtsp_stream::launch_session_t>();
@@ -348,6 +423,13 @@
map_id_sess.erase(sess.client.uniqueID);
}
/**
* @brief Return the GameStream pairing failure response.
*
* @param sess Pairing session that owns the request state.
* @param tree XML property tree used for the response body.
* @param status_msg Status msg.
*/
void fail_pair(pair_session_t &sess, pt::ptree &tree, const std::string status_msg) {
tree.put("root.paired", 0);
tree.put("root.<xmlattr>.status_code", 400);
@@ -355,6 +437,13 @@
remove_session(sess); // Security measure, delete the session when something went wrong and force a re-pair
}
/**
* @brief Return the server certificate text for pairing responses.
*
* @param sess Pairing session that owns the request state.
* @param tree XML property tree used for the response body.
* @param pin PIN supplied by the client during pairing.
*/
void getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin) {
if (sess.last_phase != PAIR_PHASE::NONE) {
fail_pair(sess, tree, "Out of order call to getservercert");
@@ -379,6 +468,13 @@
tree.put("root.<xmlattr>.status_code", 200);
}
/**
* @brief Handle the client-challenge phase of GameStream pairing.
*
* @param sess Pairing session that owns the request state.
* @param tree XML property tree used for the response body.
* @param challenge Client challenge bytes from the pairing request.
*/
void clientchallenge(pair_session_t &sess, pt::ptree &tree, const std::string &challenge) {
if (sess.last_phase != PAIR_PHASE::GETSERVERCERT) {
fail_pair(sess, tree, "Out of order call to clientchallenge");
@@ -422,6 +518,13 @@
tree.put("root.<xmlattr>.status_code", 200);
}
/**
* @brief Handle the server-challenge response phase of GameStream pairing.
*
* @param sess Pairing session that owns the request state.
* @param tree XML property tree used for the response body.
* @param encrypted_response Encrypted response.
*/
void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const std::string &encrypted_response) {
if (sess.last_phase != PAIR_PHASE::CLIENTCHALLENGE) {
fail_pair(sess, tree, "Out of order call to serverchallengeresp");
@@ -451,6 +554,14 @@
tree.put("root.<xmlattr>.status_code", 200);
}
/**
* @brief Handle the client pairing-secret phase of GameStream pairing.
*
* @param sess Pairing session that owns the request state.
* @param add_cert Add cert.
* @param tree XML property tree used for the response body.
* @param client_pairing_secret Client pairing secret.
*/
void clientpairingsecret(pair_session_t &sess, std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, pt::ptree &tree, const std::string &client_pairing_secret) {
if (sess.last_phase != PAIR_PHASE::SERVERCHALLENGERESP) {
fail_pair(sess, tree, "Out of order call to clientpairingsecret");
@@ -504,16 +615,27 @@
template<class T>
struct tunnel;
/**
* @brief HTTPS tunnel session used for encrypted client requests.
*/
template<>
struct tunnel<SunshineHTTPS> {
static auto constexpr to_string = "HTTPS"sv;
static auto constexpr to_string = "HTTPS"sv; ///< To string.
};
/**
* @brief Plain HTTP server wrapper used for non-TLS endpoints.
*/
template<>
struct tunnel<SimpleWeb::HTTP> {
static auto constexpr to_string = "NONE"sv;
static auto constexpr to_string = "NONE"sv; ///< To string.
};
/**
* @brief Write req details to the log.
*
* @param request HTTP request data from the client.
*/
template<class T>
void print_req(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
BOOST_LOG(debug) << "TUNNEL :: "sv << tunnel<T>::to_string;
@@ -534,6 +656,12 @@
BOOST_LOG(debug) << " [--] "sv;
}
/**
* @brief Return a GameStream HTTP not-found response.
*
* @param response HTTP response object to populate.
* @param request HTTP request data from the client.
*/
template<class T>
void not_found(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
print_req<T>(request);
@@ -553,6 +681,13 @@
response->close_connection_after_response = true;
}
/**
* @brief Dispatch the top-level GameStream pairing request by phase.
*
* @param add_cert Add cert.
* @param response HTTP response object to populate.
* @param request HTTP request data from the client.
*/
template<class T>
void pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
print_req<T>(request);
@@ -684,6 +819,11 @@
return true;
}
/**
* @brief Get codec mode flags.
*
* @return Moonlight codec capability bitmask for the currently probed encoders.
*/
uint32_t get_codec_mode_flags() {
uint32_t codec_mode_flags = SCM_H264;
if (video::last_encoder_probe_supported_yuv444_for_codec[0]) {
@@ -717,6 +857,12 @@
return codec_mode_flags;
}
/**
* @brief Build the GameStream server-info response.
*
* @param response HTTP response object to populate.
* @param request HTTP request data from the client.
*/
template<class T>
void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
print_req<T>(request);
@@ -801,5 +947,11 @@
return named_cert_nodes;
}
/**
* @brief Build the GameStream application list response.
*
* @param response HTTP response object to populate.
* @param request HTTP request data from the client.
*/
void applist(resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
@@ -829,5 +981,12 @@
}
}
/**
* @brief Launch the requested application for a GameStream session.
*
* @param host_audio Host audio.
* @param response HTTP response object to populate.
* @param request HTTP request data from the client.
*/
void launch(bool &host_audio, resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
@@ -940,5 +1099,12 @@
revert_display_configuration = false;
}
/**
* @brief Resume an existing GameStream session.
*
* @param host_audio Host audio.
* @param response HTTP response object to populate.
* @param request HTTP request data from the client.
*/
void resume(bool &host_audio, resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
@@ -1031,5 +1197,11 @@
rtsp_stream::launch_session_raise(launch_session);
}
/**
* @brief Check whether cel.
*
* @param response HTTP response object to populate.
* @param request HTTP request data from the client.
*/
void cancel(resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
@@ -1056,5 +1228,11 @@
display_device::revert_configuration();
}
/**
* @brief Return an application asset requested by the client.
*
* @param response HTTP response object to populate.
* @param request HTTP request data from the client.
*/
void appasset(resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
@@ -1074,6 +1252,12 @@
conf_intern.servercert = cert;
}
/**
* @brief Check whether a paired client certificate is allowed to connect.
*
* @param cert_pem PEM-encoded client certificate to look up.
* @return True when the client certificate belongs to an enabled device.
*/
bool is_client_enabled(const std::string_view cert_pem);
void start() {
@@ -1266,6 +1450,9 @@
return false;
}
/**
* @brief Get cert by UUID.
*/
std::string get_cert_by_uuid(const std::string_view uuid) {
for (const auto &named_cert : client_root.named_devices) {
if (named_cert.uuid == uuid) {
@@ -1275,6 +1462,9 @@
return {};
}
/**
* @brief Check whether a paired client certificate is allowed to connect.
*/
bool is_client_enabled(const std::string_view cert_pem) {
const client_t &client = client_root;
for (const auto &named_cert : client.named_devices) {
src/nvhttp.h +42 −6
@@ -59,8 +59,17 @@
*/
void setup(const std::string &pkey, const std::string &cert);
/**
* @brief Simple-Web-Server HTTPS backend configured for Sunshine certificate handling.
*/
class SunshineHTTPS: public SimpleWeb::HTTPS {
public:
/**
* @brief Construct an HTTPS connection using Sunshine's TLS context.
*
* @param io_context Boost.Asio context used for network operations.
* @param ctx TLS context configured with Sunshine's certificate and key.
*/
SunshineHTTPS(boost::asio::io_context &io_context, boost::asio::ssl::context &ctx):
SimpleWeb::HTTPS(io_context, ctx) {
}
@@ -72,6 +81,9 @@
}
};
/**
* @brief Enumerates supported pAIR PHASE options.
*/
enum class PAIR_PHASE {
NONE, ///< Sunshine is not in a pairing phase
GETSERVERCERT, ///< Sunshine is in the get server certificate phase
@@ -80,18 +92,21 @@
CLIENTPAIRINGSECRET ///< Sunshine is in the client pairing secret phase
};
/**
* @brief Pairing handshake state exchanged with a Moonlight client.
*/
struct pair_session_t {
struct {
std::string uniqueID = {};
std::string cert = {};
std::string name = {};
} client;
} client; ///< Client object or client certificate data owned by this state..
std::unique_ptr<crypto::aes_t> cipher_key = {};
std::vector<uint8_t> clienthash = {};
std::unique_ptr<crypto::aes_t> cipher_key = {}; ///< Cipher key.
std::vector<uint8_t> clienthash = {}; ///< Client certificate hash used during pairing.
std::string serversecret = {};
std::string serverchallenge = {};
std::string serversecret = {}; ///< Server pairing secret.
std::string serverchallenge = {}; ///< Server challenge sent during pairing.
struct {
util::Either<
@@ -99,7 +114,7 @@
std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Response>>
response;
std::string salt = {};
} async_insert_pin; ///< Async insert pin.
} async_insert_pin;
/**
* @brief used as a security measure to prevent out of order calls
@@ -122,5 +137,8 @@
* in order to be used to decrypt_symmetric in the next phases.
*
* At this stage we only have to send back our public certificate.
* @param sess Pairing session that owns the request state.
* @param tree XML property tree used for the response body.
* @param pin PIN supplied by the client during pairing.
*/
void getservercert(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &pin);
@@ -136,5 +154,8 @@
* - Server secret: a randomly generated secret
*
* The hash + server_challenge will then be AES encrypted and sent as the `challengeresponse` in the returned XML
* @param sess Pairing session that owns the request state.
* @param tree XML property tree used for the response body.
* @param challenge Client challenge bytes from the pairing request.
*/
void clientchallenge(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &challenge);
@@ -145,5 +166,8 @@
* Moonlight will send back a `serverchallengeresp`: an AES encrypted client hash,
* we have to send back the `pairingsecret`:
* using our private key we have to sign the certificate_signature + server_secret (generated in phase 2)
* @param sess Pairing session that owns the request state.
* @param tree XML property tree used for the response body.
* @param encrypted_response Encrypted response.
*/
void serverchallengeresp(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &encrypted_response);
@@ -161,5 +185,9 @@
*
* Then using the client certificate public key we should be able to verify that
* the client secret has been signed by Moonlight
* @param sess Pairing session that owns the request state.
* @param add_cert Add cert.
* @param tree XML property tree used for the response body.
* @param client_pairing_secret Client pairing secret.
*/
void clientpairingsecret(pair_session_t &sess, std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, boost::property_tree::ptree &tree, const std::string &client_pairing_secret);
@@ -181,5 +209,7 @@
* @examples
* nvhttp::unpair_client("4D7BB2DD-5704-A405-B41C-891A022932E1");
* @examples_end
*
* @return True when the client entry was found and removed.
*/
bool unpair_client(std::string_view uuid);
@@ -191,6 +221,12 @@
* @return true if the client was found and updated.
*/
bool set_client_enabled(std::string_view uuid, bool enabled);
/**
* @brief Get cert by UUID.
*
* @param uuid Client UUID being looked up or removed.
* @return PEM certificate for the paired client, or an empty string when unknown.
*/
std::string get_cert_by_uuid(std::string_view uuid);
/**
src/platform/common.h +551 −129
@@ -57,6 +57,9 @@
class group;
template<typename Char>
class basic_environment;
/**
* @brief Map of environment variable names to values.
*/
typedef basic_environment<char> environment;
} // namespace process::v1
} // namespace boost
@@ -71,36 +74,42 @@
namespace platf {
// Limited by bits in activeGamepadMask
constexpr auto MAX_GAMEPADS = 16;
constexpr auto MAX_GAMEPADS = 16; ///< Maximum number of simultaneously tracked gamepads.
constexpr std::uint32_t DPAD_UP = 0x0001; ///< Moonlight gamepad button mask bit for D-pad up.
constexpr std::uint32_t DPAD_DOWN = 0x0002; ///< Moonlight gamepad button mask bit for D-pad down.
constexpr std::uint32_t DPAD_LEFT = 0x0004; ///< Moonlight gamepad button mask bit for D-pad left.
constexpr std::uint32_t DPAD_RIGHT = 0x0008; ///< Moonlight gamepad button mask bit for D-pad right.
constexpr std::uint32_t START = 0x0010; ///< Moonlight gamepad button mask bit for Start.
constexpr std::uint32_t BACK = 0x0020; ///< Moonlight gamepad button mask bit for Back.
constexpr std::uint32_t LEFT_STICK = 0x0040; ///< Moonlight gamepad button mask bit for left stick press.
constexpr std::uint32_t RIGHT_STICK = 0x0080; ///< Moonlight gamepad button mask bit for right stick press.
constexpr std::uint32_t LEFT_BUTTON = 0x0100; ///< Moonlight gamepad button mask bit for left shoulder.
constexpr std::uint32_t RIGHT_BUTTON = 0x0200; ///< Moonlight gamepad button mask bit for right shoulder.
constexpr std::uint32_t HOME = 0x0400; ///< Moonlight gamepad button mask bit for Home.
constexpr std::uint32_t A = 0x1000; ///< Moonlight gamepad button mask bit for A.
constexpr std::uint32_t B = 0x2000; ///< Moonlight gamepad button mask bit for B.
constexpr std::uint32_t X = 0x4000; ///< Moonlight gamepad button mask bit for X.
constexpr std::uint32_t Y = 0x8000; ///< Moonlight gamepad button mask bit for Y.
constexpr std::uint32_t PADDLE1 = 0x010000; ///< Moonlight gamepad button mask bit for paddle 1.
constexpr std::uint32_t PADDLE2 = 0x020000; ///< Moonlight gamepad button mask bit for paddle 2.
constexpr std::uint32_t DPAD_UP = 0x0001;
constexpr std::uint32_t DPAD_DOWN = 0x0002;
constexpr std::uint32_t DPAD_LEFT = 0x0004;
constexpr std::uint32_t DPAD_RIGHT = 0x0008;
constexpr std::uint32_t START = 0x0010;
constexpr std::uint32_t BACK = 0x0020;
constexpr std::uint32_t LEFT_STICK = 0x0040;
constexpr std::uint32_t RIGHT_STICK = 0x0080;
constexpr std::uint32_t LEFT_BUTTON = 0x0100;
constexpr std::uint32_t RIGHT_BUTTON = 0x0200;
constexpr std::uint32_t HOME = 0x0400;
constexpr std::uint32_t A = 0x1000;
constexpr std::uint32_t B = 0x2000;
constexpr std::uint32_t X = 0x4000;
constexpr std::uint32_t Y = 0x8000;
constexpr std::uint32_t PADDLE1 = 0x010000;
constexpr std::uint32_t PADDLE2 = 0x020000;
constexpr std::uint32_t PADDLE3 = 0x040000; ///< Moonlight gamepad button mask bit for paddle 3.
constexpr std::uint32_t PADDLE4 = 0x080000; ///< Moonlight gamepad button mask bit for paddle 4.
constexpr std::uint32_t TOUCHPAD_BUTTON = 0x100000; ///< Moonlight gamepad button mask bit for touchpad click.
constexpr std::uint32_t MISC_BUTTON = 0x200000; ///< Moonlight gamepad button mask bit for the miscellaneous button.
constexpr std::uint32_t PADDLE3 = 0x040000;
constexpr std::uint32_t PADDLE4 = 0x080000;
constexpr std::uint32_t TOUCHPAD_BUTTON = 0x100000;
constexpr std::uint32_t MISC_BUTTON = 0x200000;
/**
* @brief Gamepad type exposed to clients and why it may be disabled.
*/
struct supported_gamepad_t {
std::string name; ///< Human-readable name for this item.
bool is_enabled; ///< Whether this gamepad type is currently available.
std::string name;
std::string reason_disabled; ///< Human-readable reason the gamepad type is disabled.
bool is_enabled;
std::string reason_disabled;
};
/**
* @brief Enumerates supported gamepad feedback options.
*/
enum class gamepad_feedback_e {
rumble, ///< Rumble
rumble_triggers, ///< Rumble triggers
@@ -109,7 +118,18 @@
set_adaptive_triggers, ///< Set adaptive triggers
};
/**
* @brief Feedback command sent from Sunshine to a virtual gamepad.
*/
struct gamepad_feedback_msg_t {
/**
* @brief Create a rumble object or message.
*
* @param id Identifier for the controller, session, display, or resource.
* @param lowfreq Low-frequency rumble motor intensity.
* @param highfreq High-frequency rumble motor intensity.
* @return Constructed rumble object.
*/
static gamepad_feedback_msg_t make_rumble(std::uint16_t id, std::uint16_t lowfreq, std::uint16_t highfreq) {
gamepad_feedback_msg_t msg;
msg.type = gamepad_feedback_e::rumble;
@@ -118,6 +138,14 @@
return msg;
}
/**
* @brief Create rumble triggers.
*
* @param id Identifier for the controller, session, display, or resource.
* @param left Left trigger or motor payload for the feedback command.
* @param right Right trigger or motor payload for the feedback command.
* @return Constructed rumble triggers object.
*/
static gamepad_feedback_msg_t make_rumble_triggers(std::uint16_t id, std::uint16_t left, std::uint16_t right) {
gamepad_feedback_msg_t msg;
msg.type = gamepad_feedback_e::rumble_triggers;
@@ -126,6 +154,14 @@
return msg;
}
/**
* @brief Motion-event feedback command payload for a controller.
*
* @param id Identifier for the controller, session, display, or resource.
* @param motion_type Motion type.
* @param report_rate Report rate.
* @return Constructed motion event state object.
*/
static gamepad_feedback_msg_t make_motion_event_state(std::uint16_t id, std::uint8_t motion_type, std::uint16_t report_rate) {
gamepad_feedback_msg_t msg;
msg.type = gamepad_feedback_e::set_motion_event_state;
@@ -135,6 +171,15 @@
return msg;
}
/**
* @brief Create RGB led.
*
* @param id Identifier for the controller, session, display, or resource.
* @param r Red color channel value.
* @param g Green color channel value.
* @param b Blue color channel value.
* @return Constructed RGB led object.
*/
static gamepad_feedback_msg_t make_rgb_led(std::uint16_t id, std::uint8_t r, std::uint8_t g, std::uint8_t b) {
gamepad_feedback_msg_t msg;
msg.type = gamepad_feedback_e::set_rgb_led;
@@ -143,6 +188,17 @@
return msg;
}
/**
* @brief Create adaptive triggers.
*
* @param id Identifier for the controller, session, display, or resource.
* @param event_flags Event flags.
* @param type_left Type left.
* @param type_right Type right.
* @param left Left trigger or motor payload for the feedback command.
* @param right Right trigger or motor payload for the feedback command.
* @return Constructed adaptive triggers object.
*/
static gamepad_feedback_msg_t make_adaptive_triggers(std::uint16_t id, uint8_t event_flags, uint8_t type_left, uint8_t type_right, const std::array<uint8_t, 10> &left, const std::array<uint8_t, 10> &right) {
gamepad_feedback_msg_t msg;
msg.type = gamepad_feedback_e::set_adaptive_triggers;
@@ -151,8 +207,8 @@
return msg;
}
gamepad_feedback_e type; ///< Feedback command type stored in the union payload.
gamepad_feedback_e type;
std::uint16_t id;
std::uint16_t id; ///< Controller identifier associated with this message.
union {
struct {
@@ -184,12 +240,18 @@
std::array<uint8_t, 10> left;
std::array<uint8_t, 10> right;
} adaptive_triggers;
} data;
} data; ///< Controller feedback payload for the selected feedback type.
};
/**
* @brief Queue used to deliver controller feedback commands to the platform backend.
*/
using feedback_queue_t = safe::mail_raw_t::queue_t<gamepad_feedback_msg_t>;
namespace speaker {
/**
* @brief Enumerates supported speaker options.
*/
enum speaker_e {
FRONT_LEFT, ///< Front left
FRONT_RIGHT, ///< Front right
@@ -202,10 +264,16 @@
MAX_SPEAKERS, ///< Maximum number of speakers
};
/**
* @brief Moonlight speaker order for stereo audio.
*/
constexpr std::uint8_t map_stereo[] {
FRONT_LEFT,
FRONT_RIGHT
};
/**
* @brief Moonlight speaker order for 5.1 surround audio.
*/
constexpr std::uint8_t map_surround51[] {
FRONT_LEFT,
FRONT_RIGHT,
@@ -214,6 +282,9 @@
BACK_LEFT,
BACK_RIGHT,
};
/**
* @brief Moonlight speaker order for 7.1 surround audio.
*/
constexpr std::uint8_t map_surround71[] {
FRONT_LEFT,
FRONT_RIGHT,
@@ -226,6 +297,9 @@
};
} // namespace speaker
/**
* @brief Enumerates supported mem type options.
*/
enum class mem_type_e {
system, ///< System memory
vaapi, ///< VAAPI
@@ -236,6 +310,9 @@
unknown ///< Unknown
};
/**
* @brief Enumerates supported pix fmt options.
*/
enum class pix_fmt_e {
yuv420p, ///< YUV 4:2:0
yuv420p10, ///< YUV 4:2:0 10-bit
@@ -248,11 +325,19 @@
unknown ///< Unknown
};
/**
* @brief Convert a Sunshine pixel format enum to its string name.
*
* @param pix_fmt Sunshine pixel format to convert or allocate for.
* @return Value converted from pix fmt.
*/
inline std::string_view from_pix_fmt(pix_fmt_e pix_fmt) {
using namespace std::literals;
#define _CONVERT(x) \
case pix_fmt_e::x: \
return #x##sv
#ifndef DOXYGEN
#define _CONVERT(x) \
case pix_fmt_e::x: \
return #x##sv
#endif
switch (pix_fmt) {
_CONVERT(yuv420p);
_CONVERT(yuv420p10);
@@ -270,105 +355,150 @@
}
// Dimensions for touchscreen input
/**
* @brief Touchscreen coordinate bounds used to scale absolute input.
*/
struct touch_port_t {
int offset_x; ///< Horizontal offset in physical pixels.
int offset_y; ///< Vertical offset in physical pixels.
int width; ///< Frame or display width in pixels.
int height; ///< Frame or display height in pixels.
int logical_width; ///< Logical width after display scaling.
int offset_x;
int offset_y;
int width;
int height;
int logical_height; ///< Logical height after display scaling.
int logical_width;
int logical_height;
};
// These values must match Limelight-internal.h's SS_FF_* constants!
namespace platform_caps {
/**
* @brief Bitset containing platform capability flags.
*/
typedef uint32_t caps_t;
/**
* @brief Capability bit indicating native pen and touch support.
*/
constexpr caps_t pen_touch = 0x01; // Pen and touch events
/**
* @brief Capability bit indicating controller touchpad support.
*/
constexpr caps_t controller_touch = 0x02; // Controller touch events
}; // namespace platform_caps
/**
* @brief Button and axis state received for a virtual gamepad.
*/
struct gamepad_state_t {
std::uint32_t buttonFlags;
std::uint8_t lt;
std::uint8_t rt;
std::int16_t lsX;
std::int16_t lsY;
std::int16_t rsX;
std::int16_t rsY;
std::uint32_t buttonFlags; ///< Moonlight button mask for the current gamepad state.
std::uint8_t lt; ///< Left trigger value.
std::uint8_t rt; ///< Right trigger value.
std::int16_t lsX; ///< Left stick X-axis value.
std::int16_t lsY; ///< Left stick Y-axis value.
std::int16_t rsX; ///< Right stick X-axis value.
std::int16_t rsY; ///< Right stick Y-axis value.
};
/**
* @brief Global and client-relative identifiers for a virtual gamepad.
*/
struct gamepad_id_t {
// The global index is used when looking up gamepads in the platform's
// gamepad array. It identifies gamepads uniquely among all clients.
int globalIndex; ///< Index unique across all connected clients.
int globalIndex;
// The client-relative index is the controller number as reported by the
// client. It must be used when communicating back to the client via
// the input feedback queue.
std::uint8_t clientRelativeIndex; ///< Client relative index.
std::uint8_t clientRelativeIndex;
};
/**
* @brief Capabilities reported when a controller is connected.
*/
struct gamepad_arrival_t {
std::uint8_t type;
std::uint16_t capabilities;
std::uint8_t type; ///< Protocol or controller type discriminator.
std::uint16_t capabilities; ///< Capability flags advertised by the controller.
std::uint32_t supportedButtons; ///< Button mask supported by the connected controller.
std::uint32_t supportedButtons;
};
/**
* @brief Touchpad contact data reported by a controller.
*/
struct gamepad_touch_t {
gamepad_id_t id; ///< Gamepad identifier for the event.
std::uint8_t eventType; ///< Moonlight event type for the input packet.
std::uint32_t pointerId; ///< Client-provided pointer identifier for a touch contact.
float x; ///< Horizontal coordinate or vector component.
float y; ///< Vertical coordinate or vector component.
float pressure; ///< Contact pressure reported by the client.
gamepad_id_t id;
std::uint8_t eventType;
std::uint32_t pointerId;
float x;
float y;
float pressure;
};
/**
* @brief Accelerometer or gyroscope sample from a controller.
*/
struct gamepad_motion_t {
gamepad_id_t id; ///< Gamepad identifier for the event.
std::uint8_t motionType; ///< Motion type.
gamepad_id_t id;
std::uint8_t motionType;
// Accel: m/s^2
// Gyro: deg/s
float x;
float y;
float z;
float x; ///< Horizontal coordinate or vector component.
float y; ///< Vertical coordinate or vector component.
float z; ///< Depth or Z-axis vector component.
};
/**
* @brief Battery state reported by a virtual gamepad.
*/
struct gamepad_battery_t {
gamepad_id_t id;
std::uint8_t state;
gamepad_id_t id; ///< Gamepad identifier for the event.
std::uint8_t state; ///< Battery state reported by the client.
std::uint8_t percentage; ///< Battery charge percentage.
std::uint8_t percentage;
};
/**
* @brief Absolute touchscreen event data from the client.
*/
struct touch_input_t {
std::uint8_t eventType; ///< Moonlight event type for the input packet.
std::uint8_t eventType;
std::uint16_t rotation; // Degrees (0..360) or LI_ROT_UNKNOWN
std::uint32_t pointerId;
float x;
float y;
float pressureOrDistance; // Distance for hover and pressure for contact
float contactAreaMajor;
float contactAreaMinor;
std::uint16_t rotation; ///< Degrees (0..360) or LI_ROT_UNKNOWN.
std::uint32_t pointerId; ///< Client-provided pointer identifier for a touch contact.
float x; ///< Horizontal coordinate or vector component.
float y; ///< Vertical coordinate or vector component.
float pressureOrDistance; ///< Distance for hover and pressure for contact.
float contactAreaMajor; ///< Major axis of the reported contact area.
float contactAreaMinor; ///< Minor axis of the reported contact area.
};
/**
* @brief Pen tablet event data from the client.
*/
struct pen_input_t {
std::uint8_t eventType;
std::uint8_t toolType;
std::uint8_t penButtons;
std::uint8_t tilt; // Degrees (0..90) or LI_TILT_UNKNOWN
std::uint8_t eventType; ///< Moonlight event type for the input packet.
std::uint8_t toolType; ///< Pen tool type reported by the client.
std::uint8_t penButtons; ///< Button mask for the active pen tool.
std::uint8_t tilt; ///< Degrees (0..90) or LI_TILT_UNKNOWN.
std::uint16_t rotation; ///< Degrees (0..360) or LI_ROT_UNKNOWN.
float x; ///< Horizontal coordinate or vector component.
float y; ///< Vertical coordinate or vector component.
float pressureOrDistance; ///< Distance for hover and pressure for contact.
float contactAreaMajor; ///< Major axis of the reported contact area.
float contactAreaMinor; ///< Minor axis of the reported contact area.
std::uint16_t rotation; // Degrees (0..360) or LI_ROT_UNKNOWN
float x;
float y;
float pressureOrDistance; // Distance for hover and pressure for contact
float contactAreaMajor;
float contactAreaMinor;
};
/**
* @brief RAII helper that runs shutdown cleanup when destroyed.
*/
class deinit_t {
public:
/**
* @brief Destroy the deinitializer.
*/
virtual ~deinit_t() = default;
};
/**
* @brief Captured frame buffer shared between capture and encode stages.
*/
struct img_t: std::enable_shared_from_this<img_t> {
public:
img_t() = default;
@@ -378,54 +508,88 @@
img_t &operator=(img_t &&) = delete;
img_t &operator=(const img_t &) = delete;
std::uint8_t *data {};
std::uint8_t *data {}; ///< Pointer to the captured image buffer.
std::int32_t width {}; ///< Image width in pixels.
std::int32_t height {}; ///< Image height in pixels.
std::int32_t pixel_pitch {}; ///< Bytes per pixel in the image buffer.
std::int32_t row_pitch {}; ///< Bytes between consecutive image rows.
std::int32_t width {};
std::int32_t height {};
std::int32_t pixel_pitch {};
std::int32_t row_pitch {};
std::optional<std::chrono::steady_clock::time_point> frame_timestamp; ///< Capture timestamp associated with the frame.
std::optional<std::chrono::steady_clock::time_point> frame_timestamp;
/**
* @brief Destroy the image.
*/
virtual ~img_t() = default;
};
/**
* @brief Host and virtual audio sink names for audio routing.
*/
struct sink_t {
// Play on host PC
std::string host; ///< Host playback sink name.
std::string host;
// On macOS and Windows, it is not possible to create a virtual sink
// Therefore, it is optional
/**
* @brief Optional virtual sink names for each supported channel layout.
*/
struct null_t {
std::string stereo; ///< Virtual sink name for stereo audio.
std::string stereo;
std::string surround51;
std::string surround71;
std::string surround51; ///< Virtual sink name for 5.1 surround audio.
std::string surround71; ///< Virtual sink name for 7.1 surround audio.
};
std::optional<null_t> null;
std::optional<null_t> null; ///< Optional virtual sink names for monitor capture.
};
/**
* @brief Base interface for hardware or software frame conversion.
*/
struct encode_device_t {
virtual ~encode_device_t() = default;
/**
* @brief Convert a captured image into the encoder input representation.
*
* @param img Image or frame object to read from or populate.
* @return Conversion status.
*/
virtual int convert(platf::img_t &img) = 0;
video::sunshine_colorspace_t colorspace; ///< Colorspace metadata expected by the encoder.
video::sunshine_colorspace_t colorspace;
};
/**
* @brief AVCodec-backed encode device and frame state.
*/
struct avcodec_encode_device_t: encode_device_t {
void *data {}; ///< Backend-specific conversion state.
void *data {};
AVFrame *frame {};
AVFrame *frame {}; ///< FFmpeg frame currently owned by the encode device.
/**
* @brief Convert a captured image into the encoder input representation.
*
* @param img Image or frame object to read from or populate.
* @return Conversion status.
*/
int convert(platf::img_t &img) override {
return -1;
}
/**
* @brief Apply the configured colorspace metadata to the active frame.
*/
virtual void apply_colorspace() {
}
/**
* @brief Set the frame to be encoded.
* @note Implementations must take ownership of 'frame'.
*
* @param frame Video or graphics frame being processed.
* @param hw_frames_ctx FFmpeg hardware frames context associated with the frame.
* @return Status from updating frame.
*/
virtual int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) {
BOOST_LOG(error) << "Illegal call to hwdevice_t::set_frame(). Did you forget to override it?";
@@ -435,30 +599,51 @@
/**
* @brief Initialize the hwframes context.
* @note Implementations may set parameters during initialization of the hwframes context.
*
* @param frames FFmpeg hardware frames context to initialize.
*/
virtual void init_hwframes(AVHWFramesContext *frames) {};
/**
* @brief Provides a hook for allow platform-specific code to adjust codec options.
* @note Implementations may set or modify codec options prior to codec initialization.
*
* @param ctx Native context object used by the operation or callback.
* @param options Request options or socket options to apply.
*/
virtual void init_codec_options(AVCodecContext *ctx, AVDictionary **options) {};
/**
* @brief Prepare to derive a context.
* @note Implementations may make modifications required before context derivation
*
* @param hw_device_type FFmpeg hardware device type requested for context derivation.
* @return 0 when context derivation may continue; nonzero to abort.
*/
virtual int prepare_to_derive_context(int hw_device_type) {
return 0;
};
};
/**
* @brief NVENC-backed encode device state.
*/
struct nvenc_encode_device_t: encode_device_t {
/**
* @brief Initialize the platform encoder for the client stream configuration.
*
* @param client_config Client stream configuration negotiated for this session.
* @param colorspace Colorimetry information used for conversion or encoding.
* @return True when the backend successfully completes the requested action.
*/
virtual bool init_encoder(const video::config_t &client_config, const video::sunshine_colorspace_t &colorspace) = 0;
nvenc::nvenc_base *nvenc = nullptr; ///< NVENC encoder instance owned by the encode device.
nvenc::nvenc_base *nvenc = nullptr;
};
/**
* @brief Enumerates supported capture options.
*/
enum class capture_e : int {
ok, ///< Success
reinit, ///< Need to reinitialize
@@ -467,6 +652,9 @@
error ///< Error
};
/**
* @brief Abstract display capture backend used by the streaming pipeline.
*/
class display_t {
public:
/**
@@ -503,22 +691,56 @@
*/
virtual capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) = 0;
/**
* @brief Allocate an image buffer compatible with this display backend.
*
* @return Allocated img object, or null when unavailable.
*/
virtual std::shared_ptr<img_t> alloc_img() = 0;
/**
* @brief Populate a fallback image when real capture data is unavailable.
*
* @param img Image or frame object to read from or populate.
* @return Capture status reported to the streaming pipeline.
*/
virtual int dummy_img(img_t *img) = 0;
/**
* @brief Create AVCodec encode device.
*
* @param pix_fmt Sunshine pixel format to convert or allocate for.
* @return Constructed AVCodec encode device object.
*/
virtual std::unique_ptr<avcodec_encode_device_t> make_avcodec_encode_device(pix_fmt_e pix_fmt) {
return nullptr;
}
/**
* @brief Create NVENC encode device.
*
* @param pix_fmt Sunshine pixel format to convert or allocate for.
* @return Constructed NVENC encode device object.
*/
virtual std::unique_ptr<nvenc_encode_device_t> make_nvenc_encode_device(pix_fmt_e pix_fmt) {
return nullptr;
}
/**
* @brief Report whether the active display mode is HDR.
*
* @return True when the active display mode is HDR.
*/
virtual bool is_hdr() {
return false;
}
/**
* @brief Read HDR metadata for the active display mode.
*
* @param metadata Output structure populated with HDR metadata.
* @return True when HDR metadata was written to `metadata`.
*/
virtual bool get_hdr_metadata(SS_HDR_METADATA &metadata) {
std::memset(&metadata, 0, sizeof(metadata));
return false;
@@ -537,33 +759,62 @@
virtual ~display_t() = default;
// Offsets for when streaming a specific monitor. By default, they are 0.
int offset_x {0}; ///< Horizontal capture offset in physical pixels.
int offset_x {0};
int offset_y {0};
int env_width {0};
int env_height {0};
int env_logical_width {0};
int env_logical_height {0};
int width {0};
int height {0};
int logical_width {0};
int logical_height {0};
int offset_y {0}; ///< Vertical capture offset in physical pixels.
int env_width {0}; ///< Width of the full capture environment in physical pixels.
int env_height {0}; ///< Height of the full capture environment in physical pixels.
int env_logical_width {0}; ///< Width of the full capture environment after display scaling.
int env_logical_height {0}; ///< Height of the full capture environment after display scaling.
int width {0}; ///< Width of the captured display in physical pixels.
int height {0}; ///< Height of the captured display in physical pixels.
int logical_width {0}; ///< Width of the captured display after display scaling.
int logical_height {0}; ///< Height of the captured display after display scaling.
protected:
// collect capture timing data (at loglevel debug)
logging::time_delta_periodic_logger sleep_overshoot_logger = {debug, "Frame capture sleep overshoot"};
logging::time_delta_periodic_logger sleep_overshoot_logger = {debug, "Frame capture sleep overshoot"}; ///< Periodic logger for capture sleep overshoot measurements.
};
/**
* @brief Audio capture source used by the streaming pipeline.
*/
class mic_t {
public:
/**
* @brief Deliver a captured audio sample to Sunshine's audio pipeline.
*
* @param frame_buffer Destination for captured floating-point PCM samples.
* @return Capture status reported to the streaming pipeline.
*/
virtual capture_e sample(std::vector<float> &frame_buffer) = 0;
virtual ~mic_t() = default;
};
/**
* @brief Platform audio controller that manages sinks and microphone capture.
*/
class audio_control_t {
public:
/**
* @brief Update the sink value on the backend.
*
* @param sink Audio sink name to route or capture.
* @return Status from updating sink.
*/
virtual int set_sink(const std::string &sink) = 0;
/**
* @brief Create a microphone capture stream for the requested layout.
*
* @param mapping Opus channel mapping table for the requested layout.
* @param channels Number of audio channels in the stream.
* @param sample_rate Audio sample rate in hertz.
* @param frame_size Number of samples captured per audio frame.
* @param continuous Whether silent audio should continue to be emitted.
* @param host_audio_enabled Whether host playback should remain enabled during capture.
* @return Microphone capture object for the requested audio layout.
*/
virtual std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, bool continuous, [[maybe_unused]] bool host_audio_enabled) = 0;
/**
@@ -573,22 +824,61 @@
*/
virtual bool is_sink_available(const std::string &sink) = 0;
/**
* @brief Query host and virtual sink names available to Sunshine.
*
* @return Host and virtual sink names when the backend can report them.
*/
virtual std::optional<sink_t> sink_info() = 0;
/**
* @brief Destroy the audio control.
*/
virtual ~audio_control_t() = default;
};
/**
* @brief Release a platform input backend created by input().
*
* @param p Pointer passed to the deleter or conversion helper.
*/
void freeInput(void *);
/**
* @brief Owning pointer for a platform input backend.
*/
using input_t = util::safe_ptr<void, freeInput>;
std::filesystem::path appdata();
/**
* @brief Return the hardware MAC address associated with a network address.
*
* @param address Network address being parsed or filtered.
* @return Hardware MAC address string, or an empty string when it cannot be resolved.
*/
std::string get_mac_address(const std::string_view &address);
/**
* @brief Convert a socket address to a printable IP address.
*
* @param ip_addr Socket address to format.
* @return Value converted from sockaddr.
*/
std::string from_sockaddr(const sockaddr *const);
/**
* @brief Convert a socket address to a port and printable IP address.
*
* @param ip_addr Socket address to format.
* @return Value converted from sockaddr ex.
*/
std::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const);
/**
* @brief Create the platform audio controller.
*
* @return Platform audio controller, or nullptr when audio control is unavailable.
*/
std::unique_ptr<audio_control_t> audio_control();
/**
@@ -602,6 +892,12 @@
std::shared_ptr<display_t> display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config);
// A list of names of displays accepted as display_name with the mem_type_e
/**
* @brief List display names accepted by the selected capture backend.
*
* @param hwdevice_type Hardware device type requested for capture or encode.
* @return Display names accepted by the selected capture backend.
*/
std::vector<std::string> display_names(mem_type_e hwdevice_type);
/**
@@ -610,18 +906,41 @@
*/
bool needs_encoder_reenumeration();
/**
* @brief Launch a configured preparation or application command.
*
* @param elevated Whether the command should run with elevated privileges.
* @param interactive Whether the command should run in an interactive session.
* @param cmd Command line to execute or inspect.
* @param working_dir Working directory for the child process.
* @param env Environment variables for the child process.
* @param file Optional stdio file handle connected to the child process.
* @param ec Error code returned by the asynchronous operation.
* @param group Process group used when launching the command.
* @return Child process handle for the launched command.
*/
boost::process::v1::child run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const boost::process::v1::environment &env, FILE *file, std::error_code &ec, boost::process::v1::group *group);
/**
* @brief Enumerates supported thread priority options.
*/
enum class thread_priority_e : int {
low, ///< Low priority
normal, ///< Normal priority
high, ///< High priority
critical ///< Critical priority
};
/**
* @brief Apply the requested scheduling priority to the current thread.
*
* @param priority Thread priority requested from the platform backend.
*/
void adjust_thread_priority(thread_priority_e priority);
/**
* @brief Name the current thread for use with development tools.
* @note On Linux this will be truncated after 15 characters.
*
* @param name Human-readable name to assign.
*/
void set_thread_name(const std::string &name);
@@ -649,11 +968,20 @@
*/
int unset_env(const std::string &name);
/**
* @brief Platform buffer pointer and size for batched socket sends.
*/
struct buffer_descriptor_t {
const char *buffer; ///< Pointer to the payload buffer.
size_t size; ///< Size of the buffer in bytes.
const char *buffer;
size_t size;
// Constructors required for emplace_back() prior to C++20
/**
* @brief Describe a contiguous byte buffer for batched socket sending.
*
* @param buffer Serialized byte buffer to read from or write to.
* @param size Number of bytes or elements requested.
*/
buffer_descriptor_t(const char *buffer, size_t size):
buffer(buffer),
size(size) {
@@ -665,28 +993,31 @@
}
};
/**
* @brief Buffers and native metadata for one batched send operation.
*/
struct batched_send_info_t {
// Optional headers to be prepended to each packet
const char *headers; ///< Optional header bytes prepended to each payload.
size_t header_size; ///< Header size in bytes.
const char *headers;
size_t header_size;
// One or more data buffers to use for the payloads
//
// NB: Data buffers must be aligned to payload size!
std::vector<buffer_descriptor_t> &payload_buffers;
size_t payload_size;
std::vector<buffer_descriptor_t> &payload_buffers; ///< Payload buffers containing one or more packet blocks.
size_t payload_size; ///< Payload size in bytes for each packet block.
// The offset (in header+payload message blocks) in the header and payload
// buffers to begin sending messages from
size_t block_offset; ///< First packet block index to send.
size_t block_offset;
// The number of header+payload message blocks to send
size_t block_count;
size_t block_count; ///< Number of packet blocks to send.
std::uintptr_t native_socket; ///< Platform socket handle used for the send operation.
boost::asio::ip::address &target_address; ///< Destination IP address for outgoing packets.
uint16_t target_port; ///< Destination UDP port for outgoing packets.
std::uintptr_t native_socket;
boost::asio::ip::address &target_address;
uint16_t target_port;
boost::asio::ip::address &source_address;
boost::asio::ip::address &source_address; ///< Local source IP address for outgoing packets.
/**
* @brief Returns a payload buffer descriptor for the given payload offset.
@@ -708,22 +1039,40 @@
}
};
/**
* @brief Send multiple fixed-size UDP payload blocks using the platform backend.
*
* @param send_info Socket addresses, buffers, and sizes for the send operation.
* @return True when all requested packet blocks are submitted to the socket.
*/
bool send_batch(batched_send_info_t &send_info);
/**
* @brief Destination address and payload data for one UDP send.
*/
struct send_info_t {
const char *header; ///< Optional header bytes prepended to the payload.
size_t header_size; ///< Header size in bytes.
const char *header;
size_t header_size;
const char *payload;
const char *payload; ///< Payload bytes to send after the header.
size_t payload_size; ///< Payload size in bytes for each packet block.
size_t payload_size;
std::uintptr_t native_socket;
boost::asio::ip::address &target_address;
uint16_t target_port;
boost::asio::ip::address &source_address;
std::uintptr_t native_socket; ///< Platform socket handle used for the send operation.
boost::asio::ip::address &target_address; ///< Destination IP address for outgoing packets.
uint16_t target_port; ///< Destination UDP port for outgoing packets.
boost::asio::ip::address &source_address; ///< Local source IP address for outgoing packets.
};
/**
* @brief Send the serialized response over the active socket.
*
* @param send_info Socket addresses, buffers, and sizes for the send operation.
* @return True when the packet is submitted to the socket.
*/
bool send(send_info_t &send_info);
/**
* @brief Identifies traffic classes used for socket QoS tagging.
*/
enum class qos_data_type_e : int {
audio, ///< Audio
video ///< Video
@@ -736,5 +1085,7 @@
* @param port The destination port for traffic sent on this socket.
* @param data_type The type of traffic sent on this socket.
* @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic.
*
* @return Cleanup handle that restores or releases QoS state when destroyed.
*/
std::unique_ptr<deinit_t> enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging);
@@ -759,6 +1110,11 @@
*/
bool process_group_running(std::uintptr_t native_handle);
/**
* @brief Create the platform input backend for a stream.
*
* @return Platform-specific input backend for the active stream.
*/
input_t input();
/**
* @brief Get the current mouse position on screen
@@ -769,15 +1125,67 @@
* @examples_end
*/
util::point_t get_mouse_loc(input_t &input);
/**
* @brief Move mouse using the backend coordinate system.
*
* @param input Platform input backend that receives the event.
* @param deltaX Delta x.
* @param deltaY Delta y.
*/
void move_mouse(input_t &input, int deltaX, int deltaY);
/**
* @brief Move the pointer to an absolute client-provided touch coordinate.
*
* @param input Platform input backend that receives the event.
* @param touch_port Touch coordinate bounds used for scaling.
* @param x Horizontal absolute coordinate from the client.
* @param y Vertical absolute coordinate from the client.
*/
void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y);
/**
* @brief Press or release a virtual mouse button.
*
* @param input Platform input backend that receives the event.
* @param button Mouse button identifier to press or release.
* @param release Whether the key or button event is a release.
*/
void button_mouse(input_t &input, int button, bool release);
/**
* @brief Apply a vertical scroll event to the virtual mouse.
*
* @param input Platform input backend that receives the event.
* @param distance High-resolution scroll distance reported by the client.
*/
void scroll(input_t &input, int distance);
/**
* @brief Apply a horizontal scroll event to the virtual mouse.
*
* @param input Platform input backend that receives the event.
* @param distance High-resolution scroll distance reported by the client.
*/
void hscroll(input_t &input, int distance);
/**
* @brief Press or release a virtual keyboard key.
*
* @param input Platform input backend that receives the event.
* @param modcode Modifier key code to update.
* @param release Whether the key or button event is a release.
* @param flags Bit flags that modify the requested operation.
*/
void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags);
void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state);
/**
* @brief Submit UTF-8 text input to the keyboard backend.
*
* @param input Platform input backend that receives the event.
* @param utf8 UTF-8 text submitted by the client.
* @param size Number of bytes or elements requested.
*/
void unicode(input_t &input, char *utf8, int size);
/**
* @brief Per-client input context allocated by a platform backend.
*/
typedef deinit_t client_input_t;
/**
@@ -833,6 +1241,12 @@
* @return 0 on success.
*/
int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue);
/**
* @brief Release gamepad resources.
*
* @param input Platform input backend that receives the event.
* @param nr Controller index assigned by the client.
*/
void free_gamepad(input_t &input, int nr);
/**
@@ -841,13 +1255,18 @@
*/
platform_caps::caps_t get_capabilities();
constexpr auto SERVICE_NAME = "Sunshine"; ///< mDNS service instance name advertised for GameStream discovery.
constexpr auto SERVICE_TYPE = "_nvstream._tcp"; ///< mDNS service type advertised for GameStream discovery.
constexpr auto SERVICE_NAME = "Sunshine";
constexpr auto SERVICE_TYPE = "_nvstream._tcp";
namespace publish {
[[nodiscard]] std::unique_ptr<deinit_t> start();
}
/**
* @brief Initialize the platform-specific high precision timer.
*
* @return Cleanup handle for initialized platform resources, or null if none are needed.
*/
[[nodiscard]] std::unique_ptr<deinit_t> init();
/**
@@ -873,5 +1292,8 @@
*/
std::vector<supported_gamepad_t> &supported_gamepads(input_t *input);
/**
* @brief Platform timer object used for precise frame pacing.
*/
struct high_precision_timer: private boost::noncopyable {
virtual ~high_precision_timer() = default;
src/platform/linux/audio.cpp +169 −8
@@ -22,6 +22,9 @@
namespace platf {
using namespace std::literals;
/**
* @brief Position mapping.
*/
constexpr pa_channel_position_t position_mapping[] {
PA_CHANNEL_POSITION_FRONT_LEFT,
PA_CHANNEL_POSITION_FRONT_RIGHT,
@@ -33,5 +36,13 @@
PA_CHANNEL_POSITION_SIDE_RIGHT,
};
/**
* @brief Convert a PulseAudio operation result to a log string.
*
* @param name Human-readable name to assign.
* @param mapping Opus channel mapping table for the requested layout.
* @param channels Number of audio channels in the stream.
* @return Value converted to string.
*/
std::string to_string(const char *name, const std::uint8_t *mapping, int channels) {
std::stringstream ss;
@@ -50,8 +61,17 @@
return result;
}
/**
* @brief PulseAudio recording stream and channel metadata.
*/
struct mic_attr_t: public mic_t {
util::safe_ptr<pa_simple, pa_simple_free> mic;
util::safe_ptr<pa_simple, pa_simple_free> mic; ///< PulseAudio simple recording stream for microphone capture.
/**
* @brief Deliver a captured audio sample to Sunshine's audio pipeline.
*
* @param sample_buf Sample buf.
* @return Capture status reported to the streaming pipeline.
*/
capture_e sample(std::vector<float> &sample_buf) override {
auto sample_size = sample_buf.size();
@@ -68,5 +88,15 @@
}
};
/**
* @brief Create a microphone capture stream for the requested layout.
*
* @param mapping Opus channel mapping table for the requested layout.
* @param channels Number of audio channels in the stream.
* @param sample_rate Audio sample rate in hertz.
* @param frame_size Number of samples captured per audio frame.
* @param source_name Source name.
* @return Microphone capture object for the requested audio layout.
*/
std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, std::string source_name) {
auto mic = std::make_unique<mic_attr_t>();
@@ -106,32 +136,74 @@
template<bool B, class T>
struct add_const_helper;
/**
* @brief Template helper that preserves constness for const inputs.
*/
template<class T>
struct add_const_helper<true, T> {
/**
* @brief PulseAudio object type passed to the safe pointer wrapper.
*/
using type = const std::remove_pointer_t<T> *;
};
/**
* @brief Template helper that leaves non-const inputs mutable.
*/
template<class T>
struct add_const_helper<false, T> {
/**
* @brief PulseAudio object type passed to the safe pointer wrapper.
*/
using type = const T *;
};
/**
* @brief PulseAudio callback info type with pointer constness normalized.
*/
template<class T>
using add_const_t = typename add_const_helper<std::is_pointer_v<T>, T>::type;
/**
* @brief Release memory allocated by PulseAudio.
*
* @param p Pointer allocated by PulseAudio and released with `pa_xfree`.
*/
template<class T>
void pa_free(T *p) {
pa_xfree(p);
}
/**
* @brief Owning pointer for a PulseAudio context.
*/
using ctx_t = util::safe_ptr<pa_context, pa_context_unref>;
/**
* @brief Owning pointer for a PulseAudio mainloop.
*/
using loop_t = util::safe_ptr<pa_mainloop, pa_mainloop_free>;
/**
* @brief Owning pointer for a PulseAudio asynchronous operation.
*/
using op_t = util::safe_ptr<pa_operation, pa_operation_unref>;
/**
* @brief Owning pointer for PulseAudio strings allocated with `pa_xmalloc`.
*/
using string_t = util::safe_ptr<char, pa_free<char>>;
/**
* @brief Callback wrapper for PulseAudio introspection results without an end marker.
*/
template<class T>
using cb_simple_t = std::function<void(ctx_t::pointer, add_const_t<T> i)>;
/**
* @brief Handle PulseAudio sink-input introspection results.
*
* @param ctx Native context object used by the operation or callback.
* @param i PulseAudio introspection info supplied to the callback.
* @param userdata Caller-provided pointer passed through the callback.
*/
template<class T>
void cb(ctx_t::pointer ctx, add_const_t<T> i, void *userdata) {
auto &f = *(cb_simple_t<T> *) userdata;
@@ -141,9 +213,20 @@
f(ctx, i);
}
/**
* @brief Callback wrapper for PulseAudio introspection results with an end marker.
*/
template<class T>
using cb_t = std::function<void(ctx_t::pointer, add_const_t<T> i, int eol)>;
/**
* @brief Handle PulseAudio source introspection results.
*
* @param ctx Native context object used by the operation or callback.
* @param i PulseAudio introspection info supplied to the callback.
* @param eol PulseAudio end-of-list marker.
* @param userdata Caller-provided pointer passed through the callback.
*/
template<class T>
void cb(ctx_t::pointer ctx, add_const_t<T> i, int eol, void *userdata) {
auto &f = *(cb_t<T> *) userdata;
@@ -156,17 +239,37 @@
f(ctx, i, eol);
}
/**
* @brief Forward a PulseAudio integer callback value into a Sunshine alarm.
*
* @param ctx PulseAudio context that emitted the callback.
* @param i Integer value returned by the PulseAudio operation.
* @param userdata Caller-provided pointer passed through the callback.
*/
void cb_i(ctx_t::pointer ctx, std::uint32_t i, void *userdata) {
auto alarm = (safe::alarm_raw_t<int> *) userdata;
alarm->ring(i);
}
/**
* @brief Translate PulseAudio context state changes into server events.
*
* @param ctx Native context object used by the operation or callback.
* @param userdata Caller-provided pointer passed through the callback.
*/
void ctx_state_cb(ctx_t::pointer ctx, void *userdata) {
auto &f = *(std::function<void(ctx_t::pointer)> *) userdata;
f(ctx);
}
/**
* @brief Record completion of a PulseAudio asynchronous operation.
*
* @param ctx Native context object used by the operation or callback.
* @param status Native status code returned by the platform API.
* @param userdata Caller-provided pointer passed through the callback.
*/
void success_cb(ctx_t::pointer ctx, int status, void *userdata) {
assert(userdata != nullptr);
@@ -175,6 +278,9 @@
alarm->ring(status ? 0 : 1);
}
/**
* @brief PulseAudio server controller that creates and removes Sunshine sinks.
*/
class server_t: public audio_control_t {
enum ctx_event_e : int {
ready,
@@ -183,21 +289,26 @@
};
public:
loop_t loop;
ctx_t ctx;
std::string requested_sink;
loop_t loop; ///< PulseAudio threaded mainloop instance.
ctx_t ctx; ///< PulseAudio threaded mainloop context.
std::string requested_sink; ///< Requested sink.
struct {
std::uint32_t stereo = PA_INVALID_INDEX;
std::uint32_t surround51 = PA_INVALID_INDEX;
std::uint32_t surround71 = PA_INVALID_INDEX;
} index;
} index; ///< PulseAudio module indexes for Sunshine-created null sinks.
std::unique_ptr<safe::event_t<ctx_event_e>> events; ///< Event queue receiving PulseAudio context state changes.
std::unique_ptr<safe::event_t<ctx_event_e>> events;
std::unique_ptr<std::function<void(ctx_t::pointer)>> events_cb;
std::unique_ptr<std::function<void(ctx_t::pointer)>> events_cb; ///< Callback that translates PulseAudio context updates into events.
std::thread worker;
std::thread worker; ///< Thread running the PulseAudio mainloop.
/**
* @brief Initialize PulseAudio mainloop, context, and Sunshine null sinks.
*
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init() {
events = std::make_unique<safe::event_t<ctx_event_e>>();
loop.reset(pa_mainloop_new());
@@ -255,5 +366,13 @@
return 0;
}
/**
* @brief Create a PulseAudio null sink for one channel layout.
*
* @param name Human-readable name to assign.
* @param channel_mapping Channel mapping.
* @param channels Number of audio channels in the stream.
* @return PulseAudio module index for the new sink, or PA_INVALID_INDEX on failure.
*/
int load_null(const char *name, const std::uint8_t *channel_mapping, int channels) {
auto alarm = safe::make_alarm<int>();
@@ -272,6 +391,12 @@
return *alarm->status();
}
/**
* @brief Unload a Sunshine-created PulseAudio null sink.
*
* @param i PulseAudio introspection info supplied to the callback.
* @return 0 when the sink is absent or unloaded; nonzero on PulseAudio failure.
*/
int unload_null(std::uint32_t i) {
if (i == PA_INVALID_INDEX) {
return 0;
@@ -293,6 +418,11 @@
return 0;
}
/**
* @brief Query host and virtual sink names available to Sunshine.
*
* @return Host and virtual sink names when the backend can report them.
*/
std::optional<sink_t> sink_info() override {
constexpr auto stereo = "sink-sunshine-stereo";
constexpr auto surround51 = "sink-sunshine-surround51";
@@ -388,6 +518,11 @@
return std::make_optional(std::move(sink));
}
/**
* @brief Get default sink name.
*
* @return PulseAudio name of the current default sink, or an empty string.
*/
std::string get_default_sink_name() {
std::string sink_name;
auto alarm = safe::make_alarm<int>();
@@ -410,6 +545,12 @@
return sink_name;
}
/**
* @brief Get monitor name.
*
* @param sink_name Sink name.
* @return PulseAudio monitor source name for the supplied sink, or an empty string.
*/
std::string get_monitor_name(const std::string &sink_name) {
std::string monitor_name;
auto alarm = safe::make_alarm<int>();
@@ -441,6 +582,17 @@
return monitor_name;
}
/**
* @brief Create a microphone capture stream for the requested layout.
*
* @param mapping Opus channel mapping table for the requested layout.
* @param channels Number of audio channels in the stream.
* @param sample_rate Audio sample rate in hertz.
* @param frame_size Number of samples captured per audio frame.
* @param continuous_audio Continuous audio.
* @param host_audio_enabled Whether host playback should remain enabled during capture.
* @return Microphone capture object for the requested audio layout.
*/
std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, bool continuous_audio, [[maybe_unused]] bool host_audio_enabled) override {
// Sink choice priority:
// 1. Config sink
@@ -465,5 +617,11 @@
return true;
}
/**
* @brief Update the sink value on the backend.
*
* @param sink Audio sink name to route or capture.
* @return Status from updating sink.
*/
int set_sink(const std::string &sink) override {
auto alarm = safe::make_alarm<int>();
@@ -514,5 +672,8 @@
};
} // namespace pa
/**
* @brief Create the platform audio controller.
*/
std::unique_ptr<audio_control_t> audio_control() {
auto audio = std::make_unique<pa::server_t>();
src/platform/linux/cuda.cpp +269 −40
@@ -26,12 +26,28 @@
#include "src/video.h"
#include "wayland.h"
/**
* @def SUNSHINE_STRINGVIEW_HELPER(x)
* @brief Macro for SUNSHINE STRINGVIEW HELPER.
*/
#define SUNSHINE_STRINGVIEW_HELPER(x) x##sv
/**
* @def SUNSHINE_STRINGVIEW(x)
* @brief Macro for SUNSHINE STRINGVIEW.
*/
#define SUNSHINE_STRINGVIEW(x) SUNSHINE_STRINGVIEW_HELPER(x)
/**
* @def CU_CHECK(x, y)
* @brief Macro for CU CHECK.
*/
#define CU_CHECK(x, y) \
if (check((x), SUNSHINE_STRINGVIEW(y ": "))) \
return -1
/**
* @def CU_CHECK_IGNORE(x, y)
* @brief Macro for CU CHECK IGNORE.
*/
#define CU_CHECK_IGNORE(x, y) \
check((x), SUNSHINE_STRINGVIEW(y ": "))
@@ -41,17 +57,32 @@
using namespace std::literals;
namespace cuda {
constexpr auto cudaDevAttrMaxThreadsPerBlock = (CUdevice_attribute) 1; ///< CUDA dev attr max threads per block.
constexpr auto cudaDevAttrMaxThreadsPerMultiProcessor = (CUdevice_attribute) 39; ///< CUDA dev attr max threads per multi processor.
constexpr auto cudaDevAttrMaxThreadsPerBlock = (CUdevice_attribute) 1;
constexpr auto cudaDevAttrMaxThreadsPerMultiProcessor = (CUdevice_attribute) 39;
/**
* @brief Convert a CUDA result code into Sunshine's capture status.
*
* @param sv String view containing the text to inspect.
* @param name Human-readable name to assign.
* @param description Human-readable description used in log output.
*/
void pass_error(const std::string_view &sv, const char *name, const char *description) {
BOOST_LOG(error) << sv << name << ':' << description;
}
/**
* @brief Release a Core Foundation object when the wrapper is destroyed.
*
* @param cf Core Foundation object passed to the scoped releaser.
*/
void cff(CudaFunctions *cf) {
cuda_free_functions(&cf);
}
/**
* @brief Handle to a CUDA dynamic-library function table.
*/
using cdf_t = util::safe_ptr<CudaFunctions, cff>;
static cdf_t cdf;
@@ -71,21 +102,42 @@
return 0;
}
/**
* @brief Release stream resources.
*
* @param stream CUDA stream or PipeWire stream involved in the operation.
*/
void freeStream(CUstream stream) {
CU_CHECK_IGNORE(cdf->cuStreamDestroy(stream), "Couldn't destroy cuda stream");
}
/**
* @brief Unregister a CUDA graphics resource if it is still registered.
*
* @param resource CUDA graphics resource being mapped or unmapped.
*/
void unregisterResource(CUgraphicsResource resource) {
CU_CHECK_IGNORE(cdf->cuGraphicsUnregisterResource(resource), "Couldn't unregister resource");
}
/**
* @brief CUDA graphics resource pointer released with `cuGraphicsUnregisterResource`.
*/
using registered_resource_t = util::safe_ptr<CUgraphicsResource_st, unregisterResource>;
/**
* @brief CUDA image wrapper that owns mapped graphics resources for one frame.
*/
class img_t: public platf::img_t {
public:
tex_t tex; ///< CUDA texture object used as the conversion source.
tex_t tex;
};
/**
* @brief Map CUDA graphics resources for use as an image.
*
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init() {
auto status = cuda_load_functions(&cdf, nullptr);
if (status) {
@@ -99,8 +151,18 @@
return 0;
}
/**
* @brief CUDA encode device that imports captured frames into CUDA memory.
*/
class cuda_t: public platf::avcodec_encode_device_t {
public:
/**
* @brief Initialize the CUDA device context used for frame conversion.
*
* @param in_width In width.
* @param in_height In height.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(int in_width, int in_height) {
if (!cdf) {
BOOST_LOG(warning) << "cuda not initialized"sv;
@@ -115,6 +177,13 @@
return 0;
}
/**
* @brief Attach frame resources used by the next conversion or encode operation.
*
* @param frame Video or graphics frame being processed.
* @param hw_frames_ctx FFmpeg hardware frames context associated with the frame.
* @return Status from updating frame.
*/
int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override {
this->hwframe.reset(frame);
this->frame = frame;
@@ -157,5 +226,8 @@
return 0;
}
/**
* @brief Apply the configured colorspace metadata to the active frame.
*/
void apply_colorspace() override {
sws.apply_colorspace(colorspace);
@@ -189,26 +261,41 @@
}
}
/**
* @brief Select the CUDA texture object for the configured filtering mode.
*
* @param tex Texture resource used by the converter.
* @return CUDA texture object using linear or point sampling.
*/
cudaTextureObject_t tex_obj(const tex_t &tex) const {
return linear_interpolation ? tex.texture.linear : tex.texture.point;
}
stream_t stream; ///< CUDA stream used for asynchronous conversion work.
frame_t hwframe; ///< FFmpeg hardware frame backed by CUDA resources.
stream_t stream;
frame_t hwframe;
int height;
int height; ///< Frame or display height in pixels.
int width; ///< Frame or display width in pixels.
int width;
// When height and width don't change, it's not necessary to use linear interpolation
bool linear_interpolation;
bool linear_interpolation; ///< Whether the CUDA converter uses linear interpolation.
bool is_yuv444; ///< Whether the CUDA converter outputs YUV 4:4:4.
bool is_yuv444;
sws_t sws;
sws_t sws; ///< Software scaler used for CUDA frame conversion fallback paths.
};
/**
* @brief CUDA encode device path that converts frames through system memory.
*/
class cuda_ram_t: public cuda_t {
public:
/**
* @brief Convert a captured frame through CUDA into system-memory encoder input.
*
* @param img Image or frame object to read from or populate.
* @return Conversion status.
*/
int convert(platf::img_t &img) override {
if (is_yuv444) {
return sws.load_ram(img, tex.array) || sws.convert_yuv444(frame->data[0], frame->data[1], frame->data[2], frame->linesize[0], tex_obj(tex), stream.get());
@@ -216,6 +303,13 @@
return sws.load_ram(img, tex.array) || sws.convert_nv12(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex_obj(tex), stream.get());
}
/**
* @brief Attach frame resources used by the next conversion or encode operation.
*
* @param frame Video or graphics frame being processed.
* @param hw_frames_ctx FFmpeg hardware frames context associated with the frame.
* @return Status from updating frame.
*/
int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override {
if (cuda_t::set_frame(frame, hw_frames_ctx)) {
return -1;
@@ -231,11 +325,20 @@
return 0;
}
tex_t tex;
tex_t tex; ///< CUDA texture object used as the conversion source.
};
/**
* @brief CUDA encode device path that keeps converted frames in GPU memory.
*/
class cuda_vram_t: public cuda_t {
public:
/**
* @brief Convert a captured frame through CUDA into GPU encoder input.
*
* @param img Image or frame object to read from or populate.
* @return Conversion status.
*/
int convert(platf::img_t &img) override {
if (is_yuv444) {
return sws.convert_yuv444(frame->data[0], frame->data[1], frame->data[2], frame->linesize[0], tex_obj(((img_t *) &img)->tex), stream.get());
@@ -290,13 +393,19 @@
return -1;
}
/**
* @brief CUDA frame resources registered for interop conversion.
*/
struct cu_resources {
registered_resource_t y_res; ///< Y res.
registered_resource_t u_res; ///< U res.
registered_resource_t v_res; ///< V res.
registered_resource_t uv_res; ///< Uv res.
registered_resource_t y_res;
registered_resource_t u_res;
registered_resource_t v_res;
registered_resource_t uv_res;
};
/**
* @brief OpenGL/CUDA interop resources used for GPU-side frame conversion.
*/
class gl_cuda_vram_t: public platf::avcodec_encode_device_t {
public:
/**
@@ -509,34 +618,42 @@
sws.apply_colorspace(colorspace, is_yuv444);
}
file_t file;
gbm::gbm_t gbm;
egl::display_t display;
file_t file; ///< File descriptor for the imported DMA-BUF.
gbm::gbm_t gbm; ///< GBM device used for buffer allocation..
egl::display_t display; ///< EGL display used to import captured frames.
egl::ctx_t ctx; ///< EGL context used to import captured frames.
egl::ctx_t ctx;
// This must be destroyed before display_t
stream_t stream; ///< CUDA stream used for asynchronous conversion work.
frame_t hwframe; ///< FFmpeg hardware frame backed by CUDA resources.
stream_t stream;
frame_t hwframe;
egl::sws_t sws; ///< Software scaler used for CUDA frame conversion fallback paths.
egl::nv12_t nv12; ///< EGL/OpenGL resources used for NV12 output frames.
egl::yuv444_t yuv444; ///< EGL/OpenGL resources used for YUV444 output frames.
AVPixelFormat sw_format; ///< FFmpeg software pixel format produced by conversion.
egl::sws_t sws;
egl::nv12_t nv12;
egl::yuv444_t yuv444;
AVPixelFormat sw_format;
int height;
int width;
int height; ///< Frame or display height in pixels.
int width; ///< Frame or display width in pixels.
std::uint64_t sequence;
egl::rgb_t rgb;
std::uint64_t sequence; ///< Capture sequence number associated with the frame.
egl::rgb_t rgb; ///< Imported RGB source image used before CUDA conversion.
cu_resources cu_res; ///< CUDA graphics resources registered for the current frame.
cu_resources cu_res;
int offset_x;
int offset_y;
int offset_x; ///< Horizontal offset in physical pixels.
int offset_y; ///< Vertical offset in physical pixels.
bool is_yuv444;
bool is_yuv444; ///< Whether the CUDA converter outputs YUV 4:4:4.
};
/**
* @brief Create AVCodec encode device.
*
* @param width Frame or display width in pixels.
* @param height Frame or display height in pixels.
* @param vram Whether the image should use GPU memory instead of system memory.
* @return Constructed AVCodec encode device object.
*/
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device(int width, int height, bool vram) {
if (init()) {
return nullptr;
@@ -589,5 +706,10 @@
static void *handle {nullptr};
/**
* @brief Load NvFBC and create the CUDA capture helper.
*
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init() {
static bool funcs_loaded = false;
@@ -627,7 +749,15 @@
return 0;
}
/**
* @brief NvFBC CUDA context selected for a capture session.
*/
class ctx_t {
public:
/**
* @brief Create an NvFBC session context for a native capture handle.
*
* @param handle Native library or object handle used by the operation.
*/
ctx_t(NVFBC_SESSION_HANDLE handle) {
NVFBC_BIND_CONTEXT_PARAMS params {NVFBC_BIND_CONTEXT_PARAMS_VER};
@@ -646,9 +776,12 @@
}
}
NVFBC_SESSION_HANDLE handle; ///< NVIDIA FBC capture session handle.
NVFBC_SESSION_HANDLE handle;
};
/**
* @brief NvFBC dynamic-library handle and function table.
*/
class handle_t {
enum flag_e {
SESSION_HANDLE,
@@ -659,12 +792,23 @@
public:
handle_t() = default;
/**
* @brief Move an NvFBC API handle and its resolved function table.
*
* @param other Source object whose state is copied or moved into this object.
*/
handle_t(handle_t &&other):
handle_flags {other.handle_flags},
handle {other.handle} {
other.handle_flags.reset();
}
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @param other Source object whose state is copied or moved into this object.
* @return Reference or value produced by the operator.
*/
handle_t &operator=(handle_t &&other) {
std::swap(handle_flags, other.handle_flags);
std::swap(handle, other.handle);
@@ -672,5 +816,10 @@
return *this;
}
/**
* @brief Allocate the underlying object and wrap it in the owning handle.
*
* @return Created backend object, or null when creation fails.
*/
static std::optional<handle_t> make() {
NVFBC_CREATE_HANDLE_PARAMS params {NVFBC_CREATE_HANDLE_PARAMS_VER};
@@ -694,9 +843,19 @@
return handle;
}
/**
* @brief Read the last error string from the active NvFBC session.
*
* @return Human-readable NvFBC error string.
*/
const char *last_error() {
return func.nvFBCGetLastErrorStr(handle);
}
/**
* @brief Return or update the current status value.
*
* @return Status status.
*/
std::optional<NVFBC_GET_STATUS_PARAMS> status() {
NVFBC_GET_STATUS_PARAMS params {NVFBC_GET_STATUS_PARAMS_VER};
@@ -711,6 +870,12 @@
return params;
}
/**
* @brief Run the capture loop for this backend.
*
* @param capture_params Capture params.
* @return Capture status reported to the streaming pipeline.
*/
int capture(NVFBC_CREATE_CAPTURE_SESSION_PARAMS &capture_params) {
if (func.nvFBCCreateCaptureSession(handle, &capture_params)) {
BOOST_LOG(error) << "Failed to start capture session: "sv << last_error();
@@ -731,6 +896,11 @@
return 0;
}
/**
* @brief Release the NvFBC capture session and wait for capture work to stop.
*
* @return Stop status.
*/
int stop() {
if (!handle_flags[SESSION_CAPTURE]) {
return 0;
@@ -749,6 +919,11 @@
return 0;
}
/**
* @brief Reset the object to its initial empty state.
*
* @return Reset status.
*/
int reset() {
if (!handle_flags[SESSION_HANDLE]) {
return 0;
@@ -772,13 +947,23 @@
reset();
}
std::bitset<MAX_FLAGS> handle_flags; ///< Handle flags.
std::bitset<MAX_FLAGS> handle_flags;
NVFBC_SESSION_HANDLE handle;
NVFBC_SESSION_HANDLE handle; ///< NVIDIA FBC capture session handle.
};
/**
* @brief NvFBC display capture backend that produces CUDA frames.
*/
class display_t: public platf::display_t {
public:
/**
* @brief Initialize NvFBC capture for the selected display.
*
* @param display_name Display name.
* @param config Configuration values to apply.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(const std::string_view &display_name, const ::video::config_t &config) {
auto handle = handle_t::make();
if (!handle) {
@@ -900,6 +1085,12 @@
}
// Reinitialize the capture session.
/**
* @brief Reinitialize NvFBC capture after a recoverable failure.
*
* @param cursor Cursor image or visibility state to composite.
* @return Reinit status.
*/
platf::capture_e reinit(bool cursor) {
if (handle.stop()) {
return platf::capture_e::error;
@@ -968,6 +1159,15 @@
return platf::capture_e::ok;
}
/**
* @brief Capture a display frame into the provided image object.
*
* @param pull_free_image_cb Callback that provides an available image buffer.
* @param img_out Captured CUDA image returned to the streaming pipeline.
* @param timeout Maximum time to wait for the operation.
* @param cursor Cursor image or visibility state to composite.
* @return Capture status reported to the streaming pipeline.
*/
platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor) {
if (cursor != cursor_visible) {
auto status = reinit(cursor);
@@ -1008,9 +1208,20 @@
return platf::capture_e::ok;
}
/**
* @brief Create AVCodec encode device.
*
* @param pix_fmt Sunshine pixel format to convert or allocate for.
* @return Constructed AVCodec encode device object.
*/
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override {
return ::cuda::make_avcodec_encode_device(width, height, true);
}
/**
* @brief Allocate an image buffer compatible with this display backend.
*
* @return Allocated img object, or null when unavailable.
*/
std::shared_ptr<platf::img_t> alloc_img() override {
auto img = std::make_shared<cuda::img_t>();
@@ -1031,21 +1242,34 @@
return img;
};
/**
* @brief Populate a fallback image when real capture data is unavailable.
*
* @return Capture status reported to the streaming pipeline.
*/
int dummy_img(platf::img_t *) override {
return 0;
}
std::chrono::nanoseconds delay;
std::chrono::nanoseconds delay; ///< Delay before the timer task becomes eligible to run.
bool cursor_visible;
bool cursor_visible; ///< Whether the cursor should be included in the capture.
handle_t handle; ///< NvFBC capture handle owning the active capture session.
handle_t handle;
NVFBC_CREATE_CAPTURE_SESSION_PARAMS capture_params;
NVFBC_CREATE_CAPTURE_SESSION_PARAMS capture_params; ///< NvFBC capture-session parameters used for frame grabs.
};
} // namespace nvfbc
} // namespace cuda
namespace platf {
/**
* @brief Create an NvFBC CUDA display capture backend.
*
* @param hwdevice_type Hardware device type requested for capture or encode.
* @param display_name Display name.
* @param config Configuration values to apply.
* @return Display backend, or nullptr when NvFBC/CUDA initialization fails.
*/
std::shared_ptr<display_t> nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) {
if (hwdevice_type != mem_type_e::cuda) {
BOOST_LOG(error) << "Could not initialize nvfbc display with the given hw device type"sv;
@@ -1061,6 +1285,11 @@
return display;
}
/**
* @brief Enumerate display names exposed by NvFBC.
*
* @return NvFBC display names, or an empty list when enumeration fails.
*/
std::vector<std::string> nvfbc_display_names() {
if (cuda::init() || cuda::nvfbc::init()) {
return {};
src/platform/linux/cuda.cu +10 −7
@@ -226,6 +226,6 @@
float scale,
const viewport_t viewport,
const cuda_color_t *const color_matrix
) {
) {
int idX = threadIdx.x + blockDim.x * blockIdx.x;
int idY = threadIdx.y + blockDim.y * blockIdx.y;
@@ -252,7 +252,6 @@
dstY[0] = calcY(rgb, color_matrix) * 255.0f;
dstU[0] = calcU(rgb, color_matrix) * 255.0f;
dstV[0] = calcV(rgb, color_matrix) * 255.0f;
}
int tex_t::copy(std::uint8_t *src, int height, int pitch) {
@@ -383,8 +382,6 @@
return convert_yuv444(Y, U, V, pitch, texture, stream, viewport);
}
int sws_t::convert_yuv444(std::uint8_t *Y, std::uint8_t *U, std::uint8_t *V, std::uint32_t pitch,
int sws_t::convert_yuv444(std::uint8_t *Y, std::uint8_t *U, std::uint8_t *V, std::uint32_t pitch, cudaTextureObject_t texture, stream_t::pointer stream, const viewport_t &viewport) {
cudaTextureObject_t texture, stream_t::pointer stream,
const viewport_t &viewport) {
int threadsX = viewport.width;
int threadsY = viewport.height;
@@ -393,8 +390,14 @@
dim3 grid(div_align(threadsX, threadsPerBlock), threadsY);
RGBA_to_YUV444<<<grid, block, 0, stream>>>(
texture,
Y,
texture, Y, U, V, pitch, scale, viewport,
(cuda_color_t *) color_matrix.get()
U,
V,
pitch,
scale,
viewport,
(cuda_color_t *) color_matrix.get()
);
return CU_CHECK_IGNORE(cudaGetLastError(), "RGBA_to_YUV444 failed");
src/platform/linux/graphics.cpp +103 −13
@@ -24,25 +24,47 @@
// There aren't that many DRM_FORMAT I need to use, so define them here
//
// They aren't likely to change any time soon.
/**
* @def fourcc_code(a, b, c, d)
* @brief Macro for fourcc code.
*/
#define fourcc_code(a, b, c, d) ((std::uint32_t) (a) | ((std::uint32_t) (b) << 8) | ((std::uint32_t) (c) << 16) | ((std::uint32_t) (d) << 24))
/**
* @def fourcc_mod_code(vendor, val)
* @brief Macro for fourcc mod code.
*/
#define fourcc_mod_code(vendor, val) ((((uint64_t) vendor) << 56) | ((val) & 0x00ffffffffffffffULL))
/**
* @def DRM_FORMAT_MOD_INVALID
* @brief Macro for DRM FORMAT MOD INVALID.
*/
#define DRM_FORMAT_MOD_INVALID fourcc_mod_code(0, ((1ULL << 56) - 1))
#if !defined(SUNSHINE_SHADERS_DIR) // for testing this needs to be defined in cmake as we don't do an install
/**
* @def SUNSHINE_SHADERS_DIR
* @brief Macro for SUNSHINE SHADERS DIR.
*/
#define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/opengl"
#endif
using namespace std::literals;
namespace gl {
GladGLContext ctx;
GladGLContext ctx; ///< Loaded OpenGL function table for the active context.
static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC egl_image_target_texture_2d_fn = nullptr;
/**
* @brief Bind an EGL image to the current OpenGL texture target.
*/
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC egl_image_target_texture_2d() {
return egl_image_target_texture_2d_fn;
}
/**
* @brief Drain and log pending OpenGL errors.
*/
void drain_errors(const std::string_view &prefix) {
GLenum err;
while ((err = ctx.GetError()) != GL_NO_ERROR) {
@@ -275,9 +297,12 @@
} // namespace gl
namespace gbm {
device_destroy_fn device_destroy; ///< Device destroy.
create_device_fn create_device; ///< Create device.
device_destroy_fn device_destroy;
create_device_fn create_device;
/**
* @brief Load GBM symbols required for EGL device creation.
*/
int init() {
static void *handle {nullptr};
static bool funcs_loaded = false;
@@ -309,13 +334,13 @@
namespace egl {
/**
* @brief Log EGL failure details and return an error code.
*/
bool fail() {
return eglGetError() != EGL_SUCCESS;
}
/**
* @memberof egl::display_t
*/
display_t make_display(std::variant<gbm::gbm_t::pointer, wl_display *, _XDisplay *> native_display) {
int egl_platform;
void *native_display_p;
@@ -391,6 +416,9 @@
return display;
}
/**
* @brief Create an EGL/OpenGL context for capture or conversion.
*/
std::optional<ctx_t> make_ctx(display_t::pointer display) {
bool nice_warning = false;
#if !defined(__FreeBSD__)
@@ -503,14 +531,23 @@
return ctx;
}
/**
* @brief EGL attribute pair describing one DMA-BUF plane.
*/
struct plane_attr_t {
EGLAttrib fd; ///< EGL attribute key for a plane file descriptor.
EGLAttrib fd;
EGLAttrib offset;
EGLAttrib pitch;
EGLAttrib lo;
EGLAttrib hi;
EGLAttrib offset; ///< Offset.
EGLAttrib pitch; ///< Pitch.
EGLAttrib lo; ///< Lo.
EGLAttrib hi; ///< Hi.
};
/**
* @brief Build EGL attributes for one DMA-BUF plane.
*
* @param plane_indice Zero-based plane index in the DMA-BUF descriptor.
* @return EGL attribute keys for that plane's file descriptor, offset, pitch, and modifier.
*/
inline plane_attr_t get_plane(std::uint32_t plane_indice) {
switch (plane_indice) {
case 0:
@@ -593,5 +630,12 @@
return attribs;
}
/**
* @brief Import the source frame texture for EGL/OpenGL conversion.
*
* @param egl_display EGL display used to create the image.
* @param xrgb XRGB surface descriptor to import.
* @return Imported RGB image, or empty when import fails.
*/
std::optional<rgb_t> import_source(display_t::pointer egl_display, const surface_descriptor_t &xrgb) {
auto attribs = surface_descriptor_to_egl_attribs(xrgb);
@@ -652,8 +696,13 @@
}
// Constants for clear black color Y, U, V. U & V are same so:
const float y_black[] = {0.0f, 0.0f, 0.0f, 0.0f};
const float uv_black[] = {0.5f, 0.5f, 0.5f, 0.5f};
const float y_black[] = {0.0f, 0.0f, 0.0f, 0.0f}; ///< Y black.
const float uv_black[] = {0.5f, 0.5f, 0.5f, 0.5f}; ///< Uv black.
/**
* @brief Bind NV12 target framebuffers to their Y and UV plane textures.
*
* @param nv12 Imported NV12 target whose textures receive rendered output.
*/
void nv12_bind_framebuffers(nv12_t &nv12) {
constexpr std::array<GLenum, 2> attachments {{GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1}};
@@ -669,5 +718,10 @@
gl_drain_errors;
}
/**
* @brief Bind YUV444 target framebuffers to their Y, U, and V plane textures.
*
* @param yuv444 Imported YUV444 target whose textures receive rendered output.
*/
void yuv44_bind_framebuffers(yuv444_t &yuv444) {
constexpr std::array<GLenum, 3> attachments {{GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2}};
@@ -683,6 +737,15 @@
gl_drain_errors;
}
/**
* @brief Import the output frame target for EGL/OpenGL conversion.
*
* @param egl_display EGL display used to create the images.
* @param fds File descriptors backing the NV12 image.
* @param y Luma plane descriptor.
* @param uv Chroma plane descriptor.
* @return Imported NV12 image, or empty when import fails.
*/
std::optional<nv12_t> import_target(display_t::pointer egl_display, std::array<file_t, nv12_img_t::num_fds> &&fds, const surface_descriptor_t &y, const surface_descriptor_t &uv) {
auto y_attribs = surface_descriptor_to_egl_attribs(y);
auto uv_attribs = surface_descriptor_to_egl_attribs(uv);
@@ -719,6 +782,16 @@
return nv12;
}
/**
* @brief Import a YUV444 target image from DMA-BUF descriptors.
*
* @param egl_display EGL display used to create the images.
* @param fds File descriptors backing the YUV444 image.
* @param y Luma plane descriptor.
* @param u U chroma plane descriptor.
* @param v V chroma plane descriptor.
* @return Imported YUV444 image, or empty when import fails.
*/
std::optional<yuv444_t> import_target_yuv444(
display_t::pointer egl_display,
std::array<file_t, yuv444_img_t::num_fds> &&fds,
@@ -811,6 +884,9 @@
return nv12;
}
/**
* @brief Create YUV444 target.
*/
std::optional<yuv444_t> create_yuv444_target(int width, int height, AVPixelFormat format) {
yuv444_t yuv444 {
EGL_NO_DISPLAY,
@@ -876,6 +952,15 @@
}
}
/**
* @brief Configure the EGL/OpenGL scaling and colorspace conversion pipeline.
*
* @param sws Software-scaling pipeline to configure.
* @param color_p Color p.
* @param tex Texture resource used by the converter.
* @param is_yuv444 Is YUV444.
* @return 0 when shaders, framebuffers, and color uniforms are ready; nonzero on failure.
*/
int configure_sws_pipeline(sws_t &sws, const video::color_t *color_p, gl::tex_t &&tex, bool is_yuv444) {
std::array<std::pair<const char *, std::string_view>, 5> members {{
std::make_pair("color_vec_y", util::view(color_p->color_vec_y)),
@@ -1310,6 +1395,11 @@
}
} // namespace egl
/**
* @brief Release an FFmpeg frame allocated by the capture or conversion backend.
*
* @param frame Video or graphics frame being processed.
*/
void free_frame(AVFrame *frame) {
av_frame_free(&frame);
}
src/platform/linux/graphics.h +428 −56
@@ -19,19 +19,49 @@
#include "src/utility.h"
#include "src/video_colorspace.h"
/**
* @def SUNSHINE_STRINGIFY_HELPER(x)
* @brief Macro for SUNSHINE STRINGIFY HELPER.
*/
#define SUNSHINE_STRINGIFY_HELPER(x) #x
/**
* @def SUNSHINE_STRINGIFY(x)
* @brief Macro for SUNSHINE STRINGIFY.
*/
#define SUNSHINE_STRINGIFY(x) SUNSHINE_STRINGIFY_HELPER(x)
/**
* @def gl_drain_errors_helper(x)
* @brief Macro for gl drain errors helper.
*/
#define gl_drain_errors_helper(x) gl::drain_errors(x)
/**
* @def gl_drain_errors
* @brief Macro for gl drain errors.
*/
#define gl_drain_errors gl_drain_errors_helper(__FILE__ ":" SUNSHINE_STRINGIFY(__LINE__))
/**
* @brief Release the native resource held by the RAII wrapper.
*
* @param __fd File descriptor owned by the RAII wrapper.
* @return 0 on success, or -1 with errno set by the system close call.
*/
extern "C" int close(int __fd);
// X11 Display
extern "C" struct _XDisplay;
struct AVFrame;
/**
* @brief Release an FFmpeg frame allocated by the capture or conversion backend.
*
* @param frame Video or graphics frame being processed.
*/
void free_frame(AVFrame *frame);
/**
* @brief Owning pointer for an EGL frame object.
*/
using frame_t = util::safe_ptr<AVFrame, free_frame>;
namespace gl {
@@ -40,34 +70,84 @@
// glEGLImageTargetTexture2DOES (GL_OES_EGL_image) is not part of desktop GL —
// it is a GLES extension that must be loaded manually via eglGetProcAddress.
// GLeglImageOES is typedef void* per the Khronos spec (gl.xml).
/**
* @brief Function pointer type for glEGLImageTargetTexture2DOES.
*/
using PFNGLEGLIMAGETARGETTEXTURE2DOESPROC = void (*)(GLenum target, void *image);
/**
* @brief Resolve the GLES extension used to bind EGL images as textures.
*
* @return Extension function pointer, or nullptr when the driver does not expose it.
*/
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC egl_image_target_texture_2d();
/**
* @brief Drain and log pending OpenGL errors.
*
* @param prefix Text prefix used when formatting the message.
*/
void drain_errors(const std::string_view &prefix);
/**
* @brief OpenGL texture handle wrapper.
*/
class tex_t: public util::buffer_t<GLuint> {
using util::buffer_t<GLuint>::buffer_t;
public:
/**
* @brief Move ownership of OpenGL texture object names.
*/
tex_t(tex_t &&) = default;
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @return Reference or value produced by the operator.
*/
tex_t &operator=(tex_t &&) = default;
~tex_t();
/**
* @brief Allocate the underlying object and wrap it in the owning handle.
*
* @param count Number of objects or handles to create.
* @return Created backend object, or null when creation fails.
*/
static tex_t make(std::size_t count);
};
/**
* @brief OpenGL framebuffer handle wrapper.
*/
class frame_buf_t: public util::buffer_t<GLuint> {
using util::buffer_t<GLuint>::buffer_t;
public:
/**
* @brief Move ownership of OpenGL framebuffer object names.
*/
frame_buf_t(frame_buf_t &&) = default;
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @return Reference or value produced by the operator.
*/
frame_buf_t &operator=(frame_buf_t &&) = default;
~frame_buf_t();
/**
* @brief Allocate the underlying object and wrap it in the owning handle.
*
* @param count Number of objects or handles to create.
* @return Created backend object, or null when creation fails.
*/
static frame_buf_t make(std::size_t count);
/**
* @brief Bind each framebuffer and clear its color attachment.
*/
inline void bind(std::nullptr_t, std::nullptr_t) {
int x = 0;
for (auto fb : (*this)) {
@@ -79,6 +159,12 @@
return;
}
/**
* @brief Bind textures to this object's framebuffers as color attachments.
*
* @param it_begin First texture object to attach.
* @param it_end One-past-the-end iterator for texture objects to attach.
*/
template<class It>
void bind(It it_begin, It it_end) {
using namespace std::literals;
@@ -100,10 +186,20 @@
/**
* Copies a part of the framebuffer to texture
*
* @param id Framebuffer index to copy from.
* @param texture Destination texture receiving the copied pixels.
* @param offset_x Source X offset in pixels.
* @param offset_y Source Y offset in pixels.
* @param width Frame or display width in pixels.
* @param height Frame or display height in pixels.
*/
void copy(int id, int texture, int offset_x, int offset_y, int width, int height);
};
/**
* @brief OpenGL shader object that compiles GLSL source.
*/
class shader_t {
KITTY_USING_MOVE_T(shader_internal_t, GLuint, std::numeric_limits<GLuint>::max(), {
if (el != std::numeric_limits<GLuint>::max()) {
@@ -112,16 +208,36 @@
});
public:
/**
* @brief Read the shader compiler log.
*
* @return Shader compiler error log.
*/
std::string err_str();
/**
* @brief Compile an OpenGL shader and report compiler errors.
*
* @param source Shader source code to compile.
* @param type OpenGL shader type, such as GL_VERTEX_SHADER or GL_FRAGMENT_SHADER.
* @return Compiled shader object, or compiler log on failure.
*/
static util::Either<shader_t, std::string> compile(const std::string_view &source, GLenum type);
/**
* @brief Return the native handle owned by the wrapper.
*
* @return OpenGL shader object name.
*/
GLuint handle() const;
private:
shader_internal_t _shader;
};
/**
* @brief EGL image buffer with plane descriptors and imported GL textures.
*/
class buffer_t {
KITTY_USING_MOVE_T(buffer_internal_t, GLuint, std::numeric_limits<GLuint>::max(), {
if (el != std::numeric_limits<GLuint>::max()) {
@@ -130,13 +246,44 @@
});
public:
/**
* @brief Allocate the underlying object and wrap it in the owning handle.
*
* @param offsets Byte offsets for each uniform member in the block.
* @param block Uniform block name used for diagnostics and updates.
* @param data Initial bytes copied into the uniform buffer.
* @return Created backend object, or null when creation fails.
*/
static buffer_t make(util::buffer_t<GLint> &&offsets, const char *block, const std::string_view &data);
/**
* @brief Return the native handle owned by the wrapper.
*
* @return OpenGL buffer object name.
*/
GLuint handle() const;
/**
* @brief Query a uniform block index from an OpenGL program.
*
* @return Uniform block name associated with the buffer.
*/
const char *block() const;
/**
* @brief Update one uniform member in the block buffer.
*
* @param view Raw bytes to copy into the uniform block.
* @param offset Uniform-member index whose offset is used as the destination.
*/
void update(const std::string_view &view, std::size_t offset = 0);
/**
* @brief Update multiple uniform members in the block buffer.
*
* @param members Uniform members to query within the block.
* @param count Number of members in the array.
* @param offset First uniform-member index to update.
*/
void update(std::string_view *members, std::size_t count, std::size_t offset = 0);
private:
@@ -149,6 +296,9 @@
buffer_internal_t _buffer;
};
/**
* @brief OpenGL shader program with attached shader stages.
*/
class program_t {
KITTY_USING_MOVE_T(program_internal_t, GLuint, std::numeric_limits<GLuint>::max(), {
if (el != std::numeric_limits<GLuint>::max()) {
@@ -157,14 +307,44 @@
});
public:
/**
* @brief Read the program linker log.
*
* @return OpenGL program link error log.
*/
std::string err_str();
/**
* @brief Link an OpenGL program from compiled shaders.
*
* @param vert Compiled vertex shader object.
* @param frag Compiled fragment shader object.
* @return Linked program object, or linker log on failure.
*/
static util::Either<program_t, std::string> link(const shader_t &vert, const shader_t &frag);
/**
* @brief Bind this program and attach a uniform buffer block.
*
* @param buffer Uniform buffer block used by the program.
*/
void bind(const buffer_t &buffer);
/**
* @brief Query a uniform location from an OpenGL program.
*
* @param block Uniform block name to bind.
* @param members Uniform members to query within the block.
* @param count Number of uniform members to resolve.
* @return Uniform buffer wrapper, or std::nullopt when lookup/allocation fails.
*/
std::optional<buffer_t> uniform(const char *block, std::pair<const char *, std::string_view> *members, std::size_t count);
/**
* @brief Return the native handle owned by the wrapper.
*
* @return OpenGL program object name.
*/
GLuint handle() const;
private:
@@ -174,56 +354,83 @@
namespace gbm {
struct device;
/**
* @brief Function pointer used to destroy a GBM device.
*/
typedef void (*device_destroy_fn)(device *gbm);
/**
* @brief Function pointer used to create a GBM device from a file descriptor.
*/
typedef device *(*create_device_fn)(int fd);
extern device_destroy_fn device_destroy;
extern create_device_fn create_device;
/**
* @brief Owning GBM device pointer released with the GBM destroy callback.
*/
using gbm_t = util::dyn_safe_ptr<device, &device_destroy>;
/**
* @brief Load GBM symbols required for EGL device creation.
*
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init();
} // namespace gbm
namespace egl {
/**
* @brief Owning pointer for an EGL display connection.
*/
using display_t = util::dyn_safe_ptr_v2<void, EGLBoolean, &eglTerminate>;
/**
* @brief RGB capture image backed by EGL and OpenGL resources.
*/
struct rgb_img_t {
display_t::pointer display;
EGLImage xrgb8;
display_t::pointer display; ///< EGL display that owns the imported image.
EGLImage xrgb8; ///< EGL image for the imported XRGB plane.
gl::tex_t tex; ///< Texture containing the imported RGB plane.
gl::tex_t tex;
};
/**
* @brief NV12 capture image backed by EGL and OpenGL resources.
*/
struct nv12_img_t {
display_t::pointer display;
EGLImage r8;
EGLImage bg88;
display_t::pointer display; ///< EGL display that owns the imported planes.
EGLImage r8; ///< EGL image for the NV12 luma plane.
EGLImage bg88; ///< EGL image for the NV12 interleaved chroma plane.
gl::tex_t tex;
gl::frame_buf_t buf;
gl::tex_t tex; ///< Textures containing the imported Y and UV planes.
gl::frame_buf_t buf; ///< OpenGL framebuffer object used for rendering.
// sizeof(va::DRMPRIMESurfaceDescriptor::objects) / sizeof(va::DRMPRIMESurfaceDescriptor::objects[0]);
static constexpr std::size_t num_fds = 4; ///< Maximum number of DMA-BUF plane file descriptors exported by VAAPI.
static constexpr std::size_t num_fds = 4;
std::array<file_t, num_fds> fds;
std::array<file_t, num_fds> fds; ///< DMA-BUF file descriptors for each exported plane.
};
/**
* @brief YUV 4:4:4 capture image backed by EGL and OpenGL resources.
*/
struct yuv444_img_t {
display_t::pointer display;
EGLImage r8;
EGLImage g8;
EGLImage b8;
display_t::pointer display; ///< EGL display that owns the imported planes.
EGLImage r8; ///< EGL image for the Y plane.
EGLImage g8; ///< EGL image for the U plane.
EGLImage b8; ///< EGL image for the V plane.
gl::tex_t tex; ///< Textures containing the imported Y, U, and V planes.
gl::tex_t tex;
gl::frame_buf_t buf;
gl::frame_buf_t buf; ///< OpenGL framebuffer object used for rendering.
static constexpr std::size_t num_fds = 4; ///< Num fds.
static constexpr std::size_t num_fds = 4;
std::array<file_t, num_fds> fds; ///< DMA-BUF file descriptors for each exported plane.
std::array<file_t, num_fds> fds;
};
#ifndef DOXYGEN
KITTY_USING_MOVE_T(rgb_t, rgb_img_t, , {
if (el.xrgb8) {
eglDestroyImage(el.display, el.xrgb8);
@@ -261,28 +468,75 @@
eglDestroyContext(disp, ctx);
}
});
#else
/**
* @brief Move-only wrapper for RGB EGL image resources.
*/
class rgb_t;
/**
* @brief Move-only wrapper for NV12 EGL image resources.
*/
class nv12_t;
/**
* @brief Move-only wrapper for YUV444 EGL image resources.
*/
class yuv444_t;
/**
* @brief Move-only wrapper for an EGL context.
*/
class ctx_t;
#endif
/**
* @brief EGL surface descriptor used to import a captured DMA-BUF.
*/
struct surface_descriptor_t {
int width; ///< Frame or display width in pixels.
int height; ///< Frame or display height in pixels.
int fds[4]; ///< DMA-BUF file descriptors for up to four planes.
std::uint32_t fourcc; ///< DRM fourcc pixel format for the buffer.
std::uint64_t modifier; ///< DRM format modifier describing the buffer layout.
std::uint32_t pitches[4]; ///< Row stride in bytes for each DMA-BUF plane.
int width;
int height;
int fds[4];
std::uint32_t fourcc;
std::uint64_t modifier;
std::uint32_t pitches[4];
std::uint32_t offsets[4];
std::uint32_t offsets[4]; ///< Byte offset to the first pixel for each DMA-BUF plane.
};
/**
* @brief Open and initialize the display connection used for capture.
*
* @param native_display Native display.
* @return Constructed display object.
*/
display_t make_display(std::variant<gbm::gbm_t::pointer, wl_display *, _XDisplay *> native_display);
/**
* @brief Create an EGL/OpenGL context for capture or conversion.
*
* @param display Display object or identifier associated with the operation.
* @return EGL context wrapper, or std::nullopt when context creation fails.
*/
std::optional<ctx_t> make_ctx(display_t::pointer display);
/**
* @brief Import an RGB source surface.
*
std::optional<rgb_t>
import_source(
display_t::pointer egl_display,
const surface_descriptor_t &xrgb
* @return Imported RGB image, or std::nullopt on failure.
*/
std::optional<rgb_t> import_source(display_t::pointer egl_display, const surface_descriptor_t &xrgb);
);
/**
* @brief Create a blank RGB texture for an image.
*
* @return Blank RGB image.
*/
rgb_t create_blank(platf::img_t &img);
/**
* @brief Import an NV12 target surface.
*
* @return Imported NV12 image, or std::nullopt on failure.
*/
std::optional<nv12_t> import_target(
display_t::pointer egl_display,
std::array<file_t, nv12_img_t::num_fds> &&fds,
@@ -290,6 +544,16 @@
const surface_descriptor_t &uv
);
/**
* @brief Import a YUV444 target surface.
*
* @param egl_display EGL display.
* @param fds Target plane file descriptors.
* @param y Y plane descriptor.
* @param u U plane descriptor.
* @param v V plane descriptor.
* @return Imported YUV444 image, or std::nullopt on failure.
*/
std::optional<yuv444_t> import_target(
display_t::pointer egl_display,
std::array<file_t, yuv444_img_t::num_fds> &&fds,
@@ -307,27 +571,44 @@
*/
std::optional<nv12_t> create_nv12_target(int width, int height, AVPixelFormat format);
/**
* @brief Create YUV444 target.
*
* @param width Frame or display width in pixels.
* @param height Frame or display height in pixels.
* @param format Pixel, audio, or protocol format being converted.
* @return Created YUV444 target object or status.
*/
std::optional<yuv444_t> create_yuv444_target(int width, int height, AVPixelFormat format);
/**
* @brief Cursor image and hotspot metadata captured from the window system.
*/
class cursor_t: public platf::img_t {
public:
int x;
int y;
int x; ///< Cursor hotspot or surface X coordinate.
int y; ///< Cursor hotspot or surface Y coordinate.
int src_w; ///< Cursor source image width in pixels.
int src_h; ///< Cursor source image height in pixels.
int src_w;
int src_h;
unsigned long serial;
unsigned long serial; ///< X11 cursor serial used to detect cursor image changes.
std::vector<std::uint8_t> buffer;
std::vector<std::uint8_t> buffer; ///< Cursor image pixels.
};
// Allow cursor and the underlying image to be kept together
/**
* @brief Captured image descriptor shared by EGL conversion paths.
*/
class img_descriptor_t: public cursor_t {
public:
~img_descriptor_t() {
reset();
}
/**
* @brief Reset the object to its initial empty state.
*/
void reset() {
for (auto x = 0; x < 4; ++x) {
if (sd.fds[x] >= 0) {
@@ -338,71 +619,162 @@
}
}
surface_descriptor_t sd;
surface_descriptor_t sd; ///< DMA-BUF surface descriptor for the captured image.
// Increment sequence when new rgb_t needs to be created
std::uint64_t sequence; ///< Monotonic value used to detect when GL resources must be recreated.
std::uint64_t sequence;
// Frame is vertically flipped (GL convention)
bool y_invert {false};
bool y_invert {false}; ///< Whether the shader should invert the Y axis.
// PipeWire metadata
std::optional<uint64_t> pts;
std::optional<uint64_t> seq;
std::optional<bool> pw_damage;
std::optional<uint32_t> pw_flags;
std::optional<uint64_t> pts; ///< PipeWire presentation timestamp.
std::optional<uint64_t> seq; ///< PipeWire frame sequence number.
std::optional<bool> pw_damage; ///< Whether PipeWire damage tracking should be used.
std::optional<uint32_t> pw_flags; ///< PipeWire frame flags reported with the buffer.
};
/**
* @brief EGL/OpenGL scaler and colorspace conversion pipeline.
*/
class sws_t {
public:
/**
* @brief Create a software-scaling pipeline that renders to NV12 planes.
*
* @param in_width Source frame width in pixels.
* @param in_height Source frame height in pixels.
* @param out_width Destination frame width in pixels.
* @param out_height Destination frame height in pixels.
* @param tex Texture resource used by the converter.
* @return Constructed NV12 object.
*/
static std::optional<sws_t> make_nv12(int in_width, int in_height, int out_width, int out_height, gl::tex_t &&tex);
/**
* @brief Create a software-scaling pipeline that renders to YUV444 planes.
*
* @param in_width Source frame width in pixels.
* @param in_height Source frame height in pixels.
* @param out_width Destination frame width in pixels.
* @param out_height Destination frame height in pixels.
* @param tex Texture resource used by the converter.
* @return Constructed YUV444 object.
*/
static std::optional<sws_t> make_yuv444(int in_width, int in_height, int out_width, int out_height, gl::tex_t &&tex);
/**
* @brief Allocate the underlying object and wrap it in the owning handle.
*
* @param in_width Source frame width in pixels.
* @param in_height Source frame height in pixels.
* @param out_width Destination frame width in pixels.
* @param out_height Destination frame height in pixels.
* @param format Destination FFmpeg pixel format.
* @param is_yuv444 Whether the destination uses three full-resolution planes.
* @return Created backend object, or null when creation fails.
*/
static std::optional<sws_t> make(int in_width, int in_height, int out_width, int out_height, AVPixelFormat format, bool is_yuv444);
// Convert the loaded image into the first two framebuffers
/**
* @brief Convert the loaded source image into NV12 output planes.
*
* @param fb Framebuffer object to bind or update.
* @return Conversion status.
*/
int convert_nv12(gl::frame_buf_t &fb);
// Convert the loaded image into the first three framebuffers
/**
* @brief Convert the loaded source image into YUV444 output planes.
*
* @param fb Framebuffer object to bind or update.
* @return Conversion status.
*/
int convert_yuv444(gl::frame_buf_t &fb);
// Draw loaded image by programs to frame buffers
/**
* @brief Render the loaded source texture into output framebuffers.
*
* @param attachments Framebuffer attachments to bind.
* @param fb Framebuffer object to bind or update.
* @param count Number of output planes to draw.
* @param is_yuv444 Whether to render three YUV444 planes instead of NV12 planes.
* @return 0 when all draw calls complete; nonzero on OpenGL failure.
*/
int draw_programs_to_buffers(GLenum attachments[], gl::frame_buf_t &fb, int count, bool is_yuv444);
// Make an area of the image black
/**
* @brief Clear the render target to a blank frame.
*
* @param fb Framebuffer object to bind or update.
* @param offsetX_ Offset x.
* @param offsetY_ Offset y.
* @param width Frame or display width in pixels.
* @param height Frame or display height in pixels.
* @param is_yuv444 Is YUV444.
* @return 0 when the target area is cleared; nonzero on OpenGL failure.
*/
int blank(gl::frame_buf_t &fb, int offsetX_, int offsetY_, int width, int height, bool is_yuv444);
/**
* @brief Load ram data from the backing API or store.
*
* @param img Image or frame object to read from or populate.
*/
void load_ram(platf::img_t &img);
/**
* @brief Load vram data from the backing API or store.
*
* @param img Image or frame object to read from or populate.
* @param offset_x Offset x.
* @param offset_y Offset y.
* @param texture Texture resource to bind, update, or attach.
* @param is_yuv444 Is YUV444.
*/
void load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture, bool is_yuv444);
/**
* @brief Apply the configured colorspace metadata to the active frame.
*
* @param colorspace Colorimetry information used for conversion or encoding.
* @param is_yuv444 Is YUV444.
*/
void apply_colorspace(const video::sunshine_colorspace_t &colorspace, bool is_yuv444);
// The first texture is the monitor image.
// The second texture is the cursor image
gl::tex_t tex;
gl::tex_t tex; ///< Source and cursor textures used by the conversion pipeline.
// The cursor image will be blended into this framebuffer
gl::frame_buf_t cursor_framebuffer;
gl::frame_buf_t copy_framebuffer;
gl::frame_buf_t cursor_framebuffer; ///< Cursor framebuffer.
gl::frame_buf_t copy_framebuffer; ///< Copy framebuffer.
// Y - shader, UV - shader, Cursor - shader : for nv12
// Y - shader, U - shader, V - shader, Cursor - shader : for yuv444
std::array<gl::program_t, 4> program;
std::array<gl::program_t, 4> program; ///< Program.
gl::buffer_t color_matrix; ///< Color matrix.
gl::buffer_t color_matrix;
int out_width;
int out_height;
int in_width;
int in_height;
int offsetX;
int offsetY;
int out_width; ///< Out width.
int out_height; ///< Out height.
int in_width; ///< In width.
int in_height; ///< In height.
int offsetX; ///< Offset x.
int offsetY; ///< Offset y.
// Pointer to the texture to be converted to nv12
int loaded_texture;
int loaded_texture; ///< Loaded texture.
// Store latest cursor for load_vram
std::uint64_t serial;
std::uint64_t serial; ///< Serial.
};
/**
* @brief Log EGL failure details and return an error code.
*
* @return False after logging the EGL failure.
*/
bool fail();
} // namespace egl
src/platform/linux/input/inputtino.cpp +30 −0
@@ -21,6 +21,9 @@
namespace platf {
/**
* @brief Create the platform input backend for a stream.
*/
input_t input() {
return {new input_raw_t()};
}
@@ -29,41 +32,65 @@
return std::make_unique<client_input_raw_t>(input);
}
/**
* @brief Release a platform input backend created by input().
*/
void freeInput(void *p) {
auto *input = (input_raw_t *) p;
delete input;
}
/**
* @brief Move mouse using the backend coordinate system.
*/
void move_mouse(input_t &input, int deltaX, int deltaY) {
auto raw = (input_raw_t *) input.get();
platf::mouse::move(raw, deltaX, deltaY);
}
/**
* @brief Move the pointer to an absolute client-provided touch coordinate.
*/
void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) {
auto raw = (input_raw_t *) input.get();
platf::mouse::move_abs(raw, touch_port, x, y);
}
/**
* @brief Press or release a virtual mouse button.
*/
void button_mouse(input_t &input, int button, bool release) {
auto raw = (input_raw_t *) input.get();
platf::mouse::button(raw, button, release);
}
/**
* @brief Apply a vertical scroll event to the virtual mouse.
*/
void scroll(input_t &input, int high_res_distance) {
auto raw = (input_raw_t *) input.get();
platf::mouse::scroll(raw, high_res_distance);
}
/**
* @brief Apply a horizontal scroll event to the virtual mouse.
*/
void hscroll(input_t &input, int high_res_distance) {
auto raw = (input_raw_t *) input.get();
platf::mouse::hscroll(raw, high_res_distance);
}
/**
* @brief Press or release a virtual keyboard key.
*/
void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) {
auto raw = (input_raw_t *) input.get();
platf::keyboard::update(raw, modcode, release, flags);
}
/**
* @brief Submit UTF-8 text input to the keyboard backend.
*/
void unicode(input_t &input, char *utf8, int size) {
auto raw = (input_raw_t *) input.get();
platf::keyboard::unicode(raw, utf8, size);
@@ -84,6 +111,9 @@
return platf::gamepad::alloc(raw, id, metadata, feedback_queue);
}
/**
* @brief Release gamepad resources.
*/
void free_gamepad(input_t &input, int nr) {
auto raw = (input_raw_t *) input.get();
platf::gamepad::free(raw, nr);
src/platform/linux/input/inputtino_common.h +37 −8
@@ -20,6 +20,12 @@
namespace platf {
/**
* @brief Append the target seat name to an inputtino device name when needed.
*
* @param base_name Base uinput device name.
* @return Device name scoped to the target seat.
*/
inline std::string inputtino_name_for_seat(std::string_view base_name) {
auto seat_id = inputtino_seat::get_target_seat();
if (seat_id.empty() || seat_id == "seat0") {
@@ -35,14 +41,23 @@
return name;
}
/**
* @brief Variant of inputtino virtual gamepad implementations Sunshine can create.
*/
using joypads_t = std::variant<inputtino::XboxOneJoypad, inputtino::SwitchJoypad, inputtino::PS5Joypad>;
/**
* @brief inputtino joypad collection and its ownership state.
*/
struct joypad_state {
std::unique_ptr<joypads_t> joypad;
gamepad_feedback_msg_t last_rumble;
gamepad_feedback_msg_t last_rgb_led;
std::unique_ptr<joypads_t> joypad; ///< Active virtual gamepad object for one connected client slot.
gamepad_feedback_msg_t last_rumble; ///< Last rumble.
gamepad_feedback_msg_t last_rgb_led; ///< Last RGB led.
};
/**
* @brief Global inputtino device handles shared by clients.
*/
struct input_raw_t {
input_raw_t():
mouse(inputtino::Mouse::create({
@@ -69,8 +84,8 @@
~input_raw_t() = default;
// All devices are wrapped in Result because it might be that we aren't able to create them (ex: udev permission denied)
inputtino::Result<inputtino::Mouse> mouse; ///< Shared inputtino virtual mouse device.
inputtino::Result<inputtino::Keyboard> keyboard; ///< inputtino virtual keyboard device.
inputtino::Result<inputtino::Mouse> mouse;
inputtino::Result<inputtino::Keyboard> keyboard;
/**
* A list of gamepads that are currently connected.
@@ -79,7 +94,15 @@
std::vector<std::shared_ptr<joypad_state>> gamepads;
};
/**
* @brief Per-client inputtino devices for touch and pen input.
*/
struct client_input_raw_t: public client_input_t {
/**
* @brief Create per-client inputtino devices for touch and pen input.
*
* @param input Platform input backend that receives the event.
*/
client_input_raw_t(input_t &input):
touch(inputtino::TouchScreen::create({
.name = inputtino_name_for_seat("Touch passthrough"sv),
@@ -102,16 +125,22 @@
}
}
input_raw_t *global;
input_raw_t *global; ///< Shared inputtino device set owned by the global input context.
// Device state and handles for pen and touch input must be stored in the per-client
// input context, because each connected client may be sending their own independent
// pen/touch events. To maintain separation, we expose separate pen and touch devices
// for each client.
inputtino::Result<inputtino::TouchScreen> touch;
inputtino::Result<inputtino::PenTablet> pen;
inputtino::Result<inputtino::TouchScreen> touch; ///< Per-client virtual touchscreen device.
inputtino::Result<inputtino::PenTablet> pen; ///< Per-client virtual pen tablet device.
};
/**
* @brief Convert degrees to radians for controller motion data.
*
* @param degree Angle in degrees to convert.
* @return Angle in radians.
*/
inline float deg2rad(float degree) {
return degree * (M_PI / 180.f);
}
src/platform/linux/input/inputtino_gamepad.cpp +40 −0
@@ -20,6 +20,9 @@
namespace platf::gamepad {
/**
* @brief Enumerates supported gamepad status options.
*/
enum GamepadStatus {
UHID_NOT_AVAILABLE = 0, ///< UHID is not available
UINPUT_NOT_AVAILABLE, ///< UINPUT is not available
@@ -27,6 +30,11 @@
GAMEPAD_STATUS ///< Helper to indicate the number of status
};
/**
* @brief Create xbox one.
*
* @return Created xbox one object or status.
*/
auto create_xbox_one() {
return inputtino::XboxOneJoypad::create({.name = inputtino_name_for_seat("Sunshine X-Box One (virtual) pad"sv),
// https://github.com/torvalds/linux/blob/master/drivers/input/joystick/xpad.c#L147
@@ -35,6 +43,11 @@
.version = 0x0408});
}
/**
* @brief Create an inputtino Nintendo Switch Pro controller.
*
* @return Created switch object or status.
*/
auto create_switch() {
return inputtino::SwitchJoypad::create({.name = inputtino_name_for_seat("Sunshine Nintendo (virtual) pad"sv),
// https://github.com/torvalds/linux/blob/master/drivers/hid/hid-ids.h#L981
@@ -43,5 +56,11 @@
.version = 0x8111});
}
/**
* @brief Create an inputtino DualSense controller.
*
* @param globalIndex Global index.
* @return Created DS5 object or status.
*/
auto create_ds5(int globalIndex) {
std::string device_mac = ""; // Inputtino checks empty() to generate a random MAC
@@ -54,5 +73,8 @@
return inputtino::PS5Joypad::create({.name = inputtino_name_for_seat("Sunshine PS5 (virtual) pad"sv), .vendor_id = 0x054C, .product_id = 0x0CE6, .version = 0x8111, .device_phys = device_mac, .device_uniq = device_mac});
}
/**
* @brief Allocate and initialize platform input state for a stream.
*/
int alloc(input_raw_t *raw, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) {
ControllerType selectedGamepadType;
@@ -180,12 +202,18 @@
return -1;
}
/**
* @brief Release backend resources for the indexed gamepad.
*/
void free(input_raw_t *raw, int nr) {
// This will call the destructor which in turn will stop the background threads for rumble and LED (and ultimately remove the joypad device)
raw->gamepads[nr]->joypad.reset();
raw->gamepads[nr].reset();
}
/**
* @brief Apply the supplied state update to the platform backend.
*/
void update(input_raw_t *raw, int nr, const gamepad_state_t &gamepad_state) {
auto gamepad = raw->gamepads[nr];
if (!gamepad) {
@@ -201,6 +229,9 @@
*gamepad->joypad);
}
/**
* @brief Apply controller touchpad data to the backend device.
*/
void touch(input_raw_t *raw, const gamepad_touch_t &touch) {
auto gamepad = raw->gamepads[touch.id.globalIndex];
if (!gamepad) {
@@ -216,6 +247,9 @@
}
}
/**
* @brief Apply controller motion sensor data to the backend device.
*/
void motion(input_raw_t *raw, const gamepad_motion_t &motion) {
auto gamepad = raw->gamepads[motion.id.globalIndex];
if (!gamepad) {
@@ -234,6 +268,9 @@
}
}
/**
* @brief Apply controller battery status to the backend device.
*/
void battery(input_raw_t *raw, const gamepad_battery_t &battery) {
auto gamepad = raw->gamepads[battery.id.globalIndex];
if (!gamepad) {
@@ -263,6 +300,9 @@
}
}
/**
* @brief Return the virtual gamepad types supported by inputtino.
*/
std::vector<supported_gamepad_t> &supported_gamepads(input_t *input) {
if (!input) {
static std::vector gps {
src/platform/linux/input/inputtino_gamepad.h +49 −0
@@ -17,23 +17,72 @@
namespace platf::gamepad {
/**
* @brief Enumerates supported controller type options.
*/
enum ControllerType {
XboxOneWired, ///< Xbox One Wired Controller
DualSenseWired, ///< DualSense Wired Controller
SwitchProWired ///< Switch Pro Wired Controller
};
/**
* @brief Allocate and initialize platform input state for a stream.
*
* @param raw Platform-specific input backend state.
* @param id Identifier for the controller, session, display, or resource.
* @param metadata Output structure populated with HDR metadata.
* @param feedback_queue Feedback queue.
* @return Allocated object or identifier, or an error value on failure.
*/
int alloc(input_raw_t *raw, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue);
/**
* @brief Release backend resources for the indexed gamepad.
*
* @param raw Platform-specific input backend state.
* @param nr Controller index assigned by the client.
*/
void free(input_raw_t *raw, int nr);
/**
* @brief Apply the supplied state update to the platform backend.
*
* @param raw Platform-specific input backend state.
* @param nr Controller index assigned by the client.
* @param gamepad_state Gamepad state.
*/
void update(input_raw_t *raw, int nr, const gamepad_state_t &gamepad_state);
/**
* @brief Apply controller touchpad data to the backend device.
*
* @param raw Platform-specific input backend state.
* @param touch Touch event data to apply to the virtual device.
*/
void touch(input_raw_t *raw, const gamepad_touch_t &touch);
/**
* @brief Apply controller motion sensor data to the backend device.
*
* @param raw Platform-specific input backend state.
* @param motion Motion sensor data to apply to the virtual device.
*/
void motion(input_raw_t *raw, const gamepad_motion_t &motion);
/**
* @brief Apply controller battery status to the backend device.
*
* @param raw Platform-specific input backend state.
* @param battery Battery status data reported by the virtual device.
*/
void battery(input_raw_t *raw, const gamepad_battery_t &battery);
/**
* @brief Return gamepad slots supported by the inputtino backend.
*
* @param input Platform input backend that receives the event.
* @return Mutable list of supported virtual gamepads for the input backend.
*/
std::vector<supported_gamepad_t> &supported_gamepads(input_t *input);
} // namespace platf::gamepad
src/platform/linux/input/inputtino_keyboard.cpp +8 −0
@@ -25,6 +25,8 @@
* ex: ['👱'] = "1F471" // see UTF encoding at https://www.compart.com/en/unicode/U+1F471
*
* adapted from: https://stackoverflow.com/a/7639754
* @param str UTF-8 text to encode as hexadecimal.
* @return Value converted to hex.
*/
std::string to_hex(const std::basic_string<char32_t> &str) {
std::stringstream ss;
@@ -160,6 +162,9 @@
{KEY_102ND, 0xE2}
};
/**
* @brief Apply the supplied state update to the platform backend.
*/
void update(input_raw_t *raw, uint16_t modcode, bool release, uint8_t flags) {
if (raw->keyboard) {
if (release) {
@@ -170,6 +175,9 @@
}
}
/**
* @brief Submit UTF-8 text input to the keyboard backend.
*/
void unicode(input_raw_t *raw, char *utf8, int size) {
if (raw->keyboard) {
/* Reading input text as UTF-8 */
src/platform/linux/input/inputtino_keyboard.h +15 −0
@@ -15,7 +15,22 @@
using namespace std::literals;
namespace platf::keyboard {
/**
* @brief Apply the supplied state update to the platform backend.
*
* @param raw Platform-specific input backend state.
* @param modcode Modifier key code to update.
* @param release Whether the key or button event is a release.
* @param flags Bit flags that modify the requested operation.
*/
void update(input_raw_t *raw, uint16_t modcode, bool release, uint8_t flags);
/**
* @brief Submit UTF-8 text input to the keyboard backend.
*
* @param raw Platform-specific input backend state.
* @param utf8 UTF-8 text submitted by the client.
* @param size Number of bytes or elements requested.
*/
void unicode(input_raw_t *raw, char *utf8, int size);
} // namespace platf::keyboard
src/platform/linux/input/inputtino_mouse.cpp +18 −0
@@ -19,18 +19,27 @@
namespace platf::mouse {
/**
* @brief Apply a relative pointer movement to the virtual mouse.
*/
void move(input_raw_t *raw, int deltaX, int deltaY) {
if (raw->mouse) {
(*raw->mouse).move(deltaX, deltaY);
}
}
/**
* @brief Move abs using the backend coordinate system.
*/
void move_abs(input_raw_t *raw, const touch_port_t &touch_port, float x, float y) {
if (raw->mouse) {
(*raw->mouse).move_abs(x, y, touch_port.width, touch_port.height);
}
}
/**
* @brief Press or release a virtual mouse button.
*/
void button(input_raw_t *raw, int button, bool release) {
if (raw->mouse) {
inputtino::Mouse::MOUSE_BUTTON btn_type;
@@ -62,18 +71,27 @@
}
}
/**
* @brief Apply a vertical scroll event to the virtual mouse.
*/
void scroll(input_raw_t *raw, int high_res_distance) {
if (raw->mouse) {
(*raw->mouse).vertical_scroll(high_res_distance);
}
}
/**
* @brief Apply a horizontal scroll event to the virtual mouse.
*/
void hscroll(input_raw_t *raw, int high_res_distance) {
if (raw->mouse) {
(*raw->mouse).horizontal_scroll(high_res_distance);
}
}
/**
* @brief Return the current virtual pointer location.
*/
util::point_t get_location(input_raw_t *raw) {
if (raw->mouse) {
// TODO: decide what to do after https://github.com/games-on-whales/inputtino/issues/6 is resolved.
src/platform/linux/input/inputtino_mouse.h +40 −0
@@ -15,15 +15,55 @@
using namespace std::literals;
namespace platf::mouse {
/**
* @brief Apply a relative pointer movement to the virtual mouse.
*
* @param raw Platform-specific input backend state.
* @param deltaX Horizontal relative movement in client coordinates.
* @param deltaY Vertical relative movement in client coordinates.
*/
void move(input_raw_t *raw, int deltaX, int deltaY);
/**
* @brief Move the pointer to an absolute client-provided touch coordinate.
*
* @param raw Platform-specific input backend state.
* @param touch_port Touch coordinate bounds used for scaling.
* @param x Horizontal absolute coordinate from the client.
* @param y Vertical absolute coordinate from the client.
*/
void move_abs(input_raw_t *raw, const touch_port_t &touch_port, float x, float y);
/**
* @brief Press or release a virtual mouse button.
*
* @param raw Platform-specific input backend state.
* @param button Mouse button identifier to press or release.
* @param release Whether the key or button event is a release.
*/
void button(input_raw_t *raw, int button, bool release);
/**
* @brief Apply a vertical scroll event to the virtual mouse.
*
* @param raw Platform-specific input backend state.
* @param high_res_distance High-resolution scroll distance reported by the client.
*/
void scroll(input_raw_t *raw, int high_res_distance);
/**
* @brief Apply a horizontal scroll event to the virtual mouse.
*
* @param raw Platform-specific input backend state.
* @param high_res_distance High-resolution scroll distance reported by the client.
*/
void hscroll(input_raw_t *raw, int high_res_distance);
/**
* @brief Return the current virtual pointer location.
*
* @param raw Platform-specific input backend state.
* @return Current virtual pointer location in screen coordinates.
*/
util::point_t get_location(input_raw_t *raw);
} // namespace platf::mouse
src/platform/linux/input/inputtino_pen.cpp +3 −0
@@ -18,6 +18,9 @@
using namespace std::literals;
namespace platf::pen {
/**
* @brief Apply the supplied state update to the platform backend.
*/
void update(client_input_raw_t *raw, const touch_port_t &touch_port, const pen_input_t &pen) {
if (raw->pen) {
// First set the buttons
src/platform/linux/input/inputtino_pen.h +8 −1
@@ -16,5 +16,12 @@
using namespace std::literals;
namespace platf::pen {
/**
* @brief Apply the supplied state update to the platform backend.
*
* @param raw Platform-specific input backend state.
* @param touch_port Touch coordinate bounds used for scaling.
* @param pen Pen event data to inject.
*/
void update(client_input_raw_t *raw, const touch_port_t &touch_port, const pen_input_t &pen);
}
} // namespace platf::pen
src/platform/linux/input/inputtino_seat.h +2 −0
@@ -11,5 +11,7 @@
/**
* Determine the target seat for the current Sunshine instance.
* Returns empty string if no seat could be determined.
*
* @return Seat name used for virtual input devices, or an empty string when unknown.
*/
std::string get_target_seat();
src/platform/linux/input/inputtino_touch.cpp +3 −0
@@ -18,6 +18,9 @@
using namespace std::literals;
namespace platf::touch {
/**
* @brief Apply the supplied state update to the platform backend.
*/
void update(client_input_raw_t *raw, const touch_port_t &touch_port, const touch_input_t &touch) {
if (raw->touch) {
switch (touch.eventType) {
src/platform/linux/input/inputtino_touch.h +8 −1
@@ -16,5 +16,12 @@
using namespace std::literals;
namespace platf::touch {
/**
* @brief Apply the supplied state update to the platform backend.
*
* @param raw Platform-specific input backend state.
* @param touch_port Touch coordinate bounds used for scaling.
* @param touch Touch event data to apply to the virtual device.
*/
void update(client_input_raw_t *raw, const touch_port_t &touch_port, const touch_input_t &touch);
}
} // namespace platf::touch
src/platform/linux/kmsgrab.cpp +452 −65
@@ -37,6 +37,9 @@
namespace kms {
/**
* @brief Temporarily owns CAP_SYS_ADMIN while opening DRM capture resources.
*/
class cap_sys_admin {
public:
cap_sys_admin() {
@@ -56,11 +59,20 @@
cap_free(caps);
}
cap_t caps;
cap_t caps; ///< Caps.
};
/**
* @brief RAII wrapper for DRM framebuffer metadata and GEM handles.
*/
class wrapper_fb {
public:
/**
* @brief Wrap legacy DRM framebuffer metadata.
*
* @param card_fd DRM card file descriptor used to close GEM handles.
* @param fb Legacy framebuffer metadata returned by drmModeGetFB.
*/
wrapper_fb(uint32_t card_fd, drmModeFB *fb):
card_fd {card_fd},
fb {fb},
@@ -76,6 +88,12 @@
pitches[0] = fb->pitch;
}
/**
* @brief Wrap DRM framebuffer metadata with per-plane modifiers.
*
* @param card_fd DRM card file descriptor used to close GEM handles.
* @param fb2 Framebuffer metadata returned by drmModeGetFB2.
*/
wrapper_fb(uint32_t card_fd, drmModeFB2 *fb2):
card_fd {card_fd},
fb2 {fb2},
@@ -107,30 +125,63 @@
}
}
uint32_t card_fd;
drmModeFB *fb = nullptr;
uint32_t card_fd; ///< DRM card file descriptor that owns the framebuffer ID.
drmModeFB *fb = nullptr; ///< Legacy DRM framebuffer metadata when modifier data is unavailable.
drmModeFB2 *fb2 = nullptr; ///< DRM framebuffer metadata with modifier support.
uint32_t fb_id; ///< DRM framebuffer ID being captured.
uint32_t width; ///< Framebuffer width in pixels.
uint32_t height; ///< Framebuffer height in pixels.
uint32_t pixel_format; ///< DRM fourcc pixel format for the framebuffer.
uint64_t modifier; ///< DRM format modifier describing framebuffer memory layout.
uint32_t handles[4]; ///< GEM handles for each framebuffer plane.
uint32_t pitches[4]; ///< Row stride in bytes for each framebuffer plane.
uint32_t offsets[4]; ///< Byte offset to the first pixel for each framebuffer plane.
drmModeFB2 *fb2 = nullptr;
uint32_t fb_id;
uint32_t width;
uint32_t height;
uint32_t pixel_format;
uint64_t modifier;
uint32_t handles[4];
uint32_t pitches[4];
uint32_t offsets[4];
};
/**
* @brief DRM plane resource list released with `drmModeFreePlaneResources`.
*/
using plane_res_t = util::safe_ptr<drmModePlaneRes, drmModeFreePlaneResources>;
/**
* @brief DRM encoder pointer released with `drmModeFreeEncoder`.
*/
using encoder_t = util::safe_ptr<drmModeEncoder, drmModeFreeEncoder>;
/**
* @brief DRM resource list released with `drmModeFreeResources`.
*/
using res_t = util::safe_ptr<drmModeRes, drmModeFreeResources>;
/**
* @brief DRM plane pointer released with `drmModeFreePlane`.
*/
using plane_t = util::safe_ptr<drmModePlane, drmModeFreePlane>;
/**
* @brief DRM framebuffer pointer released with `drmModeFreeFB2`.
*/
using fb_t = std::unique_ptr<wrapper_fb>;
/**
* @brief DRM CRTC pointer released with `drmModeFreeCrtc`.
*/
using crtc_t = util::safe_ptr<drmModeCrtc, drmModeFreeCrtc>;
/**
* @brief DRM object-property list released with `drmModeFreeObjectProperties`.
*/
using obj_prop_t = util::safe_ptr<drmModeObjectProperties, drmModeFreeObjectProperties>;
/**
* @brief DRM property pointer released with `drmModeFreeProperty`.
*/
using prop_t = util::safe_ptr<drmModePropertyRes, drmModeFreeProperty>;
/**
* @brief DRM property-blob pointer released with `drmModeFreePropertyBlob`.
*/
using prop_blob_t = util::safe_ptr<drmModePropertyBlobRes, drmModeFreePropertyBlob>;
/**
* @brief DRM version pointer released with `drmFreeVersion`.
*/
using version_t = util::safe_ptr<drmVersion, drmFreeVersion>;
/**
* @brief Counter map keyed by DRM connector type.
*/
using conn_type_count_t = std::map<std::uint32_t, std::uint32_t>;
static int env_width;
@@ -139,6 +190,12 @@
static int env_logical_width;
static int env_logical_height;
/**
* @brief Convert a DRM plane type value to a diagnostic string.
*
* @param val Value assigned to the synchronized object.
* @return Static text for the DRM plane type.
*/
std::string_view plane_type(std::uint64_t val) {
switch (val) {
case DRM_PLANE_TYPE_OVERLAY:
@@ -152,45 +209,56 @@
return "UNKNOWN"sv;
}
/**
* @brief DRM connector and mode information for one display.
*/
struct connector_t {
// For example: HDMI-A or HDMI
std::uint32_t type;
std::uint32_t type; ///< Type.
// Equals zero if not applicable
std::uint32_t crtc_id;
std::uint32_t crtc_id; ///< Crtc ID.
// For example HDMI-A-{index} or HDMI-{index}
std::uint32_t index;
std::uint32_t index; ///< Index.
// ID of the connector
std::uint32_t connector_id;
std::uint32_t connector_id; ///< Connector ID.
bool connected;
bool connected; ///< Whether the DRM connector is connected.
};
/**
* @brief KMS monitor capture state and DRM resources.
*/
struct monitor_t {
// Connector attributes
std::uint32_t type; ///< Type.
std::uint32_t type;
std::uint32_t index;
std::uint32_t index; ///< Index.
// Monitor index in the global list
std::uint32_t monitor_index;
std::uint32_t monitor_index; ///< Monitor index.
platf::touch_port_t viewport;
platf::touch_port_t viewport; ///< Viewport.
};
/**
* @brief DRM card, device path, and render-node metadata.
*/
struct card_descriptor_t {
std::string path;
std::string path; ///< Path.
std::map<std::uint32_t, monitor_t> crtc_to_monitor;
std::map<std::uint32_t, monitor_t> crtc_to_monitor; ///< Crtc to monitor.
};
static std::vector<card_descriptor_t> card_descriptors;
static std::uint32_t from_view(const std::string_view &string) {
#define _CONVERT(x, y) \
if (string == x) \
return DRM_MODE_CONNECTOR_##y
#ifndef DOXYGEN
#define _CONVERT(x, y) \
if (string == x) \
return DRM_MODE_CONNECTOR_##y
#endif
// This list was created from the following sources:
// https://gitlab.freedesktop.org/mesa/drm/-/blob/main/xf86drmMode.c (drmModeGetConnectorTypeName)
@@ -244,8 +312,18 @@
return DRM_MODE_CONNECTOR_Unknown;
}
/**
* @brief Iterator over DRM planes and their associated properties.
*/
class plane_it_t: public round_robin_util::it_wrap_t<plane_t::element_type, plane_it_t> {
public:
/**
* @brief Create an iterator over DRM planes starting at a specific plane ID.
*
* @param fd DRM card file descriptor.
* @param plane_p Current plane ID pointer.
* @param end One-past-the-end plane ID pointer.
*/
plane_it_t(int fd, std::uint32_t *plane_p, std::uint32_t *end):
fd {fd},
plane_p {plane_p},
@@ -253,11 +331,20 @@
load_next_valid_plane();
}
/**
* @brief Create the end iterator for a DRM plane range.
*
* @param fd DRM card file descriptor.
* @param end One-past-the-end plane ID pointer.
*/
plane_it_t(int fd, std::uint32_t *end):
fd {fd},
plane_p {end},
end {end} {
}
/**
* @brief Load next valid plane.
*/
void load_next_valid_plane() {
this->plane.reset();
@@ -274,50 +361,79 @@
}
}
/**
* @brief Advance the iterator to the next element.
*/
void inc() {
++plane_p;
load_next_valid_plane();
}
/**
* @brief Compare two iterators for equality.
*
* @param other Plane iterator to compare against.
* @return True when both iterators reference the same plane position.
*/
bool eq(const plane_it_t &other) const {
return plane_p == other.plane_p;
}
/**
* @brief Return the currently wrapped value or handle.
*
* @return Underlying native handle or object pointer.
*/
plane_t::pointer get() {
return plane.get();
}
int fd;
std::uint32_t *plane_p;
std::uint32_t *end;
int fd; ///< DRM card file descriptor.
std::uint32_t *plane_p; ///< Current plane ID pointer.
std::uint32_t *end; ///< One-past-the-end plane ID pointer.
util::shared_t<plane_t> plane;
util::shared_t<plane_t> plane; ///< Plane.
};
/**
* @brief Cursor position and visibility for the current capture frame.
*/
struct cursor_t {
// Public properties used during blending
bool visible = false;
std::int32_t x;
std::int32_t y;
std::uint32_t dst_w;
std::uint32_t dst_h;
std::uint32_t src_w;
std::uint32_t src_h;
std::vector<std::uint8_t> pixels;
unsigned long serial;
bool visible = false; ///< Whether the KMS cursor plane is visible.
std::int32_t x; ///< X.
std::int32_t y; ///< Y.
std::uint32_t dst_w; ///< Dst w.
std::uint32_t dst_h; ///< Dst h.
std::uint32_t src_w; ///< Src w.
std::uint32_t src_h; ///< Src h.
std::vector<std::uint8_t> pixels; ///< Pixels.
unsigned long serial; ///< Serial.
// Private properties used for tracking cursor changes
std::uint64_t prop_src_x;
std::uint64_t prop_src_y;
std::uint64_t prop_src_w;
std::uint64_t prop_src_h;
std::uint32_t fb_id;
std::uint64_t prop_src_x; ///< Prop src x.
std::uint64_t prop_src_y; ///< Prop src y.
std::uint64_t prop_src_w; ///< Prop src w.
std::uint64_t prop_src_h; ///< Prop src h.
std::uint32_t fb_id; ///< Fb ID.
};
/**
* @brief DRM card, render node, and plane metadata used for KMS capture.
*/
class card_t {
public:
/**
* @brief Internal connector metadata tuple used while selecting a monitor.
*/
using connector_interal_t = util::safe_ptr<drmModeConnector, drmModeFreeConnector>;
/**
* @brief Open a DRM connector and cache its monitor metadata.
*
* @param path Filesystem path for the DRM device or resource.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(const char *path) {
cap_sys_admin admin;
fd.el = open(path, O_RDWR);
@@ -373,5 +489,11 @@
return 0;
}
/**
* @brief Return framebuffer metadata for the current CRTC.
*
* @param plane DRM or EGL plane index being described.
* @return Framebuffer metadata wrapper, or nullptr when the framebuffer cannot be read.
*/
fb_t fb(plane_t::pointer plane) {
cap_sys_admin admin;
@@ -389,23 +511,51 @@
return nullptr;
}
/**
* @brief Return the DRM CRTC object for the monitor.
*
* @param id DRM CRTC ID.
* @return Owning pointer to the DRM CRTC object.
*/
crtc_t crtc(std::uint32_t id) {
return drmModeGetCrtc(fd.el, id);
}
/**
* @brief Return the DRM encoder object for the connector.
*
* @param id DRM encoder ID.
* @return Owning pointer to the DRM encoder object.
*/
encoder_t encoder(std::uint32_t id) {
return drmModeGetEncoder(fd.el, id);
}
/**
* @brief Return the DRM resource list for the card.
*
* @return DRM card resource list.
*/
res_t res() {
return drmModeGetResources(fd.el);
}
/**
* @brief Check whether nvidia.
*
* @return True when the DRM device appears to be driven by NVIDIA.
*/
bool is_nvidia() {
version_t ver {drmGetVersion(fd.el)};
return ver && ver->name && strncmp(ver->name, "nvidia-drm", 10) == 0;
}
/**
* @brief Check whether cursor.
*
* @param plane_id Plane ID.
* @return True when the DRM plane is a cursor plane.
*/
bool is_cursor(std::uint32_t plane_id) {
auto props = plane_props(plane_id);
for (auto &[prop, val] : props) {
@@ -421,6 +571,13 @@
return false;
}
/**
* @brief Look up a DRM property value by property name.
*
* @param props DRM property collection to inspect.
* @param name DRM property name to search for.
* @return Property value when the property exists.
*/
std::optional<std::uint64_t> prop_value_by_name(const std::vector<std::pair<prop_t, std::uint64_t>> &props, std::string_view name) {
for (auto &[prop, val] : props) {
if (prop->name == name) {
@@ -430,6 +587,12 @@
return std::nullopt;
}
/**
* @brief Get panel orientation.
*
* @param plane_id Plane ID.
* @return DRM rotation bitmask for the panel orientation.
*/
std::uint32_t get_panel_orientation(std::uint32_t plane_id) {
auto props = plane_props(plane_id);
auto value = prop_value_by_name(props, "rotation"sv);
@@ -441,6 +604,12 @@
return DRM_MODE_ROTATE_0;
}
/**
* @brief Get crtc index by ID.
*
* @param crtc_id Crtc ID.
* @return Zero-based CRTC index, or -1 when the CRTC ID is unknown.
*/
int get_crtc_index_by_id(std::uint32_t crtc_id) {
auto resources = res();
for (int i = 0; i < resources->count_crtcs; i++) {
@@ -451,10 +620,22 @@
return -1;
}
/**
* @brief Return the DRM connector object for the display.
*
* @param id DRM connector ID.
* @return Owning pointer to the DRM connector object.
*/
connector_interal_t connector(std::uint32_t id) {
return drmModeGetConnector(fd.el, id);
}
/**
* @brief Refresh the monitor list reported by the display server.
*
* @param conn_type_count Conn type count.
* @return Connector descriptors for monitors known to the DRM card.
*/
std::vector<connector_t> monitors(conn_type_count_t &conn_type_count) {
auto resources = res();
if (!resources) {
@@ -489,5 +670,11 @@
return monitors;
}
/**
* @brief Return the DRM file descriptor owned by the object.
*
* @param handle GEM handle exported by the DRM framebuffer.
* @return DMA-BUF file descriptor for the GEM handle.
*/
file_t handleFD(std::uint32_t handle) {
file_t fb_fd;
@@ -500,6 +687,13 @@
return fb_fd;
}
/**
* @brief Return DRM object properties for the resource.
*
* @param id DRM object ID.
* @param type DRM object type.
* @return DRM properties and their current values.
*/
std::vector<std::pair<prop_t, std::uint64_t>> props(std::uint32_t id, std::uint32_t type) {
obj_prop_t obj_prop = drmModeObjectGetProperties(fd.el, id, type);
if (!obj_prop) {
@@ -516,38 +710,83 @@
return props;
}
/**
* @brief Return DRM plane properties.
*
* @param id DRM plane ID.
* @return Plane properties and their current values.
*/
std::vector<std::pair<prop_t, std::uint64_t>> plane_props(std::uint32_t id) {
return props(id, DRM_MODE_OBJECT_PLANE);
}
/**
* @brief Return DRM CRTC properties.
*
* @param id DRM CRTC ID.
* @return CRTC properties and their current values.
*/
std::vector<std::pair<prop_t, std::uint64_t>> crtc_props(std::uint32_t id) {
return props(id, DRM_MODE_OBJECT_CRTC);
}
/**
* @brief Return DRM connector properties.
*
* @param id DRM connector ID.
* @return Connector properties and their current values.
*/
std::vector<std::pair<prop_t, std::uint64_t>> connector_props(std::uint32_t id) {
return props(id, DRM_MODE_OBJECT_CONNECTOR);
}
/**
* @brief Fetch DRM plane metadata by plane-list index.
*
* @param index Zero-based index into the DRM plane resource list.
* @return Plane metadata for the requested DRM plane.
*/
plane_t operator[](std::uint32_t index) {
return drmModeGetPlane(fd.el, plane_res->planes[index]);
}
/**
* @brief Return the number of items in the wrapped DRM collection.
*
* @return Number of DRM planes reported by the card.
*/
std::uint32_t count() {
return plane_res->count_planes;
}
/**
* @brief Return an iterator to the first byte in the buffer view.
*
* @return Iterator to the first element.
*/
plane_it_t begin() const {
return plane_it_t {fd.el, plane_res->planes, plane_res->planes + plane_res->count_planes};
}
/**
* @brief Return an iterator one past the final byte in the buffer view.
*
* @return Iterator one past the last element.
*/
plane_it_t end() const {
return plane_it_t {fd.el, plane_res->planes + plane_res->count_planes};
}
file_t fd; ///< DRM card file descriptor.
file_t render_fd; ///< Render fd.
plane_res_t plane_res; ///< Plane res.
file_t fd;
file_t render_fd;
plane_res_t plane_res;
};
/**
* @brief Build a lookup from DRM CRTC IDs to monitor descriptors.
*
* @param connectors DRM connector list being mapped to monitors.
* @return Map keyed by CRTC ID.
*/
std::map<std::uint32_t, monitor_t> map_crtc_to_monitor(const std::vector<connector_t> &connectors) {
std::map<std::uint32_t, monitor_t> result;
@@ -562,6 +801,9 @@
return result;
}
/**
* @brief Multi-source KMS image assembled for encoding.
*/
struct kms_img_t: public img_t {
~kms_img_t() override {
delete[] data;
@@ -569,6 +811,13 @@
}
};
/**
* @brief Write a debug log representation of the input packet.
*
* @param plane DRM or EGL plane index being described.
* @param fb Framebuffer object to bind or update.
* @param crtc DRM CRTC identifier to map.
*/
void print(plane_t::pointer plane, fb_t::pointer fb, crtc_t::pointer crtc) {
if (crtc) {
BOOST_LOG(debug) << "crtc("sv << crtc->x << ", "sv << crtc->y << ')';
@@ -601,12 +850,27 @@
BOOST_LOG(debug) << ss.str();
}
/**
* @brief Base KMS display capture backend shared by RAM and VRAM paths.
*/
class display_t: public platf::display_t {
public:
/**
* @brief Initialize common KMS display state for the requested memory type.
*
* @param mem_type Mem type.
*/
display_t(mem_type_e mem_type):
platf::display_t(),
mem_type {mem_type} {
}
/**
* @brief Initialize the base KMS capture state for the selected monitor.
*
* @param display_name Display name.
* @param config Configuration values to apply.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(const std::string &display_name, const ::video::config_t &config) {
delay = std::chrono::nanoseconds {1s} / config.framerate;
@@ -816,6 +1080,11 @@
return 0;
}
/**
* @brief Report whether the active display mode is HDR.
*
* @return True when the active display mode is HDR.
*/
bool is_hdr() {
if (!hdr_metadata_blob_id || *hdr_metadata_blob_id == 0) {
return false;
@@ -862,6 +1131,12 @@
}
}
/**
* @brief Read HDR metadata for the active display mode.
*
* @param metadata Output structure populated with HDR metadata.
* @return True when HDR metadata was written to the output structure.
*/
bool get_hdr_metadata(SS_HDR_METADATA &metadata) {
// This performs all the metadata validation
if (!is_hdr()) {
@@ -891,6 +1166,9 @@
return true;
}
/**
* @brief Update cached cursor-plane image and position.
*/
void update_cursor() {
if (cursor_plane_id < 0) {
return;
@@ -1069,6 +1347,14 @@
}
}
/**
* @brief Refresh cached platform state from the operating system.
*
* @param file DMA-BUF file descriptors exported for the current framebuffer.
* @param sd EGL surface descriptor to import.
* @param frame_timestamp Output timestamp assigned to the captured frame.
* @return Capture status after refreshing framebuffer and cursor state.
*/
inline capture_e refresh(file_t *file, egl::surface_descriptor_t *sd, std::optional<std::chrono::steady_clock::time_point> &frame_timestamp) {
// Check for a change in HDR metadata
if (connector_id) {
@@ -1133,34 +1419,49 @@
return capture_e::ok;
}
mem_type_e mem_type;
mem_type_e mem_type; ///< Mem type.
std::chrono::nanoseconds delay;
std::chrono::nanoseconds delay; ///< Delay before the timer task becomes eligible to run.
int img_width;
int img_height;
int img_offset_x;
int img_offset_y;
int img_width; ///< Img width.
int img_height; ///< Img height.
int img_offset_x; ///< Img offset x.
int img_offset_y; ///< Img offset y.
int plane_id; ///< Plane ID.
int plane_id;
int crtc_id;
int crtc_id; ///< Crtc ID.
int crtc_index; ///< Crtc index.
int crtc_index;
std::optional<uint32_t> connector_id;
std::optional<uint64_t> hdr_metadata_blob_id;
std::optional<uint32_t> connector_id; ///< Connector ID.
std::optional<uint64_t> hdr_metadata_blob_id; ///< HDR metadata blob ID.
int cursor_plane_id;
int cursor_plane_id; ///< Cursor plane ID.
cursor_t captured_cursor {}; ///< Captured cursor.
cursor_t captured_cursor {};
card_t card;
card_t card; ///< Card.
};
/**
* @brief KMS capture backend that copies frames into system memory.
*/
class display_ram_t: public display_t {
public:
/**
* @brief Initialize a KMS display backend that copies frames through RAM.
*
* @param mem_type Mem type.
*/
display_ram_t(mem_type_e mem_type):
display_t(mem_type) {
}
/**
* @brief Initialize KMS capture that copies frames into system memory.
*
* @param display_name Display name.
* @param config Configuration values to apply.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(const std::string &display_name, const ::video::config_t &config) {
if (!gbm::create_device) {
BOOST_LOG(warning) << "libgbm not initialized"sv;
@@ -1237,6 +1538,12 @@
return capture_e::ok;
}
/**
* @brief Create AVCodec encode device.
*
* @param pix_fmt Sunshine pixel format to convert or allocate for.
* @return Constructed AVCodec encode device object.
*/
std::unique_ptr<avcodec_encode_device_t> make_avcodec_encode_device(pix_fmt_e pix_fmt) override {
#ifdef SUNSHINE_BUILD_VAAPI
if (mem_type == mem_type_e::vaapi) {
@@ -1253,6 +1560,11 @@
return std::make_unique<avcodec_encode_device_t>();
}
/**
* @brief Blend the captured cursor plane into a KMS frame.
*
* @param img Image or frame object to read from or populate.
*/
void blend_cursor(img_t &img) {
// TODO: Cursor scaling is not supported in this codepath.
// We always draw the cursor at the source size.
@@ -1301,5 +1613,14 @@
}
}
/**
* @brief Capture a display frame into the provided image object.
*
* @param pull_free_image_cb Callback that provides an available image buffer.
* @param img_out Captured KMS image returned to the streaming pipeline.
* @param timeout Maximum time to wait for the operation.
* @param cursor Cursor image or visibility state to composite.
* @return Capture status reported to the streaming pipeline.
*/
capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor) {
file_t fb_fd[4];
@@ -1344,6 +1665,11 @@
return capture_e::ok;
}
/**
* @brief Allocate an image buffer compatible with this display backend.
*
* @return Allocated img object, or null when unavailable.
*/
std::shared_ptr<img_t> alloc_img() override {
auto img = std::make_shared<kms_img_t>();
img->width = width;
@@ -1355,21 +1681,41 @@
return img;
}
/**
* @brief Populate a fallback image when real capture data is unavailable.
*
* @param img Image or frame object to read from or populate.
* @return Capture status reported to the streaming pipeline.
*/
int dummy_img(platf::img_t *img) override {
return 0;
}
gbm::gbm_t gbm;
egl::display_t display;
egl::ctx_t ctx;
gbm::gbm_t gbm; ///< GBM device used for buffer allocation.
egl::display_t display; ///< EGL display created from the GBM device.
egl::ctx_t ctx; ///< EGL context used to copy KMS frames into RAM.
};
/**
* @brief KMS capture backend that exports frames as GPU resources.
*/
class display_vram_t: public display_t {
public:
/**
* @brief Initialize a KMS display backend that exports frames as GPU resources.
*
* @param mem_type Mem type.
*/
display_vram_t(mem_type_e mem_type):
display_t(mem_type) {
}
/**
* @brief Create AVCodec encode device.
*
* @param pix_fmt Sunshine pixel format to convert or allocate for.
* @return Constructed AVCodec encode device object.
*/
std::unique_ptr<avcodec_encode_device_t> make_avcodec_encode_device(pix_fmt_e pix_fmt) override {
#ifdef SUNSHINE_BUILD_VAAPI
if (mem_type == mem_type_e::vaapi) {
@@ -1393,5 +1739,10 @@
return nullptr;
}
/**
* @brief Allocate an image buffer compatible with this display backend.
*
* @return Allocated img object, or null when unavailable.
*/
std::shared_ptr<img_t> alloc_img() override {
auto img = std::make_shared<egl::img_descriptor_t>();
@@ -1408,6 +1759,12 @@
return img;
}
/**
* @brief Populate a fallback image when real capture data is unavailable.
*
* @param img Image or frame object to read from or populate.
* @return Capture status reported to the streaming pipeline.
*/
int dummy_img(platf::img_t *img) override {
// Empty images are recognized as dummies by the zero sequence number
return 0;
@@ -1458,5 +1815,13 @@
return capture_e::ok;
}
/**
* @brief Capture a display frame into the provided image object.
*
* @param pull_free_image_cb Pull free image cb.
* @param img_out Captured KMS image descriptor returned to the streaming pipeline.
* @param cursor Cursor image or visibility state to composite.
* @return Capture status reported to the streaming pipeline.
*/
capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds /* timeout */, bool cursor) {
file_t fb_fd[4];
@@ -1500,6 +1865,13 @@
return capture_e::ok;
}
/**
* @brief Initialize KMS capture that exports frames as GPU resources.
*
* @param display_name Display name.
* @param config Configuration values to apply.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(const std::string &display_name, const ::video::config_t &config) {
if (display_t::init(display_name, config)) {
return -1;
@@ -1522,11 +1894,19 @@
return 0;
}
std::uint64_t sequence {};
std::uint64_t sequence {}; ///< Monotonic capture sequence assigned to KMS frames.
};
} // namespace kms
/**
* @brief Create a KMS display capture backend.
*
* @param hwdevice_type Hardware device type requested for capture or encode.
* @param display_name Display name.
* @param config Configuration values to apply.
* @return KMS display backend, or nullptr when initialization fails.
*/
std::shared_ptr<display_t> kms_display(mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) {
if (hwdevice_type == mem_type_e::vaapi || hwdevice_type == mem_type_e::cuda || hwdevice_type == mem_type_e::vulkan) {
auto disp = std::make_shared<kms::display_vram_t>(hwdevice_type);
@@ -1556,6 +1936,7 @@
* But, it's necessary for absolute mouse coordinates to work.
*
* This is an ugly hack :(
* @param cds Capture display state used by the KMS backend.
*/
void correlate_to_wayland(std::vector<kms::card_descriptor_t> &cds) {
auto monitors = wl::monitors();
@@ -1613,5 +1994,11 @@
}
// A list of names of displays accepted as display_name
/**
* @brief Enumerate display names accepted by the KMS backend.
*
* @param hwdevice_type Hardware device type requested for capture or encode.
* @return KMS display names, or an empty list when KMS capture is unavailable.
*/
std::vector<std::string> kms_display_names(mem_type_e hwdevice_type) {
int count = 0;
src/platform/linux/kwingrab.cpp +46 −9
@@ -49,10 +49,18 @@
*/
class screencast_permission_helper_t {
public:
/**
* @brief Check whether permission system deactivated.
*
* @return True when KWin reports that the permission system is disabled.
*/
static bool is_permission_system_deactivated() {
return getenvstr("KWIN_WAYLAND_NO_PERMISSION_CHECKS") == "1";
}
/**
* @brief Configure the KWin screencast session.
*/
static void setup() {
if (initialized) {
return;
@@ -139,6 +147,11 @@
initialized = true;
}
/**
* @brief Check whether newly initialized.
*
* @return True when KWin was initialized during this check.
*/
static bool is_newly_initialized() {
return create_file;
}
@@ -238,12 +251,18 @@
};
// Output parameters
/**
* @brief KWin screencast output name and geometry.
*/
struct output_parameter_t {
std::string name = "";
int width = 0;
int height = 0;
int pos_x = 0;
int pos_y = 0;
std::string name = ""; ///< KWin output name.
int width = 0; ///< Output width in pixels.
int height = 0; ///< Output height in pixels.
int pos_x = 0; ///< Output X position in the compositor layout.
int pos_y = 0; ///< Output Y position in the compositor layout.
// order is needed to get a sorted output list and should be updated before sorting to have current values
/**
* @brief Order.
*/
size_t order = SIZE_MAX; // Use high number to keep monitors with uninitialized order value to the back
};
@@ -443,9 +462,9 @@
return 0;
}
uint32_t out_node_id = PW_ID_ANY; ///< Out node ID.
uint64_t out_objectserial = SPA_ID_INVALID; ///< Out objectserial.
std::shared_ptr<output_parameter_t> out_params = nullptr; ///< Out params.
uint32_t out_node_id = PW_ID_ANY;
uint64_t out_objectserial = SPA_ID_INVALID;
std::shared_ptr<output_parameter_t> out_params = nullptr;
private:
// Wayland objects
@@ -688,12 +707,20 @@
return -1;
}
std::unique_ptr<screencast_t> screencast;
std::unique_ptr<screencast_t> screencast; ///< Screencast.
};
} // namespace kwin
// Public API for misc.cpp
namespace platf {
/**
* @brief Create a KWin screencast display backend.
*
* @param hwdevice_type Hardware device type requested for capture or encode.
* @param display_name Display name.
* @param config Configuration values to apply.
* @return KWin/PipeWire display backend, or nullptr when initialization fails.
*/
std::shared_ptr<display_t> kwin_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) {
if (!pipewire::pipewire_display_t::init_pipewire_and_check_hwdevice_type(hwdevice_type)) {
BOOST_LOG(error) << "[kwingrab] Could not initialize pipewire-based display with the given hw device type."sv;
@@ -708,6 +735,11 @@
return display;
}
/**
* @brief Enumerate KWin screencast display names.
*
* @return KWin display names, or an empty list when KWin capture is unavailable.
*/
std::vector<std::string> kwin_display_names() {
if (has_elevated_privileges(false)) {
// We're still in the probing phase of Sunshine startup. Dropping portal security early will break KMS.
@@ -724,6 +756,11 @@
return screencast->get_output_names();
}
/**
* @brief Check whether KWin screencast capture is available.
*
* @return True when KWin capture support is available.
*/
bool kwin_available() {
// Init screencast without permission setup (to not cause unneeded logs / temporary desktop files) and check KWin availability
if (const auto screencast = std::make_unique<kwin::screencast_t>(); screencast->init(false) < 0 || !screencast->kwin_available()) {
src/platform/linux/misc.cpp +145 −10
@@ -5,5 +5,9 @@
// Required for in6_pktinfo with glibc headers
#ifndef _GNU_SOURCE
/**
* @def _GNU_SOURCE
* @brief Macro for GNU SOURCE.
*/
#define _GNU_SOURCE 1
#endif
@@ -60,15 +64,31 @@
#ifdef __GNUC__
#define SUNSHINE_GNUC_EXTENSION __extension__
#else
/**
* @def SUNSHINE_GNUC_EXTENSION
* @brief Macro for SUNSHINE GNUC EXTENSION.
*/
#define SUNSHINE_GNUC_EXTENSION
#endif
#ifndef SOL_IP
/**
* @def SOL_IP
* @brief Macro for SOL IP.
*/
#define SOL_IP IPPROTO_IP
#endif
#ifndef SOL_IPV6
/**
* @def SOL_IPV6
* @brief Macro for SOL IPv6.
*/
#define SOL_IPV6 IPPROTO_IPV6
#endif
#ifndef SOL_UDP
/**
* @def SOL_UDP
* @brief Macro for SOL UDP.
*/
#define SOL_UDP IPPROTO_UDP
#endif
@@ -77,8 +97,11 @@
namespace fs = std::filesystem;
namespace bp = boost::process::v1;
window_system_e window_system;
window_system_e window_system; ///< Window system.
namespace dyn {
/**
* @brief Return the native handle owned by the wrapper.
*/
void *handle(const std::vector<const char *> &libs) {
void *handle;
@@ -103,6 +126,9 @@
return nullptr;
}
/**
* @brief Load persisted state from its backing store.
*/
int load(void *handle, const std::vector<std::tuple<apiproc *, const char *>> &funcs, bool strict) {
int err = 0;
for (auto &func : funcs) {
@@ -122,7 +148,15 @@
} // namespace dyn
namespace platf {
/**
* @brief Owning pointer for `getifaddrs` results.
*/
using ifaddr_t = util::safe_ptr<ifaddrs, freeifaddrs>;
/**
* @brief Read the local interface address list.
*
* @return Owning pointer to the interface address list, or nullptr on failure.
*/
ifaddr_t get_ifaddrs() {
ifaddrs *p {nullptr};
@@ -215,5 +249,8 @@
return config_path;
}
/**
* @brief Convert a socket address to a printable IP address.
*/
std::string from_sockaddr(const sockaddr *const ip_addr) {
char data[INET6_ADDRSTRLEN] = {};
@@ -228,5 +265,8 @@
return std::string {data};
}
/**
* @brief Convert a socket address to a port and printable IP address.
*/
std::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const ip_addr) {
char data[INET6_ADDRSTRLEN] = {};
@@ -244,5 +284,8 @@
return {port, std::string {data}};
}
/**
* @brief Return the hardware MAC address associated with a network address.
*/
std::string get_mac_address(const std::string_view &address) {
auto ifaddrs = get_ifaddrs();
@@ -336,6 +379,9 @@
}
}
/**
* @brief Apply the requested scheduling priority to the current thread.
*/
void adjust_thread_priority(thread_priority_e priority) {
#if defined(__FreeBSD__)
pid_t tid = syscall(SYS_thr_self);
@@ -405,18 +451,30 @@
pthread_setname_np(pthread_self(), name.c_str());
}
/**
* @brief Enable or disable X11 mouse keys for the current session.
*/
void enable_mouse_keys() {
// Unimplemented
}
/**
* @brief Apply Linux platform state before streaming starts.
*/
void streaming_will_start() {
// Nothing to do
}
/**
* @brief Restore Linux platform state after streaming stops.
*/
void streaming_will_stop() {
// Nothing to do
}
/**
* @brief Request a Sunshine process restart on exit.
*/
void restart_on_exit() {
char executable[PATH_MAX];
ssize_t len = readlink("/proc/self/exe", executable, PATH_MAX - 1);
@@ -439,12 +497,21 @@
}
}
/**
* @brief Restart the Sunshine process through the platform launcher.
*/
void restart() {
// Gracefully clean up and restart ourselves instead of exiting
atexit(restart_on_exit);
lifetime::exit_sunshine(0, true);
}
/**
* @brief Read an environment variable as an optional string.
*
* @param name Human-readable name to assign.
* @return Environment variable value, or an empty string when unset.
*/
std::string get_env(const std::string &name) {
if (const auto value = getenv(name.c_str()); value != nullptr) {
return value;
@@ -456,6 +523,14 @@
return setenv(name.c_str(), value.c_str(), 1);
}
/**
* @brief Append a value to a separator-delimited environment variable.
*
* @param name Human-readable name to assign.
* @param value Entry to add when it is not already present.
* @param separator Character used to join or split the value.
* @return Result from updating the environment variable.
*/
int append_env(const std::string &name, const std::string &value, const std::string &separator) {
if (const std::string old_value = get_env(name); !old_value.contains(value)) {
return set_env(name, old_value.empty() ? value : old_value + separator + value);
@@ -481,5 +556,12 @@
return waitpid(-((pid_t) native_handle), nullptr, WNOHANG) >= 0;
}
/**
* @brief Convert to sockaddr.
*
* @param address Network address being parsed or filtered.
* @param port TCP or UDP port number.
* @return Value converted to sockaddr.
*/
struct sockaddr_in to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) {
struct sockaddr_in saddr_v4 = {};
@@ -493,5 +575,12 @@
return saddr_v4;
}
/**
* @brief Convert to sockaddr.
*
* @param address Network address being parsed or filtered.
* @param port TCP or UDP port number.
* @return Value converted to sockaddr.
*/
struct sockaddr_in6 to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) {
struct sockaddr_in6 saddr_v6 = {};
@@ -506,6 +595,9 @@
return saddr_v6;
}
/**
* @brief Send multiple fixed-size UDP payload blocks using the platform backend.
*/
bool send_batch(batched_send_info_t &send_info) {
auto sockfd = (int) send_info.native_socket;
struct msghdr msg = {};
@@ -731,6 +823,9 @@
}
}
/**
* @brief Send the serialized response over the active socket.
*/
bool send(send_info_t &send_info) {
auto sockfd = (int) send_info.native_socket;
struct msghdr msg = {};
@@ -854,8 +949,17 @@
// are disconnected.
static std::atomic<int> qos_ref_count = 0;
/**
* @brief Linux QoS state used to tune socket priority while streaming.
*/
class qos_t: public deinit_t {
public:
/**
* @brief Apply Linux socket priority and DSCP QoS settings for scoped cleanup.
*
* @param sockfd Native socket descriptor whose options are updated.
* @param options Request options or socket options to apply.
*/
qos_t(int sockfd, std::vector<std::tuple<int, int, int>> options):
sockfd(sockfd),
options(options) {
@@ -880,11 +984,6 @@
/**
* @brief Enables QoS on the given socket for traffic to the specified destination.
* @param native_socket The native socket handle.
* @param address The destination address for traffic sent on this socket.
* @param port The destination port for traffic sent on this socket.
* @param data_type The type of traffic sent on this socket.
* @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic.
*/
std::unique_ptr<deinit_t> enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) {
int sockfd = (int) native_socket;
@@ -964,6 +1063,9 @@
}
namespace source {
/**
* @brief Enumerates supported source options.
*/
enum source_e : std::size_t {
#ifdef SUNSHINE_BUILD_CUDA
NVFBC, ///< NvFBC
@@ -999,9 +1101,27 @@
#endif
#ifdef SUNSHINE_BUILD_WAYLAND
/**
* @brief Enumerate displays available through the Wayland capture backend.
*
* @return Wayland display names, or an empty list when discovery fails.
*/
std::vector<std::string> wl_display_names();
/**
* @brief Create a Wayland display capture backend.
*
* @param hwdevice_type Hardware device type requested for capture or encode.
* @param display_name Display name.
* @param config Configuration values to apply.
* @return Display backend, or nullptr when Wayland capture initialization fails.
*/
std::shared_ptr<display_t> wl_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config);
/**
* @brief Check whether Wayland capture is available for the current session.
*
* @return True when the active window system is Wayland and at least one output is discoverable.
*/
bool verify_wl() {
return window_system == window_system_e::WAYLAND && !wl_display_names().empty();
}
@@ -1045,6 +1165,9 @@
}
#endif
/**
* @brief List display names accepted by the selected capture backend.
*/
std::vector<std::string> display_names(mem_type_e hwdevice_type) {
#ifdef SUNSHINE_BUILD_CUDA
// display using NvFBC only supports mem_type_e::cuda
@@ -1081,8 +1204,9 @@
}
/**
* @brief Returns if GPUs/drivers have changed since the last call to this function.
* @brief Report whether encoder backends should be probed again before streaming.
*
* @return Always `true` because Linux GPU changes are not tracked by this backend.
* @return `true` if a change has occurred or if it is unknown whether a change occurred.
*/
bool needs_encoder_reenumeration() {
// We don't track GPU state, so we will always reenumerate. Fortunately, it is fast on Linux.
@@ -1137,6 +1261,9 @@
return nullptr;
}
/**
* @brief Initialize the Linux high-precision timer file descriptor.
*/
std::unique_ptr<deinit_t> init() {
// enable low latency mode for AMD
// https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/30039
@@ -1212,6 +1339,9 @@
return std::make_unique<deinit_t>();
}
/**
* @brief Linux high-precision timer implementation backed by `timerfd`.
*/
class linux_high_precision_timer: public high_precision_timer {
public:
void sleep_for(const std::chrono::nanoseconds &duration) override {
@@ -1227,6 +1357,11 @@
return std::make_unique<linux_high_precision_timer>();
}
/**
* @brief Find the DRM render node associated with the active display.
*
* @return Render-node path, or an empty string when no matching node is found.
*/
std::string find_render_node_with_display() {
#ifdef SUNSHINE_BUILD_DRM
auto *dir = opendir("/dev/dri");
@@ -1287,8 +1422,8 @@
static constexpr cap_value_t FULL_CAPS[] = {CAP_SYS_ADMIN, CAP_SYS_NICE};
static constexpr cap_value_t ADMIN_CAPS[] = {CAP_SYS_ADMIN};
constexpr std::span<const cap_value_t> ELEVATED_PRIVILEGES_FULL {FULL_CAPS};
constexpr std::span<const cap_value_t> ELEVATED_PRIVILEGES_ADMIN {ADMIN_CAPS};
constexpr std::span<const cap_value_t> ELEVATED_PRIVILEGES_FULL {FULL_CAPS}; ///< Protocol or platform constant for elevated privileges full.
constexpr std::span<const cap_value_t> ELEVATED_PRIVILEGES_ADMIN {ADMIN_CAPS}; ///< Protocol or platform constant for elevated privileges admin.
#endif
bool has_elevated_privileges(bool all_caps) {
src/platform/linux/misc.h +14 −1
@@ -11,21 +11,34 @@
// local includes
#include "src/utility.h"
#ifndef DOXYGEN
KITTY_USING_MOVE_T(file_t, int, -1, {
if (el >= 0) {
close(el);
}
});
#else
/**
* @brief Move-only wrapper for a POSIX file descriptor.
*/
class file_t;
#endif
/**
* @brief Enumerates supported window system options.
*/
enum class window_system_e {
NONE, ///< No window system
X11, ///< X11
WAYLAND, ///< Wayland
};
extern window_system_e window_system;
extern window_system_e window_system; ///< Window system.
namespace dyn {
/**
* @brief Generic GLX procedure pointer returned by the loader.
*/
typedef void (*apiproc)(void);
int load(void *handle, const std::vector<std::tuple<apiproc *, const char *>> &funcs, bool strict = true);
src/platform/linux/pipewire.cpp +182 −31
@@ -25,7 +25,7 @@
#include "wayland.h"
#if !PW_CHECK_VERSION(1, 6, 0)
constexpr int SPA_VIDEO_TRANSFER_SMPTE2084 = 14;
constexpr int SPA_VIDEO_TRANSFER_SMPTE2084 = 14; ///< Protocol or platform constant for spa video transfer smpte2084.
#endif
#if PW_CHECK_VERSION(0, 3, 75)
@@ -36,6 +36,10 @@
constexpr bool SUNSHINE_USE_PIPEWIRE_OBJECT_SERIAL = true;
#else
// Pipewire object serials are unsupported without PW_KEY_TARGET_OBJECT (we define it here so compilation won't break but don't use it).
constexpr bool SUNSHINE_USE_PIPEWIRE_OBJECT_SERIAL = false; ///< Whether PipeWire object serials should be used for matching.
/**
constexpr bool SUNSHINE_USE_PIPEWIRE_OBJECT_SERIAL = false;
* @def PW_KEY_TARGET_OBJECT
* @brief Macro for PW KEY TARGET OBJECT.
*/
#define PW_KEY_TARGET_OBJECT "target.object"
#endif
@@ -51,9 +55,12 @@
using namespace std::literals;
namespace pipewire {
/**
* @brief PipeWire SPA format mapped to Sunshine pixel format.
*/
struct format_map_t {
uint64_t fourcc;
uint64_t fourcc; ///< DRM fourcc pixel format.
int32_t pw_format; ///< Matching PipeWire SPA video format.
int32_t pw_format;
};
static constexpr std::array<format_map_t, 7> format_map = {{
@@ -66,47 +73,59 @@
{DRM_FORMAT_XRGB8888, SPA_VIDEO_FORMAT_BGRx},
}};
/**
* @brief PipeWire capture state shared with callback threads.
*/
struct shared_state_t {
std::atomic<int> negotiated_width {0};
std::atomic<int> negotiated_height {0};
std::atomic<int> negotiated_width {0}; ///< Width negotiated with PipeWire for the stream.
std::atomic<int> color_primaries {0};
std::atomic<int> transfer_function {0};
std::atomic<bool> stream_dead {false};
pw_stream_state previous_state;
pw_stream_state current_state;
std::string err_msg;
std::atomic<int> negotiated_height {0}; ///< Height negotiated with PipeWire for the stream.
std::atomic<int> color_primaries {0}; ///< PipeWire color-primaries metadata for the stream.
std::atomic<int> transfer_function {0}; ///< PipeWire transfer-function metadata for the stream.
std::atomic<bool> stream_dead {false}; ///< Whether the PipeWire stream has been destroyed.
pw_stream_state previous_state; ///< Previous PipeWire stream state reported by callbacks.
pw_stream_state current_state; ///< Current PipeWire stream state reported by callbacks.
std::string err_msg; ///< Last PipeWire error message reported by the stream.
};
/**
* @brief PipeWire stream handle, format, and shared state pointer.
*/
struct stream_data_t {
struct pw_stream *stream; ///< PipeWire stream handle used for screencast frames.
struct spa_hook stream_listener; ///< Hook registering callbacks on the PipeWire stream.
struct spa_video_info format; ///< Negotiated PipeWire video format.
struct pw_buffer *current_buffer; ///< PipeWire buffer currently exposed to the capture thread.
struct pw_stream *stream;
struct spa_hook stream_listener;
struct spa_video_info format;
struct pw_buffer *current_buffer;
uint64_t drm_format;
std::shared_ptr<shared_state_t> shared;
std::mutex frame_mutex;
uint64_t drm_format; ///< DRM format.
std::shared_ptr<shared_state_t> shared; ///< State shared between PipeWire callbacks and the capture backend.
std::mutex frame_mutex; ///< Synchronizes access to the current PipeWire frame.
std::condition_variable frame_cv; ///< Signals arrival or release of a PipeWire frame.
size_t local_stride = 0; ///< Local stride.
bool frame_ready = false; ///< Whether a PipeWire frame is ready to consume.
std::condition_variable frame_cv;
size_t local_stride = 0;
bool frame_ready = false;
// Two distinct memory pools
std::vector<uint8_t> buffer_a; ///< First staging buffer used for CPU-copy PipeWire frames.
std::vector<uint8_t> buffer_a;
std::vector<uint8_t> buffer_b;
std::vector<uint8_t> buffer_b; ///< Second staging buffer used for CPU-copy PipeWire frames.
// Points to the buffer currently owned by fill_img
std::vector<uint8_t> *front_buffer;
std::vector<uint8_t> *front_buffer; ///< Staging buffer currently readable by `fill_img`.
// Points to the buffer currently being written by on_process
std::vector<uint8_t> *back_buffer;
std::vector<uint8_t> *back_buffer; ///< Staging buffer currently writable by PipeWire callbacks.
stream_data_t():
front_buffer(&buffer_a),
back_buffer(&buffer_b) {}
};
/**
* @brief DMA-BUF format and modifier list advertised by PipeWire.
*/
struct dmabuf_format_info_t {
int32_t format;
int32_t format; ///< PipeWire SPA video format being advertised.
uint64_t *modifiers; ///< DRM format modifiers supported for the format.
int n_modifiers; ///< Number of entries in `modifiers`.
uint64_t *modifiers;
int n_modifiers;
};
/**
* @brief PipeWire core, context, and stream setup used for screencast capture.
*/
class pipewire_t {
public:
pipewire_t():
@@ -154,22 +173,51 @@
pw_thread_loop_destroy(loop);
}
/**
* @brief Return the mutex protecting PipeWire frame state.
*
* @return Mutex used by producer and capture threads.
*/
std::mutex &frame_mutex() {
return stream_data.frame_mutex;
}
/**
* @brief Return the condition variable signaled when frame state changes.
*
* @return Condition variable used to wait for frames or shutdown.
*/
std::condition_variable &frame_cv() {
return stream_data.frame_cv;
}
/**
* @brief Check whether frame ready.
*
* @return True when PipeWire has delivered a frame ready for capture.
*/
bool is_frame_ready() const {
return stream_data.frame_ready;
}
/**
* @brief Set frame ready.
*
* @param ready Whether the PipeWire frame is ready for capture.
*/
void set_frame_ready(bool ready) {
stream_data.frame_ready = ready;
}
/**
* @brief Initialize PipeWire core objects and optional stream negotiation.
*
* @param stream_fd Stream fd.
* @param stream_node Stream node.
* @param stream_object_serial Stream object serial.
* @param shared_state Shared state.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(const int stream_fd, const uint32_t stream_node, const uint64_t stream_object_serial, std::shared_ptr<shared_state_t> shared_state) {
fd = stream_fd;
node = stream_node;
@@ -201,6 +249,9 @@
return 0;
}
/**
* @brief Release the active PipeWire stream and listener.
*/
void cleanup_stream() {
BOOST_LOG(debug) << "[pipewire] Cleaning up stream"sv;
if (loop && stream_data.stream) {
@@ -226,6 +277,18 @@
}
}
/**
* @brief Create the PipeWire stream if it is not already active.
*
* @param mem_type Mem type.
* @param width Frame or display width in pixels.
* @param height Frame or display height in pixels.
* @param refresh_rate Refresh rate.
* @param dmabuf_infos Dmabuf infos.
* @param n_dmabuf_infos N dmabuf infos.
* @param display_is_nvidia Display is nvidia.
* @return 0 when the PipeWire stream is configured; nonzero on negotiation failure.
*/
int ensure_stream(const platf::mem_type_e mem_type, const uint32_t width, const uint32_t height, const uint32_t refresh_rate, const struct dmabuf_format_info_t *dmabuf_infos, const int n_dmabuf_infos, const bool display_is_nvidia) {
pw_thread_loop_lock(loop);
int result = 0;
@@ -293,6 +356,11 @@
return result;
}
/**
* @brief Close img fds.
*
* @param img_descriptor Image descriptor whose duplicated DMA-BUF fds are closed.
*/
static void close_img_fds(egl::img_descriptor_t *img_descriptor) {
for (int &fd : img_descriptor->sd.fds) {
if (fd >= 0) {
@@ -302,5 +370,11 @@
}
}
/**
* @brief Copy PipeWire metadata into the Sunshine image descriptor.
*
* @param img_descriptor Image descriptor receiving timestamps, sequence, and damage flags.
* @param buf Raw byte buffer used for serialization.
*/
static void fill_img_metadata(egl::img_descriptor_t *img_descriptor, struct spa_buffer *buf) {
img_descriptor->frame_timestamp = std::chrono::steady_clock::now();
@@ -323,6 +397,13 @@
img_descriptor->pw_damage = (damage && damage->region.size.width > 0 && damage->region.size.height > 0) ? std::optional<bool>(true) : std::nullopt;
}
/**
* @brief Populate a Sunshine image descriptor from PipeWire DMA-BUF planes.
*
* @param img_descriptor Image descriptor receiving duplicated fds and plane layout.
* @param buf Raw byte buffer used for serialization.
* @param d PipeWire listener data passed to the callback.
*/
static void fill_img_dmabuf(egl::img_descriptor_t *img_descriptor, struct spa_buffer *buf, const stream_data_t &d) {
img_descriptor->sd.width = d.format.info.raw.size.width;
img_descriptor->sd.height = d.format.info.raw.size.height;
@@ -335,6 +416,11 @@
}
}
/**
* @brief Copy the latest PipeWire frame into Sunshine's image buffer.
*
* @param img Image or frame object to read from or populate.
*/
void fill_img(platf::img_t *img) {
pw_thread_loop_lock(loop);
std::scoped_lock lock(stream_data.frame_mutex);
@@ -367,6 +453,11 @@
pw_thread_loop_unlock(loop);
}
/**
* @brief Set negotiate maxframerate.
*
* @param negotiate_maxframerate Negotiate maxframerate.
*/
void set_negotiate_maxframerate(bool negotiate_maxframerate) {
negotiate_maxframerate_ = negotiate_maxframerate;
}
@@ -626,8 +717,17 @@
};
};
/**
* @brief Display capture backend that consumes frames from a PipeWire stream.
*/
class pipewire_display_t: public platf::display_t {
public:
/**
* @brief Initialize pipewire and check hwdevice type.
*
* @param hwdevice_type Hardware device type requested for capture or encode.
* @return True when PipeWire is initialized and the hardware device type is supported.
*/
static bool init_pipewire_and_check_hwdevice_type(platf::mem_type_e hwdevice_type) {
// Initialize pipewire to load necessary modules
pw_init(nullptr, nullptr);
@@ -694,6 +794,14 @@
}
}
/**
* @brief Initialize the PipeWire display backend for a selected stream.
*
* @param hwdevice_type Hardware device type requested for capture or encode.
* @param display_name Display name.
* @param config Configuration values to apply.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) {
// calculate frame interval we should capture at
framerate = config.framerate;
@@ -781,6 +889,15 @@
return 0;
}
/**
* @brief Capture a display frame into the provided image object.
*
* @param pull_free_image_cb Callback that provides an available image buffer.
* @param img_out Captured PipeWire image returned to the streaming pipeline.
* @param timeout Maximum time to wait for the operation.
* @param show_cursor Show cursor.
* @return Capture status reported to the streaming pipeline.
*/
platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool show_cursor) {
// FIXME: show_cursor is ignored
auto deadline = std::chrono::steady_clock::now() + timeout;
@@ -812,6 +929,11 @@
return platf::capture_e::timeout;
}
/**
* @brief Allocate an image buffer compatible with this display backend.
*
* @return Allocated img object, or null when unavailable.
*/
std::shared_ptr<platf::img_t> alloc_img() override {
// Note: this img_t type is also used for memory buffers
auto img = std::make_shared<egl::img_descriptor_t>();
@@ -828,6 +950,12 @@
return img;
}
/**
* @brief Check stream dead.
*
* @param out_status Out status.
* @return True when the PipeWire stream can no longer produce frames.
*/
virtual bool check_stream_dead(platf::capture_e &out_status) {
return false; // Return to default stream dead handling.
}
@@ -899,6 +1027,12 @@
return platf::capture_e::ok;
}
/**
* @brief Create AVCodec encode device.
*
* @param pix_fmt Sunshine pixel format to convert or allocate for.
* @return Constructed AVCodec encode device object.
*/
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override {
#ifdef SUNSHINE_BUILD_VAAPI
if (mem_type == platf::mem_type_e::vaapi) {
@@ -928,6 +1062,12 @@
return std::make_unique<platf::avcodec_encode_device_t>();
}
/**
* @brief Populate a fallback image when real capture data is unavailable.
*
* @param img Image or frame object to read from or populate.
* @return Capture status reported to the streaming pipeline.
*/
int dummy_img(platf::img_t *img) override {
if (!img) {
return -1;
@@ -938,6 +1078,11 @@
return 0;
}
/**
* @brief Report whether the active display mode is HDR.
*
* @return True when the active display mode is HDR.
*/
bool is_hdr() override {
int color_primaries = shared_state->color_primaries.load();
int transfer_function = shared_state->transfer_function.load();
@@ -949,6 +1094,12 @@
return false;
}
/**
* @brief Read HDR metadata for the active display mode.
*
* @param metadata Output structure populated with HDR metadata.
* @return True when HDR metadata was written to the output structure.
*/
bool get_hdr_metadata(SS_HDR_METADATA &metadata) override {
int color_primaries = shared_state->color_primaries.load();
int transfer_function = shared_state->transfer_function.load();
@@ -1122,7 +1273,7 @@
protected:
// Allow subclasses to access for pipewire requirements setup and stream dead checks
pipewire_t pipewire;
std::shared_ptr<shared_state_t> shared_state;
pipewire_t pipewire; ///< Pipewire.
std::shared_ptr<shared_state_t> shared_state; ///< Shared state.
};
} // namespace pipewire
src/platform/linux/portalgrab.cpp +110 −14
@@ -35,20 +35,41 @@
// Forward declarations
class runtime_t;
/**
* @brief Persistent portal restore token used to reuse screencast permission.
*/
class restore_token_t {
public:
/**
* @brief Return the currently wrapped value or handle.
*
* @return Underlying native handle or object pointer.
*/
static std::string get() {
return *token_;
}
/**
* @brief Store the new value and mark it dirty for persistence.
*
* @param value Portal restore token received from xdg-desktop-portal.
*/
static void set(std::string_view value) {
*token_ = value;
}
/**
* @brief Return whether the persisted value is empty.
*
* @return True when no portal display id has been persisted.
*/
static bool empty() {
return token_->empty();
}
/**
* @brief Load persisted state from its backing store.
*/
static void load() {
std::ifstream file(get_file_path());
if (file.is_open()) {
@@ -59,6 +80,9 @@
}
}
/**
* @brief Save current state to its backing store.
*/
static void save() {
if (token_->empty()) {
return;
@@ -80,21 +104,32 @@
}
};
/**
* @brief DBus response loop and response variant for portal calls.
*/
struct dbus_response_t {
GMainLoop *loop; ///< GLib main loop waiting for a portal response signal.
GMainLoop *loop;
GVariant *response;
guint subscription_id;
GVariant *response; ///< DBus response payload returned by the portal.
guint subscription_id; ///< Subscription ID.
};
/**
* @brief PipeWire stream node and negotiated capture size.
*/
struct pipewire_streaminfo_t {
uint32_t pipewire_node = PW_ID_ANY;
uint64_t pipewire_object_serial = SPA_ID_INVALID;
int width = 0;
int height = 0;
uint32_t pipewire_node = PW_ID_ANY; ///< PipeWire node ID selected by the portal.
uint64_t pipewire_object_serial = SPA_ID_INVALID; ///< PipeWire object serial selected by the portal.
int width = 0; ///< Stream width in pixels.
int height = 0; ///< Stream height in pixels.
int pos_x = 0; ///< Output X position reported by the portal.
int pos_y = 0; ///< Output Y position reported by the portal.
std::string monitor_name; ///< Monitor name.
int pos_x = 0;
int pos_y = 0;
std::string monitor_name;
/**
* @brief Convert to display name.
*
* @return Value converted to display name.
*/
std::string to_display_name() {
if (!monitor_name.empty()) {
return monitor_name;
@@ -102,12 +137,21 @@
return std::format("position-{}x{}-resolution-{}x{}", pos_x, pos_y, width, height);
}
/**
* @brief Check whether a portal stream matches a requested display name.
*
* @param display_name Display name.
* @return True when the portal display id matches the requested display name.
*/
bool match_display_name(const std::string_view &display_name) {
// Check the given non-empty display name matches the display name for this struct
return !display_name.empty() && display_name == to_display_name();
}
};
/**
* @brief DBus connection and portal request helpers for screencast setup.
*/
class dbus_t {
public:
dbus_t &operator=(dbus_t &&) = delete; // Do not allow to copying
@@ -157,5 +201,10 @@
}
}
/**
* @brief Open DBus and prepare portal screencast request handling.
*
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init() {
restore_token_t::load();
@@ -176,6 +225,11 @@
return 0;
}
/**
* @brief Connect to xdg-desktop-portal and restore or create a screencast session.
*
* @return 0 when a portal session is ready; nonzero when D-Bus or portal setup fails.
*/
int connect_to_portal() {
g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, FALSE);
g_autofree gchar *session_path = nullptr;
@@ -203,6 +257,14 @@
// Try to create a combined RemoteDesktop + ScreenCast session
// Returns true on success, false if should fall back to ScreenCast-only
/**
* @brief Try to create a RemoteDesktop portal session.
*
* @param loop GLib main loop associated with the portal request.
* @param session_path Session path.
* @param session_token Session token.
* @return True when the portal request or state check succeeds.
*/
bool try_remote_desktop_session(GMainLoop *loop, gchar **session_path, const gchar *session_token) {
if (create_portal_session(loop, session_path, session_token, false) < 0) {
return false;
@@ -226,6 +288,13 @@
}
// Create a ScreenCast-only session
/**
* @brief Create a screencast-only portal session without remote-desktop control.
*
* @param loop GLib main loop associated with the portal request.
* @param session_path Session path.
* @return 0 when the portal returns a session path; nonzero on request failure.
*/
int try_screencast_only_session(GMainLoop *loop, gchar **session_path) {
g_autofree gchar *new_session_token = nullptr;
create_session_path(conn, nullptr, &new_session_token);
@@ -240,6 +309,11 @@
return 0;
}
/**
* @brief Check whether session closed.
*
* @return True when the portal session has been closed.
*/
bool is_session_closed() const {
if (conn && !session_handle.empty()) {
// Try to retrieve property org.freedesktop.portal.Session::version
@@ -267,8 +341,8 @@
return false;
}
std::vector<pipewire_streaminfo_t> pipewire_streams;
int pipewire_fd;
std::vector<pipewire_streaminfo_t> pipewire_streams; ///< Pipewire streams.
int pipewire_fd; ///< Pipewire fd.
private:
GDBusConnection *conn;
@@ -628,6 +702,9 @@
}
};
/**
* @brief Portal screencast backend that negotiates PipeWire streams over DBus.
*/
class portal_t: public pipewire::pipewire_display_t {
public:
int configure_stream(const std::string &display_name, int &out_pipewire_fd, uint32_t &out_pipewire_node, uint64_t &out_pipewire_object_serial [[maybe_unused]]) override {
@@ -680,6 +757,12 @@
return 0;
}
/**
* @brief Check stream dead.
*
* @param out_status Out status.
* @return True when the PipeWire stream can no longer produce frames.
*/
bool check_stream_dead(platf::capture_e &out_status) override {
// If the pipewire stream stopped due to closed portal session stop the capture with an error
if (dbus.is_session_closed()) {
@@ -700,14 +783,22 @@
}
// DBus portal connection
dbus_t dbus;
dbus_t dbus; ///< DBus connection used for portal screencast requests.
// Class variable to store runtime state of maxFramerate negotiation
static inline std::atomic<bool> negotiate_maxframerate {true};
static inline std::atomic<bool> negotiate_maxframerate {true}; ///< Whether portal negotiation should request the maximum frame rate.
};
} // namespace portal
namespace platf {
/**
* @brief Create a portal-based display capture backend.
*
* @param hwdevice_type Hardware device type requested for capture or encode.
* @param display_name Display name.
* @param config Configuration values to apply.
* @return Display backend backed by xdg-desktop-portal and PipeWire, or nullptr.
*/
std::shared_ptr<display_t> portal_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) {
using enum platf::mem_type_e;
if (!pipewire::pipewire_display_t::init_pipewire_and_check_hwdevice_type(hwdevice_type)) {
@@ -728,6 +819,11 @@
return portal;
}
/**
* @brief Enumerate capture targets available through xdg-desktop-portal.
*
* @return Portal display names, or an empty list when portal discovery fails.
*/
std::vector<std::string> portal_display_names() {
std::vector<std::string> display_names;
auto dbus = std::make_shared<portal::dbus_t>();
src/platform/linux/publish.cpp +160 −24
@@ -85,14 +85,20 @@
ERR_MAX = -54 ///< TODO
};
constexpr auto IF_UNSPEC = -1;
constexpr auto IF_UNSPEC = -1; ///< Protocol or platform constant for if unspec.
/**
* @brief Enumerates supported proto options.
*/
enum proto {
PROTO_INET = 0, ///< IPv4
PROTO_INET6 = 1, ///< IPv6
PROTO_UNSPEC = -1 ///< Unspecified/all protocol(s)
};
/**
* @brief Enumerates supported server state options.
*/
enum ServerState {
SERVER_INVALID, ///< Invalid state (initial)
SERVER_REGISTERING, ///< Host RRs are being registered
@@ -101,6 +107,9 @@
SERVER_FAILURE ///< Some fatal failure happened, the server is unable to proceed
};
/**
* @brief Enumerates supported client state options.
*/
enum ClientState {
CLIENT_S_REGISTERING = SERVER_REGISTERING, ///< Server state: REGISTERING
CLIENT_S_RUNNING = SERVER_RUNNING, ///< Server state: RUNNING
@@ -109,6 +118,9 @@
CLIENT_CONNECTING = 101 ///< We're still connecting. This state is only entered when AVAHI_CLIENT_NO_FAIL has been passed to avahi_client_new() and the daemon is not yet available.
};
/**
* @brief Enumerates supported entry group state options.
*/
enum EntryGroupState {
ENTRY_GROUP_UNCOMMITED, ///< The group has not yet been committed, the user must still call avahi_entry_group_commit()
ENTRY_GROUP_REGISTERING, ///< The entries of the group are currently being registered
@@ -117,6 +129,9 @@
ENTRY_GROUP_FAILURE ///< Some kind of failure happened, the entries have been withdrawn
};
/**
* @brief Enumerates supported client flags options.
*/
enum ClientFlags {
CLIENT_IGNORE_USER_CONFIG = 1, ///< Don't read user configuration
CLIENT_NO_FAIL = 2 ///< Don't fail if the daemon is not available when avahi_client_new() is called, instead enter CLIENT_CONNECTING state and wait for the daemon to appear
@@ -137,7 +152,13 @@
PUBLISH_USE_MULTICAST = 256 ///< Register the record using multicast DNS
};
/**
* @brief Network interface index type used by Avahi.
*/
using IfIndex = int;
/**
* @brief Avahi protocol/address-family selector type.
*/
using Protocol = int;
struct EntryGroup;
@@ -145,18 +166,45 @@
struct SimplePoll;
struct Client;
/**
* @brief Avahi client state-change callback signature.
*/
typedef void (*ClientCallback)(Client *, ClientState, void *userdata);
/**
* @brief Avahi entry-group state-change callback signature.
*/
typedef void (*EntryGroupCallback)(EntryGroup *g, EntryGroupState state, void *userdata);
/**
* @brief Function pointer used to free Avahi-allocated memory.
*/
typedef void (*free_fn)(void *);
/**
* @brief Function pointer used to create an Avahi client.
*/
typedef Client *(*client_new_fn)(const Poll *poll_api, ClientFlags flags, ClientCallback callback, void *userdata, int *error);
/**
* @brief Function pointer used to destroy an Avahi client.
*/
typedef void (*client_free_fn)(Client *);
/**
* @brief Function pointer used to request an alternative Avahi service name.
*/
typedef char *(*alternative_service_name_fn)(char *);
/**
* @brief Function pointer used to get an Avahi client from an entry group.
*/
typedef Client *(*entry_group_get_client_fn)(EntryGroup *);
/**
* @brief Function pointer used to create an Avahi entry group.
*/
typedef EntryGroup *(*entry_group_new_fn)(Client *, EntryGroupCallback, void *userdata);
/**
* @brief Function pointer used to add a service to an Avahi entry group.
*/
typedef int (*entry_group_add_service_fn)(
EntryGroup *group,
IfIndex interface,
@@ -170,39 +218,77 @@
...
);
/**
* @brief Function pointer used to test whether an Avahi entry group is empty.
*/
typedef int (*entry_group_is_empty_fn)(EntryGroup *);
/**
* @brief Function pointer used to reset an Avahi entry group.
*/
typedef int (*entry_group_reset_fn)(EntryGroup *);
/**
* @brief Function pointer used to publish an Avahi entry group.
*/
typedef int (*entry_group_commit_fn)(EntryGroup *);
/**
* @brief Function pointer used to duplicate Avahi strings.
*/
typedef char *(*strdup_fn)(const char *);
/**
* @brief Function pointer used to format Avahi error codes.
*/
typedef char *(*strerror_fn)(int);
/**
* @brief Function pointer used to read the last Avahi client error.
*/
typedef int (*client_errno_fn)(Client *);
/**
* @brief Function pointer used to get the Avahi simple-poll API.
*/
typedef Poll *(*simple_poll_get_fn)(SimplePoll *);
/**
* @brief Function pointer used to run the Avahi simple-poll loop.
*/
typedef int (*simple_poll_loop_fn)(SimplePoll *);
/**
* @brief Function pointer used to stop the Avahi simple-poll loop.
*/
typedef void (*simple_poll_quit_fn)(SimplePoll *);
/**
* @brief Function pointer used to allocate an Avahi simple-poll instance.
*/
typedef SimplePoll *(*simple_poll_new_fn)();
/**
* @brief Function pointer used to free an Avahi simple-poll instance.
*/
typedef void (*simple_poll_free_fn)(SimplePoll *);
free_fn free;
client_new_fn client_new;
client_free_fn client_free;
alternative_service_name_fn alternative_service_name;
entry_group_get_client_fn entry_group_get_client;
entry_group_new_fn entry_group_new;
entry_group_add_service_fn entry_group_add_service;
entry_group_is_empty_fn entry_group_is_empty;
entry_group_reset_fn entry_group_reset;
entry_group_commit_fn entry_group_commit;
strdup_fn strdup;
strerror_fn strerror;
client_errno_fn client_errno;
simple_poll_get_fn simple_poll_get;
simple_poll_loop_fn simple_poll_loop;
simple_poll_quit_fn simple_poll_quit;
simple_poll_new_fn simple_poll_new;
simple_poll_free_fn simple_poll_free;
free_fn free; ///< Free.
client_new_fn client_new; ///< Client new.
client_free_fn client_free; ///< Client free.
alternative_service_name_fn alternative_service_name; ///< Alternative service name.
entry_group_get_client_fn entry_group_get_client; ///< Entry group get client.
entry_group_new_fn entry_group_new; ///< Entry group new.
entry_group_add_service_fn entry_group_add_service; ///< Entry group add service.
entry_group_is_empty_fn entry_group_is_empty; ///< Entry group is empty.
entry_group_reset_fn entry_group_reset; ///< Entry group reset.
entry_group_commit_fn entry_group_commit; ///< Entry group commit.
strdup_fn strdup; ///< Strdup.
strerror_fn strerror; ///< Strerror.
client_errno_fn client_errno; ///< Client errno.
simple_poll_get_fn simple_poll_get; ///< Simple poll get.
simple_poll_loop_fn simple_poll_loop; ///< Simple poll loop.
simple_poll_quit_fn simple_poll_quit; ///< Simple poll quit.
simple_poll_new_fn simple_poll_new; ///< Simple poll new.
simple_poll_free_fn simple_poll_free; ///< Simple poll free.
/**
* @brief Load common Avahi client and entry-group function pointers.
*
* @return 0 when common Avahi functions are available; nonzero otherwise.
*/
int init_common() {
static void *handle {nullptr};
static bool funcs_loaded = false;
@@ -238,6 +324,11 @@
return 0;
}
/**
* @brief Load Avahi threaded-poll and client function pointers.
*
* @return 0 when all client functions are available; nonzero otherwise.
*/
int init_client() {
if (init_common()) {
return -1;
@@ -280,24 +371,49 @@
namespace platf::publish {
/**
* @brief Release memory allocated by Avahi.
*
* @param p Avahi-allocated pointer to release.
*/
template<class T>
void free(T *p) {
avahi::free(p);
}
/**
* @brief Owning pointer for Avahi allocations released with `avahi_free`.
*/
template<class T>
using ptr_t = util::safe_ptr<T, free<T>>;
/**
* @brief Owning pointer for an Avahi client.
*/
using client_t = util::dyn_safe_ptr<avahi::Client, &avahi::client_free>;
/**
* @brief Owning pointer for an Avahi simple poll loop.
*/
using poll_t = util::dyn_safe_ptr<avahi::SimplePoll, &avahi::simple_poll_free>;
avahi::EntryGroup *group = nullptr;
avahi::EntryGroup *group = nullptr; ///< Active Avahi entry group that owns the published service.
poll_t poll;
client_t client;
poll_t poll; ///< Avahi poll loop used while the service is published.
client_t client; ///< Avahi client used to register the Sunshine service.
ptr_t<char> name; ///< Current service name, updated when Avahi reports a collision.
ptr_t<char> name;
/**
* @brief Create or update the Avahi service entry group.
*
* @param c Avahi client used to allocate and commit the service entry group.
*/
void create_services(avahi::Client *c);
/**
* @brief React to Avahi entry-group state changes.
*
* @param g Entry group that emitted the state change.
* @param state Avahi entry-group state reported by the callback.
*/
void entry_group_callback(avahi::EntryGroup *g, avahi::EntryGroupState state, void *) {
group = g;
@@ -322,5 +438,8 @@
}
}
/**
* @brief Publish Sunshine's mDNS service through Avahi.
*/
void create_services(avahi::Client *c) {
int ret;
@@ -380,6 +499,12 @@
fg.disable();
}
/**
* @brief React to Avahi client state changes and register services when ready.
*
* @param c Avahi client that emitted the state change.
* @param state Avahi client state reported by the callback.
*/
void client_callback(avahi::Client *c, avahi::ClientState state, void *) {
switch (state) {
case avahi::CLIENT_S_RUNNING:
@@ -399,14 +524,25 @@
}
}
/**
* @brief RAII helper that runs shutdown cleanup when destroyed.
*/
class deinit_t: public ::platf::deinit_t {
public:
std::thread poll_thread; ///< Poll thread.
std::thread poll_thread;
/**
* @brief Store the Avahi polling thread for shutdown on destruction.
*
* @param poll_thread Poll thread.
*/
deinit_t(std::thread poll_thread):
poll_thread {std::move(poll_thread)} {
}
/**
* @brief Destroy the Avahi publisher deinitializer.
*/
~deinit_t() override {
if (avahi::simple_poll_quit && poll) {
avahi::simple_poll_quit(poll.get());
src/platform/linux/vaapi.cpp +135 −28
@@ -14,7 +14,14 @@
#include <va/va.h>
#include <va/va_drm.h>
#if !VA_CHECK_VERSION(1, 9, 0)
/**
* @brief Stub vaSyncBuffer when building against libva before 2.9.0.
*
* @param dpy VA display.
* @param buf_id VA buffer ID.
* @param timeout_ns Sync timeout in nanoseconds.
// vaSyncBuffer stub allows Sunshine built against libva <2.9.0 to link against ffmpeg on libva 2.9.0 or later
* @return VA status code.
*/
VAStatus
vaSyncBuffer(
VADisplay dpy,
@@ -25,7 +32,15 @@
}
#endif
#if !VA_CHECK_VERSION(1, 21, 0)
/**
* @brief Stub vaMapBuffer2 when building against libva before 2.21.0.
// vaMapBuffer2 stub allows Sunshine built against libva <2.21.0 to link against ffmpeg on libva 2.21.0 or later
*
* @param dpy VA display.
* @param buf_id VA buffer ID.
* @param pbuf Output mapped buffer pointer.
* @param flags Mapping flags.
* @return VA status code.
*/
VAStatus
vaMapBuffer2(
VADisplay dpy,
@@ -52,24 +67,39 @@
extern "C" struct AVBufferRef;
namespace va {
constexpr auto SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2 = 0x40000000; ///< Protocol or platform constant for surface attrib mem type drm prime 2.
constexpr auto EXPORT_SURFACE_WRITE_ONLY = 0x0002; ///< GameStream port offset for export surface write only.
constexpr auto SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2 = 0x40000000;
constexpr auto EXPORT_SURFACE_WRITE_ONLY = 0x0002;
constexpr auto EXPORT_SURFACE_SEPARATE_LAYERS = 0x0004; ///< GameStream port offset for export surface separate layers.
constexpr auto EXPORT_SURFACE_SEPARATE_LAYERS = 0x0004;
/**
* @brief Native VA display handle.
*/
using VADisplay = void *;
/**
* @brief Status code returned by VAAPI functions.
*/
using VAStatus = int;
/**
* @brief Generic numeric VAAPI object identifier.
*/
using VAGenericID = unsigned int;
/**
* @brief VAAPI surface identifier.
*/
using VASurfaceID = VAGenericID;
/**
* @brief DRM PRIME descriptor imported from a VAAPI surface.
*/
struct DRMPRIMESurfaceDescriptor {
// VA Pixel format fourcc of the whole surface (VA_FOURCC_*).
uint32_t fourcc;
uint32_t fourcc; ///< VA fourcc pixel format for the imported surface.
uint32_t width;
uint32_t height;
uint32_t width; ///< Surface width in pixels.
uint32_t height; ///< Surface height in pixels.
// Number of distinct DRM objects making up the surface.
uint32_t num_objects; ///< Num objects.
uint32_t num_objects;
struct {
// DRM PRIME file descriptor for this object.
@@ -80,10 +110,10 @@
uint32_t size;
// Format modifier applied to this object, not sure what that means
uint64_t drm_format_modifier;
} objects[4]; ///< DRM PRIME backing objects referenced by the descriptor..
} objects[4];
// Number of layers making up the surface.
uint32_t num_layers;
uint32_t num_layers; ///< Num layers.
struct {
// DRM format fourcc of this layer (DRM_FOURCC_*).
@@ -100,14 +130,35 @@
// Pitch of each plane.
uint32_t pitch[4];
} layers[4];
} layers[4]; ///< DRM PRIME layer descriptions for the frame..
};
/**
* @brief VA display handle released with `vaTerminate`.
*/
using display_t = util::safe_ptr_v2<void, VAStatus, vaTerminate>;
/**
* @brief Create an FFmpeg VA-API hardware device context from a Sunshine encode device.
*
* @param encode_device Encode device.
* @param hw_device_buf Output FFmpeg hardware device buffer.
* @return 0 when the buffer is initialized; negative FFmpeg error code on failure.
*/
int vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device, AVBufferRef **hw_device_buf);
/**
* @brief VAAPI encode device that imports captured frames into VA surfaces.
*/
class va_t: public platf::avcodec_encode_device_t {
public:
/**
* @brief Initialize VAAPI display, EGL, and conversion resources.
*
* @param in_width In width.
* @param in_height In height.
* @param render_device Render device.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(int in_width, int in_height, file_t &&render_device) {
file = std::move(render_device);
@@ -229,6 +280,12 @@
return VAProfileNone;
}
/**
* @brief Initialize codec options.
*
* @param ctx Native context object used by the operation or callback.
* @param options Request options or socket options to apply.
*/
void init_codec_options(AVCodecContext *ctx, AVDictionary **options) override {
auto va_profile = get_va_profile(ctx);
if (va_profile == VAProfileNone || !is_va_profile_supported(va_profile)) {
@@ -303,6 +360,13 @@
}
}
/**
* @brief Attach frame resources used by the next conversion or encode operation.
*
* @param frame Video or graphics frame being processed.
* @param hw_frames_ctx_buf Hardware frames context buffer.
* @return Status from updating frame.
*/
int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx_buf) override {
this->hwframe.reset(frame);
this->frame = frame;
@@ -380,29 +444,41 @@
return 0;
}
/**
* @brief Apply the configured colorspace metadata to the active frame.
*/
void apply_colorspace() override {
sws.apply_colorspace(colorspace, false);
}
va::display_t::pointer va_display; ///< VA display used to allocate and destroy surfaces.
file_t file; ///< DRM render-node file descriptor used by VAAPI and EGL.
va::display_t::pointer va_display;
file_t file;
gbm::gbm_t gbm;
egl::display_t display;
egl::ctx_t ctx;
gbm::gbm_t gbm; ///< GBM device used for buffer allocation.
egl::display_t display; ///< EGL display created from the DRM render node.
egl::ctx_t ctx; ///< EGL context used for VA-API frame conversion.
// This must be destroyed before display_t to ensure the GPU
// driver is still loaded when vaDestroySurfaces() is called.
frame_t hwframe;
frame_t hwframe; ///< FFmpeg hardware frame backed by a VAAPI surface.
egl::sws_t sws;
egl::sws_t sws; ///< EGL/OpenGL conversion pipeline for VA-API frames.
egl::nv12_t nv12; ///< EGL/OpenGL resources used for NV12 output frames.
egl::nv12_t nv12;
int width;
int height;
int width; ///< Frame or display width in pixels.
int height; ///< Frame or display height in pixels.
};
/**
* @brief VAAPI encode path that copies converted frames through system memory.
*/
class va_ram_t: public va_t {
public:
/**
* @brief Convert a captured VAAPI frame into system-memory encoder input.
*
* @param img Image or frame object to read from or populate.
* @return Conversion status.
*/
int convert(platf::img_t &img) override {
sws.load_ram(img);
@@ -412,7 +488,16 @@
}
};
/**
* @brief VAAPI encode path that keeps converted frames in GPU memory.
*/
class va_vram_t: public va_t {
public:
/**
* @brief Convert a captured VAAPI frame into GPU encoder input.
*
* @param img Image or frame object to read from or populate.
* @return Conversion status.
*/
int convert(platf::img_t &img) override {
auto &descriptor = (egl::img_descriptor_t &) img;
@@ -440,6 +525,16 @@
return 0;
}
/**
* @brief Initialize VAAPI GPU-frame conversion for the selected display.
*
* @param in_width In width.
* @param in_height In height.
* @param render_device Render device.
* @param offset_x Offset x.
* @param offset_y Offset y.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(int in_width, int in_height, file_t &&render_device, int offset_x, int offset_y) {
if (va_t::init(in_width, in_height, std::move(render_device))) {
return -1;
@@ -453,11 +548,11 @@
return 0;
}
std::uint64_t sequence; ///< Monotonic sequence used to recreate imported EGL resources.
egl::rgb_t rgb; ///< Imported RGB image used before VAAPI conversion.
std::uint64_t sequence;
egl::rgb_t rgb;
int offset_x;
int offset_x; ///< Horizontal offset in physical pixels.
int offset_y; ///< Vertical offset in physical pixels.
int offset_y;
};
/**
@@ -470,9 +565,9 @@
union {
void *xdisplay;
int fd;
} drm;
} drm; ///< Native display or DRM fd passed to FFmpeg's VA-API context.
int drm_fd; ///< DRM fd.
int drm_fd;
} VAAPIDevicePriv;
/**
@@ -508,6 +603,9 @@
av_freep(&priv);
}
/**
* @brief Initialize FFmpeg's VA-API hardware device buffer.
*/
int vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *base, AVBufferRef **hw_device_buf) {
auto va = (va::va_t *) base;
auto fd = dup(va->file.el);
@@ -583,6 +681,9 @@
return false;
}
/**
* @brief Validate that the configured VAAPI device can be used.
*/
bool validate(int fd) {
va::display_t display {vaGetDisplayDRM(fd)};
if (!display) {
@@ -619,6 +720,9 @@
return true;
}
/**
* @brief Create AVCodec encode device.
*/
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram) {
if (vram) {
auto egl = std::make_unique<va::va_vram_t>();
@@ -639,5 +743,8 @@
}
}
/**
* @brief Create AVCodec encode device.
*/
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device(int width, int height, int offset_x, int offset_y, bool vram) {
auto render_device = platf::resolve_render_device();
src/platform/linux/vaapi.h +32 −0
@@ -19,11 +19,43 @@
* offset_x --> Horizontal offset of the image in the texture
* offset_y --> Vertical offset of the image in the texture
* file_t card --> The file descriptor of the render device used for encoding
*
* @param width Frame or display width in pixels.
* @param height Frame or display height in pixels.
* @param vram Whether the image should use GPU memory instead of system memory.
* @return Constructed AVCodec encode device object.
*/
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device(int width, int height, bool vram);
/**
* @brief Create AVCodec encode device.
*
* @param width Frame or display width in pixels.
* @param height Frame or display height in pixels.
* @param offset_x Offset x.
* @param offset_y Offset y.
* @param vram Whether the image should use GPU memory instead of system memory.
* @return Constructed AVCodec encode device object.
*/
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device(int width, int height, int offset_x, int offset_y, bool vram);
/**
* @brief Create AVCodec encode device.
*
* @param width Frame or display width in pixels.
* @param height Frame or display height in pixels.
* @param card Video device path or render node used for VAAPI.
* @param offset_x Offset x.
* @param offset_y Offset y.
* @param vram Whether the image should use GPU memory instead of system memory.
* @return Constructed AVCodec encode device object.
*/
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram);
// Ensure the render device pointed to by fd is capable of encoding h264 with the hevc_mode configured
/**
* @brief Validate that the configured VAAPI device can be used.
*
* @param fd Native file descriptor to wrap or inspect.
* @return True when the VAAPI device is usable for capture or encode.
*/
bool validate(int fd);
} // namespace va
src/platform/linux/vulkan_encode.cpp +63 −13
@@ -106,23 +106,30 @@
return -1;
}
/**
* @brief Vulkan shader constants used by the conversion pass.
*/
struct PushConstants {
std::array<float, 4> color_vec_y;
std::array<float, 4> color_vec_u;
std::array<float, 4> color_vec_v;
std::array<float, 2> range_y;
std::array<float, 4> color_vec_y; ///< Color vec y.
std::array<float, 4> color_vec_u; ///< Color vec u.
std::array<float, 4> color_vec_v; ///< Color vec v.
std::array<float, 2> range_uv;
std::array<int32_t, 2> src_offset;
std::array<int32_t, 2> src_size;
std::array<int32_t, 2> dst_offset;
std::array<int32_t, 2> dst_size;
std::array<int32_t, 2> dst_full_size;
std::array<int32_t, 2> cursor_pos;
std::array<int32_t, 2> cursor_size;
int32_t y_invert;
std::array<float, 2> range_y; ///< Range y.
std::array<float, 2> range_uv; ///< Range uv.
std::array<int32_t, 2> src_offset; ///< Src offset.
std::array<int32_t, 2> src_size; ///< Src size.
std::array<int32_t, 2> dst_offset; ///< Dst offset.
std::array<int32_t, 2> dst_size; ///< Dst size.
std::array<int32_t, 2> dst_full_size; ///< Dst full size.
std::array<int32_t, 2> cursor_pos; ///< Cursor pos.
std::array<int32_t, 2> cursor_size; ///< Cursor size.
int32_t y_invert; ///< Y invert.
};
// Helper to check VkResult
/**
* @def VK_CHECK(expr)
* @brief Macro for VK CHECK.
*/
#define VK_CHECK(expr) \
do { \
VkResult _r = (expr); \
@@ -131,6 +138,10 @@
return -1; \
} \
} while (0)
/**
* @def VK_CHECK_BOOL(expr)
* @brief Macro for VK CHECK BOOL.
*/
#define VK_CHECK_BOOL(expr) \
do { \
VkResult _r = (expr); \
@@ -140,12 +151,24 @@
} \
} while (0)
/**
* @brief Vulkan encode device that keeps converted frames in GPU memory.
*/
class vk_vram_t: public platf::avcodec_encode_device_t {
public:
~vk_vram_t() override {
cleanup_pipeline();
}
/**
* @brief Initialize Vulkan encode device and conversion resources.
*
* @param in_width In width.
* @param in_height In height.
* @param in_offset_x In offset x.
* @param in_offset_y In offset y.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(int in_width, int in_height, int in_offset_x = 0, int in_offset_y = 0) {
width = in_width;
height = in_height;
@@ -155,6 +178,12 @@
return 0;
}
/**
* @brief Initialize codec options.
*
* @param ctx Native context object used by the operation or callback.
* @param options Request options or socket options to apply.
*/
void init_codec_options(AVCodecContext *ctx, AVDictionary **options) override {
// When VBR mode is selected (rc_mode=4), don't pin rc_min_rate to the target bitrate.
// Having rc_min_rate == rc_max_rate == bit_rate in VBR mode prevents the encoder from
@@ -165,6 +194,13 @@
}
}
/**
* @brief Attach frame resources used by the next conversion or encode operation.
*
* @param new_frame Frame to attach.
* @param hw_frames_ctx_buf Hardware frames context buffer.
* @return Status from updating frame.
*/
int set_frame(AVFrame *new_frame, AVBufferRef *hw_frames_ctx_buf) override {
this->hwframe.reset(new_frame);
this->frame = new_frame;
@@ -212,6 +248,9 @@
return 0;
}
/**
* @brief Apply the configured colorspace metadata to the active frame.
*/
void apply_colorspace() override {
auto *colors = video::color_vectors_from_colorspace(colorspace, true);
if (colors) {
@@ -223,6 +262,11 @@
}
}
/**
* @brief Configure FFmpeg Vulkan hardware frames for video encode input.
*
* @param frames FFmpeg hardware frames context to initialize.
*/
void init_hwframes(AVHWFramesContext *frames) override {
frames->initial_pool_size = 4;
auto *vk_frames = (AVVulkanFramesContext *) frames->hwctx;
@@ -233,5 +277,11 @@
VK_IMAGE_USAGE_VIDEO_ENCODE_SRC_BIT_KHR);
}
/**
* @brief Convert a captured frame into a Vulkan hardware frame.
*
* @param img Image or frame object to read from or populate.
* @return Conversion status.
*/
int convert(platf::img_t &img) override {
auto &descriptor = (egl::img_descriptor_t &) img;
src/platform/linux/vulkan_encode.h +12 −0
@@ -20,15 +20,27 @@
/**
* @brief Create a Vulkan encode device for RAM capture.
*
* @param width Frame or display width in pixels.
* @param height Frame or display height in pixels.
* @return Constructed AVCodec encode device ram object.
*/
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device_ram(int width, int height);
/**
* @brief Create a Vulkan encode device for VRAM capture.
*
* @param width Frame or display width in pixels.
* @param height Frame or display height in pixels.
* @param offset_x Offset x.
* @param offset_y Offset y.
* @return Constructed AVCodec encode device VRAM object.
*/
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device_vram(int width, int height, int offset_x, int offset_y);
/**
* @brief Check if FFmpeg Vulkan Video encoding is available.
*
* @return True when FFmpeg Vulkan Video encoding is available.
*/
bool validate();
src/platform/linux/wayland.cpp +11 −1
@@ -23,6 +23,6 @@
#include "src/utility.h"
#include "wayland.h"
extern const wl_interface wl_output_interface;
extern const wl_interface wl_output_interface; ///< Wayland output interface.
using namespace std::literals;
@@ -40,6 +40,10 @@
return ((*reinterpret_cast<T *>(data)).*m)(params...);
}
/**
* @def CLASS_CALL(c, m)
* @brief Macro for CLASS CALL.
*/
#define CLASS_CALL(c, m) classCall<c, decltype(&c::m), &c::m>
// Define buffer params listener
@@ -546,5 +550,8 @@
std::fill_n(sd.fds, 4, -1);
};
/**
* @brief Refresh the monitor list reported by the display server.
*/
std::vector<std::unique_ptr<monitor_t>> monitors(const char *display_name) {
display_t display;
@@ -578,5 +585,8 @@
return display.init() == 0;
}
/**
* @brief Initialize Wayland registry interfaces required for capture.
*/
int init() {
static bool validated = validate();
src/platform/linux/wayland.h +242 −23
@@ -26,19 +26,34 @@
#ifdef SUNSHINE_BUILD_WAYLAND
namespace wl {
/**
* @brief Owning pointer for a Wayland display connection.
*/
using display_internal_t = util::safe_ptr<wl_display, wl_display_disconnect>;
/**
* @brief Captured Wayland frame metadata and DMA-BUF surface state.
*/
class frame_t {
public:
frame_t();
/**
* @brief Release file descriptors and native buffers associated with this frame.
*/
void destroy();
egl::surface_descriptor_t sd;
std::optional<std::chrono::steady_clock::time_point> frame_timestamp;
egl::surface_descriptor_t sd; ///< DMA-BUF surface descriptor received from the compositor.
std::optional<std::chrono::steady_clock::time_point> frame_timestamp; ///< Capture timestamp associated with the frame.
};
/**
* @brief Listener state for Wayland screencopy frames backed by DMA-BUFs.
*/
class dmabuf_t {
public:
/**
* @brief Capture state for the active screencopy request.
*/
enum status_e {
WAITING, ///< Waiting for a frame
READY, ///< Frame is ready
@@ -53,25 +68,102 @@
dmabuf_t &operator=(const dmabuf_t &) = delete;
dmabuf_t &operator=(dmabuf_t &&) = delete;
/**
* @brief Start a screencopy request and attach the DMA-BUF listener callbacks.
*
* @param screencopy_manager Compositor screencopy manager used to request frames.
* @param dmabuf_interface Compositor DMA-BUF interface used to allocate buffers.
* @param supported_modifiers DMA-BUF format modifiers supported by the compositor.
* @param output Wayland output to capture.
* @param blend_cursor Whether the compositor should include the cursor in the frame.
*/
void listen(zwlr_screencopy_manager_v1 *screencopy_manager, zwp_linux_dmabuf_v1 *dmabuf_interface, const std::map<std::uint32_t, std::vector<std::uint64_t>> *supported_modifiers, wl_output *output, bool blend_cursor = false);
/**
* @brief Store the Wayland buffer created for a DMA-BUF parameter request.
*
* @param data Pointer to the `dmabuf_t` instance associated with the callback.
* @param params DMA-BUF parameter object that completed buffer creation.
* @param wl_buffer Wayland buffer created from the DMA-BUF planes.
*/
static void buffer_params_created(void *data, struct zwp_linux_buffer_params_v1 *params, struct wl_buffer *wl_buffer);
/**
* @brief Mark DMA-BUF buffer creation as failed.
*
* @param data Pointer to the `dmabuf_t` instance associated with the callback.
* @param params DMA-BUF parameter object that failed buffer creation.
*/
static void buffer_params_failed(void *data, struct zwp_linux_buffer_params_v1 *params);
/**
* @brief Record DMA-BUF buffer parameters from the compositor.
*
* @param frame Screencopy frame that advertised shared-memory fallback data.
* @param format DRM pixel format for the shared-memory buffer.
* @param width Frame width in pixels.
* @param height Frame height in pixels.
* @param stride Number of bytes per row in the buffer.
*/
void buffer(zwlr_screencopy_frame_v1 *frame, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t stride);
/**
* @brief Record Linux DMA-BUF parameters advertised for the frame.
*
* @param frame Screencopy frame that advertised DMA-BUF data.
* @param format DRM pixel format for the DMA-BUF.
* @param width Frame width in pixels.
* @param height Frame height in pixels.
*/
void linux_dmabuf(zwlr_screencopy_frame_v1 *frame, std::uint32_t format, std::uint32_t width, std::uint32_t height);
/**
* @brief Allocate or import the buffer once the compositor finished describing it.
*
* @param frame Screencopy frame whose buffer description is complete.
*/
void buffer_done(zwlr_screencopy_frame_v1 *frame);
/**
* @brief Record frame flags reported by the compositor.
*
* @param frame Screencopy frame that reported flags.
* @param flags Wayland screencopy flags such as vertical inversion.
*/
void flags(zwlr_screencopy_frame_v1 *frame, std::uint32_t flags);
/**
* @brief Record damaged frame bounds reported by the compositor.
*
* @param frame Screencopy frame that reported damage.
* @param x Left edge of the damaged rectangle.
* @param y Top edge of the damaged rectangle.
* @param width Damaged rectangle width in pixels.
* @param height Damaged rectangle height in pixels.
*/
void damage(zwlr_screencopy_frame_v1 *frame, std::uint32_t x, std::uint32_t y, std::uint32_t width, std::uint32_t height);
/**
* @brief Mark the current screencopy frame as ready and store its timestamp.
*
* @param frame Screencopy frame that completed.
* @param tv_sec_hi High 32 bits of the compositor-provided seconds value.
* @param tv_sec_lo Low 32 bits of the compositor-provided seconds value.
* @param tv_nsec Nanosecond component of the compositor-provided timestamp.
*/
void ready(zwlr_screencopy_frame_v1 *frame, std::uint32_t tv_sec_hi, std::uint32_t tv_sec_lo, std::uint32_t tv_nsec);
/**
* @brief Mark the frame capture as failed.
*
* @param frame Screencopy frame that failed.
*/
void failed(zwlr_screencopy_frame_v1 *frame);
/**
* @brief Select the inactive frame slot for the next screencopy request.
*
* @return Inactive frame buffer that can receive the next capture.
*/
frame_t *get_next_frame() {
return current_frame == &frames[0] ? &frames[1] : &frames[0];
}
status_e status;
std::array<frame_t, 2> frames;
frame_t *current_frame;
zwlr_screencopy_frame_v1_listener listener;
status_e status; ///< Current state of the active screencopy request.
std::array<frame_t, 2> frames; ///< Double-buffered frame descriptors.
frame_t *current_frame; ///< Frame descriptor currently being filled by the compositor.
zwlr_screencopy_frame_v1_listener listener; ///< Callback table registered on screencopy frames.
private:
bool init_gbm();
@@ -102,8 +194,16 @@
bool y_invert {false};
};
/**
* @brief Wayland output metadata used to match a configured display name.
*/
class monitor_t {
public:
/**
* @brief Track a Wayland output and its advertised modes.
*
* @param output Wayland output object being described.
*/
explicit monitor_t(wl_output *output);
monitor_t(monitor_t &&) = delete;
@@ -111,30 +211,96 @@
monitor_t &operator=(const monitor_t &) = delete;
monitor_t &operator=(monitor_t &&) = delete;
/**
* @brief Attach xdg-output and wl-output listeners for this monitor.
*
* @param output_manager xdg-output manager used to query logical monitor metadata.
*/
void listen(zxdg_output_manager_v1 *output_manager);
/**
* @brief Store the xdg-output logical monitor name.
*
* @param name Human-readable name to assign.
*/
void xdg_name(zxdg_output_v1 *, const char *name);
/**
* @brief Store the xdg-output human-readable monitor description.
*
* @param description Human-readable description used in log output.
*/
void xdg_description(zxdg_output_v1 *, const char *description);
/**
* @brief Store the logical monitor position reported by xdg-output.
*
* @param x Horizontal coordinate reported by Wayland.
* @param y Vertical coordinate reported by Wayland.
*/
void xdg_position(zxdg_output_v1 *, std::int32_t x, std::int32_t y);
/**
* @brief Store the logical monitor size reported by xdg-output.
*
* @param width Frame or display width in pixels.
* @param height Frame or display height in pixels.
*/
void xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height);
/**
* @brief Acknowledge completion of an xdg-output metadata update.
*/
void xdg_done(zxdg_output_v1 *) {}
/**
* @brief Receive physical wl-output geometry metadata.
*
* @param wl_output Wayland output.
* @param x Horizontal coordinate reported by Wayland.
* @param y Vertical coordinate reported by Wayland.
* @param physical_width Physical width in millimeters.
* @param physical_height Physical height in millimeters.
* @param subpixel Wayland subpixel layout enum.
* @param make Monitor manufacturer string reported by Wayland.
* @param model Monitor model string reported by Wayland.
* @param transform Wayland output transform value.
*/
void wl_geometry(wl_output *wl_output, std::int32_t x, std::int32_t y, std::int32_t physical_width, std::int32_t physical_height, std::int32_t subpixel, const char *make, const char *model, std::int32_t transform) {}
/**
* @brief Store the active pixel mode reported by wl-output.
*
* @param wl_output Wayland output.
* @param flags Bit flags that modify the requested operation.
* @param width Frame or display width in pixels.
* @param height Frame or display height in pixels.
* @param refresh Output refresh rate in mHz.
*/
void wl_mode(wl_output *wl_output, std::uint32_t flags, std::int32_t width, std::int32_t height, std::int32_t refresh);
/**
* @brief Acknowledge completion of a wl-output metadata update.
*
* @param wl_output Wayland output.
*/
void wl_done(wl_output *wl_output) {}
/**
* @brief Receive the wl-output scale factor.
*
* @param wl_output Wayland output.
* @param factor Wayland output scale factor.
*/
void wl_scale(wl_output *wl_output, std::int32_t factor) {}
wl_output *output;
std::string name;
wl_output *output; ///< Wayland output associated with this monitor.
std::string name; ///< xdg-output name used for display selection.
std::string description; ///< xdg-output description used for logs and UI.
platf::touch_port_t viewport; ///< Logical monitor bounds used to scale absolute input.
wl_output_listener wl_listener; ///< Callback table for wl-output events.
zxdg_output_v1_listener xdg_listener; ///< Callback table for xdg-output events.
std::string description;
platf::touch_port_t viewport;
wl_output_listener wl_listener;
zxdg_output_v1_listener xdg_listener;
};
/**
* @brief Wayland registry state for screencopy, DMA-BUF, and output globals.
*/
class interface_t {
struct bind_t {
std::uint32_t id;
@@ -142,6 +308,9 @@
};
public:
/**
* @brief Wayland globals required by the WLR capture backend.
*/
enum interface_e {
XDG_OUTPUT, ///< xdg-output
WLR_EXPORT_DMABUF, ///< screencopy manager
@@ -156,20 +325,45 @@
interface_t &operator=(const interface_t &) = delete;
interface_t &operator=(interface_t &&) = delete;
/**
* @brief Discover compositor globals from the Wayland registry.
*
* @param registry Wayland registry announcing globals.
*/
void listen(wl_registry *registry);
/**
* @brief Test whether a required Wayland global was advertised.
*
* @param bit Interface bit to test.
* @return True when the requested Wayland interface bit is set.
*/
bool operator[](interface_e bit) const {
return interface[bit];
}
/**
* @brief Record a DMA-BUF format advertised by the compositor.
*
* @param zwp_linux_dmabuf DMA-BUF interface that emitted the format event.
* @param format DRM format supported by the compositor.
*/
void dmabuf_format(zwp_linux_dmabuf_v1 *zwp_linux_dmabuf, uint32_t format);
/**
* @brief Record a DMA-BUF format modifier advertised by the compositor.
*
* @param zwp_linux_dmabuf DMA-BUF interface that emitted the modifier event.
* @param format DRM format associated with the modifier.
* @param modifier_hi High 32 bits of the DRM format modifier.
* @param modifier_lo Low 32 bits of the DRM format modifier.
*/
void dmabuf_modifier(zwp_linux_dmabuf_v1 *zwp_linux_dmabuf, uint32_t format, uint32_t modifier_hi, uint32_t modifier_lo);
std::vector<std::unique_ptr<monitor_t>> monitors;
std::map<std::uint32_t, std::vector<std::uint64_t>> supported_modifiers;
zwlr_screencopy_manager_v1 *screencopy_manager {nullptr};
zwp_linux_dmabuf_v1 *dmabuf_interface {nullptr};
zxdg_output_manager_v1 *output_manager {nullptr};
std::vector<std::unique_ptr<monitor_t>> monitors; ///< Outputs discovered from the Wayland registry.
std::map<std::uint32_t, std::vector<std::uint64_t>> supported_modifiers; ///< DRM format modifiers grouped by format.
zwlr_screencopy_manager_v1 *screencopy_manager {nullptr}; ///< WLR screencopy global used to request frames.
zwp_linux_dmabuf_v1 *dmabuf_interface {nullptr}; ///< Linux DMA-BUF global used to allocate frame buffers.
zxdg_output_manager_v1 *output_manager {nullptr}; ///< xdg-output global used to query monitor names and sizes.
private:
void add_interface(wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version);
@@ -180,26 +374,40 @@
zwp_linux_dmabuf_v1_listener dmabuf_listener;
};
/**
* @brief Wayland display connection used to dispatch capture events.
*/
class display_t {
public:
/**
* @brief Initialize display.
* If display_name == nullptr -> display_name = std::getenv("WAYLAND_DISPLAY")
* @brief Connect to the requested Wayland display.
*
* @param display_name Wayland display name, or nullptr to use WAYLAND_DISPLAY.
* @return 0 when the connection is opened and initialized; -1 on failure.
* @param display_name The name of the display.
* @return 0 on success, -1 on failure.
*/
int init(const char *display_name = nullptr);
// Roundtrip with Wayland connection
/**
* @brief Flush pending Wayland requests and wait for replies.
*/
void roundtrip();
// Wait up to the timeout to read and dispatch new events
bool dispatch(std::chrono::milliseconds timeout);
// Get the registry associated with the display
// No need to manually free the registry
/**
* @brief Return the Wayland registry for global discovery.
*
* @return Wayland registry used to discover compositor globals.
*/
wl_registry *registry();
/**
* @brief Return the native Wayland display pointer.
*
* @return Native display connection owned by this wrapper.
*/
inline display_internal_t::pointer get() {
return display_internal.get();
}
@@ -208,7 +416,18 @@
display_internal_t display_internal;
};
/**
* @brief Refresh the monitor list reported by the display server.
*
* @param display_name Display name.
* @return Monitors reported by the Wayland compositor, optionally filtered by name.
*/
std::vector<std::unique_ptr<monitor_t>> monitors(const char *display_name = nullptr);
/**
* @brief Initialize Wayland registry interfaces required for capture.
*
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init();
} // namespace wl
#else
src/platform/linux/wlgrab.cpp +107 −9
@@ -19,15 +19,32 @@
static int env_width;
static int env_height;
/**
* @brief Captured frame buffer shared between capture and encode stages.
*/
struct img_t: public platf::img_t {
/**
* @brief Destroy the Wayland capture image.
*/
~img_t() override {
delete[] data;
data = nullptr;
}
};
/**
* @brief Wayland screencopy capture backend shared by RAM and VRAM paths.
*/
class wlr_t: public platf::display_t {
public:
/**
* @brief Initialize Wayland screencopy capture for the selected output.
*
* @param hwdevice_type Hardware device type requested for capture or encode.
* @param display_name Display name.
* @param config Configuration values to apply.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) {
// calculate frame interval we should capture at
if (config.framerateX100 > 0) {
@@ -124,9 +141,24 @@
return 0;
}
/**
* @brief Populate a fallback image when real capture data is unavailable.
*
* @param img Image or frame object to read from or populate.
* @return Capture status reported to the streaming pipeline.
*/
int dummy_img(platf::img_t *img) override {
return 0;
}
/**
* @brief Capture a display frame into the provided image object.
*
* @param pull_free_image_cb Callback that provides an available image buffer.
* @param img_out Captured wlroots image returned to the streaming pipeline.
* @param timeout Maximum time to wait for the operation.
* @param cursor Cursor image or visibility state to composite.
* @return Capture status reported to the streaming pipeline.
*/
inline platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor) {
auto to = std::chrono::steady_clock::now() + timeout;
@@ -153,17 +185,20 @@
return platf::capture_e::ok;
}
platf::mem_type_e mem_type;
platf::mem_type_e mem_type; ///< Mem type.
std::chrono::nanoseconds delay;
std::chrono::nanoseconds delay; ///< Delay before the timer task becomes eligible to run.
wl::display_t display; ///< Wayland display connection used for capture.
wl::display_t display;
interface_t interface;
interface_t interface; ///< Wayland registry interfaces required by screencopy.
dmabuf_t dmabuf; ///< DMA-BUF feedback and format state advertised by the compositor.
dmabuf_t dmabuf;
wl_output *output; ///< Wayland output selected for capture.
wl_output *output;
};
/**
* @brief Wayland screencopy backend that copies frames into system memory.
*/
class wlr_ram_t: public wlr_t {
public:
platf::capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override {
@@ -211,6 +246,15 @@
return platf::capture_e::ok;
}
/**
* @brief Capture a display frame into the provided image object.
*
* @param pull_free_image_cb Callback that provides an available image buffer.
* @param img_out Captured wlroots image returned to the streaming pipeline.
* @param timeout Maximum time to wait for the operation.
* @param cursor Cursor image or visibility state to composite.
* @return Capture status reported to the streaming pipeline.
*/
platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor) {
auto status = wlr_t::snapshot(pull_free_image_cb, img_out, timeout, cursor);
if (status != platf::capture_e::ok) {
@@ -246,6 +290,14 @@
return platf::capture_e::ok;
}
/**
* @brief Initialize Wayland capture that copies frames into system memory.
*
* @param hwdevice_type Hardware device type requested for capture or encode.
* @param display_name Display name.
* @param config Configuration values to apply.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) {
if (wlr_t::init(hwdevice_type, display_name, config)) {
return -1;
@@ -266,6 +318,12 @@
return 0;
}
/**
* @brief Create AVCodec encode device.
*
* @param pix_fmt Sunshine pixel format to convert or allocate for.
* @return Constructed AVCodec encode device object.
*/
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override {
#ifdef SUNSHINE_BUILD_VAAPI
if (mem_type == platf::mem_type_e::vaapi) {
@@ -282,6 +340,11 @@
return std::make_unique<platf::avcodec_encode_device_t>();
}
/**
* @brief Allocate an image buffer compatible with this display backend.
*
* @return Allocated img object, or null when unavailable.
*/
std::shared_ptr<platf::img_t> alloc_img() override {
auto img = std::make_shared<img_t>();
img->width = width;
@@ -293,10 +356,13 @@
return img;
}
egl::display_t egl_display; ///< EGL display.
egl::ctx_t ctx; ///< EGL context used for wlroots capture conversion.
egl::display_t egl_display;
egl::ctx_t ctx;
};
/**
* @brief Wayland screencopy backend that exports frames as GPU resources.
*/
class wlr_vram_t: public wlr_t {
public:
platf::capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override {
@@ -344,6 +410,15 @@
return platf::capture_e::ok;
}
/**
* @brief Capture a display frame into the provided image object.
*
* @param pull_free_image_cb Callback that provides an available image buffer.
* @param img_out Captured wlroots image returned to the streaming pipeline.
* @param timeout Maximum time to wait for the operation.
* @param cursor Cursor image or visibility state to composite.
* @return Capture status reported to the streaming pipeline.
*/
platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor) {
auto status = wlr_t::snapshot(pull_free_image_cb, img_out, timeout, cursor);
if (status != platf::capture_e::ok) {
@@ -370,5 +445,10 @@
return platf::capture_e::ok;
}
/**
* @brief Allocate an image buffer compatible with this display backend.
*
* @return Allocated img object, or null when unavailable.
*/
std::shared_ptr<platf::img_t> alloc_img() override {
auto img = std::make_shared<egl::img_descriptor_t>();
@@ -385,6 +465,12 @@
return img;
}
/**
* @brief Create AVCodec encode device.
*
* @param pix_fmt Sunshine pixel format to convert or allocate for.
* @return Constructed AVCodec encode device object.
*/
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override {
#ifdef SUNSHINE_BUILD_VAAPI
if (mem_type == platf::mem_type_e::vaapi) {
@@ -401,17 +487,26 @@
return std::make_unique<platf::avcodec_encode_device_t>();
}
/**
* @brief Populate a fallback image when real capture data is unavailable.
*
* @param img Image or frame object to read from or populate.
* @return Capture status reported to the streaming pipeline.
*/
int dummy_img(platf::img_t *img) override {
// Empty images are recognized as dummies by the zero sequence number
return 0;
}
std::uint64_t sequence {};
std::uint64_t sequence {}; ///< Monotonic capture sequence assigned to Wayland frames.
};
} // namespace wl
namespace platf {
/**
* @brief Create a Wayland capture backend for the requested memory type.
*/
std::shared_ptr<display_t> wl_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) {
if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) {
BOOST_LOG(error) << "[wlgrab] Could not initialize display with the given hw device type."sv;
@@ -435,5 +530,8 @@
return wlr;
}
/**
* @brief Enumerate capture display names reported by the Wayland compositor.
*/
std::vector<std::string> wl_display_names() {
std::vector<std::string> display_names;
src/platform/linux/x11grab.cpp +228 −15
@@ -33,12 +33,32 @@
using namespace std::literals;
namespace platf {
/**
* @brief Load XCB entry points used by the X11 capture backend.
*
* @return 0 when required XCB symbols are loaded; nonzero otherwise.
*/
int load_xcb();
/**
* @brief Load X11 entry points used by the X11 capture backend.
*
* @return 0 when required X11 symbols are loaded; nonzero otherwise.
*/
int load_x11();
namespace x11 {
/**
* @def _FN(x, ret, args)
* @brief Macro for FN.
*/
#define _FN(x, ret, args) \
/** \
* @brief Function pointer type for the dynamically loaded X11 entry point. \
*/ \
typedef ret(*x##_fn) args; \
/** \
* @brief Loaded X11 entry point pointer. \
*/ \
static x##_fn x
_FN(GetImage, XImage *, (Display * display, Drawable d, int x, int y, unsigned int width, unsigned int height, unsigned long plane_mask, int format));
@@ -174,6 +194,11 @@
_FN(setup_roots_iterator, xcb_screen_iterator_t, (const xcb_setup_t *R));
_FN(generate_id, std::uint32_t, (xcb_connection_t * c));
/**
* @brief Initialize shared-memory support for X11 capture.
*
* @return 0 when XCB shared-memory functions are loaded; nonzero otherwise.
*/
int init_shm() {
static void *handle {nullptr};
static bool funcs_loaded = false;
@@ -204,6 +229,11 @@
return 0;
}
/**
* @brief Initialize XFixes cursor tracking for an X11 display.
*
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init() {
static void *handle {nullptr};
static bool funcs_loaded = false;
@@ -240,29 +270,73 @@
#undef _FN
} // namespace xcb
/**
* @brief Release image resources.
*
* @param p Pointer passed to the deleter or conversion helper.
*/
void freeImage(XImage *);
/**
* @brief Release x resources.
*
* @param p Pointer passed to the deleter or conversion helper.
*/
void freeX(XFixesCursorImage *);
/**
* @brief XCB connection pointer released with `xcb_disconnect`.
*/
using xcb_connect_t = util::dyn_safe_ptr<xcb_connection_t, &xcb::disconnect>;
/**
* @brief XCB image pointer released with `xcb_image_destroy`.
*/
using xcb_img_t = util::c_ptr<xcb_shm_get_image_reply_t>;
/**
* @brief XImage pointer released with `XDestroyImage`.
*/
using ximg_t = util::safe_ptr<XImage, freeImage>;
/**
* @brief XFixes cursor image pointer released with `XFree`.
*/
using xcursor_t = util::safe_ptr<XFixesCursorImage, freeX>;
/**
* @brief XRandR CRTC info pointer released with `XRRFreeCrtcInfo`.
*/
using crtc_info_t = util::dyn_safe_ptr<_XRRCrtcInfo, &x11::rr::FreeCrtcInfo>;
/**
* @brief XRandR output info pointer released with `XRRFreeOutputInfo`.
*/
using output_info_t = util::dyn_safe_ptr<_XRROutputInfo, &x11::rr::FreeOutputInfo>;
/**
* @brief XRandR screen resources pointer released with `XRRFreeScreenResources`.
*/
using screen_res_t = util::dyn_safe_ptr<_XRRScreenResources, &x11::rr::FreeScreenResources>;
/**
* @brief RAII wrapper that removes a SysV shared-memory segment.
*/
class shm_id_t {
public:
shm_id_t():
id {-1} {
}
/**
* @brief Take ownership of a SysV shared-memory segment ID.
*
* @param id SysV shared-memory segment ID.
*/
shm_id_t(int id):
id {id} {
}
/**
* @brief Move ownership of a SysV shared-memory segment ID.
*
* @param other Shared-memory ID wrapper whose segment ownership is moved.
*/
shm_id_t(shm_id_t &&other) noexcept:
id(other.id) {
other.id = -1;
@@ -275,19 +349,32 @@
}
}
int id;
int id; ///< SysV shared-memory segment identifier returned by shmget.
};
/**
* @brief RAII wrapper that detaches mapped SysV shared memory.
*/
class shm_data_t {
public:
shm_data_t():
data {(void *) -1} {
}
/**
* @brief Take ownership of an attached shared-memory mapping.
*
* @param data Pointer returned by shmat.
*/
shm_data_t(void *data):
data {data} {
}
/**
* @brief Move ownership of an attached shared-memory mapping.
*
* @param other Shared-memory mapping wrapper whose attachment is moved.
*/
shm_data_t(shm_data_t &&other) noexcept:
data(other.data) {
other.data = (void *) -1;
@@ -299,13 +386,19 @@
}
}
void *data;
void *data; ///< Address returned by shmat for the shared-memory segment.
};
/**
* @brief X11 image wrapper used by the software capture path.
*/
struct x11_img_t: public img_t {
ximg_t img;
ximg_t img; ///< XImage backing the current software-captured frame.
};
/**
* @brief X11 shared-memory image and segment ownership.
*/
struct shm_img_t: public img_t {
~shm_img_t() override {
delete[] data;
@@ -362,14 +455,17 @@
}
}
/**
* @brief X11 display, window, and attribute handles for capture.
*/
struct x11_attr_t: public display_t {
std::chrono::nanoseconds delay;
std::chrono::nanoseconds delay; ///< Delay before the timer task becomes eligible to run.
x11::xdisplay_t xdisplay;
Window xwindow;
XWindowAttributes xattr;
x11::xdisplay_t xdisplay; ///< X11 display connection used for capture.
Window xwindow; ///< Root window being captured.
XWindowAttributes xattr; ///< Cached X11 window attributes used to detect size changes.
mem_type_e mem_type;
mem_type_e mem_type; ///< Mem type.
/**
* Last X (NOT the streamed monitor!) size.
@@ -377,6 +473,11 @@
*/
// int env_width, env_height;
/**
* @brief Open the X11 display and initialize capture attributes.
*
* @param mem_type Requested memory path for the capture backend.
*/
x11_attr_t(mem_type_e mem_type):
xdisplay {x11::OpenDisplay(nullptr)},
xwindow {},
@@ -385,6 +486,13 @@
x11::InitThreads();
}
/**
* @brief Open the X11 display and cache capture window attributes.
*
* @param display_name Display name.
* @param config Configuration values to apply.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(const std::string &display_name, const ::video::config_t &config) {
if (!xdisplay) {
BOOST_LOG(error) << "Could not open X11 display"sv;
@@ -501,5 +609,14 @@
return capture_e::ok;
}
/**
* @brief Capture a display frame into the provided image object.
*
* @param pull_free_image_cb Callback that provides an available image buffer.
* @param img_out XImage-backed captured frame returned to the streaming pipeline.
* @param timeout Maximum time to wait for the operation.
* @param cursor Cursor image or visibility state to composite.
* @return Capture status reported to the streaming pipeline.
*/
capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor) {
refresh();
@@ -532,10 +649,21 @@
return capture_e::ok;
}
/**
* @brief Allocate an image buffer compatible with this display backend.
*
* @return Allocated img object, or null when unavailable.
*/
std::shared_ptr<img_t> alloc_img() override {
return std::make_shared<x11_img_t>();
}
/**
* @brief Create AVCodec encode device.
*
* @param pix_fmt Sunshine pixel format to convert or allocate for.
* @return Constructed AVCodec encode device object.
*/
std::unique_ptr<avcodec_encode_device_t> make_avcodec_encode_device(pix_fmt_e pix_fmt) override {
#ifdef SUNSHINE_BUILD_VAAPI
if (mem_type == mem_type_e::vaapi) {
@@ -552,6 +680,12 @@
return std::make_unique<avcodec_encode_device_t>();
}
/**
* @brief Populate a fallback image when real capture data is unavailable.
*
* @param img Image or frame object to read from or populate.
* @return Capture status reported to the streaming pipeline.
*/
int dummy_img(img_t *img) override {
// TODO: stop cheating and give black image
if (!img) {
@@ -567,24 +701,35 @@
}
};
/**
* @brief X11 shared-memory image dimensions and identifiers.
*/
struct shm_attr_t: public x11_attr_t {
x11::xdisplay_t shm_xdisplay; // Prevent race condition with x11_attr_t::xdisplay
xcb_connect_t xcb;
xcb_screen_t *display;
std::uint32_t seg;
x11::xdisplay_t shm_xdisplay; ///< X11 display held separately to prevent races with x11_attr_t::xdisplay.
xcb_connect_t xcb; ///< XCB connection used by the shared-memory capture path.
xcb_screen_t *display; ///< XCB screen containing the captured root window.
std::uint32_t seg; ///< XCB shared-memory segment ID attached to the image.
shm_id_t shm_id;
shm_id_t shm_id; ///< Shm ID.
shm_data_t data;
shm_data_t data; ///< Attached SysV shared-memory data used by XShm.
task_pool_util::TaskPool::task_id_t refresh_task_id;
task_pool_util::TaskPool::task_id_t refresh_task_id; ///< Refresh task ID.
/**
* @brief Refresh X11 shared-memory capture after a scheduled delay.
*/
void delayed_refresh() {
refresh();
refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id;
}
/**
* @brief Open an X11 shared-memory capture backend.
*
* @param mem_type Requested memory path for the capture backend.
*/
shm_attr_t(mem_type_e mem_type):
x11_attr_t(mem_type),
shm_xdisplay {x11::OpenDisplay(nullptr)} {
@@ -640,6 +785,15 @@
return capture_e::ok;
}
/**
* @brief Capture a display frame into the provided image object.
*
* @param pull_free_image_cb Callback that provides an available image buffer.
* @param img_out Shared-memory captured frame returned to the streaming pipeline.
* @param timeout Maximum time to wait for the operation.
* @param cursor Cursor image or visibility state to composite.
* @return Capture status reported to the streaming pipeline.
*/
capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor) {
// The whole X server changed, so we must reinit everything
if (xattr.width != env_width || xattr.height != env_height) {
@@ -670,6 +824,11 @@
}
}
/**
* @brief Allocate an image buffer compatible with this display backend.
*
* @return Allocated img object, or null when unavailable.
*/
std::shared_ptr<img_t> alloc_img() override {
auto img = std::make_shared<shm_img_t>();
img->width = width;
@@ -681,10 +840,23 @@
return img;
}
/**
* @brief Populate a fallback image when real capture data is unavailable.
*
* @param img Image or frame object to read from or populate.
* @return Capture status reported to the streaming pipeline.
*/
int dummy_img(platf::img_t *img) override {
return 0;
}
/**
* @brief Initialize X11 shared-memory capture for the selected display.
*
* @param display_name Display name.
* @param config Configuration values to apply.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(const std::string &display_name, const ::video::config_t &config) {
if (x11_attr_t::init(display_name, config)) {
return 1;
@@ -724,11 +896,24 @@
return 0;
}
/**
* @brief Calculate the XCB shared-memory frame size.
*
* @return Frame size in bytes for BGRA pixels.
*/
std::uint32_t frame_size() {
return width * height * 4;
}
};
/**
* @brief Create an X11 display capture backend.
*
* @param hwdevice_type Hardware device type requested for capture or encode.
* @param display_name Display name.
* @param config Configuration values to apply.
* @return X11 display backend, or nullptr when initialization fails.
*/
std::shared_ptr<display_t> x11_display(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) {
if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) {
BOOST_LOG(error) << "Could not initialize x11 display with the given hw device type"sv;
@@ -763,6 +948,11 @@
return x11_disp;
}
/**
* @brief Enumerate display names accepted by the X11 backend.
*
* @return X11 display names, or an empty list when X11 probing fails.
*/
std::vector<std::string> x11_display_names() {
if (load_x11() || load_xcb()) {
BOOST_LOG(error) << "Couldn't init x11 libraries"sv;
@@ -800,14 +990,23 @@
return names;
}
/**
* @brief Release image resources.
*/
void freeImage(XImage *p) {
XDestroyImage(p);
}
/**
* @brief Release x resources.
*/
void freeX(XFixesCursorImage *p) {
x11::Free(p);
}
/**
* @brief Load XCB entry points used by the X11 capture backend.
*/
int load_xcb() {
// This will be called once only
static int xcb_status = xcb::init_shm() || xcb::init();
@@ -815,6 +1014,9 @@
return xcb_status;
}
/**
* @brief Load X11 entry points used by the X11 capture backend.
*/
int load_x11() {
// This will be called once only
static int x11_status =
@@ -868,14 +1070,25 @@
blend_cursor((xdisplay_t::pointer) ctx.get(), img, offsetX, offsetY);
}
/**
* @brief Open and initialize the display connection used for capture.
*/
xdisplay_t make_display() {
return OpenDisplay(nullptr);
}
/**
* @brief Release display resources.
*/
void freeDisplay(_XDisplay *xdisplay) {
CloseDisplay(xdisplay);
}
/**
* @brief Release cursor context resources.
*
* @param ctx Native context object used by the operation or callback.
*/
void freeCursorCtx(cursor_ctx_t::pointer ctx) {
CloseDisplay((xdisplay_t::pointer) ctx);
}
src/platform/linux/x11grab.h +38 −1
@@ -20,16 +20,45 @@
namespace platf::x11 {
struct cursor_ctx_raw_t;
/**
* @brief Release cursor context resources.
*
* @param ctx Native context object used by the operation or callback.
*/
void freeCursorCtx(cursor_ctx_raw_t *ctx);
/**
* @brief Release display resources.
*
* @param xdisplay X11 display connection.
*/
void freeDisplay(_XDisplay *xdisplay);
/**
* @brief XFixes cursor image pointer released with `XFree`.
*/
using cursor_ctx_t = util::safe_ptr<cursor_ctx_raw_t, freeCursorCtx>;
/**
* @brief X11 display pointer released with `XCloseDisplay`.
*/
using xdisplay_t = util::safe_ptr<_XDisplay, freeDisplay>;
/**
* @brief X11 cursor image and positioning state used during capture.
*/
class cursor_t {
public:
/**
* @brief Allocate the underlying object and wrap it in the owning handle.
*
* @return Created backend object, or null when creation fails.
*/
static std::optional<cursor_t> make();
/**
* @brief Run the capture loop for this backend.
*
* @param img Image or frame object to read from or populate.
*/
void capture(egl::cursor_t &img);
/**
@@ -37,11 +66,19 @@
*
* img <-- destination image
* offsetX, offsetY <--- Top left corner of the virtual screen
* @param img Image or frame object to read from or populate.
* @param offsetX Offset x.
* @param offsetY Offset y.
*/
void blend(img_t &img, int offsetX, int offsetY);
cursor_ctx_t ctx; ///< X11 cursor context used to track and blend cursor images.
cursor_ctx_t ctx;
};
/**
* @brief Open and initialize the display connection used for capture.
*
* @return Constructed display object.
*/
xdisplay_t make_display();
} // namespace platf::x11
src/platform/macos/av_audio.h +22 −0
@@ -27,6 +27,28 @@
@class CATapDescription;
namespace platf {
/**
* @brief Provide captured PCM frames to AudioConverter.
*
* @param inAudioConverter In audio converter.
* @param ioNumberDataPackets Requested and returned packet count.
* @param ioData Buffer list filled with input audio.
* @param outDataPacketDescription Optional packet description output.
* @param inUserData AudioConverterInputData state used by the callback.
* @return Core Audio status code from the callback.
*/
OSStatus audioConverterComplexInputProc(AudioConverterRef _Nullable inAudioConverter, UInt32 *_Nonnull ioNumberDataPackets, AudioBufferList *_Nonnull ioData, AudioStreamPacketDescription *_Nullable *_Nullable outDataPacketDescription, void *_Nonnull inUserData);
/**
* @brief Receive system-audio tap samples from Core Audio.
*
* @param inDevice In device.
* @param inNow In now.
* @param inInputData In input data.
* @param inInputTime In input time.
* @param outOutputData Out output data.
* @param inOutputTime In output time.
* @param inClientData In client data.
* @return Core Audio status code from the IO callback.
*/
OSStatus systemAudioIOProc(AudioObjectID inDevice, const AudioTimeStamp *_Nullable inNow, const AudioBufferList *_Nullable inInputData, const AudioTimeStamp *_Nullable inInputTime, AudioBufferList *_Nullable outOutputData, const AudioTimeStamp *_Nullable inOutputTime, void *_Nullable inClientData);
} // namespace platf
src/platform/macos/av_img_t.h +41 −7
@@ -12,9 +12,17 @@
#include "src/platform/common.h"
namespace platf {
/**
* @brief CoreMedia sample buffer retained by an AV image wrapper.
*/
struct av_sample_buf_t {
CMSampleBufferRef buf;
CMSampleBufferRef buf; ///< Retained CoreMedia sample buffer.
/**
* @brief Retain a CoreMedia sample buffer for captured-image lifetime.
*
* @param buf Sample buffer received from AVFoundation.
*/
explicit av_sample_buf_t(CMSampleBufferRef buf):
buf((CMSampleBufferRef) CFRetain(buf)) {
}
@@ -26,10 +34,18 @@
}
};
/**
* @brief CoreVideo pixel buffer retained by an AV image wrapper.
*/
struct av_pixel_buf_t {
CVPixelBufferRef buf; ///< Pixel buffer extracted from the sample buffer.
CVPixelBufferRef buf;
// Constructor
/**
* @brief Lock the sample buffer's pixel buffer for read-only access.
*
* @param sb Sample buffer that owns the image data.
*/
explicit av_pixel_buf_t(CMSampleBufferRef sb):
buf(
CMSampleBufferGetImageBuffer(sb)
@@ -37,6 +53,11 @@
CVPixelBufferLockBaseAddress(buf, kCVPixelBufferLock_ReadOnly);
}
/**
* @brief Return the base address of the locked Core Video pixel buffer.
*
* @return Pointer to the first byte of image data in the pixel buffer.
*/
[[nodiscard]] uint8_t *data() const {
return static_cast<uint8_t *>(CVPixelBufferGetBaseAddress(buf));
}
@@ -49,16 +70,29 @@
}
};
/**
* @brief Captured macOS image backed by an AV sample buffer.
*/
struct av_img_t: img_t {
std::shared_ptr<av_sample_buf_t> sample_buffer;
std::shared_ptr<av_pixel_buf_t> pixel_buffer;
std::shared_ptr<av_sample_buf_t> sample_buffer; ///< Retained AVFoundation sample buffer for the captured frame.
std::shared_ptr<av_pixel_buf_t> pixel_buffer; ///< Retained CoreVideo pixel buffer extracted from the sample.
};
/**
* @brief Temporary retain wrapper used while AV image data is borrowed.
*/
struct temp_retain_av_img_t {
std::shared_ptr<av_sample_buf_t> sample_buffer;
std::shared_ptr<av_pixel_buf_t> pixel_buffer;
uint8_t *data;
std::shared_ptr<av_sample_buf_t> sample_buffer; ///< Sample buffer kept alive while `data` is borrowed.
std::shared_ptr<av_pixel_buf_t> pixel_buffer; ///< Pixel buffer kept locked while `data` is borrowed.
uint8_t *data; ///< Pointer to the locked pixel buffer bytes.
/**
* @brief Construct a temporary AV image retain wrapper.
*
* @param sb Sample buffer to retain.
* @param pb Pixel buffer to retain.
* @param dt Image data pointer.
*/
temp_retain_av_img_t(
std::shared_ptr<av_sample_buf_t> sb,
std::shared_ptr<av_pixel_buf_t> pb,
src/platform/macos/av_video.h +68 −2
@@ -8,34 +8,100 @@
#import <AppKit/AppKit.h>
#import <AVFoundation/AVFoundation.h>
/**
* @brief macOS capture session and video output handles.
*/
struct CaptureSession {
AVCaptureVideoDataOutput *output; ///< Output.
NSCondition *captureStopped; ///< Capture stopped.
AVCaptureVideoDataOutput *output;
NSCondition *captureStopped;
};
static const int kMaxDisplays = 32;
/**
* @brief AVFoundation video capture controller used by the macOS backend.
*/
@interface AVVideo: NSObject <AVCaptureVideoDataOutputSampleBufferDelegate>
/**
* @brief Display ID property.
*/
@property (nonatomic, assign) CGDirectDisplayID displayID;
/**
* @brief Min frame duration property.
*/
@property (nonatomic, assign) CMTime minFrameDuration;
/**
* @brief Pixel format property.
*/
@property (nonatomic, assign) OSType pixelFormat;
/**
* @brief Frame width property.
*/
@property (nonatomic, assign) int frameWidth;
/**
* @brief Frame height property.
*/
@property (nonatomic, assign) int frameHeight;
/**
* @brief Objective-C block invoked for each captured sample buffer.
*/
typedef bool (^FrameCallbackBlock)(CMSampleBufferRef);
/**
* @brief Capture session that owns the active AVFoundation inputs and outputs.
*/
@property (nonatomic, assign) AVCaptureSession *session;
/**
* @brief Video outputs property.
*/
@property (nonatomic, assign) NSMapTable<AVCaptureConnection *, AVCaptureVideoDataOutput *> *videoOutputs;
/**
* @brief Capture callbacks property.
*/
@property (nonatomic, assign) NSMapTable<AVCaptureConnection *, FrameCallbackBlock> *captureCallbacks;
/**
* @brief Capture signals property.
*/
@property (nonatomic, assign) NSMapTable<AVCaptureConnection *, dispatch_semaphore_t> *captureSignals;
/**
* @brief List display names accepted by the selected capture backend.
*
* @return Display names accepted by the selected capture backend.
*/
+ (NSArray<NSDictionary *> *)displayNames;
/**
* @brief Return the user-visible name for a CoreGraphics display.
*
* @param displayID Display ID.
* @return Display name for the supplied CoreGraphics display ID.
*/
+ (NSString *)getDisplayName:(CGDirectDisplayID)displayID;
/**
* @brief Initialize AVFoundation capture for a display and frame rate.
*
* @param displayID Display ID.
* @param frameRate Frame rate.
* @return Initialized AVVideo instance, or nil on failure.
*/
- (id)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate;
/**
* @brief Set frame width frame height.
*
* @param frameWidth Frame width.
* @param frameHeight Frame height.
*/
- (void)setFrameWidth:(int)frameWidth frameHeight:(int)frameHeight;
/**
* @brief Run the capture loop for this backend.
*
* @param frameCallback Frame callback.
* @return Capture status reported to the streaming pipeline.
*/
- (dispatch_semaphore_t)capture:(FrameCallbackBlock)frameCallback;
@end
src/platform/macos/coreaudio_helpers.h +4 −1
@@ -52,8 +52,11 @@
}
namespace detail {
/**
* @brief Small wrapper for displaying CoreAudio OSStatus values.
*/
struct StatusView {
OSStatus e;
OSStatus e; ///< E.
};
inline std::ostream &operator<<(std::ostream &os, StatusView v) {
src/platform/macos/display.mm +42 −5
@@ -21,6 +21,10 @@
#include "src/platform/macos/nv12_zero_device.h"
// Avoid conflict between AVFoundation and libavutil both defining AVMediaType
/**
* @def AVMediaType
* @brief Macro for AV media type.
*/
#define AVMediaType AVMediaType_FFmpeg
#include "src/video.h"
#undef AVMediaType
@@ -210,10 +214,13 @@
}
} // namespace
/**
* @brief macOS display capture source and image buffers.
*/
struct av_display_t: public display_t {
AVVideo *av_capture {};
CGDirectDisplayID display_id {};
IOPMAssertionID display_sleep_assertion {kIOPMNullAssertionID};
AVVideo *av_capture {}; ///< AV capture.
CGDirectDisplayID display_id {}; ///< Display ID.
IOPMAssertionID display_sleep_assertion {kIOPMNullAssertionID}; ///< Display sleep assertion.
~av_display_t() override {
[av_capture release];
@@ -226,6 +233,9 @@
}
}
/**
* @brief Prevent macOS from sleeping the captured display while streaming.
*/
void prevent_display_sleep() {
if (display_sleep_assertion != kIOPMNullAssertionID) {
return;
@@ -292,10 +302,21 @@
return capture_e::ok;
}
/**
* @brief Allocate an image buffer compatible with this display backend.
*
* @return Allocated img object, or null when unavailable.
*/
std::shared_ptr<img_t> alloc_img() override {
return std::make_shared<av_img_t>();
}
/**
* @brief Create AVCodec encode device.
*
* @param pix_fmt Sunshine pixel format to convert or allocate for.
* @return Constructed AVCodec encode device object.
*/
std::unique_ptr<avcodec_encode_device_t> make_avcodec_encode_device(pix_fmt_e pix_fmt) override {
if (pix_fmt == pix_fmt_e::yuv420p) {
av_capture.pixelFormat = kCVPixelFormatType_32BGRA;
@@ -313,6 +334,12 @@
}
}
/**
* @brief Populate a fallback image when real capture data is unavailable.
*
* @param img Image or frame object to read from or populate.
* @return Capture status reported to the streaming pipeline.
*/
int dummy_img(img_t *img) override {
if (!platf::is_screen_capture_allowed()) {
// If we don't have the screen capture permission, this function will hang
@@ -359,11 +386,20 @@
* display --> an opaque pointer to an object of this class
* width --> the intended capture width
* height --> the intended capture height
* @param display Display object or identifier associated with the operation.
* @param width Frame or display width in pixels.
* @param height Frame or display height in pixels.
*/
static void setResolution(void *display, int width, int height) {
[static_cast<AVVideo *>(display) setFrameWidth:width frameHeight:height];
}
/**
* @brief Set pixel format.
*
* @param display Display object or identifier associated with the operation.
* @param pixelFormat Pixel format.
*/
static void setPixelFormat(void *display, OSType pixelFormat) {
static_cast<AVVideo *>(display).pixelFormat = pixelFormat;
}
@@ -440,8 +476,9 @@
}
/**
* @brief Returns if GPUs/drivers have changed since the last call to this function.
* @return `true` if a change has occurred or if it is unknown whether a change occurred.
* @brief Report whether encoder backends should be probed again before streaming.
*
* @return Always `true` because macOS GPU changes are not tracked by this backend.
*/
bool needs_encoder_reenumeration() {
// We don't track GPU state, so we will always reenumerate. Fortunately, it is fast on macOS.
src/platform/macos/input.cpp +91 −21
@@ -24,49 +24,67 @@
#include "src/platform/common.h"
#include "src/utility.h"
/**
* @brief Delay for a double click, in milliseconds.
* @todo Make this configurable.
constexpr auto MULTICLICK_DELAY_MS = std::chrono::milliseconds(500); ///< Maximum gap between clicks that macOS should treat as a double click.
*/
constexpr std::chrono::milliseconds MULTICLICK_DELAY_MS(500);
namespace platf {
using namespace std::literals;
constexpr int WHEEL_DELTA = 120;
constexpr double DEFAULT_SCROLLWHEEL_SCALING = 0.3125;
constexpr int WHEEL_DELTA = 120; ///< Protocol or platform constant for wheel delta.
constexpr double DEFAULT_SCROLLWHEEL_SCALING = 0.3125; ///< Protocol or platform constant for default scrollwheel scaling.
constexpr int DEFAULT_SCROLL_LINES_PER_DETENT = 5; ///< Protocol or platform constant for default scroll lines per detent.
constexpr int DEFAULT_SCROLL_LINES_PER_DETENT = 5;
/**
* @brief macOS input source and target display state.
*/
struct macos_input_t {
public:
CGDirectDisplayID display {}; ///< CoreGraphics identifier for the display receiving injected input.
CGFloat displayScaling {}; ///< Scale factor used to translate client coordinates to display pixels.
CGEventSourceRef source {}; ///< CoreGraphics event source used for mouse and scroll events.
CGDirectDisplayID display {};
CGFloat displayScaling {};
CGEventSourceRef source {};
// keyboard related stuff
CGEventSourceRef keyboard_source {};
CGEventFlags kb_flags {};
CGEventSourceRef keyboard_source {}; ///< CoreGraphics event source used for keyboard injection.
CGEventFlags kb_flags {}; ///< Active keyboard modifier flags currently held down by the client.
// mouse related stuff
CGEventRef mouse_event {}; // mouse event source
double scrollwheel_scaling {DEFAULT_SCROLLWHEEL_SCALING};
int scroll_lines_per_detent {DEFAULT_SCROLL_LINES_PER_DETENT};
CGEventRef mouse_event {}; ///< Reusable CoreGraphics mouse event updated before posting.
double scrollwheel_scaling {DEFAULT_SCROLLWHEEL_SCALING}; ///< Multiplier applied to incoming scroll-wheel deltas.
int scroll_lines_per_detent {DEFAULT_SCROLL_LINES_PER_DETENT}; ///< Number of logical scroll lines represented by one wheel detent.
/**
* @brief Tracks whether the mouse button is currently pressed.
*/
bool mouse_down[3] {}; // mouse button status
/**
* @brief Last mouse event.
*/
std::chrono::steady_clock::steady_clock::time_point last_mouse_event[3][2]; // timestamp of last mouse events
};
// A struct to hold a Windows keycode to Mac virtual keycode mapping.
/**
* @brief Mapping from Sunshine key symbols to macOS virtual key codes.
*/
struct KeyCodeMap {
int win_keycode;
int mac_keycode;
int win_keycode; ///< Sunshine/Windows virtual key code received from the client.
int mac_keycode; ///< macOS virtual key code sent through CoreGraphics.
};
// Customized less operator for using std::lower_bound() on a KeyCodeMap array.
/**
* @brief Order key mappings by Sunshine key code for binary search.
*
* @param a Left-hand mapping being compared.
* @param b Right-hand mapping being compared.
* @return True when `a` has a lower Sunshine key code than `b`.
*/
bool operator<(const KeyCodeMap &a, const KeyCodeMap &b) {
return a.win_keycode < b.win_keycode;
}
// clang-format off
/**
* @brief Key codes map.
*/
const KeyCodeMap kKeyCodesMap[] = {
{ 0x08 /* VKEY_BACK */, kVK_Delete },
{ 0x09 /* VKEY_TAB */, kVK_Tab },
@@ -238,5 +256,11 @@
};
// clang-format on
/**
* @brief Translate a platform keycode to a Sunshine key symbol.
*
* @param keycode Platform keycode being translated or emitted.
* @return Sunshine key symbol, or 0 when the keycode is unmapped.
*/
int keysym(int keycode) {
KeyCodeMap key_map {};
@@ -256,12 +280,22 @@
return temp_map->mac_keycode;
}
/**
* @brief macOS modifier flags split into generic and device-specific bits.
*/
struct modifier_flags_t {
CGEventFlags generic {};
CGEventFlags generic {}; ///< Modifier bits represented by device-independent CoreGraphics flags.
CGEventFlags device {}; ///< Modifier bits represented by left/right device-specific flags.
CGEventFlags all_devices {}; ///< Mask covering all device-specific variants for this modifier.
CGEventFlags device {};
CGEventFlags all_devices {};
};
/**
* @brief Resolve the CoreGraphics modifier masks associated with a key code.
*
* @param key Sunshine key code that may represent a modifier key.
* @param flags Output masks for the matching CoreGraphics modifier.
* @return True when modifier flags were found for the key.
*/
bool modifier_flags_for_key(int key, modifier_flags_t &flags) {
switch (key) {
case kVK_Shift:
@@ -368,6 +402,16 @@
};
}
/**
* @brief Post a mouse event at the clamped display location.
*
* @param input Platform input context.
* @param button Mouse button.
* @param type CoreGraphics mouse event type.
* @param raw_location Requested mouse location.
* @param previous_location Previous mouse location.
* @param click_count Click count for the event.
*/
void post_mouse(
input_t &input,
const CGMouseButton button,
@@ -410,5 +454,11 @@
CGWarpMouseCursorPosition(location);
}
/**
* @brief Choose the CoreGraphics mouse event type for the current button action.
*
* @param input Platform input backend that receives the event.
* @return CoreGraphics mouse event type for button press or release.
*/
inline CGEventType event_type_mouse(input_t &input) {
const auto macos_input = static_cast<macos_input_t *>(input.get());
@@ -494,6 +544,12 @@
macos_input->last_mouse_event[mac_button][release] = now;
}
/**
* @brief Get scroll lines per detent.
*
* @param scrollwheel_scaling Scrollwheel scaling.
* @return Number of logical lines represented by one wheel detent.
*/
int get_scroll_lines_per_detent(double &scrollwheel_scaling) {
double scale = DEFAULT_SCROLLWHEEL_SCALING;
const auto value = CFPreferencesCopyValue(CFSTR("com.apple.scrollwheel.scaling"), kCFPreferencesAnyApplication, kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
@@ -521,6 +577,13 @@
return std::max(1, static_cast<int>(std::ceil(1.0 + scroll_scale * lines_per_scroll_scale)));
}
/**
* @brief Convert a high-resolution wheel delta to CoreGraphics scroll pixels.
*
* @param macos_input macOS input state containing scroll scaling settings.
* @param high_res_distance Wheel delta in Windows high-resolution units.
* @return Pixel distance to send to CoreGraphics.
*/
int scroll_pixels(const macos_input_t *macos_input, const int high_res_distance) {
const auto source_pixels_per_line = CGEventSourceGetPixelsPerLine(macos_input->source);
const auto pixels_per_line = source_pixels_per_line > 0 ? static_cast<int>(source_pixels_per_line + 0.5) : 10;
@@ -529,6 +592,13 @@
return static_cast<int>(scaled_pixels / WHEEL_DELTA);
}
/**
* @brief Post a macOS scroll event to the target display.
*
* @param input Platform input backend that receives the event.
* @param wheelY Wheel y.
* @param wheelX Wheel x.
*/
void post_scroll(input_t &input, const int wheelY, const int wheelX) {
if (wheelY == 0 && wheelX == 0) {
return;
src/platform/macos/microphone.mm +36 −2
@@ -11,13 +11,22 @@
namespace platf {
using namespace std::literals;
/**
* @brief macOS microphone capture device and audio format state.
*/
struct av_mic_t: public mic_t {
AVAudio *av_audio_capture {};
AVAudio *av_audio_capture {}; ///< AV audio capture.
~av_mic_t() override {
[av_audio_capture release];
}
/**
* @brief Deliver a captured audio sample to Sunshine's audio pipeline.
*
* @param sample_in Sample in.
* @return Capture status reported to the streaming pipeline.
*/
capture_e sample(std::vector<float> &sample_in) override {
const uint32_t neededBytes = static_cast<uint32_t>(sample_in.size() * sizeof(float));
uint8_t *dst = reinterpret_cast<uint8_t *>(sample_in.data());
@@ -54,15 +63,35 @@
}
};
/**
* @brief macOS audio control state used to create microphone streams.
*/
struct macos_audio_control_t: public audio_control_t {
AVCaptureDevice *audio_capture_device {}; ///< Audio capture device.
AVCaptureDevice *audio_capture_device {};
public:
/**
* @brief Update the sink value on the backend.
*
* @param sink Audio sink name to route or capture.
* @return Status from updating sink.
*/
int set_sink(const std::string &sink) override {
BOOST_LOG(warning) << "audio_control_t::set_sink() unimplemented: "sv << sink;
return 0;
}
/**
* @brief Create a microphone capture stream for the requested layout.
*
* @param mapping Opus channel mapping table for the requested layout.
* @param channels Number of audio channels in the stream.
* @param sample_rate Audio sample rate in hertz.
* @param frame_size Number of samples captured per audio frame.
* @param continuous_audio Continuous audio.
* @param host_audio_enabled Whether host playback should remain enabled during capture.
* @return Microphone capture object for the requested audio layout.
*/
std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, bool continuous_audio, bool host_audio_enabled) override {
auto mic = std::make_unique<av_mic_t>();
mic->av_audio_capture = [[AVAudio alloc] init];
@@ -110,5 +139,10 @@
return true;
}
/**
* @brief Query host and virtual sink names available to Sunshine.
*
* @return Host and virtual sink names when the backend can report them.
*/
std::optional<sink_t> sink_info() override {
sink_t sink;
src/platform/macos/misc.h +20 −1
@@ -11,13 +11,32 @@
#include <CoreGraphics/CoreGraphics.h>
namespace platf {
/**
* @brief Check whether macOS has granted screen-capture permission.
*
* @return True when Sunshine can capture the screen.
*/
bool is_screen_capture_allowed();
} // namespace platf
}
namespace dyn {
typedef void (*apiproc)();
/**
* @brief Load persisted state from its backing store.
*
* @param handle Native library or object handle used by the operation.
* @param funcs Function table populated from the loaded library.
* @param strict Whether missing functions should be treated as an error.
* @return 0 when all required symbols are loaded; nonzero when loading fails.
*/
int load(void *handle, const std::vector<std::tuple<apiproc *, const char *>> &funcs, bool strict = true);
/**
* @brief Return the native handle owned by the wrapper.
*
* @param libs List of libraries to probe for the requested symbol.
* @return Native dynamic-library handle, or nullptr when no library can be opened.
*/
void *handle(const std::vector<const char *> &libs);
} // namespace dyn
src/platform/macos/misc.mm +29 −5
@@ -5,5 +5,9 @@
// Required for IPV6_PKTINFO with Darwin headers
#ifndef __APPLE_USE_RFC_3542 // NOLINT(bugprone-reserved-identifier)
/**
* @def __APPLE_USE_RFC_3542
* @brief Macro for APPLE USE RFC 3542.
*/
#define __APPLE_USE_RFC_3542 1
#endif
@@ -43,6 +47,16 @@
#if __MAC_OS_X_VERSION_MAX_ALLOWED < 110000 // __MAC_11_0
// If they're not in the SDK then we can use our own function definitions.
// Need to use weak import so that this will link in macOS 10.14 and earlier
/**
* @brief Query macOS screen-capture permission without prompting the user.
*
* @return True when screen-capture permission is granted.
*/
extern "C" bool CGPreflightScreenCaptureAccess(void) __attribute__((weak_import));
/**
* @brief Request macOS screen-capture permission from the user.
*
* @return True when screen-capture permission is granted.
*/
extern "C" bool CGRequestScreenCaptureAccess(void) __attribute__((weak_import));
#endif
@@ -52,6 +66,9 @@
} // namespace
// Return whether screen capture is allowed for this process.
/**
* @brief Check whether screen capture allowed.
*/
bool is_screen_capture_allowed() {
return screen_capture_allowed;
}
@@ -436,8 +453,17 @@
// are disconnected.
static std::atomic<int> qos_ref_count = 0;
/**
* @brief Owns platform QoS state that is restored during cleanup.
*/
class qos_t: public deinit_t {
public:
/**
* @brief Apply macOS socket QoS settings for scoped cleanup.
*
* @param sockfd Native socket descriptor whose options are updated.
* @param options Request options or socket options to apply.
*/
qos_t(int sockfd, std::vector<std::tuple<int, int, int>> options):
sockfd(sockfd),
options(options) {
@@ -462,11 +488,6 @@
/**
* @brief Enables QoS on the given socket for traffic to the specified destination.
* @param native_socket The native socket handle.
* @param address The destination address for traffic sent on this socket.
* @param port The destination port for traffic sent on this socket.
* @param data_type The type of traffic sent on this socket.
* @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic.
*/
std::unique_ptr<deinit_t> enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) {
int sockfd = (int) native_socket;
@@ -546,6 +567,9 @@
}
}
/**
* @brief macOS high-precision timer implementation backed by a worker thread.
*/
class macos_high_precision_timer: public high_precision_timer {
public:
void sleep_for(const std::chrono::nanoseconds &duration) override {
src/platform/macos/nv12_zero_device.cpp +10 −1
@@ -16,15 +16,24 @@
namespace platf {
/**
* @brief Release an FFmpeg frame allocated by the capture or conversion backend.
*/
void free_frame(AVFrame *frame) {
av_frame_free(&frame);
}
/**
* @brief Release a backend buffer allocated for capture or conversion.
*
* @param opaque Opaque user pointer provided to the callback.
* @param data Payload or state data to serialize, deserialize, or forward.
*/
void free_buffer(void *opaque, uint8_t *data) {
CVPixelBufferRelease((CVPixelBufferRef) data);
}
util::safe_ptr<AVFrame, free_frame> av_frame;
util::safe_ptr<AVFrame, free_frame> av_frame; ///< AV frame.
int nv12_zero_device::convert(platf::img_t &img) {
auto *av_img = (av_img_t *) &img;
src/platform/macos/nv12_zero_device.h +37 −1
@@ -10,8 +10,16 @@
struct AVFrame;
namespace platf {
/**
* @brief Release an FFmpeg frame allocated by the capture or conversion backend.
*
* @param frame Video or graphics frame being processed.
*/
void free_frame(AVFrame *frame);
/**
* @brief macOS zero-copy encode device that forwards NV12 frames from AVFoundation to FFmpeg.
*/
class nv12_zero_device: public avcodec_encode_device_t {
// display holds a pointer to an av_video object. Since the namespaces of AVFoundation
// and FFMPEG collide, we need this opaque pointer and cannot use the definition
@@ -20,13 +28,41 @@
public:
// this function is used to set the resolution on an av_video object that we cannot
// call directly because of namespace collisions between AVFoundation and FFMPEG
/**
* @brief Callback signature used to update the opaque AVFoundation display resolution.
*/
using resolution_fn_t = std::function<void(void *display, int width, int height)>;
resolution_fn_t resolution_fn; ///< Callback stored for later AVFoundation resolution updates.
/**
* @brief Callback signature used to update the opaque AVFoundation pixel format.
*/
resolution_fn_t resolution_fn;
using pixel_format_fn_t = std::function<void(void *display, int pixelFormat)>;
/**
* @brief Initialize zero-copy NV12 encoding for an AVFoundation display.
*
* @param display Display object or identifier associated with the operation.
* @param pix_fmt Sunshine pixel format to convert or allocate for.
* @param resolution_fn Callback used to resize the AVFoundation capture output.
* @param pixel_format_fn Pixel format.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(void *display, pix_fmt_e pix_fmt, resolution_fn_t resolution_fn, const pixel_format_fn_t &pixel_format_fn);
/**
* @brief Forward a captured AVFoundation frame to FFmpeg without copying.
*
* @param img Image or frame object to read from or populate.
* @return Conversion status.
*/
int convert(img_t &img) override;
/**
* @brief Attach frame resources used by the next conversion or encode operation.
*
* @param frame Video or graphics frame being processed.
* @param hw_frames_ctx FFmpeg hardware frames context associated with the frame.
* @return Status from updating frame.
*/
int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override;
private:
src/platform/windows/audio.cpp +234 −22
@@ -26,9 +26,24 @@
#include "PolicyConfig.h"
// clang-format on
#ifdef DOXYGEN
/**
* @brief Property key for a device description.
*/
extern const PROPERTYKEY PKEY_Device_DeviceDesc;
/**
* @brief Property key for a device friendly name.
*/
extern const PROPERTYKEY PKEY_Device_FriendlyName;
/**
* @brief Property key for a device interface friendly name.
*/
extern const PROPERTYKEY PKEY_DeviceInterface_FriendlyName;
#else
DEFINE_PROPERTYKEY(PKEY_Device_DeviceDesc, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 2); // DEVPROP_TYPE_STRING
DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14); // DEVPROP_TYPE_STRING
DEFINE_PROPERTYKEY(PKEY_DeviceInterface_FriendlyName, 0x026e516e, 0xb814, 0x414b, 0x83, 0xcd, 0x85, 0x6d, 0x6f, 0xef, 0x48, 0x22, 2);
#endif
#if defined(__x86_64) || defined(__x86_64__) || defined(__amd64) || defined(__amd64__) || defined(_M_AMD64)
#define STEAM_DRIVER_SUBDIR L"x64"
@@ -203,27 +218,70 @@
using namespace std::literals;
namespace platf::audio {
/**
* @brief Release the COM or platform reference owned by the pointer.
*
* @param p Pointer passed to the deleter or conversion helper.
*/
template<class T>
void Release(T *p) {
p->Release();
}
/**
* @brief Free memory allocated by COM task APIs.
*
* @param p Pointer passed to the deleter or conversion helper.
*/
template<class T>
void co_task_free(T *p) {
CoTaskMemFree((LPVOID) p);
}
/**
* @brief COM device enumerator pointer for WASAPI endpoint discovery.
*/
using device_enum_t = util::safe_ptr<IMMDeviceEnumerator, Release<IMMDeviceEnumerator>>;
/**
* @brief COM pointer to a Windows audio endpoint device.
*/
using device_t = util::safe_ptr<IMMDevice, Release<IMMDevice>>;
/**
* @brief COM pointer to a collection of Windows audio endpoint devices.
*/
using collection_t = util::safe_ptr<IMMDeviceCollection, Release<IMMDeviceCollection>>;
/**
* @brief COM pointer to the WASAPI audio client interface.
*/
using audio_client_t = util::safe_ptr<IAudioClient, Release<IAudioClient>>;
/**
* @brief COM pointer to the WASAPI capture client interface.
*/
using audio_capture_t = util::safe_ptr<IAudioCaptureClient, Release<IAudioCaptureClient>>;
/**
* @brief CoTaskMem-allocated WAVEFORMATEX pointer.
*/
using wave_format_t = util::safe_ptr<WAVEFORMATEX, co_task_free<WAVEFORMATEX>>;
/**
* @brief CoTaskMem-allocated wide string pointer.
*/
using wstring_t = util::safe_ptr<WCHAR, co_task_free<WCHAR>>;
/**
* @brief Windows HANDLE wrapper closed with `CloseHandle`.
*/
using handle_t = util::safe_ptr_v2<void, BOOL, CloseHandle>;
/**
* @brief COM pointer to the Windows policy configuration interface.
*/
using policy_t = util::safe_ptr<IPolicyConfig, Release<IPolicyConfig>>;
/**
* @brief COM pointer to a Windows property store.
*/
using prop_t = util::safe_ptr<IPropertyStore, Release<IPropertyStore>>;
/**
* @brief Initializes COM for the current thread and uninitializes it on exit.
*/
class co_init_t: public deinit_t {
public:
co_init_t() {
@@ -235,6 +293,9 @@
}
};
/**
* @brief RAII wrapper that initializes and clears a Windows PROPVARIANT.
*/
class prop_var_t {
public:
prop_var_t() {
@@ -245,16 +306,22 @@
PropVariantClear(&prop);
}
PROPVARIANT prop;
PROPVARIANT prop; ///< Variant value returned by Windows property-store queries.
};
/**
* @brief Windows audio format details selected for capture.
*/
struct format_t {
WORD channel_count;
std::string name;
int capture_waveformat_channel_mask;
virtual_sink_waveformats_t virtual_sink_waveformats;
WORD channel_count; ///< Channel count.
std::string name; ///< Human-readable name for this item.
int capture_waveformat_channel_mask; ///< Capture waveformat channel mask.
virtual_sink_waveformats_t virtual_sink_waveformats; ///< Virtual sink waveformats.
};
/**
* @brief Formats.
*/
const std::array<const format_t, 3> formats = {
format_t {
2,
@@ -276,6 +343,13 @@
},
};
/**
* @brief Create audio client.
*
* @param device D3D, audio, or platform device used by the operation.
* @param format Pixel, audio, or protocol format being converted.
* @return Constructed audio client object.
*/
audio_client_t make_audio_client(device_t &device, const format_t &format) {
audio_client_t audio_client;
auto status = device->Activate(
@@ -335,6 +409,12 @@
return audio_client;
}
/**
* @brief Query the default Windows render endpoint.
*
* @param device_enum Windows multimedia device enumerator.
* @return Default render endpoint, or an empty handle if lookup fails.
*/
device_t default_device(device_enum_t &device_enum) {
device_t device;
HRESULT status;
@@ -353,20 +433,40 @@
return device;
}
/**
* @brief Windows audio endpoint notification callback registered with MMDevice.
*/
class audio_notification_t: public ::IMMNotificationClient {
public:
audio_notification_t() {
}
// IUnknown implementation (unused by IMMDeviceEnumerator)
/**
* @brief Satisfy IUnknown reference counting for the notification callback.
*
* @return Static reference count because the callback lifetime is externally owned.
*/
ULONG STDMETHODCALLTYPE AddRef() {
return 1;
}
/**
* @brief Release the COM or platform reference owned by the pointer.
*
* @return Reference count or status returned after releasing the object.
*/
ULONG STDMETHODCALLTYPE Release() {
return 1;
}
/**
* @brief Return the supported COM interface for the notification callback.
*
* @param riid COM interface identifier requested by QueryInterface.
* @param ppvInterface Output pointer receiving the requested interface.
* @return S_OK when the interface is supported; E_NOINTERFACE otherwise.
*/
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID **ppvInterface) {
if (IID_IUnknown == riid) {
AddRef();
@@ -383,6 +483,14 @@
}
// IMMNotificationClient
/**
* @brief Handle a Windows default-audio-device change notification.
*
* @param flow Audio endpoint data-flow direction.
* @param role Audio endpoint role used for default-device lookup.
* @param pwstrDeviceId Windows endpoint ID for the new default device.
* @return S_OK after recording the render-device change notification.
*/
HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) {
if (flow == eRender) {
default_render_device_changed_flag.store(true);
@@ -390,14 +498,33 @@
return S_OK;
}
/**
* @brief Ignore endpoint-add notifications.
*
* @param pwstrDeviceId Windows endpoint ID for the added device.
* @return S_OK because Sunshine does not act on this notification.
*/
HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) {
return S_OK;
}
/**
* @brief Ignore endpoint-removal notifications.
*
* @param pwstrDeviceId Windows endpoint ID for the removed device.
* @return S_OK because Sunshine does not act on this notification.
*/
HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) {
return S_OK;
}
/**
* @brief Handle Windows audio endpoint state changes.
*
* @param pwstrDeviceId Audio device ID.
* @param dwNewState New device state.
* @return COM status code.
*/
HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(
LPCWSTR pwstrDeviceId,
DWORD dwNewState
@@ -405,6 +532,13 @@
return S_OK;
}
/**
* @brief Handle Windows audio endpoint property changes.
*
* @param pwstrDeviceId Audio device ID.
* @param key Changed property key.
* @return COM status code.
*/
HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(
LPCWSTR pwstrDeviceId,
const PROPERTYKEY key
@@ -424,7 +558,16 @@
std::atomic_bool default_render_device_changed_flag;
};
/**
* @brief WASAPI microphone capture stream and endpoint notification state.
*/
class mic_wasapi_t: public mic_t {
public:
/**
* @brief Deliver a captured audio sample to Sunshine's audio pipeline.
*
* @param sample_out Sample out.
* @return Capture status reported to the streaming pipeline.
*/
capture_e sample(std::vector<float> &sample_out) override {
auto sample_size = sample_out.size();
@@ -451,6 +594,15 @@
return capture_e::ok;
}
/**
* @brief Initialize WASAPI capture for the selected audio endpoint.
*
* @param sample_rate Audio sample rate in hertz.
* @param frame_size Number of samples captured per audio frame.
* @param channels_out Channels out.
* @param continuous Whether silent audio should continue to be emitted.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(std::uint32_t sample_rate, std::uint32_t frame_size, std::uint32_t channels_out, bool continuous) {
audio_event.reset(CreateEventA(nullptr, FALSE, FALSE, nullptr));
if (!audio_event) {
@@ -668,27 +820,35 @@
}
public:
handle_t audio_event;
handle_t audio_event; ///< Event signaled by WASAPI when captured audio is available.
device_enum_t device_enum;
device_t device;
audio_client_t audio_client;
audio_capture_t audio_capture;
device_enum_t device_enum; ///< Device enum.
device_t device; ///< WASAPI endpoint device selected for capture.
audio_client_t audio_client; ///< WASAPI audio client configured for shared-mode capture.
audio_capture_t audio_capture; ///< WASAPI capture client used to read sample packets.
audio_notification_t endpt_notification;
std::optional<std::function<void()>> default_endpt_changed_cb;
audio_notification_t endpt_notification; ///< Endpoint notification callback registered with Windows.
std::optional<std::function<void()>> default_endpt_changed_cb; ///< Callback invoked when the default endpoint changes.
REFERENCE_TIME default_latency_ms;
REFERENCE_TIME default_latency_ms; ///< WASAPI default device period used as capture latency.
util::buffer_t<float> sample_buf;
float *sample_buf_pos;
int channels;
bool continuous_audio;
util::buffer_t<float> sample_buf; ///< Floating-point sample buffer filled from WASAPI packets.
float *sample_buf_pos; ///< Current write position in `sample_buf`.
int channels; ///< Number of channels in the capture format.
bool continuous_audio; ///< Whether audio packets continue during silence.
HANDLE mmcss_task_handle = nullptr;
HANDLE mmcss_task_handle = nullptr; ///< MMCSS task handle for the audio capture thread.
};
/**
* @brief Platform audio controller that manages sinks and microphone capture.
*/
class audio_control_t: public ::platf::audio_control_t {
public:
/**
* @brief Query host and virtual sink names available to Sunshine.
*
* @return Host and virtual sink names when the backend can report them.
*/
std::optional<sink_t> sink_info() override {
sink_t sink;
@@ -767,5 +927,16 @@
return std::nullopt;
}
/**
* @brief Create a microphone capture stream for the requested layout.
*
* @param mapping Opus channel mapping table for the requested layout.
* @param channels Number of audio channels in the stream.
* @param sample_rate Audio sample rate in hertz.
* @param frame_size Number of samples captured per audio frame.
* @param continuous_audio Continuous audio.
* @param host_audio_enabled Whether host playback should remain enabled during capture.
* @return Microphone capture object for the requested audio layout.
*/
std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, bool continuous_audio, [[maybe_unused]] bool host_audio_enabled) override {
auto mic = std::make_unique<mic_wasapi_t>();
@@ -793,6 +964,8 @@
* Any virtual sink detected will be prefixed by:
* virtual-(format name)
* If it doesn't contain that prefix, then the format will not be changed
* @param sink Audio sink name to route or capture.
* @return Status from updating format.
*/
std::optional<std::wstring> set_format(const std::string &sink) {
if (sink.empty()) {
@@ -854,6 +1027,12 @@
return std::nullopt;
}
/**
* @brief Update the sink value on the backend.
*
* @param sink Audio sink name to route or capture.
* @return Status from updating sink.
*/
int set_sink(const std::string &sink) override {
auto device_id = set_format(sink);
if (!device_id) {
@@ -884,6 +1063,9 @@
return failure;
}
/**
* @brief Enumerates supported match field options.
*/
enum class match_field_e {
device_id, ///< Match device_id
device_friendly_name, ///< Match endpoint friendly name
@@ -891,15 +1073,32 @@
device_description, ///< Match endpoint description
};
/**
* @brief List of format fields used to compare audio formats.
*/
using match_fields_list_t = std::vector<std::pair<match_field_e, std::wstring>>;
/**
* @brief One matched audio-format field and its expected value.
*/
using matched_field_t = std::pair<match_field_e, std::wstring>;
/**
* @brief Build matching fields for Steam Streaming Speakers.
*
* @return Field list used to identify Steam's virtual speaker endpoint.
*/
audio_control_t::match_fields_list_t match_steam_speakers() {
return {
{match_field_e::adapter_friendly_name, L"Steam Streaming Speakers"}
};
}
/**
* @brief Build matching fields that all contain the same endpoint name.
*
* @param name Endpoint name or identifier to match across all fields.
* @return Field list requiring every supported endpoint field to match the name.
*/
audio_control_t::match_fields_list_t match_all_fields(const std::wstring &name) {
return {
{match_field_e::device_id, name}, // {0.0.0.00000000}.{29dd7668-45b2-4846-882d-950f55bf7eb8}
@@ -1116,6 +1315,11 @@
#endif
}
/**
* @brief Initialize Windows audio policy interfaces.
*
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init() {
auto status = CoCreateInstance(
CLSID_CPolicyConfigClient,
@@ -1147,11 +1351,14 @@
return 0;
}
/**
* @brief Destroy the Windows audio control.
*/
~audio_control_t() override {
}
policy_t policy;
audio::device_enum_t device_enum;
std::string assigned_sink;
policy_t policy; ///< Windows policy configuration interface used to switch default audio devices.
audio::device_enum_t device_enum; ///< Device enumerator used to query and watch audio endpoints.
std::string assigned_sink; ///< Virtual sink assigned while Sunshine captures host audio.
};
} // namespace platf::audio
@@ -1160,8 +1367,13 @@
// It's not big enough to justify it's own source file :/
namespace dxgi {
/**
* @brief Initialize the Windows audio-control backend.
*
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init();
}
} // namespace dxgi
std::unique_ptr<audio_control_t> audio_control() {
auto control = std::make_unique<audio::audio_control_t>();
src/platform/windows/display.h +499 −66
@@ -24,69 +24,198 @@
// Add D3D11_CREATE_DEVICE_DEBUG here to enable the D3D11 debug runtime.
// You should have a debugger like WinDbg attached to receive debug messages.
auto constexpr D3D11_CREATE_DEVICE_FLAGS = 0;
auto constexpr D3D11_CREATE_DEVICE_FLAGS = 0; ///< Protocol or platform constant for d3 d11 create device flags.
/**
* @brief Release the COM or platform reference owned by the pointer.
*
* @param dxgi DXGI object whose COM reference should be released.
*/
template<class T>
void Release(T *dxgi) {
dxgi->Release();
}
/**
* @brief Owning COM pointer for the DXGI factory used to enumerate adapters.
*/
using factory1_t = util::safe_ptr<IDXGIFactory1, Release<IDXGIFactory1>>;
/**
* @brief Owning COM pointer for a DXGI device interface.
*/
using dxgi_t = util::safe_ptr<IDXGIDevice, Release<IDXGIDevice>>;
/**
* @brief Owning COM pointer for a DXGI 1.1 device interface.
*/
using dxgi1_t = util::safe_ptr<IDXGIDevice1, Release<IDXGIDevice1>>;
/**
* @brief Owning COM pointer for the D3D11 device used by capture and conversion.
*/
using device_t = util::safe_ptr<ID3D11Device, Release<ID3D11Device>>;
/**
* @brief Owning COM pointer for the D3D11.1 device interface.
*/
using device1_t = util::safe_ptr<ID3D11Device1, Release<ID3D11Device1>>;
/**
* @brief Owning COM pointer for the immediate D3D11 device context.
*/
using device_ctx_t = util::safe_ptr<ID3D11DeviceContext, Release<ID3D11DeviceContext>>;
/**
* @brief Owning COM pointer for a DXGI display adapter.
*/
using adapter_t = util::safe_ptr<IDXGIAdapter1, Release<IDXGIAdapter1>>;
/**
* @brief Owning COM pointer for a DXGI output.
*/
using output_t = util::safe_ptr<IDXGIOutput, Release<IDXGIOutput>>;
/**
* @brief Owning COM pointer for a DXGI output that supports duplication.
*/
using output1_t = util::safe_ptr<IDXGIOutput1, Release<IDXGIOutput1>>;
/**
* @brief Owning COM pointer for a DXGI output with HDR metadata support.
*/
using output5_t = util::safe_ptr<IDXGIOutput5, Release<IDXGIOutput5>>;
/**
* @brief Owning COM pointer for a DXGI output with modern display descriptors.
*/
using output6_t = util::safe_ptr<IDXGIOutput6, Release<IDXGIOutput6>>;
/**
* @brief Owning COM pointer for the DXGI desktop duplication session.
*/
using dup_t = util::safe_ptr<IDXGIOutputDuplication, Release<IDXGIOutputDuplication>>;
/**
* @brief Owning COM pointer for a D3D11 2D texture.
*/
using texture2d_t = util::safe_ptr<ID3D11Texture2D, Release<ID3D11Texture2D>>;
/**
* @brief Owning COM pointer for a D3D11 1D texture.
*/
using texture1d_t = util::safe_ptr<ID3D11Texture1D, Release<ID3D11Texture1D>>;
/**
* @brief Owning COM pointer for the DXGI resource view of a shared texture.
*/
using resource_t = util::safe_ptr<IDXGIResource, Release<IDXGIResource>>;
/**
* @brief Owning COM pointer for the DXGI 1.1 resource sharing interface.
*/
using resource1_t = util::safe_ptr<IDXGIResource1, Release<IDXGIResource1>>;
/**
* @brief Owning COM pointer for the D3D11 multithread-protection interface.
*/
using multithread_t = util::safe_ptr<ID3D11Multithread, Release<ID3D11Multithread>>;
/**
* @brief Owning COM pointer for a D3D11 vertex shader.
*/
using vs_t = util::safe_ptr<ID3D11VertexShader, Release<ID3D11VertexShader>>;
/**
* @brief Owning COM pointer for a D3D11 pixel shader.
*/
using ps_t = util::safe_ptr<ID3D11PixelShader, Release<ID3D11PixelShader>>;
/**
* @brief Owning COM pointer for a D3D11 blend state.
*/
using blend_t = util::safe_ptr<ID3D11BlendState, Release<ID3D11BlendState>>;
/**
* @brief Owning COM pointer for a D3D11 input layout.
*/
using input_layout_t = util::safe_ptr<ID3D11InputLayout, Release<ID3D11InputLayout>>;
/**
* @brief Owning COM pointer for a D3D11 render-target view.
*/
using render_target_t = util::safe_ptr<ID3D11RenderTargetView, Release<ID3D11RenderTargetView>>;
/**
* @brief Owning COM pointer for a D3D11 shader-resource view.
*/
using shader_res_t = util::safe_ptr<ID3D11ShaderResourceView, Release<ID3D11ShaderResourceView>>;
/**
* @brief Owning COM pointer for a D3D11 buffer.
*/
using buf_t = util::safe_ptr<ID3D11Buffer, Release<ID3D11Buffer>>;
/**
* @brief D3D rasterizer state handle type.
*/
using raster_state_t = util::safe_ptr<ID3D11RasterizerState, Release<ID3D11RasterizerState>>;
/**
* @brief D3D sampler state handle type.
*/
using sampler_state_t = util::safe_ptr<ID3D11SamplerState, Release<ID3D11SamplerState>>;
/**
* @brief Owning COM pointer for a compiled shader bytecode blob.
*/
using blob_t = util::safe_ptr<ID3DBlob, Release<ID3DBlob>>;
/**
* @brief D3D depth-stencil state handle type.
*/
using depth_stencil_state_t = util::safe_ptr<ID3D11DepthStencilState, Release<ID3D11DepthStencilState>>;
/**
* @brief Owning COM pointer for a D3D11 depth-stencil view.
*/
using depth_stencil_view_t = util::safe_ptr<ID3D11DepthStencilView, Release<ID3D11DepthStencilView>>;
/**
* @brief Owning COM pointer for a keyed mutex on a shared DXGI resource.
*/
using keyed_mutex_t = util::safe_ptr<IDXGIKeyedMutex, Release<IDXGIKeyedMutex>>;
namespace video {
/**
* @brief Owning COM pointer for the D3D11 video device interface.
*/
using device_t = util::safe_ptr<ID3D11VideoDevice, Release<ID3D11VideoDevice>>;
/**
* @brief Owning COM pointer for the D3D11 video context interface.
*/
using ctx_t = util::safe_ptr<ID3D11VideoContext, Release<ID3D11VideoContext>>;
/**
* @brief Owning COM pointer for a D3D11 video processor.
*/
using processor_t = util::safe_ptr<ID3D11VideoProcessor, Release<ID3D11VideoProcessor>>;
/**
* @brief Owning COM pointer for a video processor output view.
*/
using processor_out_t = util::safe_ptr<ID3D11VideoProcessorOutputView, Release<ID3D11VideoProcessorOutputView>>;
/**
* @brief Owning COM pointer for a video processor input view.
*/
using processor_in_t = util::safe_ptr<ID3D11VideoProcessorInputView, Release<ID3D11VideoProcessorInputView>>;
/**
* @brief Owning COM pointer for a video processor capability enumerator.
*/
using processor_enum_t = util::safe_ptr<ID3D11VideoProcessorEnumerator, Release<ID3D11VideoProcessorEnumerator>>;
} // namespace video
class hwdevice_t;
/**
* @brief Cursor position and visibility for the current capture frame.
*/
struct cursor_t {
std::vector<std::uint8_t> img_data; ///< Raw pointer-shape bytes from DXGI output duplication.
std::vector<std::uint8_t> img_data;
DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info; ///< Shape info.
DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info;
int x; ///< X.
int y; ///< Y.
bool visible; ///< Whether the cursor is visible.
int x;
int y;
bool visible;
};
/**
* @brief GPU resources used to render a captured cursor shape.
*/
class gpu_cursor_t {
public:
gpu_cursor_t():
cursor_view {0, 0, 0, 0, 0.0f, 1.0f} {};
/**
* @brief Update the cursor position and display geometry used for rendering.
*
* @param topleft_x Cursor left edge in desktop coordinates.
* @param topleft_y Cursor top edge in desktop coordinates.
* @param display_width Captured display width in pixels.
* @param display_height Captured display height in pixels.
* @param display_rotation DXGI rotation applied to the captured display.
* @param visible Whether the cursor should be visible in the frame.
*/
void set_pos(LONG topleft_x, LONG topleft_y, LONG display_width, LONG display_height, DXGI_MODE_ROTATION display_rotation, bool visible) {
this->topleft_x = topleft_x;
this->topleft_y = topleft_y;
@@ -97,6 +226,13 @@
update_viewport();
}
/**
* @brief Replace the cursor texture and update its render viewport.
*
* @param texture_width Cursor texture width in pixels.
* @param texture_height Cursor texture height in pixels.
* @param texture D3D11 cursor texture to render.
*/
void set_texture(LONG texture_width, LONG texture_height, texture2d_t &&texture) {
this->texture = std::move(texture);
this->texture_width = texture_width;
@@ -104,6 +240,9 @@
update_viewport();
}
/**
* @brief Recalculate the D3D viewport for the cursor texture.
*/
void update_viewport() {
switch (display_rotation) {
case DXGI_MODE_ROTATION_UNSPECIFIED:
@@ -137,50 +276,63 @@
}
}
texture2d_t texture;
LONG texture_width;
texture2d_t texture; ///< D3D11 texture backing the captured frame.
LONG texture_width; ///< Texture width.
LONG texture_height; ///< Texture height.
LONG texture_height;
LONG topleft_x;
LONG topleft_y;
LONG topleft_x; ///< Topleft x.
LONG topleft_y; ///< Topleft y.
LONG display_width; ///< Display width.
LONG display_height; ///< Display height.
LONG display_width;
DXGI_MODE_ROTATION display_rotation; ///< Display rotation.
LONG display_height;
DXGI_MODE_ROTATION display_rotation;
shader_res_t input_res;
shader_res_t input_res; ///< Input res.
D3D11_VIEWPORT cursor_view;
D3D11_VIEWPORT cursor_view; ///< Cursor view.
bool visible; ///< Whether the cursor is visible.
bool visible;
};
/**
* @brief Shared D3D11/DXGI state used by Windows display capture backends.
*/
class display_base_t: public display_t {
public:
/**
* @brief Initialize D3D11 desktop duplication for the selected output.
*
* @param config Configuration values to apply.
* @param display_name Display name.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(const ::video::config_t &config, const std::string &display_name);
capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override;
factory1_t factory;
adapter_t adapter;
output_t output;
device_t device;
device_ctx_t device_ctx;
DXGI_RATIONAL display_refresh_rate;
factory1_t factory; ///< DXGI factory used to enumerate adapters and outputs.
adapter_t adapter; ///< DXGI adapter containing the selected output.
output_t output; ///< DXGI output duplicated for capture.
device_t device; ///< D3D11 device used for desktop duplication capture.
device_ctx_t device_ctx; ///< D3D11 device context used for copy and render operations.
DXGI_RATIONAL display_refresh_rate; ///< Display refresh rate.
int display_refresh_rate_rounded; ///< Display refresh rate rounded.
int display_refresh_rate_rounded;
DXGI_MODE_ROTATION display_rotation = DXGI_MODE_ROTATION_UNSPECIFIED; ///< Display rotation.
int width_before_rotation; ///< Width before rotation.
DXGI_MODE_ROTATION display_rotation = DXGI_MODE_ROTATION_UNSPECIFIED;
int height_before_rotation; ///< Height before rotation.
int width_before_rotation;
int height_before_rotation;
int client_frame_rate; ///< Client frame rate.
DXGI_RATIONAL client_frame_rate_strict; ///< Client frame rate strict.
int client_frame_rate;
DXGI_RATIONAL client_frame_rate_strict;
DXGI_FORMAT capture_format;
D3D_FEATURE_LEVEL feature_level;
DXGI_FORMAT capture_format; ///< Capture format.
D3D_FEATURE_LEVEL feature_level; ///< Feature level.
std::unique_ptr<high_precision_timer> timer = create_high_precision_timer();
std::unique_ptr<high_precision_timer> timer = create_high_precision_timer(); ///< Timer.
/**
* @brief Enumerates supported d3 DKMT SCHEDULINGPRIORITYCLASS options.
*/
typedef enum _D3DKMT_SCHEDULINGPRIORITYCLASS {
D3DKMT_SCHEDULINGPRIORITYCLASS_IDLE, ///< Idle priority class
D3DKMT_SCHEDULINGPRIORITYCLASS_BELOW_NORMAL, ///< Below normal priority class
@@ -188,15 +340,24 @@
D3DKMT_SCHEDULINGPRIORITYCLASS_ABOVE_NORMAL, ///< Above normal priority class
D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH, ///< High priority class
D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME ///< Realtime priority class
} D3DKMT_SCHEDULINGPRIORITYCLASS;
} D3DKMT_SCHEDULINGPRIORITYCLASS; ///< Alias for D3 DKMT SCHEDULINGPRIORITYCLASS.
/**
* @brief Kernel-mode D3DKMT adapter handle.
*/
typedef UINT D3DKMT_HANDLE;
/**
* @brief Win32 D3DKMT adapter-open request structure.
*/
typedef struct _D3DKMT_OPENADAPTERFROMLUID {
LUID AdapterLuid; ///< Adapter luid.
D3DKMT_HANDLE hAdapter; ///< D3DKMT adapter handle used for low-level display queries.
} D3DKMT_OPENADAPTERFROMLUID; ///< Alias for D3 DKMT OPENADAPTERFROMLUID.
LUID AdapterLuid;
D3DKMT_HANDLE hAdapter;
} D3DKMT_OPENADAPTERFROMLUID;
/**
* @brief Win32 D3DKMT WDDM 2.7 capability flags.
*/
typedef struct _D3DKMT_WDDM_2_7_CAPS {
union {
struct
@@ -210,39 +371,111 @@
UINT Value;
};
} D3DKMT_WDDM_2_7_CAPS; ///< Alias for D3 DKMT WDDM 2 7 CAPS.
} D3DKMT_WDDM_2_7_CAPS;
/**
* @brief Win32 D3DKMT adapter information query.
*/
typedef struct _D3DKMT_QUERYADAPTERINFO {
D3DKMT_HANDLE hAdapter; ///< D3DKMT adapter handle used for low-level display queries.
UINT Type; ///< Type.
D3DKMT_HANDLE hAdapter;
UINT Type;
VOID *pPrivateDriverData;
UINT PrivateDriverDataSize;
} D3DKMT_QUERYADAPTERINFO;
VOID *pPrivateDriverData; ///< P private driver data.
UINT PrivateDriverDataSize; ///< Private driver data size.
} D3DKMT_QUERYADAPTERINFO; ///< Alias for D3 DKMT QUERYADAPTERINFO.
const UINT KMTQAITYPE_WDDM_2_7_CAPS = 70;
const UINT KMTQAITYPE_WDDM_2_7_CAPS = 70; ///< Protocol or platform constant for kmtqaitype wddm 2 7 caps.
/**
* @brief Win32 D3DKMT adapter-close request structure.
*/
typedef struct _D3DKMT_CLOSEADAPTER {
D3DKMT_HANDLE hAdapter;
} D3DKMT_CLOSEADAPTER;
D3DKMT_HANDLE hAdapter; ///< D3DKMT adapter handle used for low-level display queries.
} D3DKMT_CLOSEADAPTER; ///< Alias for D3 DKMT CLOSEADAPTER.
/**
* @brief Function pointer for setting D3DKMT process scheduling priority.
*/
typedef NTSTATUS(WINAPI *PD3DKMTSetProcessSchedulingPriorityClass)(HANDLE, D3DKMT_SCHEDULINGPRIORITYCLASS);
/**
* @brief Function pointer for opening a D3DKMT adapter by LUID.
*/
typedef NTSTATUS(WINAPI *PD3DKMTOpenAdapterFromLuid)(D3DKMT_OPENADAPTERFROMLUID *);
/**
* @brief Function pointer for querying D3DKMT adapter information.
*/
typedef NTSTATUS(WINAPI *PD3DKMTQueryAdapterInfo)(D3DKMT_QUERYADAPTERINFO *);
/**
* @brief Function pointer for closing a D3DKMT adapter handle.
*/
typedef NTSTATUS(WINAPI *PD3DKMTCloseAdapter)(D3DKMT_CLOSEADAPTER *);
/**
* @brief Report whether the active display mode is HDR.
*
* @return True when the active display mode is HDR.
*/
virtual bool is_hdr() override;
/**
* @brief Read HDR metadata for the active display mode.
*
* @param metadata Output structure populated with HDR metadata.
* @return True when HDR metadata was written to `metadata`.
*/
virtual bool get_hdr_metadata(SS_HDR_METADATA &metadata) override;
/**
* @brief Convert a DXGI format enum to a diagnostic string.
*
* @param format Pixel, audio, or protocol format being converted.
* @return Static string describing the DXGI format.
*/
const char *dxgi_format_to_string(DXGI_FORMAT format);
/**
* @brief Convert a DXGI colorspace enum to a diagnostic string.
*
* @param type DXGI colorspace value reported by the output.
* @return Static string describing the colorspace.
*/
const char *colorspace_to_string(DXGI_COLOR_SPACE_TYPE type);
/**
* @brief Get supported capture formats.
*
* @return Capture formats supported by this display backend.
*/
virtual std::vector<DXGI_FORMAT> get_supported_capture_formats() = 0;
protected:
/**
* @brief Get pixel pitch.
*
* @return Bytes per pixel for the active capture format.
*/
int get_pixel_pitch() {
return (capture_format == DXGI_FORMAT_R16G16B16A16_FLOAT) ? 8 : 4;
}
/**
* @brief Capture a display frame into the provided image object.
*
* @param pull_free_image_cb Callback that provides an available image buffer.
* @param img_out Captured image buffer returned to the streaming pipeline.
* @param timeout Maximum time to wait for the operation.
* @param cursor_visible Cursor visible.
* @return Capture status reported to the streaming pipeline.
*/
virtual capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) = 0;
/**
* @brief Release resources associated with the last captured snapshot.
*
* @return Capture status after releasing the current snapshot.
*/
virtual capture_e release_snapshot() = 0;
/**
* @brief Finish cursor composition into a RAM-backed image.
*
* @param img Image or frame object to read from or populate.
* @param dummy Unused placeholder required by the interface signature.
* @return Capture status after finalizing the captured image.
*/
virtual int complete_img(img_t *img, bool dummy) = 0;
};
@@ -252,15 +485,44 @@
*/
class display_ram_t: public display_base_t {
public:
/**
* @brief Allocate an image buffer compatible with this display backend.
*
* @return Allocated img object, or null when unavailable.
*/
std::shared_ptr<img_t> alloc_img() override;
/**
* @brief Populate a fallback image when real capture data is unavailable.
*
* @param img Image or frame object to read from or populate.
* @return Capture status reported to the streaming pipeline.
*/
int dummy_img(img_t *img) override;
/**
* @brief Finish cursor composition into a GPU-backed image.
*
* @param img Image or frame object to read from or populate.
* @param dummy Unused placeholder required by the interface signature.
* @return Capture status after finalizing the captured image.
*/
int complete_img(img_t *img, bool dummy) override;
/**
* @brief Get supported capture formats.
*
* @return Capture formats supported by this display backend.
*/
std::vector<DXGI_FORMAT> get_supported_capture_formats() override;
/**
* @brief Create AVCodec encode device.
*
* @param pix_fmt Sunshine pixel format to convert or allocate for.
* @return Constructed AVCodec encode device object.
*/
std::unique_ptr<avcodec_encode_device_t> make_avcodec_encode_device(pix_fmt_e pix_fmt) override;
D3D11_MAPPED_SUBRESOURCE img_info; ///< CPU mapping information for the captured texture.
texture2d_t texture; ///< D3D11 texture backing the captured frame.
D3D11_MAPPED_SUBRESOURCE img_info;
texture2d_t texture;
};
/**
@@ -268,18 +530,53 @@
*/
class display_vram_t: public display_base_t, public std::enable_shared_from_this<display_vram_t> {
public:
/**
* @brief Allocate an image buffer compatible with this display backend.
*
* @return Allocated img object, or null when unavailable.
*/
std::shared_ptr<img_t> alloc_img() override;
/**
* @brief Populate a fallback image when real capture data is unavailable.
*
* @param img_base Image buffer populated with a fallback frame.
* @return Capture status reported to the streaming pipeline.
*/
int dummy_img(img_t *img_base) override;
/**
* @brief Finalize a VRAM image after capture or dummy-frame generation.
*
* @param img_base Image buffer whose D3D resources are finalized.
* @param dummy Unused placeholder required by the interface signature.
* @return Capture status after finalizing the captured image.
*/
int complete_img(img_t *img_base, bool dummy) override;
/**
* @brief Get supported capture formats.
*
* @return Capture formats supported by this display backend.
*/
std::vector<DXGI_FORMAT> get_supported_capture_formats() override;
bool is_codec_supported(std::string_view name, const ::video::config_t &config) override;
/**
* @brief Create AVCodec encode device.
*
* @param pix_fmt Sunshine pixel format to convert or allocate for.
* @return Constructed AVCodec encode device object.
*/
std::unique_ptr<avcodec_encode_device_t> make_avcodec_encode_device(pix_fmt_e pix_fmt) override;
/**
* @brief Create NVENC encode device.
*
* @param pix_fmt Sunshine pixel format to convert or allocate for.
* @return Constructed NVENC encode device object.
*/
std::unique_ptr<nvenc_encode_device_t> make_nvenc_encode_device(pix_fmt_e pix_fmt) override;
std::atomic<uint32_t> next_image_id; ///< Next image ID.
std::atomic<uint32_t> next_image_id;
};
/**
@@ -287,13 +584,39 @@
*/
class duplication_t {
public:
dup_t dup; ///< Desktop Duplication capture session.
dup_t dup;
bool has_frame {};
std::chrono::steady_clock::time_point last_protected_content_warning_time {};
bool has_frame {}; ///< Whether a frame is currently held by the duplication object.
std::chrono::steady_clock::time_point last_protected_content_warning_time {}; ///< Last protected content warning time.
/**
* @brief Initialize WGC capture for the selected display.
*
* @param display Display object or identifier associated with the operation.
* @param config Configuration values to apply.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(display_base_t *display, const ::video::config_t &config);
/**
* @brief Acquire the next frame from the Windows capture backend.
*
* @param frame_info Frame info.
* @param timeout Maximum time to wait for the operation.
* @param res_p Res p.
* @return Capture status for the frame acquisition attempt.
*/
capture_e next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p);
/**
* @brief Reset the object to its initial empty state.
*
* @param dup_p Dup p.
* @return Reset status.
*/
capture_e reset(dup_t::pointer dup_p = dup_t::pointer());
/**
* @brief Release resources associated with frame.
*
* @return Capture status after releasing the current frame.
*/
capture_e release_frame();
~duplication_t();
@@ -304,12 +627,33 @@
*/
class display_ddup_ram_t: public display_ram_t {
public:
/**
* @brief Initialize shared D3D capture resources for a RAM frame.
*
* @param config Configuration values to apply.
* @param display_name Display name.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(const ::video::config_t &config, const std::string &display_name);
/**
* @brief Capture a display frame into the provided image object.
*
* @param pull_free_image_cb Callback that provides an available image buffer.
* @param img_out Captured image buffer returned to the streaming pipeline.
* @param timeout Maximum time to wait for the operation.
* @param cursor_visible Cursor visible.
* @return Capture status reported to the streaming pipeline.
*/
capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override;
/**
* @brief Release resources associated with the last captured snapshot.
*
* @return Capture status after releasing the current snapshot.
*/
capture_e release_snapshot() override;
duplication_t dup; ///< Desktop Duplication session used to acquire frames.
duplication_t dup;
cursor_t cursor;
cursor_t cursor; ///< Cursor.
};
/**
@@ -317,26 +661,47 @@
*/
class display_ddup_vram_t: public display_vram_t {
public:
/**
* @brief Initialize D3D cursor rendering resources for RAM capture.
*
* @param config Configuration values to apply.
* @param display_name Display name.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(const ::video::config_t &config, const std::string &display_name);
/**
* @brief Capture a display frame into the provided image object.
*
* @param pull_free_image_cb Callback that provides an available image buffer.
* @param img_out Captured image buffer returned to the streaming pipeline.
* @param timeout Maximum time to wait for the operation.
* @param cursor_visible Cursor visible.
* @return Capture status reported to the streaming pipeline.
*/
capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override;
/**
* @brief Release resources associated with the last captured snapshot.
*
* @return Capture status after releasing the current snapshot.
*/
capture_e release_snapshot() override;
duplication_t dup; ///< Desktop Duplication session used to acquire frames.
duplication_t dup;
sampler_state_t sampler_linear; ///< Sampler linear.
sampler_state_t sampler_linear;
blend_t blend_alpha; ///< Blend alpha.
blend_t blend_invert; ///< Blend invert.
blend_t blend_disable; ///< Blend disable.
blend_t blend_alpha;
blend_t blend_invert;
blend_t blend_disable;
ps_t cursor_ps; ///< Cursor ps.
vs_t cursor_vs; ///< Cursor vs.
ps_t cursor_ps;
vs_t cursor_vs;
gpu_cursor_t cursor_alpha; ///< Cursor alpha.
gpu_cursor_t cursor_xor; ///< Cursor xor.
gpu_cursor_t cursor_alpha;
gpu_cursor_t cursor_xor;
texture2d_t old_surface_delayed_destruction;
std::chrono::steady_clock::time_point old_surface_timestamp;
std::variant<std::monostate, texture2d_t, std::shared_ptr<platf::img_t>> last_frame_variant;
texture2d_t old_surface_delayed_destruction; ///< Old surface delayed destruction.
std::chrono::steady_clock::time_point old_surface_timestamp; ///< Old surface timestamp.
std::variant<std::monostate, texture2d_t, std::shared_ptr<platf::img_t>> last_frame_variant; ///< Last frame variant.
};
/**
@@ -358,10 +723,36 @@
wgc_capture_t();
~wgc_capture_t();
/**
* @brief Initialize D3D cursor rendering resources for GPU capture.
*
* @param display Display object or identifier associated with the operation.
* @param config Configuration values to apply.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(display_base_t *display, const ::video::config_t &config);
/**
* @brief Acquire the next frame from the Windows capture backend.
*
* @param timeout Maximum time to wait for the operation.
* @param out Output object populated by the operation.
* @param out_time QPC timestamp associated with the acquired frame.
* @return Capture status for the frame acquisition attempt.
*/
capture_e next_frame(std::chrono::milliseconds timeout, ID3D11Texture2D **out, uint64_t &out_time);
/**
* @brief Release resources associated with frame.
*
* @return Capture status after releasing the current frame.
*/
capture_e release_frame();
/**
* @brief Enable or disable cursor composition in Windows.Graphics.Capture frames.
*
* @param cursor_visible Whether the cursor should be included in captured frames.
* @return Zero when the cursor visibility state was accepted by the capture session.
*/
int set_cursor_visible(bool cursor_visible);
int set_cursor_visible(bool);
};
/**
@@ -371,7 +762,28 @@
wgc_capture_t dup;
public:
/**
* @brief Initialize shared D3D capture resources for a GPU frame.
*
* @param config Configuration values to apply.
* @param display_name Display name.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(const ::video::config_t &config, const std::string &display_name);
/**
* @brief Capture a display frame into the provided image object.
*
* @param pull_free_image_cb Callback that provides an available image buffer.
* @param img_out Captured image buffer returned to the streaming pipeline.
* @param timeout Maximum time to wait for the operation.
* @param cursor_visible Cursor visible.
* @return Capture status reported to the streaming pipeline.
*/
capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override;
/**
* @brief Release resources associated with the last captured snapshot.
*
* @return Capture status after releasing the current snapshot.
*/
capture_e release_snapshot() override;
};
@@ -383,8 +795,29 @@
wgc_capture_t dup;
public:
/**
* @brief Initialize Windows Graphics Capture frame-pool resources.
*
* @param config Configuration values to apply.
* @param display_name Display name.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(const ::video::config_t &config, const std::string &display_name);
/**
* @brief Capture a display frame into the provided image object.
*
* @param pull_free_image_cb Callback that provides an available image buffer.
* @param img_out Captured image buffer returned to the streaming pipeline.
* @param timeout Maximum time to wait for the operation.
* @param cursor_visible Cursor visible.
* @return Capture status reported to the streaming pipeline.
*/
capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override;
/**
* @brief Release resources associated with the last captured snapshot.
*
* @return Capture status after releasing the current snapshot.
*/
capture_e release_snapshot() override;
};
} // namespace platf::dxgi
src/platform/windows/display_base.cpp +14 −2
@@ -19,9 +19,15 @@
// We have to include boost/process/v1.hpp before display.h due to WinSock.h,
// but that prevents the definition of NTSTATUS so we must define it ourself.
/**
* @brief Windows NT status code returned by native APIs.
*/
typedef long NTSTATUS;
// Definition from the WDK's d3dkmthk.h
/**
* @brief Enumerates supported d3 DKMT GPU PREFERENCE QUERY STATE options.
*/
typedef enum _D3DKMT_GPU_PREFERENCE_QUERY_STATE: DWORD {
D3DKMT_GPU_PREFERENCE_STATE_UNINITIALIZED, ///< The GPU preference isn't initialized.
D3DKMT_GPU_PREFERENCE_STATE_HIGH_PERFORMANCE, ///< The highest performing GPU is preferred.
@@ -29,7 +35,7 @@
D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED, ///< A GPU preference isn't specified.
D3DKMT_GPU_PREFERENCE_STATE_NOT_FOUND, ///< A GPU preference isn't found.
D3DKMT_GPU_PREFERENCE_STATE_USER_SPECIFIED_GPU ///< A specific GPU is preferred.
} D3DKMT_GPU_PREFERENCE_QUERY_STATE;
} D3DKMT_GPU_PREFERENCE_QUERY_STATE; ///< Alias for D3 DKMT GPU PREFERENCE QUERY STATE.
#include "display.h"
#include "misc.h"
@@ -354,6 +360,8 @@
* @param adapter The DXGI adapter to use for capture.
* @param output The DXGI output to capture.
* @param enumeration_only Specifies whether this test is occurring for display enumeration.
*
* @return True when Desktop Duplication can capture the requested output.
*/
bool test_dxgi_duplication(adapter_t &adapter, output_t &output, bool enumeration_only) {
D3D_FEATURE_LEVEL featureLevels[] {
@@ -819,6 +827,9 @@
return true;
}
/**
* @brief Names for DXGI_FORMAT values used in diagnostic logging.
*/
const char *format_str[] = {
"DXGI_FORMAT_UNKNOWN",
"DXGI_FORMAT_R32G32B32A32_TYPELESS",
@@ -1109,7 +1120,8 @@
}
/**
* @brief Returns if GPUs/drivers have changed since the last call to this function.
* @brief Check whether DXGI reports that adapter or driver enumeration is stale.
*
* @return `true` if a change has occurred or if it is unknown whether a change occurred.
*/
bool needs_encoder_reenumeration() {
src/platform/windows/display_ram.cpp +37 −0
@@ -12,13 +12,25 @@
}
namespace platf::dxgi {
/**
* @brief Captured frame buffer shared between capture and encode stages.
*/
struct img_t: public ::platf::img_t {
/**
* @brief Destroy the RAM-backed DXGI image.
*/
~img_t() override {
delete[] data;
data = nullptr;
}
};
/**
* @brief Blend a monochrome cursor shape into a RAM frame.
*
* @param cursor Cursor image or visibility state to composite.
* @param img Image or frame object to read from or populate.
*/
void blend_cursor_monochrome(const cursor_t &cursor, img_t &img) {
int height = cursor.shape_info.Height / 2;
int width = cursor.shape_info.Width;
@@ -81,6 +93,12 @@
}
}
/**
* @brief Alpha-blend a color cursor pixel into a captured RAM frame.
*
* @param img_pixel_p Pixel in the destination frame.
* @param cursor_pixel ARGB cursor pixel to blend.
*/
void apply_color_alpha(int *img_pixel_p, int cursor_pixel) {
auto colors_out = (std::uint8_t *) &cursor_pixel;
auto colors_in = (std::uint8_t *) img_pixel_p;
@@ -96,6 +114,12 @@
}
}
/**
* @brief Apply a masked-color cursor pixel into a captured RAM frame.
*
* @param img_pixel_p Pixel in the destination frame.
* @param cursor_pixel ARGB cursor pixel with masked-cursor semantics.
*/
void apply_color_masked(int *img_pixel_p, int cursor_pixel) {
// TODO: When use of IDXGIOutput5 is implemented, support different color formats
auto alpha = ((std::uint8_t *) &cursor_pixel)[3];
@@ -106,6 +130,13 @@
}
}
/**
* @brief Blend a color cursor shape into a RAM frame.
*
* @param cursor Cursor image or visibility state to composite.
* @param img Image or frame object to read from or populate.
* @param masked Whether the cursor image uses an AND/XOR mask.
*/
void blend_cursor_color(const cursor_t &cursor, img_t &img, const bool masked) {
int height = cursor.shape_info.Height;
int width = cursor.shape_info.Width;
@@ -152,6 +183,12 @@
}
}
/**
* @brief Blend the current cursor image into a RAM frame.
*
* @param cursor Cursor image or visibility state to composite.
* @param img Image or frame object to read from or populate.
*/
void blend_cursor(const cursor_t &cursor, img_t &img) {
switch (cursor.shape_info.Type) {
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR:
src/platform/windows/display_vram.cpp +293 −84
@@ -31,6 +31,10 @@
#include "utf_utils.h"
#if !defined(SUNSHINE_SHADERS_DIR) // for testing this needs to be defined in cmake as we don't do an install
/**
* @def SUNSHINE_SHADERS_DIR
* @brief Macro for SUNSHINE SHADERS DIR.
*/
#define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/directx"
#endif
namespace platf {
@@ -41,10 +45,20 @@
av_frame_free(&frame);
}
/**
* @brief FFmpeg hardware frame pointer released with `av_frame_free`.
*/
using frame_t = util::safe_ptr<AVFrame, free_frame>;
namespace platf::dxgi {
/**
* @brief Create a buffer object or message.
*
* @param device D3D, audio, or platform device used by the operation.
* @param t Initial value used to populate the GPU buffer.
* @return Constructed buffer object.
*/
template<class T>
buf_t make_buffer(device_t::pointer device, const T &t) {
static_assert(sizeof(T) % 16 == 0, "Buffer needs to be aligned on a 16-byte alignment");
@@ -69,6 +83,14 @@
return buf_t {buf_p};
}
/**
* @brief Create a blend object or message.
*
* @param device D3D, audio, or platform device used by the operation.
* @param enable Whether the blend state should enable blending.
* @param invert Whether the blend state should invert the color channels.
* @return Constructed blend object.
*/
blend_t make_blend(device_t::pointer device, bool enable, bool invert) {
D3D11_BLEND_DESC bdesc {};
auto &rt = bdesc.RenderTarget[0];
@@ -103,53 +125,56 @@
return blend;
}
blob_t convert_yuv420_packed_uv_type0_ps_hlsl; ///< Convert yuv420 packed uv type0 ps hlsl.
blob_t convert_yuv420_packed_uv_type0_ps_hlsl;
blob_t convert_yuv420_packed_uv_type0_ps_linear_hlsl; ///< Convert yuv420 packed uv type0 ps linear hlsl.
blob_t convert_yuv420_packed_uv_type0_ps_perceptual_quantizer_hlsl; ///< Convert yuv420 packed uv type0 ps perceptual quantizer hlsl.
blob_t convert_yuv420_packed_uv_type0_vs_hlsl; ///< Convert yuv420 packed uv type0 vs hlsl.
blob_t convert_yuv420_packed_uv_type0s_ps_hlsl; ///< Convert yuv420 packed uv type0s ps hlsl.
blob_t convert_yuv420_packed_uv_type0s_ps_linear_hlsl; ///< Convert yuv420 packed uv type0s ps linear hlsl.
blob_t convert_yuv420_packed_uv_type0s_ps_perceptual_quantizer_hlsl; ///< Convert yuv420 packed uv type0s ps perceptual quantizer hlsl.
blob_t convert_yuv420_packed_uv_type0s_vs_hlsl; ///< Convert yuv420 packed uv type0s vs hlsl.
blob_t convert_yuv420_planar_y_ps_hlsl; ///< Convert yuv420 planar y ps hlsl.
blob_t convert_yuv420_planar_y_ps_linear_hlsl; ///< Convert yuv420 planar y ps linear hlsl.
blob_t convert_yuv420_planar_y_ps_perceptual_quantizer_hlsl; ///< Convert yuv420 planar y ps perceptual quantizer hlsl.
blob_t convert_yuv420_planar_y_vs_hlsl; ///< Convert yuv420 planar y vs hlsl.
blob_t convert_yuv444_packed_ayuv_ps_hlsl; ///< Convert YUV444 packed ayuv ps hlsl.
blob_t convert_yuv444_packed_ayuv_ps_linear_hlsl; ///< Convert YUV444 packed ayuv ps linear hlsl.
blob_t convert_yuv444_packed_vs_hlsl; ///< Convert YUV444 packed vs hlsl.
blob_t convert_yuv444_planar_ps_hlsl; ///< Convert YUV444 planar ps hlsl.
blob_t convert_yuv444_planar_ps_linear_hlsl; ///< Convert YUV444 planar ps linear hlsl.
blob_t convert_yuv444_planar_ps_perceptual_quantizer_hlsl; ///< Convert YUV444 planar ps perceptual quantizer hlsl.
blob_t convert_yuv444_packed_y410_ps_hlsl; ///< Convert YUV444 packed y410 ps hlsl.
blob_t convert_yuv444_packed_y410_ps_linear_hlsl; ///< Convert YUV444 packed y410 ps linear hlsl.
blob_t convert_yuv444_packed_y410_ps_perceptual_quantizer_hlsl; ///< Convert YUV444 packed y410 ps perceptual quantizer hlsl.
blob_t convert_yuv444_planar_vs_hlsl; ///< Convert YUV444 planar vs hlsl.
blob_t cursor_ps_hlsl; ///< Cursor ps hlsl.
blob_t cursor_ps_normalize_white_hlsl; ///< Cursor ps normalize white hlsl.
blob_t cursor_vs_hlsl; ///< Cursor vs hlsl.
blob_t convert_yuv420_packed_uv_type0_ps_linear_hlsl;
blob_t convert_yuv420_packed_uv_type0_ps_perceptual_quantizer_hlsl;
blob_t convert_yuv420_packed_uv_type0_vs_hlsl;
blob_t convert_yuv420_packed_uv_type0s_ps_hlsl;
blob_t convert_yuv420_packed_uv_type0s_ps_linear_hlsl;
blob_t convert_yuv420_packed_uv_type0s_ps_perceptual_quantizer_hlsl;
blob_t convert_yuv420_packed_uv_type0s_vs_hlsl;
blob_t convert_yuv420_planar_y_ps_hlsl;
blob_t convert_yuv420_planar_y_ps_linear_hlsl;
blob_t convert_yuv420_planar_y_ps_perceptual_quantizer_hlsl;
blob_t convert_yuv420_planar_y_vs_hlsl;
blob_t convert_yuv444_packed_ayuv_ps_hlsl;
blob_t convert_yuv444_packed_ayuv_ps_linear_hlsl;
blob_t convert_yuv444_packed_vs_hlsl;
blob_t convert_yuv444_planar_ps_hlsl;
blob_t convert_yuv444_planar_ps_linear_hlsl;
blob_t convert_yuv444_planar_ps_perceptual_quantizer_hlsl;
blob_t convert_yuv444_packed_y410_ps_hlsl;
blob_t convert_yuv444_packed_y410_ps_linear_hlsl;
blob_t convert_yuv444_packed_y410_ps_perceptual_quantizer_hlsl;
blob_t convert_yuv444_planar_vs_hlsl;
blob_t cursor_ps_hlsl;
blob_t cursor_ps_normalize_white_hlsl;
blob_t cursor_vs_hlsl;
/**
* @brief D3D-backed captured image and duplication metadata.
*/
struct img_d3d_t: public platf::img_t {
// These objects are owned by the display_t's ID3D11Device
texture2d_t capture_texture;
render_target_t capture_rt;
keyed_mutex_t capture_mutex;
texture2d_t capture_texture; ///< Capture texture.
render_target_t capture_rt; ///< Capture rt.
keyed_mutex_t capture_mutex; ///< Capture mutex.
// This is the shared handle used by hwdevice_t to open capture_texture
HANDLE encoder_texture_handle = {};
HANDLE encoder_texture_handle = {}; ///< Encoder texture handle.
// Set to true if the image corresponds to a dummy texture used prior to
// the first successful capture of a desktop frame
bool dummy = false; ///< Whether this image is a dummy placeholder.
bool dummy = false;
// Set to true if the image is blank (contains no content at all, including a cursor)
bool blank = true; ///< Whether the texture currently contains a blank frame.
bool blank = true;
// Unique identifier for this image
uint32_t id = 0;
uint32_t id = 0; ///< Unique identifier used to cache encoder resources for this image.
// DXGI format of this image texture
DXGI_FORMAT format; ///< DXGI format of the captured texture.
DXGI_FORMAT format;
virtual ~img_d3d_t() override {
if (encoder_texture_handle) {
@@ -158,19 +183,33 @@
};
};
/**
* @brief Keyed-mutex guard used while sharing a D3D texture.
*/
struct texture_lock_helper {
keyed_mutex_t _mutex; ///< D3D keyed mutex acquired for shared-texture access.
keyed_mutex_t _mutex;
bool _locked = false;
bool _locked = false; ///< Whether the keyed mutex is currently locked.
texture_lock_helper(const texture_lock_helper &) = delete;
texture_lock_helper &operator=(const texture_lock_helper &) = delete;
/**
* @brief Move a keyed-mutex lock helper while preserving lock ownership.
*
* @param other Helper whose acquired keyed mutex is transferred.
*/
texture_lock_helper(texture_lock_helper &&other) {
_mutex.reset(other._mutex.release());
_locked = other._locked;
other._locked = false;
}
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @param other Source object whose state is copied or moved into this object.
* @return Reference or value produced by the operator.
*/
texture_lock_helper &operator=(texture_lock_helper &&other) {
if (_locked) {
_mutex->ReleaseSync(0);
@@ -181,6 +220,11 @@
return *this;
}
/**
* @brief Acquire a D3D keyed mutex for scoped texture access.
*
* @param mutex Keyed mutex to acquire for texture access.
*/
texture_lock_helper(IDXGIKeyedMutex *mutex):
_mutex(mutex) {
if (_mutex) {
@@ -194,6 +238,11 @@
}
}
/**
* @brief Acquire the underlying lock or keyed mutex.
*
* @return True when the keyed mutex is acquired.
*/
bool lock() {
if (_locked) {
return true;
@@ -208,6 +257,13 @@
}
};
/**
* @brief Create cursor xor image.
*
* @param img_data Raw pointer-shape bytes returned by DXGI.
* @param shape_info DXGI metadata describing the pointer shape.
* @return Constructed cursor xor image object.
*/
util::buffer_t<std::uint8_t> make_cursor_xor_image(const util::buffer_t<std::uint8_t> &img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info) {
constexpr std::uint32_t inverted = 0xFFFFFFFF;
constexpr std::uint32_t transparent = 0;
@@ -277,6 +333,13 @@
return cursor_img;
}
/**
* @brief Create cursor alpha image.
*
* @param img_data Raw pointer-shape bytes returned by DXGI.
* @param shape_info DXGI metadata describing the pointer shape.
* @return Constructed cursor alpha image object.
*/
util::buffer_t<std::uint8_t> make_cursor_alpha_image(const util::buffer_t<std::uint8_t> &img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info) {
constexpr std::uint32_t black = 0xFF000000;
constexpr std::uint32_t white = 0xFFFFFFFF;
@@ -350,6 +413,14 @@
return cursor_img;
}
/**
* @brief Compile an HLSL shader from source text.
*
* @param file Optional stdio file handle connected to the child process.
* @param entrypoint Function entry point to resolve from the library.
* @param shader_model Shader model.
* @return Compiled shader blob, or an empty blob when compilation fails.
*/
blob_t compile_shader(LPCSTR file, LPCSTR entrypoint, LPCSTR shader_model) {
blob_t::pointer msg_p = nullptr;
blob_t::pointer compiled_p;
@@ -376,16 +447,37 @@
return blob_t {compiled_p};
}
/**
* @brief Compile an HLSL pixel shader from source text.
*
* @param file Optional stdio file handle connected to the child process.
* @return Compiled pixel shader object, or an empty handle when compilation fails.
*/
blob_t compile_pixel_shader(LPCSTR file) {
return compile_shader(file, "main_ps", "ps_5_0");
}
/**
* @brief Compile an HLSL vertex shader from source text.
*
* @param file Optional stdio file handle connected to the child process.
* @return Compiled vertex shader object, or an empty handle when compilation fails.
*/
blob_t compile_vertex_shader(LPCSTR file) {
return compile_shader(file, "main_vs", "vs_5_0");
}
/**
* @brief Shared D3D11 conversion resources for AVCodec and NVENC encode devices.
*/
class d3d_base_encode_device final {
public:
/**
* @brief Convert a captured D3D image into encoder input textures.
*
* @param img_base D3D image supplied by the capture backend.
* @return Conversion status.
*/
int convert(platf::img_t &img_base) {
// Garbage collect mapped capture images whose weak references have expired
for (auto it = img_ctx_map.begin(); it != img_ctx_map.end();) {
@@ -457,5 +549,10 @@
return 0;
}
/**
* @brief Apply the configured colorspace metadata to the active frame.
*
* @param colorspace Colorimetry information used for conversion or encoding.
*/
void apply_colorspace(const ::video::sunshine_colorspace_t &colorspace) {
auto color_vectors = ::video::color_vectors_from_colorspace(colorspace, true);
@@ -482,6 +579,14 @@
this->color_matrix = std::move(color_matrix);
}
/**
* @brief Create D3D11 output textures, views, and shaders for frame conversion.
*
* @param frame_texture Frame texture.
* @param width Frame or display width in pixels.
* @param height Frame or display height in pixels.
* @return 0 when output resources are initialized; nonzero on D3D failure.
*/
int init_output(ID3D11Texture2D *frame_texture, int width, int height) {
// The underlying frame pool owns the texture, so we must reference it for ourselves
frame_texture->AddRef();
@@ -489,15 +594,19 @@
HRESULT status = S_OK;
#ifndef DOXYGEN
#define create_vertex_shader_helper(x, y) \
if (FAILED(status = device->CreateVertexShader(x->GetBufferPointer(), x->GetBufferSize(), nullptr, &y))) { \
BOOST_LOG(error) << "Failed to create vertex shader " << #x << ": " << util::log_hex(status); \
return -1; \
}
#endif
#ifndef DOXYGEN
#define create_pixel_shader_helper(x, y) \
if (FAILED(status = device->CreatePixelShader(x->GetBufferPointer(), x->GetBufferSize(), nullptr, &y))) { \
BOOST_LOG(error) << "Failed to create pixel shader " << #x << ": " << util::log_hex(status); \
return -1; \
}
#endif
#define create_vertex_shader_helper(x, y) \
if (FAILED(status = device->CreateVertexShader(x->GetBufferPointer(), x->GetBufferSize(), nullptr, &y))) { \
BOOST_LOG(error) << "Failed to create vertex shader " << #x << ": " << util::log_hex(status); \
return -1; \
}
#define create_pixel_shader_helper(x, y) \
if (FAILED(status = device->CreatePixelShader(x->GetBufferPointer(), x->GetBufferSize(), nullptr, &y))) { \
BOOST_LOG(error) << "Failed to create pixel shader " << #x << ": " << util::log_hex(status); \
return -1; \
}
const bool downscaling = display->width > width || display->height > height;
@@ -709,6 +818,14 @@
return 0;
}
/**
* @brief Initialize shared D3D conversion resources for the encoder.
*
* @param display Display object or identifier associated with the operation.
* @param adapter_p Adapter p.
* @param pix_fmt Sunshine pixel format to convert or allocate for.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(std::shared_ptr<platf::display_t> display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) {
switch (pix_fmt) {
case pix_fmt_e::nv12:
@@ -823,17 +940,23 @@
return 0;
}
/**
* @brief D3D texture and format information for encoder input.
*/
struct encoder_img_ctx_t {
// Used to determine if the underlying texture changes.
// Not safe for actual use by the encoder!
texture2d_t::const_pointer capture_texture_p;
texture2d_t::const_pointer capture_texture_p; ///< Capture texture p.
texture2d_t encoder_texture; ///< Encoder texture.
texture2d_t encoder_texture;
shader_res_t encoder_input_res;
keyed_mutex_t encoder_mutex;
shader_res_t encoder_input_res; ///< Encoder input res.
keyed_mutex_t encoder_mutex; ///< Encoder mutex.
std::weak_ptr<const platf::img_t> img_weak;
std::weak_ptr<const platf::img_t> img_weak; ///< Captured image lifetime tracked for cache cleanup.
/**
* @brief Reset the object to its initial empty state.
*/
void reset() {
capture_texture_p = nullptr;
encoder_texture.reset();
@@ -843,6 +966,13 @@
}
};
/**
* @brief Initialize encoder-side D3D resources for a captured image.
*
* @param img Image or frame object to read from or populate.
* @param img_ctx Cached encoder resources associated with the image ID.
* @return 0 when the shared texture is opened and bound; nonzero on D3D failure.
*/
int initialize_image_context(const img_d3d_t &img, encoder_img_ctx_t &img_ctx) {
// If we've already opened the shared texture, we're done
if (img_ctx.encoder_texture && img.capture_texture.get() == img_ctx.capture_texture_p) {
@@ -888,6 +1018,11 @@
return 0;
}
/**
* @brief Create black texture for rtv clear.
*
* @return Created black texture for rtv clear object or status.
*/
shader_res_t create_black_texture_for_rtv_clear() {
constexpr auto width = 32;
constexpr auto height = 32;
@@ -922,63 +1057,88 @@
return resource_view;
}
::video::color_t *color_p;
::video::color_t *color_p; ///< Color p.
buf_t subsample_offset;
buf_t color_matrix;
buf_t subsample_offset; ///< Subsample offset.
buf_t color_matrix; ///< Color matrix.
blend_t blend_disable;
sampler_state_t sampler_linear;
blend_t blend_disable; ///< Blend disable.
sampler_state_t sampler_linear; ///< Sampler linear.
render_target_t out_Y_or_YUV_rtv;
render_target_t out_UV_rtv;
bool rtvs_cleared = false;
render_target_t out_Y_or_YUV_rtv; ///< Out y or YUV rtv.
render_target_t out_UV_rtv; ///< Out UV rtv.
bool rtvs_cleared = false; ///< Whether render-target views have been cleared for the frame.
// d3d_img_t::id -> encoder_img_ctx_t
// These store the encoder textures for each img_t that passes through
// convert(). We can't store them in the img_t itself because it is shared
// amongst multiple hwdevice_t objects (and therefore multiple ID3D11Devices).
std::map<uint32_t, encoder_img_ctx_t> img_ctx_map;
std::map<uint32_t, encoder_img_ctx_t> img_ctx_map; ///< Encoder resources cached by captured image ID.
std::shared_ptr<display_base_t> display;
std::shared_ptr<display_base_t> display; ///< Display capture backend that supplies source textures.
vs_t convert_Y_or_YUV_vs;
ps_t convert_Y_or_YUV_ps;
vs_t convert_Y_or_YUV_vs; ///< Convert y or YUV vs.
ps_t convert_Y_or_YUV_ps; ///< Convert y or YUV ps.
ps_t convert_Y_or_YUV_fp16_ps; ///< Convert y or YUV fp16 ps.
ps_t convert_Y_or_YUV_fp16_ps;
vs_t convert_UV_vs;
ps_t convert_UV_ps;
ps_t convert_UV_fp16_ps;
vs_t convert_UV_vs; ///< Convert UV vs.
ps_t convert_UV_ps; ///< Convert UV ps.
ps_t convert_UV_fp16_ps; ///< Convert UV fp16 ps.
std::array<D3D11_VIEWPORT, 3> out_Y_or_YUV_viewports; ///< Out y or YUV viewports.
std::array<D3D11_VIEWPORT, 3> out_Y_or_YUV_viewports;
std::array<D3D11_VIEWPORT, 3> out_Y_or_YUV_viewports_for_clear; ///< Out y or YUV viewports for clear.
D3D11_VIEWPORT out_UV_viewport; ///< Out UV viewport.
D3D11_VIEWPORT out_UV_viewport_for_clear; ///< Out UV viewport for clear.
std::array<D3D11_VIEWPORT, 3> out_Y_or_YUV_viewports_for_clear;
D3D11_VIEWPORT out_UV_viewport;
D3D11_VIEWPORT out_UV_viewport_for_clear;
DXGI_FORMAT format;
DXGI_FORMAT format; ///< DXGI format required by the encoder input texture.
device_t device; ///< D3D11 device used for encoder-side texture conversion.
device_t device;
device_ctx_t device_ctx;
device_ctx_t device_ctx; ///< D3D11 device context used to issue conversion commands.
texture2d_t output_texture;
texture2d_t output_texture; ///< Output texture.
};
/**
* @brief D3D11 encode device that exposes captured textures to FFmpeg AVCodec.
*/
class d3d_avcodec_encode_device_t: public avcodec_encode_device_t {
public:
/**
* @brief Initialize the D3D11 AVCodec encode device.
*
* @param display Display object or identifier associated with the operation.
* @param adapter_p Adapter p.
* @param pix_fmt Sunshine pixel format to convert or allocate for.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(std::shared_ptr<platf::display_t> display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) {
int result = base.init(display, adapter_p, pix_fmt);
data = base.device.get();
return result;
}
/**
* @brief Convert a captured D3D texture for FFmpeg AVCodec encoding.
*
* @param img_base D3D image supplied by the capture backend.
* @return Conversion status.
*/
int convert(platf::img_t &img_base) override {
return base.convert(img_base);
}
/**
* @brief Apply the configured colorspace metadata to the active frame.
*/
void apply_colorspace() override {
base.apply_colorspace(colorspace);
}
/**
* @brief Configure FFmpeg hardware frames for D3D11 encoder input textures.
*
* @param frames FFmpeg hardware frames context to initialize.
*/
void init_hwframes(AVHWFramesContext *frames) override {
// We may be called with a QSV or D3D11VA context
if (frames->device_ctx->type == AV_HWDEVICE_TYPE_D3D11VA) {
@@ -993,6 +1153,12 @@
frames->initial_pool_size = 1;
}
/**
* @brief Prepare the D3D device before FFmpeg derives a child hardware context.
*
* @param hw_device_type FFmpeg hardware device type requested for context derivation.
* @return 0 when context derivation may continue; nonzero to abort.
*/
int prepare_to_derive_context(int hw_device_type) override {
// QuickSync requires our device to be multithread-protected
if (hw_device_type == AV_HWDEVICE_TYPE_QSV) {
@@ -1010,6 +1176,13 @@
return 0;
}
/**
* @brief Attach frame resources used by the next conversion or encode operation.
*
* @param frame Video or graphics frame being processed.
* @param hw_frames_ctx FFmpeg hardware frames context associated with the frame.
* @return Status from updating frame.
*/
int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override {
this->hwframe.reset(frame);
this->frame = frame;
@@ -1053,8 +1226,19 @@
frame_t hwframe;
};
/**
* @brief D3D11 encode device that exposes captured textures to NVENC.
*/
class d3d_nvenc_encode_device_t: public nvenc_encode_device_t {
public:
/**
* @brief Create D3D11 and NVENC resources for texture-based encoding.
*
* @param display Display object or identifier associated with the operation.
* @param adapter_p Adapter p.
* @param pix_fmt Sunshine pixel format to convert or allocate for.
* @return True when the D3D11 device resources are initialized.
*/
bool init_device(std::shared_ptr<platf::display_t> display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) {
buffer_format = nvenc::nvenc_format_from_sunshine_format(pix_fmt);
if (buffer_format == NV_ENC_BUFFER_FORMAT_UNDEFINED) {
@@ -1076,6 +1260,13 @@
return true;
}
/**
* @brief Initialize the platform encoder for the client stream configuration.
*
* @param client_config Client stream configuration negotiated for this session.
* @param colorspace Colorimetry information used for conversion or encoding.
* @return True when the NVENC encoder initializes for the client configuration.
*/
bool init_encoder(const ::video::config_t &client_config, const ::video::sunshine_colorspace_t &colorspace) override {
if (!nvenc_d3d) {
return false;
@@ -1090,6 +1281,12 @@
return base.init_output(nvenc_d3d->get_input_texture(), client_config.width, client_config.height) == 0;
}
/**
* @brief Convert a captured D3D texture for NVENC encoding.
*
* @param img_base D3D image supplied by the capture backend.
* @return Conversion status.
*/
int convert(platf::img_t &img_base) override {
return base.convert(img_base);
}
@@ -1100,6 +1297,15 @@
NV_ENC_BUFFER_FORMAT buffer_format = NV_ENC_BUFFER_FORMAT_UNDEFINED;
};
/**
* @brief Set cursor texture.
*
* @param device D3D, audio, or platform device used by the operation.
* @param cursor Cursor image or visibility state to composite.
* @param cursor_img Cursor img.
* @param shape_info Shape info.
* @return True when the cursor image is uploaded to the D3D texture.
*/
bool set_cursor_texture(device_t::pointer device, gpu_cursor_t &cursor, util::buffer_t<std::uint8_t> &&cursor_img, DXGI_OUTDUPL_POINTER_SHAPE_INFO &shape_info) {
// This cursor image may not be used
if (cursor_img.size() == 0) {
@@ -1638,10 +1844,6 @@
/**
* Get the next frame from the Windows.Graphics.Capture API and copy it into a new snapshot texture.
* @param pull_free_image_cb call this to get a new free image from the video subsystem.
* @param img_out the captured frame is returned here
* @param timeout how long to wait for the next frame
* @param cursor_visible
*/
capture_e display_wgc_vram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) {
texture2d_t src;
@@ -1938,15 +2140,22 @@
return device;
}
/**
* @brief Initialize global D3D11 desktop duplication support.
*/
int init() {
BOOST_LOG(info) << "Compiling shaders..."sv;
#ifndef DOXYGEN
#define compile_vertex_shader_helper(x) \
if (!(x##_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/" #x ".hlsl"))) \
return -1;
#endif
#define compile_vertex_shader_helper(x) \
if (!(x##_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/" #x ".hlsl"))) \
return -1;
#define compile_pixel_shader_helper(x) \
if (!(x##_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/" #x ".hlsl"))) \
return -1;
#ifndef DOXYGEN
#define compile_pixel_shader_helper(x) \
if (!(x##_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/" #x ".hlsl"))) \
return -1;
#endif
compile_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps);
compile_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_linear);
src/platform/windows/display_wgc.cpp +19 −7
@@ -29,5 +29,12 @@
using namespace Windows::Graphics::DirectX::Direct3D11;
extern "C" {
/**
* @brief Create direct3 D11 device from DXGI device.
*
* @param dxgiDevice DXGI device.
* @param graphicsDevice Graphics device.
* @return Created direct3 D11 device from DXGI device object or status.
*/
HRESULT __stdcall CreateDirect3D11DeviceFromDXGIDevice(::IDXGIDevice *dxgiDevice, ::IInspectable **graphicsDevice);
}
@@ -42,6 +49,13 @@
__declspec(uuid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1"))
#endif
IDirect3DDxgiInterfaceAccess: ::IUnknown {
/**
* @brief Retrieve a DXGI interface from a WinRT Direct3D object.
*
* @param id COM interface ID requested from the WinRT wrapper.
* @param object Output pointer that receives the requested COM interface.
* @return HRESULT from the WinRT object's interface query.
*/
virtual HRESULT __stdcall GetInterface(REFIID id, void **object) = 0;
};
} // namespace winrt
@@ -54,6 +68,11 @@
// compare with __declspec(uuid(...)) for the struct above.
};
/**
* @brief Return the GUID used to request IDirect3DDxgiInterfaceAccess.
*
* @return GUID for the WinRT DXGI interface-access helper.
*/
template<>
constexpr auto __mingw_uuidof<winrt::IDirect3DDxgiInterfaceAccess>() -> GUID const & {
return GUID__IDirect3DDxgiInterfaceAccess;
@@ -183,9 +202,6 @@
/**
* @brief Get the next frame from the producer thread.
* If not available, the capture thread blocks until one is, or the wait times out.
* @param timeout how long to wait for the next frame
* @param out a texture containing the frame just captured
* @param out_time the timestamp of the frame just captured
*/
capture_e wgc_capture_t::next_frame(std::chrono::milliseconds timeout, ID3D11Texture2D **out, uint64_t &out_time) {
// this CONSUMER runs in the capture thread
@@ -248,10 +264,6 @@
/**
* @brief Get the next frame from the Windows.Graphics.Capture API and copy it into a new snapshot texture.
* @param pull_free_image_cb call this to get a new free image from the video subsystem.
* @param img_out the captured frame is returned here
* @param timeout how long to wait for the next frame
* @param cursor_visible whether to capture the cursor
*/
capture_e display_wgc_ram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) {
HRESULT status;
src/platform/windows/input.cpp +115 −35
@@ -2,7 +2,16 @@
* @file src/platform/windows/input.cpp
* @brief Definitions for input handling on Windows.
*/
#ifndef DOXYGEN
#define WINVER 0x0A00
#endif
#ifdef DOXYGEN
/**
* @def CALLBACK
* @brief Windows callback calling convention marker.
*/
#define CALLBACK
#endif
#define WINVER 0x0A00
// platform includes
#include <Windows.h>
@@ -26,8 +35,11 @@
namespace platf {
using namespace std::literals;
thread_local HDESK _lastKnownInputDesktop = nullptr; ///< Last known input desktop.
thread_local HDESK _lastKnownInputDesktop = nullptr;
/**
* @brief Target touch port.
*/
constexpr touch_port_t target_touch_port {
0,
0,
@@ -35,9 +47,24 @@
65535
};
/**
* @brief ViGEm client pointer released with `vigem_free`.
*/
using client_t = util::safe_ptr<_VIGEM_CLIENT_T, vigem_free>;
/**
* @brief ViGEm target pointer released with `vigem_target_free`.
*/
using target_t = util::safe_ptr<_VIGEM_TARGET_T, vigem_target_free>;
/**
* @brief Handle Xbox 360 virtual gamepad notification events.
*
* @param client ViGEm client.
* @param target ViGEm target.
* @param largeMotor Large motor strength.
* @param smallMotor Small motor strength.
* @param userdata User data pointer.
*/
void CALLBACK x360_notify(
client_t::pointer client,
target_t::pointer target,
@@ -47,6 +74,16 @@
void *userdata
);
/**
* @brief Handle DualShock 4 virtual gamepad notification events.
*
* @param client ViGEm client.
* @param target ViGEm target.
* @param largeMotor Large motor strength.
* @param smallMotor Small motor strength.
* @param led_color Requested lightbar color.
* @param userdata User data pointer.
*/
void CALLBACK ds4_notify(
client_t::pointer client,
target_t::pointer target,
@@ -56,41 +93,62 @@
void *userdata
);
/**
* @brief Last touch coordinates reported by a virtual gamepad.
*/
struct gp_touch_context_t {
uint8_t pointerIndex; ///< Pointer index.
uint16_t x; ///< X.
uint8_t pointerIndex;
uint16_t y; ///< Y.
uint16_t x;
uint16_t y;
};
/**
* @brief ViGEm target and report buffers for one virtual gamepad.
*/
struct gamepad_context_t {
target_t gp;
feedback_queue_t feedback_queue;
target_t gp; ///< Gp.
feedback_queue_t feedback_queue; ///< Feedback queue.
union {
XUSB_REPORT x360;
DS4_REPORT_EX ds4;
} report;
} report; ///< Current HID report for the virtual controller..
// Map from pointer ID to pointer index
std::map<uint32_t, uint8_t> pointer_id_map; ///< Pointer ID map.
std::map<uint32_t, uint8_t> pointer_id_map;
uint8_t available_pointers; ///< Available pointers.
uint8_t available_pointers;
uint8_t client_relative_index;
uint8_t client_relative_index; ///< Client relative index.
thread_pool_util::ThreadPool::task_id_t repeat_task {};
std::chrono::steady_clock::time_point last_report_ts;
thread_pool_util::ThreadPool::task_id_t repeat_task {}; ///< Repeat task.
std::chrono::steady_clock::time_point last_report_ts; ///< Last report ts.
gamepad_feedback_msg_t last_rumble; ///< Last rumble.
gamepad_feedback_msg_t last_rgb_led; ///< Last RGB led.
gamepad_feedback_msg_t last_rumble;
gamepad_feedback_msg_t last_rgb_led;
};
constexpr float EARTH_G = 9.80665f;
constexpr float EARTH_G = 9.80665f; ///< Meters per second squared represented by one gravity unit.
/**
* @def MPS2_TO_DS4_ACCEL(x)
* @brief Macro for MPS2 TO DS4 ACCEL.
*/
#define MPS2_TO_DS4_ACCEL(x) (int32_t) (((x) / EARTH_G) * 8192)
/**
* @def DPS_TO_DS4_GYRO(x)
* @brief Macro for DPS TO DS4 GYRO.
*/
#define DPS_TO_DS4_GYRO(x) (int32_t) ((x) * (1024 / 64))
/**
* @def APPLY_CALIBRATION(val, bias, scale)
* @brief Macro for APPLY CALIBRATION.
*/
#define APPLY_CALIBRATION(val, bias, scale) (int32_t) (((float) (val) + (bias)) / (scale))
/**
* @brief DS4 touch unused.
*/
constexpr DS4_TOUCH ds4_touch_unused = {
.bPacketCounter = 0,
.bIsUpTrackingNum1 = 0x80,
@@ -100,6 +158,9 @@
};
// See https://github.com/ViGEm/ViGEmBus/blob/22835473d17fbf0c4d4bb2f2d42fd692b6e44df4/sys/Ds4Pdo.cpp#L153-L164
/**
* @brief DS4 report init ex.
*/
constexpr DS4_REPORT_EX ds4_report_init_ex = {
{{.bThumbLX = 0x80,
.bThumbLY = 0x80,
@@ -191,8 +252,16 @@
}
}
/**
* @brief ViGEm client connection and virtual gamepad collection.
*/
class vigem_t {
public:
/**
* @brief Connect to ViGEm and prepare virtual gamepad slots.
*
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init() {
// Probe ViGEm during startup to see if we can successfully attach gamepads. This will allow us to
// immediately display the error message in the web UI even before the user tries to stream.
@@ -375,7 +444,7 @@
}
/**
* @brief Detach all virtual gamepads and disconnect from the ViGEm client.
* @brief vigem_t destructor.
*/
~vigem_t() {
if (client) {
@@ -392,9 +461,9 @@
}
}
std::vector<gamepad_context_t> gamepads; ///< Virtual gamepads owned by this ViGEm connection.
std::vector<gamepad_context_t> gamepads;
client_t client; ///< ViGEm client connection used to create virtual gamepads.
client_t client;
};
void CALLBACK x360_notify(
@@ -431,16 +500,19 @@
task_pool.push(&vigem_t::set_rgb_led, (vigem_t *) userdata, target, led_color.Red, led_color.Green, led_color.Blue);
}
/**
* @brief Global inputtino device handles shared by clients.
*/
struct input_raw_t {
~input_raw_t() {
delete vigem;
}
vigem_t *vigem;
vigem_t *vigem; ///< Vigem.
decltype(CreateSyntheticPointerDevice) *fnCreateSyntheticPointerDevice; ///< Fn create synthetic pointer device.
decltype(CreateSyntheticPointerDevice) *fnCreateSyntheticPointerDevice;
decltype(InjectSyntheticPointerInput) *fnInjectSyntheticPointerInput;
decltype(DestroySyntheticPointerDevice) *fnDestroySyntheticPointerDevice;
decltype(InjectSyntheticPointerInput) *fnInjectSyntheticPointerInput; ///< Fn inject synthetic pointer input.
decltype(DestroySyntheticPointerDevice) *fnDestroySyntheticPointerDevice; ///< Fn destroy synthetic pointer device.
};
input_t input() {
@@ -653,7 +725,15 @@
send_input(i);
}
/**
* @brief Per-client inputtino devices for touch and pen input.
*/
struct client_input_raw_t: public client_input_t {
/**
* @brief Create per-client raw input devices for touch and pen events.
*
* @param input Platform input backend that receives the event.
*/
client_input_raw_t(input_t &input) {
global = (input_raw_t *) input.get();
}
@@ -681,14 +761,14 @@
// pen/touch events. To maintain separation, we expose separate pen and touch devices
// for each client.
HSYNTHETICPOINTERDEVICE pen {}; ///< Windows synthetic pointer device used for pen events.
POINTER_TYPE_INFO penInfo {}; ///< Pen info.
HSYNTHETICPOINTERDEVICE pen {};
POINTER_TYPE_INFO penInfo {};
thread_pool_util::ThreadPool::task_id_t penRepeatTask {};
thread_pool_util::ThreadPool::task_id_t penRepeatTask {}; ///< Pen repeat task.
HSYNTHETICPOINTERDEVICE touch {};
HSYNTHETICPOINTERDEVICE touch {}; ///< Windows synthetic pointer device used for touch events.
POINTER_TYPE_INFO touchInfo[10] {}; ///< Touch info.
UINT32 activeTouchSlots {}; ///< Active touch slots.
thread_pool_util::ThreadPool::task_id_t touchRepeatTask {}; ///< Touch repeat task.
POINTER_TYPE_INFO touchInfo[10] {};
UINT32 activeTouchSlots {};
thread_pool_util::ThreadPool::task_id_t touchRepeatTask {};
};
/**
@@ -830,7 +910,7 @@
// Active pointer interactions sent via InjectSyntheticPointerInput() seem to be automatically
// cancelled by Windows if not repeated/updated within about a second. To avoid this, refresh
// the injected input periodically.
constexpr auto ISPI_REPEAT_INTERVAL = 50ms;
constexpr auto ISPI_REPEAT_INTERVAL = 50ms; ///< Protocol or platform constant for ispi repeat interval.
/**
* @brief Repeats the current touch state to avoid the interactions timing out.
@@ -890,7 +970,7 @@
}
// These are edge-triggered pointer state flags that should always be cleared next frame
constexpr auto EDGE_TRIGGERED_POINTER_FLAGS = POINTER_FLAG_DOWN | POINTER_FLAG_UP | POINTER_FLAG_CANCELED | POINTER_FLAG_UPDATE; ///< Protocol or platform constant for edge triggered pointer flags.
constexpr auto EDGE_TRIGGERED_POINTER_FLAGS = POINTER_FLAG_DOWN | POINTER_FLAG_UP | POINTER_FLAG_CANCELED | POINTER_FLAG_UPDATE;
/**
* @brief Sends a touch event to the OS.
@@ -1288,7 +1368,7 @@
}
/**
* @brief Updates the X360 input report with the provided gamepad state.
* @brief Update an X360 report from Sunshine gamepad state.
* @param gamepad The gamepad to update.
* @param gamepad_state The gamepad button/axis state sent from the client.
*/
@@ -1418,7 +1498,7 @@
}
/**
* @brief Updates the DS4 input report with the provided gamepad state.
* @brief Update a DS4 report from Sunshine gamepad state.
* @param gamepad The gamepad to update.
* @param gamepad_state The gamepad button/axis state sent from the client.
*/
@@ -1474,7 +1554,7 @@
}
/**
* @brief Updates virtual gamepad with the provided gamepad state.
* @brief Submit updated Sunshine gamepad state to the virtual device.
* @param input The input context.
* @param nr The gamepad index to update.
* @param gamepad_state The gamepad button/axis state sent from the client.
src/platform/windows/keylayout.h +3 −0
@@ -11,6 +11,9 @@
namespace platf {
// Virtual Key to Scan Code mapping for the US English layout (00000409).
// GameStream uses this as the canonical key layout for scancode conversion.
/**
* @brief Protocol or platform constant for vk to scancode map.
*/
constexpr std::array<std::uint8_t, std::numeric_limits<std::uint8_t>::max() + 1> VK_TO_SCANCODE_MAP {
0, /* 0x00 */
0, /* 0x01 */
src/platform/windows/misc.cpp +111 −19
@@ -35,5 +35,9 @@
// Boost overrides NTDDI_VERSION, so we re-override it here
#undef NTDDI_VERSION
/**
* @def NTDDI_VERSION
* @brief Macro for NTDDI VERSION.
*/
#define NTDDI_VERSION NTDDI_WIN10
#include <Shlwapi.h>
@@ -50,21 +54,41 @@
// UDP_SEND_MSG_SIZE was added in the Windows 10 20H1 SDK
#ifndef UDP_SEND_MSG_SIZE
/**
* @def UDP_SEND_MSG_SIZE
* @brief Macro for UDP SEND MSG SIZE.
*/
#define UDP_SEND_MSG_SIZE 2
#endif
// PROC_THREAD_ATTRIBUTE_JOB_LIST is currently missing from MinGW headers
#ifndef PROC_THREAD_ATTRIBUTE_JOB_LIST
/**
* @def PROC_THREAD_ATTRIBUTE_JOB_LIST
* @brief Macro for PROC THREAD ATTRIBUTE JOB LIST.
*/
#define PROC_THREAD_ATTRIBUTE_JOB_LIST ProcThreadAttributeValue(13, FALSE, TRUE, FALSE)
#endif
#include <qos2.h>
#ifndef WLAN_API_MAKE_VERSION
/**
* @def WLAN_API_MAKE_VERSION(_major, _minor)
* @brief Macro for WLAN API MAKE VERSION.
*/
#define WLAN_API_MAKE_VERSION(_major, _minor) (((DWORD) (_minor)) << 16 | (_major))
#endif
#include <winternl.h>
extern "C" {
/**
* @brief Dynamically resolve NtSetTimerResolution from ntdll.
*
* @param DesiredResolution Desired resolution.
* @param SetResolution Set resolution.
* @param CurrentResolution Current resolution.
* @return NTSTATUS reported by the system timer-resolution request.
*/
NTSTATUS NTAPI NtSetTimerResolution(ULONG DesiredResolution, BOOLEAN SetResolution, PULONG CurrentResolution);
}
@@ -100,24 +124,27 @@
using namespace std::literals;
namespace platf {
/**
* @brief Owning pointer for `GetAdaptersAddresses` results.
*/
using adapteraddrs_t = util::c_ptr<IP_ADAPTER_ADDRESSES>;
bool enabled_mouse_keys = false;
MOUSEKEYS previous_mouse_keys_state;
bool enabled_mouse_keys = false; ///< Tracks whether Windows Mouse Keys was enabled before Sunshine changed it.
MOUSEKEYS previous_mouse_keys_state; ///< Previous mouse keys state.
HANDLE qos_handle = nullptr;
HANDLE qos_handle = nullptr; ///< QoS handle.
decltype(QOSCreateHandle) *fn_QOSCreateHandle = nullptr;
decltype(QOSAddSocketToFlow) *fn_QOSAddSocketToFlow = nullptr;
decltype(QOSRemoveSocketFromFlow) *fn_QOSRemoveSocketFromFlow = nullptr;
decltype(QOSCreateHandle) *fn_QOSCreateHandle = nullptr; ///< Fn QoS create handle.
decltype(QOSAddSocketToFlow) *fn_QOSAddSocketToFlow = nullptr; ///< Fn QoS add socket to flow.
decltype(QOSRemoveSocketFromFlow) *fn_QOSRemoveSocketFromFlow = nullptr; ///< Fn QoS remove socket from flow.
HANDLE wlan_handle = nullptr;
HANDLE wlan_handle = nullptr; ///< Wlan handle.
decltype(WlanOpenHandle) *fn_WlanOpenHandle = nullptr;
decltype(WlanCloseHandle) *fn_WlanCloseHandle = nullptr;
decltype(WlanFreeMemory) *fn_WlanFreeMemory = nullptr;
decltype(WlanEnumInterfaces) *fn_WlanEnumInterfaces = nullptr;
decltype(WlanSetInterface) *fn_WlanSetInterface = nullptr;
decltype(WlanOpenHandle) *fn_WlanOpenHandle = nullptr; ///< Fn wlan open handle.
decltype(WlanCloseHandle) *fn_WlanCloseHandle = nullptr; ///< Fn wlan close handle.
decltype(WlanFreeMemory) *fn_WlanFreeMemory = nullptr; ///< Fn wlan free memory.
decltype(WlanEnumInterfaces) *fn_WlanEnumInterfaces = nullptr; ///< Fn wlan enum interfaces.
decltype(WlanSetInterface) *fn_WlanSetInterface = nullptr; ///< Fn wlan set interface.
std::filesystem::path appdata() {
WCHAR sunshine_path[MAX_PATH];
@@ -154,6 +181,11 @@
return {port, std::string {data}};
}
/**
* @brief Read Windows adapter addresses with automatic buffer sizing.
*
* @return Adapter-address list populated by GetAdaptersAddresses.
*/
adapteraddrs_t get_adapteraddrs() {
adapteraddrs_t info {nullptr};
ULONG size = 0;
@@ -186,6 +218,9 @@
return "00:00:00:00:00:00"s;
}
/**
* @brief Synchronize thread desktop.
*/
HDESK syncThreadDesktop() {
auto hDesk = OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, FALSE, GENERIC_ALL);
if (!hDesk) {
@@ -205,5 +240,8 @@
return hDesk;
}
/**
* @brief Write status details to the log.
*/
void print_status(const std::string_view &prefix, HRESULT status) {
char err_string[1024];
@@ -213,6 +251,12 @@
BOOST_LOG(error) << prefix << ": "sv << std::string_view {err_string, bytes};
}
/**
* @brief Check whether a Windows access token belongs to an administrator.
*
* @param user_token Windows access token to inspect.
* @return True when the inspected token has administrator privileges.
*/
bool IsUserAdmin(HANDLE user_token) {
WINBOOL ret;
SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY;
@@ -246,6 +290,8 @@
/**
* @brief Obtain the current sessions user's primary token with elevated privileges.
* @return The user's token. If user has admin capability it will be elevated, otherwise it will be a limited token. On error, `nullptr`.
*
* @param elevated Whether the command should run with elevated privileges.
*/
HANDLE retrieve_users_token(bool elevated) {
DWORD consoleSessionId;
@@ -308,6 +354,13 @@
return userToken;
}
/**
* @brief Merge user environment variables into a Windows environment block.
*
* @param env Environment variables for the child process.
* @param shell_token Shell token.
* @return True when the user environment block was merged into `env`.
*/
bool merge_user_environment_block(bp::environment &env, HANDLE shell_token) {
// Get the target user's environment block
PVOID env_block;
@@ -378,11 +431,24 @@
}
// Note: This does NOT append a null terminator
/**
* @brief Append a null-terminated string to a Windows environment block.
*
* @param env_block Env block.
* @param offset Byte offset used when converting from the wide string buffer.
* @param wstr Wide-character string being converted to UTF-8.
*/
void append_string_to_environment_block(wchar_t *env_block, int &offset, const std::wstring &wstr) {
std::memcpy(&env_block[offset], wstr.data(), wstr.length() * sizeof(wchar_t));
offset += wstr.length();
}
/**
* @brief Create environment block.
*
* @param env Environment variables for the child process.
* @return Created environment block object or status.
*/
std::wstring create_environment_block(const bp::environment &env) {
int size = 0;
for (const auto &entry : env) {
@@ -415,6 +481,12 @@
return std::wstring(env_block.data(), offset);
}
/**
* @brief Allocate and initialize a Windows process-thread attribute list.
*
* @param attribute_count Attribute count.
* @return Initialized attribute list, or nullptr when allocation fails.
*/
LPPROC_THREAD_ATTRIBUTE_LIST allocate_proc_thread_attr_list(DWORD attribute_count) {
SIZE_T size;
InitializeProcThreadAttributeList(nullptr, attribute_count, 0, &size);
@@ -432,6 +504,11 @@
return list;
}
/**
* @brief Release proc thread attr list resources.
*
* @param list Multi-string list returned by the Windows API.
*/
void free_proc_thread_attr_list(LPPROC_THREAD_ATTRIBUTE_LIST list) {
DeleteProcThreadAttributeList(list);
HeapFree(GetProcessHeap(), 0, list);
@@ -1255,9 +1332,12 @@
return _putenv_s(name.c_str(), "");
}
/**
* @brief Stores state while enumerating top-level Windows windows.
*/
struct enum_wnd_context_t {
std::set<DWORD> process_ids;
bool requested_exit;
std::set<DWORD> process_ids; ///< Process ids.
bool requested_exit; ///< Whether a close request was observed while enumerating windows.
};
static BOOL CALLBACK prgrp_enum_windows(HWND hwnd, LPARAM lParam) {
@@ -1549,8 +1629,16 @@
return true;
}
/**
* @brief Owns platform QoS state that is restored during cleanup.
*/
class qos_t: public deinit_t {
public:
/**
* @brief Store a Windows QoS flow ID for cleanup on destruction.
*
* @param flow_id Flow ID.
*/
qos_t(QOS_FLOWID flow_id):
flow_id(flow_id) {
}
@@ -1568,11 +1656,6 @@
/**
* @brief Enables QoS on the given socket for traffic to the specified destination.
* @param native_socket The native socket handle.
* @param address The destination address for traffic sent on this socket.
* @param port The destination port for traffic sent on this socket.
* @param data_type The type of traffic sent on this socket.
* @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic.
*/
std::unique_ptr<deinit_t> enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) {
SOCKADDR_IN saddr_v4;
@@ -1683,6 +1766,9 @@
return std::make_unique<qos_t>(flow_id);
}
/**
* @brief Read the current Windows high-resolution performance counter.
*/
int64_t qpc_counter() {
LARGE_INTEGER performance_counter;
if (QueryPerformanceCounter(&performance_counter)) {
@@ -1691,6 +1777,9 @@
return 0;
}
/**
* @brief Convert the difference between two QPC readings to nanoseconds.
*/
std::chrono::nanoseconds qpc_time_difference(int64_t performance_counter1, int64_t performance_counter2) {
auto get_frequency = []() {
LARGE_INTEGER frequency;
@@ -1714,6 +1803,9 @@
return utf_utils::to_utf8(hostname);
}
/**
* @brief Implements the high-precision timer using Win32 waitable timers.
*/
class win32_high_precision_timer: public high_precision_timer {
public:
win32_high_precision_timer() {
src/platform/windows/misc.h +23 −0
@@ -15,11 +15,34 @@
#include <winnt.h>
namespace platf {
/**
* @brief Write status details to the log.
*
* @param prefix Text prefix used when formatting the message.
* @param status Native status code returned by the platform API.
*/
void print_status(const std::string_view &prefix, HRESULT status);
/**
* @brief Synchronize thread desktop.
*
* @return true when the thread desktop was synchronized successfully.
*/
HDESK syncThreadDesktop();
/**
* @brief Read the current Windows high-resolution performance counter.
*
* @return Raw QPC tick value from `QueryPerformanceCounter`.
*/
int64_t qpc_counter();
/**
* @brief Convert the difference between two QPC readings to nanoseconds.
*
* @param performance_counter1 Newer performance-counter reading.
* @param performance_counter2 Older performance-counter reading.
* @return Duration represented by the difference between two QPC values.
*/
std::chrono::nanoseconds qpc_time_difference(int64_t performance_counter1, int64_t performance_counter2);
/**
src/platform/windows/nvprefs/driver_settings.h +39 −0
@@ -36,22 +36,61 @@
namespace nvprefs {
/**
* @brief NVIDIA driver profile settings loaded for inspection or modification.
*/
class driver_settings_t {
public:
~driver_settings_t();
/**
* @brief Load NVIDIA profile settings for the current driver state.
*
* @return True when the NVIDIA driver-settings operation succeeds.
*/
bool init();
/**
* @brief Destroy the native resource owned by the wrapper.
*/
void destroy();
/**
* @brief Load settings data from the backing API or store.
*
* @return True when the NVIDIA driver-settings operation succeeds.
*/
bool load_settings();
/**
* @brief Save settings data through the backing API or store.
*
* @return True when the NVIDIA driver-settings operation succeeds.
*/
bool save_settings();
/**
* @brief Restore global NVIDIA profile settings from undo data.
*
* @param undo_data Driver settings captured before Sunshine modified them.
* @return True when the NVIDIA driver-settings operation succeeds.
*/
bool restore_global_profile_to_undo(const undo_data_t &undo_data);
/**
* @brief Compare and update global NVIDIA profile settings.
*
* @param undo_data Driver settings captured before Sunshine modified them.
* @return True when the NVIDIA driver-settings operation succeeds.
*/
bool check_and_modify_global_profile(std::optional<undo_data_t> &undo_data);
/**
* @brief Compare and update Sunshine NVIDIA application profile settings.
*
* @param modified Whether NVIDIA driver settings were changed and need saving.
* @return True when the NVIDIA driver-settings operation succeeds.
*/
bool check_and_modify_application_profile(bool &modified);
private:
src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp +117 −0
@@ -31,11 +31,27 @@
} // namespace
#undef NVAPI_INTERFACE
/**
* @def NVAPI_INTERFACE
* @brief Macro for NVAPI INTERFACE.
*/
#define NVAPI_INTERFACE NvAPI_Status __cdecl
/**
* @brief NVAPI export used to resolve function pointers by interface ID.
*
* @param id NVAPI interface ID from the generated interface table.
* @return Function pointer for the requested NVAPI interface, or nullptr.
*/
extern void *__cdecl nvapi_QueryInterface(NvU32 id);
NVAPI_INTERFACE
/**
* @brief Load NVAPI and resolve the subset of interfaces used by Sunshine.
*
* @return NVAPI_OK on success, or NVAPI_LIBRARY_NOT_FOUND when the DLL cannot be loaded.
*/
NvAPI_Initialize() {
if (dll) {
return NVAPI_OK;
@@ -60,6 +76,11 @@
return NVAPI_LIBRARY_NOT_FOUND;
}
/**
* @brief Unload NVAPI and clear all resolved interface pointers.
*
* @return NVAPI_OK after cleanup.
*/
NVAPI_INTERFACE NvAPI_Unload() {
if (dll) {
interfaces.clear();
@@ -69,56 +90,152 @@
return NVAPI_OK;
}
/**
* @brief Forward NvAPI_GetErrorMessage to the loaded NVAPI DLL.
*
* @param nr Controller index assigned by the client.
* @param szDesc Output buffer receiving the human-readable error string.
* @return NVAPI status returned by the loaded function.
*/
NVAPI_INTERFACE NvAPI_GetErrorMessage(NvAPI_Status nr, NvAPI_ShortString szDesc) {
return call_interface<decltype(NvAPI_GetErrorMessage)>("NvAPI_GetErrorMessage", nr, szDesc);
}
// This is only a subset of NvAPI_DRS_* functions, more can be added if needed
/**
* @brief Forward NvAPI_DRS_CreateSession to the loaded NVAPI DLL.
*
* @param phSession Output handle for the created DRS session.
* @return NVAPI status returned by the loaded function.
*/
NVAPI_INTERFACE NvAPI_DRS_CreateSession(NvDRSSessionHandle *phSession) {
return call_interface<decltype(NvAPI_DRS_CreateSession)>("NvAPI_DRS_CreateSession", phSession);
}
/**
* @brief Forward NvAPI_DRS_DestroySession to the loaded NVAPI DLL.
*
* @param hSession DRS session handle to destroy.
* @return NVAPI status returned by the loaded function.
*/
NVAPI_INTERFACE NvAPI_DRS_DestroySession(NvDRSSessionHandle hSession) {
return call_interface<decltype(NvAPI_DRS_DestroySession)>("NvAPI_DRS_DestroySession", hSession);
}
/**
* @brief Forward NvAPI_DRS_LoadSettings to the loaded NVAPI DLL.
*
* @param hSession DRS session whose settings are loaded.
* @return NVAPI status returned by the loaded function.
*/
NVAPI_INTERFACE NvAPI_DRS_LoadSettings(NvDRSSessionHandle hSession) {
return call_interface<decltype(NvAPI_DRS_LoadSettings)>("NvAPI_DRS_LoadSettings", hSession);
}
/**
* @brief Forward NvAPI_DRS_SaveSettings to the loaded NVAPI DLL.
*
* @param hSession DRS session whose settings are saved.
* @return NVAPI status returned by the loaded function.
*/
NVAPI_INTERFACE NvAPI_DRS_SaveSettings(NvDRSSessionHandle hSession) {
return call_interface<decltype(NvAPI_DRS_SaveSettings)>("NvAPI_DRS_SaveSettings", hSession);
}
/**
* @brief Forward NvAPI_DRS_CreateProfile to the loaded NVAPI DLL.
*
* @param hSession DRS session that owns the profile.
* @param pProfileInfo Profile definition to create.
* @param phProfile Output handle for the created profile.
* @return NVAPI status returned by the loaded function.
*/
NVAPI_INTERFACE NvAPI_DRS_CreateProfile(NvDRSSessionHandle hSession, NVDRS_PROFILE *pProfileInfo, NvDRSProfileHandle *phProfile) {
return call_interface<decltype(NvAPI_DRS_CreateProfile)>("NvAPI_DRS_CreateProfile", hSession, pProfileInfo, phProfile);
}
/**
* @brief Forward NvAPI_DRS_FindProfileByName to the loaded NVAPI DLL.
*
* @param hSession DRS session to search.
* @param profileName Profile name.
* @param phProfile Output handle for the matched profile.
* @return NVAPI status returned by the loaded function.
*/
NVAPI_INTERFACE NvAPI_DRS_FindProfileByName(NvDRSSessionHandle hSession, NvAPI_UnicodeString profileName, NvDRSProfileHandle *phProfile) {
return call_interface<decltype(NvAPI_DRS_FindProfileByName)>("NvAPI_DRS_FindProfileByName", hSession, profileName, phProfile);
}
/**
* @brief Forward NvAPI_DRS_CreateApplication to the loaded NVAPI DLL.
*
* @param hSession DRS session that owns the profile.
* @param hProfile Profile receiving the application entry.
* @param pApplication Application entry to create.
* @return NVAPI status returned by the loaded function.
*/
NVAPI_INTERFACE NvAPI_DRS_CreateApplication(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_APPLICATION *pApplication) {
return call_interface<decltype(NvAPI_DRS_CreateApplication)>("NvAPI_DRS_CreateApplication", hSession, hProfile, pApplication);
}
/**
* @brief Forward NvAPI_DRS_GetApplicationInfo to the loaded NVAPI DLL.
*
* @param hSession DRS session that owns the profile.
* @param hProfile Profile containing the application entry.
* @param appName App name.
* @param pApplication Output application information.
* @return NVAPI status returned by the loaded function.
*/
NVAPI_INTERFACE NvAPI_DRS_GetApplicationInfo(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvAPI_UnicodeString appName, NVDRS_APPLICATION *pApplication) {
return call_interface<decltype(NvAPI_DRS_GetApplicationInfo)>("NvAPI_DRS_GetApplicationInfo", hSession, hProfile, appName, pApplication);
}
/**
* @brief Forward NvAPI_DRS_SetSetting to the loaded NVAPI DLL.
*
* @param hSession DRS session that owns the profile.
* @param hProfile Profile receiving the setting.
* @param pSetting Setting value to apply.
* @return NVAPI status returned by the loaded function.
*/
NVAPI_INTERFACE NvAPI_DRS_SetSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_SETTING *pSetting) {
return call_interface<decltype(NvAPI_DRS_SetSetting)>("NvAPI_DRS_SetSetting", hSession, hProfile, pSetting);
}
/**
* @brief Forward NvAPI_DRS_GetSetting to the loaded NVAPI DLL.
*
* @param hSession DRS session that owns the profile.
* @param hProfile Profile containing the setting.
* @param settingId Setting ID.
* @param pSetting Output setting value.
* @return NVAPI status returned by the loaded function.
*/
NVAPI_INTERFACE NvAPI_DRS_GetSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvU32 settingId, NVDRS_SETTING *pSetting) {
return call_interface<decltype(NvAPI_DRS_GetSetting)>("NvAPI_DRS_GetSetting", hSession, hProfile, settingId, pSetting);
}
/**
* @brief Forward NvAPI_DRS_DeleteProfileSetting to the loaded NVAPI DLL.
*
* @param hSession DRS session that owns the profile.
* @param hProfile Profile containing the setting.
* @param settingId Setting ID.
* @return NVAPI status returned by the loaded function.
*/
NVAPI_INTERFACE NvAPI_DRS_DeleteProfileSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvU32 settingId) {
return call_interface<decltype(NvAPI_DRS_DeleteProfileSetting)>("NvAPI_DRS_DeleteProfileSetting", hSession, hProfile, settingId);
}
/**
* @brief Forward NvAPI_DRS_GetBaseProfile to the loaded NVAPI DLL.
*
* @param hSession DRS session to query.
* @param phProfile Output handle for the base profile.
* @return NVAPI status returned by the loaded function.
*/
NVAPI_INTERFACE NvAPI_DRS_GetBaseProfile(NvDRSSessionHandle hSession, NvDRSProfileHandle *phProfile) {
return call_interface<decltype(NvAPI_DRS_GetBaseProfile)>("NvAPI_DRS_GetBaseProfile", hSession, phProfile);
}
src/platform/windows/nvprefs/nvprefs_common.cpp +15 −0
@@ -11,22 +11,37 @@
namespace nvprefs {
/**
* @brief Forward an informational NVPrefs message to the logger.
*/
void info_message(const std::wstring &message) {
BOOST_LOG(info) << "nvprefs: " << message;
}
/**
* @brief Forward an informational NVPrefs message to the logger.
*/
void info_message(const std::string &message) {
BOOST_LOG(info) << "nvprefs: " << message;
}
/**
* @brief Forward an NVPrefs error message to the logger.
*/
void error_message(const std::wstring &message) {
BOOST_LOG(error) << "nvprefs: " << message;
}
/**
* @brief Forward an NVPrefs error message to the logger.
*/
void error_message(const std::string &message) {
BOOST_LOG(error) << "nvprefs: " << message;
}
/**
* @brief Get nvprefs options.
*/
nvprefs_options get_nvprefs_options() {
nvprefs_options options;
options.opengl_vulkan_on_dxgi = config::video.nv_opengl_vulkan_on_dxgi;
src/platform/windows/nvprefs/nvprefs_common.h +50 −2
@@ -16,39 +16,87 @@
namespace nvprefs {
/**
* @brief Owning Windows HANDLE wrapper that closes handles automatically.
*/
struct safe_handle: public util::safe_ptr_v2<void, BOOL, CloseHandle> {
using util::safe_ptr_v2<void, BOOL, CloseHandle>::safe_ptr_v2;
/**
* @brief Check whether the wrapped Windows handle is valid.
*/
explicit operator bool() const {
auto handle = get();
return handle != nullptr && handle != INVALID_HANDLE_VALUE;
}
};
/**
* @brief Deleter for memory allocated by Windows LocalAlloc APIs.
*/
struct safe_hlocal_deleter {
/**
* @brief Free memory allocated by Windows local-memory APIs.
*
* @param p LocalAlloc-compatible pointer to release.
*/
void operator()(void *p) {
LocalFree(p);
}
};
/**
* @brief Unique pointer for memory released with `LocalFree`.
*/
template<typename T>
using safe_hlocal = util::uniq_ptr<std::remove_pointer_t<T>, safe_hlocal_deleter>;
/**
* @brief Safe pointer for Windows security identifiers released with `FreeSid`.
*/
using safe_sid = util::safe_ptr_v2<void, PVOID, FreeSid>;
/**
* @brief Forward an informational NVPrefs message to the logger.
*
* @param message Message text to log or report.
*/
void info_message(const std::wstring &message);
/**
* @brief Forward an informational NVPrefs message to the logger.
*
* @param message Message text to log or report.
*/
void info_message(const std::string &message);
/**
* @brief Forward an NVPrefs error message to the logger.
*
* @param message Message text to log or report.
*/
void error_message(const std::wstring &message);
/**
* @brief Forward an NVPrefs error message to the logger.
*
* @param message Message text to log or report.
*/
void error_message(const std::string &message);
/**
* @brief Parsed command-line options for the NVIDIA preferences helper.
*/
struct nvprefs_options {
bool opengl_vulkan_on_dxgi = true;
bool opengl_vulkan_on_dxgi = true; ///< Whether NVIDIA OpenGL/Vulkan-on-DXGI should be enabled.
bool sunshine_high_power_mode = true; ///< Whether NVIDIA high-power mode should be enabled for Sunshine.
bool sunshine_high_power_mode = true;
};
/**
* @brief Get nvprefs options.
*
* @return Parsed command-line options for NVIDIA profile preference handling.
*/
nvprefs_options get_nvprefs_options();
} // namespace nvprefs
src/platform/windows/nvprefs/nvprefs_interface.cpp +9 −6
@@ -19,13 +19,16 @@
namespace nvprefs {
/**
* @brief Private state owned by the NVIDIA preferences interface.
*/
struct nvprefs_interface::impl {
bool loaded = false; ///< Whether NVIDIA preference state has been loaded.
driver_settings_t driver_settings; ///< Driver settings.
std::filesystem::path undo_folder_path; ///< Undo folder path.
std::filesystem::path undo_file_path; ///< Undo file path.
std::optional<undo_data_t> undo_data; ///< Undo data.
bool loaded = false;
driver_settings_t driver_settings;
std::filesystem::path undo_folder_path;
std::filesystem::path undo_file_path;
std::optional<undo_data_t> undo_data;
std::optional<undo_file_t> undo_file;
std::optional<undo_file_t> undo_file; ///< Undo file.
};
nvprefs_interface::nvprefs_interface():
src/platform/windows/nvprefs/nvprefs_interface.h +36 −0
@@ -9,23 +9,59 @@
namespace nvprefs {
/**
* @brief High-level NVIDIA profile preferences interface with undo support.
*/
class nvprefs_interface {
public:
nvprefs_interface();
~nvprefs_interface();
/**
* @brief Load persisted state from its backing store.
*
* @return True when the NVIDIA preferences operation succeeds.
*/
bool load();
/**
* @brief Release loaded NVIDIA preference state.
*/
void unload();
/**
* @brief Restore NVIDIA settings from the undo file, then remove it.
*
* @return True when the NVIDIA preferences operation succeeds.
*/
bool restore_from_and_delete_undo_file_if_exists();
/**
* @brief Apply Sunshine-specific NVIDIA application profile changes.
*
* @return True when the NVIDIA preferences operation succeeds.
*/
bool modify_application_profile();
/**
* @brief Apply Sunshine-specific NVIDIA global profile changes.
*
* @return True when the NVIDIA preferences operation succeeds.
*/
bool modify_global_profile();
/**
* @brief Check whether this interface owns the active undo file.
*
* @return True when the NVIDIA preferences operation succeeds.
*/
bool owning_undo_file();
/**
* @brief Restore NVIDIA global profile settings from saved undo data.
*
* @return True when the NVIDIA preferences operation succeeds.
*/
bool restore_global_profile();
private:
src/platform/windows/nvprefs/undo_data.cpp +54 −0
@@ -9,16 +9,34 @@
#include "nvprefs_common.h"
#include "undo_data.h"
/**
* @brief nlohmann JSON type used for NVPrefs undo serialization.
*/
using json = nlohmann::json;
// Separate namespace for ADL, otherwise we need to define json
// functions in the same namespace as our types
namespace nlohmann {
/**
* @brief Serialized undo-data representation.
*/
using data_t = nvprefs::undo_data_t::data_t;
/**
* @brief Serialized OpenGL swapchain profile settings.
*/
using opengl_swapchain_t = data_t::opengl_swapchain_t;
/**
* @brief Helper specialization for optional values.
*/
template<typename T>
struct adl_serializer<std::optional<T>> {
/**
* @brief Serialize the undo-data object to JSON.
*
* @param j JSON value being read from or written to.
* @param opt Optional value being serialized or restored.
*/
static void to_json(json &j, const std::optional<T> &opt) {
if (opt == std::nullopt) {
j = nullptr;
@@ -27,6 +45,12 @@
}
}
/**
* @brief Deserialize the undo-data object from JSON.
*
* @param j JSON value being read from or written to.
* @param opt Optional value being serialized or restored.
*/
static void from_json(const json &j, std::optional<T> &opt) {
if (j.is_null()) {
opt = std::nullopt;
@@ -36,19 +60,43 @@
}
};
/**
* @brief JSON serializer specialization for undo-data records.
*/
template<>
struct adl_serializer<data_t> {
/**
* @brief Serialize the undo-data object to JSON.
*
* @param j JSON value being read from or written to.
* @param data Payload or state data to serialize, deserialize, or forward.
*/
static void to_json(json &j, const data_t &data) {
j = json {{"opengl_swapchain", data.opengl_swapchain}};
}
/**
* @brief Deserialize the undo-data object from JSON.
*
* @param j JSON value being read from or written to.
* @param data Payload or state data to serialize, deserialize, or forward.
*/
static void from_json(const json &j, data_t &data) {
j.at("opengl_swapchain").get_to(data.opengl_swapchain);
}
};
/**
* @brief JSON serializer specialization for OpenGL swapchain settings.
*/
template<>
struct adl_serializer<opengl_swapchain_t> {
/**
* @brief Serialize the undo-data object to JSON.
*
* @param j JSON value being read from or written to.
* @param opengl_swapchain Opengl swapchain.
*/
static void to_json(json &j, const opengl_swapchain_t &opengl_swapchain) {
j = json {
{"our_value", opengl_swapchain.our_value},
@@ -56,6 +104,12 @@
};
}
/**
* @brief Deserialize the undo-data object from JSON.
*
* @param j JSON value being read from or written to.
* @param opengl_swapchain Opengl swapchain.
*/
static void from_json(const json &j, opengl_swapchain_t &opengl_swapchain) {
j.at("our_value").get_to(opengl_swapchain.our_value);
j.at("undo_value").get_to(opengl_swapchain.undo_value);
src/platform/windows/nvprefs/undo_data.h +38 −3
@@ -12,25 +12,60 @@
namespace nvprefs {
/**
* @brief Serializable NVIDIA profile state saved before preference changes.
*/
class undo_data_t {
public:
/**
* @brief NVIDIA profile settings captured for undo.
*/
struct data_t {
/**
* @brief OpenGL swapchain setting values captured from the driver.
*/
struct opengl_swapchain_t {
uint32_t our_value; ///< NVIDIA setting value applied by Sunshine.
std::optional<uint32_t> undo_value; ///< Previous NVIDIA setting value to restore, if present.
uint32_t our_value;
std::optional<uint32_t> undo_value;
};
std::optional<opengl_swapchain_t> opengl_swapchain; ///< Opengl swapchain.
std::optional<opengl_swapchain_t> opengl_swapchain;
};
/**
* @brief Set opengl swapchain.
*
* @param our_value NVIDIA setting value applied by Sunshine.
* @param undo_value Previous NVIDIA setting value to restore, if present.
*/
void set_opengl_swapchain(uint32_t our_value, std::optional<uint32_t> undo_value);
/**
* @brief Get opengl swapchain.
*
* @return Stored OpenGL swapchain override and optional restore setting.
*/
std::optional<data_t::opengl_swapchain_t> get_opengl_swapchain() const;
/**
* @brief Serialize undo data to its JSON representation.
*
* @return JSON string containing the undo data.
*/
std::string write() const;
/**
* @brief Read persisted data into the current object.
*
* @param buffer Serialized byte buffer to read from or write to.
*/
void read(const std::vector<char> &buffer);
/**
* @brief Merge newer undo data with the current data set.
*
* @param newer_data Newer data.
*/
void merge(const undo_data_t &newer_data);
private:
src/platform/windows/nvprefs/undo_file.h +32 −0
@@ -13,16 +13,48 @@
namespace nvprefs {
/**
* @brief File-backed storage for NVIDIA preference undo data.
*/
class undo_file_t {
public:
/**
* @brief Open existing file.
*
* @param file_path File path.
* @param access_denied Access denied.
* @return Open undo file wrapper, or std::nullopt when the file cannot be opened.
*/
static std::optional<undo_file_t> open_existing_file(std::filesystem::path file_path, bool &access_denied);
/**
* @brief Create new file.
*
* @param file_path File path.
* @return Created new file object or status.
*/
static std::optional<undo_file_t> create_new_file(std::filesystem::path file_path);
/**
* @brief Delete the persisted NVIDIA settings undo file.
*
* @return True when the undo-file operation succeeds.
*/
bool delete_file();
/**
* @brief Write undo data.
*
* @param undo_data Driver settings to persist for a later restore.
* @return True when the undo-file operation succeeds.
*/
bool write_undo_data(const undo_data_t &undo_data);
/**
* @brief Read undo data.
*
* @return Parsed undo data, or std::nullopt when the file is empty or invalid.
*/
std::optional<undo_data_t> read_undo_data();
private:
src/platform/windows/PolicyConfig.h +76 −0
@@ -24,8 +24,22 @@
#endif
/**
* @brief Policy configuration COM interface.
*/
#ifdef DOXYGEN
class IPolicyConfig;
#else
interface DECLSPEC_UUID("f8679f50-850a-41cf-9c72-430f290290c8") IPolicyConfig;
#endif
/**
* @brief Policy configuration COM class.
*/
#ifdef DOXYGEN
class CPolicyConfigClient;
#else
class DECLSPEC_UUID("870af99c-171d-4f9e-af0d-e63df40c2bc9") CPolicyConfigClient;
#endif
// ----------------------------------------------------------------------------
// class CPolicyConfigClient
// {870af99c-171d-4f9e-af0d-e63df40c2bc9}
@@ -41,21 +55,41 @@
// ----------------------------------------------------------------------------
interface IPolicyConfig: public IUnknown {
public:
/**
* @brief Get the mix format for an endpoint.
*
* @return HRESULT or platform status returned by the wrapped API.
*/
virtual HRESULT GetMixFormat(
PCWSTR,
WAVEFORMATEX **
);
/**
* @brief Get the device format for an endpoint.
*
* @return HRESULT or platform status returned by the wrapped API.
*/
virtual HRESULT STDMETHODCALLTYPE GetDeviceFormat(
PCWSTR,
INT,
WAVEFORMATEX **
);
/**
* @brief Reset the device format for an endpoint.
*
* @return HRESULT or platform status returned by the wrapped API.
*/
virtual HRESULT STDMETHODCALLTYPE ResetDeviceFormat(
PCWSTR
);
/**
* @brief Set the device format for an endpoint.
*
* @return HRESULT or platform status returned by the wrapped API.
*/
virtual HRESULT STDMETHODCALLTYPE
SetDeviceFormat(
PCWSTR,
@@ -63,6 +97,11 @@
WAVEFORMATEX *
);
/**
* @brief Get the processing period for an endpoint.
*
* @return HRESULT or platform status returned by the wrapped API.
*/
virtual HRESULT STDMETHODCALLTYPE GetProcessingPeriod(
PCWSTR,
INT,
@@ -70,38 +109,75 @@
PINT64
);
/**
* @brief Set the processing period for an endpoint.
*
* @return HRESULT or platform status returned by the wrapped API.
*/
virtual HRESULT STDMETHODCALLTYPE SetProcessingPeriod(
PCWSTR,
PINT64
);
/**
* @brief Get the share mode for an endpoint.
*
* @return HRESULT or platform status returned by the wrapped API.
*/
virtual HRESULT STDMETHODCALLTYPE GetShareMode(
PCWSTR,
struct DeviceShareMode *
);
/**
* @brief Set the share mode for an endpoint.
*
* @return HRESULT or platform status returned by the wrapped API.
*/
virtual HRESULT STDMETHODCALLTYPE SetShareMode(
PCWSTR,
struct DeviceShareMode *
);
/**
* @brief Get a property value for an endpoint.
*
* @return HRESULT or platform status returned by the wrapped API.
*/
virtual HRESULT STDMETHODCALLTYPE GetPropertyValue(
PCWSTR,
const PROPERTYKEY &,
PROPVARIANT *
);
/**
* @brief Set a property value for an endpoint.
*
* @return HRESULT or platform status returned by the wrapped API.
*/
virtual HRESULT STDMETHODCALLTYPE SetPropertyValue(
PCWSTR,
const PROPERTYKEY &,
PROPVARIANT *
);
/**
* @brief Set the default endpoint for a role.
*
* @param wszDeviceId Endpoint device identifier.
* @param eRole Endpoint role.
* @return HRESULT or platform status returned by the wrapped API.
*/
virtual HRESULT STDMETHODCALLTYPE SetDefaultEndpoint(
PCWSTR wszDeviceId,
ERole eRole
);
/**
* @brief Set endpoint visibility.
*
* @return HRESULT or platform status returned by the wrapped API.
*/
virtual HRESULT STDMETHODCALLTYPE SetEndpointVisibility(
PCWSTR,
INT
src/platform/windows/publish.cpp +81 −27
@@ -21,47 +21,75 @@
#include "src/thread_safe.h"
#include "utf_utils.h"
/**
* @def _FN(x, ret, args)
* @brief Macro for FN.
*/
#define _FN(x, ret, args) \
/** \
* @brief Function pointer type for the dynamically loaded DNS-SD entry point. \
*/ \
typedef ret(*x##_fn) args; \
/** \
* @brief Loaded DNS-SD entry point pointer. \
*/ \
static x##_fn x
using namespace std::literals;
/**
* @def __SV(quote)
* @brief Macro for SV.
*/
#define __SV(quote) L##quote##sv
/**
* @def SV(quote)
* @brief Macro for SV.
*/
#define SV(quote) __SV(quote)
extern "C" {
#ifndef __MINGW32__
constexpr auto DNS_REQUEST_PENDING = 9506L; ///< Windows DNS API constant for request pending.
constexpr auto DNS_QUERY_REQUEST_VERSION1 = 0x1; ///< Windows DNS API constant for query request version1.
constexpr auto DNS_QUERY_RESULTS_VERSION1 = 0x1; ///< Windows DNS API constant for query results version1.
constexpr auto DNS_REQUEST_PENDING = 9506L;
constexpr auto DNS_QUERY_REQUEST_VERSION1 = 0x1;
constexpr auto DNS_QUERY_RESULTS_VERSION1 = 0x1;
#endif
constexpr auto SERVICE_DOMAIN = "local";
const auto SERVICE_TYPE_DOMAIN = std::format("{}.{}"sv, platf::SERVICE_TYPE, SERVICE_DOMAIN);
constexpr auto SERVICE_DOMAIN = "local"; ///< Protocol or platform constant for service domain.
const auto SERVICE_TYPE_DOMAIN = std::format("{}.{}"sv, platf::SERVICE_TYPE, SERVICE_DOMAIN); ///< Protocol or platform constant for service type domain.
#ifndef __MINGW32__
/**
* @brief Windows DNS-SD service instance registration data.
*/
typedef struct _DNS_SERVICE_INSTANCE {
LPWSTR pszInstanceName;
LPWSTR pszHostName;
LPWSTR pszInstanceName; ///< DNS-SD service instance name.
LPWSTR pszHostName; ///< Hostname advertising the DNS-SD service.
IP4_ADDRESS *ip4Address;
IP4_ADDRESS *ip4Address; ///< Optional IPv4 address advertised with the service.
IP6_ADDRESS *ip6Address; ///< Optional IPv6 address advertised with the service.
IP6_ADDRESS *ip6Address;
WORD wPort; ///< TCP or UDP port advertised for the service.
WORD wPriority; ///< DNS-SD priority value.
WORD wPort;
WORD wPriority;
WORD wWeight;
WORD wWeight; ///< DNS-SD weight value.
// Property list
DWORD dwPropertyCount;
DWORD dwPropertyCount; ///< Number of TXT record key/value pairs.
PWSTR *keys; ///< DNS TXT record keys.
PWSTR *keys;
PWSTR *values;
PWSTR *values; ///< DNS TXT record values paired with `keys`.
DWORD dwInterfaceIndex;
} DNS_SERVICE_INSTANCE, *PDNS_SERVICE_INSTANCE;
DWORD dwInterfaceIndex; ///< Network interface index used for registration.
} DNS_SERVICE_INSTANCE, *PDNS_SERVICE_INSTANCE; ///< Alias for DNS SERVICE INSTANCE.
#endif
/**
* @brief DNS service registration completion callback.
*
* @param Status Registration status.
* @param pQueryContext User query context.
* @param pInstance DNS service instance.
*/
typedef VOID WINAPI
DNS_SERVICE_REGISTER_COMPLETE(
_In_ DWORD Status,
@@ -69,22 +97,31 @@
_In_ PDNS_SERVICE_INSTANCE pInstance
);
/**
* @brief Pointer to the Windows DNS-SD registration completion callback.
*/
typedef DNS_SERVICE_REGISTER_COMPLETE *PDNS_SERVICE_REGISTER_COMPLETE;
#ifndef __MINGW32__
/**
* @brief Windows DNS-SD cancellation request data.
*/
typedef struct _DNS_SERVICE_CANCEL {
PVOID reserved;
} DNS_SERVICE_CANCEL, *PDNS_SERVICE_CANCEL;
PVOID reserved; ///< Reserved by the Windows DNS-SD API and left null.
} DNS_SERVICE_CANCEL, *PDNS_SERVICE_CANCEL; ///< Alias for DNS SERVICE CANCEL.
/**
* @brief Windows DNS-SD service registration request data.
*/
typedef struct _DNS_SERVICE_REGISTER_REQUEST {
ULONG Version;
ULONG InterfaceIndex;
PDNS_SERVICE_INSTANCE pServiceInstance;
PDNS_SERVICE_REGISTER_COMPLETE pRegisterCompletionCallback;
PVOID pQueryContext;
HANDLE hCredentials;
BOOL unicastEnabled;
} DNS_SERVICE_REGISTER_REQUEST, *PDNS_SERVICE_REGISTER_REQUEST;
ULONG Version; ///< Windows DNS-SD request structure version.
ULONG InterfaceIndex; ///< Network interface index used for registration.
PDNS_SERVICE_INSTANCE pServiceInstance; ///< Service instance being registered.
PDNS_SERVICE_REGISTER_COMPLETE pRegisterCompletionCallback; ///< Callback invoked when registration completes.
PVOID pQueryContext; ///< Caller-provided context passed to the completion callback.
HANDLE hCredentials; ///< Optional credentials handle supplied to Windows DNS-SD.
BOOL unicastEnabled; ///< Whether the DNS-SD registration is unicast-only.
} DNS_SERVICE_REGISTER_REQUEST, *PDNS_SERVICE_REGISTER_REQUEST; ///< Alias for DNS SERVICE REGISTER REQUEST.
#endif
_FN(_DnsServiceFreeInstance, VOID, (_In_ PDNS_SERVICE_INSTANCE pInstance));
@@ -93,5 +130,13 @@
} /* extern "C" */
namespace platf::publish {
/**
* @brief Handle completion of a Windows DNS-SD registration request.
*
* @param status Native status code returned by the platform API.
* @param pQueryContext Alarm object signaled when registration completes.
* @param pInstance Registered DNS-SD service instance returned by Windows.
* @return Callback has no return value; completion is reported through the alarm.
*/
VOID WINAPI register_cb(DWORD status, PVOID pQueryContext, PDNS_SERVICE_INSTANCE pInstance) {
auto alarm = (safe::alarm_t<PDNS_SERVICE_INSTANCE>::element_type *) pQueryContext;
@@ -169,6 +214,9 @@
return registered_instance ? 0 : -1;
}
/**
* @brief Windows DNS-SD registration lifetime for the advertised Sunshine service.
*/
class mdns_registration_t: public ::platf::deinit_t {
public:
mdns_registration_t():
@@ -196,6 +244,12 @@
PDNS_SERVICE_INSTANCE existing_instance;
};
/**
* @brief Resolve required function pointers from the native library.
*
* @param handle Native library or object handle used by the operation.
* @return 0 when all required DNS-SD functions are resolved; nonzero otherwise.
*/
int load_funcs(HMODULE handle) {
auto fg = util::fail_guard([handle]() {
FreeLibrary(handle);
src/process.cpp +49 −1
@@ -43,10 +43,16 @@
using namespace std::literals;
namespace pt = boost::property_tree;
proc_t proc;
proc_t proc; ///< Global process registry used to track and terminate child processes.
/**
* @brief RAII helper that runs shutdown cleanup when destroyed.
*/
class deinit_t: public platf::deinit_t {
public:
/**
* @brief Destroy the process subsystem deinitializer.
*/
~deinit_t() {
proc.terminate();
}
@@ -94,6 +100,13 @@
}
}
/**
* @brief Resolve the working directory for a configured command.
*
* @param cmd Command line to execute or inspect.
* @param env Environment variables for the child process.
* @return Directory used to launch the command, falling back to PATH lookup when needed.
*/
boost::filesystem::path find_working_directory(const std::string &cmd, boost::process::v1::environment &env) {
// Parse the raw command string into parts to get the actual command portion
std::vector<std::string> parts;
@@ -388,5 +401,12 @@
assert(!_process.running());
}
/**
* @brief Find the closing parenthesis for an environment-variable expression.
*
* @param begin Iterator positioned at the opening parenthesis.
* @param end End iterator for the expression being scanned.
* @return Iterator for the matching closing parenthesis, or end when unmatched.
*/
std::string_view::iterator find_match(std::string_view::iterator begin, std::string_view::iterator end) {
int stack = 0;
@@ -409,6 +429,13 @@
return begin;
}
/**
* @brief Parse env val.
*
* @param env Environment variables for the child process.
* @param val_raw Raw value that may contain $(NAME) substitutions.
* @return Value with recognized environment-variable substitutions expanded.
*/
std::string parse_env_val(boost::process::v1::native_environment &env, const std::string_view &val_raw) {
auto pos = std::begin(val_raw);
auto dollar = std::find(pos, std::end(val_raw), '$');
@@ -498,6 +525,9 @@
return header == PNG_SIGNATURE;
}
/**
* @brief Validate app image path.
*/
std::string validate_app_image_path(std::string app_image_path) {
if (app_image_path.empty()) {
return DEFAULT_APP_IMAGE_PATH;
@@ -545,6 +575,12 @@
return app_image_path;
}
/**
* @brief Calculate the SHA-256 digest for a file.
*
* @param filename File path whose contents should be hashed.
* @return Lowercase hexadecimal SHA-256 digest, or std::nullopt on read/hash failure.
*/
std::optional<std::string> calculate_sha256(const std::string &filename) {
crypto::md_ctx_t ctx {EVP_MD_CTX_create()};
if (!ctx) {
@@ -580,6 +616,12 @@
return ss.str();
}
/**
* @brief Calculate the CRC-32 checksum for a string.
*
* @param input Bytes to include in the checksum.
* @return CRC-32 value for the input bytes.
*/
uint32_t calculate_crc32(const std::string &input) {
boost::crc_32_type result;
result.process_bytes(input.data(), input.length());
@@ -617,5 +659,8 @@
return std::make_tuple(id_no_index, id_with_index);
}
/**
* @brief Parse serialized text into the corresponding runtime representation.
*/
std::optional<proc::proc_t> parse(const std::string &file_name) {
pt::ptree tree;
@@ -748,5 +793,8 @@
return std::nullopt;
}
/**
* @brief Refresh cached platform state from the operating system.
*/
void refresh(const std::string &file_name) {
auto proc_opt = proc::parse(file_name);
src/process.h +86 −11
@@ -5,5 +5,9 @@
#pragma once
#ifndef __kernel_entry
/**
* @def __kernel_entry
* @brief Macro for kernel entry.
*/
#define __kernel_entry
#endif
@@ -21,11 +25,21 @@
#include "rtsp.h"
#include "utility.h"
/**
* @def DEFAULT_APP_IMAGE_PATH
* @brief Macro for DEFAULT APP IMAGE PATH.
*/
#define DEFAULT_APP_IMAGE_PATH SUNSHINE_ASSETS_DIR "/box.png"
namespace proc {
/**
* @brief Boost.Process pipe stream used for child-process I/O.
*/
using file_t = util::safe_ptr_v2<FILE, int, fclose>;
/**
* @brief Parsed command arguments used when launching a child process.
*/
typedef config::prep_cmd_t cmd_t;
/**
@@ -41,7 +55,7 @@
* filename -- The output of the commands are appended to filename
*/
struct ctx_t {
std::vector<cmd_t> prep_cmds; ///< Prep cmds.
std::vector<cmd_t> prep_cmds;
/**
* Some applications, such as Steam, either exit quickly, or keep running indefinitely.
@@ -57,22 +71,31 @@
*/
std::vector<std::string> detached;
std::string name; ///< Human-readable name for this item.
std::string cmd; ///< Command line used to launch the application.
std::string working_dir; ///< Working dir.
std::string output; ///< Captured output from the launched process.
std::string image_path; ///< Image path.
std::string id; ///< Stable identifier for the configured application.
bool elevated; ///< Whether the process should be launched elevated.
bool auto_detach; ///< Whether the process should detach automatically.
bool wait_all; ///< Whether Sunshine waits for all child processes.
std::string name;
std::string cmd;
std::string working_dir;
std::string output;
std::string image_path;
std::string id;
bool elevated;
bool auto_detach;
bool wait_all;
std::chrono::seconds exit_timeout;
std::chrono::seconds exit_timeout; ///< Exit timeout.
};
/**
* @brief Tracks launched child processes and terminates them during shutdown.
*/
class proc_t {
public:
KITTY_DEFAULT_CONSTR_MOVE_THROW(proc_t)
/**
* @brief Construct a process manager.
*
* @param env Environment used when launching processes.
* @param apps Application launch contexts.
*/
proc_t(
boost::process::v1::environment &&env,
std::vector<ctx_t> &&apps
@@ -82,6 +105,13 @@
_apps(std::move(apps)) {
}
/**
* @brief Launch the configured application process.
*
* @param app_id App ID.
* @param launch_session Launch session.
* @return Process exit code or launch error status.
*/
int execute(int app_id, std::shared_ptr<rtsp_stream::launch_session_t> launch_session);
/**
@@ -91,10 +121,34 @@
~proc_t();
/**
* @brief Return the configured applications.
*
* @return Immutable application list owned by the process manager.
*/
const std::vector<ctx_t> &get_apps() const;
/**
* @brief Return the configured applications.
*
* @return Mutable application list owned by the process manager.
*/
std::vector<ctx_t> &get_apps();
/**
* @brief Get app image.
*
* @param app_id App ID.
* @return Validated image path for the requested application.
*/
std::string get_app_image(int app_id);
/**
* @brief Get last run app name.
*
* @return Name of the most recently launched application.
*/
std::string get_last_run_app_name();
/**
* @brief Terminate the launched application process.
*/
void terminate();
private:
@@ -119,12 +173,33 @@
/**
* @brief Calculate a stable id based on name and image data
* @return Tuple of id calculated without index (for use if no collision) and one with.
*
* @param app_name App name.
* @param app_image_path App image path.
* @param index Zero-based index of the item being addressed.
*/
std::tuple<std::string, std::string> calculate_app_id(const std::string &app_name, std::string app_image_path, int index);
bool check_valid_png(const std::filesystem::path &path);
/**
* @brief Validate app image path.
*
* @param app_image_path Candidate image path from the application configuration.
* @return Existing PNG path, or the default application image when validation fails.
*/
std::string validate_app_image_path(std::string app_image_path);
/**
* @brief Refresh cached platform state from the operating system.
*
* @param file_name File name.
*/
void refresh(const std::string &file_name);
/**
* @brief Parse serialized text into the corresponding runtime representation.
*
* @param file_name File name.
* @return Parsed value or parse status.
*/
std::optional<proc::proc_t> parse(const std::string &file_name);
/**
src/round_robin.h +175 −0
@@ -13,20 +13,56 @@
* @tparam T The iterator type.
*/
namespace round_robin_util {
/**
* @brief CRTP base that provides iterator operators for round-robin iterators.
*/
template<class V, class T>
class it_wrap_t {
public:
/**
* @brief Iterator category advertised to standard algorithms.
*/
using iterator_category = std::random_access_iterator_tag;
/**
* @brief Value type exposed by the wrapped iterator.
*/
using value_type = V;
/**
* @brief Difference type used for iterator movement.
*/
using difference_type = V;
/**
* @brief Mutable pointer to a value in the cycled range.
*/
using pointer = V *;
/**
* @brief Const pointer to a value in the cycled range.
*/
using const_pointer = V const *;
/**
* @brief Mutable reference to a value in the cycled range.
*/
using reference = V &;
/**
* @brief Const reference to a value in the cycled range.
*/
using const_reference = V const &;
/**
* @brief Concrete iterator type supplied by the CRTP child.
*/
typedef T iterator;
/**
* @brief Signed offset type used when moving through the range.
*/
typedef std::ptrdiff_t diff_t;
/**
* @brief Advance this iterator by repeatedly wrapping at the end of the range.
*
* @param step Number of positions to advance.
* @return Iterator positioned after the requested number of wrapped increments.
*/
iterator operator+=(diff_t step) {
while (step-- > 0) {
++_this();
@@ -35,6 +71,12 @@
return _this();
}
/**
* @brief Move this iterator backward by repeatedly wrapping at the beginning of the range.
*
* @param step Number of positions to rewind.
* @return Iterator positioned after the requested number of wrapped decrements.
*/
iterator operator-=(diff_t step) {
while (step-- > 0) {
--_this();
@@ -43,18 +85,36 @@
return _this();
}
/**
* @brief Return a copy advanced by a wrapped offset.
*
* @param step Number of positions to advance.
* @return Advanced iterator copy.
*/
iterator operator+(diff_t step) {
iterator new_ = _this();
return new_ += step;
}
/**
* @brief Return a copy moved backward by a wrapped offset.
*
* @param step Number of positions to rewind.
* @return Rewound iterator copy.
*/
iterator operator-(diff_t step) {
iterator new_ = _this();
return new_ -= step;
}
/**
* @brief Count wrapped increments needed to reach this iterator from another one.
*
* @param first Iterator used as the starting position.
* @return Number of increments from `first` to this iterator.
*/
diff_t operator-(iterator first) {
diff_t step = 0;
while (first != _this()) {
@@ -65,15 +125,30 @@
return step;
}
/**
* @brief Advance to the next element, wrapping back to the beginning when needed.
*
* @return Iterator after advancing.
*/
iterator operator++() {
_this().inc();
return _this();
}
/**
* @brief Move to the previous element, wrapping to the end when needed.
*
* @return Iterator after moving backward.
*/
iterator operator--() {
_this().dec();
return _this();
}
/**
* @brief Advance to the next element and return the previous iterator position.
*
* @return Iterator position before advancing.
*/
iterator operator++(int) {
iterator new_ = _this();
@@ -83,5 +158,10 @@
return new_;
}
/**
* @brief Move to the previous element and return the previous iterator position.
*
* @return Iterator position before moving backward.
*/
iterator operator--(int) {
iterator new_ = _this();
@@ -91,42 +171,98 @@
return new_;
}
/**
* @brief Dereference the current element in the cycled range.
*
* @return Mutable reference to the current value.
*/
reference operator*() {
return *_this().get();
}
/**
* @brief Dereference the current element in the cycled range.
*
* @return Const reference to the current value.
*/
const_reference operator*() const {
return *_this().get();
}
/**
* @brief Access the current element in the cycled range.
*
* @return Mutable pointer to the current value.
*/
pointer operator->() {
return &*_this();
}
/**
* @brief Access the current element in the cycled range.
*
* @return Const pointer to the current value.
*/
const_pointer operator->() const {
return &*_this();
}
/**
* @brief Compare whether two wrapped iterator positions differ.
*
* @param other Iterator position to compare against.
* @return True when the iterators do not refer to the same position.
*/
bool operator!=(const iterator &other) const {
return !(_this() == other);
}
/**
* @brief Compare whether this wrapped position sorts before another one.
*
* @param other Iterator position to compare against.
* @return True when this iterator is ordered before `other`.
*/
bool operator<(const iterator &other) const {
return !(_this() >= other);
}
/**
* @brief Compare whether this wrapped position sorts at or after another one.
*
* @param other Iterator position to compare against.
* @return True when this iterator is ordered at or after `other`.
*/
bool operator>=(const iterator &other) const {
return _this() == other || _this() > other;
}
/**
* @brief Compare whether this wrapped position sorts at or before another one.
*
* @param other Iterator position to compare against.
* @return True when this iterator is ordered at or before `other`.
*/
bool operator<=(const iterator &other) const {
return _this() == other || _this() < other;
}
/**
* @brief Compare whether two wrapped iterator positions are equal.
*
* @param other Iterator position to compare against.
* @return True when the iterators refer to the same wrapped position.
*/
bool operator==(const iterator &other) const {
return _this().eq(other);
};
/**
* @brief Compare whether this wrapped position sorts after another one.
*
* @param other Iterator position to compare against.
* @return True when this iterator is ordered after `other`.
*/
bool operator>(const iterator &other) const {
return _this().gt(other);
}
@@ -141,17 +277,35 @@
}
};
/**
* @brief Iterator that cycles indefinitely over a fixed begin/end range.
*/
template<class V, class It>
class round_robin_t: public it_wrap_t<V, round_robin_t<V, It>> {
public:
/**
* @brief Underlying iterator type for the cycled range.
*/
using iterator = It;
/**
* @brief Mutable pointer to values in the cycled range.
*/
using pointer = V *;
/**
* @brief Construct a round-robin iterator over a fixed range.
*
* @param begin First element in the range to cycle through.
* @param end One-past-the-end iterator for the range to cycle through.
*/
round_robin_t(iterator begin, iterator end):
_begin(begin),
_end(end),
_pos(begin) {
}
/**
* @brief Advance the iterator to the next element.
*/
void inc() {
++_pos;
@@ -161,6 +315,9 @@
}
}
/**
* @brief Move the iterator to the previous element.
*/
void dec() {
if (_pos == _begin) {
_pos = _end;
@@ -169,10 +326,21 @@
--_pos;
}
/**
* @brief Compare two iterators for equality.
*
* @param other Iterator or container to compare against.
* @return True when both iterators point to equivalent values.
*/
bool eq(const round_robin_t &other) const {
return *_pos == *other._pos;
}
/**
* @brief Return the currently wrapped value or handle.
*
* @return Underlying native handle or object pointer.
*/
pointer get() const {
return &*_pos;
}
@@ -184,6 +352,13 @@
It _pos;
};
/**
* @brief Create a round-robin iterator over a fixed range.
*
* @param begin Iterator or pointer marking the start of the input range.
* @param end Iterator or pointer marking the end of the input range.
* @return Iterator initialized to `begin` and wrapping before `end`.
*/
template<class V, class It>
round_robin_t<V, It> make_round_robin(It begin, It end) {
return round_robin_t<V, It>(begin, end);
src/rswrapper.c +84 −4
@@ -12,31 +12,107 @@
// The assert() function is decorated with __cold on macOS which
// is incompatible with Clang's target multiversioning feature
#ifndef NDEBUG
/**
* @def NDEBUG
* @brief Macro for NDEBUG.
*/
#define NDEBUG
#endif
/**
* @def DECORATE_FUNC_I(a, b)
* @brief Macro for DECORATE FUNC i.
*/
#define DECORATE_FUNC_I(a, b) a##b
/**
* @def DECORATE_FUNC(a, b)
* @brief Macro for DECORATE FUNC.
*/
#define DECORATE_FUNC(a, b) DECORATE_FUNC_I(a, b)
// Append an ISA suffix to the public RS API
#define reed_solomon_init DECORATE_FUNC(reed_solomon_init, ISA_SUFFIX)
/**
* @def reed_solomon_new
* @brief Macro for reed solomon new.
*/
#define reed_solomon_new DECORATE_FUNC(reed_solomon_new, ISA_SUFFIX)
/**
* @def reed_solomon_new_static
* @brief Macro for reed solomon new static.
*/
#define reed_solomon_new_static DECORATE_FUNC(reed_solomon_new_static, ISA_SUFFIX)
/**
* @def reed_solomon_release
* @brief Macro for reed solomon release.
*/
#define reed_solomon_release DECORATE_FUNC(reed_solomon_release, ISA_SUFFIX)
/**
* @def reed_solomon_decode
* @brief Macro for reed solomon decode.
*/
#define reed_solomon_decode DECORATE_FUNC(reed_solomon_decode, ISA_SUFFIX)
/**
* @def reed_solomon_encode
* @brief Macro for reed solomon encode.
*/
#define reed_solomon_encode DECORATE_FUNC(reed_solomon_encode, ISA_SUFFIX)
// Append an ISA suffix to internal functions to prevent multiple definition errors
/**
* @def obl_axpy_ref
* @brief Macro for obl axpy ref.
*/
#define obl_axpy_ref DECORATE_FUNC(obl_axpy_ref, ISA_SUFFIX)
/**
* @def obl_scal_ref
* @brief Macro for obl scal ref.
*/
#define obl_scal_ref DECORATE_FUNC(obl_scal_ref, ISA_SUFFIX)
/**
* @def obl_axpyb32_ref
* @brief Macro for obl axpyb32 ref.
*/
#define obl_axpyb32_ref DECORATE_FUNC(obl_axpyb32_ref, ISA_SUFFIX)
/**
* @def obl_axpy
* @brief Macro for obl axpy.
*/
#define obl_axpy DECORATE_FUNC(obl_axpy, ISA_SUFFIX)
/**
* @def obl_scal
* @brief Macro for obl scal.
*/
#define obl_scal DECORATE_FUNC(obl_scal, ISA_SUFFIX)
/**
* @def obl_swap
* @brief Macro for obl swap.
*/
#define obl_swap DECORATE_FUNC(obl_swap, ISA_SUFFIX)
/**
* @def obl_axpyb32
* @brief Macro for obl axpyb32.
*/
#define obl_axpyb32 DECORATE_FUNC(obl_axpyb32, ISA_SUFFIX)
/**
* @def axpy
* @brief Macro for axpy.
*/
#define axpy DECORATE_FUNC(axpy, ISA_SUFFIX)
/**
* @def scal
* @brief Macro for scal.
*/
#define scal DECORATE_FUNC(scal, ISA_SUFFIX)
/**
* @def gemm
* @brief Macro for gemm.
*/
#define gemm DECORATE_FUNC(gemm, ISA_SUFFIX)
/**
* @def invert_mat
* @brief Macro for invert mat.
*/
#define invert_mat DECORATE_FUNC(invert_mat, ISA_SUFFIX)
#if defined(__x86_64) || defined(__x86_64__) || defined(__amd64) || defined(__amd64__) || defined(_M_AMD64)
@@ -98,6 +174,10 @@
#endif
// Compile a default variant
/**
* @def ISA_SUFFIX
* @brief Macro for ISA SUFFIX.
*/
#define ISA_SUFFIX _def
#include "../third-party/nanors/deps/obl/autoshim.h"
#include "../third-party/nanors/rs.c"
@@ -112,10 +192,10 @@
#include "rswrapper.h"
reed_solomon_new_t reed_solomon_new_fn; ///< Reed solomon new.
reed_solomon_release_t reed_solomon_release_fn; ///< Reed solomon release.
reed_solomon_encode_t reed_solomon_encode_fn; ///< Reed solomon encode.
reed_solomon_new_t reed_solomon_new_fn;
reed_solomon_release_t reed_solomon_release_fn;
reed_solomon_encode_t reed_solomon_encode_fn;
reed_solomon_decode_t reed_solomon_decode_fn;
reed_solomon_decode_t reed_solomon_decode_fn; ///< Reed solomon decode.
/**
* @brief This initializes the RS function pointers to the best vectorized version available.
src/rswrapper.h +39 −4
@@ -8,23 +8,58 @@
// standard includes
#include <stdint.h>
/**
* @def DATA_SHARDS_MAX
* @brief Macro for DATA SHARDS MAX.
*/
#define DATA_SHARDS_MAX 255
/**
* @brief Opaque Reed-Solomon encoder/decoder context.
*/
typedef struct _reed_solomon reed_solomon;
/**
* @brief Function pointer used to create a Reed-Solomon context.
*/
typedef reed_solomon *(*reed_solomon_new_t)(int data_shards, int parity_shards);
/**
* @brief Function pointer used to release a Reed-Solomon context.
*/
typedef void (*reed_solomon_release_t)(reed_solomon *rs);
/**
* @brief Function pointer used to encode Reed-Solomon recovery shards.
*/
typedef int (*reed_solomon_encode_t)(reed_solomon *rs, uint8_t **shards, int nr_shards, int bs);
/**
* @brief Function pointer used to decode Reed-Solomon recovery shards.
*/
typedef int (*reed_solomon_decode_t)(reed_solomon *rs, uint8_t **shards, uint8_t *marks, int nr_shards, int bs);
extern reed_solomon_new_t reed_solomon_new_fn; ///< Reed solomon new.
extern reed_solomon_release_t reed_solomon_release_fn; ///< Reed solomon release.
extern reed_solomon_encode_t reed_solomon_encode_fn; ///< Reed solomon encode.
extern reed_solomon_new_t reed_solomon_new_fn;
extern reed_solomon_release_t reed_solomon_release_fn;
extern reed_solomon_encode_t reed_solomon_encode_fn;
extern reed_solomon_decode_t reed_solomon_decode_fn;
extern reed_solomon_decode_t reed_solomon_decode_fn; ///< Reed solomon decode.
/**
* @def reed_solomon_new
* @brief Macro for reed solomon new.
*/
#define reed_solomon_new reed_solomon_new_fn
/**
* @def reed_solomon_release
* @brief Macro for reed solomon release.
*/
#define reed_solomon_release reed_solomon_release_fn
/**
* @def reed_solomon_encode
* @brief Macro for reed solomon encode.
*/
#define reed_solomon_encode reed_solomon_encode_fn
/**
* @def reed_solomon_decode
* @brief Macro for reed solomon decode.
*/
#define reed_solomon_decode reed_solomon_decode_fn
/**
src/rtsp.cpp +185 −16
@@ -40,5 +40,10 @@
using namespace std::literals;
namespace rtsp_stream {
/**
* @brief Release msg resources.
*
* @param msg Raw RTSP message buffer being parsed or encrypted.
*/
void free_msg(PRTSP_MESSAGE msg) {
freeMessage(msg);
@@ -48,46 +53,102 @@
#pragma pack(push, 1)
/**
* @brief Encrypted RTSP message header used by GameStream.
*/
struct encrypted_rtsp_header_t {
// We set the MSB in encrypted RTSP messages to allow format-agnostic
// parsing code to be able to tell encrypted from plaintext messages.
static constexpr std::uint32_t ENCRYPTED_MESSAGE_TYPE_BIT = 0x80000000; ///< Protocol or platform constant for encrypted message type bit.
static constexpr std::uint32_t ENCRYPTED_MESSAGE_TYPE_BIT = 0x80000000;
/**
* @brief Return a pointer to the protocol payload following the packet header.
*
* @return Parsed or serialized payload data.
*/
uint8_t *payload() {
return (uint8_t *) (this + 1);
}
/**
* @brief Decode the RTSP interleaved-frame payload length.
*
* @return Payload byte count with the encrypted-message flag masked out.
*/
std::uint32_t payload_length() {
return util::endian::big<std::uint32_t>(typeAndLength) & ~ENCRYPTED_MESSAGE_TYPE_BIT;
}
/**
* @brief Check whether encrypted.
*
* @return True when the RTSP connection is using TLS.
*/
bool is_encrypted() {
return !!(util::endian::big<std::uint32_t>(typeAndLength) & ENCRYPTED_MESSAGE_TYPE_BIT);
}
// This field is the length of the payload + ENCRYPTED_MESSAGE_TYPE_BIT in big-endian
std::uint32_t typeAndLength; ///< Type and length.
std::uint32_t typeAndLength;
// This field is the number used to initialize the bottom 4 bytes of the AES IV in big-endian
std::uint32_t sequenceNumber;
std::uint32_t sequenceNumber; ///< Sequence number.
// This field is the AES GCM authentication tag
std::uint8_t tag[16]; ///< Authentication tag appended to the encrypted payload.
std::uint8_t tag[16];
};
#pragma pack(pop)
class rtsp_server_t;
/**
* @brief Owning pointer for an RTSP message allocated by moonlight-common-c.
*/
using msg_t = util::safe_ptr<RTSP_MESSAGE, free_msg>;
/**
* @brief RTSP command handler signature used by the server dispatch table.
*/
using cmd_func_t = std::function<void(rtsp_server_t *server, tcp::socket &, launch_session_t &, msg_t &&)>;
/**
* @brief Write msg details to the log.
*
* @param msg Raw RTSP message buffer being parsed or encrypted.
*/
void print_msg(PRTSP_MESSAGE msg);
/**
* @brief Send the RTSP response used for unsupported commands.
*
* @param sock Socket used to read or write the protocol message.
* @param session Active streaming or pairing session for the request.
* @param req Parsed RTSP request being handled.
*/
void cmd_not_found(tcp::socket &sock, launch_session_t &, msg_t &&req);
/**
* @brief Send the protocol response for the current request.
*
* @param sock Socket used to read or write the protocol message.
* @param session Active streaming or pairing session for the request.
* @param options Request options or socket options to apply.
* @param statuscode RTSP status code to send in the response.
* @param status_msg Status msg.
* @param seqn RTSP CSeq value associated with the request.
* @param payload Optional payload body to include in the response.
*/
void respond(tcp::socket &sock, launch_session_t &session, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload);
/**
* @brief RTSP client socket state and receive buffer parser.
*/
class socket_t: public std::enable_shared_from_this<socket_t> {
public:
/**
* @brief Construct an RTSP socket wrapper around an accepted TCP connection.
*
* @param io_context Io context.
* @param handle_data_fn Handle data.
*/
socket_t(boost::asio::io_context &io_context, std::function<void(tcp::socket &sock, launch_session_t &, msg_t &&)> &&handle_data_fn):
handle_data_fn {std::move(handle_data_fn)},
sock {io_context} {
@@ -125,7 +186,7 @@
}
/**
* @brief Handle the initial read of the header of an encrypted message.
* @brief Read the encrypted RTSP header before receiving the encrypted payload.
* @param socket The socket the message was received on.
* @param ec The error code of the read operation.
* @param bytes The number of bytes read.
@@ -174,7 +235,7 @@
}
/**
* @brief Read and decrypt the encrypted RTSP payload.
* @brief Handle the final read of the content of an encrypted message.
* @param socket The socket the message was received on.
* @param ec The error code of the read operation.
* @param bytes The number of bytes read.
@@ -265,7 +326,7 @@
}
/**
* @brief Handle the read of the payload portion of a plaintext message.
* @brief Read the plaintext RTSP payload after the header is parsed.
* @param socket The socket the message was received on.
* @param ec The error code of the read operation.
* @param bytes The number of bytes read.
@@ -335,7 +396,7 @@
}
/**
* @brief Handle the read of the header portion of a plaintext message.
* @brief Read and parse the plaintext RTSP request header.
* @param socket The socket the message was received on.
* @param ec The error code of the read operation.
* @param bytes The number of bytes read.
@@ -382,28 +443,44 @@
handle_plaintext_payload(socket, ec, buf_size);
}
/**
* @brief Dispatch a parsed RTSP request to the socket's command handler.
*
* @param req Parsed RTSP request being handled.
*/
void handle_data(msg_t &&req) {
handle_data_fn(sock, *session, std::move(req));
}
std::function<void(tcp::socket &sock, launch_session_t &, msg_t &&)> handle_data_fn; ///< Command dispatcher installed by the RTSP server.
std::function<void(tcp::socket &sock, launch_session_t &, msg_t &&)> handle_data_fn;
tcp::socket sock; ///< TCP socket connected to the RTSP client.
tcp::socket sock;
std::array<char, 2048> msg_buf;
std::array<char, 2048> msg_buf; ///< Receive buffer for one RTSP request header and payload.
char *crlf; ///< Pointer to the next CRLF delimiter while parsing the receive buffer.
char *crlf;
char *begin = msg_buf.data();
char *begin = msg_buf.data(); ///< Start of the unparsed data currently in `msg_buf`.
std::shared_ptr<launch_session_t> session;
std::shared_ptr<launch_session_t> session; ///< Launch session claimed by this RTSP socket.
};
/**
* @brief RTSP listener that matches incoming clients to pending launch sessions.
*/
class rtsp_server_t {
public:
~rtsp_server_t() {
clear();
}
/**
* @brief Bind the underlying socket or graphics resource to its target.
*
* @param af Address family used for socket creation or binding.
* @param port TCP or UDP port number.
* @param ec Error code returned by the asynchronous operation.
* @return Network operation status.
*/
int bind(net::af_e af, std::uint16_t port, boost::system::error_code &ec) {
acceptor.open(af == net::IPV4 ? tcp::v4() : tcp::v6(), ec);
if (ec) {
@@ -440,6 +517,13 @@
return 0;
}
/**
* @brief Dispatch a parsed RTSP request to its handler.
*
* @param sock Socket used to read or write the protocol message.
* @param session Active streaming or pairing session for the request.
* @param req Parsed RTSP request being handled.
*/
void handle_msg(tcp::socket &sock, launch_session_t &session, msg_t &&req) {
auto func = _map_cmd_cb.find(req->message.request.command);
if (func != std::end(_map_cmd_cb)) {
@@ -452,6 +536,11 @@
sock.shutdown(boost::asio::socket_base::shutdown_type::shutdown_both, ec);
}
/**
* @brief Accept a pending connection and arm the server for the next client.
*
* @param ec Error code returned by the asynchronous operation.
*/
void handle_accept(const boost::system::error_code &ec) {
if (ec) {
BOOST_LOG(error) << "Couldn't accept incoming connections: "sv << ec.message();
@@ -486,6 +575,12 @@
});
}
/**
* @brief Register or visit handlers stored in the map.
*
* @param type Protocol, message, or resource type selector.
* @param cb Callback invoked for each matching message or session.
*/
void map(const std::string_view &type, cmd_func_t cb) {
_map_cmd_cb.emplace(type, std::move(cb));
}
@@ -544,7 +639,7 @@
return (int) _session_slots->size();
}
safe::event_t<std::shared_ptr<launch_session_t>> launch_event;
safe::event_t<std::shared_ptr<launch_session_t>> launch_event; ///< Launch event.
/**
* @brief Clear launch sessions.
@@ -569,6 +664,11 @@
}
}
/**
* @brief Clear by cert state.
*
* @param cert Certificate data or object used by the operation.
*/
void clear_by_cert(std::string_view cert) {
auto lg = _session_slots.lock();
for (auto i = _session_slots->begin(); i != _session_slots->end();) {
@@ -636,8 +736,11 @@
std::shared_ptr<socket_t> next_socket;
};
rtsp_server_t server {};
rtsp_server_t server {}; ///< Singleton RTSP server used by GameStream launch sessions.
/**
* @brief Queue a launch session until the RTSP client connects.
*/
void launch_session_raise(std::shared_ptr<launch_session_t> launch_session) {
server.session_raise(std::move(launch_session));
}
@@ -657,9 +760,19 @@
server.clear(true);
}
/**
* @brief Terminate active sessions associated with a client certificate.
*/
void terminate_sessions_by_cert(std::string_view cert) {
server.clear_by_cert(cert);
}
/**
* @brief Send the serialized response over the active socket.
*
* @param sock Socket used to read or write the protocol message.
* @param sv String view containing the text to inspect.
* @return Network operation status.
*/
int send(tcp::socket &sock, const std::string_view &sv) {
std::size_t bytes_send = 0;
@@ -677,5 +790,12 @@
return 0;
}
/**
* @brief Send the protocol response for the current request.
*
* @param sock Socket used to read or write the protocol message.
* @param session Active streaming or pairing session for the request.
* @param resp RTSP response string to send to the client.
*/
void respond(tcp::socket &sock, launch_session_t &session, msg_t &resp) {
auto payload = std::make_pair(resp->payload, resp->payloadLength);
@@ -745,6 +865,9 @@
}
}
/**
* @brief Send the protocol response for the current request.
*/
void respond(tcp::socket &sock, launch_session_t &session, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload) {
msg_t resp {new msg_t::element_type};
createRtspResponse(resp.get(), nullptr, 0, const_cast<char *>("RTSP/1.0"), statuscode, const_cast<char *>(status_msg), seqn, options, const_cast<char *>(payload.data()), (int) payload.size());
@@ -752,9 +875,20 @@
respond(sock, session, resp);
}
/**
* @brief Handle an unsupported RTSP command.
*/
void cmd_not_found(tcp::socket &sock, launch_session_t &session, msg_t &&req) {
respond(sock, session, nullptr, 404, "NOT FOUND", req->sequenceNumber, {});
}
/**
* @brief Handle RTSP OPTIONS requests.
*
* @param server RTSP server instance handling the request.
* @param sock Socket used to read or write the protocol message.
* @param session Active streaming or pairing session for the request.
* @param req Parsed RTSP request being handled.
*/
void cmd_option(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) {
OPTION_ITEM option {};
@@ -768,5 +902,13 @@
respond(sock, session, &option, 200, "OK", req->sequenceNumber, {});
}
/**
* @brief Handle RTSP DESCRIBE requests.
*
* @param server RTSP server instance.
* @param sock Client socket.
* @param session Launch session.
* @param req RTSP request message.
*/
void cmd_describe(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) {
OPTION_ITEM option {};
@@ -851,5 +993,13 @@
respond(sock, session, &option, 200, "OK", req->sequenceNumber, ss.str());
}
/**
* @brief Handle RTSP SETUP requests and prepare transport state.
*
* @param server RTSP server instance handling the request.
* @param sock Socket used to read or write the protocol message.
* @param session Active streaming or pairing session for the request.
* @param req Parsed RTSP request being handled.
*/
void cmd_setup(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) {
OPTION_ITEM options[4] {};
@@ -910,5 +1060,13 @@
respond(sock, session, &seqn, 200, "OK", req->sequenceNumber, {});
}
/**
* @brief Handle RTSP ANNOUNCE requests from the client.
*
* @param server RTSP server instance handling the request.
* @param sock Socket used to read or write the protocol message.
* @param session Active streaming or pairing session for the request.
* @param req Parsed RTSP request being handled.
*/
void cmd_announce(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) {
OPTION_ITEM option {};
@@ -1153,5 +1311,13 @@
respond(sock, session, &option, 200, "OK", req->sequenceNumber, {});
}
/**
* @brief Handle RTSP PLAY requests and start streaming.
*
* @param server RTSP server instance handling the request.
* @param sock Socket used to read or write the protocol message.
* @param session Active streaming or pairing session for the request.
* @param req Parsed RTSP request being handled.
*/
void cmd_play(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) {
OPTION_ITEM option {};
@@ -1209,5 +1375,8 @@
rtsp_thread.join();
}
/**
* @brief Write msg details to the log.
*/
void print_msg(PRTSP_MESSAGE msg) {
std::string_view type = msg->type == TYPE_RESPONSE ? "RESPONSE"sv : "REQUEST"sv;
src/rtsp.h +35 −22
@@ -12,36 +12,44 @@
#include "thread_safe.h"
namespace rtsp_stream {
constexpr auto RTSP_SETUP_PORT = 21;
constexpr auto RTSP_SETUP_PORT = 21; ///< GameStream base-port offset used for the RTSP setup listener.
/**
* @brief RTSP launch session state shared with stream setup.
*/
struct launch_session_t {
uint32_t id;
uint32_t id; ///< RTSP launch-session identifier assigned before stream startup.
crypto::aes_t gcm_key; ///< AES-GCM key negotiated for encrypted RTSP messages.
crypto::aes_t gcm_key;
crypto::aes_t iv;
crypto::aes_t iv; ///< Initial RTSP AES-GCM IV supplied by the client.
std::string av_ping_payload; ///< AV ping payload.
std::string av_ping_payload;
uint32_t control_connect_data;
uint32_t control_connect_data; ///< Client-provided token used when connecting the control channel.
bool host_audio;
std::string unique_id;
bool host_audio; ///< Whether host audio should be played locally.
std::string unique_id; ///< Moonlight client unique identifier for this launch request.
int width; ///< Frame or display width in pixels.
int height; ///< Frame or display height in pixels.
int fps; ///< Requested video frame rate.
int gcmap; ///< Game controller mapping requested by the client.
int appid; ///< Application ID requested for launch or resume.
int surround_info; ///< Encoded GameStream surround-sound capability flags.
std::string surround_params; ///< Client-provided surround-sound layout parameters.
bool continuous_audio; ///< Whether audio packets continue during silence.
bool enable_hdr; ///< Whether HDR streaming is requested.
bool enable_sops; ///< Whether sequence output protection is requested.
int width;
int height;
int fps;
int gcmap;
int appid;
int surround_info;
std::string surround_params;
bool continuous_audio;
bool enable_hdr;
bool enable_sops;
std::optional<crypto::cipher::gcm_t> rtsp_cipher;
std::string rtsp_url_scheme;
uint32_t rtsp_iv_counter;
std::string client_cert;
std::optional<crypto::cipher::gcm_t> rtsp_cipher; ///< AES-GCM cipher used once encrypted RTSP is negotiated.
std::string rtsp_url_scheme; ///< URL scheme selected by the RTSP SETUP flow.
uint32_t rtsp_iv_counter; ///< Counter value mixed into encrypted RTSP IVs.
std::string client_cert; ///< PEM certificate for the paired Moonlight client.
};
/**
* @brief Queue a launch session until the RTSP client connects.
*
* @param launch_session Session state prepared by the GameStream launch handler.
*/
void launch_session_raise(std::shared_ptr<launch_session_t> launch_session);
/**
@@ -60,6 +68,11 @@
* @brief Terminates all running streaming sessions.
*/
void terminate_sessions();
/**
* @brief Terminate active sessions associated with a client certificate.
*
* @param cert Certificate data or object used by the operation.
*/
void terminate_sessions_by_cert(std::string_view cert);
/**
src/stat_trackers.cpp +6 −0
@@ -7,10 +7,16 @@
namespace stat_trackers {
/**
* @brief Create a Boost formatter with one fractional digit.
*/
boost::format one_digit_after_decimal() {
return boost::format("%1$.1f");
}
/**
* @brief Create a Boost formatter with two fractional digits.
*/
boost::format two_digits_after_decimal() {
return boost::format("%1$.2f");
}
src/stat_trackers.h +26 −0
@@ -14,15 +14,38 @@
namespace stat_trackers {
/**
* @brief Create a formatter for values with one digit after the decimal point.
*
* @return Boost format configured for one fractional digit.
*/
boost::format one_digit_after_decimal();
/**
* @brief Create a formatter for values with two digits after the decimal point.
*
* @return Boost format configured for two fractional digits.
*/
boost::format two_digits_after_decimal();
/**
* @brief Accumulates minimum, maximum, and average values between periodic callbacks.
*/
template<typename T>
class min_max_avg_tracker {
public:
/**
* @brief Callback invoked with the minimum, maximum, and average for one interval.
*/
using callback_function = std::function<void(T stat_min, T stat_max, double stat_avg)>;
/**
* @brief Add one statistic sample and invoke the callback when the interval elapses.
*
* @param stat Statistic value used to update the tracker.
* @param callback Callback invoked with the calculated statistic.
* @param interval_in_seconds Minimum time between callback invocations.
*/
void collect_and_callback_on_interval(T stat, const callback_function &callback, std::chrono::seconds interval_in_seconds) {
if (data.calls == 0) {
data.last_callback_time = std::chrono::steady_clock::now();
@@ -36,6 +59,9 @@
data.calls += 1;
}
/**
* @brief Reset the object to its initial empty state.
*/
void reset() {
data = {};
}
src/stream.cpp +350 −104
@@ -34,21 +34,21 @@
#include "thread_safe.h"
#include "utility.h"
constexpr int IDX_START_A = 0;
constexpr int IDX_START_B = 1;
constexpr int IDX_INVALIDATE_REF_FRAMES = 2;
constexpr int IDX_LOSS_STATS = 3;
constexpr int IDX_INPUT_DATA = 5;
constexpr int IDX_RUMBLE_DATA = 6;
constexpr int IDX_TERMINATION = 7;
constexpr int IDX_PERIODIC_PING = 8;
constexpr int IDX_REQUEST_IDR_FRAME = 9;
constexpr int IDX_ENCRYPTED = 10;
constexpr int IDX_HDR_MODE = 11;
constexpr int IDX_RUMBLE_TRIGGER_DATA = 12;
constexpr int IDX_SET_MOTION_EVENT = 13;
constexpr int IDX_SET_RGB_LED = 14;
constexpr int IDX_START_A = 0; ///< Control-stream message index for the first stream-start packet.
constexpr int IDX_START_B = 1; ///< Control-stream message index for the second stream-start packet.
constexpr int IDX_INVALIDATE_REF_FRAMES = 2; ///< Control-stream message index for invalidate ref frames.
constexpr int IDX_LOSS_STATS = 3; ///< Control-stream message index for loss stats.
constexpr int IDX_INPUT_DATA = 5; ///< Control-stream message index for input data.
constexpr int IDX_RUMBLE_DATA = 6; ///< Control-stream message index for rumble data.
constexpr int IDX_TERMINATION = 7; ///< Control-stream message index for termination.
constexpr int IDX_PERIODIC_PING = 8; ///< Control-stream message index for periodic ping.
constexpr int IDX_REQUEST_IDR_FRAME = 9; ///< Control-stream message index for request idr frame.
constexpr int IDX_ENCRYPTED = 10; ///< Control-stream message index for encrypted.
constexpr int IDX_HDR_MODE = 11; ///< Control-stream message index for hdr mode.
constexpr int IDX_RUMBLE_TRIGGER_DATA = 12; ///< Control-stream message index for rumble trigger data.
constexpr int IDX_SET_MOTION_EVENT = 13; ///< Control-stream message index for set motion event.
constexpr int IDX_SET_RGB_LED = 14; ///< Control-stream message index for set rgb led.
constexpr int IDX_SET_ADAPTIVE_TRIGGERS = 15; ///< Control-stream message index for set adaptive triggers.
constexpr int IDX_SET_ADAPTIVE_TRIGGERS = 15;
static const short packetTypes[] = {
0x0305, // Start A
@@ -79,6 +79,9 @@
namespace stream {
/**
* @brief Enumerates supported socket options.
*/
enum class socket_e : int {
video, ///< Video
audio ///< Audio
@@ -86,30 +89,38 @@
#pragma pack(push, 1)
/**
* @brief Packed short video frame header sent before video payload bytes.
*/
struct video_short_frame_header_t {
/**
* @brief Return a pointer to the protocol payload following the packet header.
*
* @return Parsed or serialized payload data.
*/
uint8_t *payload() {
return (uint8_t *) (this + 1);
}
std::uint8_t headerType; ///< Always 0x01 for short headers.
std::uint8_t headerType; // Always 0x01 for short headers
// Sunshine extension
// Frame processing latency, in 1/10 ms units
// zero when the frame is repeated or there is no backend implementation
boost::endian::little_uint16_at frame_processing_latency;
boost::endian::little_uint16_at frame_processing_latency; ///< Frame processing latency.
// Currently known values:
// 1 = Normal P-frame
// 2 = IDR-frame
// 4 = P-frame with intra-refresh blocks
// 5 = P-frame after reference frame invalidation
std::uint8_t frameType;
std::uint8_t frameType; ///< Frame type.
// Length of the final packet payload for codecs that cannot handle
// zero padding, such as AV1 (Sunshine extension).
boost::endian::little_uint16_at lastPayloadLen;
boost::endian::little_uint16_at lastPayloadLen; ///< Last payload len.
std::uint8_t unknown[2];
std::uint8_t unknown[2]; ///< Reserved bytes with no known client-visible meaning.
};
static_assert(
@@ -117,132 +128,207 @@
"Short frame header must be 8 bytes"
);
/**
* @brief Packed RTP and video headers for an unencrypted video packet.
*/
struct video_packet_raw_t {
/**
* @brief Return a pointer to the protocol payload following the packet header.
*
* @return Parsed or serialized payload data.
*/
uint8_t *payload() {
return (uint8_t *) (this + 1);
}
RTP_PACKET rtp;
char reserved[4];
RTP_PACKET rtp; ///< RTP header that prefixes this payload.
char reserved[4]; ///< Reserved protocol padding bytes.
NV_VIDEO_PACKET packet;
NV_VIDEO_PACKET packet; ///< GameStream video packet header.
};
/**
* @brief AES-GCM prefix written before encrypted video packet payloads.
*/
struct video_packet_enc_prefix_t {
/**
* @brief IV.
*/
std::uint8_t iv[12]; // 12-byte IV is ideal for AES-GCM
std::uint32_t frameNumber; ///< Frame number.
std::uint32_t frameNumber;
std::uint8_t tag[16];
std::uint8_t tag[16]; ///< Authentication tag appended to the encrypted payload.
};
/**
* @brief Packed RTP header for an audio packet.
*/
struct audio_packet_t {
RTP_PACKET rtp;
RTP_PACKET rtp; ///< RTP header that prefixes this payload.
};
/**
* @brief Packed control-channel header used before control payloads.
*/
struct control_header_v2 {
std::uint16_t type;
std::uint16_t type; ///< Control message type.
std::uint16_t payloadLength; ///< Payload length.
std::uint16_t payloadLength;
/**
* @brief Return a pointer to the protocol payload following the packet header.
*
* @return Parsed or serialized payload data.
*/
uint8_t *payload() {
return (uint8_t *) (this + 1);
}
};
/**
* @brief Control-channel termination message payload.
*/
struct control_terminate_t {
control_header_v2 header;
control_header_v2 header; ///< Control message header preceding this payload.
std::uint32_t ec;
std::uint32_t ec; ///< Error code reported by the termination message.
};
/**
* @brief Control payload that sets controller rumble motors.
*/
struct control_rumble_t {
control_header_v2 header;
control_header_v2 header; ///< Control message header preceding this payload.
std::uint32_t useless;
std::uint32_t useless; ///< Reserved field kept for protocol compatibility.
std::uint16_t id;
std::uint16_t lowfreq;
std::uint16_t id; ///< Controller identifier associated with this message.
std::uint16_t lowfreq; ///< Low-frequency rumble motor intensity.
std::uint16_t highfreq; ///< High-frequency rumble motor intensity.
std::uint16_t highfreq;
};
/**
* @brief Control payload that sets trigger rumble motors.
*/
struct control_rumble_triggers_t {
control_header_v2 header;
control_header_v2 header; ///< Control message header preceding this payload.
std::uint16_t id;
std::uint16_t left;
std::uint16_t id; ///< Controller identifier associated with this message.
std::uint16_t left; ///< Left trigger or motor intensity.
std::uint16_t right; ///< Right trigger or motor intensity.
std::uint16_t right;
};
/**
* @brief Control payload that enables or disables motion reports.
*/
struct control_set_motion_event_t {
control_header_v2 header;
control_header_v2 header; ///< Control message header preceding this payload.
std::uint16_t id; ///< Controller identifier associated with this message.
std::uint16_t id;
std::uint16_t reportrate;
std::uint16_t reportrate; ///< Requested motion report rate.
std::uint8_t type; ///< Protocol or controller type discriminator.
std::uint8_t type;
};
/**
* @brief Control payload that sets controller RGB LED color.
*/
struct control_set_rgb_led_t {
control_header_v2 header;
control_header_v2 header; ///< Control message header preceding this payload.
std::uint16_t id; ///< Controller identifier associated with this message.
std::uint8_t r; ///< Red LED channel.
std::uint8_t g; ///< Green LED channel.
std::uint8_t b; ///< Blue LED channel.
std::uint16_t id;
std::uint8_t r;
std::uint8_t g;
std::uint8_t b;
};
/**
* @brief Control payload that configures DualSense adaptive triggers.
*/
struct control_adaptive_triggers_t {
control_header_v2 header;
control_header_v2 header; ///< Control message header preceding this payload.
std::uint16_t id;
std::uint16_t id; ///< Controller identifier associated with this message.
/**
* 0x04 - Right trigger
* 0x08 - Left trigger
*/
std::uint8_t event_flags;
std::uint8_t type_left;
std::uint8_t type_right;
std::uint8_t left[DS_EFFECT_PAYLOAD_SIZE];
std::uint8_t type_left; ///< Adaptive-trigger mode for the left trigger.
std::uint8_t type_right; ///< Adaptive-trigger mode for the right trigger.
std::uint8_t left[DS_EFFECT_PAYLOAD_SIZE]; ///< Left adaptive-trigger effect payload.
std::uint8_t right[DS_EFFECT_PAYLOAD_SIZE]; ///< Right adaptive-trigger effect payload.
std::uint8_t right[DS_EFFECT_PAYLOAD_SIZE];
};
/**
* @brief Control payload that toggles HDR mode and carries metadata.
*/
struct control_hdr_mode_t {
control_header_v2 header;
control_header_v2 header; ///< Control message header preceding this payload.
std::uint8_t enabled; ///< Nonzero when HDR should be enabled.
std::uint8_t enabled;
// Sunshine protocol extension
SS_HDR_METADATA metadata; ///< HDR10 metadata sent with the control message.
SS_HDR_METADATA metadata;
};
/**
* @brief Packed encrypted control-channel envelope.
*/
typedef struct control_encrypted_t {
std::uint16_t encryptedHeaderType; ///< Always LE 0x0001.
std::uint16_t encryptedHeaderType; // Always LE 0x0001
std::uint16_t length; // sizeof(seq) + 16 byte tag + secondary header and data
std::uint16_t length; ///< Size of seq, tag, secondary header, and data.
// seq is accepted as an arbitrary value in Moonlight
std::uint32_t seq; // Monotonically increasing sequence number (used as IV for AES-GCM)
std::uint32_t seq; ///< Monotonically increasing sequence number used as the AES-GCM IV.
/**
* @brief Return a pointer to the protocol payload following the packet header.
*
* @return Parsed or serialized payload data.
*/
uint8_t *payload() {
return (uint8_t *) (this + 1);
}
// encrypted control_header_v2 and payload data follow
} *control_encrypted_p;
} *control_encrypted_p; ///< Alias for control encrypted p.
/**
* @brief Packed RTP and FEC headers for an audio recovery packet.
*/
struct audio_fec_packet_t {
RTP_PACKET rtp;
AUDIO_FEC_HEADER fecHeader;
RTP_PACKET rtp; ///< RTP header that prefixes this payload.
AUDIO_FEC_HEADER fecHeader; ///< Audio forward-error-correction header.
};
#pragma pack(pop)
/**
* @brief Round a byte count up to the next PKCS#7 padding boundary.
*
* @param size Number of bytes or elements requested.
* @return `size` rounded up to the next PKCS#7 block boundary.
*/
constexpr std::size_t round_to_pkcs7_padded(std::size_t size) {
return ((size + 15) / 16) * 16;
}
constexpr std::size_t MAX_AUDIO_PACKET_SIZE = 1400;
constexpr std::size_t MAX_AUDIO_PACKET_SIZE = 1400; ///< Protocol or platform constant for max audio packet size.
/**
* @brief AES key storage used for audio packet encryption.
*/
using audio_aes_t = std::array<char, round_to_pkcs7_padded(MAX_AUDIO_PACKET_SIZE)>;
/**
* @brief Audio/video session identifier carried by GameStream packets.
*/
using av_session_id_t = std::variant<asio::ip::address, std::string>; // IP address or SS-Ping-Payload from RTSP handshake
/**
* @brief Mail queue carrying encoded stream packets to sender threads.
*/
using message_queue_t = std::shared_ptr<safe::queue_t<std::pair<udp::endpoint, std::string>>>;
/**
* @brief Shared queue set used to distribute packet queues to broadcast workers.
*/
using message_queue_queue_t = std::shared_ptr<safe::queue_t<std::tuple<socket_e, av_session_id_t, message_queue_t>>>;
// return bytes written on success
@@ -263,7 +349,17 @@
}
}
/**
* @brief ENet control server that routes incoming control packets to stream sessions.
*/
class control_server_t {
public:
/**
* @brief Bind the underlying socket or graphics resource to its target.
*
* @param address_family Address family.
* @param port TCP or UDP port number.
* @return Network operation status.
*/
int bind(net::af_e address_family, std::uint16_t port) {
_host = net::host_create(address_family, _addr, port);
@@ -274,6 +370,13 @@
// Get session associated with address.
// If none are found, try to find a session not yet claimed. (It will be marked by a port of value 0
// If none of those are found, return nullptr
/**
* @brief Return the session value from the backend.
*
* @param peer Remote endpoint associated with the socket.
* @param connect_data Connect data.
* @return Existing session for the peer/connect-data pair, or nullptr when none matches.
*/
session_t *get_session(const net::peer_t peer, uint32_t connect_data);
// Circular dependency:
@@ -281,6 +384,11 @@
// session refers to broadcast_ctx_t
// broadcast_ctx_t refers to control_server_t
// Therefore, iterate is implemented further down the source file
/**
* @brief Visit each active control server session.
*
* @param timeout Maximum time to wait for the operation.
*/
void iterate(std::chrono::milliseconds timeout);
/**
@@ -292,10 +400,23 @@
*/
void call(std::uint16_t type, session_t *session, const std::string_view &payload, bool reinjected);
/**
* @brief Register or visit handlers stored in the map.
*
* @param type Protocol, message, or resource type selector.
* @param cb Callback invoked for each matching message or session.
*/
void map(uint16_t type, std::function<void(session_t *, const std::string_view &)> cb) {
_map_type_cb.emplace(type, std::move(cb));
}
/**
* @brief Send the serialized response over the active socket.
*
* @param payload Optional payload body to include in the response.
* @param peer Remote endpoint associated with the socket.
* @return Network operation status.
*/
int send(const std::string_view &payload, net::peer_t peer) {
auto packet = enet_packet_create(payload.data(), payload.size(), ENET_PACKET_FLAG_RELIABLE);
if (enet_peer_send(peer, 0, packet)) {
@@ -307,54 +428,63 @@
return 0;
}
/**
* @brief Flush pending packets to the stream socket.
*/
void flush() {
enet_host_flush(_host.get());
}
// Callbacks
std::unordered_map<std::uint16_t, std::function<void(session_t *, const std::string_view &)>> _map_type_cb; ///< Control-message handlers keyed by packet type.
std::unordered_map<std::uint16_t, std::function<void(session_t *, const std::string_view &)>> _map_type_cb;
// All active sessions (including those still waiting for a peer to connect)
sync_util::sync_t<std::vector<session_t *>> _sessions; ///< Active sessions registered with the control server.
sync_util::sync_t<std::vector<session_t *>> _sessions;
// ENet peer to session mapping for sessions with a peer connected
sync_util::sync_t<std::map<net::peer_t, session_t *>> _peer_to_session;
sync_util::sync_t<std::map<net::peer_t, session_t *>> _peer_to_session; ///< Peer to session.
ENetAddress _addr; ///< Local ENet address used by the control channel.
net::host_t _host; ///< ENet host object that owns the control socket.
ENetAddress _addr;
net::host_t _host;
};
/**
* @brief UDP broadcast socket and target address state.
*/
struct broadcast_ctx_t {
message_queue_queue_t message_queue_queue;
message_queue_queue_t message_queue_queue; ///< Queues carrying encoded video and audio packets to sender threads.
std::thread recv_thread;
std::thread video_thread;
std::thread audio_thread;
std::thread recv_thread; ///< Thread that receives incoming control-channel messages.
std::thread video_thread; ///< Thread that sends encoded video packets.
std::thread audio_thread; ///< Thread that sends encoded audio packets.
std::thread control_thread; ///< Thread that runs the ENet control server.
std::thread control_thread;
asio::io_context io_context; ///< Asio context used by the UDP broadcast sockets.
asio::io_context io_context;
udp::socket video_sock {io_context};
udp::socket audio_sock {io_context};
udp::socket video_sock {io_context}; ///< UDP socket bound for video packet transmission.
udp::socket audio_sock {io_context}; ///< UDP socket bound for audio packet transmission.
control_server_t control_server;
control_server_t control_server; ///< ENet server for GameStream control packets.
};
/**
* @brief Runtime state for one audio/video streaming session.
*/
struct session_t {
config_t config;
config_t config; ///< Stream or encoder configuration captured for the worker.
safe::mail_t mail;
safe::mail_t mail; ///< Mailbox used to distribute packets and lifecycle events.
std::shared_ptr<input::input_t> input;
std::shared_ptr<input::input_t> input; ///< Platform input device state for this stream.
std::thread audioThread;
std::thread videoThread;
std::thread audioThread; ///< Audio thread.
std::thread videoThread; ///< Video thread.
std::chrono::steady_clock::time_point pingTimeout;
std::chrono::steady_clock::time_point pingTimeout; ///< Deadline for receiving the next client ping.
safe::shared_t<broadcast_ctx_t>::ptr_t broadcast_ref; ///< Shared broadcast context retained while the session is active.
safe::shared_t<broadcast_ctx_t>::ptr_t broadcast_ref;
boost::asio::ip::address localAddress;
boost::asio::ip::address localAddress; ///< Local address.
struct {
std::string ping_payload;
@@ -369,7 +499,7 @@
safe::mail_raw_t::event_t<std::pair<int64_t, int64_t>> invalidate_ref_frames_events;
std::unique_ptr<platf::deinit_t> qos;
} video; ///< Video worker thread state for the active stream.
} video;
struct {
crypto::cipher::cbc_t cipher;
@@ -386,7 +516,7 @@
audio_fec_packet_t fec_packet;
std::unique_ptr<platf::deinit_t> qos;
} audio; ///< Audio capture configuration for the stream..
} audio;
struct {
crypto::cipher::gcm_t cipher;
@@ -402,15 +532,15 @@
platf::feedback_queue_t feedback_queue;
safe::mail_raw_t::event_t<video::hdr_info_t> hdr_queue;
} control;
} control; ///< Runtime state for the encrypted GameStream control channel.
std::uint32_t launch_session_id; ///< RTSP launch-session ID associated with this stream.
std::string client_cert; ///< PEM certificate for the paired client owning the stream.
std::uint32_t launch_session_id;
std::string client_cert;
safe::mail_raw_t::event_t<bool> shutdown_event; ///< Event raised when the stream should shut down.
safe::mail_raw_t::event_t<bool> shutdown_event;
safe::signal_t controlEnd;
safe::signal_t controlEnd; ///< Signal raised when the control channel exits.
std::atomic<session::state_e> state; ///< Current lifecycle state observed by stream workers.
std::atomic<session::state_e> state;
};
/**
@@ -470,7 +600,18 @@
return std::string_view {(char *) tagged_cipher.data(), packet_length + sizeof(control_encrypted_t) - sizeof(control_encrypted_t::seq)};
}
/**
* @brief Start periodic mDNS and service-discovery broadcasts.
*
* @param ctx Native context object used by the operation or callback.
* @return 0 on success; nonzero when broadcast setup fails.
*/
int start_broadcast(broadcast_ctx_t &ctx);
/**
* @brief Stop broadcast processing.
*
* @param ctx Native context object used by the operation or callback.
*/
void end_broadcast(broadcast_ctx_t &ctx);
static auto broadcast = safe::make_shared<broadcast_ctx_t>(start_broadcast, end_broadcast);
@@ -609,31 +750,54 @@
}
namespace fec {
/**
* @brief Owning pointer for a Reed-Solomon encoder instance.
*/
using rs_t = util::safe_ptr<reed_solomon, [](reed_solomon *rs) {
reed_solomon_release(rs);
}>;
/**
* @brief Reed-Solomon FEC encoder state for video packets.
*/
struct fec_t {
size_t data_shards; ///< Number of original packet shards in each FEC block.
size_t nr_shards; ///< Total data and recovery shards generated for each FEC block.
size_t percentage; ///< Recovery-shard percentage requested for the stream.
size_t data_shards;
size_t nr_shards;
size_t percentage;
size_t blocksize;
size_t prefixsize;
util::buffer_t<char> shards;
size_t blocksize; ///< Bytes reserved for the payload portion of each shard.
size_t prefixsize; ///< Bytes reserved before each shard payload for protocol headers.
util::buffer_t<char> shards; ///< Contiguous backing storage for all encoded FEC shards.
util::buffer_t<char> headers; ///< Backing storage for the RTP/FEC headers attached to shards.
util::buffer_t<uint8_t *> shards_p; ///< Pointer table passed to the Reed-Solomon encoder.
util::buffer_t<char> headers;
util::buffer_t<uint8_t *> shards_p;
std::vector<platf::buffer_descriptor_t> payload_buffers;
std::vector<platf::buffer_descriptor_t> payload_buffers; ///< Platform send descriptors for FEC payload buffers.
/**
* @brief Return the FEC shard data pointer for a packet-group element.
*
* @param el Packet-group element index.
* @return Pointer to the shard bytes for the requested element.
*/
char *data(size_t el) {
return (char *) shards_p[el];
}
/**
* @brief Return the FEC prefix bytes for the current packet group.
*
* @param el Packet-group element index.
* @return Pointer to the element's prefix bytes, or nullptr when no prefix is used.
*/
char *prefix(size_t el) {
return prefixsize ? &headers[el * prefixsize] : nullptr;
}
/**
* @brief Return the serialized size of the current object.
*
* @return Number of elements currently stored.
*/
size_t size() const {
return nr_shards;
}
@@ -724,6 +888,8 @@
* @param slice_size The number of bytes between insertions.
* @param data1 The first data buffer.
* @param data2 The second data buffer.
*
* @return Combined buffer with insert padding written at each slice boundary.
*/
std::vector<uint8_t> concat_and_insert(uint64_t insert_size, uint64_t slice_size, const std::string_view &data1, const std::string_view &data2) {
auto data_size = data1.size() + data2.size();
@@ -763,6 +929,14 @@
return result;
}
/**
* @brief Replace a byte sequence in an encoded packet.
*
* @param original Original text value used when reporting a parsing failure.
* @param old Byte sequence to replace in encoded packets.
* @param _new Replacement byte sequence inserted into encoded packets.
* @return Copy of the original buffer with each matching byte sequence replaced.
*/
std::vector<uint8_t> replace(const std::string_view &original, const std::string_view &old, const std::string_view &_new) {
std::vector<uint8_t> replaced;
replaced.reserve(original.size() + _new.size() - old.size());
@@ -891,6 +1065,13 @@
return 0;
}
/**
* @brief Send the selected HDR mode to the connected client over the control channel.
*
* @param session Active streaming or pairing session for the request.
* @param hdr_info HDR info.
* @return 0 when the control message is queued; nonzero when no control peer is ready.
*/
int send_hdr_mode(session_t *session, video::hdr_info_t hdr_info) {
if (!session->control.peer) {
BOOST_LOG(warning) << "Couldn't send HDR mode, still waiting for PING from Moonlight"sv;
@@ -920,6 +1101,11 @@
return 0;
}
/**
* @brief Run the broadcast control-channel worker thread.
*
* @param server RTSP server instance handling the request.
*/
void controlBroadcastThread(control_server_t *server) {
server->map(packetTypes[IDX_PERIODIC_PING], [](session_t *session, const std::string_view &payload) {
BOOST_LOG(verbose) << "type [IDX_PERIODIC_PING]"sv;
@@ -1174,6 +1360,11 @@
server->flush();
}
/**
* @brief Receive thread data.
*
* @param ctx Native context object used by the operation or callback.
*/
void recvThread(broadcast_ctx_t &ctx) {
std::map<av_session_id_t, message_queue_t> peer_to_video_session;
std::map<av_session_id_t, message_queue_t> peer_to_audio_session;
@@ -1269,6 +1460,11 @@
}
}
/**
* @brief Run the broadcast video sender thread.
*
* @param sock Socket used to read or write the protocol message.
*/
void videoBroadcastThread(udp::socket &sock) {
auto shutdown_event = mail::man->event<bool>(mail::broadcast_shutdown);
auto packets = mail::man->queue<video::packet_t>(mail::video_packets);
@@ -1593,6 +1789,11 @@
shutdown_event->raise(true);
}
/**
* @brief Run the broadcast audio sender thread.
*
* @param sock Socket used to read or write the protocol message.
*/
void audioBroadcastThread(udp::socket &sock) {
auto shutdown_event = mail::man->event<bool>(mail::broadcast_shutdown);
auto packets = mail::man->queue<audio::packet_t>(mail::audio_packets);
@@ -1698,6 +1899,9 @@
shutdown_event->raise(true);
}
/**
* @brief Bind the GameStream UDP and control sockets used for a streaming session.
*/
int start_broadcast(broadcast_ctx_t &ctx) {
auto address_family = net::af_from_enum_string(config::sunshine.address_family);
auto protocol = address_family == net::IPV4 ? udp::v4() : udp::v6();
@@ -1765,5 +1969,8 @@
return 0;
}
/**
* @brief Stop broadcast processing.
*/
void end_broadcast(broadcast_ctx_t &ctx) {
auto broadcast_shutdown_event = mail::man->event<bool>(mail::broadcast_shutdown);
@@ -1799,6 +2006,17 @@
broadcast_shutdown_event->reset();
}
/**
* @brief Receive ping data.
*
* @param session Active streaming or pairing session for the request.
* @param ref Reference frame metadata used by the encoder.
* @param type Protocol, message, or resource type selector.
* @param expected_payload Expected payload.
* @param peer Remote endpoint associated with the socket.
* @param timeout Maximum time to wait for the operation.
* @return Network operation status.
*/
int recv_ping(session_t *session, decltype(broadcast)::ptr_t ref, socket_e type, std::string_view expected_payload, udp::endpoint &peer, std::chrono::milliseconds timeout) {
auto messages = std::make_shared<message_queue_t::element_type>(30);
av_session_id_t session_id = std::string {expected_payload};
@@ -1852,6 +2070,11 @@
return -1;
}
/**
* @brief Run the session video capture and encode thread.
*
* @param session Active streaming or pairing session for the request.
*/
void videoThread(session_t *session) {
platf::set_thread_name("session::video");
auto fg = util::fail_guard([&]() {
@@ -1874,6 +2097,11 @@
video::capture(session->mail, session->config.monitor, session);
}
/**
* @brief Run the session audio capture and encode thread.
*
* @param session Active streaming or pairing session for the request.
*/
void audioThread(session_t *session) {
platf::set_thread_name("session::audio");
auto fg = util::fail_guard([&]() {
@@ -1897,16 +2125,25 @@
}
namespace session {
std::atomic_uint running_sessions;
std::atomic_uint running_sessions; ///< Running sessions.
/**
* @brief Platform handle returned from stream setup.
*/
state_e state(session_t &session) {
return session.state.load(std::memory_order_relaxed);
}
/**
* @brief Return the paired client certificate for this session.
*/
const std::string &client_cert(session_t &session) {
return session.client_cert;
}
/**
* @brief Stop the active streaming session and prevent new packets from being queued.
*/
void stop(session_t &session) {
while_starting_do_nothing(session.state);
auto expected = state_e::RUNNING;
@@ -1918,6 +2155,9 @@
session.shutdown_event->raise(true);
}
/**
* @brief Wait for worker threads owned by the session to exit.
*/
void join(session_t &session) {
// Current Nvidia drivers have a bug where NVENC can deadlock the encoder thread with hardware-accelerated
// GPU scheduling enabled. If this happens, we will terminate ourselves and the service can restart.
@@ -1965,5 +2205,8 @@
BOOST_LOG(debug) << "Session ended"sv;
}
/**
* @brief Start the audio, video, and control workers for a streaming session.
*/
int start(session_t &session, const std::string &addr_string) {
session.input = input::alloc(session.mail);
@@ -2007,5 +2250,8 @@
return 0;
}
/**
* @brief Allocate and initialize platform input state for a stream.
*/
std::shared_ptr<session_t> alloc(config_t &config, rtsp_stream::launch_session_t &launch_session) {
auto session = std::make_shared<session_t>();
src/stream.h +55 −13
@@ -16,29 +16,35 @@
#include "video.h"
namespace stream {
constexpr auto VIDEO_STREAM_PORT = 9;
constexpr auto CONTROL_PORT = 10;
constexpr auto VIDEO_STREAM_PORT = 9; ///< GameStream base-port offset used for the video UDP stream.
constexpr auto CONTROL_PORT = 10; ///< GameStream base-port offset used for the control channel.
constexpr auto AUDIO_STREAM_PORT = 11; ///< GameStream base-port offset used for the audio UDP stream.
constexpr auto AUDIO_STREAM_PORT = 11;
struct session_t;
/**
* @brief Stream configuration shared by capture and network senders.
*/
struct config_t {
audio::config_t audio;
video::config_t monitor;
audio::config_t audio; ///< Audio capture configuration for the stream.
video::config_t monitor; ///< Video capture and encoder configuration for the selected monitor.
int packetsize;
int minRequiredFecPackets;
int mlFeatureFlags;
int controlProtocolType;
int packetsize; ///< Maximum payload size for network packets.
int minRequiredFecPackets; ///< Minimum recovery packets required before FEC is emitted.
int mlFeatureFlags; ///< Moonlight feature flags negotiated for this session.
int controlProtocolType; ///< GameStream control protocol variant selected by the client.
int audioQosType; ///< Audio QoS type.
int audioQosType;
int videoQosType;
int videoQosType; ///< Video QoS type.
uint32_t encryptionFlagsEnabled; ///< Bitmask of GameStream encryption features enabled for the session.
uint32_t encryptionFlagsEnabled;
std::optional<int> gcmap;
std::optional<int> gcmap; ///< Optional game-controller mapping override from the launch request.
};
namespace session {
/**
* @brief Enumerates supported state options.
*/
enum class state_e : int {
STOPPED, ///< The session is stopped
STOPPING, ///< The session is stopping
@@ -46,11 +52,47 @@
RUNNING, ///< The session is running
};
/**
* @brief Allocate and initialize platform input state for a stream.
*
* @param config Configuration values to apply.
* @param launch_session Launch session.
* @return Allocated object or identifier, or an error value on failure.
*/
std::shared_ptr<session_t> alloc(config_t &config, rtsp_stream::launch_session_t &launch_session);
/**
* @brief Start a streaming session for the supplied peer address.
*
* @param session Active streaming or pairing session for the request.
* @param addr_string Addr string.
* @return Start status.
*/
int start(session_t &session, const std::string &addr_string);
/**
* @brief Stop a streaming session and prevent more packets from being queued.
*
* @param session Active streaming or pairing session for the request.
*/
void stop(session_t &session);
/**
* @brief Wait for worker threads owned by the session to exit.
*
* @param session Active streaming or pairing session for the request.
*/
void join(session_t &session);
/**
* @brief Platform handle returned from stream setup.
*
* @param session Active streaming or pairing session for the request.
* @return Current lifecycle state for the stream session.
*/
state_e state(session_t &session);
/**
* @brief Return the paired client certificate for a stream session.
*
* @param session Active streaming or pairing session for the request.
* @return PEM certificate associated with the session's client.
*/
const std::string &client_cert(session_t &session);
} // namespace session
} // namespace stream
src/sync.h +65 −1
@@ -11,20 +11,45 @@
namespace sync_util {
/**
* @brief Value wrapper that pairs an object with the mutex protecting it.
*/
template<class T, class M = std::mutex>
class sync_t {
public:
/**
* @brief Type stored behind the synchronization wrapper.
*/
using value_t = T;
/**
* @brief Mutex type used to protect the stored value.
*/
using mutex_t = M;
/**
* @brief Acquire the underlying lock or keyed mutex.
*
* @return Lock guard owning the synchronized object until destruction.
*/
std::lock_guard<mutex_t> lock() {
return std::lock_guard {_lock};
}
/**
* @brief Initialize the protected object from forwarded arguments.
*
* @param args Arguments forwarded to the protected object's constructor.
*/
template<class... Args>
sync_t(Args &&...args):
raw {std::forward<Args>(args)...} {
}
/**
* @brief Move another synchronized value into this instance while locking both wrappers.
*
* @param other Source object whose state is copied or moved into this object.
* @return Reference to this wrapper.
*/
sync_t &operator=(sync_t &&other) noexcept {
std::lock(_lock, other._lock);
@@ -37,5 +62,11 @@
return *this;
}
/**
* @brief Copy another synchronized value into this instance while locking both wrappers.
*
* @param other Source object whose state is copied or moved into this object.
* @return Reference to this wrapper.
*/
sync_t &operator=(sync_t &other) noexcept {
std::lock(_lock, other._lock);
@@ -48,6 +79,12 @@
return *this;
}
/**
* @brief Assign a new value while holding this wrapper's lock.
*
* @param val Value assigned to the synchronized object.
* @return Reference to this wrapper.
*/
template<class V>
sync_t &operator=(V &&val) {
auto lg = lock();
@@ -57,5 +94,11 @@
return *this;
}
/**
* @brief Copy-assign the protected value while holding this wrapper's lock.
*
* @param val Value assigned to the synchronized object.
* @return Reference to this wrapper.
*/
sync_t &operator=(const value_t &val) noexcept {
auto lg = lock();
@@ -65,5 +108,11 @@
return *this;
}
/**
* @brief Move-assign the protected value while holding this wrapper's lock.
*
* @param val Value assigned to the synchronized object.
* @return Reference to this wrapper.
*/
sync_t &operator=(value_t &&val) noexcept {
auto lg = lock();
@@ -73,19 +122,34 @@
return *this;
}
/**
* @brief Access the protected value directly.
*
* @return Pointer to the protected value.
*/
value_t *operator->() {
return &raw;
}
/**
* @brief Dereference the protected value.
*
* @return Mutable reference to the protected value.
*/
value_t &operator*() {
return raw;
}
/**
* @brief Dereference the protected value.
*
* @return Const reference to the protected value.
*/
const value_t &operator*() const {
return raw;
}
value_t raw;
value_t raw; ///< Protected value accessed while this helper's lock is held.
private:
mutex_t _lock;
src/system_tray.cpp +26 −0
@@ -6,12 +6,32 @@
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
#if defined(_WIN32)
/**
* @def WIN32_LEAN_AND_MEAN
* @brief Macro for WIN32 LEAN AND MEAN.
*/
#define WIN32_LEAN_AND_MEAN
#include <accctrl.h>
#include <aclapi.h>
/**
* @def TRAY_ICON
* @brief Macro for TRAY ICON.
*/
#define TRAY_ICON WEB_DIR "images/sunshine.ico"
/**
* @def TRAY_ICON_PLAYING
* @brief Macro for TRAY ICON PLAYING.
*/
#define TRAY_ICON_PLAYING WEB_DIR "images/sunshine-playing.ico"
/**
* @def TRAY_ICON_PAUSING
* @brief Macro for TRAY ICON PAUSING.
*/
#define TRAY_ICON_PAUSING WEB_DIR "images/sunshine-pausing.ico"
/**
* @def TRAY_ICON_LOCKED
* @brief Macro for TRAY ICON LOCKED.
*/
#define TRAY_ICON_LOCKED WEB_DIR "images/sunshine-locked.ico"
#elif defined(__linux__) || defined(linux) || defined(__linux) || defined(__FreeBSD__)
#define TRAY_ICON WEB_DIR "images/logo-sunshine.svg"
@@ -156,6 +176,12 @@
.allIconPaths = {TRAY_ICON, TRAY_ICON_LOCKED, TRAY_ICON_PLAYING, TRAY_ICON_PAUSING},
};
/**
* @brief Get resource path.
*
* @param relativePath Relative path.
* @return Absolute path to the resource file for the current platform bundle layout.
*/
const char *GetResourcePath(const char *relativePath) {
#ifdef __APPLE__
if (!relativePath || !*relativePath) {
src/task_pool.h +97 −5
@@ -21,42 +21,80 @@
namespace task_pool_util {
/**
* @brief Type-erased task interface stored by the task pool.
*/
class _ImplBase {
public:
// _unique_base_type _this_ptr;
inline virtual ~_ImplBase() = default;
/**
* @brief Execute the queued task body.
*/
virtual void run() = 0;
};
/**
* @brief Concrete task wrapper that stores and invokes a callable.
*/
template<class Function>
class _Impl: public _ImplBase {
Function _func;
public:
/**
* @brief Store a concrete callable behind the task type-erasure interface.
*
* @param f Callable executed by the helper.
*/
_Impl(Function &&f):
_func(std::forward<Function>(f)) {
}
/**
* @brief Execute the queued task body.
*/
void run() override {
_func();
}
};
/**
* @brief Queue of immediate and delayed tasks executed by worker threads.
*/
class TaskPool {
public:
/**
* @brief Type-erased task owned by the task pool.
*/
typedef std::unique_ptr<_ImplBase> __task;
/**
* @brief Stable pointer used to identify a queued task.
*/
typedef _ImplBase *task_id_t;
/**
* @brief Steady-clock timestamp used to schedule delayed tasks.
*/
typedef std::chrono::steady_clock::time_point __time_point;
/**
* @brief Queued delayed task identifier paired with its future result.
*/
template<class R>
class timer_task_t {
public:
task_id_t task_id; ///< Task ID.
task_id_t task_id;
std::future<R> future;
std::future<R> future; ///< Future that resolves with the timer task result.
/**
* @brief Track a queued timer task and the future for its result.
*
* @param task_id Identifier returned when the task was queued.
* @param future Future object whose result will be collected later.
*/
timer_task_t(task_id_t task_id, std::future<R> &future):
task_id {task_id},
future {std::move(future)} {
@@ -64,18 +102,29 @@
};
protected:
std::deque<__task> _tasks;
std::vector<std::pair<__time_point, __task>> _timer_tasks;
std::mutex _task_mutex;
std::deque<__task> _tasks; ///< Immediate tasks waiting for worker execution.
std::vector<std::pair<__time_point, __task>> _timer_tasks; ///< Delayed tasks sorted by their due time.
std::mutex _task_mutex; ///< Mutex protecting task queues and worker wakeups.
public:
TaskPool() = default;
/**
* @brief Move queued tasks and timers from another task pool.
*
* @param other Pool whose queues and worker state are moved into this pool.
*/
TaskPool(TaskPool &&other) noexcept:
_tasks {std::move(other._tasks)},
_timer_tasks {std::move(other._timer_tasks)} {
}
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @param other Source object whose state is copied or moved into this object.
* @return Reference or value produced by the operator.
*/
TaskPool &operator=(TaskPool &&other) noexcept {
std::swap(_tasks, other._tasks);
std::swap(_timer_tasks, other._timer_tasks);
@@ -83,6 +132,13 @@
return *this;
}
/**
* @brief Queue work for asynchronous execution.
*
* @param newTask New task.
* @param args Arguments forwarded to the callable or parser.
* @return Identifier assigned to the queued task.
*/
template<class Function, class... Args>
auto push(Function &&newTask, Args &&...args) {
static_assert(std::is_invocable_v<Function, Args &&...>, "arguments don't match the function");
@@ -104,5 +160,10 @@
return future;
}
/**
* @brief Queue a task that becomes runnable after a delay.
*
* @param task Task object to enqueue or execute.
*/
void pushDelayed(std::pair<__time_point, __task> &&task) {
std::lock_guard lg(_task_mutex);
@@ -119,6 +180,10 @@
/**
* @return An id to potentially delay the task.
*
* @param newTask New task.
* @param duration Delay before the timed task should run.
* @param args Arguments forwarded to the callable or parser.
*/
template<class Function, class X, class Y, class... Args>
auto pushDelayed(Function &&newTask, std::chrono::duration<X, Y> duration, Args &&...args) {
@@ -185,5 +250,11 @@
}
}
/**
* @brief Cancel a queued or delayed task before it runs.
*
* @param task_id Identifier returned when the task was queued.
* @return True when the task was found and cancelled.
*/
bool cancel(task_id_t task_id) {
std::lock_guard lg(_task_mutex);
@@ -202,5 +273,11 @@
return false;
}
/**
* @brief Remove and return the next queued item, waiting when requested.
*
* @param task_id Identifier returned when the task was queued.
* @return Removed queue item, or empty result when the queue is stopped or empty.
*/
std::optional<std::pair<__time_point, __task>> pop(task_id_t task_id) {
std::lock_guard lg(_task_mutex);
@@ -216,5 +293,10 @@
return std::move(*pos);
}
/**
* @brief Remove and return the next queued item, waiting when requested.
*
* @return Removed queue item, or empty result when the queue is stopped or empty.
*/
std::optional<__task> pop() {
std::lock_guard lg(_task_mutex);
@@ -234,11 +316,21 @@
return std::nullopt;
}
/**
* @brief Check whether the task pool has active workers.
*
* @return True when the task was found and cancelled.
*/
bool ready() {
std::lock_guard<std::mutex> lg(_task_mutex);
return !_tasks.empty() || (!_timer_tasks.empty() && std::get<0>(_timer_tasks.back()) <= std::chrono::steady_clock::now());
}
/**
* @brief Return the next timer task ready for execution.
*
* @return Time point for the next queued timer task, or std::nullopt when none exist.
*/
std::optional<__time_point> next() {
std::lock_guard<std::mutex> lg(_task_mutex);
src/thread_pool.h +42 −0
@@ -17,6 +17,9 @@
*/
class ThreadPool: public task_pool_util::TaskPool {
public:
/**
* @brief Callable unit executed by a worker thread.
*/
typedef TaskPool::__task __task;
private:
@@ -32,6 +35,11 @@
_continue {false} {
}
/**
* @brief Start a pool with the requested number of worker threads.
*
* @param threads Number of worker threads to start.
*/
explicit ThreadPool(int threads):
_thread(threads),
_continue {true} {
@@ -49,6 +57,13 @@
join();
}
/**
* @brief Queue work for asynchronous execution.
*
* @param newTask New task.
* @param args Arguments forwarded to the callable or parser.
* @return Identifier assigned to the queued task.
*/
template<class Function, class... Args>
auto push(Function &&newTask, Args &&...args) {
std::lock_guard lg(_lock);
@@ -58,12 +73,25 @@
return future;
}
/**
* @brief Queue a task that becomes runnable after a delay.
*
* @param task Task object to enqueue or execute.
*/
void pushDelayed(std::pair<__time_point, __task> &&task) {
std::lock_guard lg(_lock);
TaskPool::pushDelayed(std::move(task));
}
/**
* @brief Queue a task that becomes runnable after a delay.
*
* @param newTask New task.
* @param duration Delay before the timed task should run.
* @param args Arguments forwarded to the callable or parser.
* @return Future that becomes ready after the delayed task completes.
*/
template<class Function, class X, class Y, class... Args>
auto pushDelayed(Function &&newTask, std::chrono::duration<X, Y> duration, Args &&...args) {
std::lock_guard lg(_lock);
@@ -74,5 +102,10 @@
return future;
}
/**
* @brief Start worker threads for queued thread-pool tasks.
*
* @param threads Number of worker threads to start.
*/
void start(int threads) {
_continue = true;
@@ -84,5 +117,8 @@
}
}
/**
* @brief Stop worker threads and prevent additional task execution.
*/
void stop() {
std::lock_guard lg(_lock);
@@ -91,6 +127,9 @@
_cv.notify_all();
}
/**
* @brief Wait for worker threads owned by the session to exit.
*/
void join() {
for (auto &t : _thread) {
t.join();
@@ -98,6 +137,9 @@
}
public:
/**
* @brief Run the main application or worker loop.
*/
void _main() {
platf::set_thread_name("TaskPool::worker");
while (_continue) {
src/thread_safe.h +301 −4
@@ -17,11 +17,22 @@
#include "utility.h"
namespace safe {
/**
* @brief Thread-safe event value that blocks readers until a value is raised.
*/
template<class T>
class event_t {
public:
/**
* @brief Status value stored in the event.
*/
using status_t = util::optional_t<T>;
/**
* @brief Notify waiters that a new event value is available.
*
* @param args Arguments forwarded to the callable or parser.
*/
template<class... Args>
void raise(Args &&...args) {
std::lock_guard lg {_lock};
@@ -39,5 +50,10 @@
}
// pop and view should not be used interchangeably
/**
* @brief Remove and return the next queued item, waiting when requested.
*
* @return Removed queue item, or empty result when the queue is stopped or empty.
*/
status_t pop() {
std::unique_lock ul {_lock};
@@ -60,6 +76,12 @@
}
// pop and view should not be used interchangeably
/**
* @brief Remove and return the next queued item, waiting when requested.
*
* @param delay Maximum wait duration before timing out.
* @return Removed queue item, or empty result when the queue is stopped or empty.
*/
template<typename Rep, typename Period>
status_t pop(std::chrono::duration<Rep, Period> delay) {
std::unique_lock ul {_lock};
@@ -77,5 +99,10 @@
}
// pop and view should not be used interchangeably
/**
* @brief Read the current value without removing it from the queue.
*
* @return Optional copy of the front item, or empty status when the queue is closed.
*/
status_t view() {
std::unique_lock ul {_lock};
@@ -96,6 +123,12 @@
}
// pop and view should not be used interchangeably
/**
* @brief Read the current value without removing it from the queue.
*
* @param delay Maximum wait duration before timing out.
* @return Optional copy of the front item, or empty status after timeout/closure.
*/
template<class Rep, class Period>
status_t view(std::chrono::duration<Rep, Period> delay) {
std::unique_lock ul {_lock};
@@ -113,9 +146,17 @@
return _status;
}
/**
* @brief Inspect the next queued value without popping it.
*
* @return True when a value is available to inspect.
*/
bool peek() {
return _continue && (bool) _status;
}
/**
* @brief Raise the alarm and wake all waiting threads.
*/
void stop() {
std::lock_guard lg {_lock};
@@ -125,5 +166,8 @@
_cv.notify_all();
}
/**
* @brief Reset the object to its initial empty state.
*/
void reset() {
std::lock_guard lg {_lock};
@@ -133,6 +177,11 @@
_status = util::false_v<status_t>;
}
/**
* @brief Return whether the queue is still running.
*
* @return True while the queue accepts producers and consumers.
*/
[[nodiscard]] bool running() const {
return _continue;
}
@@ -145,10 +194,21 @@
std::mutex _lock;
};
/**
* @brief Alarm primitive used to wait for and raise shutdown-style signals.
*/
template<class T>
class alarm_raw_t {
public:
/**
* @brief Status value carried by the alarm.
*/
using status_t = util::optional_t<T>;
/**
* @brief Mark the alarm as rung and wake waiting threads.
*
* @param status Native status code returned by the platform API.
*/
void ring(const status_t &status) {
std::lock_guard lg(_lock);
@@ -158,5 +218,10 @@
_cv.notify_one();
}
/**
* @brief Mark the alarm as rung and wake waiting threads.
*
* @param status Native status code returned by the platform API.
*/
void ring(status_t &&status) {
std::lock_guard lg(_lock);
@@ -166,6 +231,12 @@
_cv.notify_one();
}
/**
* @brief Wait for for.
*
* @param rel_time Rel time.
* @return Wait result indicating whether the condition was satisfied before timeout.
*/
template<class Rep, class Period>
auto wait_for(const std::chrono::duration<Rep, Period> &rel_time) {
std::unique_lock ul(_lock);
@@ -175,6 +246,13 @@
});
}
/**
* @brief Wait for for.
*
* @param rel_time Rel time.
* @param pred Predicate used to decide whether the wait is complete.
* @return Wait result indicating whether the condition was satisfied before timeout.
*/
template<class Rep, class Period, class Pred>
auto wait_for(const std::chrono::duration<Rep, Period> &rel_time, Pred &&pred) {
std::unique_lock ul(_lock);
@@ -184,6 +262,12 @@
});
}
/**
* @brief Wait for until.
*
* @param rel_time Rel time.
* @return Wait result indicating whether the condition was satisfied before the deadline.
*/
template<class Rep, class Period>
auto wait_until(const std::chrono::duration<Rep, Period> &rel_time) {
std::unique_lock ul(_lock);
@@ -193,6 +277,13 @@
});
}
/**
* @brief Wait for until.
*
* @param rel_time Rel time.
* @param pred Predicate used to decide whether the wait is complete.
* @return Wait result indicating whether the condition was satisfied before the deadline.
*/
template<class Rep, class Period, class Pred>
auto wait_until(const std::chrono::duration<Rep, Period> &rel_time, Pred &&pred) {
std::unique_lock ul(_lock);
@@ -202,6 +293,11 @@
});
}
/**
* @brief Block until the event is raised.
*
* @return Lock object held after the event is observed.
*/
auto wait() {
std::unique_lock ul(_lock);
_cv.wait(ul, [this]() {
@@ -209,6 +305,12 @@
});
}
/**
* @brief Block until the event is raised and the predicate accepts the state.
*
* @param pred Predicate used to decide whether the wait is complete.
* @return Lock object held after the predicate succeeds.
*/
template<class Pred>
auto wait(Pred &&pred) {
std::unique_lock ul(_lock);
@@ -217,14 +319,27 @@
});
}
/**
* @brief Return or update the current status value.
*
* @return Status status.
*/
const status_t &status() const {
return _status;
}
/**
* @brief Return or update the current status value.
*
* @return Status status.
*/
status_t &status() {
return _status;
}
/**
* @brief Reset the object to its initial empty state.
*/
void reset() {
_status = status_t {};
_rang = false;
@@ -238,23 +353,47 @@
bool _rang {false};
};
/**
* @brief Shared alarm primitive used for cross-thread shutdown signals.
*/
template<class T>
using alarm_t = std::shared_ptr<alarm_raw_t<T>>;
/**
* @brief Create a alarm object or message.
*
* @return Constructed alarm object.
*/
template<class T>
alarm_t<T> make_alarm() {
return std::make_shared<alarm_raw_t<T>>();
}
/**
* @brief Thread-safe queue with blocking and shutdown-aware consumers.
*/
template<class T>
class queue_t {
public:
/**
* @brief Value type stored in the queue.
*/
using status_t = util::optional_t<T>;
/**
* @brief Construct a bounded blocking queue.
*
* @param max_elements Maximum number of queued elements before producers block.
*/
queue_t(std::uint32_t max_elements = 32):
_max_elements {max_elements} {
}
/**
* @brief Notify waiters that a new event value is available.
*
* @param args Arguments forwarded to the callable or parser.
*/
template<class... Args>
void raise(Args &&...args) {
std::lock_guard ul {_lock};
@@ -272,10 +411,21 @@
_cv.notify_all();
}
/**
* @brief Inspect the next queued value without popping it.
*
* @return True when a value is available to inspect.
*/
bool peek() {
return _continue && !_queue.empty();
}
/**
* @brief Remove and return the next queued item, waiting when requested.
*
* @param delay Maximum wait duration before timing out.
* @return Removed queue item, or empty result when the queue is stopped or empty.
*/
template<class Rep, class Period>
status_t pop(std::chrono::duration<Rep, Period> delay) {
std::unique_lock ul {_lock};
@@ -296,5 +446,10 @@
return val;
}
/**
* @brief Remove and return the next queued item, waiting when requested.
*
* @return Removed queue item, or empty result when the queue is stopped or empty.
*/
status_t pop() {
std::unique_lock ul {_lock};
@@ -317,9 +472,17 @@
return val;
}
/**
* @brief Return the wrapped pointer without synchronization checks.
*
* @return Mutable queue storage for callers that already hold external synchronization.
*/
std::vector<T> &unsafe() {
return _queue;
}
/**
* @brief Shut down the queue and wake blocked consumers.
*/
void stop() {
std::lock_guard lg {_lock};
@@ -329,6 +492,11 @@
_cv.notify_all();
}
/**
* @brief Return whether the queue is still running.
*
* @return True while the queue accepts producers and consumers.
*/
[[nodiscard]] bool running() const {
return _continue;
}
@@ -343,30 +511,60 @@
std::vector<T> _queue;
};
/**
* @brief Shared object storage with custom construction and destruction hooks.
*/
template<class T>
class shared_t {
public:
/**
* @brief Object type stored in shared protected storage.
*/
using element_type = T;
/**
* @brief Callback signature used to construct the protected object.
*/
using construct_f = std::function<int(element_type &)>;
/**
* @brief Callback signature used to destroy the protected object.
*/
using destruct_f = std::function<void(element_type &)>;
/**
* @brief Pointer wrapper state protected by synchronization.
*/
struct ptr_t {
shared_t *owner; ///< Shared object that owns the protected value.
shared_t *owner;
ptr_t():
owner {nullptr} {
}
/**
* @brief Construct a reference bound to a shared protected object.
*
* @param owner Shared object that owns this pointer wrapper.
*/
explicit ptr_t(shared_t *owner):
owner {owner} {
}
/**
* @brief Move a shared-object reference without locking the protected object.
*
* @param ptr Pointer managed by the safe pointer wrapper.
*/
ptr_t(ptr_t &&ptr) noexcept:
owner {ptr.owner} {
ptr.owner = nullptr;
}
/**
* @brief Copy a shared-object reference without locking the protected object.
*
* @param ptr Pointer managed by the safe pointer wrapper.
*/
ptr_t(const ptr_t &ptr) noexcept:
owner {ptr.owner} {
if (!owner) {
@@ -377,6 +575,12 @@
tmp.owner = nullptr;
}
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @param ptr Pointer managed by the safe pointer wrapper.
* @return Reference or value produced by the operator.
*/
ptr_t &operator=(const ptr_t &ptr) noexcept {
if (!ptr.owner) {
release();
@@ -387,6 +591,12 @@
return *this = std::move(*ptr.owner->ref());
}
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @param ptr Pointer managed by the safe pointer wrapper.
* @return Reference or value produced by the operator.
*/
ptr_t &operator=(ptr_t &&ptr) noexcept {
if (owner) {
release();
@@ -403,9 +613,15 @@
}
}
/**
* @brief Check whether this shared pointer wrapper owns a live object.
*/
operator bool() const {
return owner != nullptr;
}
/**
* @brief Release the COM or platform reference owned by the pointer.
*/
void release() {
std::lock_guard lg {owner->_lock};
@@ -418,20 +634,41 @@
owner = nullptr;
}
/**
* @brief Return the currently wrapped value or handle.
*
* @return Underlying native handle or object pointer.
*/
element_type *get() const {
return reinterpret_cast<element_type *>(owner->_object_buf.data());
}
/**
* @brief Access the shared protected object.
*
* @return Pointer to the object stored in the shared wrapper.
*/
element_type *operator->() {
return reinterpret_cast<element_type *>(owner->_object_buf.data());
}
};
/**
* @brief Construct a shared protected object with custom close and destruction hooks.
*
* @param fc Callable invoked when the shared object is closed.
* @param fd Callable invoked when the shared object is destroyed.
*/
template<class FC, class FD>
shared_t(FC &&fc, FD &&fd):
_construct {std::forward<FC>(fc)},
_destruct {std::forward<FD>(fd)} {
}
/**
* @brief Return a shared reference to the protected object.
*
* @return Reference handle that keeps the protected object alive.
*/
[[nodiscard]] ptr_t ref() {
std::lock_guard lg {_lock};
@@ -458,6 +695,13 @@
std::mutex _lock;
};
/**
* @brief Create a shared object or message.
*
* @param fc Callable executed while the shared object is locked.
* @param fd Native file descriptor to wrap or inspect.
* @return Constructed shared object.
*/
template<class T, class F_Construct, class F_Destruct>
auto make_shared(F_Construct &&fc, F_Destruct &&fd) {
return shared_t<T> {
@@ -466,42 +710,83 @@
};
}
/**
* @brief Boolean alarm used as a simple signal.
*/
using signal_t = event_t<bool>;
class mail_raw_t;
/**
* @brief Shared mailbox handle used by event and queue wrappers.
*/
using mail_t = std::shared_ptr<mail_raw_t>;
/**
* @brief Run cleanup for completed asynchronous work.
*
* @param mail Mailbox used to exchange messages with worker threads.
*/
void cleanup(mail_raw_t *);
/**
* @brief Wrapper that posts cleanup work to a mailbox when destroyed.
*/
template<class T>
class post_t: public T {
public:
/**
* @brief Construct a message-posting wrapper around an existing type.
*
* @param mail Mailbox used to exchange messages with worker threads.
* @param args Arguments forwarded to the wrapped type constructor.
*/
template<class... Args>
post_t(mail_t mail, Args &&...args):
T(std::forward<Args>(args)...),
mail {std::move(mail)} {
}
mail_t mail;
mail_t mail; ///< Mailbox kept alive until the posted object is destroyed.
~post_t() {
cleanup(mail.get());
}
};
/**
* @brief Acquire the underlying lock or keyed mutex.
*
* @param wp Weak pointer used to test whether the object is still alive.
* @return Lock guard owning the synchronized object until destruction.
*/
template<class T>
inline auto lock(const std::weak_ptr<void> &wp) {
return std::reinterpret_pointer_cast<typename T::element_type>(wp.lock());
}
/**
* @brief Mailbox backing store for events, queues, and posted cleanup objects.
*/
class mail_raw_t: public std::enable_shared_from_this<mail_raw_t> {
public:
/**
* @brief Mailbox-backed event wrapper for a typed value.
*/
template<class T>
using event_t = std::shared_ptr<post_t<event_t<T>>>;
/**
* @brief Mailbox-backed queue wrapper for typed messages.
*/
template<class T>
using queue_t = std::shared_ptr<post_t<queue_t<T>>>;
/**
* @brief Create a typed event channel from the raw mailbox.
*
* @param id Identifier for the controller, session, display, or resource.
* @return Typed event channel associated with the supplied identifier.
*/
template<class T>
event_t<T> event(const std::string_view &id) {
std::lock_guard lg {mutex};
@@ -517,6 +802,12 @@
return post;
}
/**
* @brief Create a typed queue channel from the raw mailbox.
*
* @param id Identifier for the controller, session, display, or resource.
* @return Typed queue channel associated with the supplied identifier.
*/
template<class T>
queue_t<T> queue(const std::string_view &id) {
std::lock_guard lg {mutex};
@@ -532,5 +823,8 @@
return post;
}
/**
* @brief Run cleanup for completed asynchronous work.
*/
void cleanup() {
std::lock_guard lg {mutex};
@@ -546,11 +840,14 @@
}
}
std::mutex mutex; ///< Mutex protecting the map of live posted objects.
std::mutex mutex;
std::map<std::string, std::weak_ptr<void>, std::less<>> id_to_post; ///< Posted objects keyed by cleanup identifier.
std::map<std::string, std::weak_ptr<void>, std::less<>> id_to_post;
};
/**
* @brief Run cleanup for completed asynchronous work.
*/
inline void cleanup(mail_raw_t *mail) {
mail->cleanup();
}
src/upnp.cpp +16 −4
@@ -25,14 +25,17 @@
namespace upnp {
/**
* @brief UPnP port mapping description and lease state.
*/
struct mapping_t {
struct {
std::string wan;
std::string lan;
std::string proto;
} port;
} port; ///< WAN/LAN/protocol tuple for the mapped port.
std::string description;
std::string description; ///< Human-readable UPnP lease description advertised to the gateway.
};
static std::string_view status_string(int status) {
@@ -62,6 +65,9 @@
#endif
}
/**
* @brief RAII helper that runs shutdown cleanup when destroyed.
*/
class deinit_t: public platf::deinit_t {
public:
deinit_t() {
@@ -91,6 +97,9 @@
upnp_thread = std::thread {&deinit_t::upnp_thread_proc, this};
}
/**
* @brief Destroy the UPnP deinitializer.
*/
~deinit_t() {
upnp_thread.join();
}
@@ -363,10 +372,13 @@
}
}
std::vector<mapping_t> mappings;
std::thread upnp_thread;
std::vector<mapping_t> mappings; ///< Port mappings Sunshine should keep registered with the gateway.
std::thread upnp_thread; ///< Worker thread that refreshes mappings until shutdown.
};
/**
* @brief Start UPnP port mapping and return its shutdown guard.
*/
std::unique_ptr<platf::deinit_t> start() {
if (!config::sunshine.flags[config::flag::UPNP]) {
return nullptr;
src/upnp.h +14 −6
@@ -14,17 +14,20 @@
* @brief UPnP port mapping.
*/
namespace upnp {
constexpr auto INET6_ADDRESS_STRLEN = 46;
constexpr auto IPv4 = 0;
constexpr auto IPv6 = 1;
constexpr auto PORT_MAPPING_LIFETIME = 3600s;
constexpr auto REFRESH_INTERVAL = 120s;
constexpr auto INET6_ADDRESS_STRLEN = 46; ///< Protocol or platform constant for inet6 address strlen.
constexpr auto IPv4 = 0; ///< I pv4.
constexpr auto IPv6 = 1; ///< I pv6.
constexpr auto PORT_MAPPING_LIFETIME = 3600s; ///< GameStream port offset for port mapping lifetime.
constexpr auto REFRESH_INTERVAL = 120s; ///< Protocol or platform constant for refresh interval.
/**
* @brief Owning pointer to miniupnpc device discovery results.
*/
using device_t = util::safe_ptr<UPNPDev, freeUPNPDevlist>;
KITTY_USING_MOVE_T(urls_t, UPNPUrls, , {
FreeUPNPUrls(&el);
});
}); ///< Alias for element type.
/**
* @brief Get the valid IGD status.
@@ -40,5 +43,10 @@
*/
int UPNP_GetValidIGDStatus(device_t &device, urls_t *urls, IGDdatas *data, std::array<char, INET6_ADDRESS_STRLEN> &lan_addr);
/**
* @brief Start UPnP port mapping and return its shutdown guard.
*
* @return Start status.
*/
[[nodiscard]] std::unique_ptr<platf::deinit_t> start();
} // namespace upnp
src/utility.h +816 −8
@@ -17,6 +17,10 @@
#include <variant>
#include <vector>
/**
* @def KITTY_WHILE_LOOP(x, y, z)
* @brief Execute an initializer followed by a while loop body without leaking helper names.
*/
#define KITTY_WHILE_LOOP(x, y, z) \
{ \
x; \
@@ -26,98 +30,238 @@
template<typename T>
struct argument_type;
/**
* @brief Extracts the argument type from a single-argument function signature.
*/
template<typename T, typename U>
struct argument_type<T(U)> {
/**
* @brief Type extracted from the matched function signature.
*/
typedef U type;
};
/**
* @def KITTY_USING_MOVE_T(move_t, t, init_val, z)
* @brief Define a move-only RAII wrapper with caller-provided initial value and destructor body.
*/
#define KITTY_USING_MOVE_T(move_t, t, init_val, z) \
/** \
* @brief Move-only RAII wrapper generated by KITTY_USING_MOVE_T. \
*/ \
class move_t { \
public: \
/** \
* @brief Wrapped element type. \
*/ \
using element_type = typename argument_type<void(t)>::type; \
\
/** \
* @brief Initialize the generated wrapper with the configured initial value. \
*/ \
move_t(): \
el {init_val} { \
} \
/** \
* @brief Initialize the generated wrapper from forwarded element arguments. \
* \
* @param args Arguments forwarded to the wrapped element. \
*/ \
template<class... Args> \
move_t(Args &&...args): \
el {std::forward<Args>(args)...} { \
} \
/** \
* @brief Copy construction is disabled. \
*/ \
move_t(const move_t &) = delete; \
\
/** \
* @brief Move-construct the wrapper and reset the source wrapper. \
* \
* @param other Wrapper to move from. \
*/ \
move_t(move_t &&other) noexcept: \
el {std::move(other.el)} { \
other.el = element_type {init_val}; \
} \
\
/** \
* @brief Copy assignment is disabled. \
*/ \
move_t &operator=(const move_t &) = delete; \
\
/** \
* @brief Move-assign the wrapped element by swapping with another wrapper. \
* \
* @param other Wrapper to move from. \
* @return This wrapper. \
*/ \
move_t &operator=(move_t &&other) { \
std::swap(el, other.el); \
return *this; \
} \
/** \
* @brief Access the wrapped element. \
* \
* @return Pointer to the wrapped element. \
*/ \
element_type *operator->() { \
return &el; \
} \
/** \
* @brief Access the wrapped element. \
* \
* @return Pointer to the wrapped element. \
*/ \
const element_type *operator->() const { \
return &el; \
} \
\
/** \
* @brief Release the wrapped element and reset the wrapper. \
* \
* @return Previously wrapped element after ownership is released. \
*/ \
inline element_type release() { \
element_type val = std::move(el); \
el = element_type {init_val}; \
return val; \
} \
\
/** \
* @brief Destroy the wrapper and run the configured cleanup body. \
*/ \
~move_t() z \
\
/** \
* @brief Wrapped element value. \
*/ \
element_type el; \
}
/**
* @def KITTY_DECL_CONSTR(x)
* @brief Declare the standard move operations and out-of-line default constructor for a type.
*/
#define KITTY_DECL_CONSTR(x) \
/** \
* @brief Defaulted move constructor for the generated type. \
*/ \
x(x &&) noexcept = default; \
/** \
* @brief Defaulted move assignment for the generated type. \
* \
* @return This instance. \
*/ \
x &operator=(x &&) noexcept = default; \
/** \
* @brief Default constructor for the generated type. \
*/ \
x();
/**
* @def KITTY_DEFAULT_CONSTR_MOVE(x)
* @brief Declare defaulted noexcept move construction and assignment for a type.
*/
#define KITTY_DEFAULT_CONSTR_MOVE(x) \
/** \
* @brief Defaulted move constructor for the generated type. \
*/ \
x(x &&) noexcept = default; \
/** \
* @brief Defaulted move assignment for the generated type. \
* \
* @return This instance. \
*/ \
x &operator=(x &&) noexcept = default;
/**
* @def KITTY_DEFAULT_CONSTR_MOVE_THROW(x)
* @brief Declare defaulted move construction and assignment that may throw.
*/
#define KITTY_DEFAULT_CONSTR_MOVE_THROW(x) \
/** \
* @brief Defaulted move constructor for the generated type. \
*/ \
x(x &&) = default; \
/** \
* @brief Defaulted move assignment for the generated type. \
* \
* @return This instance. \
*/ \
x &operator=(x &&) = default; \
/** \
* @brief Default constructor for the generated type. \
*/ \
x() = default;
/**
* @def KITTY_DEFAULT_CONSTR(x)
* @brief Declare defaulted copy and move operations for a value type.
*/
#define KITTY_DEFAULT_CONSTR(x) \
KITTY_DEFAULT_CONSTR_MOVE(x) \
/** \
* @brief Defaulted copy constructor for the generated type. \
*/ \
x(const x &) noexcept = default; \
/** \
* @brief Defaulted copy assignment for the generated type. \
* \
* @return This instance. \
*/ \
x &operator=(const x &) = default;
/**
* @def TUPLE_2D(a, b, expr)
* @brief Evaluate an expression returning a 2-tuple and bind both elements to local references.
*/
#define TUPLE_2D(a, b, expr) \
decltype(expr) a##_##b = expr; \
auto &a = std::get<0>(a##_##b); \
auto &b = std::get<1>(a##_##b)
/**
* @def TUPLE_2D_REF(a, b, expr)
* @brief Bind both elements of an existing 2-tuple expression to local references.
*/
#define TUPLE_2D_REF(a, b, expr) \
auto &a##_##b = expr; \
auto &a = std::get<0>(a##_##b); \
auto &b = std::get<1>(a##_##b)
/**
* @def TUPLE_3D(a, b, c, expr)
* @brief Evaluate an expression returning a 3-tuple and bind all elements to local references.
*/
#define TUPLE_3D(a, b, c, expr) \
decltype(expr) a##_##b##_##c = expr; \
auto &a = std::get<0>(a##_##b##_##c); \
auto &b = std::get<1>(a##_##b##_##c); \
auto &c = std::get<2>(a##_##b##_##c)
/**
* @def TUPLE_3D_REF(a, b, c, expr)
* @brief Bind all elements of an existing 3-tuple expression to local references.
*/
#define TUPLE_3D_REF(a, b, c, expr) \
auto &a##_##b##_##c = expr; \
auto &a = std::get<0>(a##_##b##_##c); \
auto &b = std::get<1>(a##_##b##_##c); \
auto &c = std::get<2>(a##_##b##_##c)
/**
* @def TUPLE_EL(a, b, expr)
* @brief Evaluate a tuple expression and bind one selected element to a local reference.
*/
#define TUPLE_EL(a, b, expr) \
decltype(expr) a##_ = expr; \
auto &a = std::get<b>(a##_)
/**
* @def TUPLE_EL_REF(a, b, expr)
* @brief Bind one selected element of an existing tuple expression to a local reference.
*/
#define TUPLE_EL_REF(a, b, expr) \
auto &a = std::get<b>(expr)
@@ -136,15 +280,30 @@
template<bool V, class X, class Y>
struct __either;
/**
* @brief Type selector that chooses the first type when the condition is true.
*/
template<class X, class Y>
struct __either<true, X, Y> {
/**
* @brief Type extracted from the matched function signature.
*/
using type = X;
};
/**
* @brief Type selector that chooses the second type when the condition is false.
*/
template<class X, class Y>
struct __either<false, X, Y> {
/**
* @brief Type extracted from the matched function signature.
*/
using type = Y;
};
/**
* @brief Compile-time selector for one of two types.
*/
template<bool V, class X, class Y>
using either_t = typename __either<V, X, Y>::type;
@@ -153,18 +312,34 @@
struct overloaded: Ts... {
using Ts::operator()...;
};
/**
* @brief Combine multiple callables into one overload set for std::visit.
*/
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
/**
* @brief Scope guard that runs a cleanup action unless it is disabled.
*/
template<class T>
class FailGuard {
public:
FailGuard() = delete;
/**
* @brief Construct a fail guard that owns the cleanup callback.
*
* @param f Callable executed by the helper.
*/
FailGuard(T &&f) noexcept:
_func {std::forward<T>(f)} {
}
/**
* @brief Construct a fail guard that owns the cleanup callback.
*
* @param other Source object whose state is copied or moved into this object.
*/
FailGuard(FailGuard &&other) noexcept:
_func {std::move(other._func)} {
this->failure = other.failure;
@@ -183,21 +358,36 @@
}
}
/**
* @brief Disable the fail guard so destruction will not run the cleanup action.
*/
void disable() {
failure = false;
}
bool failure {true};
bool failure {true}; ///< Whether the fail guard should run its cleanup action.
private:
T _func;
};
/**
* @brief Create a scope guard that runs unless it is disabled.
*
* @param f Callable executed by the helper.
* @return Fail guard object that invokes the callable on scope exit.
*/
template<class T>
[[nodiscard]] auto fail_guard(T &&f) {
return FailGuard<T> {std::forward<T>(f)};
}
/**
* @brief Append the raw bytes of a trivially-copyable structure to a byte buffer.
*
* @param buf Destination buffer used for protocol serialization.
* @param _struct Structure instance whose in-memory bytes should be appended.
*/
template<class T>
void append_struct(std::vector<uint8_t> &buf, const T &_struct) {
constexpr size_t data_len = sizeof(_struct);
@@ -211,9 +401,15 @@
}
}
/**
* @brief Formatter that exposes a value's bytes as hexadecimal text.
*/
template<class T>
class Hex {
public:
/**
* @brief Integral type used for one formatted hex element.
*/
typedef T elem_type;
private:
@@ -239,6 +435,12 @@
char _hex[sizeof(elem_type) * 2];
public:
/**
* @brief Construct a hexadecimal byte view for the supplied value.
*
* @param elem Element value being serialized as bytes.
* @param rev Whether bytes should be emitted in reverse order.
*/
Hex(const elem_type &elem, bool rev) {
if (!rev) {
const uint8_t *data = reinterpret_cast<const uint8_t *>(&elem) + sizeof(elem_type) - 1;
@@ -255,49 +457,110 @@
}
}
/**
* @brief Return an iterator to the first byte in the buffer view.
*
* @return Iterator to the first element.
*/
char *begin() {
return _hex;
}
/**
* @brief Return an iterator one past the final byte in the buffer view.
*
* @return Iterator one past the last element.
*/
char *end() {
return _hex + sizeof(elem_type) * 2;
}
/**
* @brief Return an iterator to the first byte in the buffer view.
*
* @return Iterator to the first element.
*/
const char *begin() const {
return _hex;
}
/**
* @brief Return an iterator one past the final byte in the buffer view.
*
* @return Iterator one past the last element.
*/
const char *end() const {
return _hex + sizeof(elem_type) * 2;
}
/**
* @brief Return a const iterator to the first byte in the view.
*
* @return Pointer to the first formatted hexadecimal character.
*/
const char *cbegin() const {
return _hex;
}
/**
* @brief Return a const iterator one past the last byte in the view.
*
* @return Pointer one past the last formatted hexadecimal character.
*/
const char *cend() const {
return _hex + sizeof(elem_type) * 2;
}
/**
* @brief Convert to string.
*
* @return Value converted to string.
*/
std::string to_string() const {
return {begin(), end()};
}
/**
* @brief Convert to string view.
*
* @return Value converted to string view.
*/
std::string_view to_string_view() const {
return {begin(), sizeof(elem_type) * 2};
}
};
/**
* @brief Serialize an element as hexadecimal text.
*
* @param elem Element value being serialized as bytes.
* @param rev Whether bytes should be emitted in reverse order.
* @return Lightweight hexadecimal formatter for the input object.
*/
template<class T>
Hex<T> hex(const T &elem, bool rev = false) {
return Hex<T>(elem, rev);
}
/**
* @brief Format a value as hexadecimal text for logging.
*
* @param value Value whose bytes should be logged in hexadecimal.
* @return Hexadecimal string representation of the input object.
*/
template<typename T>
std::string log_hex(const T &value) {
return "0x" + Hex<T>(value, false).to_string();
}
/**
* @brief Convert a value to a vector of hexadecimal bytes.
*
* @param begin Iterator or pointer marking the start of the input range.
* @param end Iterator or pointer marking the end of the input range.
* @param rev Whether bytes should be emitted in reverse order.
* @return Hexadecimal string for the byte range.
*/
template<class It>
std::string hex_vec(It begin, It end, bool rev = false) {
auto str_size = 2 * std::distance(begin, end);
@@ -340,11 +603,25 @@
return hex;
}
/**
* @brief Parse hexadecimal text into a byte vector.
*
* @param c Character or context value being converted or released.
* @param rev Whether bytes should be emitted in reverse order.
* @return Hexadecimal string for the contiguous container.
*/
template<class C>
std::string hex_vec(C &&c, bool rev = false) {
return hex_vec(std::begin(c), std::end(c), rev);
}
/**
* @brief Convert from hex.
*
* @param hex Hexadecimal text to decode.
* @param rev Whether bytes should be emitted in reverse order.
* @return Value converted from hex.
*/
template<class T>
T from_hex(const std::string_view &hex, bool rev = false) {
std::uint8_t buf[sizeof(T)];
@@ -401,5 +678,12 @@
return *reinterpret_cast<T *>(buf);
}
/**
* @brief Convert from hex vec.
*
* @param hex Hexadecimal text to decode.
* @param rev Whether bytes should be emitted in reverse order.
* @return Value converted from hex vec.
*/
inline std::string from_hex_vec(const std::string &hex, bool rev = false) {
std::string buf;
@@ -453,10 +737,22 @@
return buf;
}
/**
* @brief Hash functor that hashes the raw bytes of trivially-copyable values.
*/
template<class T>
class hash {
public:
/**
* @brief Value type accepted by the hash functor.
*/
using value_type = T;
/**
* @brief Hash a value by viewing its object representation as bytes.
*
* @param value Value whose raw bytes are hashed.
* @return Hash value for the byte representation.
*/
std::size_t operator()(const value_type &value) const {
const auto *p = reinterpret_cast<const char *>(&value);
@@ -465,16 +761,35 @@
}
};
/**
* @brief Convert between enum values and their string names.
*
* @param val Value assigned to the synchronized object.
* @return Const reference to the enum's underlying integral storage.
*/
template<class T>
auto enm(const T &val) -> const std::underlying_type_t<T> & {
return *reinterpret_cast<const std::underlying_type_t<T> *>(&val);
}
/**
* @brief Convert between enum values and their string names.
*
* @param val Value assigned to the synchronized object.
* @return Mutable reference to the enum's underlying integral storage.
*/
template<class T>
auto enm(T &val) -> std::underlying_type_t<T> & {
return *reinterpret_cast<std::underlying_type_t<T> *>(&val);
}
/**
* @brief Convert from chars.
*
* @param begin Iterator or pointer marking the start of the input range.
* @param end Iterator or pointer marking the end of the input range.
* @return Value converted from chars.
*/
inline std::int64_t from_chars(const char *begin, const char *end) {
if (begin == end) {
return 0;
@@ -491,53 +806,110 @@
return *begin != '-' ? res + (std::int64_t) (*begin - '0') * mul : -res;
}
/**
* @brief Convert from view.
*
* @param number Integer value to serialize or convert.
* @return Value converted from view.
*/
inline std::int64_t from_view(const std::string_view &number) {
return from_chars(std::begin(number), std::end(number));
}
/**
* @brief Tagged storage for one of two possible value types.
*/
template<class X, class Y>
class Either: public std::variant<std::monostate, X, Y> {
public:
using std::variant<std::monostate, X, Y>::variant;
/**
* @brief Check whether left.
*
* @return True when the comparison condition is satisfied.
*/
constexpr bool has_left() const {
return std::holds_alternative<X>(*this);
}
/**
* @brief Check whether right.
*
* @return True when the comparison condition is satisfied.
*/
constexpr bool has_right() const {
return std::holds_alternative<Y>(*this);
}
/**
* @brief Return the left-hand alternative held by the variant.
*
* @return Mutable reference to the left-hand alternative.
*/
X &left() {
return std::get<X>(*this);
}
/**
* @brief Return the right-hand alternative held by the variant.
*
* @return Mutable reference to the right-hand alternative.
*/
Y &right() {
return std::get<Y>(*this);
}
/**
* @brief Return the left-hand alternative held by the variant.
*
* @return Const reference to the left-hand alternative.
*/
const X &left() const {
return std::get<X>(*this);
}
/**
* @brief Return the right-hand alternative held by the variant.
*
* @return Const reference to the right-hand alternative.
*/
const Y &right() const {
return std::get<Y>(*this);
}
};
// Compared to std::unique_ptr, it adds the ability to get the address of the pointer itself
/**
* @brief Unique pointer wrapper with customizable pointer and deleter types.
*/
template<typename T, typename D = std::default_delete<T>>
class uniq_ptr {
public:
/**
* @brief Object type managed by the unique pointer wrapper.
*/
using element_type = T;
/**
* @brief Pointer type stored by the unique pointer wrapper.
*/
using pointer = element_type *;
/**
* @brief Const pointer type exposed by the unique pointer wrapper.
*/
using const_pointer = element_type const *;
/**
* @brief Callable type used to release the managed pointer.
*/
using deleter_type = D;
constexpr uniq_ptr() noexcept:
_p {nullptr} {
}
/**
* @brief Construct a unique ownership wrapper.
*/
constexpr uniq_ptr(std::nullptr_t) noexcept:
_p {nullptr} {
}
@@ -545,24 +917,45 @@
uniq_ptr(const uniq_ptr &other) noexcept = delete;
uniq_ptr &operator=(const uniq_ptr &other) noexcept = delete;
/**
* @brief Construct a unique ownership wrapper.
*
* @param p Pointer passed to the deleter or conversion helper.
*/
template<class V>
uniq_ptr(V *p) noexcept:
_p {p} {
static_assert(std::is_same_v<element_type, void> || std::is_same_v<element_type, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
}
/**
* @brief Construct a unique ownership wrapper.
*
* @param uniq Unique pointer whose owned value is transferred.
*/
template<class V>
uniq_ptr(std::unique_ptr<V, deleter_type> &&uniq) noexcept:
_p {uniq.release()} {
static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
}
/**
* @brief Construct a unique ownership wrapper.
*
* @param other Source object whose state is copied or moved into this object.
*/
template<class V>
uniq_ptr(uniq_ptr<V, deleter_type> &&other) noexcept:
_p {other.release()} {
static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
}
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @param other Source object whose state is copied or moved into this object.
* @return Reference returned by the operator overload.
*/
template<class V>
uniq_ptr &operator=(uniq_ptr<V, deleter_type> &&other) noexcept {
static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
@@ -571,6 +964,12 @@
return *this;
}
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @param uniq Unique pointer whose owned value is transferred.
* @return Reference returned by the operator overload.
*/
template<class V>
uniq_ptr &operator=(std::unique_ptr<V, deleter_type> &&uniq) noexcept {
static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
@@ -584,6 +983,11 @@
reset();
}
/**
* @brief Reset the object to its initial empty state.
*
* @param p Pointer passed to the deleter or conversion helper.
*/
void reset(pointer p = pointer()) {
if (_p) {
_deleter(_p);
@@ -592,126 +996,277 @@
_p = p;
}
/**
* @brief Release the COM or platform reference owned by the pointer.
*
* @return Reference count or status returned after releasing the object.
*/
pointer release() {
auto tmp = _p;
_p = nullptr;
return tmp;
}
/**
* @brief Return the currently wrapped value or handle.
*
* @return Underlying native handle or object pointer.
*/
pointer get() {
return _p;
}
/**
* @brief Return the currently wrapped value or handle.
*
* @return Underlying native handle or object pointer.
*/
const_pointer get() const {
return _p;
}
/**
* @brief Dereference the managed pointer.
*
* @return Const reference to the pointed-to object.
*/
std::add_lvalue_reference_t<element_type const> operator*() const {
return *_p;
}
/**
* @brief Dereference the managed pointer.
*
* @return Mutable reference to the pointed-to object.
*/
std::add_lvalue_reference_t<element_type> operator*() {
return *_p;
}
/**
* @brief Access members of the managed pointer.
*
* @return Const pointer to the managed object.
*/
const_pointer operator->() const {
return _p;
}
/**
* @brief Access members of the managed pointer.
*
* @return Mutable pointer to the managed object.
*/
pointer operator->() {
return _p;
}
/**
* @brief Expose the stored pointer address for C APIs that write handles.
*
* @return Address of the stored pointer.
*/
pointer *operator&() const {
return &_p;
}
/**
* @brief Expose the stored pointer address for C APIs that write handles.
*
* @return Address of the stored pointer.
*/
pointer *operator&() {
return &_p;
}
/**
* @brief Return the deleter used when resetting the wrapped pointer.
*
* @return Mutable deleter stored by the wrapper.
*/
deleter_type &get_deleter() {
return _deleter;
}
/**
* @brief Return the deleter used when resetting the wrapped pointer.
*
* @return Const deleter stored by the wrapper.
*/
const deleter_type &get_deleter() const {
return _deleter;
}
/**
* @brief Check whether the wrapper currently owns a non-null pointer.
*/
explicit operator bool() const {
return _p != nullptr;
}
protected:
pointer _p;
deleter_type _deleter;
pointer _p; ///< Pointer currently owned by the wrapper.
deleter_type _deleter; ///< Callable used to release `_p`.
};
/**
* @brief Compare two Sunshine unique pointers by their stored addresses.
*
* @param x Left-hand pointer or value being compared.
* @param y Right-hand pointer or value being compared.
* @return True when both wrappers store the same pointer address.
*/
template<class T1, class D1, class T2, class D2>
bool operator==(const uniq_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {
return x.get() == y.get();
}
/**
* @brief Compare two Sunshine unique pointers by their stored addresses.
*
* @param x Left-hand pointer or value being compared.
* @param y Right-hand pointer or value being compared.
* @return True when the wrappers store different pointer addresses.
*/
template<class T1, class D1, class T2, class D2>
bool operator!=(const uniq_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {
return x.get() != y.get();
}
/**
* @brief Compare a standard unique pointer with a Sunshine unique pointer.
*
* @param x Left-hand pointer or value being compared.
* @param y Right-hand pointer or value being compared.
* @return True when both wrappers store the same pointer address.
*/
template<class T1, class D1, class T2, class D2>
bool operator==(const std::unique_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {
return x.get() == y.get();
}
/**
* @brief Compare a standard unique pointer with a Sunshine unique pointer.
*
* @param x Left-hand pointer or value being compared.
* @param y Right-hand pointer or value being compared.
* @return True when the wrappers store different pointer addresses.
*/
template<class T1, class D1, class T2, class D2>
bool operator!=(const std::unique_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {
return x.get() != y.get();
}
/**
* @brief Compare a Sunshine unique pointer with a standard unique pointer.
*
* @param x Left-hand pointer or value being compared.
* @param y Right-hand pointer or value being compared.
* @return True when both wrappers store the same pointer address.
*/
template<class T1, class D1, class T2, class D2>
bool operator==(const uniq_ptr<T1, D1> &x, const std::unique_ptr<T1, D1> &y) {
return x.get() == y.get();
}
/**
* @brief Compare a Sunshine unique pointer with a standard unique pointer.
*
* @param x Left-hand pointer or value being compared.
* @param y Right-hand pointer or value being compared.
* @return True when the wrappers store different pointer addresses.
*/
template<class T1, class D1, class T2, class D2>
bool operator!=(const uniq_ptr<T1, D1> &x, const std::unique_ptr<T1, D1> &y) {
return x.get() != y.get();
}
/**
* @brief Compare a Sunshine unique pointer with null.
*
* @param x Left-hand pointer or value being compared.
* @return True when the wrapper does not own a pointer.
*/
template<class T, class D>
bool operator==(const uniq_ptr<T, D> &x, std::nullptr_t) {
return !(bool) x;
}
/**
* @brief Compare a Sunshine unique pointer with null.
*
* @param x Left-hand pointer or value being compared.
* @return True when the wrapper owns a pointer.
*/
template<class T, class D>
bool operator!=(const uniq_ptr<T, D> &x, std::nullptr_t) {
return (bool) x;
}
/**
* @brief Compare null with a Sunshine unique pointer.
*
* @param y Right-hand pointer or value being compared.
* @return True when the wrapper does not own a pointer.
*/
template<class T, class D>
bool operator==(std::nullptr_t, const uniq_ptr<T, D> &y) {
return !(bool) y;
}
/**
* @brief Compare null with a Sunshine unique pointer.
*
* @param y Right-hand pointer or value being compared.
* @return True when the wrapper owns a pointer.
*/
template<class T, class D>
bool operator!=(std::nullptr_t, const uniq_ptr<T, D> &y) {
return (bool) y;
}
/**
* @brief Shared pointer type matching a safe pointer wrapper's element type.
*/
template<class P>
using shared_t = std::shared_ptr<typename P::element_type>;
/**
* @brief Create a shared object or message.
*
* @param pointer Raw pointer adopted by the safe pointer wrapper.
* @return Constructed shared object.
*/
template<class P, class T>
shared_t<P> make_shared(T *pointer) {
return shared_t<P>(reinterpret_cast<typename P::pointer>(pointer), typename P::deleter_type());
}
/**
* @brief Pointer wrapper that may borrow or own the pointee.
*/
template<class T>
class wrap_ptr {
public:
/**
* @brief Object type referenced by the pointer wrapper.
*/
using element_type = T;
/**
* @brief Mutable pointer type exposed by the pointer wrapper.
*/
using pointer = element_type *;
/**
* @brief Const pointer type exposed by the pointer wrapper.
*/
using const_pointer = element_type const *;
/**
* @brief Mutable reference type exposed by the pointer wrapper.
*/
using reference = element_type &;
/**
* @brief Const reference type exposed by the pointer wrapper.
*/
using const_reference = element_type const &;
wrap_ptr():
@@ -719,22 +1274,43 @@
_p {nullptr} {
}
/**
* @brief Construct an owning or non-owning wrapper around a raw pointer.
*
* @param p Pointer passed to the deleter or conversion helper.
*/
wrap_ptr(pointer p):
_own_ptr {false},
_p {p} {
}
/**
* @brief Construct an owning or non-owning wrapper around a raw pointer.
*
* @param uniq_p Uniq p.
*/
wrap_ptr(std::unique_ptr<element_type> &&uniq_p):
_own_ptr {true},
_p {uniq_p.release()} {
}
/**
* @brief Construct an owning or non-owning wrapper around a raw pointer.
*
* @param other Source object whose state is copied or moved into this object.
*/
wrap_ptr(wrap_ptr &&other):
_own_ptr {other._own_ptr},
_p {other._p} {
other._own_ptr = false;
}
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @param other Source object whose state is copied or moved into this object.
* @return Reference returned by the operator overload.
*/
wrap_ptr &operator=(wrap_ptr &&other) noexcept {
if (_own_ptr) {
delete _p;
@@ -748,6 +1324,12 @@
return *this;
}
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @param uniq_ptr Unique pointer whose ownership is transferred to the wrapper.
* @return Reference returned by the operator overload.
*/
template<class V>
wrap_ptr &operator=(std::unique_ptr<V> &&uniq_ptr) {
static_assert(std::is_base_of_v<element_type, V>, "element_type must be base class of V");
@@ -757,6 +1339,12 @@
return *this;
}
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @param p Pointer passed to the deleter or conversion helper.
* @return Reference returned by the operator overload.
*/
wrap_ptr &operator=(pointer p) {
if (_own_ptr) {
delete _p;
@@ -776,18 +1364,38 @@
_own_ptr = false;
}
/**
* @brief Dereference the wrapped pointer.
*
* @return Const reference to the pointed-to object.
*/
const_reference operator*() const {
return *_p;
}
/**
* @brief Dereference the wrapped pointer.
*
* @return Mutable reference to the pointed-to object.
*/
reference operator*() {
return *_p;
}
/**
* @brief Access members of the wrapped pointer.
*
* @return Const pointer to the wrapped object.
*/
const_pointer operator->() const {
return _p;
}
/**
* @brief Access members of the wrapped pointer.
*
* @return Mutable pointer to the wrapped object.
*/
pointer operator->() {
return _p;
}
@@ -798,6 +1406,9 @@
};
template<class T>
/**
* @brief Trait value indicating whether the template argument is a pointer.
*/
constexpr bool is_pointer_v =
instantiation_of_v<std::unique_ptr, T> ||
instantiation_of_v<std::shared_ptr, T> ||
@@ -807,48 +1418,79 @@
template<class T, class V = void>
struct __false_v;
/**
* @brief Helper specialization for optional values.
*/
template<class T>
struct __false_v<T, std::enable_if_t<instantiation_of_v<std::optional, T>>> {
static constexpr std::nullopt_t value = std::nullopt;
static constexpr std::nullopt_t value = std::nullopt; ///< Value.
};
/**
* @brief Enables an overload only when the provided type is a pointer.
*/
template<class T>
struct __false_v<T, std::enable_if_t<is_pointer_v<T>>> {
static constexpr std::nullptr_t value = nullptr;
static constexpr std::nullptr_t value = nullptr; ///< Value.
};
/**
* @brief Type trait comparing a value with a boolean template argument.
*/
template<class T>
struct __false_v<T, std::enable_if_t<std::is_same_v<T, bool>>> {
static constexpr bool value = false;
static constexpr bool value = false; ///< Trait value for the false specialization.
};
template<class T>
static constexpr auto false_v = __false_v<T>::value;
/**
* @brief Optional trait value used by endian serialization helpers.
*/
template<class T>
using optional_t = either_t<
(std::is_same_v<T, bool> || is_pointer_v<T>),
T,
std::optional<T>>;
/**
* @brief Owning contiguous buffer with an explicit logical element count.
*/
template<class T>
class buffer_t {
public:
buffer_t():
_els {0} {};
/**
* @brief Construct an owning contiguous buffer.
*
* @param o Source object used for comparison or assignment.
*/
buffer_t(buffer_t &&o) noexcept:
_els {o._els},
_buf {std::move(o._buf)} {
o._els = 0;
}
/**
* @brief Construct an owning contiguous buffer.
*
* @param o Source object used for comparison or assignment.
*/
buffer_t(const buffer_t &o):
_els {o._els},
_buf {std::make_unique<T[]>(_els)} {
std::copy(o.begin(), o.end(), begin());
}
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @param o Source object used for comparison or assignment.
* @return Reference returned by the operator overload.
*/
buffer_t &operator=(buffer_t &&o) noexcept {
std::swap(_els, o._els);
std::swap(_buf, o._buf);
@@ -856,45 +1498,98 @@
return *this;
};
/**
* @brief Construct an owning contiguous buffer.
*
* @param elements Elements copied into the buffer.
*/
explicit buffer_t(size_t elements):
_els {elements},
_buf {std::make_unique<T[]>(elements)} {
}
/**
* @brief Construct an owning contiguous buffer.
*
* @param elements Elements copied into the buffer.
* @param t Initial value used to populate the GPU buffer.
*/
explicit buffer_t(size_t elements, const T &t):
_els {elements},
_buf {std::make_unique<T[]>(elements)} {
std::fill_n(_buf.get(), elements, t);
}
/**
* @brief Access an element in the owning buffer.
*
* @param el Zero-based element index.
* @return Mutable reference to the requested element.
*/
T &operator[](size_t el) {
return _buf[el];
}
/**
* @brief Access an element in the owning buffer.
*
* @param el Zero-based element index.
* @return Const reference to the requested element.
*/
const T &operator[](size_t el) const {
return _buf[el];
}
/**
* @brief Return the serialized size of the current object.
*
* @return Number of elements currently stored.
*/
size_t size() const {
return _els;
}
/**
* @brief Update the logical element count without reallocating storage.
*
* @param els Elements used to initialize the buffer.
*/
void fake_resize(std::size_t els) {
_els = els;
}
/**
* @brief Return an iterator to the first byte in the buffer view.
*
* @return Iterator to the first element.
*/
T *begin() {
return _buf.get();
}
/**
* @brief Return an iterator to the first byte in the buffer view.
*
* @return Iterator to the first element.
*/
const T *begin() const {
return _buf.get();
}
/**
* @brief Return an iterator one past the final byte in the buffer view.
*
* @return Iterator one past the last element.
*/
T *end() {
return _buf.get() + _els;
}
/**
* @brief Return an iterator one past the final byte in the buffer view.
*
* @return Iterator one past the last element.
*/
const T *end() const {
return _buf.get() + _els;
}
@@ -904,6 +1599,13 @@
std::unique_ptr<T[]> _buf;
};
/**
* @brief Build an either value from a left-hand value.
*
* @param l Left-hand value used to construct the either object.
* @param r Fallback value returned when the optional left-hand value is empty.
* @return Left-hand optional value when present, otherwise the fallback.
*/
template<class T>
T either(std::optional<T> &&l, T &&r) {
if (l) {
@@ -913,66 +1615,130 @@
return std::forward<T>(r);
}
/**
* @brief Callable wrapper used by utility metaprogramming helpers.
*/
template<class ReturnType, class... Args>
struct Function {
/**
* @brief Type extracted from the matched function signature.
*/
typedef ReturnType (*type)(Args...);
};
/**
* @brief Deleter adapter that destroys the wrapped pointer.
*/
template<class T, class ReturnType, typename Function<ReturnType, T>::type function>
struct Destroy {
/**
* @brief Pointer type accepted by this deleter.
*/
typedef T pointer;
/**
* @brief Invoke the configured destroy function on a pointer.
*
* @param p Pointer to release.
*/
void operator()(pointer p) {
function(p);
}
};
/**
* @brief Unique pointer using a compile-time C-style destroy function.
*/
template<class T, typename Function<void, T *>::type function>
using safe_ptr = uniq_ptr<T, Destroy<T *, void, function>>;
// You cannot specialize an alias
/**
* @brief Safe pointer wrapper for APIs whose release function returns a status value.
*/
template<class T, class ReturnType, typename Function<ReturnType, T *>::type function>
using safe_ptr_v2 = uniq_ptr<T, Destroy<T *, ReturnType, function>>;
/**
* @brief Release memory allocated by C APIs with `free`.
*
* @param p Pointer passed to the deleter or conversion helper.
*/
template<class T>
void c_free(T *p) {
free(p);
}
/**
* @brief Create a non-owning dynamic buffer view over contiguous memory.
*
* @param p Pointer passed to the deleter or conversion helper.
*/
template<class T, class ReturnType, ReturnType (**function)(T *)>
void dynamic(T *p) {
(*function)(p);
}
/**
* @brief Unique pointer using a runtime-provided destroy function.
*/
template<class T, void (**function)(T *)>
using dyn_safe_ptr = safe_ptr<T, dynamic<T, void, function>>;
/**
* @brief Safe pointer wrapper for runtime-resolved release functions with status returns.
*/
template<class T, class ReturnType, ReturnType (**function)(T *)>
using dyn_safe_ptr_v2 = safe_ptr<T, dynamic<T, ReturnType, function>>;
/**
* @brief Safe pointer wrapper for memory released by `std::free`.
*/
template<class T>
using c_ptr = safe_ptr<T, c_free<T>>;
/**
* @brief Read the current value without removing it from the queue.
*
* @param begin Iterator or pointer marking the start of the input range.
* @param end Iterator or pointer marking the end of the input range.
* @return String view spanning the iterator range.
*/
template<class It>
std::string_view view(It begin, It end) {
return std::string_view {(const char *) begin, (std::size_t) (end - begin)};
}
/**
* @brief Read the current value without removing it from the queue.
*
* @param data Payload or state data to serialize, deserialize, or forward.
* @return String view over the contiguous object's bytes.
*/
template<class T>
std::string_view view(const T &data) {
return std::string_view((const char *) &data, sizeof(T));
}
/**
* @brief Two-dimensional integer point.
*/
struct point_t {
double x; ///< X.
double x;
double y;
double y; ///< Y.
/**
* @brief Operator.
*/
friend std::ostream &operator<<(std::ostream &os, const point_t &p) {
return (os << "Point(x: " << p.x << ", y: " << p.y << ")");
}
};
namespace endian {
/**
* @brief Describes the byte order used when serializing values.
*/
template<class T = void>
struct endianness {
enum : bool {
@@ -1002,8 +1768,17 @@
template<class T, class S = void>
struct endian_helper {};
/**
* @brief Helper specialization for optional values.
*/
template<class T>
struct endian_helper<T, std::enable_if_t<!(instantiation_of_v<std::optional, T>)>> {
/**
* @brief Convert a value to or from big-endian byte order.
*
* @param x Value to convert.
* @return Value represented in big-endian byte order.
*/
static inline T big(T x) {
if constexpr (endianness<T>::little) {
uint8_t *data = reinterpret_cast<uint8_t *>(&x);
@@ -1014,6 +1789,12 @@
return x;
}
/**
* @brief Convert a value to or from little-endian byte order.
*
* @param x Value to convert.
* @return Value represented in little-endian byte order.
*/
static inline T little(T x) {
if constexpr (endianness<T>::big) {
uint8_t *data = reinterpret_cast<uint8_t *>(&x);
@@ -1025,8 +1806,17 @@
}
};
/**
* @brief Helper specialization for optional values.
*/
template<class T>
struct endian_helper<T, std::enable_if_t<instantiation_of_v<std::optional, T>>> {
/**
* @brief Convert a value to or from little-endian byte order.
*
* @param x Pointer to convert.
* @return Pointer with the pointed-to value represented in little-endian byte order.
*/
static inline T little(T x) {
if (!x) {
return x;
@@ -1041,6 +1831,12 @@
return x;
}
/**
* @brief Convert a value to or from big-endian byte order.
*
* @param x Pointer to convert.
* @return Pointer with the pointed-to value represented in big-endian byte order.
*/
static inline T big(T x) {
if (!x) {
return x;
@@ -1056,11 +1852,23 @@
}
};
/**
* @brief Convert a value to or from little-endian byte order.
*
* @param x Value or pointer to convert.
* @return Input represented in little-endian byte order.
*/
template<class T>
inline auto little(T x) {
return endian_helper<T>::little(x);
}
/**
* @brief Convert a value to or from big-endian byte order.
*
* @param x Value or pointer to convert.
* @return Input represented in big-endian byte order.
*/
template<class T>
inline auto big(T x) {
return endian_helper<T>::big(x);
src/uuid.h +41 −4
@@ -11,11 +11,20 @@
* @brief UUID utilities.
*/
namespace uuid_util {
/**
* @brief UUID value exposed through multiple integer views.
*/
union uuid_t {
std::uint8_t b8[16];
std::uint16_t b16[8];
std::uint32_t b32[4];
std::uint64_t b64[2];
std::uint8_t b8[16]; ///< UUID bytes.
std::uint16_t b16[8]; ///< UUID viewed as 16-bit words.
std::uint32_t b32[4]; ///< UUID viewed as 32-bit words.
std::uint64_t b64[2]; ///< UUID viewed as 64-bit words.
/**
* @brief Generate a UUID value.
*
* @param engine Random-number engine used to generate UUID bytes.
* @return Random UUID generated from the supplied engine.
*/
static uuid_t generate(std::default_random_engine &engine) {
std::uniform_int_distribution<std::uint8_t> dist(0, std::numeric_limits<std::uint8_t>::max());
@@ -31,5 +40,10 @@
return buf;
}
/**
* @brief Generate a UUID value.
*
* @return Random UUID generated from std::random_device seeding.
*/
static uuid_t generate() {
std::random_device r;
@@ -39,5 +53,10 @@
return generate(engine);
}
/**
* @brief Format the UUID using canonical text form.
*
* @return Canonical lowercase UUID string.
*/
[[nodiscard]] std::string string() const {
std::string result;
@@ -66,14 +85,32 @@
return result;
}
/**
* @brief Compare two UUID values for equality.
*
* @param other UUID value to compare against.
* @return True when both UUID values contain identical bytes.
*/
constexpr bool operator==(const uuid_t &other) const {
return b64[0] == other.b64[0] && b64[1] == other.b64[1];
}
/**
* @brief Order UUID values by their 64-bit word representation.
*
* @param other UUID value to compare against.
* @return True when this UUID sorts before `other`.
*/
constexpr bool operator<(const uuid_t &other) const {
return (b64[0] < other.b64[0] || (b64[0] == other.b64[0] && b64[1] < other.b64[1]));
}
/**
* @brief Order UUID values by their 64-bit word representation.
*
* @param other UUID value to compare against.
* @return True when this UUID sorts after `other`.
*/
constexpr bool operator>(const uuid_t &other) const {
return (b64[0] > other.b64[0] || (b64[0] == other.b64[0] && b64[1] > other.b64[1]));
}
src/video.cpp +446 −38
@@ -72,25 +72,40 @@
}
} // namespace
/**
* @brief Release context resources.
*/
void free_ctx(AVCodecContext *ctx) {
avcodec_free_context(&ctx);
}
/**
* @brief Release an FFmpeg frame allocated by the capture or conversion backend.
*/
void free_frame(AVFrame *frame) {
av_frame_free(&frame);
}
/**
* @brief Release a backend buffer allocated for capture or conversion.
*/
void free_buffer(AVBufferRef *ref) {
av_buffer_unref(&ref);
}
namespace nv {
/**
* @brief Enumerates supported profile h264 options.
*/
enum class profile_h264_e : int {
high = 2, ///< High profile
high_444p = 3, ///< High 4:4:4 Predictive profile
};
/**
* @brief Enumerates supported profile HEVC options.
*/
enum class profile_hevc_e : int {
main = 0, ///< Main profile
main_10 = 1, ///< Main 10 profile
@@ -101,17 +116,26 @@
namespace qsv {
/**
* @brief Enumerates supported profile h264 options.
*/
enum class profile_h264_e : int {
high = 100, ///< High profile
high_444p = 244, ///< High 4:4:4 Predictive profile
};
/**
* @brief Enumerates supported profile HEVC options.
*/
enum class profile_hevc_e : int {
main = 1, ///< Main profile
main_10 = 2, ///< Main 10 profile
rext = 4, ///< RExt profile
};
/**
* @brief Enumerates supported profile AV1 options.
*/
enum class profile_av1_e : int {
main = 1, ///< Main profile
high = 2, ///< High profile
@@ -119,14 +143,52 @@
} // namespace qsv
/**
* @brief Create an FFmpeg hardware device buffer for D3D11VA input.
*
* @param encode_device Encode device.
* @return Hardware buffer on success, or an error code on failure.
*/
util::Either<avcodec_buffer_t, int> dxgi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);
/**
* @brief Create an FFmpeg hardware device buffer for VA-API input.
*
* @param encode_device Encode device.
* @return Hardware buffer on success, or an error code on failure.
*/
util::Either<avcodec_buffer_t, int> vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);
/**
* @brief Create an FFmpeg hardware device buffer for CUDA input.
*
* @param encode_device Encode device.
* @return Hardware buffer on success, or an error code on failure.
*/
util::Either<avcodec_buffer_t, int> cuda_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);
/**
* @brief Create an FFmpeg hardware device buffer for VideoToolbox input.
*
* @param encode_device Encode device.
* @return Hardware buffer on success, or an error code on failure.
*/
util::Either<avcodec_buffer_t, int> vt_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);
/**
* @brief Create an FFmpeg hardware device buffer for Vulkan input.
*
* @return Hardware buffer on success, or an error code on failure.
*/
util::Either<avcodec_buffer_t, int> vulkan_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);
/**
* @brief FFmpeg software encode device used when no hardware frames are required.
*/
class avcodec_software_encode_device_t: public platf::avcodec_encode_device_t {
public:
/**
* @brief Accept a software frame without additional hardware conversion.
*
* @param img Image or frame object to read from or populate.
* @return Conversion status.
*/
int convert(platf::img_t &img) override {
// If we need to add aspect ratio padding, we need to scale into an intermediate output buffer
bool requires_padding = (sw_frame->width != sws_output_frame->width || sw_frame->height != sws_output_frame->height);
@@ -173,5 +235,12 @@
return 0;
}
/**
* @brief Attach frame resources used by the next conversion or encode operation.
*
* @param frame Video or graphics frame being processed.
* @param hw_frames_ctx FFmpeg hardware frames context associated with the frame.
* @return Status from updating frame.
*/
int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override {
this->frame = frame;
@@ -190,6 +259,9 @@
return 0;
}
/**
* @brief Apply the configured colorspace metadata to the active frame.
*/
void apply_colorspace() override {
auto avcodec_colorspace = avcodec_colorspace_from_sunshine_colorspace(colorspace);
sws_setColorspaceDetails(sws.get(), sws_getCoefficients(SWS_CS_DEFAULT), 0, sws_getCoefficients(avcodec_colorspace.software_format), avcodec_colorspace.range - 1, 0, 1 << 16, 1 << 16);
@@ -206,6 +278,16 @@
av_image_fill_black(frame->data, linesize, (AVPixelFormat) frame->format, frame->color_range, frame->width, frame->height);
}
/**
* @brief Initialize FFmpeg software encoding for the requested codec.
*
* @param in_width In width.
* @param in_height In height.
* @param frame Video or graphics frame being processed.
* @param format Pixel, audio, or protocol format being converted.
* @param hardware Whether the frame is backed by hardware resources.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(int in_width, int in_height, AVFrame *frame, AVPixelFormat format, bool hardware) {
// If the device used is hardware, yet the image resides on main memory
if (hardware) {
@@ -277,18 +359,21 @@
}
// Store ownership when frame is hw_frame
avcodec_frame_t hw_frame;
avcodec_frame_t hw_frame; ///< Hw frame.
avcodec_frame_t sw_frame;
avcodec_frame_t sws_input_frame;
avcodec_frame_t sws_output_frame;
sws_t sws;
avcodec_frame_t sw_frame; ///< Sw frame.
avcodec_frame_t sws_input_frame; ///< Sws input frame.
avcodec_frame_t sws_output_frame; ///< Sws output frame.
sws_t sws; ///< Software scaler used when frames need CPU-side pixel conversion.
// Offset of input image to output frame in pixels
int offsetW;
int offsetH;
int offsetW; ///< Offset w.
int offsetH; ///< Offset h.
};
/**
* @brief Enumerates supported flag options.
*/
enum flag_e : uint32_t {
DEFAULT = 0, ///< Default flags
PARALLEL_ENCODING = 1 << 1, ///< Capture and encoding can run concurrently on separate threads
@@ -305,16 +390,31 @@
FIXED_GOP_SIZE = 1 << 12, ///< Use fixed small GOP size (encoder doesn't support on-demand IDR frames)
};
/**
* @brief FFmpeg AVCodec encode session and parameter-set rewriting state.
*/
class avcodec_encode_session_t: public encode_session_t {
public:
avcodec_encode_session_t() = default;
/**
* @brief Initialize an FFmpeg encode session and its hardware encode device.
*
* @param avcodec_ctx Open FFmpeg codec context for the selected encoder.
* @param encode_device Platform encode device that supplies frames to FFmpeg.
* @param inject Whether SPS/VPS replacement data should be injected.
*/
avcodec_encode_session_t(avcodec_ctx_t &&avcodec_ctx, std::unique_ptr<platf::avcodec_encode_device_t> encode_device, int inject):
avcodec_ctx {std::move(avcodec_ctx)},
device {std::move(encode_device)},
inject {inject} {
}
/**
* @brief Move an FFmpeg encode session without duplicating codec/device ownership.
*
* @param other Source object whose state is copied or moved into this object.
*/
avcodec_encode_session_t(avcodec_encode_session_t &&other) noexcept = default;
~avcodec_encode_session_t() {
@@ -330,6 +430,12 @@
}
// Ensure objects are destroyed in the correct order
/**
* @brief Assign state from another instance while preserving ownership semantics.
*
* @param other Source object whose state is copied or moved into this object.
* @return Reference or value produced by the operator.
*/
avcodec_encode_session_t &operator=(avcodec_encode_session_t &&other) {
device = std::move(other.device);
avcodec_ctx = std::move(other.avcodec_ctx);
@@ -342,6 +448,12 @@
return *this;
}
/**
* @brief Encode one frame with FFmpeg AVCodec and prepare packet replacements.
*
* @param img Image or frame object to read from or populate.
* @return Conversion status.
*/
int convert(platf::img_t &img) override {
if (!device) {
return -1;
@@ -349,6 +461,9 @@
return device->convert(img);
}
/**
* @brief Mark the frame as a request for an IDR frame.
*/
void request_idr_frame() override {
if (device && device->frame) {
auto &frame = device->frame;
@@ -357,6 +472,9 @@
}
}
/**
* @brief Mark the frame as a request for a normal inter frame.
*/
void request_normal_frame() override {
if (device && device->frame) {
auto &frame = device->frame;
@@ -365,29 +483,49 @@
}
}
/**
* @brief Mark the frame range whose references must be invalidated.
*
* @param first_frame First frame.
* @param last_frame Last frame.
*/
void invalidate_ref_frames(int64_t first_frame, int64_t last_frame) override {
BOOST_LOG(error) << "Encoder doesn't support reference frame invalidation";
request_idr_frame();
}
avcodec_ctx_t avcodec_ctx;
std::unique_ptr<platf::avcodec_encode_device_t> device;
avcodec_ctx_t avcodec_ctx; ///< FFmpeg codec context owned by the encode session.
std::unique_ptr<platf::avcodec_encode_device_t> device; ///< Platform device used by the FFmpeg hardware encoder.
std::vector<packet_raw_t::replace_t> replacements;
std::vector<packet_raw_t::replace_t> replacements; ///< NAL-unit byte ranges that must be replaced before packet send.
cbs::nal_t sps; ///< Original and rewritten sequence parameter set for IDR injection.
cbs::nal_t sps;
cbs::nal_t vps;
cbs::nal_t vps; ///< Original and rewritten HEVC video parameter set for IDR injection.
// inject sps/vps data into idr pictures
int inject; ///< Number of upcoming IDR frames that should receive rewritten parameter sets.
int inject;
};
/**
* @brief NVENC encode session and device state for hardware encoding.
*/
class nvenc_encode_session_t: public encode_session_t {
public:
/**
* @brief Initialize an NVENC encode session and take ownership of its device.
*
* @param encode_device Encode device.
*/
nvenc_encode_session_t(std::unique_ptr<platf::nvenc_encode_device_t> encode_device):
device(std::move(encode_device)) {
}
/**
* @brief Encode one frame with NVENC and return the packet payload.
*
* @param img Image or frame object to read from or populate.
* @return Conversion status.
*/
int convert(platf::img_t &img) override {
if (!device) {
return -1;
@@ -395,14 +533,26 @@
return device->convert(img);
}
/**
* @brief Mark the frame as a request for an IDR frame.
*/
void request_idr_frame() override {
force_idr = true;
}
/**
* @brief Mark the frame as a request for a normal inter frame.
*/
void request_normal_frame() override {
force_idr = false;
}
/**
* @brief Mark the frame range whose references must be invalidated.
*
* @param first_frame First frame.
* @param last_frame Last frame.
*/
void invalidate_ref_frames(int64_t first_frame, int64_t last_frame) override {
if (!device || !device->nvenc) {
return;
@@ -413,6 +563,12 @@
}
}
/**
* @brief Submit the next frame to NVENC and return the encoded payload.
*
* @param frame_index Monotonic frame index assigned by the video pipeline.
* @return Encoded NVENC frame payload and frame metadata.
*/
nvenc::nvenc_encoded_frame encode_frame(uint64_t frame_index) {
if (!device || !device->nvenc) {
return {};
@@ -428,55 +584,101 @@
bool force_idr = false;
};
/**
* @brief Context object used while synchronizing encode sessions.
*/
struct sync_session_ctx_t {
safe::signal_t *join_event; ///< Signal raised when the capture and encode workers should join.
safe::mail_raw_t::event_t<bool> shutdown_event; ///< Event raised when the stream should shut down.
safe::mail_raw_t::queue_t<packet_t> packets; ///< Queue receiving encoded video packets for the stream sender.
safe::mail_raw_t::event_t<bool> idr_events; ///< Event raised when an IDR frame is requested.
safe::mail_raw_t::event_t<hdr_info_t> hdr_events; ///< Event carrying updated HDR metadata.
safe::signal_t *join_event;
safe::mail_raw_t::event_t<bool> shutdown_event;
safe::mail_raw_t::queue_t<packet_t> packets;
safe::mail_raw_t::event_t<bool> idr_events;
safe::mail_raw_t::event_t<hdr_info_t> hdr_events;
safe::mail_raw_t::event_t<input::touch_port_t> touch_port_events;
safe::mail_raw_t::event_t<input::touch_port_t> touch_port_events; ///< Event carrying updated touch viewport metadata.
config_t config;
int frame_nr;
void *channel_data;
config_t config; ///< Stream or encoder configuration captured for the worker.
int frame_nr; ///< Next capture-frame number assigned to encoded packets.
void *channel_data; ///< Platform-specific channel data forwarded to packet senders.
};
/**
* @brief Synchronization state for one encode session.
*/
struct sync_session_t {
sync_session_ctx_t *ctx;
sync_session_ctx_t *ctx; ///< Shared capture/encode synchronization context.
std::unique_ptr<encode_session_t> session; ///< Active encoder session used by the capture thread.
std::unique_ptr<encode_session_t> session;
};
/**
* @brief Queue of encode-session contexts waiting for capture work.
*/
using encode_session_ctx_queue_t = safe::queue_t<sync_session_ctx_t>;
/**
* @brief Platform capture status returned by encode operations.
*/
using encode_e = platf::capture_e;
/**
* @brief Capture thread context shared with the encoder session.
*/
struct capture_ctx_t {
img_event_t images; ///< Queue of captured images waiting for encode.
img_event_t images;
config_t config;
config_t config; ///< Stream or encoder configuration captured for the worker.
};
/**
* @brief Asynchronous capture thread state.
*/
struct capture_thread_async_ctx_t {
std::shared_ptr<safe::queue_t<capture_ctx_t>> capture_ctx_queue;
std::thread capture_thread;
std::shared_ptr<safe::queue_t<capture_ctx_t>> capture_ctx_queue; ///< Capture ctx queue.
std::thread capture_thread; ///< Capture thread.
safe::signal_t reinit_event;
const encoder_t *encoder_p;
safe::signal_t reinit_event; ///< Reinit event.
const encoder_t *encoder_p; ///< Encoder p.
sync_util::sync_t<std::weak_ptr<platf::display_t>> display_wp; ///< Display wp.
sync_util::sync_t<std::weak_ptr<platf::display_t>> display_wp;
};
/**
* @brief Synchronous capture thread state.
*/
struct capture_thread_sync_ctx_t {
encode_session_ctx_queue_t encode_session_ctx_queue {30};
encode_session_ctx_queue_t encode_session_ctx_queue {30}; ///< Encode session ctx queue.
};
/**
* @brief Start the synchronous multi-client capture thread.
*
* @param ctx Native context object used by the operation or callback.
* @return 0 when the capture thread is started.
*/
int start_capture_sync(capture_thread_sync_ctx_t &ctx);
/**
* @brief Stop capture sync processing.
*
* @param ctx Native context object used by the operation or callback.
*/
void end_capture_sync(capture_thread_sync_ctx_t &ctx);
/**
* @brief Start the asynchronous capture thread.
*
* @param ctx Native context object used by the operation or callback.
* @return 0 when the capture thread is started; nonzero on setup failure.
*/
int start_capture_async(capture_thread_async_ctx_t &ctx);
/**
* @brief Stop capture async processing.
*
* @param ctx Native context object used by the operation or callback.
*/
void end_capture_async(capture_thread_async_ctx_t &ctx);
// Keep a reference counter to ensure the capture thread only runs when other threads have a reference to the capture thread
auto capture_thread_async = safe::make_shared<capture_thread_async_ctx_t>(start_capture_async, end_capture_async);
auto capture_thread_async = safe::make_shared<capture_thread_async_ctx_t>(start_capture_async, end_capture_async); ///< Capture thread async.
auto capture_thread_sync = safe::make_shared<capture_thread_sync_ctx_t>(start_capture_sync, end_capture_sync); ///< Capture thread sync.
auto capture_thread_sync = safe::make_shared<capture_thread_sync_ctx_t>(start_capture_sync, end_capture_sync);
#ifdef _WIN32
/**
* @brief NVENC.
*/
encoder_t nvenc {
"nvenc"sv,
std::make_unique<encoder_platform_formats_nvenc>(
@@ -615,6 +817,9 @@
#endif
#ifdef _WIN32
/**
* @brief Quicksync.
*/
encoder_t quicksync {
"quicksync"sv,
std::make_unique<encoder_platform_formats_avcodec>(
@@ -723,6 +928,9 @@
PARALLEL_ENCODING | CBR_WITH_VBR | RELAXED_COMPLIANCE | NO_RC_BUF_LIMIT | YUV444_SUPPORT
};
/**
* @brief Amdvce.
*/
encoder_t amdvce {
"amdvce"sv,
std::make_unique<encoder_platform_formats_avcodec>(
@@ -829,6 +1037,9 @@
PARALLEL_ENCODING
};
/**
* @brief Mediafoundation.
*/
encoder_t mediafoundation {
"mediafoundation"sv,
std::make_unique<encoder_platform_formats_avcodec>(
@@ -887,6 +1098,9 @@
};
#endif
/**
* @brief Software.
*/
encoder_t software {
"software"sv,
std::make_unique<encoder_platform_formats_avcodec>(
@@ -958,6 +1172,9 @@
};
#if defined(__linux__) || defined(linux) || defined(__linux) || defined(__FreeBSD__)
/**
* @brief VA-API.
*/
encoder_t vaapi {
"vaapi"sv,
std::make_unique<encoder_platform_formats_avcodec>(
@@ -1088,6 +1305,9 @@
#endif // linux
#ifdef __APPLE__
/**
* @brief Videotoolbox.
*/
encoder_t videotoolbox {
"videotoolbox"sv,
std::make_unique<encoder_platform_formats_avcodec>(
@@ -1181,11 +1401,19 @@
};
static encoder_t *chosen_encoder;
int active_hevc_mode;
int active_av1_mode;
bool last_encoder_probe_supported_ref_frames_invalidation = false;
std::array<bool, 3> last_encoder_probe_supported_yuv444_for_codec = {};
int active_hevc_mode; ///< HEVC mode selected by the most recent encoder probe.
int active_av1_mode; ///< AV1 mode selected by the most recent encoder probe.
bool last_encoder_probe_supported_ref_frames_invalidation = false; ///< Whether the last probe found reference-frame invalidation support.
std::array<bool, 3> last_encoder_probe_supported_yuv444_for_codec = {}; ///< YUV444 support discovered for each probed codec.
/**
* @brief Recreate a display capture object after a capture failure.
*
* @param disp Display connection or display handle.
* @param type Protocol, message, or resource type selector.
* @param display_name Display name.
* @param config Configuration values to apply.
*/
void reset_display(std::shared_ptr<platf::display_t> &disp, const platf::mem_type_e &type, const std::string &display_name, const config_t &config) {
// We try this twice, in case we still get an error on reinitialization
for (int x = 0; x < 2; ++x) {
@@ -1254,6 +1482,14 @@
}
}
/**
* @brief Run the shared display capture thread for asynchronous encoding.
*
* @param capture_ctx_queue Capture context queue.
* @param display_wp Weak pointer holder for the active display.
* @param reinit_event Signal raised while the display is being reinitialized.
* @param encoder Selected encoder.
*/
void captureThread(
std::shared_ptr<safe::queue_t<capture_ctx_t>> capture_ctx_queue,
sync_util::sync_t<std::weak_ptr<platf::display_t>> &display_wp,
@@ -1511,6 +1747,16 @@
}
}
/**
* @brief Drain encoded packets from an FFmpeg encoder session.
*
* @param frame_nr Monotonic frame index assigned by the video pipeline.
* @param session Active FFmpeg encoder session.
* @param packets Output queue that receives encoded packets.
* @param channel_data Platform or protocol state attached to each packet.
* @param frame_timestamp Capture timestamp associated with the encoded frame.
* @return 0 when packets are queued; nonzero when encoding or packetization fails.
*/
int encode_avcodec(int64_t frame_nr, avcodec_encode_session_t &session, safe::mail_raw_t::queue_t<packet_t> &packets, void *channel_data, std::optional<std::chrono::steady_clock::time_point> frame_timestamp) {
auto &frame = session.device->frame;
frame->pts = frame_nr;
@@ -1585,6 +1831,16 @@
return 0;
}
/**
* @brief Encode one frame through NVENC and queue the resulting packet.
*
* @param frame_nr Monotonic frame index assigned by the video pipeline.
* @param session Active NVENC encoder session.
* @param packets Output queue that receives the encoded packet.
* @param channel_data Platform or protocol state attached to the packet.
* @param frame_timestamp Capture timestamp associated with the encoded frame.
* @return 0 when packets are queued; nonzero when NVENC encoding fails.
*/
int encode_nvenc(int64_t frame_nr, nvenc_encode_session_t &session, safe::mail_raw_t::queue_t<packet_t> &packets, void *channel_data, std::optional<std::chrono::steady_clock::time_point> frame_timestamp) {
auto encoded_frame = session.encode_frame(frame_nr);
if (encoded_frame.data.empty()) {
@@ -1605,6 +1861,16 @@
return 0;
}
/**
* @brief Encode one captured frame and queue packets for transmission.
*
* @param frame_nr Frame nr.
* @param session Active streaming or pairing session for the request.
* @param packets Packets queued or emitted by the stream.
* @param channel_data Channel data.
* @param frame_timestamp Frame timestamp.
* @return 0 when the frame is encoded and queued; nonzero on encoder failure.
*/
int encode(int64_t frame_nr, encode_session_t &session, safe::mail_raw_t::queue_t<packet_t> &packets, void *channel_data, std::optional<std::chrono::steady_clock::time_point> frame_timestamp) {
if (auto avcodec_session = dynamic_cast<avcodec_encode_session_t *>(&session)) {
return encode_avcodec(frame_nr, *avcodec_session, packets, channel_data, frame_timestamp);
@@ -1615,6 +1881,17 @@
return -1;
}
/**
* @brief Create an AVCodec encode session.
*
* @param disp Display being encoded.
* @param encoder Selected encoder.
* @param config Video configuration.
* @param width Encoded frame width.
* @param height Encoded frame height.
* @param encode_device AVCodec encode device.
* @return AVCodec encode session, or nullptr on failure.
*/
std::unique_ptr<avcodec_encode_session_t> make_avcodec_encode_session(
platf::display_t *disp,
const encoder_t &encoder,
@@ -2007,6 +2284,13 @@
return session;
}
/**
* @brief Create NVENC encode session.
*
* @param client_config Client stream configuration negotiated for this session.
* @param encode_device Encode device.
* @return Constructed NVENC encode session object.
*/
std::unique_ptr<nvenc_encode_session_t> make_nvenc_encode_session(const config_t &client_config, std::unique_ptr<platf::nvenc_encode_device_t> encode_device) {
if (!encode_device->init_encoder(client_config, encode_device->colorspace)) {
return nullptr;
@@ -2015,6 +2299,17 @@
return std::make_unique<nvenc_encode_session_t>(std::move(encode_device));
}
/**
* @brief Create encode session.
*
* @param disp Display connection or display handle.
* @param encoder Encoder configuration or encoder instance.
* @param config Configuration values to apply.
* @param width Frame or display width in pixels.
* @param height Frame or display height in pixels.
* @param encode_device Encode device.
* @return Constructed encode session object.
*/
std::unique_ptr<encode_session_t> make_encode_session(platf::display_t *disp, const encoder_t &encoder, const config_t &config, int width, int height, std::unique_ptr<platf::encode_device_t> encode_device) {
if (dynamic_cast<platf::avcodec_encode_device_t *>(encode_device.get())) {
auto avcodec_encode_device = boost::dynamic_pointer_cast<platf::avcodec_encode_device_t>(std::move(encode_device));
@@ -2027,6 +2322,19 @@
return nullptr;
}
/**
* @brief Run one encode loop for a display capture stream.
*
* @param frame_nr Frame counter updated as frames are encoded.
* @param mail Session mail bus.
* @param images Captured image event source.
* @param config Video configuration.
* @param disp Display being encoded.
* @param encode_device Platform encode device.
* @param reinit_event Signal raised while the encoder/display is reinitializing.
* @param encoder Selected encoder.
* @param channel_data Opaque channel data passed to packets.
*/
void encode_run(
int &frame_nr, // Store progress of the frame number
safe::mail_t mail,
@@ -2138,6 +2446,13 @@
}
}
/**
* @brief Create a port object or message.
*
* @param display Display object or identifier associated with the operation.
* @param config Configuration values to apply.
* @return Constructed port object.
*/
input::touch_port_t make_port(platf::display_t *display, const config_t &config) {
float wd = display->width;
float hd = display->height;
@@ -2183,5 +2498,13 @@
};
}
/**
* @brief Create encode device.
*
* @param disp Display connection or display handle.
* @param encoder Encoder configuration or encoder instance.
* @param config Configuration values to apply.
* @return Constructed encode device object.
*/
std::unique_ptr<platf::encode_device_t> make_encode_device(platf::display_t &disp, const encoder_t &encoder, const config_t &config) {
std::unique_ptr<platf::encode_device_t> result;
@@ -2234,5 +2557,14 @@
return result;
}
/**
* @brief Create synced session.
*
* @param disp Display connection or display handle.
* @param encoder Encoder configuration or encoder instance.
* @param img Image or frame object to read from or populate.
* @param ctx Native context object used by the operation or callback.
* @return Constructed synced session object.
*/
std::optional<sync_session_t> make_synced_session(platf::display_t *disp, const encoder_t &encoder, platf::img_t &img, sync_session_ctx_t &ctx) {
sync_session_t encode_session;
@@ -2274,6 +2606,15 @@
return encode_session;
}
/**
* @brief Run synchronized capture and encoding.
*
* @param synced_session_ctxs Active synchronized session contexts.
* @param encode_session_ctx_queue Pending synchronized session context queue.
* @param display_names Cached display names.
* @param display_p Active display index.
* @return Encoder loop result.
*/
encode_e encode_run_sync(
std::vector<std::unique_ptr<sync_session_ctx_t>> &synced_session_ctxs,
encode_session_ctx_queue_t &encode_session_ctx_queue,
@@ -2425,5 +2766,8 @@
return encode_e::ok;
}
/**
* @brief Run synchronous capture and encode work on the capture thread.
*/
void captureThreadSync() {
auto ref = capture_thread_sync.ref();
@@ -2454,6 +2798,13 @@
while (encode_run_sync(synced_session_ctxs, ctx, display_names, display_p) == encode_e::reinit) {}
}
/**
* @brief Capture and encode video using the asynchronous capture thread.
*
* @param mail Session mail bus.
* @param config Video configuration.
* @param channel_data Opaque channel data passed to packets.
*/
void capture_async(
safe::mail_t mail,
config_t &config,
@@ -2538,6 +2889,13 @@
}
}
/**
* @brief Capture and encode video for a streaming session.
*
* @param mail Session mail bus.
* @param config Video configuration.
* @param channel_data Opaque channel data passed to packets.
*/
void capture(
safe::mail_t mail,
config_t config,
@@ -2568,10 +2926,21 @@
}
}
/**
* @brief Enumerates supported validate flag options.
*/
enum validate_flag_e {
VUI_PARAMS = 0x01, ///< VUI parameters
};
/**
* @brief Validate config before it is used.
*
* @param disp Display connection or display handle.
* @param encoder Encoder configuration or encoder instance.
* @param config Configuration values to apply.
* @return 0 when the selected encoder/device accepts the configuration; nonzero otherwise.
*/
int validate_config(std::shared_ptr<platf::display_t> disp, const encoder_t &encoder, const config_t &config) {
auto encode_device = make_encode_device(*disp, encoder, config);
if (!encode_device) {
@@ -2624,6 +2993,9 @@
return flag;
}
/**
* @brief Validate encoder before it is used.
*/
bool validate_encoder(encoder_t &encoder, bool expect_failure) {
const auto output_name {display_device::map_output_name(config::video.output_name)};
std::shared_ptr<platf::display_t> disp;
@@ -3073,7 +3445,13 @@
}
// Linux only declaration
/**
* @brief Callback signature for VA-API AVCodec hardware input initialization.
*/
typedef int (*vaapi_init_avcodec_hardware_input_buffer_fn)(platf::avcodec_encode_device_t *encode_device, AVBufferRef **hw_device_buf);
/**
* @brief Initialize AVCodec hardware input buffers for VA-API.
*/
util::Either<avcodec_buffer_t, int> vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) {
avcodec_buffer_t hw_device_buf;
@@ -3136,5 +3514,8 @@
}
#endif
/**
* @brief Initialize AVCodec hardware input buffers for CUDA.
*/
util::Either<avcodec_buffer_t, int> cuda_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) {
avcodec_buffer_t hw_device_buf;
@@ -3149,5 +3530,8 @@
return hw_device_buf;
}
/**
* @brief Initialize AVCodec hardware input buffers for VideoToolbox.
*/
util::Either<avcodec_buffer_t, int> vt_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) {
avcodec_buffer_t hw_device_buf;
@@ -3165,10 +3549,16 @@
#ifdef _WIN32
}
/**
* @brief No-op lock callback used when FFmpeg requires a D3D11VA lock function.
*/
void do_nothing(void *) {
}
namespace video {
/**
* @brief Create an FFmpeg D3D11VA hardware device from Sunshine's DXGI device.
*/
util::Either<avcodec_buffer_t, int> dxgi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) {
avcodec_buffer_t ctx_buf {av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_D3D11VA)};
auto ctx = (AVD3D11VADeviceContext *) ((AVHWDeviceContext *) ctx_buf->data)->hwctx;
@@ -3196,6 +3586,9 @@
}
#endif
/**
* @brief Start capture async.
*/
int start_capture_async(capture_thread_async_ctx_t &capture_thread_ctx) {
capture_thread_ctx.encoder_p = chosen_encoder;
capture_thread_ctx.reinit_event.reset();
@@ -3213,20 +3606,32 @@
return 0;
}
/**
* @brief Stop capture async processing.
*/
void end_capture_async(capture_thread_async_ctx_t &capture_thread_ctx) {
capture_thread_ctx.capture_ctx_queue->stop();
capture_thread_ctx.capture_thread.join();
}
/**
* @brief Start capture sync.
*/
int start_capture_sync(capture_thread_sync_ctx_t &ctx) {
std::thread {&captureThreadSync}.detach();
return 0;
}
/**
* @brief Stop capture sync processing.
*/
void end_capture_sync(capture_thread_sync_ctx_t &ctx) {
}
/**
* @brief Map base dev type values.
*/
platf::mem_type_e map_base_dev_type(AVHWDeviceType type) {
switch (type) {
case AV_HWDEVICE_TYPE_D3D11VA:
@@ -3250,6 +3655,9 @@
return platf::mem_type_e::unknown;
}
/**
* @brief Map pix fmt values.
*/
platf::pix_fmt_e map_pix_fmt(AVPixelFormat fmt) {
switch (fmt) {
case AV_PIX_FMT_VUYX:
src/video.h +321 −67
@@ -19,57 +19,112 @@
namespace video {
/* Encoding configuration requested by remote client */
/**
* @brief Encoding configuration requested by a remote client.
*/
struct config_t {
int width; ///< Video width in pixels.
int height; ///< Video height in pixels.
int framerate; ///< Requested framerate used in the per-frame bitrate budget.
int framerateX100; ///< Optional NTSC-style framerate value, e.g. 59.94 as 5994.
int bitrate; ///< Video bitrate in kilobits for the requested framerate.
int slicesPerFrame; ///< Number of slices per frame.
int numRefFrames; ///< Maximum number of reference frames.
int encoderCscMode; ///< Requested color range and SDR colorspace; HDR always uses BT.2020 and ST2084.
int videoFormat; ///< Video codec format: 0 = H.264, 1 = HEVC, 2 = AV1.
int width; // Video width in pixels
int height; // Video height in pixels
int framerate; // Requested framerate, used in individual frame bitrate budget calculation
int framerateX100; // Optional field for streaming at NTSC or similar rates e.g. 59.94 = 5994
int bitrate; // Video bitrate in kilobits (1000 bits) for requested framerate
int slicesPerFrame; // Number of slices per frame
int numRefFrames; // Max number of reference frames
/* Requested color range and SDR encoding colorspace, HDR encoding colorspace is always BT.2020+ST2084
int dynamicRange; ///< Encoding color depth: 0 = 8-bit, 1 = 10-bit.
int chromaSamplingType; ///< Chroma sampling type: 0 = 4:2:0, 1 = 4:4:4.
int enableIntraRefresh; ///< Intra refresh setting: 0 = disabled, 1 = enabled.
Color range (encoderCscMode & 0x1) : 0 - limited, 1 - full
SDR encoding colorspace (encoderCscMode >> 1) : 0 - BT.601, 1 - BT.709, 2 - BT.2020 */
int encoderCscMode;
int videoFormat; // 0 - H.264, 1 - HEVC, 2 - AV1
/* Encoding color depth (bit depth): 0 - 8-bit, 1 - 10-bit
HDR encoding activates when color depth is higher than 8-bit and the display which is being captured is operating in HDR mode */
int dynamicRange;
int chromaSamplingType; // 0 - 4:2:0, 1 - 4:4:4
int enableIntraRefresh; // 0 - disabled, 1 - enabled
};
/**
* @brief Map an FFmpeg hardware device type to Sunshine's memory type.
*
* @param type FFmpeg hardware device type reported by the encoder backend.
* @return Sunshine memory type used by the capture and encode pipeline.
*/
platf::mem_type_e map_base_dev_type(AVHWDeviceType type);
/**
* @brief Map an FFmpeg pixel format to Sunshine's pixel format enum.
*
* @param fmt FFmpeg pixel format to convert.
* @return Sunshine pixel format used by display and encoder backends.
*/
platf::pix_fmt_e map_pix_fmt(AVPixelFormat fmt);
/**
* @brief Release an FFmpeg codec context.
*
* @param ctx Codec context allocated by FFmpeg.
*/
void free_ctx(AVCodecContext *ctx);
/**
* @brief Release an FFmpeg frame allocated by the capture or conversion backend.
*
* @param frame Video or graphics frame being processed.
*/
void free_frame(AVFrame *frame);
/**
* @brief Release a backend buffer allocated for capture or conversion.
*
* @param ref FFmpeg buffer reference to unref and free.
*/
void free_buffer(AVBufferRef *ref);
/**
* @brief Owning pointer for an FFmpeg codec context.
*/
using avcodec_ctx_t = util::safe_ptr<AVCodecContext, free_ctx>;
/**
* @brief Owning pointer for an FFmpeg frame.
*/
using avcodec_frame_t = util::safe_ptr<AVFrame, free_frame>;
/**
* @brief Owning pointer for an FFmpeg buffer reference.
*/
using avcodec_buffer_t = util::safe_ptr<AVBufferRef, free_buffer>;
/**
* @brief Owning pointer for an FFmpeg software-scaling context.
*/
using sws_t = util::safe_ptr<SwsContext, sws_freeContext>;
/**
* @brief Shared event that transports captured images between capture and encode threads.
*/
using img_event_t = std::shared_ptr<safe::event_t<std::shared_ptr<platf::img_t>>>;
/**
* @brief Pixel formats supported by one encoder backend.
*/
struct encoder_platform_formats_t {
virtual ~encoder_platform_formats_t() = default;
platf::mem_type_e dev_type;
platf::pix_fmt_e pix_fmt_8bit;
platf::pix_fmt_e pix_fmt_10bit;
platf::pix_fmt_e pix_fmt_yuv444_8bit;
platf::pix_fmt_e pix_fmt_yuv444_10bit;
platf::mem_type_e dev_type; ///< Platform memory type required by this encoder.
platf::pix_fmt_e pix_fmt_8bit; ///< 8-bit 4:2:0 input format accepted by this encoder.
platf::pix_fmt_e pix_fmt_10bit; ///< 10-bit 4:2:0 input format accepted by this encoder.
platf::pix_fmt_e pix_fmt_yuv444_8bit; ///< 8-bit 4:4:4 input format accepted by this encoder.
platf::pix_fmt_e pix_fmt_yuv444_10bit; ///< 10-bit 4:4:4 input format accepted by this encoder.
};
/**
* @brief AVCodec-specific pixel formats supported by a platform.
*/
struct encoder_platform_formats_avcodec: encoder_platform_formats_t {
/**
* @brief Callback that prepares an FFmpeg hardware input buffer for the encode device.
*/
using init_buffer_function_t = std::function<util::Either<avcodec_buffer_t, int>(platf::avcodec_encode_device_t *)>;
/**
* @brief Construct AVCodec platform format mappings.
*
* @param avcodec_base_dev_type Base AVCodec hardware device type.
* @param avcodec_derived_dev_type Derived AVCodec hardware device type.
* @param avcodec_dev_pix_fmt AVCodec device pixel format.
* @param avcodec_pix_fmt_8bit AVCodec 8-bit pixel format.
* @param avcodec_pix_fmt_10bit AVCodec 10-bit pixel format.
* @param avcodec_pix_fmt_yuv444_8bit AVCodec 8-bit YUV444 pixel format.
* @param avcodec_pix_fmt_yuv444_10bit AVCodec 10-bit YUV444 pixel format.
* @param init_avcodec_hardware_input_buffer_function Hardware input buffer initialization callback.
*/
encoder_platform_formats_avcodec(
const AVHWDeviceType &avcodec_base_dev_type,
const AVHWDeviceType &avcodec_derived_dev_type,
@@ -95,18 +150,30 @@
pix_fmt_yuv444_10bit = map_pix_fmt(avcodec_pix_fmt_yuv444_10bit);
}
AVHWDeviceType avcodec_base_dev_type;
AVHWDeviceType avcodec_derived_dev_type;
AVPixelFormat avcodec_dev_pix_fmt;
AVPixelFormat avcodec_pix_fmt_8bit;
AVHWDeviceType avcodec_base_dev_type; ///< FFmpeg device type used to create the primary hardware context.
AVHWDeviceType avcodec_derived_dev_type; ///< FFmpeg device type derived from the primary hardware context.
AVPixelFormat avcodec_dev_pix_fmt; ///< FFmpeg hardware-device pixel format for frames handed to the encoder.
AVPixelFormat avcodec_pix_fmt_8bit; ///< FFmpeg 8-bit 4:2:0 software pixel format for this encoder.
AVPixelFormat avcodec_pix_fmt_10bit; ///< FFmpeg 10-bit 4:2:0 software pixel format for this encoder.
AVPixelFormat avcodec_pix_fmt_yuv444_8bit; ///< FFmpeg 8-bit 4:4:4 software pixel format for this encoder.
AVPixelFormat avcodec_pix_fmt_yuv444_10bit; ///< FFmpeg 10-bit 4:4:4 software pixel format for this encoder.
AVPixelFormat avcodec_pix_fmt_10bit;
AVPixelFormat avcodec_pix_fmt_yuv444_8bit;
AVPixelFormat avcodec_pix_fmt_yuv444_10bit;
init_buffer_function_t init_avcodec_hardware_input_buffer;
init_buffer_function_t init_avcodec_hardware_input_buffer; ///< Backend hook that allocates or imports hardware frames for FFmpeg.
};
/**
* @brief NVENC-specific pixel formats supported by a platform.
*/
struct encoder_platform_formats_nvenc: encoder_platform_formats_t {
/**
* @brief Construct NVENC platform format mappings.
*
* @param dev_type Platform memory type.
* @param pix_fmt_8bit Platform 8-bit pixel format.
* @param pix_fmt_10bit Platform 10-bit pixel format.
* @param pix_fmt_yuv444_8bit Platform 8-bit YUV444 pixel format.
* @param pix_fmt_yuv444_10bit Platform 10-bit YUV444 pixel format.
*/
encoder_platform_formats_nvenc(
const platf::mem_type_e &dev_type,
const platf::pix_fmt_e &pix_fmt_8bit,
@@ -122,9 +189,15 @@
}
};
/**
* @brief Encoder name and feature flags advertised by Sunshine.
*/
struct encoder_t {
std::string_view name;
std::string_view name; ///< Encoder name used in logs, configuration, and capability probes.
/**
* @brief Capability flags that describe which stream modes an encoder supports.
*/
enum flag_e {
PASSED, ///< Indicates the encoder is supported.
REF_FRAMES_RESTRICT, ///< Set maximum reference frames.
@@ -135,10 +208,18 @@
MAX_FLAGS ///< Maximum number of flags.
};
/**
* @brief Convert an encoder capability flag to its diagnostic string.
*
* @param flag Feature flag to name.
* @return Stable string used in logs and probe output.
*/
static std::string_view from_flag(flag_e flag) {
#define _CONVERT(x) \
case flag_e::x: \
return std::string_view(#x)
#ifndef DOXYGEN
#define _CONVERT(x) \
case flag_e::x: \
return std::string_view(#x)
#endif
switch (flag) {
_CONVERT(PASSED);
_CONVERT(REF_FRAMES_RESTRICT);
@@ -153,45 +234,78 @@
return {"unknown"};
}
/**
* @brief Runtime encoder option exposed to configuration parsing.
*/
struct option_t {
KITTY_DEFAULT_CONSTR_MOVE(option_t)
/**
* @brief Copy an encoder option descriptor.
*/
option_t(const option_t &) = default;
std::string name;
std::variant<int, int *, std::optional<int> *, std::function<int()>, std::string, std::string *, std::function<const std::string(const config_t &)>> value;
std::string name; ///< Encoder command-line option name.
std::variant<int, int *, std::optional<int> *, std::function<int()>, std::string, std::string *, std::function<const std::string(const config_t &)>> value; ///< Literal, pointer, or callback that supplies the option value.
/**
* @brief Store a named encoder option and its value source.
*
* @param name Human-readable name to assign.
* @param value Literal value, pointer, or callback used to resolve the option.
*/
option_t(std::string &&name, decltype(value) &&value):
name {std::move(name)},
value {std::move(value)} {
}
};
const std::unique_ptr<const encoder_platform_formats_t> platform_formats; ///< Platform-specific memory and pixel formats accepted by the encoder.
const std::unique_ptr<const encoder_platform_formats_t> platform_formats;
/**
* @brief Codec capabilities for AV1, HEVC, or H.264.
*/
struct codec_t {
std::vector<option_t> common_options;
std::vector<option_t> sdr_options;
std::vector<option_t> hdr_options;
std::vector<option_t> sdr444_options;
std::vector<option_t> hdr444_options;
std::vector<option_t> fallback_options;
std::vector<option_t> common_options; ///< Options applied to every encode mode for this codec.
std::vector<option_t> sdr_options; ///< Options applied to SDR 4:2:0 streams.
std::vector<option_t> hdr_options; ///< Options applied to HDR 4:2:0 streams.
std::vector<option_t> sdr444_options; ///< Options applied to SDR 4:4:4 streams.
std::vector<option_t> hdr444_options; ///< Options applied to HDR 4:4:4 streams.
std::vector<option_t> fallback_options; ///< Options used when the preferred mode cannot be selected.
std::string name;
std::bitset<MAX_FLAGS> capabilities;
std::string name; ///< Codec name passed to the encoder backend.
std::bitset<MAX_FLAGS> capabilities; ///< Capability flags supported by this codec on the encoder.
/**
* @brief Test whether a codec capability is enabled.
*
* @param flag Feature flag to test or modify.
* @return True when the requested video flag is set.
*/
bool operator[](flag_e flag) const {
return capabilities[(std::size_t) flag];
}
/**
* @brief Return a mutable reference to a codec capability flag.
*
* @param flag Feature flag to test or modify.
* @return Mutable bitset reference for the requested capability.
*/
std::bitset<MAX_FLAGS>::reference operator[](flag_e flag) {
return capabilities[(std::size_t) flag];
}
};
codec_t av1;
codec_t hevc;
codec_t h264;
codec_t av1; ///< AV1 codec capability and option set.
codec_t hevc; ///< HEVC codec capability and option set.
codec_t h264; ///< H.264 codec capability and option set.
/**
* @brief Select the codec descriptor requested by a stream configuration.
*
* @param config Configuration values to apply.
* @return Codec descriptor for H.264, HEVC, or AV1.
*/
const codec_t &codec_from_config(const config_t &config) const {
switch (config.videoFormat) {
default:
@@ -206,17 +320,38 @@
}
}
uint32_t flags;
uint32_t flags; ///< Encoder flags advertised to clients through GameStream capability responses.
};
/**
* @brief Encoder session state shared by capture and encoding threads.
*/
struct encode_session_t {
virtual ~encode_session_t() = default;
/**
* @brief Convert a captured frame into the encoder's required input representation.
*
* @param img Captured image supplied by the platform display backend.
* @return Zero when the frame was converted or imported successfully.
*/
virtual int convert(platf::img_t &img) = 0;
/**
* @brief Mark the frame as a request for an IDR frame.
*/
virtual void request_idr_frame() = 0;
/**
* @brief Mark the frame as a request for a normal inter frame.
*/
virtual void request_normal_frame() = 0;
/**
* @brief Mark the frame range whose references must be invalidated.
*
* @param first_frame First frame.
* @param last_frame Last frame.
*/
virtual void invalidate_ref_frames(int64_t first_frame, int64_t last_frame) = 0;
};
@@ -242,35 +377,70 @@
extern encoder_t videotoolbox;
#endif
/**
* @brief Encoded packet wrapper used by the streaming pipeline.
*/
struct packet_raw_t {
virtual ~packet_raw_t() = default;
/**
* @brief Report whether this packet starts an IDR frame.
*
* @return True when this frame is an IDR frame.
*/
virtual bool is_idr() = 0;
/**
* @brief Return the frame index associated with this encoded packet.
*
* @return Monotonic frame index assigned to this frame.
*/
virtual int64_t frame_index() = 0;
/**
* @brief Return writable access to the encoded packet bytes.
*
* @return Pointer to the first byte of encoded frame data.
*/
virtual uint8_t *data() = 0;
/**
* @brief Return the encoded payload length.
*
* @return Size of the encoded frame payload in bytes.
*/
virtual size_t data_size() = 0;
/**
* @brief Packet byte-range replacement descriptor.
*/
struct replace_t {
std::string_view old;
std::string_view _new;
std::string_view old; ///< Byte sequence to find in the encoded packet.
std::string_view _new; ///< Byte sequence that replaces `old` before transmission.
KITTY_DEFAULT_CONSTR_MOVE(replace_t)
/**
* @brief Store an encoded-byte replacement rule.
*
* @param old Byte sequence to replace in encoded packets.
* @param _new Replacement byte sequence inserted into encoded packets.
*/
replace_t(std::string_view old, std::string_view _new) noexcept:
old {std::move(old)},
_new {std::move(_new)} {
}
};
std::vector<replace_t> *replacements = nullptr;
void *channel_data = nullptr;
bool after_ref_frame_invalidation = false;
std::optional<std::chrono::steady_clock::time_point> frame_timestamp;
std::vector<replace_t> *replacements = nullptr; ///< Optional encoded-byte substitutions applied before packetization.
void *channel_data = nullptr; ///< Platform or protocol state carried with this packet.
bool after_ref_frame_invalidation = false; ///< Whether the frame follows reference-frame invalidation.
std::optional<std::chrono::steady_clock::time_point> frame_timestamp; ///< Capture timestamp associated with the frame.
};
/**
* @brief AVCodec packet wrapper with codec-specific metadata.
*/
struct packet_raw_avcodec: packet_raw_t {
packet_raw_avcodec() {
av_packet = av_packet_alloc();
@@ -280,67 +450,137 @@
av_packet_free(&this->av_packet);
}
/**
* @brief Report whether the FFmpeg packet is marked as a key frame.
*
* @return True when this frame is an IDR frame.
*/
bool is_idr() override {
return av_packet->flags & AV_PKT_FLAG_KEY;
}
/**
* @brief Return the FFmpeg packet PTS used as Sunshine's frame index.
*
* @return Monotonic frame index assigned to this frame.
*/
int64_t frame_index() override {
return av_packet->pts;
}
/**
* @brief Return writable access to the FFmpeg packet payload.
*
* @return Pointer to the first encoded byte in the FFmpeg packet.
*/
uint8_t *data() override {
return av_packet->data;
}
/**
* @brief Return the FFmpeg packet payload length.
*
* @return Size of the encoded frame payload in bytes.
*/
size_t data_size() override {
return av_packet->size;
}
AVPacket *av_packet;
AVPacket *av_packet; ///< FFmpeg packet that owns encoded data and frame metadata.
};
/**
* @brief Generic encoded packet bytes and metadata.
*/
struct packet_raw_generic: packet_raw_t {
/**
* @brief Wrap generic encoded frame bytes in Sunshine packet metadata.
*
* @param frame_data Encoded frame bytes produced by a non-FFmpeg encoder.
* @param frame_index Monotonic frame index assigned to the encoded frame.
* @param idr Whether the packet begins an IDR frame.
*/
packet_raw_generic(std::vector<uint8_t> &&frame_data, int64_t frame_index, bool idr):
frame_data {std::move(frame_data)},
index {frame_index},
idr {idr} {
}
/**
* @brief Report whether the generic packet starts an IDR frame.
*
* @return True when this frame is an IDR frame.
*/
bool is_idr() override {
return idr;
}
/**
* @brief Return the stored frame index for this generic packet.
*
* @return Monotonic frame index assigned to this frame.
*/
int64_t frame_index() override {
return index;
}
/**
* @brief Return writable access to the generic packet payload.
*
* @return Pointer to the first encoded byte in `frame_data`.
*/
uint8_t *data() override {
return frame_data.data();
}
/**
* @brief Return the generic packet payload length.
*
* @return Size of the encoded frame payload in bytes.
*/
size_t data_size() override {
return frame_data.size();
}
std::vector<uint8_t> frame_data;
int64_t index;
bool idr;
std::vector<uint8_t> frame_data; ///< Encoded frame bytes owned by this packet.
int64_t index; ///< Monotonic frame index assigned to this packet.
bool idr; ///< Whether the packet belongs to an IDR frame.
};
/**
* @brief Owning pointer to an encoded packet abstraction.
*/
using packet_t = std::unique_ptr<packet_raw_t>;
/**
* @brief Raw HDR metadata fields parsed from the display backend.
*/
struct hdr_info_raw_t {
/**
* @brief Initialize HDR metadata with only the enabled state.
*
* @param enabled Whether the feature should be enabled.
*/
explicit hdr_info_raw_t(bool enabled):
enabled {enabled},
metadata {} {};
/**
* @brief Initialize HDR metadata with display metadata from the backend.
*
* @param enabled Whether the feature should be enabled.
* @param metadata Output structure populated with HDR metadata.
*/
explicit hdr_info_raw_t(bool enabled, const SS_HDR_METADATA &metadata):
enabled {enabled},
metadata {metadata} {};
bool enabled;
SS_HDR_METADATA metadata;
bool enabled; ///< Whether HDR mode should be enabled.
SS_HDR_METADATA metadata; ///< Display HDR metadata forwarded to the encoder and client.
};
/**
* @brief Owning pointer to optional HDR metadata for a captured display.
*/
using hdr_info_t = std::unique_ptr<hdr_info_raw_t>;
extern int active_hevc_mode;
@@ -354,6 +594,13 @@
void *channel_data
);
/**
* @brief Validate encoder before it is used.
*
* @param encoder Encoder configuration or encoder instance.
* @param expect_failure Expect failure.
* @return True when encoder validation matches `expect_failure`.
*/
bool validate_encoder(encoder_t &encoder, bool expect_failure);
/**
@@ -363,5 +610,6 @@
* at runtime due to all sorts of things from driver updates to eGPUs.
*
* @warning This is only safe to call when there is no client actively streaming.
* @return 0 when a usable encoder is selected; nonzero when probing fails.
*/
int probe_encoders();
@@ -371,6 +619,12 @@
// reduce 29.97 to 2997/100 but this would be slightly wrong. We also include
// support for 23.976 film in case someone wants to stream a film at the perfect
// framerate.
/**
* @brief Convert a framerate stored as hundredths of Hz to an FFmpeg rational.
*
* @param framerateX100 Framerate multiplied by 100, such as 5994 for 59.94 Hz.
* @return Rational frame rate preserving NTSC fractional rates when applicable.
*/
inline AVRational framerateX100_to_rational(const int framerateX100) {
if (framerateX100 % 2997 == 0) {
// Multiples of NTSC 29.97 e.g. 59.94, 119.88
src/video_colorspace.cpp +9 −0
@@ -15,9 +15,15 @@
namespace video {
/**
* @brief Check whether a Sunshine colorspace represents HDR video.
*/
bool colorspace_is_hdr(const sunshine_colorspace_t &colorspace) {
return colorspace.colorspace == colorspace_e::bt2020;
}
/**
* @brief Derive Sunshine colorspace metadata from client stream configuration.
*/
sunshine_colorspace_t colorspace_from_client_config(const config_t &config, bool hdr_display) {
sunshine_colorspace_t colorspace;
@@ -76,5 +82,8 @@
return colorspace;
}
/**
* @brief Convert Sunshine colorspace metadata to FFmpeg AVCodec fields.
*/
avcodec_colorspace_t avcodec_colorspace_from_sunshine_colorspace(const sunshine_colorspace_t &sunshine_colorspace) {
avcodec_colorspace_t avcodec_colorspace;
src/video_colorspace.h +44 −13
@@ -10,6 +10,9 @@
namespace video {
/**
* @brief Enumerates supported colorspace options.
*/
enum class colorspace_e {
rec601, ///< Rec. 601
rec709, ///< Rec. 709
@@ -17,35 +20,63 @@
bt2020, ///< Rec. 2020 HDR
};
/**
* @brief Sunshine colorimetry values derived from stream configuration.
*/
struct sunshine_colorspace_t {
colorspace_e colorspace; ///< Colorspace.
bool full_range; ///< Whether the video range is full-range instead of limited.
colorspace_e colorspace;
bool full_range;
unsigned bit_depth;
unsigned bit_depth; ///< Bit depth.
};
/**
* @brief Check whether a Sunshine colorspace represents HDR video.
*
* @param colorspace Colorimetry information used for conversion or encoding.
* @return True when the colorspace values describe the same metadata.
*/
bool colorspace_is_hdr(const sunshine_colorspace_t &colorspace);
// Declared in video.h
struct config_t;
/**
* @brief Derive Sunshine colorspace metadata from the negotiated client config.
*
* @param config Configuration values to apply.
* @param hdr_display HDR display.
* @return Colorspace, range, transfer, and HDR flags used for encoding.
*/
sunshine_colorspace_t colorspace_from_client_config(const config_t &config, bool hdr_display);
/**
* @brief FFmpeg colorimetry values used by AVCodec encoders.
*/
struct avcodec_colorspace_t {
AVColorPrimaries primaries; ///< FFmpeg color primaries selected for the encoded stream.
AVColorPrimaries primaries;
AVColorTransferCharacteristic transfer_function;
AVColorSpace matrix;
AVColorRange range;
int software_format;
AVColorTransferCharacteristic transfer_function; ///< FFmpeg transfer function selected for the encoded stream.
AVColorSpace matrix; ///< FFmpeg YUV matrix coefficients selected for the encoded stream.
AVColorRange range; ///< FFmpeg full-range or limited-range flag selected for the encoded stream.
int software_format; ///< FFmpeg software pixel format used when hardware frames are not supplied.
};
/**
* @brief Convert Sunshine colorspace metadata to FFmpeg AVCodec fields.
*
* @param sunshine_colorspace Sunshine colorspace.
* @return FFmpeg color range, primaries, transfer, and software format values.
*/
avcodec_colorspace_t avcodec_colorspace_from_sunshine_colorspace(const sunshine_colorspace_t &sunshine_colorspace);
/**
* @brief Pair of Sunshine and AVCodec colorimetry descriptions.
*/
struct alignas(16) color_t {
float color_vec_y[4];
float color_vec_u[4];
float color_vec_v[4];
float range_y[2];
float range_uv[2];
float color_vec_y[4]; ///< Color vec y.
float color_vec_u[4]; ///< Color vec u.
float color_vec_v[4]; ///< Color vec v.
float range_y[2]; ///< Range y.
float range_uv[2]; ///< Range uv.
};
/**
third-party/lizardbyte-common +0 −0
uv.lock +207 −0
@@ -91,6 +91,73 @@
]
[[package]]
name = "babel"
version = "2.18.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" },
]
[[package]]
name = "clang-format"
version = "21.1.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/29/ed/019e682e9f8a5a5abf4258b293453092ef9524b540ada591ccfdff1246df/clang_format-21.1.8.tar.gz", hash = "sha256:99369fe76526ba6be6d7a8093fee6cd266bbf5ce72597a0d79a4ce9a625b28cf", size = 11509, upload-time = "2025-12-16T20:34:33.065Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/1d/4b1d85acb99a2ee3bad0b20cc3e82c58fbfb39bf4dd5856ac1f2c4f36e65/clang_format-21.1.8-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:f447091c346027a09728a0a96128fe058419fe06cf200ccc3dc98bcd4399e351", size = 1465604, upload-time = "2025-12-16T20:34:03.955Z" },
{ url = "https://files.pythonhosted.org/packages/84/38/a61466227a8a6bf22c583f7bd810e75c9a356cfc63c6a712a201103a4ad3/clang_format-21.1.8-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:9a78fabba6b382866819b42a7b220d235a3baf6128a40fb7bd590c037f69879b", size = 1459014, upload-time = "2025-12-16T20:34:05.929Z" },
{ url = "https://files.pythonhosted.org/packages/ab/5d/6720c895c5cfc01831fac55ecf849f10d03d6222700909af14f6047c933b/clang_format-21.1.8-py2.py3-none-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2a044953efab5f7d0261f3a76aabc01358592faac1b73af38d8c83db6e91a69", size = 1725482, upload-time = "2025-12-16T20:34:07.525Z" },
{ url = "https://files.pythonhosted.org/packages/54/0d/074b940884ced1b5c93e2e7fc8a941673519a938e7f31aeade321e3ab094/clang_format-21.1.8-py2.py3-none-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:143d2dffa71058d05ac5ad71321682e23d638bce22c8f2e3d01ed457d841a5f3", size = 1856680, upload-time = "2025-12-16T20:34:09.987Z" },
{ url = "https://files.pythonhosted.org/packages/34/d5/46e22ae8c4385a7836db7403ced63ea8056c971cbb806bf0c6f0d2133acc/clang_format-21.1.8-py2.py3-none-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6a4b61a743dad5afc5e60be0c5c8f162f6cf27fff9eed56ec0bf65c2bcbd8a8d", size = 2031282, upload-time = "2025-12-16T20:34:11.565Z" },
{ url = "https://files.pythonhosted.org/packages/e0/2a/cad75567c312d945cc26aaee7e1b7135703fed9c7e7ad15c427bf6ad7a84/clang_format-21.1.8-py2.py3-none-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:94de66c5eeca1270825348687d384750d91f1d8d12216b2df0aea892859be33a", size = 2047763, upload-time = "2025-12-16T20:34:13.175Z" },
{ url = "https://files.pythonhosted.org/packages/a8/c2/403e7371b1ab0f6175d708ecc93cf18085ea9f903e8fe7d17c43df698f06/clang_format-21.1.8-py2.py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d12b864b596b80810cdd7f97556c485dc09cfe2952503958535f01359e025fbb", size = 1804755, upload-time = "2025-12-16T20:34:14.813Z" },
{ url = "https://files.pythonhosted.org/packages/76/33/9e7db5fc4abefb25022d266957f666bfc263ae3e51abdd2c997d2880ceee/clang_format-21.1.8-py2.py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:b81f1e909f5e7ef862a7818dc22b302a3ff407f3534e3395aa7ba26746cc890e", size = 1643208, upload-time = "2025-12-16T20:34:16.898Z" },
{ url = "https://files.pythonhosted.org/packages/aa/9f/43f4e384cb8943418f94943f682835f375f15b9cc64c526d73a7360f2b2f/clang_format-21.1.8-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e10a5dca18a04997ad55c082bc0759edc7e337b24a2373ded109634fde12662a", size = 2701696, upload-time = "2025-12-16T20:34:19.043Z" },
{ url = "https://files.pythonhosted.org/packages/5d/6a/378187d72a465cebd3dbf7cd455e33b4931daa33e815b7865e6e65591a72/clang_format-21.1.8-py2.py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:10f7d7004d70b5e03fd3f764fc19fae89cd38d68b3482162cb09cac47c12c7b0", size = 2481266, upload-time = "2025-12-16T20:34:20.336Z" },
{ url = "https://files.pythonhosted.org/packages/99/eb/2332edb1846d6832932191a590203a53ec69f585e718b51796335a1dcb6e/clang_format-21.1.8-py2.py3-none-musllinux_1_2_i686.whl", hash = "sha256:303d5fd53090422119136b1a458e98db429dedd5db0add50e70f885f8c95d82a", size = 2954000, upload-time = "2025-12-16T20:34:21.739Z" },
{ url = "https://files.pythonhosted.org/packages/7b/b5/eed5c95521b77d3cde024c3a23c7a80c46e78aa5b5a81c5cf935e7267588/clang_format-21.1.8-py2.py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:6b9e0b45cfaf4a18336a6db4666dd6a6aae840e38b038e6d568a65f43ade007c", size = 3076720, upload-time = "2025-12-16T20:34:23.139Z" },
{ url = "https://files.pythonhosted.org/packages/14/55/7527fbe31423a1bb53653c2a2b20aa34a73ec33b99199bceaec0f5907295/clang_format-21.1.8-py2.py3-none-musllinux_1_2_s390x.whl", hash = "sha256:4dd0fee9eaee9915ba7fa08e60ee9ddfba1f754d10d71164482d9eb387c431f0", size = 3160892, upload-time = "2025-12-16T20:34:24.618Z" },
{ url = "https://files.pythonhosted.org/packages/53/61/1623382bc78ef139c196473a5c76b840a071c9002cb5cd2694ac50327db9/clang_format-21.1.8-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9731aecf954651004f184347d0c589d4e91a39ccda21262ee17c60f80a4408b2", size = 2812487, upload-time = "2025-12-16T20:34:26.505Z" },
{ url = "https://files.pythonhosted.org/packages/53/e1/74f2000d44e4904472e5685afb98d19566c515026214733f02bbdc73d6d8/clang_format-21.1.8-py2.py3-none-win32.whl", hash = "sha256:2f5883ca83f718d8c2272e98b3cbda422fec520824b5a1e335c631dc6d812da7", size = 1271310, upload-time = "2025-12-16T20:34:28.179Z" },
{ url = "https://files.pythonhosted.org/packages/69/7a/c49c8af9135c6a6dfb9cd103328ba7b6551643dce71b57e58ea940884015/clang_format-21.1.8-py2.py3-none-win_amd64.whl", hash = "sha256:a7606da55e31ebf5b63dd75800392e6cca7c595a74100c2cebcda2d742130732", size = 1426467, upload-time = "2025-12-16T20:34:29.805Z" },
{ url = "https://files.pythonhosted.org/packages/ad/d3/8790b539afb6cb0d4474705d3750e78a1f0847ab6d3ef1b00dd1ae52f364/clang_format-21.1.8-py2.py3-none-win_arm64.whl", hash = "sha256:1aa10b3f647268361d08bf4f17ce70964b8d9c04d5539e7d8acbebd14dc4a49c", size = 1327254, upload-time = "2025-12-16T20:34:31.579Z" },
]
[[package]]
name = "cmakelang"
version = "0.6.13"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/74/c0/75d4806cf21dcb4198e9fba02f4d2fa61c8db919b7db788862d9cd5f4433/cmakelang-0.6.13.tar.gz", hash = "sha256:03982e87b00654d024d73ef972d9d9bb0e5726cdb6b8a424a15661fb6278e67f", size = 123111, upload-time = "2020-08-19T17:15:25.44Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/a8/c4676cac062d133c6b909d7def80a3194162597968953a3291b309878721/cmakelang-0.6.13-py3-none-any.whl", hash = "sha256:764b9467195c7c36453d60a829f30229720d26c7dffd41cb516b99bd9c7daf4e", size = 159803, upload-time = "2020-08-19T17:15:23.981Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "colorlog"
version = "6.10.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" },
]
[[package]]
name = "flatpak-node-generator"
version = "0.1.0"
source = { directory = "packaging/linux/flatpak/deps/flatpak-builder-tools/node" }
@@ -170,6 +237,21 @@
]
[[package]]
name = "gcovr"
version = "8.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorlog" },
{ name = "jinja2" },
{ name = "lxml" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/07/37/b4a87dff166dc0a5002e9d03fcb6ca8eeff048247b011b67f047e31122c9/gcovr-8.6.tar.gz", hash = "sha256:b2e7042abca9321cadbab8a06eb34d19f801b831557b28cdc30a029313de8b9e", size = 199997, upload-time = "2026-01-13T20:04:30.019Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/92/be/f722c843e7875c7cf92cf0e0c1604cddda55a70278c768c6327a78fdba79/gcovr-8.6-py3-none-any.whl", hash = "sha256:dbf9d87c38042752ad6f530aa8210427e22b526611bb7b7bfed0e81977d1f1ef", size = 254618, upload-time = "2026-01-13T20:04:28.15Z" },
]
[[package]]
name = "glad2"
source = { directory = "third-party/glad" }
dependencies = [
@@ -205,6 +287,109 @@
]
[[package]]
name = "lizardbyte-common"
version = "0.0.0"
source = { directory = "third-party/lizardbyte-common" }
[package.optional-dependencies]
c = [
{ name = "babel" },
{ name = "clang-format" },
{ name = "cmakelang" },
{ name = "gcovr" },
]
[package.metadata]
requires-dist = [
{ name = "babel", marker = "extra == 'locale'", specifier = "==2.18.0" },
{ name = "clang-format", marker = "extra == 'lint-c'", specifier = "==21.1.8" },
{ name = "cmakelang", marker = "extra == 'lint-c'", specifier = "==0.6.13" },
{ name = "flake8", marker = "extra == 'lint-python'", specifier = "==7.3.0" },
{ name = "gcovr", marker = "extra == 'test-c'", specifier = "==8.6" },
{ name = "lizardbyte-common", extras = ["c"], marker = "extra == 'dev'" },
{ name = "lizardbyte-common", extras = ["lint-c"], marker = "extra == 'c'" },
{ name = "lizardbyte-common", extras = ["lint-python"], marker = "extra == 'dev'" },
{ name = "lizardbyte-common", extras = ["locale"], marker = "extra == 'c'" },
{ name = "lizardbyte-common", extras = ["test-c"], marker = "extra == 'c'" },
{ name = "lizardbyte-common", extras = ["test-python"], marker = "extra == 'dev'" },
{ name = "pytest", marker = "extra == 'test-python'", specifier = "==9.0.3" },
{ name = "pytest-cov", marker = "extra == 'test-python'", specifier = "==7.1.0" },
]
provides-extras = ["dev", "c", "lint-c", "lint-python", "locale", "test-c", "test-python"]
[package.metadata.requires-dev]
c = [
{ name = "babel", specifier = "==2.18.0" },
{ name = "clang-format", specifier = "==21.1.8" },
{ name = "cmakelang", specifier = "==0.6.13" },
{ name = "gcovr", specifier = "==8.6" },
]
dev = [
{ name = "babel", specifier = "==2.18.0" },
{ name = "clang-format", specifier = "==21.1.8" },
{ name = "cmakelang", specifier = "==0.6.13" },
{ name = "flake8", specifier = "==7.3.0" },
{ name = "gcovr", specifier = "==8.6" },
{ name = "pytest", specifier = "==9.0.3" },
{ name = "pytest-cov", specifier = "==7.1.0" },
]
lint-c = [
{ name = "clang-format", specifier = "==21.1.8" },
{ name = "cmakelang", specifier = "==0.6.13" },
]
lint-python = [{ name = "flake8", specifier = "==7.3.0" }]
locale = [{ name = "babel", specifier = "==2.18.0" }]
test-c = [{ name = "gcovr", specifier = "==8.6" }]
test-python = [
{ name = "pytest", specifier = "==9.0.3" },
{ name = "pytest-cov", specifier = "==7.1.0" },
]
[[package]]
name = "lxml"
version = "6.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" },
{ url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" },
{ url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" },
{ url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" },
{ url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" },
{ url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" },
{ url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" },
{ url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" },
{ url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" },
{ url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" },
{ url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" },
{ url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" },
{ url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" },
{ url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" },
{ url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" },
{ url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" },
{ url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" },
{ url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" },
{ url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" },
{ url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" },
{ url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" },
{ url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" },
{ url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" },
{ url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" },
{ url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" },
{ url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" },
{ url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" },
{ url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" },
{ url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" },
{ url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" },
{ url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" },
{ url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" },
{ url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" },
{ url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" },
{ url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" },
{ url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" },
]
[[package]]
name = "markupsafe"
version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
@@ -332,6 +517,15 @@
]
[[package]]
name = "pygments"
version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
@@ -379,9 +573,21 @@
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "sunshine"
version = "0.0.0"
source = { editable = "." }
dependencies = [
{ name = "lizardbyte-common", extra = ["c"] },
]
[package.dev-dependencies]
flatpak = [
@@ -394,6 +600,7 @@
]
[package.metadata]
requires-dist = [{ name = "lizardbyte-common", extras = ["c"], directory = "third-party/lizardbyte-common" }]
[package.metadata.requires-dev]
flatpak = [