Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmake/compile_definitions/windows.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ set(PLATFORM_TARGET_FILES
"${CMAKE_SOURCE_DIR}/src/platform/windows/display_vram.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/display_ram.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/display_wgc.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/display_remoteidd.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/remoteidd/SharedFrame.h"
"${CMAKE_SOURCE_DIR}/src/platform/windows/remoteidd/FrameConsumer.h"
"${CMAKE_SOURCE_DIR}/src/platform/windows/remoteidd/FrameConsumer.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/audio.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/utf_utils.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/utf_utils.h"
Expand Down
6 changes: 6 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2422,6 +2422,12 @@ consent prompt for directory ACLs, so the Web UI shows setup steps for correctin
@note{Applies to Windows only.}
@attention{This capture method is not compatible with the Sunshine service.}</td>
</tr>
<tr>
<td>remoteidd</td>
<td>Read the frames published by the OpenStreamMS RemoteIdd display driver of the current
session, without Desktop Duplication or WGC. Never chosen automatically.
@note{Applies to Windows only. Requires a hardware encoder and SDR.}</td>
</tr>
</table>

### fp16_sdr_gamma_encoded
Expand Down
5 changes: 4 additions & 1 deletion src/confighttp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1822,7 +1822,8 @@ namespace confighttp {
* @brief List client pairing requests that are waiting for PIN approval.
*
* Moonlight WebRTC TVs waiting to pair are listed beside Moonlight clients and are approved
* or cancelled through the same endpoints.
* or cancelled through the same endpoints. Each request's `kind` tells them apart:
* `moonlight` for a Moonlight client, `webrtc` for a Moonlight WebRTC TV.
*
* @api_examples{/api/pin|:| GET|:| null}
*/
Expand All @@ -1840,6 +1841,7 @@ namespace confighttp {
{"id", pairing.id},
{"name", pairing.name},
{"address", pairing.address},
{"kind", "moonlight"},
});
}
// Moonlight WebRTC TVs show their PIN like Moonlight does and are approved in the same form.
Expand All @@ -1848,6 +1850,7 @@ namespace confighttp {
{"id", pairing.id},
{"name", pairing.name},
{"address", pairing.address},
{"kind", "webrtc"},
});
}
send_response(response, output_tree);
Expand Down
46 changes: 46 additions & 0 deletions src/platform/windows/display.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
#include "src/utility.h"
#include "src/video.h"

namespace osms::ridd {
class FrameConsumer;
}

namespace platf::dxgi {
/**
* @brief Diagnostic names indexed by DXGI_FORMAT.
Expand Down Expand Up @@ -858,4 +862,46 @@ namespace platf::dxgi {
*/
capture_e release_snapshot() override;
};

/**
* Display backend that reads the frames published by the OpenStreamMS RemoteIdd driver.
*
* The driver copies every swapchain buffer into a shared texture (NT handle + keyed
* mutex) and announces it through a named slot; this backend copies that texture into
* the capture image without going through Desktop Duplication or WGC. Only selected
* with capture = remoteidd.
*/
class display_ridd_vram_t: public display_vram_t {
public:
display_ridd_vram_t();
~display_ridd_vram_t() override;

/**
* @brief Initialize the display and open the RemoteIdd frame slot.
*
* @param config Configuration values to apply.
* @param display_name Display name.
* @return 0 on success; nonzero when the driver is not publishing frames.
*/
int init(const ::video::config_t &config, const std::string &display_name);
/**
* @brief Copy the most recent RemoteIdd 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 Unused: the cursor is composed into the IddCx swapchain.
* @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;

private:
std::unique_ptr<osms::ridd::FrameConsumer> consumer; ///< Reader of the RemoteIdd frame slot.
};
} // namespace platf::dxgi
75 changes: 73 additions & 2 deletions src/platform/windows/display_base.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
* @brief Definitions for the Windows display base code.
*/
// standard includes
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <thread>
#include <utility>
Expand All @@ -20,6 +22,7 @@
#include <MinHook.h>

// local includes
#include "frame_rate_limiter.h"
#include "utf_utils.h"

// Including Boost.Process before Windows.h prevents the definition of NTSTATUS, so we must define it ourselves.
Expand Down Expand Up @@ -251,6 +254,23 @@ namespace platf::dxgi {
std::optional<std::chrono::steady_clock::time_point> frame_pacing_group_start;
uint32_t frame_pacing_group_frames = 0;

// When the display composes much faster than the client rate (e.g. an RDP session whose
// composition interval is set well above the client rate), a fixed client-rate grid loses
// frames: updates land on display ticks and a slot can come back empty. Send frames as they
// arrive instead, capped at the client rate.
std::optional<frame_rate_limiter_t> arrival_limiter;
std::shared_ptr<img_t> held_img;
if (display_refresh_rate.Numerator > 0 && display_refresh_rate.Denominator > 0) {
const double display_rate = (double) display_refresh_rate.Numerator / display_refresh_rate.Denominator;
const double client_rate = (double) client_frame_rate_adjusted.Numerator / client_frame_rate_adjusted.Denominator;
if (display_rate >= client_rate * 1.5) {
const std::chrono::nanoseconds period = std::chrono::nanoseconds(1s) * client_frame_rate_adjusted.Denominator / client_frame_rate_adjusted.Numerator;
const std::chrono::nanoseconds tick = std::chrono::nanoseconds(1s) * display_refresh_rate.Denominator / display_refresh_rate.Numerator;
arrival_limiter.emplace(period, std::min(tick, period / 2));
BOOST_LOG(info) << "Capturing frames as they arrive, capped at " << client_rate << "fps (display " << display_rate << "Hz)";
}
}

// Keep the display awake during capture. If the display goes to sleep during
// capture, best case is that capture stops until it powers back on. However,
// worst case it will trigger us to reinit DD, waking the display back up in
Expand All @@ -273,8 +293,45 @@ namespace platf::dxgi {
platf::capture_e status = capture_e::ok;
std::shared_ptr<img_t> img_out;

if (arrival_limiter) {
// Sleep until the earliest time the next frame can be accepted instead of spinning
if (const auto next_time = arrival_limiter->next_accept_time()) {
const auto now = std::chrono::steady_clock::now();
if (*next_time > now) {
timer->sleep_for(*next_time - now);
}
}

// If an early frame is already held, probe with 0ms to see if a newer one arrived;
// otherwise wait up to 200ms for the next frame.
const auto wait = held_img ? 0ms : 200ms;

status = snapshot(pull_free_image_cb, img_out, wait, *cursor);

if (status == capture_e::ok && img_out) {
const auto timestamp = img_out->frame_timestamp.value_or(std::chrono::steady_clock::now());
if (!arrival_limiter->accept(timestamp)) {
// Too early for the client rate: keep it, it is sent at its slot unless a newer frame replaces it.
held_img = std::move(img_out);
status = release_snapshot();
if (status != platf::capture_e::ok) {
return status;
}
continue;
}
held_img.reset();
} else if (status == capture_e::timeout && held_img) {
arrival_limiter->accept(std::chrono::steady_clock::now());
img_out = std::move(held_img);
status = capture_e::ok;
} else if (status == capture_e::timeout) {
// Nothing is updating the display: let the encoding thread take the device lock (see below).
std::this_thread::sleep_for(10ms);
}
}

// Try to continue frame pacing group, snapshot() is called with zero timeout after waiting for client frame interval
if (frame_pacing_group_start) {
else if (frame_pacing_group_start) {
const uint32_t seconds = (uint64_t) frame_pacing_group_frames * client_frame_rate_adjusted.Denominator / client_frame_rate_adjusted.Numerator;
const uint32_t remainder = (uint64_t) frame_pacing_group_frames * client_frame_rate_adjusted.Denominator % client_frame_rate_adjusted.Numerator;
const auto sleep_target = *frame_pacing_group_start +
Expand Down Expand Up @@ -304,7 +361,7 @@ namespace platf::dxgi {
}

// Start new frame pacing group if necessary, snapshot() is called with non-zero timeout
if (status == capture_e::timeout || (status == capture_e::ok && !frame_pacing_group_start)) {
if (!arrival_limiter && (status == capture_e::timeout || (status == capture_e::ok && !frame_pacing_group_start))) {
status = snapshot(pull_free_image_cb, img_out, 200ms, *cursor);

if (status == capture_e::ok && img_out) {
Expand Down Expand Up @@ -1027,6 +1084,20 @@ namespace platf {
* @param hwdevice_type enables possible use of hardware encoder
*/
std::shared_ptr<display_t> display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) {
// OpenStreamMS RemoteIdd: only on explicit request, never part of auto-detection.
if (config::video.capture == "remoteidd") {
if (hwdevice_type == mem_type_e::dxgi) {
auto disp = std::make_shared<dxgi::display_ridd_vram_t>();

if (!disp->init(config, display_name)) {
return disp;
}
} else {
BOOST_LOG(error) << "RemoteIdd capture requires a hardware encoder"sv;
}
return nullptr;
}

if (config::video.capture == "ddx" || config::video.capture.empty()) {
if (hwdevice_type == mem_type_e::dxgi) {
auto disp = std::make_shared<dxgi::display_ddup_vram_t>();
Expand Down
84 changes: 84 additions & 0 deletions src/platform/windows/display_remoteidd.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* @file src/platform/windows/display_remoteidd.cpp
* @brief Definitions for the OpenStreamMS RemoteIdd capture backend.
* @note snapshot() lives in display_vram.cpp, next to the other VRAM backends that share
* its image helpers.
*/
// standard includes
#include <cstdlib>
#include <iterator>
#include <string>

// local includes
#include "display.h"
#include "misc.h"
#include "remoteidd/FrameConsumer.h"
#include "src/logging.h"

namespace platf::dxgi {
using namespace std::literals;

namespace {
/**
* @brief Read a process environment variable (Win32 view, not the CRT copy).
*/
std::wstring env(const wchar_t *name) {
wchar_t buf[64];
DWORD n = GetEnvironmentVariableW(name, buf, static_cast<DWORD>(std::size(buf)));
return (n > 0 && n < std::size(buf)) ? std::wstring(buf, n) : std::wstring();
}

/**
* @brief Namespace prefix of the RemoteIdd named objects.
*
* The driver publishes in Global\. OSMS_RIDD_NS lets tests use Local\ without privileges.
*/
std::wstring ridd_namespace() {
if (auto ns = env(L"OSMS_RIDD_NS"); !ns.empty()) {
return ns;
}
return REMOTEIDD_NS;
}

/**
* @brief Terminal Services session whose slot this process reads.
*
* Sunshine runs inside the seat session; OSMS_RIDD_SESSION overrides it for tests.
*/
uint32_t ridd_session() {
if (auto s = env(L"OSMS_RIDD_SESSION"); !s.empty()) {
return static_cast<uint32_t>(std::wcstoul(s.c_str(), nullptr, 10));
}
DWORD session = 0;
ProcessIdToSessionId(GetCurrentProcessId(), &session);
return session;
}
} // namespace

// Out of line: FrameConsumer is incomplete in display.h.
display_ridd_vram_t::display_ridd_vram_t() = default;
display_ridd_vram_t::~display_ridd_vram_t() = default;

int display_ridd_vram_t::init(const ::video::config_t &config, const std::string &display_name) {
if (config.dynamicRange) {
BOOST_LOG(error) << "RemoteIdd capture does not support HDR yet"sv;
return -1;
}
if (display_base_t::init(config, display_name)) {
return -1;
}

// IddCx 1.4 swapchains are always 8-bit BGRA.
capture_format = DXGI_FORMAT_B8G8R8A8_UNORM;

auto ns = ridd_namespace();
auto session = ridd_session();
consumer = std::make_unique<osms::ridd::FrameConsumer>(Microsoft::WRL::ComPtr<ID3D11Device>(device.get()), session, ns.c_str());
if (!consumer->Open()) {
BOOST_LOG(error) << "RemoteIdd: no frame slot for session "sv << session << "; is the driver running?"sv;
return -1;
}
BOOST_LOG(info) << "RemoteIdd: reading frames for session "sv << session;
return 0;
}
} // namespace platf::dxgi
69 changes: 69 additions & 0 deletions src/platform/windows/display_vram.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ extern "C" {
// local includes
#include "display.h"
#include "misc.h"
#include "remoteidd/FrameConsumer.h"
#include "src/config.h"
#include "src/logging.h"
#include "src/nvenc/nvenc_config.h"
Expand Down Expand Up @@ -1914,6 +1915,74 @@ namespace platf::dxgi {
return dup.release_frame();
}

/**
* Copy the most recent frame published by the RemoteIdd driver into a new snapshot texture.
*/
capture_e display_ridd_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*/) {
switch (consumer->WaitFrame(static_cast<DWORD>(timeout.count()))) {
case osms::ridd::FrameConsumer::Wait::Frame:
break;
case osms::ridd::FrameConsumer::Wait::Timeout:
return capture_e::timeout;
default:
return capture_e::reinit;
}

ID3D11Texture2D *raw = nullptr;
RemoteIddFrameInfo frame;
if (!consumer->Acquire(static_cast<DWORD>(timeout.count()), &raw, &frame)) {
// Texture recreated or still held by the driver: the next frame will bring it.
return capture_e::timeout;
}
texture2d_t src {raw};

// From here on the driver cannot write the texture: copy it and let go quickly.
auto release = util::fail_guard([&]() {
consumer->Release();
});

if (frame.Width != width_before_rotation || frame.Height != height_before_rotation) {
BOOST_LOG(info) << "Capture size changed ["sv << width << 'x' << height << " -> "sv << frame.Width << 'x' << frame.Height << ']';
return capture_e::reinit;
}
if (static_cast<DXGI_FORMAT>(frame.DxgiFormat) != capture_format) {
BOOST_LOG(info) << "Capture format changed ["sv << dxgi_format_to_string(capture_format) << " -> "sv << dxgi_format_to_string(static_cast<DXGI_FORMAT>(frame.DxgiFormat)) << ']';
return capture_e::reinit;
}

std::shared_ptr<platf::img_t> img;
if (!pull_free_image_cb(img)) {
return capture_e::interrupted;
}

auto d3d_img = std::static_pointer_cast<img_d3d_t>(img);
d3d_img->blank = false;
if (complete_img(d3d_img.get(), false) != 0) {
return capture_e::capture_error;
}
{
texture_lock_helper lock_helper(d3d_img->capture_mutex.get());
if (!lock_helper.lock()) {
BOOST_LOG(error) << "Failed to lock capture texture";
return capture_e::capture_error;
}
device_ctx->CopyResource(d3d_img->capture_texture.get(), src.get());
}

auto frame_timestamp = std::chrono::steady_clock::now();
if (frame.QpcPresent) {
frame_timestamp -= qpc_time_difference(qpc_counter(), static_cast<int64_t>(frame.QpcPresent));
}
img_out = img;
img_out->frame_timestamp = frame_timestamp;
return capture_e::ok;
}

capture_e display_ridd_vram_t::release_snapshot() {
// The shared texture is released right after the copy in snapshot().
return capture_e::ok;
}

int display_wgc_vram_t::init(const ::video::config_t &config, const std::string &display_name) {
if (display_base_t::init(config, display_name) || dup.init(this, config)) {
return -1;
Expand Down
Loading
Loading