mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-22 20:43:22 +08:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d381cbaa9b | ||
|
|
5aedb09c55 | ||
|
|
fe1178e26f | ||
|
|
e0cd761bdc | ||
|
|
2000654792 | ||
|
|
19800ef350 | ||
|
|
d10b65cc19 | ||
|
|
3a0179968c | ||
|
|
55f389b834 | ||
|
|
8a58166c59 | ||
|
|
aa2ee8ce7f | ||
|
|
b150d46501 | ||
|
|
010064d5b7 | ||
|
|
db3b26efe7 | ||
|
|
cd1b53ee65 | ||
|
|
b031a3953e | ||
|
|
3f537ae63c |
@@ -15,6 +15,7 @@
|
||||
#ifdef _WIN32
|
||||
#include <cstdio>
|
||||
|
||||
#include "platform/windows/gui/slint_backend.h"
|
||||
#include "service_host.h"
|
||||
#endif
|
||||
|
||||
@@ -175,6 +176,10 @@ int HandleServiceCliCommand(const std::string& command) {
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
#ifdef _WIN32
|
||||
if (argc == 2 &&
|
||||
std::strcmp(argv[1], crossdesk::kSlintRendererProbeArgument) == 0) {
|
||||
return crossdesk::RunSlintRendererProbe();
|
||||
}
|
||||
if (argc > 1 && IsServiceCliCommand(argv[1])) {
|
||||
return HandleServiceCliCommand(argv[1]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
#include "task_queue_lock_free.h"
|
||||
|
||||
#include <exception>
|
||||
#include <new>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
TaskQueueLockFree::TaskQueueLockFree()
|
||||
: producer_(task_queue_), worker_([this] { WorkerThread(); }) {}
|
||||
|
||||
TaskQueueLockFree::~TaskQueueLockFree() { Stop(); }
|
||||
|
||||
std::future<void> TaskQueueLockFree::PostTask(std::function<void()> task) {
|
||||
TaskItem item;
|
||||
item.run = std::move(task);
|
||||
auto result = item.completion.get_future();
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
if (stopping_) {
|
||||
throw std::runtime_error("Cannot post to a stopped task queue");
|
||||
}
|
||||
|
||||
// Count before publishing: the worker may dequeue as soon as enqueue returns.
|
||||
++pending_tasks_;
|
||||
if (!task_queue_.enqueue(producer_, std::move(item))) {
|
||||
--pending_tasks_;
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
}
|
||||
wake_.notify_one();
|
||||
return result;
|
||||
}
|
||||
|
||||
void TaskQueueLockFree::Stop() {
|
||||
// Concurrent Stop callers all wait for the same worker shutdown to finish.
|
||||
std::call_once(stop_once_, [this] {
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
stopping_ = true;
|
||||
}
|
||||
wake_.notify_all();
|
||||
worker_.join();
|
||||
});
|
||||
}
|
||||
|
||||
void TaskQueueLockFree::WorkerThread() {
|
||||
while (true) {
|
||||
TaskItem item;
|
||||
while (task_queue_.try_dequeue(item)) {
|
||||
--pending_tasks_;
|
||||
|
||||
std::exception_ptr error;
|
||||
try {
|
||||
item.run();
|
||||
} catch (...) {
|
||||
error = std::current_exception();
|
||||
}
|
||||
|
||||
// Release captured resources before marking the task complete.
|
||||
item.run = nullptr;
|
||||
if (error) {
|
||||
item.completion.set_exception(error);
|
||||
} else {
|
||||
item.completion.set_value();
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_lock lock(mutex_);
|
||||
wake_.wait(lock, [this] { return stopping_ || pending_tasks_ > 0; });
|
||||
// A stop request must not discard tasks submitted while the worker was idle.
|
||||
if (stopping_ && pending_tasks_ == 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _TASK_QUEUE_LOCK_FREE_H_
|
||||
#define _TASK_QUEUE_LOCK_FREE_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include <concurrentqueue.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// A single worker executes tasks in submission order. The concurrent queue
|
||||
// stores tasks; a mutex coordinates submissions, worker wakeups and shutdown.
|
||||
class TaskQueueLockFree {
|
||||
public:
|
||||
TaskQueueLockFree();
|
||||
~TaskQueueLockFree();
|
||||
|
||||
// Returns completion or the task's exception. Throws if the queue is stopped.
|
||||
std::future<void> PostTask(std::function<void()> task);
|
||||
|
||||
// Rejects new tasks, finishes accepted tasks and joins the worker.
|
||||
// May be called repeatedly, but never from the queue's own tasks.
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
struct TaskItem {
|
||||
std::function<void()> run;
|
||||
std::promise<void> completion;
|
||||
};
|
||||
|
||||
void WorkerThread();
|
||||
|
||||
moodycamel::ConcurrentQueue<TaskItem> task_queue_;
|
||||
// All submissions share one producer so different caller threads keep FIFO.
|
||||
moodycamel::ProducerToken producer_;
|
||||
std::atomic<std::size_t> pending_tasks_{0};
|
||||
std::mutex mutex_;
|
||||
std::condition_variable wake_;
|
||||
bool stopping_ = false;
|
||||
std::once_flag stop_once_;
|
||||
std::thread worker_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -1,5 +1,11 @@
|
||||
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
#define CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _APPLICATION_STATE_H_
|
||||
#define _APPLICATION_STATE_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
@@ -192,4 +198,4 @@ struct ApplicationState : MainWindowState,
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif // CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
#endif
|
||||
@@ -37,6 +37,7 @@
|
||||
#if _WIN32
|
||||
#include <windows.h>
|
||||
|
||||
#include "platform/windows/gui/slint_backend.h"
|
||||
#include "platform/windows/gui/tray/win_tray.h"
|
||||
#elif defined(__APPLE__)
|
||||
#include "platform/macos/gui/tray/mac_tray.h"
|
||||
@@ -47,6 +48,7 @@
|
||||
#include <X11/Xlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "platform/linux/gui/cuda_graphics.h"
|
||||
#include "platform/linux/gui/tray/linux_tray.h"
|
||||
#endif
|
||||
#include "rd_log.h"
|
||||
@@ -992,6 +994,15 @@ int GuiApplication::Run() {
|
||||
if (!InitializeSDL()) {
|
||||
return -1;
|
||||
}
|
||||
#if _WIN32
|
||||
const auto backend = ConfigureWindowsSlintBackend();
|
||||
if (!backend.success) {
|
||||
LOG_ERROR("{}", backend.diagnostic);
|
||||
SDL_Quit();
|
||||
return -1;
|
||||
}
|
||||
LOG_INFO("Slint backend: {}; {}", backend.backend, backend.diagnostic);
|
||||
#endif
|
||||
InitializeModules();
|
||||
InitializeUi();
|
||||
|
||||
@@ -1020,6 +1031,7 @@ void GuiApplication::InitializeSettings() {
|
||||
|
||||
bool GuiApplication::InitializeSDL() {
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
ConfigureLinuxCudaGraphics(config_center_->IsHardwareVideoCodec());
|
||||
// Standard Wayland top-level windows cannot choose an absolute screen
|
||||
// position. Use the session's XWayland compatibility server for the GUI so
|
||||
// auxiliary windows such as the controlled-side status window can be
|
||||
@@ -1728,7 +1740,7 @@ void GuiApplication::BindServerCallbacks() {
|
||||
});
|
||||
server->on_disconnect_controller([this] {
|
||||
if (peer_ && !selected_server_remote_id_.empty()) {
|
||||
LeaveConnection(peer_, selected_server_remote_id_.c_str());
|
||||
CloseServerController(selected_server_remote_id_);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1738,6 +1750,7 @@ void GuiApplication::Tick() {
|
||||
slint::quit_event_loop();
|
||||
return;
|
||||
}
|
||||
HandleSessionCleanup();
|
||||
HandlePasswordChangeResult();
|
||||
HandleCredentialRecovery();
|
||||
if (!peer_) {
|
||||
@@ -1998,8 +2011,7 @@ void GuiApplication::HandlePasswordChangeResult() {
|
||||
}
|
||||
show_offline_warning_window_ = true;
|
||||
if (uncertain && peer_) {
|
||||
LeaveConnection(peer_, client_id_);
|
||||
DestroyPeer(&peer_);
|
||||
CloseConnectionPeer();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -2012,8 +2024,7 @@ void GuiApplication::HandlePasswordChangeResult() {
|
||||
|
||||
LOG_INFO("Password changed successfully for [{}]", client_id_);
|
||||
if (peer_) {
|
||||
LeaveConnection(peer_, client_id_);
|
||||
DestroyPeer(&peer_);
|
||||
CloseConnectionPeer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2037,7 +2048,7 @@ void GuiApplication::HandleCredentialRecovery() {
|
||||
if (retry_active) {
|
||||
LOG_INFO("Pending credential was not accepted; retrying active credential");
|
||||
if (peer_) {
|
||||
DestroyPeer(&peer_);
|
||||
CloseConnectionPeer();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -3073,9 +3084,6 @@ void GuiApplication::CloseStreamTab(const std::string& remote_id) {
|
||||
}
|
||||
keyboard_.ReleaseRemotePressedKeys(remote_id, "stream_tab_closed");
|
||||
CloseRemoteSession(props);
|
||||
WaitForThumbnailSaveTasks();
|
||||
reload_recent_connections_ = true;
|
||||
recent_connection_image_save_time_ = 0;
|
||||
ResetRemoteSessionResources(props);
|
||||
{
|
||||
std::unique_lock lock(remote_sessions_mutex_);
|
||||
@@ -3280,7 +3288,7 @@ void GuiApplication::Cleanup() {
|
||||
devices_.DestroyDevices();
|
||||
devices_.DestroyFactories();
|
||||
CloseAllRemoteSessions();
|
||||
WaitForThumbnailSaveTasks();
|
||||
WaitForSessionCleanup();
|
||||
devices_.DestroyAudioOutput();
|
||||
if (ui_->stream) {
|
||||
#if defined(__APPLE__)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _GUI_APPLICATION_H_
|
||||
#define _GUI_APPLICATION_H_
|
||||
|
||||
@@ -94,4 +100,4 @@ private:
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -1,4 +1,11 @@
|
||||
#pragma once
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SERVER_WINDOW_STATE_H_
|
||||
#define _SERVER_WINDOW_STATE_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
@@ -24,5 +31,6 @@ inline int ReconcileSelectedController(const std::vector<std::string>& ids,
|
||||
}
|
||||
return static_cast<int>(std::distance(ids.begin(), selected));
|
||||
}
|
||||
} // namespace crossdesk::server_window_state
|
||||
|
||||
} // namespace crossdesk::server_window_state
|
||||
#endif
|
||||
@@ -1,4 +1,11 @@
|
||||
#pragma once
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOW_GEOMETRY_H_
|
||||
#define _WINDOW_GEOMETRY_H_
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -38,3 +45,5 @@ constexpr PhysicalPosition BottomRightPosition(const PhysicalRect& bounds,
|
||||
}
|
||||
|
||||
} // namespace crossdesk::window_geometry
|
||||
|
||||
#endif
|
||||
@@ -74,7 +74,8 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
|
||||
const int init_ret = screen_capturer_->Init(
|
||||
fps, [this, fps](unsigned char *data, int size, int width, int height,
|
||||
const char *display_name) {
|
||||
const char *display_name,
|
||||
const MiniRtcNativeVideoFrame *native_frame) {
|
||||
const auto now_time = std::chrono::steady_clock::now();
|
||||
if (!ShouldSendCapturedFrame(now_time, fps)) {
|
||||
return;
|
||||
@@ -136,12 +137,13 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
}
|
||||
}
|
||||
|
||||
XVideoFrame frame{};
|
||||
MiniRtcVideoFrame frame{};
|
||||
frame.data = reinterpret_cast<const char *>(data);
|
||||
frame.size = size;
|
||||
frame.width = width;
|
||||
frame.height = height;
|
||||
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
|
||||
frame.native_frame = native_frame;
|
||||
for (const std::string &remote_id : connected_remote_ids) {
|
||||
SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
|
||||
remote_id.data(), remote_id.size());
|
||||
@@ -204,8 +206,11 @@ int SessionDeviceManager::StartSpeakerCapturer() {
|
||||
static_cast<SpeakerCapturer *>(speaker_capturer_factory_->Create());
|
||||
const int init_ret = speaker_capturer_->Init(
|
||||
[this](unsigned char *data, size_t size, const char *audio_name) {
|
||||
SendAudioFrame(owner_.peer_, reinterpret_cast<const char *>(data),
|
||||
size, owner_.audio_label_.c_str());
|
||||
MiniRtcAudioFrame frame{};
|
||||
frame.data = reinterpret_cast<const char *>(data);
|
||||
frame.size = size;
|
||||
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
|
||||
SendAudioFrame(owner_.peer_, &frame, owner_.audio_label_.c_str());
|
||||
});
|
||||
|
||||
if (init_ret != 0) {
|
||||
|
||||
@@ -20,6 +20,16 @@ namespace crossdesk {
|
||||
|
||||
FileTransferManager::FileTransferManager(GuiRuntime &owner) : owner_(owner) {}
|
||||
|
||||
FileTransferManager::~FileTransferManager() {
|
||||
std::vector<std::future<void>> workers;
|
||||
{
|
||||
std::lock_guard lock(workers_mutex_);
|
||||
stopping_ = true;
|
||||
workers.swap(workers_);
|
||||
}
|
||||
for (auto& worker : workers) worker.wait();
|
||||
}
|
||||
|
||||
FileTransferManager::FileTransferState &FileTransferManager::global_state() {
|
||||
return global_state_;
|
||||
}
|
||||
@@ -92,8 +102,16 @@ void FileTransferManager::Start(
|
||||
std::shared_ptr<RemoteSession> props,
|
||||
const std::filesystem::path &file_path, const std::string &file_label,
|
||||
const std::string &remote_id) {
|
||||
if (props && props->closing_) return;
|
||||
const bool is_global = !props;
|
||||
PeerPtr *peer = is_global ? owner_.peer_ : props->peer_;
|
||||
PeerPtr* peer = nullptr;
|
||||
std::shared_ptr<PeerEventHandler> peer_events;
|
||||
{
|
||||
std::shared_lock lock(owner_.remote_sessions_mutex_);
|
||||
if (props && props->closing_) return;
|
||||
peer = is_global ? owner_.peer_ : props->peer_;
|
||||
peer_events = is_global ? owner_.peer_events_ : props->peer_events_;
|
||||
}
|
||||
if (!peer) {
|
||||
LOG_ERROR("StartFileTransfer: invalid peer");
|
||||
return;
|
||||
@@ -108,7 +126,14 @@ void FileTransferManager::Start(
|
||||
}
|
||||
|
||||
const auto props_weak = std::weak_ptr<RemoteSession>(props);
|
||||
std::thread([this, peer, file_path, file_label, props_weak, remote_id,
|
||||
std::lock_guard workers_lock(workers_mutex_);
|
||||
if (stopping_) return;
|
||||
// Reap finished senders without blocking the UI.
|
||||
std::erase_if(workers_, [](auto& worker) {
|
||||
return worker.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
|
||||
});
|
||||
workers_.push_back(std::async(std::launch::async,
|
||||
[this, peer, peer_events, file_path, file_label, props_weak, remote_id,
|
||||
is_global]() {
|
||||
auto props_locked = props_weak.lock();
|
||||
FileTransferState *state = nullptr;
|
||||
@@ -166,7 +191,11 @@ void FileTransferManager::Start(
|
||||
|
||||
const int ret = sender.SendFile(
|
||||
file_path, file_path.filename().string(),
|
||||
[peer, file_label, remote_id](const char *buffer, size_t size) {
|
||||
[peer, peer_events, file_label, remote_id, props_locked](const char *buffer, size_t size) {
|
||||
// Close drains each in-flight send before detaching the peer.
|
||||
if (props_locked && props_locked->closing_) return -1;
|
||||
auto callback = peer_events->EnterCallback();
|
||||
if (!callback) return -1;
|
||||
if (remote_id.empty()) {
|
||||
return SendReliableDataFrame(peer, buffer, size,
|
||||
file_label.c_str());
|
||||
@@ -200,12 +229,13 @@ void FileTransferManager::Start(
|
||||
}
|
||||
LOG_ERROR("FileSender::SendFile failed for [{}], ret={}",
|
||||
file_path.string().c_str(), ret);
|
||||
ProcessQueue(props_locked);
|
||||
}).detach();
|
||||
if (peer_events->IsActive()) ProcessQueue(props_locked);
|
||||
}));
|
||||
}
|
||||
|
||||
void FileTransferManager::ProcessQueue(
|
||||
std::shared_ptr<RemoteSession> props) {
|
||||
if (props && props->closing_) return;
|
||||
FileTransferState &state = state_for(props);
|
||||
if (state.file_sending_.load()) {
|
||||
return;
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
@@ -21,6 +24,7 @@ public:
|
||||
using RemoteSession = gui_detail::RemoteSession;
|
||||
|
||||
explicit FileTransferManager(GuiRuntime &owner);
|
||||
~FileTransferManager();
|
||||
|
||||
FileTransferState &global_state();
|
||||
FileTransferState &
|
||||
@@ -40,6 +44,9 @@ private:
|
||||
void ProcessQueue(std::shared_ptr<RemoteSession> props);
|
||||
void Unregister(uint32_t file_id, bool per_peer);
|
||||
|
||||
std::mutex workers_mutex_;
|
||||
std::vector<std::future<void>> workers_;
|
||||
bool stopping_ = false;
|
||||
GuiRuntime &owner_;
|
||||
FileTransferState global_state_;
|
||||
std::unordered_map<uint32_t, std::weak_ptr<RemoteSession>>
|
||||
|
||||
@@ -150,6 +150,7 @@ void KeyboardController::SendHeartbeat(bool force) {
|
||||
const std::string target_id = owner_.controlled_remote_id_.empty()
|
||||
? owner_.focused_remote_id_
|
||||
: owner_.controlled_remote_id_;
|
||||
std::shared_lock sessions_lock(owner_.remote_sessions_mutex_);
|
||||
const auto props_it = owner_.remote_sessions_.find(target_id);
|
||||
if (target_id.empty() || props_it == owner_.remote_sessions_.end() ||
|
||||
props_it->second->connection_status_.load() !=
|
||||
@@ -189,6 +190,7 @@ int KeyboardController::SendKeyCommand(int key_code, bool is_down,
|
||||
const std::string target_id = owner_.controlled_remote_id_.empty()
|
||||
? owner_.focused_remote_id_
|
||||
: owner_.controlled_remote_id_;
|
||||
std::shared_lock sessions_lock(owner_.remote_sessions_mutex_);
|
||||
const auto props_it = owner_.remote_sessions_.find(target_id);
|
||||
if (!target_id.empty() && props_it != owner_.remote_sessions_.end() &&
|
||||
props_it->second->connection_status_.load() ==
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
#ifndef CROSSDESK_GUI_RENDER_H_
|
||||
#define CROSSDESK_GUI_RENDER_H_
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _RENDER_H_
|
||||
#define _RENDER_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
@@ -25,4 +31,4 @@ private:
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_RENDER_H_
|
||||
#endif
|
||||
@@ -1,5 +1,11 @@
|
||||
#ifndef CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||
#define CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SLINT_VIDEO_PRESENTER_H_
|
||||
#define _SLINT_VIDEO_PRESENTER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -72,4 +78,4 @@ private:
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||
#endif
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <display_stream_id.h>
|
||||
@@ -220,6 +221,11 @@ int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
|
||||
}
|
||||
|
||||
void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
||||
if (!props || props->closing_.exchange(true)) return;
|
||||
if (props->peer_events_) props->peer_events_->Deactivate();
|
||||
props->connection_established_ = false;
|
||||
props->streaming_ = false;
|
||||
props->connection_status_.store(ConnectionStatus::Closed);
|
||||
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
|
||||
int video_width = 0;
|
||||
int video_height = 0;
|
||||
@@ -245,85 +251,160 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
||||
}
|
||||
}
|
||||
|
||||
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
|
||||
video_height > 0) {
|
||||
std::vector<unsigned char> buffer_copy(*frame_snapshot);
|
||||
std::string remote_id = props->remote_id_;
|
||||
std::string remote_host_name = props->remote_host_name_;
|
||||
std::string password =
|
||||
props->remember_password_ ? props->remote_password_ : "";
|
||||
|
||||
std::thread save_thread([buffer_copy, video_width, video_height, remote_id,
|
||||
remote_host_name, password,
|
||||
thumbnail = thumbnail_]() {
|
||||
thumbnail->SaveToThumbnail((char*)buffer_copy.data(), video_width,
|
||||
video_height, remote_id, remote_host_name,
|
||||
password);
|
||||
});
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(thumbnail_save_threads_mutex_);
|
||||
thumbnail_save_threads_.emplace_back(std::move(save_thread));
|
||||
}
|
||||
}
|
||||
|
||||
if (native_renderer) {
|
||||
native_renderer->DiscardStream(props->remote_id_);
|
||||
video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
if (props->peer_) {
|
||||
LOG_INFO("[{}] Leave connection [{}]", props->local_id_, props->remote_id_);
|
||||
LeaveConnection(props->peer_, props->remote_id_.c_str());
|
||||
LOG_INFO("Destroy peer [{}]", props->local_id_);
|
||||
DestroyPeer(&props->peer_);
|
||||
PeerPtr* peer = nullptr;
|
||||
{
|
||||
// Audio/clipboard senders hold a shared map lock while using this pointer.
|
||||
std::unique_lock lock(remote_sessions_mutex_);
|
||||
peer = std::exchange(props->peer_, nullptr);
|
||||
}
|
||||
const auto queued_at = std::chrono::steady_clock::now();
|
||||
session_cleanup_tasks_[props->remote_id_] = session_cleanup_queue_.PostTask(
|
||||
[props, peer, frame_snapshot, video_width, video_height,
|
||||
thumbnail = thumbnail_, queued_at]() mutable {
|
||||
if (peer) {
|
||||
LOG_INFO("[{}] Background leave connection [{}]", props->local_id_,
|
||||
props->remote_id_);
|
||||
LeaveConnection(peer, props->remote_id_.c_str());
|
||||
DestroyPeer(&peer);
|
||||
}
|
||||
if (thumbnail && frame_snapshot && !frame_snapshot->empty() &&
|
||||
video_width > 0 && video_height > 0) {
|
||||
thumbnail->SaveToThumbnail(
|
||||
reinterpret_cast<char*>(frame_snapshot->data()), video_width,
|
||||
video_height, props->remote_id_, props->remote_host_name_,
|
||||
props->remember_password_ ? props->remote_password_ : "");
|
||||
}
|
||||
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - queued_at).count();
|
||||
LOG_INFO("[{}] Session cleanup completed in {} ms", props->remote_id_,
|
||||
elapsed);
|
||||
});
|
||||
}
|
||||
|
||||
void GuiRuntime::CloseServerController(const std::string& remote_id) {
|
||||
if (!peer_ || remote_id.empty() || controller_cleanup_tasks_.count(remote_id))
|
||||
return;
|
||||
controller_cleanup_tasks_[remote_id] = session_cleanup_queue_.PostTask(
|
||||
[peer = peer_, remote_id] { LeaveConnection(peer, remote_id.c_str()); });
|
||||
}
|
||||
|
||||
void GuiRuntime::CloseConnectionPeer() {
|
||||
if (!peer_) return;
|
||||
peer_events_->Deactivate();
|
||||
std::vector<std::string> controllers;
|
||||
{
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
for (const auto& [id, status] : connection_status_) controllers.push_back(id);
|
||||
}
|
||||
for (const auto& id : controllers)
|
||||
HandleServerControllerDisconnected(id, "peer_closed");
|
||||
devices_.StopMouseController();
|
||||
devices_.StopScreenCapturer();
|
||||
devices_.StopSpeakerCapturer();
|
||||
devices_.StopKeyboardCapturer();
|
||||
signal_connected_ = false;
|
||||
signal_status_ = SignalStatus::SignalClosed;
|
||||
PeerPtr* peer;
|
||||
{
|
||||
std::unique_lock lock(remote_sessions_mutex_);
|
||||
peer = std::exchange(peer_, nullptr);
|
||||
}
|
||||
// The same serial queue also owns outstanding controller disconnects, so
|
||||
// these finish before the shared peer is destroyed.
|
||||
connection_peer_cleanup_ = session_cleanup_queue_.PostTask(
|
||||
[peer, handler = peer_events_, id = std::string(client_id_)]() mutable {
|
||||
LOG_INFO("[{}] Background connection peer cleanup", id);
|
||||
LeaveConnection(peer, id.c_str());
|
||||
DestroyPeer(&peer);
|
||||
});
|
||||
}
|
||||
|
||||
void GuiRuntime::CloseAllRemoteSessions() {
|
||||
if (peer_) {
|
||||
LOG_INFO("[{}] Leave connection [{}]", client_id_, client_id_);
|
||||
LeaveConnection(peer_, client_id_);
|
||||
is_client_mode_ = false;
|
||||
devices_.StopMouseController();
|
||||
devices_.StopScreenCapturer();
|
||||
devices_.StopSpeakerCapturer();
|
||||
devices_.StopKeyboardCapturer();
|
||||
LOG_INFO("Destroy peer [{}]", client_id_);
|
||||
DestroyPeer(&peer_);
|
||||
}
|
||||
|
||||
CloseConnectionPeer();
|
||||
is_client_mode_ = false;
|
||||
pending_reconnects_.clear();
|
||||
std::vector<std::shared_ptr<RemoteSession>> sessions;
|
||||
{
|
||||
// std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (auto& it : remote_sessions_) {
|
||||
auto props = it.second;
|
||||
CloseRemoteSession(props);
|
||||
}
|
||||
std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (const auto& [id, props] : remote_sessions_) sessions.push_back(props);
|
||||
}
|
||||
|
||||
for (const auto& props : sessions) CloseRemoteSession(props);
|
||||
{
|
||||
// std::unique_lock lock(remote_sessions_mutex_);
|
||||
std::unique_lock lock(remote_sessions_mutex_);
|
||||
remote_sessions_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void GuiRuntime::WaitForThumbnailSaveTasks() {
|
||||
std::vector<std::thread> threads_to_join;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(thumbnail_save_threads_mutex_);
|
||||
threads_to_join.swap(thumbnail_save_threads_);
|
||||
void GuiRuntime::HandleSessionCleanup() {
|
||||
auto completed = [](std::future<void>& task) {
|
||||
if (!task.valid() || task.wait_for(std::chrono::seconds(0)) !=
|
||||
std::future_status::ready) return false;
|
||||
try {
|
||||
task.get();
|
||||
} catch (const std::exception& error) {
|
||||
LOG_ERROR("Connection cleanup failed: {}", error.what());
|
||||
}
|
||||
return true;
|
||||
};
|
||||
completed(connection_peer_cleanup_);
|
||||
for (auto it = controller_cleanup_tasks_.begin();
|
||||
it != controller_cleanup_tasks_.end();) {
|
||||
if (completed(it->second)) it = controller_cleanup_tasks_.erase(it);
|
||||
else ++it;
|
||||
}
|
||||
|
||||
if (threads_to_join.empty()) {
|
||||
return;
|
||||
std::vector<std::pair<std::string, PendingReconnect>> reconnects;
|
||||
for (auto it = session_cleanup_tasks_.begin();
|
||||
it != session_cleanup_tasks_.end();) {
|
||||
if (it->second.wait_for(std::chrono::seconds(0)) !=
|
||||
std::future_status::ready) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
it->second.get();
|
||||
} catch (const std::exception& error) {
|
||||
LOG_ERROR("[{}] Session cleanup failed: {}", it->first, error.what());
|
||||
}
|
||||
if (auto pending = pending_reconnects_.find(it->first);
|
||||
pending != pending_reconnects_.end()) {
|
||||
reconnects.emplace_back(pending->first, std::move(pending->second));
|
||||
pending_reconnects_.erase(pending);
|
||||
}
|
||||
it = session_cleanup_tasks_.erase(it);
|
||||
reload_recent_connections_ = true;
|
||||
recent_connection_image_save_time_ = 0;
|
||||
}
|
||||
for (const auto& [id, request] : reconnects) {
|
||||
ConnectTo(id, request.password.c_str(), request.remember_password);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& thread : threads_to_join) {
|
||||
if (thread.joinable()) {
|
||||
thread.join();
|
||||
void GuiRuntime::WaitForSessionCleanup() {
|
||||
pending_reconnects_.clear();
|
||||
auto drain = [](std::future<void>& task) {
|
||||
if (!task.valid()) return;
|
||||
try {
|
||||
task.get();
|
||||
} catch (const std::exception& error) {
|
||||
LOG_ERROR("Connection cleanup failed: {}", error.what());
|
||||
}
|
||||
};
|
||||
drain(connection_peer_cleanup_);
|
||||
for (auto& [id, task] : controller_cleanup_tasks_) drain(task);
|
||||
controller_cleanup_tasks_.clear();
|
||||
for (auto& [id, task] : session_cleanup_tasks_) {
|
||||
try {
|
||||
task.get();
|
||||
} catch (const std::exception& error) {
|
||||
LOG_ERROR("[{}] Session cleanup failed: {}", id, error.what());
|
||||
}
|
||||
}
|
||||
session_cleanup_tasks_.clear();
|
||||
}
|
||||
|
||||
void GuiRuntime::ResetRemoteSessionResources(
|
||||
@@ -365,6 +446,11 @@ std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
|
||||
|
||||
int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
bool remember_password, bool bypass_presence_check) {
|
||||
if (session_cleanup_tasks_.count(remote_id)) {
|
||||
pending_reconnects_[remote_id] = {password ? password : "", remember_password};
|
||||
LOG_INFO("[{}] Reconnect queued until previous peer is destroyed", remote_id);
|
||||
return 0;
|
||||
}
|
||||
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
|
||||
int ret =
|
||||
RequestSingleDevicePresence(remote_id, password, remember_password);
|
||||
@@ -382,16 +468,14 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
LOG_INFO("Connect to [{}]", remote_id);
|
||||
focused_remote_id_ = remote_id;
|
||||
|
||||
// std::shared_lock shared_lock(remote_sessions_mutex_);
|
||||
bool exists = (remote_sessions_.find(remote_id) != remote_sessions_.end());
|
||||
// shared_lock.unlock();
|
||||
bool exists = FindRemoteSession(remote_id) != nullptr;
|
||||
|
||||
if (!exists) {
|
||||
PeerPtr* peer_to_init = nullptr;
|
||||
std::string local_id;
|
||||
|
||||
{
|
||||
// std::unique_lock unique_lock(remote_sessions_mutex_);
|
||||
std::unique_lock unique_lock(remote_sessions_mutex_);
|
||||
if (remote_sessions_.find(remote_id) == remote_sessions_.end()) {
|
||||
remote_sessions_[remote_id] = std::make_shared<RemoteSession>();
|
||||
auto props = remote_sessions_[remote_id];
|
||||
@@ -399,6 +483,8 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
props->remote_id_ = remote_id;
|
||||
memcpy(&props->params_, ¶ms_, sizeof(Params));
|
||||
props->params_.user_id = props->local_id_.c_str();
|
||||
props->peer_events_ = std::make_shared<PeerEventHandler>(*this);
|
||||
props->params_.user_data = props->peer_events_.get();
|
||||
props->peer_ = CreatePeer(&props->params_);
|
||||
|
||||
props->control_window_width_ = title_bar_height_ * 10.0f;
|
||||
@@ -445,8 +531,8 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
}
|
||||
|
||||
int ret = -1;
|
||||
// std::shared_lock read_lock(remote_sessions_mutex_);
|
||||
auto props = remote_sessions_[remote_id];
|
||||
auto props = FindRemoteSession(remote_id);
|
||||
if (!props || props->closing_) return -1;
|
||||
if (!props->connection_established_) {
|
||||
props->connection_status_.store(ConnectionStatus::Connecting);
|
||||
show_connection_status_window_ = true;
|
||||
@@ -470,7 +556,6 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
}
|
||||
}
|
||||
}
|
||||
// read_lock.unlock();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _CURSOR_POSITION_H_
|
||||
#define _CURSOR_POSITION_H_
|
||||
|
||||
@@ -64,4 +70,4 @@ inline bool NormalizeCursorPosition(
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _CURSOR_STATE_PROVIDER_H_
|
||||
#define _CURSOR_STATE_PROVIDER_H_
|
||||
|
||||
@@ -30,4 +36,4 @@ class CursorStateProvider {
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -1,11 +1,11 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-02-28
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
|
||||
#define CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
|
||||
#ifndef _DEVICE_PRESENCE_CACHE_H_
|
||||
#define _DEVICE_PRESENCE_CACHE_H_
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
@@ -38,4 +38,4 @@ private:
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
|
||||
#endif
|
||||
@@ -19,11 +19,17 @@ namespace crossdesk {
|
||||
|
||||
GuiRuntime::GuiRuntime()
|
||||
: clipboard_(*this), devices_(*this), transfers_(*this), settings_(*this),
|
||||
keyboard_(*this), peer_events_(*this) {}
|
||||
keyboard_(*this) {}
|
||||
|
||||
GuiRuntime::~GuiRuntime() = default;
|
||||
GuiRuntime::~GuiRuntime() { WaitForSessionCleanup(); }
|
||||
|
||||
int GuiRuntime::CreateConnectionPeer() {
|
||||
// Tick retries after the old peer's leave notification and socket shutdown.
|
||||
if (connection_peer_cleanup_.valid()) return 0;
|
||||
{
|
||||
std::unique_lock lock(remote_sessions_mutex_);
|
||||
peer_events_ = std::make_shared<PeerEventHandler>(*this);
|
||||
}
|
||||
params_.use_cfg_file = false;
|
||||
|
||||
std::string signal_server_ip;
|
||||
@@ -141,9 +147,14 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
sizeof(params_.log_path) - 1);
|
||||
params_.log_path[sizeof(params_.log_path) - 1] = '\0';
|
||||
params_.hardware_acceleration = config_center_->IsHardwareVideoCodec();
|
||||
// The Slint desktop renderer currently consumes packed CPU frames. Native
|
||||
// renderers can opt into platform-native frame output independently.
|
||||
#if defined(_WIN32) || defined(__linux__) || defined(__APPLE__)
|
||||
// Windows and Linux retain pooled CPU NV12 frames or CUDA device frames;
|
||||
// macOS retains VideoToolbox CVPixelBuffers for direct Metal sampling. All
|
||||
// platforms fall back to a packed CPU copy when native upload is unavailable.
|
||||
params_.native_video_output = true;
|
||||
#else
|
||||
params_.native_video_output = false;
|
||||
#endif
|
||||
params_.av1_encoding = config_center_->GetVideoEncodeFormat() ==
|
||||
ConfigCenter::VIDEO_ENCODE_FORMAT::AV1
|
||||
? true
|
||||
@@ -184,14 +195,17 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
params_.on_connection_status = PeerEventHandler::OnConnectionStatus;
|
||||
params_.on_net_status_report = PeerEventHandler::OnNetStatusReport;
|
||||
|
||||
params_.user_data = &peer_events_;
|
||||
params_.user_data = peer_events_.get();
|
||||
|
||||
// The previous peer may have left a terminal status behind. Reset it before
|
||||
// Init() starts emitting callbacks for the newly selected server.
|
||||
signal_connected_ = false;
|
||||
signal_status_ = SignalStatus::SignalConnecting;
|
||||
|
||||
peer_ = CreatePeer(¶ms_);
|
||||
{
|
||||
std::unique_lock lock(remote_sessions_mutex_);
|
||||
peer_ = CreatePeer(¶ms_);
|
||||
}
|
||||
if (peer_) {
|
||||
LOG_INFO("Create peer instance [{}] successful", client_id_);
|
||||
Init(peer_);
|
||||
@@ -235,7 +249,7 @@ void GuiRuntime::UpdateLabels() {
|
||||
|
||||
|
||||
void GuiRuntime::HandleRecentConnections() {
|
||||
if (reload_recent_connections_ && thumbnail_) {
|
||||
if (session_cleanup_tasks_.empty() && reload_recent_connections_ && thumbnail_) {
|
||||
uint32_t now_time = SDL_GetTicks();
|
||||
if (now_time - recent_connection_image_save_time_ >= 50) {
|
||||
int ret = thumbnail_->LoadThumbnail(recent_connections_,
|
||||
@@ -264,11 +278,22 @@ void GuiRuntime::SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len) {
|
||||
|
||||
if (1) {
|
||||
std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||
uint64_t captured_timestamp = 0;
|
||||
for (const auto &it : runtime->remote_sessions_) {
|
||||
auto props = it.second;
|
||||
if (props->connection_status_.load() == ConnectionStatus::Connected) {
|
||||
if (props->peer_) {
|
||||
SendAudioFrame(props->peer_, (const char *)stream, len,
|
||||
if (captured_timestamp == 0) {
|
||||
const int64_t timestamp_us = GetSystemTimeMicros(props->peer_);
|
||||
if (timestamp_us > 0) {
|
||||
captured_timestamp = static_cast<uint64_t>(timestamp_us);
|
||||
}
|
||||
}
|
||||
MiniRtcAudioFrame frame{};
|
||||
frame.data = reinterpret_cast<const char *>(stream);
|
||||
frame.size = static_cast<size_t>(len);
|
||||
frame.captured_timestamp = captured_timestamp;
|
||||
SendAudioFrame(props->peer_, &frame,
|
||||
runtime->audio_label_.c_str());
|
||||
}
|
||||
}
|
||||
@@ -285,29 +310,6 @@ void GuiRuntime::SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len) {
|
||||
void GuiRuntime::SdlCaptureAudioOut([[maybe_unused]] void *userdata,
|
||||
[[maybe_unused]] Uint8 *stream,
|
||||
[[maybe_unused]] int len) {
|
||||
// GuiApplication *runtime = (GuiApplication *)userdata;
|
||||
// for (auto it : runtime->remote_sessions_) {
|
||||
// auto props = it.second;
|
||||
// if (props->connection_status_ == SignalStatus::SignalConnected) {
|
||||
// SendAudioFrame(props->peer_, (const char *)stream, len);
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (!runtime->audio_buffer_fresh_) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// SDL_memset(stream, 0, len);
|
||||
|
||||
// if (runtime->audio_len_ == 0) {
|
||||
// return;
|
||||
// } else {
|
||||
// }
|
||||
|
||||
// len = (len > runtime->audio_len_ ? runtime->audio_len_ : len);
|
||||
// SDL_MixAudioFormat(stream, runtime->audio_buffer_, AUDIO_S16LSB, len,
|
||||
// SDL_MIX_MAXVOLUME);
|
||||
// runtime->audio_buffer_fresh_ = false;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
#ifndef CROSSDESK_GUI_RUNTIME_H_
|
||||
#define CROSSDESK_GUI_RUNTIME_H_
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _GUI_RUNTIME_H_
|
||||
#define _GUI_RUNTIME_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
@@ -55,8 +61,11 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
|
||||
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
|
||||
void CloseAllRemoteSessions();
|
||||
void CloseConnectionPeer();
|
||||
void CloseServerController(const std::string& remote_id);
|
||||
void ResetRemoteSessionResources(std::shared_ptr<RemoteSession> props);
|
||||
void WaitForThumbnailSaveTasks();
|
||||
void HandleSessionCleanup();
|
||||
void WaitForSessionCleanup();
|
||||
std::shared_ptr<RemoteSession> FindRemoteSession(
|
||||
const std::string& remote_id);
|
||||
|
||||
@@ -83,7 +92,7 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
FileTransferManager transfers_;
|
||||
SettingsManager settings_;
|
||||
KeyboardController keyboard_;
|
||||
PeerEventHandler peer_events_;
|
||||
std::shared_ptr<PeerEventHandler> peer_events_;
|
||||
std::atomic<bool> video_frame_dirty_{false};
|
||||
std::unique_ptr<VideoRenderer> video_renderer_;
|
||||
|
||||
@@ -98,4 +107,4 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_RUNTIME_H_
|
||||
#endif
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
/*
|
||||
* Aggregate state visible to GuiRuntime and GuiApplication.
|
||||
*
|
||||
* Definitions live beside their owners: SDL/window state in application and
|
||||
* connection/session state in runtime. This header is intentionally only the
|
||||
* composition point.
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef CROSSDESK_GUI_STATE_H_
|
||||
#define CROSSDESK_GUI_STATE_H_
|
||||
#ifndef _GUI_STATE_H_
|
||||
#define _GUI_STATE_H_
|
||||
|
||||
#include "application/application_state.h"
|
||||
#include "runtime/runtime_state.h"
|
||||
@@ -18,4 +16,4 @@ struct GuiState : ApplicationState, RuntimeState {};
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif // CROSSDESK_GUI_STATE_H_
|
||||
#endif
|
||||
@@ -79,6 +79,9 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
||||
const char *data, size_t size, const char *user_id, size_t user_id_size,
|
||||
const char *src_id, size_t src_id_size, void *user_data) {
|
||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
||||
if (!handler) return;
|
||||
auto callback = handler->EnterCallback();
|
||||
if (!callback) return;
|
||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
@@ -145,9 +148,8 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
if (remote_action.type == ControlType::service_status) {
|
||||
auto props_it = runtime->remote_sessions_.find(remote_id);
|
||||
if (props_it != runtime->remote_sessions_.end()) {
|
||||
runtime->ApplyRemoteServiceStatus(*props_it->second, remote_action.ss);
|
||||
if (auto props = runtime->FindRemoteSession(remote_id)) {
|
||||
runtime->ApplyRemoteServiceStatus(*props, remote_action.ss);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,6 +60,9 @@ PeerEventHandler::PeerEventHandler(GuiRuntime& owner) : owner_(owner) {}
|
||||
void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
|
||||
void* user_data) {
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
if (!handler) return;
|
||||
auto callback = handler->EnterCallback();
|
||||
if (!callback) return;
|
||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime || !message || size == 0) {
|
||||
return;
|
||||
@@ -153,6 +156,9 @@ void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
|
||||
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
|
||||
size_t user_id_size, void* user_data) {
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
if (!handler) return;
|
||||
auto callback = handler->EnterCallback();
|
||||
if (!callback) return;
|
||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
@@ -198,12 +204,8 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
|
||||
}
|
||||
|
||||
std::string remote_id(client_id.begin() + 2, client_id.end());
|
||||
// std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||
if (runtime->remote_sessions_.find(remote_id) ==
|
||||
runtime->remote_sessions_.end()) {
|
||||
return;
|
||||
}
|
||||
auto props = runtime->remote_sessions_.find(remote_id)->second;
|
||||
auto props = runtime->FindRemoteSession(remote_id);
|
||||
if (!props) return;
|
||||
props->signal_status_ = status;
|
||||
if (SignalStatus::SignalConnecting == status) {
|
||||
props->signal_connected_ = false;
|
||||
@@ -230,6 +232,9 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
const size_t user_id_size,
|
||||
void* user_data) {
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
if (!handler) return;
|
||||
auto callback = handler->EnterCallback();
|
||||
if (!callback) return;
|
||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) return;
|
||||
|
||||
@@ -304,8 +309,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
remote_action_codec::Free(remote_action);
|
||||
}
|
||||
|
||||
if (!runtime->need_to_create_stream_window_ &&
|
||||
!runtime->remote_sessions_.empty()) {
|
||||
if (!runtime->need_to_create_stream_window_) {
|
||||
runtime->need_to_create_stream_window_ = true;
|
||||
}
|
||||
props->connection_established_ = true;
|
||||
@@ -493,9 +497,12 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
|
||||
void PeerEventHandler::OnNetStatusReport(
|
||||
const char* client_id, size_t client_id_size, TraversalMode mode,
|
||||
const XNetTrafficStats* net_traffic_stats, const char* user_id,
|
||||
const MiniRtcNetTrafficStats* net_traffic_stats, const char* user_id,
|
||||
const size_t user_id_size, void* user_data) {
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
if (!handler) return;
|
||||
auto callback = handler->EnterCallback();
|
||||
if (!callback) return;
|
||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
@@ -556,12 +563,8 @@ void PeerEventHandler::OnNetStatusReport(
|
||||
}
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
// std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||
if (runtime->remote_sessions_.find(remote_id) ==
|
||||
runtime->remote_sessions_.end()) {
|
||||
return;
|
||||
}
|
||||
auto props = runtime->remote_sessions_.find(remote_id)->second;
|
||||
auto props = runtime->FindRemoteSession(remote_id);
|
||||
if (!props) return;
|
||||
if (props->traversal_mode_ != mode) {
|
||||
props->traversal_mode_ = mode;
|
||||
LOG_INFO("Net mode: [{}]", int(props->traversal_mode_));
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
#ifndef CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
|
||||
#define CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PEER_EVENT_HANDLER_H_
|
||||
#define _PEER_EVENT_HANDLER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
#include "minirtc.h"
|
||||
#include "runtime/session_lifecycle.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -15,7 +22,11 @@ class PeerEventHandler {
|
||||
public:
|
||||
explicit PeerEventHandler(GuiRuntime &owner);
|
||||
|
||||
static void OnReceiveVideoBuffer(const XVideoFrame *video_frame,
|
||||
bool IsActive() const { return callbacks_.IsActive(); }
|
||||
auto EnterCallback() { return callbacks_.Enter(); }
|
||||
void Deactivate() { callbacks_.Close(); }
|
||||
|
||||
static void OnReceiveVideoBuffer(const MiniRtcVideoFrame *video_frame,
|
||||
const char *user_id, size_t user_id_size,
|
||||
const char *src_id, size_t src_id_size,
|
||||
void *user_data);
|
||||
@@ -35,7 +46,7 @@ public:
|
||||
size_t user_id_size, void *user_data);
|
||||
static void OnNetStatusReport(const char *client_id, size_t client_id_size,
|
||||
TraversalMode mode,
|
||||
const XNetTrafficStats *net_traffic_stats,
|
||||
const MiniRtcNetTrafficStats *net_traffic_stats,
|
||||
const char *user_id, size_t user_id_size,
|
||||
void *user_data);
|
||||
|
||||
@@ -43,8 +54,9 @@ private:
|
||||
static void SendClientInfo(PeerPtr *peer, const std::string &client_id);
|
||||
|
||||
GuiRuntime &owner_;
|
||||
gui_detail::SessionCallbackGate callbacks_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
|
||||
#endif
|
||||
@@ -14,33 +14,51 @@ namespace {
|
||||
|
||||
constexpr auto kBackgroundSnapshotInterval = std::chrono::seconds(1);
|
||||
|
||||
const MiniRtcNativeVideoFrame* GetNativeVideoFrame(const MiniRtcVideoFrame* frame) {
|
||||
if (!frame || !frame->native_frame) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto* native = frame->native_frame;
|
||||
return native->struct_size >=
|
||||
static_cast<uint32_t>(sizeof(MiniRtcNativeVideoFrame)) &&
|
||||
native->owner && native->copy_to_nv12
|
||||
? native
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
const XVideoFrame* video_frame, const char* user_id, size_t user_id_size,
|
||||
const MiniRtcVideoFrame* video_frame, const char* user_id, size_t user_id_size,
|
||||
const char* src_id, size_t src_id_size, void* user_data) {
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
if (!handler) return;
|
||||
auto callback = handler->EnterCallback();
|
||||
if (!callback) return;
|
||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
// std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||
if (runtime->remote_sessions_.find(remote_id) ==
|
||||
runtime->remote_sessions_.end()) {
|
||||
return;
|
||||
}
|
||||
GuiRuntime::RemoteSession* props =
|
||||
runtime->remote_sessions_.find(remote_id)->second.get();
|
||||
auto props = runtime->FindRemoteSession(remote_id);
|
||||
if (!props) return;
|
||||
|
||||
if (props->connection_established_) {
|
||||
bool background_snapshot_only = false;
|
||||
auto* native_renderer = runtime->video_renderer_.get();
|
||||
if (native_renderer && native_renderer->IsActive()) {
|
||||
const auto submit_result = native_renderer->SubmitNv12(
|
||||
remote_id, reinterpret_cast<const uint8_t*>(video_frame->data),
|
||||
video_frame->size, video_frame->width, video_frame->height);
|
||||
VideoRenderer::SubmitResult submit_result =
|
||||
VideoRenderer::SubmitResult::failed;
|
||||
if (GetNativeVideoFrame(video_frame)) {
|
||||
submit_result = native_renderer->SubmitNativeFrame(
|
||||
remote_id, *video_frame->native_frame);
|
||||
} else
|
||||
{
|
||||
submit_result = native_renderer->SubmitNv12(
|
||||
remote_id, reinterpret_cast<const uint8_t*>(video_frame->data),
|
||||
video_frame->size, video_frame->width, video_frame->height);
|
||||
}
|
||||
if (submit_result == VideoRenderer::SubmitResult::submitted) {
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
const bool size_changed = (props->video_width_ != video_frame->width) ||
|
||||
@@ -88,17 +106,39 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
return;
|
||||
}
|
||||
|
||||
size_t frame_size = video_frame->size;
|
||||
const MiniRtcNativeVideoFrame* native_frame =
|
||||
GetNativeVideoFrame(video_frame);
|
||||
if (native_frame) {
|
||||
frame_size = static_cast<size_t>(native_frame->width) *
|
||||
native_frame->height * 3U / 2U;
|
||||
}
|
||||
if (frame_size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate a third buffer only while the UI still owns the old snapshot.
|
||||
if (!props->back_frame_ || props->back_frame_.use_count() != 1) {
|
||||
props->back_frame_ =
|
||||
std::make_shared<std::vector<unsigned char>>(video_frame->size);
|
||||
std::make_shared<std::vector<unsigned char>>(frame_size);
|
||||
}
|
||||
if (props->back_frame_->size() != video_frame->size) {
|
||||
props->back_frame_->resize(video_frame->size);
|
||||
if (props->back_frame_->size() != frame_size) {
|
||||
props->back_frame_->resize(frame_size);
|
||||
}
|
||||
|
||||
std::memcpy(props->back_frame_->data(), video_frame->data,
|
||||
video_frame->size);
|
||||
if (native_frame) {
|
||||
if (native_frame->copy_to_nv12(
|
||||
native_frame->owner, props->back_frame_->data(),
|
||||
props->back_frame_->size()) != 0) {
|
||||
return;
|
||||
}
|
||||
} else
|
||||
{
|
||||
if (!video_frame->data || video_frame->size < frame_size) {
|
||||
return;
|
||||
}
|
||||
std::memcpy(props->back_frame_->data(), video_frame->data, frame_size);
|
||||
}
|
||||
|
||||
const bool size_changed = (props->video_width_ != video_frame->width) ||
|
||||
(props->video_height_ != video_frame->height);
|
||||
@@ -108,7 +148,7 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
|
||||
props->video_width_ = video_frame->width;
|
||||
props->video_height_ = video_frame->height;
|
||||
props->video_size_ = video_frame->size;
|
||||
props->video_size_ = frame_size;
|
||||
|
||||
props->front_frame_.swap(props->back_frame_);
|
||||
props->thumbnail_frame_ = props->front_frame_;
|
||||
@@ -132,6 +172,9 @@ void PeerEventHandler::OnReceiveAudioBuffer(
|
||||
const char* data, size_t size, const char* user_id, size_t user_id_size,
|
||||
const char* src_id, size_t src_id_size, void* user_data) {
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
if (!handler) return;
|
||||
auto callback = handler->EnterCallback();
|
||||
if (!callback) return;
|
||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
#ifndef CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
|
||||
#define CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _REMOTE_ACTION_CODEC_H_
|
||||
#define _REMOTE_ACTION_CODEC_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
@@ -14,4 +20,4 @@ void Free(RemoteAction &action);
|
||||
|
||||
} // namespace crossdesk::remote_action_codec
|
||||
|
||||
#endif // CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
|
||||
#endif
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _REMOTE_SESSION_H_
|
||||
#define _REMOTE_SESSION_H_
|
||||
|
||||
@@ -18,6 +24,8 @@
|
||||
#include "display_info.h"
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk { class PeerEventHandler; }
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
|
||||
struct FileTransferState {
|
||||
@@ -61,6 +69,8 @@ struct FileTransferState {
|
||||
struct RemoteSession {
|
||||
Params params_;
|
||||
PeerPtr* peer_ = nullptr;
|
||||
std::shared_ptr<PeerEventHandler> peer_events_;
|
||||
std::atomic<bool> closing_{false};
|
||||
std::string audio_label_ = kAudioStream;
|
||||
std::string data_label_ = kDataStream;
|
||||
std::string mouse_label_ = kMouseStream;
|
||||
@@ -162,7 +172,7 @@ struct RemoteSession {
|
||||
int fps_ = 0;
|
||||
int frame_count_ = 0;
|
||||
std::chrono::steady_clock::time_point last_time_;
|
||||
XNetTrafficStats net_traffic_stats_{};
|
||||
MiniRtcNetTrafficStats net_traffic_stats_{};
|
||||
|
||||
using QueuedFile = FileTransferState::QueuedFile;
|
||||
using FileTransferStatus = FileTransferState::FileTransferStatus;
|
||||
@@ -174,4 +184,4 @@ using RemoteSessionPtr = std::shared_ptr<RemoteSession>;
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _RUNTIME_STATE_H_
|
||||
#define _RUNTIME_STATE_H_
|
||||
|
||||
@@ -16,9 +22,11 @@
|
||||
#include <stream_names.h>
|
||||
|
||||
#include "config_center.h"
|
||||
#include "common/task_queue_lock_free.h"
|
||||
#include "path_manager.h"
|
||||
#include "runtime/device_presence_cache.h"
|
||||
#include "runtime/remote_session.h"
|
||||
#include "runtime/session_lifecycle.h"
|
||||
#include "thumbnail.h"
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
@@ -178,8 +186,15 @@ struct ConnectionState {
|
||||
std::unordered_map<std::string, RemoteSessionPtr>;
|
||||
RemoteSessionMap remote_sessions_;
|
||||
std::shared_mutex remote_sessions_mutex_;
|
||||
std::vector<std::thread> thumbnail_save_threads_;
|
||||
std::mutex thumbnail_save_threads_mutex_;
|
||||
TaskQueueLockFree session_cleanup_queue_;
|
||||
std::unordered_map<std::string, std::future<void>> session_cleanup_tasks_;
|
||||
std::future<void> connection_peer_cleanup_;
|
||||
std::unordered_map<std::string, std::future<void>> controller_cleanup_tasks_;
|
||||
struct PendingReconnect {
|
||||
std::string password;
|
||||
bool remember_password = false;
|
||||
};
|
||||
std::unordered_map<std::string, PendingReconnect> pending_reconnects_;
|
||||
|
||||
std::shared_mutex connection_status_mutex_;
|
||||
std::unordered_map<std::string, ConnectionStatus> connection_status_;
|
||||
@@ -212,4 +227,4 @@ struct RuntimeState : InfrastructureState,
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "runtime/session_lifecycle.h"
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
|
||||
CallbackGuard::CallbackGuard(SessionCallbackGate& callbacks)
|
||||
: callbacks_(callbacks), accepted_(callbacks.BeginCallback()) {}
|
||||
|
||||
CallbackGuard::~CallbackGuard() {
|
||||
if (accepted_) {
|
||||
callbacks_.EndCallback();
|
||||
}
|
||||
}
|
||||
|
||||
bool SessionCallbackGate::IsActive() const {
|
||||
std::lock_guard lock(mutex_);
|
||||
return accepting_callbacks_;
|
||||
}
|
||||
|
||||
CallbackGuard SessionCallbackGate::Enter() {
|
||||
return CallbackGuard(*this);
|
||||
}
|
||||
|
||||
bool SessionCallbackGate::BeginCallback() {
|
||||
std::lock_guard lock(mutex_);
|
||||
if (!accepting_callbacks_) {
|
||||
return false;
|
||||
}
|
||||
++running_callbacks_;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SessionCallbackGate::EndCallback() {
|
||||
std::lock_guard lock(mutex_);
|
||||
--running_callbacks_;
|
||||
if (running_callbacks_ == 0) {
|
||||
callbacks_finished_.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
void SessionCallbackGate::Close() {
|
||||
std::unique_lock lock(mutex_);
|
||||
accepting_callbacks_ = false;
|
||||
// Waiting releases the lock so running callbacks can call EndCallback().
|
||||
callbacks_finished_.wait(lock, [this] { return running_callbacks_ == 0; });
|
||||
}
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SESSION_LIFECYCLE_H_
|
||||
#define _SESSION_LIFECYCLE_H_
|
||||
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
|
||||
class SessionCallbackGate;
|
||||
|
||||
// Decrements the count on scope exit, including early returns and exceptions.
|
||||
class CallbackGuard {
|
||||
public:
|
||||
explicit CallbackGuard(SessionCallbackGate& callbacks);
|
||||
~CallbackGuard();
|
||||
|
||||
CallbackGuard(const CallbackGuard&) = delete;
|
||||
CallbackGuard& operator=(const CallbackGuard&) = delete;
|
||||
|
||||
explicit operator bool() const { return accepted_; }
|
||||
|
||||
private:
|
||||
SessionCallbackGate& callbacks_;
|
||||
bool accepted_;
|
||||
};
|
||||
|
||||
// Each peer counts callbacks as they enter and leave.
|
||||
// Closing rejects new callbacks, then waits for running callbacks to finish.
|
||||
class SessionCallbackGate {
|
||||
public:
|
||||
bool IsActive() const;
|
||||
CallbackGuard Enter();
|
||||
|
||||
// Called by the session owner, never from within this session's callbacks.
|
||||
void Close();
|
||||
|
||||
private:
|
||||
friend class CallbackGuard;
|
||||
|
||||
bool BeginCallback();
|
||||
void EndCallback();
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
std::condition_variable callbacks_finished_;
|
||||
bool accepting_callbacks_ = true;
|
||||
std::size_t running_callbacks_ = 0;
|
||||
};
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif
|
||||
@@ -1,5 +1,11 @@
|
||||
#ifndef CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
|
||||
#define CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _UI_LOCALIZATION_H_
|
||||
#define _UI_LOCALIZATION_H_
|
||||
|
||||
#include <slint.h>
|
||||
|
||||
@@ -162,4 +168,5 @@ inline int ApplyStreamWindowStrings(
|
||||
|
||||
} // namespace crossdesk::ui_localization
|
||||
|
||||
#endif // CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "platform/common/gui/cuda_driver.h"
|
||||
|
||||
#if CROSSDESK_HAS_CUDA_DRIVER
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
const CudaDriver* CudaDriver::Get() {
|
||||
static const CudaDriver driver;
|
||||
return driver.loaded_ ? &driver : nullptr;
|
||||
}
|
||||
|
||||
void* CudaDriver::Symbol(const char* name) const {
|
||||
#if defined(_WIN32)
|
||||
return reinterpret_cast<void*>(
|
||||
GetProcAddress(static_cast<HMODULE>(module_), name));
|
||||
#else
|
||||
return dlsym(module_, name);
|
||||
#endif
|
||||
}
|
||||
|
||||
CudaDriver::CudaDriver() {
|
||||
#if defined(_WIN32)
|
||||
module_ = LoadLibraryW(L"nvcuda.dll");
|
||||
#else
|
||||
module_ = dlopen("libcuda.so.1", RTLD_LAZY | RTLD_LOCAL);
|
||||
#endif
|
||||
if (!module_) return;
|
||||
const auto load = [this](auto& function, const char* name) {
|
||||
function = reinterpret_cast<std::decay_t<decltype(function)>>(Symbol(name));
|
||||
return function != nullptr;
|
||||
};
|
||||
loaded_ = load(init, "cuInit") &&
|
||||
load(device_get_count, "cuDeviceGetCount") &&
|
||||
load(device_get, "cuDeviceGet") &&
|
||||
load(context_create, "cuCtxCreate_v2") &&
|
||||
load(context_destroy, "cuCtxDestroy_v2") &&
|
||||
load(context_push, "cuCtxPushCurrent_v2") &&
|
||||
load(context_pop, "cuCtxPopCurrent_v2") &&
|
||||
load(error_name, "cuGetErrorName") &&
|
||||
load(mem_alloc, "cuMemAlloc_v2") &&
|
||||
load(mem_free, "cuMemFree_v2") &&
|
||||
load(memcpy_2d, "cuMemcpy2D_v2") &&
|
||||
load(memcpy_2d_async, "cuMemcpy2DAsync_v2") &&
|
||||
load(stream_create, "cuStreamCreate") &&
|
||||
load(stream_destroy, "cuStreamDestroy_v2") &&
|
||||
load(stream_synchronize, "cuStreamSynchronize") &&
|
||||
load(register_gl_buffer, "cuGraphicsGLRegisterBuffer") &&
|
||||
load(unregister_resource, "cuGraphicsUnregisterResource") &&
|
||||
load(map_resources, "cuGraphicsMapResources") &&
|
||||
load(unmap_resources, "cuGraphicsUnmapResources") &&
|
||||
load(mapped_pointer, "cuGraphicsResourceGetMappedPointer_v2");
|
||||
}
|
||||
|
||||
CudaDriver::~CudaDriver() {
|
||||
if (!module_) return;
|
||||
#if defined(_WIN32)
|
||||
FreeLibrary(static_cast<HMODULE>(module_));
|
||||
#else
|
||||
dlclose(module_);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool CudaDriver::Check(CUresult result, const char* operation) const {
|
||||
if (result == CUDA_SUCCESS) return true;
|
||||
const char* name = nullptr;
|
||||
error_name(result, &name);
|
||||
LOG_WARN("CUDA/OpenGL {} failed: {} ({})", operation,
|
||||
name ? name : "unknown CUDA error", static_cast<int>(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _CUDA_DRIVER_H_
|
||||
#define _CUDA_DRIVER_H_
|
||||
|
||||
#if defined(USE_CUDA) && USE_CUDA && \
|
||||
(defined(_WIN32) || \
|
||||
(defined(__linux__) && (defined(__x86_64__) || defined(__amd64__))))
|
||||
#define CROSSDESK_HAS_CUDA_DRIVER 1
|
||||
|
||||
#include <cuda.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Application-owned driver entry points. No codec library or MiniRTC internals
|
||||
// are involved; the module remains loaded for this singleton's lifetime.
|
||||
class CudaDriver {
|
||||
public:
|
||||
static const CudaDriver* Get();
|
||||
bool Check(CUresult result, const char* operation) const;
|
||||
|
||||
decltype(&cuInit) init = nullptr;
|
||||
decltype(&cuDeviceGetCount) device_get_count = nullptr;
|
||||
decltype(&cuDeviceGet) device_get = nullptr;
|
||||
CUresult(CUDAAPI* context_create)(CUcontext*, unsigned int, CUdevice) = nullptr;
|
||||
decltype(&cuCtxDestroy) context_destroy = nullptr;
|
||||
decltype(&cuCtxPushCurrent) context_push = nullptr;
|
||||
decltype(&cuCtxPopCurrent) context_pop = nullptr;
|
||||
decltype(&cuGetErrorName) error_name = nullptr;
|
||||
decltype(&cuMemAlloc) mem_alloc = nullptr;
|
||||
decltype(&cuMemFree) mem_free = nullptr;
|
||||
decltype(&cuMemcpy2D) memcpy_2d = nullptr;
|
||||
decltype(&cuMemcpy2DAsync) memcpy_2d_async = nullptr;
|
||||
decltype(&cuStreamCreate) stream_create = nullptr;
|
||||
decltype(&cuStreamDestroy) stream_destroy = nullptr;
|
||||
decltype(&cuStreamSynchronize) stream_synchronize = nullptr;
|
||||
CUresult(CUDAAPI* register_gl_buffer)(CUgraphicsResource*, unsigned int,
|
||||
unsigned int) = nullptr;
|
||||
decltype(&cuGraphicsUnregisterResource) unregister_resource = nullptr;
|
||||
decltype(&cuGraphicsMapResources) map_resources = nullptr;
|
||||
decltype(&cuGraphicsUnmapResources) unmap_resources = nullptr;
|
||||
decltype(&cuGraphicsResourceGetMappedPointer) mapped_pointer = nullptr;
|
||||
|
||||
private:
|
||||
CudaDriver();
|
||||
~CudaDriver();
|
||||
CudaDriver(const CudaDriver&) = delete;
|
||||
CudaDriver& operator=(const CudaDriver&) = delete;
|
||||
void* Symbol(const char* name) const;
|
||||
void* module_ = nullptr;
|
||||
bool loaded_ = false;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
#else
|
||||
#define CROSSDESK_HAS_CUDA_DRIVER 0
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,198 @@
|
||||
#include "platform/common/gui/cuda_opengl_interop.h"
|
||||
#include "platform/common/gui/cuda_driver.h"
|
||||
|
||||
#include <memory>
|
||||
#include <new>
|
||||
|
||||
#include "platform/common/gui/native_video_frame_ref.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
#if CROSSDESK_HAS_CUDA_DRIVER
|
||||
|
||||
namespace {
|
||||
|
||||
class ScopedCudaContext {
|
||||
public:
|
||||
ScopedCudaContext(const CudaDriver& driver, CUcontext context)
|
||||
: cuda(driver),
|
||||
active_(cuda.Check(cuda.context_push(context), "cuCtxPushCurrent")) {}
|
||||
~ScopedCudaContext() { Pop(); }
|
||||
explicit operator bool() const { return active_; }
|
||||
bool Pop() {
|
||||
if (!active_) return true;
|
||||
active_ = false;
|
||||
CUcontext context = nullptr;
|
||||
return cuda.Check(cuda.context_pop(&context), "cuCtxPopCurrent");
|
||||
}
|
||||
|
||||
private:
|
||||
const CudaDriver& cuda;
|
||||
bool active_;
|
||||
};
|
||||
|
||||
bool IsCudaFrame(const MiniRtcNativeVideoFrame* frame) {
|
||||
return frame && GetOpenGlNativeFrame(*frame) &&
|
||||
frame->type == MiniRtcNativeVideoFrameCudaNv12;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct CudaOpenGlVideoBuffer {
|
||||
const CudaDriver* driver = nullptr;
|
||||
CUcontext context = nullptr;
|
||||
CUstream stream = nullptr;
|
||||
CUgraphicsResource resource = nullptr;
|
||||
bool mapped = false;
|
||||
bool failed = false;
|
||||
NativeVideoFrameRef source_frame;
|
||||
};
|
||||
|
||||
bool IsCudaDeviceAvailable() {
|
||||
const auto* cuda = CudaDriver::Get();
|
||||
int count = 0;
|
||||
return cuda && cuda->init(0) == CUDA_SUCCESS &&
|
||||
cuda->device_get_count(&count) == CUDA_SUCCESS && count > 0;
|
||||
}
|
||||
|
||||
CudaOpenGlVideoBuffer* CreateCudaOpenGlVideoBuffer(
|
||||
const MiniRtcNativeVideoFrame* frame, uint32_t gl_buffer) {
|
||||
const auto* driver = CudaDriver::Get();
|
||||
if (!IsCudaFrame(frame) || gl_buffer == 0 || !driver) {
|
||||
return nullptr;
|
||||
}
|
||||
std::unique_ptr<CudaOpenGlVideoBuffer, decltype(&DestroyCudaOpenGlVideoBuffer)>
|
||||
buffer(new (std::nothrow) CudaOpenGlVideoBuffer,
|
||||
&DestroyCudaOpenGlVideoBuffer);
|
||||
if (!buffer) return nullptr;
|
||||
buffer->driver = driver;
|
||||
const auto& cuda = *driver;
|
||||
buffer->context = static_cast<CUcontext>(frame->payload.cuda_nv12.context);
|
||||
buffer->source_frame = NativeVideoFrameRef(frame);
|
||||
ScopedCudaContext context(cuda, buffer->context);
|
||||
if (!context ||
|
||||
!cuda.Check(cuda.stream_create(&buffer->stream,
|
||||
CU_STREAM_NON_BLOCKING),
|
||||
"cuStreamCreate") ||
|
||||
!cuda.Check(cuda.register_gl_buffer(
|
||||
&buffer->resource, gl_buffer,
|
||||
CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD),
|
||||
"cuGraphicsGLRegisterBuffer") ||
|
||||
!context.Pop()) {
|
||||
return nullptr;
|
||||
}
|
||||
return buffer.release();
|
||||
}
|
||||
|
||||
int UploadCudaFrameToOpenGlBuffer(
|
||||
CudaOpenGlVideoBuffer* buffer, const MiniRtcNativeVideoFrame* frame) {
|
||||
if (!buffer || buffer->failed || !IsCudaFrame(frame) ||
|
||||
frame->payload.cuda_nv12.context != buffer->context) {
|
||||
return -1;
|
||||
}
|
||||
const auto& cuda = *buffer->driver;
|
||||
ScopedCudaContext context(cuda, buffer->context);
|
||||
if (!context) {
|
||||
buffer->failed = true;
|
||||
return -1;
|
||||
}
|
||||
// The caller has completed this buffer's previous GL use. Hold the new
|
||||
// surface through asynchronous copies, including partially failed uploads.
|
||||
buffer->source_frame = NativeVideoFrameRef(frame);
|
||||
const char* operation = "cuGraphicsMapResources";
|
||||
CUresult result =
|
||||
cuda.map_resources(1, &buffer->resource, buffer->stream);
|
||||
buffer->mapped = result == CUDA_SUCCESS;
|
||||
CUdeviceptr destination = 0;
|
||||
size_t mapped_size = 0;
|
||||
if (result == CUDA_SUCCESS) {
|
||||
operation = "cuGraphicsResourceGetMappedPointer";
|
||||
result = cuda.mapped_pointer(
|
||||
&destination, &mapped_size, buffer->resource);
|
||||
}
|
||||
const size_t y_size = static_cast<size_t>(frame->width) * frame->height;
|
||||
if (result == CUDA_SUCCESS && mapped_size < y_size + y_size / 2) {
|
||||
operation = "validate mapped buffer size";
|
||||
result = CUDA_ERROR_INVALID_VALUE;
|
||||
}
|
||||
if (result == CUDA_SUCCESS) {
|
||||
CUDA_MEMCPY2D copy{};
|
||||
copy.srcMemoryType = CU_MEMORYTYPE_DEVICE;
|
||||
copy.srcDevice = frame->payload.cuda_nv12.y_device_pointer;
|
||||
copy.srcPitch = frame->payload.cuda_nv12.y_stride;
|
||||
copy.dstMemoryType = CU_MEMORYTYPE_DEVICE;
|
||||
copy.dstDevice = destination;
|
||||
copy.dstPitch = frame->width;
|
||||
copy.WidthInBytes = frame->width;
|
||||
copy.Height = frame->height;
|
||||
operation = "cuMemcpy2DAsync (Y plane)";
|
||||
result = cuda.memcpy_2d_async(©, buffer->stream);
|
||||
if (result == CUDA_SUCCESS) {
|
||||
copy.srcDevice = frame->payload.cuda_nv12.uv_device_pointer;
|
||||
copy.srcPitch = frame->payload.cuda_nv12.uv_stride;
|
||||
copy.dstDevice = destination + y_size;
|
||||
copy.Height = frame->height / 2;
|
||||
operation = "cuMemcpy2DAsync (UV plane)";
|
||||
result = cuda.memcpy_2d_async(©, buffer->stream);
|
||||
}
|
||||
}
|
||||
if (buffer->mapped) {
|
||||
const CUresult unmap =
|
||||
cuda.unmap_resources(1, &buffer->resource, buffer->stream);
|
||||
buffer->mapped = unmap != CUDA_SUCCESS;
|
||||
if (result == CUDA_SUCCESS) {
|
||||
operation = "cuGraphicsUnmapResources";
|
||||
result = unmap;
|
||||
} else {
|
||||
cuda.Check(unmap, "cuGraphicsUnmapResources (cleanup)");
|
||||
}
|
||||
}
|
||||
const bool copied = cuda.Check(result, operation);
|
||||
const bool popped = context.Pop();
|
||||
buffer->failed = !copied || !popped;
|
||||
return buffer->failed ? -1 : 0;
|
||||
}
|
||||
|
||||
void DestroyCudaOpenGlVideoBuffer(CudaOpenGlVideoBuffer* buffer) {
|
||||
if (!buffer) return;
|
||||
// Keep the frame's device context alive until after the context is popped.
|
||||
std::unique_ptr<CudaOpenGlVideoBuffer> owner(buffer);
|
||||
const auto& cuda = *buffer->driver;
|
||||
ScopedCudaContext context(cuda, buffer->context);
|
||||
if (!context) return;
|
||||
if (buffer->stream) {
|
||||
cuda.Check(cuda.stream_synchronize(buffer->stream),
|
||||
"cuStreamSynchronize (destroy)");
|
||||
}
|
||||
if (buffer->mapped) {
|
||||
cuda.Check(cuda.unmap_resources(
|
||||
1, &buffer->resource, buffer->stream),
|
||||
"cuGraphicsUnmapResources (destroy)");
|
||||
cuda.Check(cuda.stream_synchronize(buffer->stream),
|
||||
"cuStreamSynchronize (unmap)");
|
||||
}
|
||||
if (buffer->resource) {
|
||||
cuda.Check(cuda.unregister_resource(buffer->resource),
|
||||
"cuGraphicsUnregisterResource");
|
||||
}
|
||||
if (buffer->stream) {
|
||||
cuda.Check(cuda.stream_destroy(buffer->stream), "cuStreamDestroy");
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
bool IsCudaDeviceAvailable() { return false; }
|
||||
CudaOpenGlVideoBuffer* CreateCudaOpenGlVideoBuffer(
|
||||
const MiniRtcNativeVideoFrame*, uint32_t) {
|
||||
return nullptr;
|
||||
}
|
||||
int UploadCudaFrameToOpenGlBuffer(
|
||||
CudaOpenGlVideoBuffer*, const MiniRtcNativeVideoFrame*) {
|
||||
return -1;
|
||||
}
|
||||
void DestroyCudaOpenGlVideoBuffer(CudaOpenGlVideoBuffer*) {}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _CUDA_OPENGL_INTEROP_H_
|
||||
#define _CUDA_OPENGL_INTEROP_H_
|
||||
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
bool IsCudaDeviceAvailable();
|
||||
|
||||
struct CudaOpenGlVideoBuffer;
|
||||
|
||||
// These operations require the matching GL context to be current. The caller
|
||||
// owns the GL buffer and must allocate its storage before registering it.
|
||||
CudaOpenGlVideoBuffer* CreateCudaOpenGlVideoBuffer(
|
||||
const MiniRtcNativeVideoFrame* frame, uint32_t gl_buffer);
|
||||
|
||||
// Asynchronous, GPU-only NV12 copy. Retains the source through the next upload
|
||||
// or destruction, including on failure. Before reusing the buffer, the caller
|
||||
// must wait for the previous GL fence. Frames must use the same CUDA context.
|
||||
int UploadCudaFrameToOpenGlBuffer(CudaOpenGlVideoBuffer* buffer,
|
||||
const MiniRtcNativeVideoFrame* frame);
|
||||
|
||||
// Finish prior GL use before calling. Waits for CUDA work and releases retained
|
||||
// frames; the caller may then resize/delete the GL buffer. Accepts null.
|
||||
void DestroyCudaOpenGlVideoBuffer(CudaOpenGlVideoBuffer* buffer);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _NATIVE_VIDEO_FRAME_REF_H_
|
||||
#define _NATIVE_VIDEO_FRAME_REF_H_
|
||||
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class NativeVideoFrameRef {
|
||||
public:
|
||||
NativeVideoFrameRef() = default;
|
||||
|
||||
explicit NativeVideoFrameRef(const MiniRtcNativeVideoFrame *frame) {
|
||||
if (frame && frame->owner && frame->retain && frame->release) {
|
||||
frame_ = *frame;
|
||||
frame_.struct_size = sizeof(frame_);
|
||||
frame_.retain(frame_.owner);
|
||||
valid_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
NativeVideoFrameRef(const NativeVideoFrameRef &other)
|
||||
: NativeVideoFrameRef(other.Get()) {}
|
||||
|
||||
NativeVideoFrameRef(NativeVideoFrameRef &&other) noexcept
|
||||
: frame_(other.frame_), valid_(std::exchange(other.valid_, false)) {
|
||||
other.frame_ = {};
|
||||
}
|
||||
|
||||
NativeVideoFrameRef &operator=(NativeVideoFrameRef other) noexcept {
|
||||
Swap(other);
|
||||
return *this;
|
||||
}
|
||||
|
||||
~NativeVideoFrameRef() { Reset(); }
|
||||
|
||||
void Reset() {
|
||||
if (valid_ && frame_.owner && frame_.release) {
|
||||
frame_.release(frame_.owner);
|
||||
}
|
||||
frame_ = {};
|
||||
valid_ = false;
|
||||
}
|
||||
|
||||
void Swap(NativeVideoFrameRef &other) noexcept {
|
||||
std::swap(frame_, other.frame_);
|
||||
std::swap(valid_, other.valid_);
|
||||
}
|
||||
|
||||
const MiniRtcNativeVideoFrame *Get() const { return valid_ ? &frame_ : nullptr; }
|
||||
explicit operator bool() const { return valid_; }
|
||||
|
||||
private:
|
||||
MiniRtcNativeVideoFrame frame_{};
|
||||
bool valid_ = false;
|
||||
};
|
||||
|
||||
inline const MiniRtcNativeVideoFrame *GetOpenGlNativeFrame(
|
||||
const MiniRtcNativeVideoFrame &frame) {
|
||||
const MiniRtcNativeVideoFrame *native = &frame;
|
||||
if (native->struct_size < static_cast<uint32_t>(sizeof(MiniRtcNativeVideoFrame)) ||
|
||||
!native->owner || !native->retain || !native->release ||
|
||||
!native->copy_to_nv12 || native->width == 0 || native->height == 0 ||
|
||||
(native->width & 1U) != 0 || (native->height & 1U) != 0 ||
|
||||
native->width > static_cast<uint32_t>(std::numeric_limits<int>::max()) ||
|
||||
native->height > static_cast<uint32_t>(std::numeric_limits<int>::max())) {
|
||||
return nullptr;
|
||||
}
|
||||
if (native->type == MiniRtcNativeVideoFrameCpuNv12) {
|
||||
return native->payload.cpu_nv12.y_plane &&
|
||||
native->payload.cpu_nv12.uv_plane &&
|
||||
native->payload.cpu_nv12.y_stride >= native->width &&
|
||||
native->payload.cpu_nv12.uv_stride >= native->width
|
||||
? native
|
||||
: nullptr;
|
||||
}
|
||||
if (native->type == MiniRtcNativeVideoFrameCudaNv12) {
|
||||
return native->payload.cuda_nv12.y_device_pointer != 0 &&
|
||||
native->payload.cuda_nv12.uv_device_pointer != 0 &&
|
||||
native->payload.cuda_nv12.y_stride >= native->width &&
|
||||
native->payload.cuda_nv12.uv_stride >= native->width &&
|
||||
native->payload.cuda_nv12.context
|
||||
? native
|
||||
: nullptr;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
@@ -26,11 +27,24 @@
|
||||
#include <utility>
|
||||
|
||||
#include "rd_log.h"
|
||||
#include "platform/common/gui/cuda_opengl_interop.h"
|
||||
#include "platform/common/gui/native_video_frame_ref.h"
|
||||
#if defined(USE_CUDA) && USE_CUDA && \
|
||||
(defined(_WIN32) || \
|
||||
(defined(__linux__) && (defined(__x86_64__) || defined(__amd64__))))
|
||||
#define CROSSDESK_OPENGL_CUDA_INTEROP 1
|
||||
#else
|
||||
#define CROSSDESK_OPENGL_CUDA_INTEROP 0
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr size_t kFrameSlotCount = 3;
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
constexpr size_t kCudaUploadSlotCount = 3;
|
||||
|
||||
#endif
|
||||
|
||||
enum class SlotUse {
|
||||
available,
|
||||
@@ -40,6 +54,7 @@ enum class SlotUse {
|
||||
|
||||
struct FrameSlot {
|
||||
std::vector<unsigned char> bytes;
|
||||
NativeVideoFrameRef native_frame;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
std::string remote_id;
|
||||
@@ -107,6 +122,11 @@ struct OpenGlFunctions {
|
||||
PFNGLUNIFORM4FPROC uniform_4f = nullptr;
|
||||
PFNGLUSEPROGRAMPROC use_program = nullptr;
|
||||
PFNGLVERTEXATTRIBPOINTERPROC vertex_attrib_pointer = nullptr;
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
PFNGLFENCESYNCPROC fence_sync = nullptr;
|
||||
PFNGLCLIENTWAITSYNCPROC client_wait_sync = nullptr;
|
||||
PFNGLDELETESYNCPROC delete_sync = nullptr;
|
||||
#endif
|
||||
|
||||
bool Load() {
|
||||
return LoadOpenGlFunction(&active_texture, "glActiveTexture") &&
|
||||
@@ -142,6 +162,14 @@ struct OpenGlFunctions {
|
||||
LoadOpenGlFunction(&use_program, "glUseProgram") &&
|
||||
LoadOpenGlFunction(&vertex_attrib_pointer, "glVertexAttribPointer");
|
||||
}
|
||||
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
bool LoadSyncFunctions() {
|
||||
return LoadOpenGlFunction(&fence_sync, "glFenceSync") &&
|
||||
LoadOpenGlFunction(&client_wait_sync, "glClientWaitSync") &&
|
||||
LoadOpenGlFunction(&delete_sync, "glDeleteSync");
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
GLuint CompileShader(const OpenGlFunctions &gl, GLenum type,
|
||||
@@ -176,6 +204,21 @@ GLuint CompileShader(const OpenGlFunctions &gl, GLenum type,
|
||||
|
||||
bool IsEnabled(GLenum capability) { return glIsEnabled(capability) == GL_TRUE; }
|
||||
|
||||
bool HasOpenGlExtension(const char *extensions, const char *extension) {
|
||||
if (!extensions) {
|
||||
return false;
|
||||
}
|
||||
const size_t length = std::strlen(extension);
|
||||
for (const char *match = std::strstr(extensions, extension); match;
|
||||
match = std::strstr(match + length, extension)) {
|
||||
if ((match == extensions || match[-1] == ' ') &&
|
||||
(match[length] == '\0' || match[length] == ' ')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void RestoreCapability(GLenum capability, bool enabled) {
|
||||
if (enabled) {
|
||||
glEnable(capability);
|
||||
@@ -246,9 +289,221 @@ struct OpenGlVideoRenderer::Impl {
|
||||
GLint video_enabled_location = -1;
|
||||
int texture_width = 0;
|
||||
int texture_height = 0;
|
||||
int cpu_texture_width = 0;
|
||||
int cpu_texture_height = 0;
|
||||
bool pixel_unpack_buffer_available = false;
|
||||
bool unpack_subimage_available = false;
|
||||
std::string uploaded_stream;
|
||||
uint64_t uploaded_sequence = 0;
|
||||
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
enum class CudaUploadResult {
|
||||
ready,
|
||||
busy,
|
||||
unavailable,
|
||||
};
|
||||
|
||||
struct CudaUploadSlot {
|
||||
GLuint buffer = 0;
|
||||
GLuint y_texture = 0;
|
||||
GLuint uv_texture = 0;
|
||||
size_t capacity = 0;
|
||||
int texture_width = 0;
|
||||
int texture_height = 0;
|
||||
CudaOpenGlVideoBuffer *resource = nullptr;
|
||||
GLsync fence = nullptr;
|
||||
};
|
||||
|
||||
std::array<CudaUploadSlot, kCudaUploadSlotCount> cuda_upload_slots;
|
||||
size_t next_cuda_upload_slot = 0;
|
||||
size_t displayed_cuda_upload_slot = kCudaUploadSlotCount;
|
||||
void *cuda_context = nullptr;
|
||||
bool cuda_interop_disabled = false;
|
||||
std::atomic<bool> cuda_cpu_fallback{false};
|
||||
bool cuda_native_logged = false;
|
||||
bool cuda_fallback_logged = false;
|
||||
uint64_t cuda_busy_drop_count = 0;
|
||||
|
||||
void DisableCudaInterop() {
|
||||
cuda_interop_disabled = true;
|
||||
cuda_cpu_fallback.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool IsCudaSlotAvailable(CudaUploadSlot &slot) {
|
||||
if (!slot.fence) {
|
||||
return true;
|
||||
}
|
||||
const GLenum wait_result =
|
||||
gl.client_wait_sync(slot.fence, 0, 0);
|
||||
if (wait_result == GL_WAIT_FAILED) {
|
||||
LOG_WARN("CUDA/OpenGL glClientWaitSync failed, error={}", glGetError());
|
||||
DisableCudaInterop();
|
||||
return false;
|
||||
}
|
||||
if (wait_result != GL_ALREADY_SIGNALED &&
|
||||
wait_result != GL_CONDITION_SATISFIED) {
|
||||
return false;
|
||||
}
|
||||
gl.delete_sync(slot.fence);
|
||||
slot.fence = nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ReleaseCudaInterop() {
|
||||
bool has_gl_resources = false;
|
||||
for (const auto &slot : cuda_upload_slots) {
|
||||
has_gl_resources = has_gl_resources || slot.buffer != 0 || slot.fence;
|
||||
}
|
||||
if (has_gl_resources) {
|
||||
glFinish();
|
||||
}
|
||||
for (auto &slot : cuda_upload_slots) {
|
||||
if (slot.fence && gl.delete_sync) {
|
||||
gl.delete_sync(slot.fence);
|
||||
slot.fence = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
cuda_context = nullptr;
|
||||
|
||||
for (auto &slot : cuda_upload_slots) {
|
||||
DestroyCudaOpenGlVideoBuffer(slot.resource);
|
||||
if (slot.buffer != 0 && gl.delete_buffers) {
|
||||
gl.delete_buffers(1, &slot.buffer);
|
||||
}
|
||||
if (slot.y_texture != 0) {
|
||||
glDeleteTextures(1, &slot.y_texture);
|
||||
}
|
||||
if (slot.uv_texture != 0) {
|
||||
glDeleteTextures(1, &slot.uv_texture);
|
||||
}
|
||||
slot = {};
|
||||
}
|
||||
next_cuda_upload_slot = 0;
|
||||
displayed_cuda_upload_slot = kCudaUploadSlotCount;
|
||||
}
|
||||
|
||||
bool PrepareCudaContext(const NativeVideoFrameRef &native_frame) {
|
||||
const MiniRtcNativeVideoFrame *frame = native_frame.Get();
|
||||
if (!frame || !frame->payload.cuda_nv12.context || cuda_interop_disabled) {
|
||||
DisableCudaInterop();
|
||||
return false;
|
||||
}
|
||||
|
||||
void *frame_context = frame->payload.cuda_nv12.context;
|
||||
if (cuda_context && cuda_context != frame_context) {
|
||||
ReleaseCudaInterop();
|
||||
}
|
||||
cuda_context = frame_context;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PrepareCudaSlot(CudaUploadSlot &slot, size_t required_size,
|
||||
const MiniRtcNativeVideoFrame *frame) {
|
||||
if (slot.buffer == 0) {
|
||||
gl.gen_buffers(1, &slot.buffer);
|
||||
}
|
||||
if (slot.buffer == 0) {
|
||||
LOG_WARN("CUDA/OpenGL glGenBuffers failed, error={}", glGetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (slot.capacity < required_size) {
|
||||
DestroyCudaOpenGlVideoBuffer(slot.resource);
|
||||
slot.resource = nullptr;
|
||||
GLint previous_unpack_buffer = 0;
|
||||
glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &previous_unpack_buffer);
|
||||
gl.bind_buffer(GL_PIXEL_UNPACK_BUFFER, slot.buffer);
|
||||
gl.buffer_data(GL_PIXEL_UNPACK_BUFFER,
|
||||
static_cast<GLsizeiptr>(required_size), nullptr,
|
||||
GL_STREAM_DRAW);
|
||||
gl.bind_buffer(GL_PIXEL_UNPACK_BUFFER,
|
||||
static_cast<GLuint>(previous_unpack_buffer));
|
||||
const GLenum buffer_error = glGetError();
|
||||
if (buffer_error != GL_NO_ERROR) {
|
||||
LOG_WARN("CUDA/OpenGL glBufferData failed, error={}", buffer_error);
|
||||
return false;
|
||||
}
|
||||
slot.capacity = required_size;
|
||||
}
|
||||
|
||||
if (!slot.resource) {
|
||||
slot.resource = CreateCudaOpenGlVideoBuffer(frame, slot.buffer);
|
||||
}
|
||||
return slot.resource != nullptr;
|
||||
}
|
||||
|
||||
bool PrepareCudaTextures(CudaUploadSlot &slot) {
|
||||
const bool needs_configuration =
|
||||
slot.y_texture == 0 || slot.uv_texture == 0;
|
||||
if (slot.y_texture == 0) {
|
||||
glGenTextures(1, &slot.y_texture);
|
||||
}
|
||||
if (slot.uv_texture == 0) {
|
||||
glGenTextures(1, &slot.uv_texture);
|
||||
}
|
||||
if (slot.y_texture == 0 || slot.uv_texture == 0) {
|
||||
return false;
|
||||
}
|
||||
if (!needs_configuration) {
|
||||
return true;
|
||||
}
|
||||
for (const auto [unit, texture] :
|
||||
{std::pair{GL_TEXTURE0, slot.y_texture},
|
||||
std::pair{GL_TEXTURE1, slot.uv_texture}}) {
|
||||
gl.active_texture(unit);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
}
|
||||
return glGetError() == GL_NO_ERROR;
|
||||
}
|
||||
|
||||
CudaUploadResult CopyCudaFrameToUploadBuffer(
|
||||
const NativeVideoFrameRef &native_frame, size_t *upload_slot_index) {
|
||||
const MiniRtcNativeVideoFrame *frame = native_frame.Get();
|
||||
if (!upload_slot_index || !PrepareCudaContext(native_frame)) {
|
||||
return CudaUploadResult::unavailable;
|
||||
}
|
||||
|
||||
size_t selected_index = kCudaUploadSlotCount;
|
||||
for (size_t offset = 0; offset < cuda_upload_slots.size(); ++offset) {
|
||||
const size_t index =
|
||||
(next_cuda_upload_slot + offset) % cuda_upload_slots.size();
|
||||
if (IsCudaSlotAvailable(cuda_upload_slots[index])) {
|
||||
selected_index = index;
|
||||
break;
|
||||
}
|
||||
if (cuda_interop_disabled) {
|
||||
return CudaUploadResult::unavailable;
|
||||
}
|
||||
}
|
||||
if (selected_index == kCudaUploadSlotCount) {
|
||||
++cuda_busy_drop_count;
|
||||
if (cuda_busy_drop_count == 1 || cuda_busy_drop_count % 300 == 0) {
|
||||
LOG_WARN("CUDA/OpenGL upload ring busy; dropped {} display frames",
|
||||
cuda_busy_drop_count);
|
||||
}
|
||||
return CudaUploadResult::busy;
|
||||
}
|
||||
|
||||
CudaUploadSlot &slot = cuda_upload_slots[selected_index];
|
||||
const size_t packed_size =
|
||||
static_cast<size_t>(frame->width) * frame->height * 3U / 2U;
|
||||
if (!PrepareCudaSlot(slot, packed_size, frame) ||
|
||||
UploadCudaFrameToOpenGlBuffer(slot.resource, frame) != 0) {
|
||||
DisableCudaInterop();
|
||||
return CudaUploadResult::unavailable;
|
||||
}
|
||||
|
||||
*upload_slot_index = selected_index;
|
||||
next_cuda_upload_slot = (selected_index + 1) % cuda_upload_slots.size();
|
||||
return CudaUploadResult::ready;
|
||||
}
|
||||
#endif
|
||||
|
||||
void ResetGlHandles() {
|
||||
program = 0;
|
||||
vertex_buffer = 0;
|
||||
@@ -264,8 +519,13 @@ struct OpenGlVideoRenderer::Impl {
|
||||
video_enabled_location = -1;
|
||||
texture_width = 0;
|
||||
texture_height = 0;
|
||||
cpu_texture_width = 0;
|
||||
cpu_texture_height = 0;
|
||||
uploaded_stream.clear();
|
||||
uploaded_sequence = 0;
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
displayed_cuda_upload_slot = kCudaUploadSlotCount;
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
@@ -284,6 +544,38 @@ bool OpenGlVideoRenderer::Setup() {
|
||||
LOG_WARN("OpenGL NV12 underlay unavailable: required functions missing");
|
||||
return false;
|
||||
}
|
||||
const auto *version = reinterpret_cast<const char *>(glGetString(GL_VERSION));
|
||||
const bool is_gles = version && std::strstr(version, "OpenGL ES") != nullptr;
|
||||
int major = 0;
|
||||
int minor = 0;
|
||||
if (version) {
|
||||
std::sscanf(is_gles ? version + std::strlen("OpenGL ES ") : version,
|
||||
"%d.%d", &major, &minor);
|
||||
}
|
||||
const auto *extensions = major < 3
|
||||
? reinterpret_cast<const char *>(glGetString(GL_EXTENSIONS)) : nullptr;
|
||||
impl_->pixel_unpack_buffer_available = major >= 3 ||
|
||||
(!is_gles && ((major == 2 && minor >= 1) ||
|
||||
HasOpenGlExtension(extensions, "GL_ARB_pixel_buffer_object")));
|
||||
impl_->unpack_subimage_available = !is_gles || major >= 3 ||
|
||||
HasOpenGlExtension(extensions, "GL_EXT_unpack_subimage");
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
// GLX may return entry points even if the current GLES 2 context cannot
|
||||
// use them. Gate CUDA interop on context capabilities as well as symbols.
|
||||
const bool sync_available = is_gles ? major >= 3
|
||||
: major > 3 || (major == 3 && minor >= 2) ||
|
||||
HasOpenGlExtension(extensions, "GL_ARB_sync");
|
||||
impl_->cuda_interop_disabled = !impl_->pixel_unpack_buffer_available ||
|
||||
!sync_available || !impl_->gl.LoadSyncFunctions();
|
||||
impl_->cuda_cpu_fallback.store(impl_->cuda_interop_disabled,
|
||||
std::memory_order_release);
|
||||
impl_->cuda_native_logged = false;
|
||||
impl_->cuda_fallback_logged = false;
|
||||
impl_->cuda_busy_drop_count = 0;
|
||||
if (impl_->cuda_interop_disabled) {
|
||||
LOG_WARN("CUDA/OpenGL sync functions unavailable; using CPU fallback");
|
||||
}
|
||||
#endif
|
||||
|
||||
static constexpr char kGlesVertexShader[] = R"glsl(
|
||||
#version 100
|
||||
@@ -393,8 +685,6 @@ void main() {
|
||||
}
|
||||
)glsl";
|
||||
|
||||
const auto *version = reinterpret_cast<const char *>(glGetString(GL_VERSION));
|
||||
const bool is_gles = version && std::strstr(version, "OpenGL ES") != nullptr;
|
||||
const char *vertex_source =
|
||||
is_gles ? kGlesVertexShader : kDesktopVertexShader;
|
||||
const char *fragment_source =
|
||||
@@ -517,13 +807,19 @@ void main() {
|
||||
}
|
||||
|
||||
impl_->ready.store(true, std::memory_order_release);
|
||||
LOG_INFO("OpenGL NV12 underlay initialized ({})",
|
||||
version ? version : "unknown OpenGL version");
|
||||
const auto *vendor = reinterpret_cast<const char *>(glGetString(GL_VENDOR));
|
||||
const auto *renderer = reinterpret_cast<const char *>(glGetString(GL_RENDERER));
|
||||
LOG_INFO("OpenGL NV12 underlay initialized ({}), vendor={}, renderer={}",
|
||||
version ? version : "unknown OpenGL version",
|
||||
vendor ? vendor : "unknown", renderer ? renderer : "unknown");
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenGlVideoRenderer::Teardown() {
|
||||
impl_->ready.store(false, std::memory_order_release);
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
impl_->ReleaseCudaInterop();
|
||||
#endif
|
||||
if (impl_->y_texture != 0)
|
||||
glDeleteTextures(1, &impl_->y_texture);
|
||||
if (impl_->uv_texture != 0)
|
||||
@@ -534,6 +830,14 @@ void OpenGlVideoRenderer::Teardown() {
|
||||
if (impl_->program != 0 && impl_->gl.delete_program) {
|
||||
impl_->gl.delete_program(impl_->program);
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(impl_->frames->mutex);
|
||||
for (auto &slot : impl_->frames->slots) {
|
||||
slot.native_frame.Reset();
|
||||
slot.valid = false;
|
||||
slot.use = SlotUse::available;
|
||||
}
|
||||
}
|
||||
impl_->ResetGlHandles();
|
||||
}
|
||||
|
||||
@@ -553,6 +857,8 @@ bool OpenGlVideoRenderer::SetSelectedStream(std::string remote_id) {
|
||||
if (slot.use == SlotUse::pending &&
|
||||
slot.remote_id != impl_->frames->selected_stream) {
|
||||
slot.use = SlotUse::available;
|
||||
slot.valid = false;
|
||||
slot.native_frame.Reset();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -565,6 +871,7 @@ void OpenGlVideoRenderer::DiscardStream(std::string_view remote_id) {
|
||||
slot.valid = false;
|
||||
slot.use = SlotUse::available;
|
||||
slot.remote_id.clear();
|
||||
slot.native_frame.Reset();
|
||||
}
|
||||
}
|
||||
if (impl_->frames->selected_stream == remote_id) {
|
||||
@@ -648,6 +955,7 @@ OpenGlVideoRenderer::SubmitNv12Internal(std::string_view remote_id,
|
||||
|
||||
target->bytes.resize(required_size);
|
||||
std::memcpy(target->bytes.data(), data, required_size);
|
||||
target->native_frame.Reset();
|
||||
target->width = width;
|
||||
target->height = height;
|
||||
target->remote_id.assign(remote_id);
|
||||
@@ -657,6 +965,85 @@ OpenGlVideoRenderer::SubmitNv12Internal(std::string_view remote_id,
|
||||
return SubmitResult::submitted;
|
||||
}
|
||||
|
||||
OpenGlVideoRenderer::SubmitResult
|
||||
OpenGlVideoRenderer::SubmitNativeFrame(std::string_view remote_id,
|
||||
const MiniRtcNativeVideoFrame &frame) {
|
||||
if (!IsReady()) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
const MiniRtcNativeVideoFrame *native = GetOpenGlNativeFrame(frame);
|
||||
if (!native) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
|
||||
auto &frames = *impl_->frames;
|
||||
std::lock_guard lock(frames.mutex);
|
||||
if (!IsReady()) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
if (frames.selected_stream != remote_id) {
|
||||
return SubmitResult::not_selected;
|
||||
}
|
||||
|
||||
// Latest-frame-only queue: replacing an unconsumed decoded frame bounds
|
||||
// latency without dropping compressed delta frames before decode.
|
||||
FrameSlot *target = nullptr;
|
||||
for (auto &slot : frames.slots) {
|
||||
if (slot.use == SlotUse::pending) {
|
||||
target = &slot;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!target) {
|
||||
uint64_t oldest_sequence = std::numeric_limits<uint64_t>::max();
|
||||
for (auto &slot : frames.slots) {
|
||||
if (slot.use != SlotUse::available) {
|
||||
continue;
|
||||
}
|
||||
if (!slot.valid) {
|
||||
target = &slot;
|
||||
break;
|
||||
}
|
||||
if (slot.sequence < oldest_sequence) {
|
||||
oldest_sequence = slot.sequence;
|
||||
target = &slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!target) {
|
||||
return SubmitResult::dropped;
|
||||
}
|
||||
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
if (native->type == MiniRtcNativeVideoFrameCudaNv12 &&
|
||||
impl_->cuda_cpu_fallback.load(std::memory_order_acquire)) {
|
||||
const size_t required_size =
|
||||
static_cast<size_t>(native->width) * native->height * 3U / 2U;
|
||||
target->bytes.resize(required_size);
|
||||
if (native->copy_to_nv12(native->owner, target->bytes.data(),
|
||||
target->bytes.size()) != 0) {
|
||||
target->bytes.clear();
|
||||
target->native_frame.Reset();
|
||||
target->valid = false;
|
||||
target->use = SlotUse::available;
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
target->native_frame.Reset();
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
target->bytes.clear();
|
||||
target->native_frame = NativeVideoFrameRef(native);
|
||||
}
|
||||
target->width = static_cast<int>(native->width);
|
||||
target->height = static_cast<int>(native->height);
|
||||
target->remote_id.assign(remote_id);
|
||||
target->sequence = frames.next_sequence++;
|
||||
target->use = SlotUse::pending;
|
||||
target->valid = true;
|
||||
return SubmitResult::submitted;
|
||||
}
|
||||
|
||||
OpenGlVideoRenderer::RenderOutcome
|
||||
OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
int target_width, int target_height,
|
||||
@@ -670,7 +1057,9 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
uint64_t sequence = 0;
|
||||
int source_width = 0;
|
||||
int source_height = 0;
|
||||
const unsigned char *frame_data = nullptr;
|
||||
const unsigned char *y_data = nullptr;
|
||||
const unsigned char *uv_data = nullptr;
|
||||
NativeVideoFrameRef active_native_frame;
|
||||
{
|
||||
std::lock_guard lock(impl_->frames->mutex);
|
||||
for (size_t index = 0; index < impl_->frames->slots.size(); ++index) {
|
||||
@@ -687,12 +1076,19 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
selected.use = SlotUse::uploading;
|
||||
source_width = selected.width;
|
||||
source_height = selected.height;
|
||||
frame_data = selected.bytes.data();
|
||||
active_native_frame = selected.native_frame;
|
||||
if (!selected.bytes.empty()) {
|
||||
const size_t y_size = static_cast<size_t>(source_width) * source_height;
|
||||
y_data = selected.bytes.data();
|
||||
uv_data = selected.bytes.data() + y_size;
|
||||
}
|
||||
for (size_t index = 0; index < impl_->frames->slots.size(); ++index) {
|
||||
auto &slot = impl_->frames->slots[index];
|
||||
if (index != slot_index && slot.use == SlotUse::pending &&
|
||||
slot.remote_id == remote_id) {
|
||||
slot.use = SlotUse::available;
|
||||
slot.valid = false;
|
||||
slot.native_frame.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -701,7 +1097,11 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
GLint previous_program = 0;
|
||||
GLint previous_active_texture = GL_TEXTURE0;
|
||||
GLint previous_array_buffer = 0;
|
||||
GLint previous_unpack_buffer = 0;
|
||||
GLint previous_unpack_alignment = 4;
|
||||
GLint previous_unpack_row_length = 0;
|
||||
GLint previous_unpack_skip_rows = 0;
|
||||
GLint previous_unpack_skip_pixels = 0;
|
||||
GLint previous_viewport[4] = {};
|
||||
GLint previous_scissor_box[4] = {};
|
||||
GLfloat previous_clear_color[4] = {};
|
||||
@@ -709,6 +1109,18 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
glGetIntegerv(GL_CURRENT_PROGRAM, &previous_program);
|
||||
glGetIntegerv(GL_ACTIVE_TEXTURE, &previous_active_texture);
|
||||
glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &previous_array_buffer);
|
||||
if (impl_->pixel_unpack_buffer_available) {
|
||||
glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &previous_unpack_buffer);
|
||||
impl_->gl.bind_buffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
}
|
||||
if (impl_->unpack_subimage_available) {
|
||||
glGetIntegerv(GL_UNPACK_ROW_LENGTH, &previous_unpack_row_length);
|
||||
glGetIntegerv(GL_UNPACK_SKIP_ROWS, &previous_unpack_skip_rows);
|
||||
glGetIntegerv(GL_UNPACK_SKIP_PIXELS, &previous_unpack_skip_pixels);
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
|
||||
glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
|
||||
}
|
||||
glGetIntegerv(GL_UNPACK_ALIGNMENT, &previous_unpack_alignment);
|
||||
glGetIntegerv(GL_VIEWPORT, previous_viewport);
|
||||
glGetIntegerv(GL_SCISSOR_BOX, previous_scissor_box);
|
||||
@@ -750,38 +1162,145 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
const size_t y_size = static_cast<size_t>(source_width) * source_height;
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, impl_->y_texture);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, impl_->uv_texture);
|
||||
if (impl_->texture_width != source_width ||
|
||||
impl_->texture_height != source_height) {
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, source_width, source_height,
|
||||
0, GL_LUMINANCE, GL_UNSIGNED_BYTE, frame_data);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, source_width / 2,
|
||||
source_height / 2, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE,
|
||||
frame_data + y_size);
|
||||
} else {
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, source_width, source_height,
|
||||
GL_LUMINANCE, GL_UNSIGNED_BYTE, frame_data);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, source_width / 2,
|
||||
source_height / 2, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE,
|
||||
frame_data + y_size);
|
||||
const size_t required_size = y_size + y_size / 2U;
|
||||
bool should_upload = true;
|
||||
bool use_pixel_unpack_buffer = false;
|
||||
size_t cuda_upload_slot = 0;
|
||||
std::vector<unsigned char> cpu_fallback;
|
||||
if (const MiniRtcNativeVideoFrame *native = active_native_frame.Get()) {
|
||||
if (native->type == MiniRtcNativeVideoFrameCpuNv12 &&
|
||||
native->payload.cpu_nv12.y_stride == native->width &&
|
||||
native->payload.cpu_nv12.uv_stride == native->width) {
|
||||
y_data = native->payload.cpu_nv12.y_plane;
|
||||
uv_data = native->payload.cpu_nv12.uv_plane;
|
||||
} else if (native->type == MiniRtcNativeVideoFrameCudaNv12) {
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
const auto cuda_result = impl_->CopyCudaFrameToUploadBuffer(
|
||||
active_native_frame, &cuda_upload_slot);
|
||||
use_pixel_unpack_buffer =
|
||||
cuda_result == OpenGlVideoRenderer::Impl::CudaUploadResult::ready;
|
||||
should_upload =
|
||||
cuda_result != OpenGlVideoRenderer::Impl::CudaUploadResult::busy;
|
||||
if (use_pixel_unpack_buffer && !impl_->cuda_native_logged) {
|
||||
LOG_INFO("Native NV12 using asynchronous CUDA/OpenGL ring");
|
||||
impl_->cuda_native_logged = true;
|
||||
}
|
||||
if (should_upload && !use_pixel_unpack_buffer &&
|
||||
!impl_->cuda_fallback_logged) {
|
||||
LOG_WARN("CUDA/OpenGL native upload unavailable; using CPU fallback");
|
||||
impl_->cuda_fallback_logged = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (should_upload && !use_pixel_unpack_buffer && (!y_data || !uv_data)) {
|
||||
cpu_fallback.resize(required_size);
|
||||
if (native->copy_to_nv12(native->owner, cpu_fallback.data(),
|
||||
cpu_fallback.size()) == 0) {
|
||||
y_data = cpu_fallback.data();
|
||||
uv_data = cpu_fallback.data() + y_size;
|
||||
} else {
|
||||
upload_succeeded = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
const GLenum upload_error = glGetError();
|
||||
upload_succeeded = upload_error == GL_NO_ERROR;
|
||||
if (upload_succeeded) {
|
||||
impl_->texture_width = source_width;
|
||||
impl_->texture_height = source_height;
|
||||
impl_->uploaded_stream.assign(remote_id);
|
||||
impl_->uploaded_sequence = sequence;
|
||||
} else {
|
||||
LOG_WARN("OpenGL NV12 texture upload failed, error={}", upload_error);
|
||||
if (should_upload && !use_pixel_unpack_buffer && (!y_data || !uv_data)) {
|
||||
upload_succeeded = false;
|
||||
}
|
||||
|
||||
GLuint upload_y_texture = impl_->y_texture;
|
||||
GLuint upload_uv_texture = impl_->uv_texture;
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
if (should_upload && upload_succeeded && use_pixel_unpack_buffer) {
|
||||
auto &cuda_slot = impl_->cuda_upload_slots[cuda_upload_slot];
|
||||
upload_succeeded = impl_->PrepareCudaTextures(cuda_slot);
|
||||
upload_y_texture = cuda_slot.y_texture;
|
||||
upload_uv_texture = cuda_slot.uv_texture;
|
||||
if (!upload_succeeded) {
|
||||
impl_->DisableCudaInterop();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (should_upload && upload_succeeded) {
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, upload_y_texture);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, upload_uv_texture);
|
||||
if (use_pixel_unpack_buffer) {
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
impl_->gl.bind_buffer(
|
||||
GL_PIXEL_UNPACK_BUFFER,
|
||||
impl_->cuda_upload_slots[cuda_upload_slot].buffer);
|
||||
#endif
|
||||
}
|
||||
const void *y_pixels = use_pixel_unpack_buffer
|
||||
? nullptr
|
||||
: static_cast<const void *>(y_data);
|
||||
const void *uv_pixels =
|
||||
use_pixel_unpack_buffer
|
||||
? reinterpret_cast<const void *>(static_cast<uintptr_t>(y_size))
|
||||
: static_cast<const void *>(uv_data);
|
||||
int uploaded_texture_width = impl_->cpu_texture_width;
|
||||
int uploaded_texture_height = impl_->cpu_texture_height;
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
if (use_pixel_unpack_buffer) {
|
||||
uploaded_texture_width =
|
||||
impl_->cuda_upload_slots[cuda_upload_slot].texture_width;
|
||||
uploaded_texture_height =
|
||||
impl_->cuda_upload_slots[cuda_upload_slot].texture_height;
|
||||
}
|
||||
#endif
|
||||
if (uploaded_texture_width != source_width ||
|
||||
uploaded_texture_height != source_height) {
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, source_width,
|
||||
source_height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE,
|
||||
y_pixels);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, source_width / 2,
|
||||
source_height / 2, 0, GL_LUMINANCE_ALPHA,
|
||||
GL_UNSIGNED_BYTE, uv_pixels);
|
||||
} else {
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, source_width, source_height,
|
||||
GL_LUMINANCE, GL_UNSIGNED_BYTE, y_pixels);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, source_width / 2,
|
||||
source_height / 2, GL_LUMINANCE_ALPHA,
|
||||
GL_UNSIGNED_BYTE, uv_pixels);
|
||||
}
|
||||
const GLenum upload_error = glGetError();
|
||||
upload_succeeded = upload_error == GL_NO_ERROR;
|
||||
if (upload_succeeded) {
|
||||
impl_->texture_width = source_width;
|
||||
impl_->texture_height = source_height;
|
||||
impl_->uploaded_stream.assign(remote_id);
|
||||
impl_->uploaded_sequence = sequence;
|
||||
if (!use_pixel_unpack_buffer) {
|
||||
impl_->cpu_texture_width = source_width;
|
||||
impl_->cpu_texture_height = source_height;
|
||||
}
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
if (use_pixel_unpack_buffer) {
|
||||
auto &cuda_slot = impl_->cuda_upload_slots[cuda_upload_slot];
|
||||
cuda_slot.texture_width = source_width;
|
||||
cuda_slot.texture_height = source_height;
|
||||
impl_->displayed_cuda_upload_slot = cuda_upload_slot;
|
||||
} else {
|
||||
impl_->displayed_cuda_upload_slot = kCudaUploadSlotCount;
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
LOG_WARN("OpenGL NV12 texture upload failed, error={}", upload_error);
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
if (use_pixel_unpack_buffer) {
|
||||
// No draw fence will cover this failed upload. The upload handle retains its source
|
||||
// until the upload handle is destroyed.
|
||||
impl_->DisableCudaInterop();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,12 +1345,22 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
GLuint displayed_y_texture = impl_->y_texture;
|
||||
GLuint displayed_uv_texture = impl_->uv_texture;
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
if (impl_->displayed_cuda_upload_slot < impl_->cuda_upload_slots.size()) {
|
||||
const auto &cuda_slot =
|
||||
impl_->cuda_upload_slots[impl_->displayed_cuda_upload_slot];
|
||||
displayed_y_texture = cuda_slot.y_texture;
|
||||
displayed_uv_texture = cuda_slot.uv_texture;
|
||||
}
|
||||
#endif
|
||||
impl_->gl.use_program(impl_->program);
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, impl_->y_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, displayed_y_texture);
|
||||
impl_->gl.uniform_1i(impl_->y_texture_location, 0);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, impl_->uv_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, displayed_uv_texture);
|
||||
impl_->gl.uniform_1i(impl_->uv_texture_location, 1);
|
||||
impl_->gl.uniform_2f(impl_->target_size_location,
|
||||
static_cast<GLfloat>(target_width),
|
||||
@@ -858,6 +1387,21 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
impl_->gl.enable_vertex_attrib_array(
|
||||
static_cast<GLuint>(impl_->texcoord_location));
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
if (impl_->displayed_cuda_upload_slot < impl_->cuda_upload_slots.size()) {
|
||||
auto &cuda_slot =
|
||||
impl_->cuda_upload_slots[impl_->displayed_cuda_upload_slot];
|
||||
if (cuda_slot.fence) {
|
||||
impl_->gl.delete_sync(cuda_slot.fence);
|
||||
}
|
||||
cuda_slot.fence =
|
||||
impl_->gl.fence_sync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
if (!cuda_slot.fence) {
|
||||
glFinish();
|
||||
impl_->DisableCudaInterop();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
const GLenum draw_error = glGetError();
|
||||
RenderOutcome outcome{
|
||||
!upload_succeeded || draw_error != GL_NO_ERROR
|
||||
@@ -872,6 +1416,15 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
texcoord_state);
|
||||
impl_->gl.bind_buffer(GL_ARRAY_BUFFER,
|
||||
static_cast<GLuint>(previous_array_buffer));
|
||||
if (impl_->pixel_unpack_buffer_available) {
|
||||
impl_->gl.bind_buffer(GL_PIXEL_UNPACK_BUFFER,
|
||||
static_cast<GLuint>(previous_unpack_buffer));
|
||||
}
|
||||
if (impl_->unpack_subimage_available) {
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, previous_unpack_row_length);
|
||||
glPixelStorei(GL_UNPACK_SKIP_ROWS, previous_unpack_skip_rows);
|
||||
glPixelStorei(GL_UNPACK_SKIP_PIXELS, previous_unpack_skip_pixels);
|
||||
}
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture_0));
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
@@ -908,21 +1461,38 @@ bool OpenGlVideoRenderer::CopyLatestNv12(
|
||||
if (!output || !width || !height) {
|
||||
return false;
|
||||
}
|
||||
std::lock_guard lock(impl_->frames->mutex);
|
||||
const FrameSlot *newest = nullptr;
|
||||
for (const auto &slot : impl_->frames->slots) {
|
||||
if (slot.valid && slot.remote_id == remote_id &&
|
||||
(!newest || slot.sequence > newest->sequence)) {
|
||||
newest = &slot;
|
||||
std::vector<unsigned char> cpu_frame;
|
||||
NativeVideoFrameRef native_frame;
|
||||
{
|
||||
std::lock_guard lock(impl_->frames->mutex);
|
||||
const FrameSlot *newest = nullptr;
|
||||
for (const auto &slot : impl_->frames->slots) {
|
||||
if (slot.valid && slot.remote_id == remote_id &&
|
||||
(!newest || slot.sequence > newest->sequence)) {
|
||||
newest = &slot;
|
||||
}
|
||||
}
|
||||
if (!newest) {
|
||||
return false;
|
||||
}
|
||||
*width = newest->width;
|
||||
*height = newest->height;
|
||||
cpu_frame = newest->bytes;
|
||||
native_frame = newest->native_frame;
|
||||
}
|
||||
if (!newest || newest->bytes.empty()) {
|
||||
return false;
|
||||
|
||||
if (!cpu_frame.empty()) {
|
||||
*output = std::move(cpu_frame);
|
||||
return true;
|
||||
}
|
||||
*output = newest->bytes;
|
||||
*width = newest->width;
|
||||
*height = newest->height;
|
||||
return true;
|
||||
if (const MiniRtcNativeVideoFrame *native = native_frame.Get()) {
|
||||
const size_t required_size =
|
||||
static_cast<size_t>(native->width) * native->height * 3U / 2U;
|
||||
output->resize(required_size);
|
||||
return native->copy_to_nv12(native->owner, output->data(),
|
||||
output->size()) == 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
namespace crossdesk {
|
||||
|
||||
// NV12 underlay for Slint's FemtoVG OpenGL renderer on Windows and Linux.
|
||||
// MiniRTC submits tightly packed CPU NV12 frames from its decode callback
|
||||
// thread. The Slint UI thread uploads the newest frame as Y and UV textures and
|
||||
// performs color conversion and scaling in a fragment shader before Slint
|
||||
// draws its overlay.
|
||||
// MiniRTC submits cross-platform native NV12 descriptors or tightly packed CPU
|
||||
// fallback frames from its decode callback thread. CUDA frames rotate through
|
||||
// fenced OpenGL upload resources; CPU descriptors expose their planes without
|
||||
// an intermediate queue copy. The UI thread always consumes the newest frame.
|
||||
class OpenGlVideoRenderer final : public VideoRenderer {
|
||||
public:
|
||||
OpenGlVideoRenderer();
|
||||
@@ -42,6 +42,8 @@ public:
|
||||
size_t size, int width, int height) override;
|
||||
SubmitResult SubmitCachedNv12(std::string_view remote_id, const uint8_t *data,
|
||||
size_t size, int width, int height) override;
|
||||
SubmitResult SubmitNativeFrame(std::string_view remote_id,
|
||||
const MiniRtcNativeVideoFrame &frame) override;
|
||||
|
||||
// Draws below Slint while its OpenGL context is current. All dimensions are
|
||||
// physical pixels in the window client area. corner_radius_pixels clips the
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "platform/linux/gui/cuda_graphics.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <initializer_list>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
#include "platform/common/gui/cuda_opengl_interop.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
void ConfigureLinuxCudaGraphics(bool hardware_acceleration) {
|
||||
#if USE_CUDA && (defined(__x86_64__) || defined(__amd64__))
|
||||
if (!hardware_acceleration) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Respect explicit GPU/driver selection, including a request to use Mesa
|
||||
// or software rendering. Native frames can still use the CPU fallback.
|
||||
for (const char* name : {"__NV_PRIME_RENDER_OFFLOAD",
|
||||
"__NV_PRIME_RENDER_OFFLOAD_PROVIDER",
|
||||
"__GLX_VENDOR_LIBRARY_NAME",
|
||||
"__EGL_VENDOR_LIBRARY_FILENAMES",
|
||||
"__EGL_VENDOR_LIBRARY_DIRS", "DRI_PRIME",
|
||||
"LIBGL_ALWAYS_SOFTWARE",
|
||||
"MESA_LOADER_DRIVER_OVERRIDE"}) {
|
||||
const char* value = std::getenv(name);
|
||||
if (value && value[0] != '\0') {
|
||||
LOG_INFO("Keeping explicit Linux graphics selection: {}={}", name,
|
||||
value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsCudaDeviceAvailable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Both EGL and GLX need to render on NVIDIA to share CUDA frame buffers.
|
||||
// These are process-local, not desktop settings.
|
||||
// https://download.nvidia.com/XFree86/Linux-x86_64/595.45.04/README/primerenderoffload.html
|
||||
if (setenv("__NV_PRIME_RENDER_OFFLOAD", "1", 1) != 0 ||
|
||||
setenv("__GLX_VENDOR_LIBRARY_NAME", "nvidia", 1) != 0) {
|
||||
LOG_WARN("Unable to select NVIDIA PRIME rendering for CUDA video");
|
||||
return;
|
||||
}
|
||||
LOG_INFO("Selected NVIDIA PRIME rendering for CUDA/OpenGL video interop");
|
||||
#else
|
||||
(void)hardware_acceleration;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _CUDA_GRAPHICS_H_
|
||||
#define _CUDA_GRAPHICS_H_
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Call before SDL or Slint initializes EGL/GLX. Hybrid laptops otherwise
|
||||
// create the GUI context on the integrated GPU while NVDEC uses NVIDIA.
|
||||
void ConfigureLinuxCudaGraphics(bool hardware_acceleration);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -392,7 +392,10 @@ struct LinuxTrayImpl {
|
||||
exit_menu_label(GetExitMenuLabel(language_index_value)),
|
||||
exit_menu_ascii_label(IsAsciiPrintable(exit_menu_label)
|
||||
? exit_menu_label
|
||||
: "Exit") {}
|
||||
: "Exit") {
|
||||
|
||||
EnsureTrayIcon();
|
||||
}
|
||||
|
||||
explicit LinuxTrayImpl(std::function<void()> show_window_callback,
|
||||
std::function<void()> hide_window_callback,
|
||||
@@ -416,7 +419,9 @@ struct LinuxTrayImpl {
|
||||
exit_menu_label(GetExitMenuLabel(language_index_value)),
|
||||
exit_menu_ascii_label(IsAsciiPrintable(exit_menu_label)
|
||||
? exit_menu_label
|
||||
: "Exit") {}
|
||||
: "Exit") {
|
||||
EnsureTrayIcon();
|
||||
}
|
||||
|
||||
~LinuxTrayImpl() { RemoveTrayIcon(); }
|
||||
|
||||
|
||||
@@ -429,7 +429,7 @@ bool ScreenCapturerDrm::CaptureOutputFrame(const DrmOutput& output,
|
||||
|
||||
if (emit_callback && callback_) {
|
||||
callback_(nv12.data(), static_cast<int>(nv12.size()), capture_width,
|
||||
capture_height, output.stream_id.c_str());
|
||||
capture_height, output.stream_id.c_str(), nullptr);
|
||||
}
|
||||
|
||||
UnmapFramebuffer(mapped_ptr, mapped_size, prime_fd);
|
||||
|
||||
@@ -50,7 +50,8 @@ int ScreenCapturerLinux::Init(const int fps, cb_desktop_data cb) {
|
||||
fps_ = fps;
|
||||
callback_orig_ = std::move(cb);
|
||||
callback_ = [this](unsigned char* data, int size, int width, int height,
|
||||
const char* reported_stream_id) {
|
||||
const char* reported_stream_id,
|
||||
const MiniRtcNativeVideoFrame* native_frame) {
|
||||
const std::string mapped_stream_id = MapStreamId(reported_stream_id);
|
||||
if (mapped_stream_id.empty()) {
|
||||
if (!invalid_stream_id_logged_.exchange(true,
|
||||
@@ -64,7 +65,8 @@ int ScreenCapturerLinux::Init(const int fps, cb_desktop_data cb) {
|
||||
}
|
||||
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
|
||||
if (callback_orig_) {
|
||||
callback_orig_(data, size, width, height, mapped_stream_id.c_str());
|
||||
callback_orig_(data, size, width, height, mapped_stream_id.c_str(),
|
||||
native_frame);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -1047,7 +1047,7 @@ void ScreenCapturerWayland::HandlePipeWireBuffer() {
|
||||
if (callback_) {
|
||||
const std::string stream_id = MakeDisplayStreamId(0);
|
||||
callback_(nv12.data(), static_cast<int>(nv12.size()), even_width,
|
||||
even_height, stream_id.c_str());
|
||||
even_height, stream_id.c_str(), nullptr);
|
||||
}
|
||||
pipewire_last_frame_ms_.store(NowMs());
|
||||
|
||||
|
||||
@@ -321,7 +321,7 @@ void ScreenCapturerX11::OnFrame() {
|
||||
if (callback_) {
|
||||
const std::string stream_id = MakeDisplayStreamId(monitor_index);
|
||||
callback_(nv12.data(), width_ * height_ * 3 / 2, width_, height_,
|
||||
stream_id.c_str());
|
||||
stream_id.c_str(), nullptr);
|
||||
}
|
||||
|
||||
XDestroyImage(image);
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// macOS video compositor used by CrossDesk only. MiniRTC continues to expose
|
||||
// tightly packed CPU NV12 frames; this renderer performs the single required
|
||||
// copy into shared Metal buffers and converts NV12 to RGB in the fragment
|
||||
// shader. AppKit attachment and rendering are main-thread operations, while
|
||||
// SubmitNv12() is safe to call from MiniRTC's decode callback thread.
|
||||
// macOS video compositor used by CrossDesk only. VideoToolbox CVPixelBuffers
|
||||
// are sampled directly through CVMetalTextureCache; packed CPU NV12 remains a
|
||||
// fallback for software decoding and cached snapshots. AppKit attachment and
|
||||
// rendering are main-thread operations, while frame submission is safe from
|
||||
// MiniRTC's decode callback thread.
|
||||
class MacMetalVideoRenderer final : public VideoRenderer {
|
||||
public:
|
||||
MacMetalVideoRenderer();
|
||||
@@ -37,6 +37,11 @@ class MacMetalVideoRenderer final : public VideoRenderer {
|
||||
SubmitResult SubmitNv12(std::string_view remote_id, const uint8_t* data,
|
||||
size_t size, int width, int height) override;
|
||||
|
||||
// Retains an IOSurface-backed VideoToolbox CVPixelBuffer until the Metal
|
||||
// command buffer using it has completed.
|
||||
SubmitResult SubmitNativeFrame(std::string_view remote_id,
|
||||
const MiniRtcNativeVideoFrame& frame) override;
|
||||
|
||||
// Seeds a newly selected stream from its retained CPU snapshot without
|
||||
// replacing a newer decoded frame that is already queued or rendering.
|
||||
SubmitResult SubmitCachedNv12(std::string_view remote_id,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "platform/macos/gui/metal_video_renderer.h"
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <CoreVideo/CVMetalTextureCache.h>
|
||||
#import <Metal/Metal.h>
|
||||
#import <QuartzCore/CAMetalLayer.h>
|
||||
|
||||
@@ -194,6 +195,74 @@ namespace {
|
||||
|
||||
constexpr size_t kFrameSlotCount = 3;
|
||||
|
||||
class NativeVideoFrameRef {
|
||||
public:
|
||||
NativeVideoFrameRef() = default;
|
||||
|
||||
explicit NativeVideoFrameRef(const MiniRtcNativeVideoFrame* frame) {
|
||||
if (frame && frame->owner && frame->retain && frame->release) {
|
||||
frame_ = *frame;
|
||||
frame_.struct_size = sizeof(frame_);
|
||||
frame_.retain(frame_.owner);
|
||||
valid_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
NativeVideoFrameRef(const NativeVideoFrameRef& other)
|
||||
: NativeVideoFrameRef(other.Get()) {}
|
||||
|
||||
NativeVideoFrameRef(NativeVideoFrameRef&& other) noexcept
|
||||
: frame_(other.frame_), valid_(std::exchange(other.valid_, false)) {
|
||||
other.frame_ = {};
|
||||
}
|
||||
|
||||
NativeVideoFrameRef& operator=(NativeVideoFrameRef other) noexcept {
|
||||
Swap(other);
|
||||
return *this;
|
||||
}
|
||||
|
||||
~NativeVideoFrameRef() { Reset(); }
|
||||
|
||||
void Reset() {
|
||||
if (valid_ && frame_.owner && frame_.release) {
|
||||
frame_.release(frame_.owner);
|
||||
}
|
||||
frame_ = {};
|
||||
valid_ = false;
|
||||
}
|
||||
|
||||
void Swap(NativeVideoFrameRef& other) noexcept {
|
||||
std::swap(frame_, other.frame_);
|
||||
std::swap(valid_, other.valid_);
|
||||
}
|
||||
|
||||
const MiniRtcNativeVideoFrame* Get() const {
|
||||
return valid_ ? &frame_ : nullptr;
|
||||
}
|
||||
explicit operator bool() const { return valid_; }
|
||||
|
||||
private:
|
||||
MiniRtcNativeVideoFrame frame_{};
|
||||
bool valid_ = false;
|
||||
};
|
||||
|
||||
const MiniRtcNativeVideoFrame* GetMacNativeFrame(
|
||||
const MiniRtcNativeVideoFrame& frame) {
|
||||
const MiniRtcNativeVideoFrame* native = &frame;
|
||||
if (native->struct_size < static_cast<uint32_t>(sizeof(*native)) ||
|
||||
native->type != MiniRtcNativeVideoFrameCVPixelBuffer ||
|
||||
!native->payload.cv_pixel_buffer || !native->owner || !native->retain ||
|
||||
!native->release || !native->copy_to_nv12 || native->width == 0 ||
|
||||
native->height == 0 || (native->width & 1U) != 0 ||
|
||||
(native->height & 1U) != 0 ||
|
||||
native->width > static_cast<uint32_t>(std::numeric_limits<int>::max()) ||
|
||||
native->height >
|
||||
static_cast<uint32_t>(std::numeric_limits<int>::max())) {
|
||||
return nullptr;
|
||||
}
|
||||
return native;
|
||||
}
|
||||
|
||||
// Slint's winit backend creates AppKit windows with transparent backing so
|
||||
// transparent Slint items can be composited. Keep the content out of the
|
||||
// titlebar and choose whether AppKit's material or the semantic window color
|
||||
@@ -262,6 +331,9 @@ struct FrameSlot {
|
||||
id<MTLBuffer> uv_buffer = nil;
|
||||
id<MTLTexture> y_texture = nil;
|
||||
id<MTLTexture> uv_texture = nil;
|
||||
CVMetalTextureRef y_cv_texture = nullptr;
|
||||
CVMetalTextureRef uv_cv_texture = nullptr;
|
||||
NativeVideoFrameRef native_frame;
|
||||
size_t y_stride = 0;
|
||||
size_t uv_stride = 0;
|
||||
int width = 0;
|
||||
@@ -274,6 +346,15 @@ struct FrameSlot {
|
||||
void ReleaseResources() {
|
||||
y_texture = nil;
|
||||
uv_texture = nil;
|
||||
if (y_cv_texture) {
|
||||
CFRelease(y_cv_texture);
|
||||
y_cv_texture = nullptr;
|
||||
}
|
||||
if (uv_cv_texture) {
|
||||
CFRelease(uv_cv_texture);
|
||||
uv_cv_texture = nullptr;
|
||||
}
|
||||
native_frame.Reset();
|
||||
y_buffer = nil;
|
||||
uv_buffer = nil;
|
||||
y_stride = 0;
|
||||
@@ -390,7 +471,7 @@ fragment half4 nv12_fragment(
|
||||
- 16.0 / 255.0);
|
||||
const float2 uv = uv_texture.sample(video_sampler, in.texcoord).rg - 0.5;
|
||||
// BT.601 limited-range NV12, matching the previous libyuv NV12ToABGR/RAW
|
||||
// path. MiniRTC's current XVideoFrame API does not carry color metadata, so
|
||||
// path. MiniRTC's current MiniRtcVideoFrame API does not carry color metadata, so
|
||||
// retaining the established matrix avoids a platform-only color change.
|
||||
const float3 rgb = float3(y + 1.59602678 * uv.y,
|
||||
y - 0.39176229 * uv.x - 0.81296764 * uv.y,
|
||||
@@ -406,6 +487,7 @@ struct MacMetalVideoRenderer::Impl {
|
||||
id<MTLDevice> device = nil;
|
||||
id<MTLCommandQueue> command_queue = nil;
|
||||
id<MTLRenderPipelineState> pipeline = nil;
|
||||
CVMetalTextureCacheRef texture_cache = nullptr;
|
||||
std::shared_ptr<SharedFrameState> frames =
|
||||
std::make_shared<SharedFrameState>();
|
||||
NSView* slint_view = nil;
|
||||
@@ -417,6 +499,8 @@ struct MacMetalVideoRenderer::Impl {
|
||||
uint64_t rendered_content_generation = 0;
|
||||
std::shared_ptr<std::atomic<bool>> native_surface_needs_redraw =
|
||||
std::make_shared<std::atomic<bool>>(true);
|
||||
std::atomic<bool> native_path_logged{false};
|
||||
std::atomic<bool> native_fallback_logged{false};
|
||||
|
||||
Impl() {
|
||||
@autoreleasepool {
|
||||
@@ -430,6 +514,12 @@ struct MacMetalVideoRenderer::Impl {
|
||||
LOG_ERROR("Metal video renderer unavailable: command queue creation failed");
|
||||
return;
|
||||
}
|
||||
const CVReturn cache_status = CVMetalTextureCacheCreate(
|
||||
kCFAllocatorDefault, nullptr, device, nullptr, &texture_cache);
|
||||
if (cache_status != kCVReturnSuccess || !texture_cache) {
|
||||
LOG_WARN("Metal video renderer could not create CV texture cache: {}",
|
||||
cache_status);
|
||||
}
|
||||
|
||||
NSError* library_error = nil;
|
||||
id<MTLLibrary> library =
|
||||
@@ -466,6 +556,11 @@ struct MacMetalVideoRenderer::Impl {
|
||||
~Impl() {
|
||||
DetachViews();
|
||||
frames.reset();
|
||||
if (texture_cache) {
|
||||
CVMetalTextureCacheFlush(texture_cache, 0);
|
||||
CFRelease(texture_cache);
|
||||
texture_cache = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool Ready() const {
|
||||
@@ -655,6 +750,142 @@ MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitNv12(
|
||||
return SubmitNv12Internal(remote_id, data, size, width, height, true);
|
||||
}
|
||||
|
||||
MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitNativeFrame(
|
||||
std::string_view remote_id, const MiniRtcNativeVideoFrame& frame) {
|
||||
if (!IsReady()) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
const MiniRtcNativeVideoFrame* native = GetMacNativeFrame(frame);
|
||||
if (!native) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock(impl_->frames->mutex);
|
||||
if (impl_->frames->selected_stream != remote_id) {
|
||||
return SubmitResult::not_selected;
|
||||
}
|
||||
}
|
||||
|
||||
const int width = static_cast<int>(native->width);
|
||||
const int height = static_cast<int>(native->height);
|
||||
const auto submit_cpu_fallback = [&]() {
|
||||
if (!impl_->native_fallback_logged.exchange(true,
|
||||
std::memory_order_acq_rel)) {
|
||||
LOG_WARN("macOS native CVPixelBuffer upload unavailable; using CPU NV12 "
|
||||
"fallback");
|
||||
}
|
||||
const size_t y_size = static_cast<size_t>(width) * height;
|
||||
std::vector<unsigned char> packed_nv12(y_size + y_size / 2);
|
||||
if (native->copy_to_nv12(native->owner, packed_nv12.data(),
|
||||
packed_nv12.size()) != 0) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
return SubmitNv12(remote_id, packed_nv12.data(), packed_nv12.size(), width,
|
||||
height);
|
||||
};
|
||||
|
||||
CVPixelBufferRef pixel_buffer =
|
||||
static_cast<CVPixelBufferRef>(native->payload.cv_pixel_buffer);
|
||||
const OSType pixel_format = CVPixelBufferGetPixelFormatType(pixel_buffer);
|
||||
if (!CVPixelBufferIsPlanar(pixel_buffer) ||
|
||||
CVPixelBufferGetPlaneCount(pixel_buffer) < 2 ||
|
||||
CVPixelBufferGetWidth(pixel_buffer) != native->width ||
|
||||
CVPixelBufferGetHeight(pixel_buffer) != native->height ||
|
||||
(pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange &&
|
||||
pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)) {
|
||||
return submit_cpu_fallback();
|
||||
}
|
||||
|
||||
NativeVideoFrameRef retained(native);
|
||||
if (!retained || !impl_->texture_cache) {
|
||||
return submit_cpu_fallback();
|
||||
}
|
||||
|
||||
CVMetalTextureRef y_cv_texture = nullptr;
|
||||
CVReturn y_status = CVMetalTextureCacheCreateTextureFromImage(
|
||||
kCFAllocatorDefault, impl_->texture_cache, pixel_buffer, nullptr,
|
||||
MTLPixelFormatR8Unorm, native->width, native->height, 0,
|
||||
&y_cv_texture);
|
||||
if (y_status != kCVReturnSuccess || !y_cv_texture) {
|
||||
return submit_cpu_fallback();
|
||||
}
|
||||
|
||||
CVMetalTextureRef uv_cv_texture = nullptr;
|
||||
CVReturn uv_status = CVMetalTextureCacheCreateTextureFromImage(
|
||||
kCFAllocatorDefault, impl_->texture_cache, pixel_buffer, nullptr,
|
||||
MTLPixelFormatRG8Unorm, native->width / 2U, native->height / 2U, 1,
|
||||
&uv_cv_texture);
|
||||
if (uv_status != kCVReturnSuccess || !uv_cv_texture) {
|
||||
CFRelease(y_cv_texture);
|
||||
return submit_cpu_fallback();
|
||||
}
|
||||
|
||||
id<MTLTexture> y_texture = CVMetalTextureGetTexture(y_cv_texture);
|
||||
id<MTLTexture> uv_texture = CVMetalTextureGetTexture(uv_cv_texture);
|
||||
if (y_texture == nil || uv_texture == nil) {
|
||||
CFRelease(y_cv_texture);
|
||||
CFRelease(uv_cv_texture);
|
||||
return submit_cpu_fallback();
|
||||
}
|
||||
|
||||
auto& frames = *impl_->frames;
|
||||
std::lock_guard lock(frames.mutex);
|
||||
if (frames.selected_stream != remote_id) {
|
||||
CFRelease(y_cv_texture);
|
||||
CFRelease(uv_cv_texture);
|
||||
return SubmitResult::not_selected;
|
||||
}
|
||||
|
||||
// Latest-frame-only queue: replace an unconsumed decoded frame to bound
|
||||
// latency, while never recycling a frame still referenced by the GPU.
|
||||
FrameSlot* target = nullptr;
|
||||
for (auto& slot : frames.slots) {
|
||||
if (slot.use == SlotUse::pending) {
|
||||
target = &slot;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!target) {
|
||||
uint64_t oldest_sequence = std::numeric_limits<uint64_t>::max();
|
||||
for (auto& slot : frames.slots) {
|
||||
if (slot.use != SlotUse::available) {
|
||||
continue;
|
||||
}
|
||||
if (!slot.valid) {
|
||||
target = &slot;
|
||||
break;
|
||||
}
|
||||
if (slot.sequence < oldest_sequence) {
|
||||
oldest_sequence = slot.sequence;
|
||||
target = &slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!target) {
|
||||
CFRelease(y_cv_texture);
|
||||
CFRelease(uv_cv_texture);
|
||||
return SubmitResult::dropped;
|
||||
}
|
||||
|
||||
target->ReleaseResources();
|
||||
target->y_cv_texture = y_cv_texture;
|
||||
target->uv_cv_texture = uv_cv_texture;
|
||||
target->y_texture = y_texture;
|
||||
target->uv_texture = uv_texture;
|
||||
target->native_frame = std::move(retained);
|
||||
target->width = width;
|
||||
target->height = height;
|
||||
target->remote_id.assign(remote_id);
|
||||
target->sequence = frames.next_sequence++;
|
||||
target->use = SlotUse::pending;
|
||||
target->valid = true;
|
||||
if (!impl_->native_path_logged.exchange(true, std::memory_order_acq_rel)) {
|
||||
LOG_INFO("macOS VideoToolbox-to-Metal native NV12 path enabled");
|
||||
}
|
||||
return SubmitResult::submitted;
|
||||
}
|
||||
|
||||
MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitCachedNv12(
|
||||
std::string_view remote_id, const uint8_t* data, size_t size, int width,
|
||||
int height) {
|
||||
@@ -1045,40 +1276,57 @@ bool MacMetalVideoRenderer::CopyLatestNv12(
|
||||
return false;
|
||||
}
|
||||
|
||||
std::lock_guard lock(impl_->frames->mutex);
|
||||
const FrameSlot* newest = nullptr;
|
||||
for (const auto& slot : impl_->frames->slots) {
|
||||
if (!slot.valid || slot.remote_id != remote_id || slot.y_buffer == nil ||
|
||||
slot.uv_buffer == nil || slot.sequence == 0) {
|
||||
continue;
|
||||
NativeVideoFrameRef native_frame;
|
||||
{
|
||||
std::lock_guard lock(impl_->frames->mutex);
|
||||
const FrameSlot* newest = nullptr;
|
||||
for (const auto& slot : impl_->frames->slots) {
|
||||
if (!slot.valid || slot.remote_id != remote_id || slot.sequence == 0 ||
|
||||
(!slot.native_frame &&
|
||||
(slot.y_buffer == nil || slot.uv_buffer == nil))) {
|
||||
continue;
|
||||
}
|
||||
if (newest == nullptr || slot.sequence > newest->sequence) {
|
||||
newest = &slot;
|
||||
}
|
||||
}
|
||||
if (newest == nullptr || slot.sequence > newest->sequence) {
|
||||
newest = &slot;
|
||||
if (newest == nullptr || newest->width <= 0 || newest->height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*width = newest->width;
|
||||
*height = newest->height;
|
||||
native_frame = newest->native_frame;
|
||||
if (!native_frame) {
|
||||
const size_t y_size =
|
||||
static_cast<size_t>(newest->width) * newest->height;
|
||||
output->resize(y_size + y_size / 2);
|
||||
auto* y_source =
|
||||
static_cast<const uint8_t*>(newest->y_buffer.contents);
|
||||
auto* uv_source =
|
||||
static_cast<const uint8_t*>(newest->uv_buffer.contents);
|
||||
for (int row = 0; row < newest->height; ++row) {
|
||||
std::memcpy(output->data() +
|
||||
static_cast<size_t>(row) * newest->width,
|
||||
y_source + static_cast<size_t>(row) * newest->y_stride,
|
||||
newest->width);
|
||||
}
|
||||
uint8_t* uv_destination = output->data() + y_size;
|
||||
for (int row = 0; row < newest->height / 2; ++row) {
|
||||
std::memcpy(uv_destination +
|
||||
static_cast<size_t>(row) * newest->width,
|
||||
uv_source + static_cast<size_t>(row) * newest->uv_stride,
|
||||
newest->width);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (newest == nullptr || newest->width <= 0 || newest->height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t y_size =
|
||||
static_cast<size_t>(newest->width) * newest->height;
|
||||
const MiniRtcNativeVideoFrame* native = native_frame.Get();
|
||||
const size_t y_size = static_cast<size_t>(native->width) * native->height;
|
||||
output->resize(y_size + y_size / 2);
|
||||
auto* y_source = static_cast<const uint8_t*>(newest->y_buffer.contents);
|
||||
auto* uv_source = static_cast<const uint8_t*>(newest->uv_buffer.contents);
|
||||
for (int row = 0; row < newest->height; ++row) {
|
||||
std::memcpy(output->data() + static_cast<size_t>(row) * newest->width,
|
||||
y_source + static_cast<size_t>(row) * newest->y_stride,
|
||||
newest->width);
|
||||
}
|
||||
uint8_t* uv_destination = output->data() + y_size;
|
||||
for (int row = 0; row < newest->height / 2; ++row) {
|
||||
std::memcpy(uv_destination + static_cast<size_t>(row) * newest->width,
|
||||
uv_source + static_cast<size_t>(row) * newest->uv_stride,
|
||||
newest->width);
|
||||
}
|
||||
*width = newest->width;
|
||||
*height = newest->height;
|
||||
return true;
|
||||
return native->copy_to_nv12(native->owner, output->data(), output->size()) ==
|
||||
0;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -42,6 +42,65 @@ static std::string NSErrorToString(NSError *error) {
|
||||
return description ? description : "";
|
||||
}
|
||||
|
||||
static void RetainCapturedPixelBuffer(void *owner) {
|
||||
if (owner) {
|
||||
CVPixelBufferRetain(static_cast<CVPixelBufferRef>(owner));
|
||||
}
|
||||
}
|
||||
|
||||
static void ReleaseCapturedPixelBuffer(void *owner) {
|
||||
if (owner) {
|
||||
CVPixelBufferRelease(static_cast<CVPixelBufferRef>(owner));
|
||||
}
|
||||
}
|
||||
|
||||
static int CopyCapturedPixelBufferToNv12(void *owner, uint8_t *destination,
|
||||
size_t destination_size) {
|
||||
auto pixel_buffer = static_cast<CVPixelBufferRef>(owner);
|
||||
if (!pixel_buffer || !destination ||
|
||||
!CVPixelBufferIsPlanar(pixel_buffer) ||
|
||||
CVPixelBufferGetPlaneCount(pixel_buffer) < 2) {
|
||||
return -1;
|
||||
}
|
||||
const OSType pixel_format = CVPixelBufferGetPixelFormatType(pixel_buffer);
|
||||
if (pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange &&
|
||||
pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const size_t width = CVPixelBufferGetWidth(pixel_buffer);
|
||||
const size_t height = CVPixelBufferGetHeight(pixel_buffer);
|
||||
const size_t required_size = width * height * 3U / 2U;
|
||||
if (destination_size < required_size ||
|
||||
CVPixelBufferLockBaseAddress(pixel_buffer,
|
||||
kCVPixelBufferLock_ReadOnly) !=
|
||||
kCVReturnSuccess) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const auto *y_plane = static_cast<const uint8_t *>(
|
||||
CVPixelBufferGetBaseAddressOfPlane(pixel_buffer, 0));
|
||||
const auto *uv_plane = static_cast<const uint8_t *>(
|
||||
CVPixelBufferGetBaseAddressOfPlane(pixel_buffer, 1));
|
||||
const size_t y_stride = CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, 0);
|
||||
const size_t uv_stride = CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, 1);
|
||||
int result = 0;
|
||||
if (!y_plane || !uv_plane || y_stride < width || uv_stride < width) {
|
||||
result = -1;
|
||||
} else {
|
||||
for (size_t row = 0; row < height; ++row) {
|
||||
std::memcpy(destination + row * width, y_plane + row * y_stride, width);
|
||||
}
|
||||
uint8_t *uv_destination = destination + width * height;
|
||||
for (size_t row = 0; row < height / 2U; ++row) {
|
||||
std::memcpy(uv_destination + row * width,
|
||||
uv_plane + row * uv_stride, width);
|
||||
}
|
||||
}
|
||||
CVPixelBufferUnlockBaseAddress(pixel_buffer, kCVPixelBufferLock_ReadOnly);
|
||||
return result;
|
||||
}
|
||||
|
||||
// The ScreenCaptureKit API was available in macOS 12.3, but full-screen capture
|
||||
// was reported to be broken before macOS 13 - see http://crbug.com/40234870.
|
||||
// Also, the `SCContentFilter` fields `contentRect` and `pointPixelScale` were
|
||||
@@ -92,14 +151,11 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
||||
private:
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
std::map<int, CGDirectDisplayID> display_id_map_;
|
||||
unsigned char *nv12_frame_ = nullptr;
|
||||
size_t nv12_frame_size_ = 0;
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
int fps_ = 60;
|
||||
bool show_cursor_ = false;
|
||||
bool capture_requested_ = false;
|
||||
bool invalid_stream_id_logged_ = false;
|
||||
bool native_output_logged_ = false;
|
||||
|
||||
public:
|
||||
// Called by SckHelper when shareable content is returned by ScreenCaptureKit. `content` will be
|
||||
@@ -119,8 +175,8 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
||||
// object. The helper may outlive this C++ instance, if a completion-handler is passed to
|
||||
// ScreenCaptureKit APIs and the C++ object is deleted before the handler executes.
|
||||
SckHelper *__strong helper_ = nil;
|
||||
// Callback for returning captured frames, or errors, to the caller. Only used on the caller's
|
||||
// thread.
|
||||
// Callback for returning captured frames to the caller. ScreenCaptureKit may
|
||||
// invoke it on its sample-handler queue.
|
||||
cb_desktop_data _on_data = nullptr;
|
||||
// Signals that a permanent error occurred. This may be set on any thread, and is read by
|
||||
// CaptureFrame() which runs on the caller's thread.
|
||||
@@ -244,12 +300,6 @@ ScreenCapturerSckImpl::~ScreenCapturerSckImpl() {
|
||||
|
||||
display_info_list_.clear();
|
||||
display_id_map_.clear();
|
||||
|
||||
if (nv12_frame_) {
|
||||
delete[] nv12_frame_;
|
||||
nv12_frame_ = nullptr;
|
||||
nv12_frame_size_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
|
||||
@@ -262,6 +312,7 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
|
||||
fps_ = fps > 0 ? fps : 60;
|
||||
display_info_list_.clear();
|
||||
display_id_map_.clear();
|
||||
native_output_logged_ = false;
|
||||
|
||||
if (@available(macOS 10.15, *)) {
|
||||
bool has_permission = CGPreflightScreenCaptureAccess();
|
||||
@@ -596,81 +647,69 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
|
||||
return;
|
||||
}
|
||||
|
||||
size_t width = CVPixelBufferGetWidth(pixelBuffer);
|
||||
size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
||||
if (width == 0 || height == 0 || CVPixelBufferGetPlaneCount(pixelBuffer) < 2) {
|
||||
LOG_ERROR("Invalid CVPixelBuffer: width={}, height={}, planes={}", width, height,
|
||||
const size_t width = CVPixelBufferGetWidth(pixelBuffer);
|
||||
const size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
||||
const OSType pixel_format = CVPixelBufferGetPixelFormatType(pixelBuffer);
|
||||
if (width == 0 || height == 0 || (width & 1U) != 0 ||
|
||||
(height & 1U) != 0 || !CVPixelBufferIsPlanar(pixelBuffer) ||
|
||||
CVPixelBufferGetPlaneCount(pixelBuffer) < 2 ||
|
||||
(pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange &&
|
||||
pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)) {
|
||||
LOG_ERROR("Invalid CVPixelBuffer: width={}, height={}, format={}, planes={}",
|
||||
width, height, pixel_format,
|
||||
CVPixelBufferGetPlaneCount(pixelBuffer));
|
||||
return;
|
||||
}
|
||||
|
||||
CVReturn status = CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
if (status != kCVReturnSuccess) {
|
||||
LOG_ERROR("Failed to lock CVPixelBuffer base address: %d", status);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t required_size = width * height * 3 / 2;
|
||||
const size_t required_size = width * height * 3U / 2U;
|
||||
if (required_size > static_cast<size_t>((std::numeric_limits<int>::max)())) {
|
||||
LOG_ERROR("Captured frame is too large: {} bytes", required_size);
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (!_on_data || !capture_requested_) {
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nv12_frame_ || nv12_frame_size_ < required_size) {
|
||||
delete[] nv12_frame_;
|
||||
nv12_frame_ = new unsigned char[required_size];
|
||||
nv12_frame_size_ = required_size;
|
||||
}
|
||||
width_ = static_cast<int>(width);
|
||||
height_ = static_cast<int>(height);
|
||||
|
||||
void *base_y = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
|
||||
size_t stride_y = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
|
||||
|
||||
void *base_uv = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
|
||||
size_t stride_uv = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
|
||||
if (!base_y || !base_uv || stride_y < width || stride_uv < width) {
|
||||
LOG_ERROR("Invalid CVPixelBuffer planes: base_y={}, base_uv={}, stride_y={}, stride_uv={}",
|
||||
base_y != nullptr, base_uv != nullptr, stride_y, stride_uv);
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned char *dst_y = nv12_frame_;
|
||||
for (size_t row = 0; row < height; ++row) {
|
||||
memcpy(dst_y + row * width, static_cast<unsigned char *>(base_y) + row * stride_y, width);
|
||||
}
|
||||
|
||||
unsigned char *dst_uv = nv12_frame_ + width * height;
|
||||
for (size_t row = 0; row < height / 2; ++row) {
|
||||
memcpy(dst_uv + row * width, static_cast<unsigned char *>(base_uv) + row * stride_uv, width);
|
||||
}
|
||||
|
||||
const std::string stream_id = ResolveDisplayStreamId(
|
||||
current_stream_id_.c_str(), display_info_list_.size(),
|
||||
current_monitor_index_);
|
||||
if (stream_id.empty()) {
|
||||
if (!invalid_stream_id_logged_) {
|
||||
LOG_ERROR("Dropping macOS frames without a registered stream id, "
|
||||
"display_id={}",
|
||||
current_display_);
|
||||
invalid_stream_id_logged_ = true;
|
||||
cb_desktop_data callback;
|
||||
std::string stream_id;
|
||||
bool log_native_output = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (!_on_data || !capture_requested_) {
|
||||
return;
|
||||
}
|
||||
stream_id = ResolveDisplayStreamId(current_stream_id_.c_str(),
|
||||
display_info_list_.size(),
|
||||
current_monitor_index_);
|
||||
if (stream_id.empty()) {
|
||||
if (!invalid_stream_id_logged_) {
|
||||
LOG_ERROR("Dropping macOS frames without a registered stream id, "
|
||||
"display_id={}",
|
||||
current_display_);
|
||||
invalid_stream_id_logged_ = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
invalid_stream_id_logged_ = false;
|
||||
callback = _on_data;
|
||||
if (!native_output_logged_) {
|
||||
native_output_logged_ = true;
|
||||
log_native_output = true;
|
||||
}
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
return;
|
||||
}
|
||||
invalid_stream_id_logged_ = false;
|
||||
_on_data(nv12_frame_, static_cast<int>(required_size), static_cast<int>(width),
|
||||
static_cast<int>(height), stream_id.c_str());
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
MiniRtcNativeVideoFrame native_frame{};
|
||||
native_frame.struct_size = sizeof(native_frame);
|
||||
native_frame.type = MiniRtcNativeVideoFrameCVPixelBuffer;
|
||||
native_frame.width = static_cast<uint32_t>(width);
|
||||
native_frame.height = static_cast<uint32_t>(height);
|
||||
native_frame.payload.cv_pixel_buffer = pixelBuffer;
|
||||
native_frame.owner = pixelBuffer;
|
||||
native_frame.retain = &RetainCapturedPixelBuffer;
|
||||
native_frame.release = &ReleaseCapturedPixelBuffer;
|
||||
native_frame.copy_to_nv12 = &CopyCapturedPixelBufferToNv12;
|
||||
if (log_native_output) {
|
||||
LOG_INFO("macOS ScreenCaptureKit native CVPixelBuffer output enabled");
|
||||
}
|
||||
callback(nullptr, static_cast<int>(required_size), static_cast<int>(width),
|
||||
static_cast<int>(height), stream_id.c_str(), &native_frame);
|
||||
}
|
||||
|
||||
void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SYSTEM_INFO_H_
|
||||
#define _SYSTEM_INFO_H_
|
||||
|
||||
#include "platform.h"
|
||||
|
||||
#include <ifaddrs.h>
|
||||
@@ -53,3 +62,6 @@ std::string GetHostName() {
|
||||
bool IsWaylandSession() { return false; }
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
|
||||
#endif
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Platform-independent contract for the native stream video renderer.
|
||||
@@ -56,6 +58,12 @@ public:
|
||||
virtual SubmitResult SubmitCachedNv12(std::string_view remote_id,
|
||||
const uint8_t* data, size_t size,
|
||||
int width, int height) = 0;
|
||||
// Retains a platform-native decoded frame when the backend supports its
|
||||
// storage and handle types. Unsupported backends keep the default fallback.
|
||||
virtual SubmitResult SubmitNativeFrame(std::string_view,
|
||||
const MiniRtcNativeVideoFrame&) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
|
||||
virtual bool CopyLatestNv12(std::string_view remote_id,
|
||||
std::vector<unsigned char>* output, int* width,
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
#include "platform/windows/gui/slint_backend.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
struct HandleCloser {
|
||||
void operator()(HANDLE handle) const noexcept {
|
||||
if (handle != INVALID_HANDLE_VALUE) {
|
||||
CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
};
|
||||
using ScopedHandle = std::unique_ptr<void, HandleCloser>;
|
||||
|
||||
std::string ErrorCode(DWORD code) {
|
||||
std::ostringstream text;
|
||||
text << "0x" << std::hex << std::uppercase << code;
|
||||
return text.str();
|
||||
}
|
||||
|
||||
std::string ProbeSlintRenderer(DWORD timeout_ms) {
|
||||
std::wstring executable(32768, L'\0');
|
||||
const DWORD length = GetModuleFileNameW(
|
||||
nullptr, executable.data(), static_cast<DWORD>(executable.size()));
|
||||
if (length == 0 || length >= executable.size()) {
|
||||
return "Unable to locate renderer probe executable";
|
||||
}
|
||||
executable.resize(length);
|
||||
|
||||
// A job also cleans up the probe if the application is closed while the
|
||||
// graphics driver is stuck. Create it suspended so assignment cannot race.
|
||||
ScopedHandle job(CreateJobObjectW(nullptr, nullptr));
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = {};
|
||||
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
if (!job ||
|
||||
!SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation,
|
||||
&limits, sizeof(limits))) {
|
||||
return "Unable to create renderer probe job: " + ErrorCode(GetLastError());
|
||||
}
|
||||
|
||||
constexpr std::string_view argument = kSlintRendererProbeArgument;
|
||||
std::wstring command = L"\"" + executable + L"\" ";
|
||||
command.append(argument.begin(), argument.end());
|
||||
STARTUPINFOW startup = {sizeof(startup)};
|
||||
PROCESS_INFORMATION process = {};
|
||||
if (!CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr,
|
||||
FALSE, CREATE_NO_WINDOW | CREATE_SUSPENDED, nullptr,
|
||||
nullptr, &startup, &process)) {
|
||||
return "Unable to start renderer probe: " + ErrorCode(GetLastError());
|
||||
}
|
||||
ScopedHandle process_handle(process.hProcess);
|
||||
ScopedHandle thread_handle(process.hThread);
|
||||
if (!AssignProcessToJobObject(job.get(), process.hProcess) ||
|
||||
ResumeThread(process.hThread) == static_cast<DWORD>(-1)) {
|
||||
const DWORD error = GetLastError();
|
||||
TerminateProcess(process.hProcess, 1);
|
||||
return "Unable to run renderer probe: " + ErrorCode(error);
|
||||
}
|
||||
|
||||
const DWORD wait = WaitForSingleObject(process.hProcess, timeout_ms);
|
||||
if (wait != WAIT_OBJECT_0) {
|
||||
// Closing the job kills only our probe, including any stuck driver thread.
|
||||
return wait == WAIT_TIMEOUT ? "Slint renderer probe timed out"
|
||||
: "Waiting for renderer probe failed: " +
|
||||
ErrorCode(GetLastError());
|
||||
}
|
||||
DWORD exit_code = 0;
|
||||
if (!GetExitCodeProcess(process.hProcess, &exit_code)) {
|
||||
return "Unable to read renderer probe result: " + ErrorCode(GetLastError());
|
||||
}
|
||||
return exit_code == 0
|
||||
? std::string{}
|
||||
: "Slint renderer probe exited with " + ErrorCode(exit_code);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SlintBackendSelection ConfigureWindowsSlintBackend(uint32_t probe_timeout_ms) {
|
||||
if (const char* requested = std::getenv("SLINT_BACKEND");
|
||||
requested && requested[0] != '\0') {
|
||||
return {true, requested, "Using explicitly requested Slint backend"};
|
||||
}
|
||||
|
||||
// Slint creates its own GL context. A successful SDL/WGL capability check
|
||||
// cannot validate it, and a Rust panic cannot be caught by C++ exceptions.
|
||||
// Exercise Slint in a separate process without touching GL in this process.
|
||||
const std::string error = ProbeSlintRenderer(probe_timeout_ms);
|
||||
if (error.empty()) {
|
||||
return {true, "default", "Slint renderer probe rendered successfully"};
|
||||
}
|
||||
|
||||
// _putenv_s updates the CRT and the OS environment read by slint_cpp.dll.
|
||||
// This is process-local so a later launch can retry hardware rendering.
|
||||
if (_putenv_s("SLINT_BACKEND", "winit-software") != 0) {
|
||||
return {false, {}, "Unable to select Slint software rendering: " + error};
|
||||
}
|
||||
return {true, "winit-software", error};
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SLINT_BACKEND_H_
|
||||
#define _SLINT_BACKEND_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct SlintBackendSelection {
|
||||
bool success = true;
|
||||
std::string backend;
|
||||
std::string diagnostic;
|
||||
};
|
||||
|
||||
inline constexpr char kSlintRendererProbeArgument[] = "--slint-renderer-probe";
|
||||
|
||||
// Call before creating any Slint components. The executable's main() must
|
||||
// dispatch kSlintRendererProbeArgument before services, settings or the daemon.
|
||||
SlintBackendSelection ConfigureWindowsSlintBackend(
|
||||
uint32_t probe_timeout_ms = 10000);
|
||||
|
||||
// Internal child-process entry point; never call in the main application.
|
||||
int RunSlintRendererProbe();
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,59 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
|
||||
#include "crossdesk_ui.h"
|
||||
#include "platform/windows/gui/slint_backend.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int RunSlintRendererProbe() {
|
||||
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
|
||||
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int result = 2;
|
||||
{
|
||||
// Reuse the application's single AOT entry point. Independently generated
|
||||
// headers can contain colliding internal component names across TUs.
|
||||
auto probe = ui::MainWindow::create();
|
||||
probe->set_custom_titlebar(true);
|
||||
// AfterRendering confirms that the real GL context, shader initialization
|
||||
// and first draw all succeeded. GPU readback from an off-screen window can
|
||||
// be black even when rendering works, so do not use its pixels as a test.
|
||||
const auto notifier_error = probe->window().set_rendering_notifier(
|
||||
[&](slint::RenderingState state, slint::GraphicsAPI) {
|
||||
if (state == slint::RenderingState::AfterRendering) {
|
||||
result = 0;
|
||||
}
|
||||
});
|
||||
probe->window().set_position(
|
||||
slint::PhysicalPosition(slint::Point<int32_t>{-32000, -32000}));
|
||||
probe->show();
|
||||
slint::Timer timer;
|
||||
timer.start(slint::TimerMode::SingleShot, std::chrono::milliseconds(100),
|
||||
[&] {
|
||||
// Renderers without a rendering notifier (software) support
|
||||
// snapshots.
|
||||
if (notifier_error) {
|
||||
const auto pixels = probe->window().take_snapshot();
|
||||
if (pixels && pixels->width() > 0 && pixels->height() > 0) {
|
||||
result = 0;
|
||||
} else {
|
||||
std::cerr << "Slint probe could not read rendered pixels"
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
slint::quit_event_loop();
|
||||
});
|
||||
slint::run_event_loop();
|
||||
probe->hide();
|
||||
}
|
||||
SDL_Quit();
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -60,6 +60,8 @@ WinTray::WinTray(HWND app_hwnd, HICON icon, const std::wstring& tooltip,
|
||||
nid_.uCallbackMessage = WM_TRAY_CALLBACK;
|
||||
nid_.hIcon = icon_;
|
||||
wcsncpy_s(nid_.szTip, tip_.c_str(), _TRUNCATE);
|
||||
|
||||
EnsureTrayIcon();
|
||||
}
|
||||
|
||||
WinTray::WinTray(std::function<void()> show_window,
|
||||
@@ -80,7 +82,7 @@ WinTray::~WinTray() {
|
||||
}
|
||||
|
||||
void WinTray::MinimizeToTray() {
|
||||
Shell_NotifyIcon(NIM_ADD, &nid_);
|
||||
EnsureTrayIcon();
|
||||
if (hide_window_) {
|
||||
hide_window_();
|
||||
} else if (app_hwnd_) {
|
||||
@@ -88,7 +90,25 @@ void WinTray::MinimizeToTray() {
|
||||
}
|
||||
}
|
||||
|
||||
void WinTray::RemoveTrayIcon() { Shell_NotifyIcon(NIM_DELETE, &nid_); }
|
||||
bool WinTray::EnsureTrayIcon() {
|
||||
if (tray_icon_added_) {
|
||||
// NIM_MODIFY also detects an icon lost when Explorer was restarted.
|
||||
if (Shell_NotifyIcon(NIM_MODIFY, &nid_)) {
|
||||
return true;
|
||||
}
|
||||
tray_icon_added_ = false;
|
||||
}
|
||||
|
||||
tray_icon_added_ = Shell_NotifyIcon(NIM_ADD, &nid_) != FALSE;
|
||||
return tray_icon_added_;
|
||||
}
|
||||
|
||||
void WinTray::RemoveTrayIcon() {
|
||||
if (tray_icon_added_) {
|
||||
Shell_NotifyIcon(NIM_DELETE, &nid_);
|
||||
tray_icon_added_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void WinTray::ShowApplicationWindow() {
|
||||
if (show_window_) {
|
||||
|
||||
@@ -33,6 +33,7 @@ class WinTray {
|
||||
bool HandleTrayMessage(MSG* msg);
|
||||
|
||||
private:
|
||||
bool EnsureTrayIcon();
|
||||
void ShowApplicationWindow();
|
||||
void OpenSettings();
|
||||
|
||||
@@ -42,6 +43,7 @@ class WinTray {
|
||||
std::wstring tip_;
|
||||
int language_index_;
|
||||
NOTIFYICONDATA nid_;
|
||||
bool tray_icon_added_ = false;
|
||||
std::function<void()> show_window_;
|
||||
std::function<void()> hide_window_;
|
||||
std::function<void()> open_settings_;
|
||||
|
||||
@@ -384,7 +384,7 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
if (idx >= 0 && idx < static_cast<int>(display_info_list_.size())) {
|
||||
const std::string stream_id = MakeDisplayStreamId(idx);
|
||||
callback_(nv12_frame_, nv12_size, even_width, even_height,
|
||||
stream_id.c_str());
|
||||
stream_id.c_str(), nullptr);
|
||||
} else {
|
||||
LOG_ERROR("DXGI: CaptureLoop invalid monitor_index {} (list size {})",
|
||||
idx, display_info_list_.size());
|
||||
|
||||
@@ -212,7 +212,8 @@ void ScreenCapturerGdi::CaptureLoop() {
|
||||
|
||||
if (callback_) {
|
||||
const std::string stream_id = MakeDisplayStreamId(idx);
|
||||
callback_(nv12_frame_, nv12_size, width, height, stream_id.c_str());
|
||||
callback_(nv12_frame_, nv12_size, width, height, stream_id.c_str(),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
SelectObject(mem_dc, old);
|
||||
|
||||
@@ -406,7 +406,7 @@ void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame,
|
||||
|
||||
const std::string stream_id = MakeDisplayStreamId(id);
|
||||
on_data_(nv12_frame_, nv12_size, even_width, even_height,
|
||||
stream_id.c_str());
|
||||
stream_id.c_str(), nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,14 @@
|
||||
#include <vector>
|
||||
|
||||
#include <display_stream_id.h>
|
||||
#include "captured_nv12_frame.h"
|
||||
#include "interactive_state.h"
|
||||
#include "named_pipe_deadline.h"
|
||||
#include "rd_log.h"
|
||||
#include "screen_capturer_dxgi.h"
|
||||
#include "screen_capturer_gdi.h"
|
||||
#include "secure_desktop_frame_schedule.h"
|
||||
#include "secure_desktop_status_poller.h"
|
||||
#include "service_host.h"
|
||||
#include "session_helper_shared.h"
|
||||
#include "wgc_plugin_api.h"
|
||||
@@ -40,17 +44,6 @@ constexpr int kSecureDesktopCaptureMinFps = 30;
|
||||
constexpr int kSecureDesktopCaptureMaxIntervalMs =
|
||||
1000 / kSecureDesktopCaptureMinFps;
|
||||
|
||||
struct SecureDesktopServiceStatus {
|
||||
bool service_available = false;
|
||||
bool capture_active = false;
|
||||
bool helper_running = false;
|
||||
DWORD active_session_id = 0xFFFFFFFF;
|
||||
DWORD error_code = 0;
|
||||
std::string interactive_stage;
|
||||
std::string interactive_desktop;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
class WgcPluginCapturer final : public ScreenCapturer {
|
||||
public:
|
||||
using CreateFn = ScreenCapturer* (*)();
|
||||
@@ -185,41 +178,6 @@ bool IsTransientWindowsServiceStatusError(const std::string& error) {
|
||||
error == "pipe_read_failed";
|
||||
}
|
||||
|
||||
bool ReadPipeMessage(HANDLE pipe, std::vector<uint8_t>* response_out,
|
||||
DWORD* error_code_out = nullptr) {
|
||||
if (response_out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
response_out->clear();
|
||||
if (error_code_out != nullptr) {
|
||||
*error_code_out = 0;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> chunk(64 * 1024);
|
||||
while (true) {
|
||||
DWORD bytes_read = 0;
|
||||
if (ReadFile(pipe, chunk.data(), static_cast<DWORD>(chunk.size()),
|
||||
&bytes_read, nullptr)) {
|
||||
response_out->insert(response_out->end(), chunk.begin(),
|
||||
chunk.begin() + bytes_read);
|
||||
return true;
|
||||
}
|
||||
|
||||
const DWORD error = GetLastError();
|
||||
response_out->insert(response_out->end(), chunk.begin(),
|
||||
chunk.begin() + bytes_read);
|
||||
if (error == ERROR_MORE_DATA) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (error_code_out != nullptr) {
|
||||
*error_code_out = error;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ParseSecureDesktopFrameResponse(const std::vector<uint8_t>& response,
|
||||
std::vector<uint8_t>* nv12_frame_out,
|
||||
int* width_out, int* height_out,
|
||||
@@ -260,25 +218,27 @@ bool ParseSecureDesktopFrameResponse(const std::vector<uint8_t>& response,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QuerySecureDesktopServiceStatus(SecureDesktopServiceStatus* status) {
|
||||
if (status == nullptr) {
|
||||
return false;
|
||||
SecureDesktopServiceStatus QuerySecureDesktopServiceStatus() {
|
||||
SecureDesktopServiceStatus status;
|
||||
std::vector<uint8_t> response;
|
||||
DWORD error_code = 0;
|
||||
if (!QueryNamedPipeWithDeadline(kCrossDeskServicePipeName, "status",
|
||||
kSecureDesktopStatusPipeTimeoutMs, &response,
|
||||
&status.error, &error_code)) {
|
||||
status.error_code = error_code;
|
||||
return status;
|
||||
}
|
||||
|
||||
*status = {};
|
||||
const std::string response =
|
||||
QueryCrossDeskService("status", kSecureDesktopStatusPipeTimeoutMs);
|
||||
Json json = Json::parse(response, nullptr, false);
|
||||
Json json = Json::parse(response.begin(), response.end(), nullptr, false);
|
||||
if (json.is_discarded() || !json.is_object()) {
|
||||
status->error = "invalid_service_status_json";
|
||||
return false;
|
||||
status.error = "invalid_service_status_json";
|
||||
return status;
|
||||
}
|
||||
|
||||
status->service_available = json.value("ok", false);
|
||||
if (!status->service_available) {
|
||||
status->error = json.value("error", std::string("service_unavailable"));
|
||||
status->error_code = json.value("code", 0u);
|
||||
return true;
|
||||
status.service_available = json.value("ok", false);
|
||||
if (!status.service_available) {
|
||||
status.error = json.value("error", std::string("service_unavailable"));
|
||||
status.error_code = json.value("code", 0u);
|
||||
return status;
|
||||
}
|
||||
|
||||
if (ShouldNormalizeUnlockToUserDesktop(
|
||||
@@ -292,26 +252,27 @@ bool QuerySecureDesktopServiceStatus(SecureDesktopServiceStatus* status) {
|
||||
json.value("password_box_visible", false),
|
||||
json.value("unlock_ui_visible", false),
|
||||
json.value("last_session_event", std::string()))) {
|
||||
status->active_session_id = json.value("active_session_id", 0xFFFFFFFFu);
|
||||
status->interactive_stage = "user-desktop";
|
||||
status->interactive_desktop.clear();
|
||||
status->capture_active = false;
|
||||
return true;
|
||||
status.active_session_id = json.value("active_session_id", 0xFFFFFFFFu);
|
||||
status.interactive_stage = "user-desktop";
|
||||
status.interactive_desktop.clear();
|
||||
status.capture_active = false;
|
||||
return status;
|
||||
}
|
||||
|
||||
status->active_session_id = json.value("active_session_id", 0xFFFFFFFFu);
|
||||
status->helper_running = json.value("secure_input_helper_running", false);
|
||||
status->interactive_stage = json.value("interactive_stage", std::string());
|
||||
status->interactive_desktop =
|
||||
status.active_session_id = json.value("active_session_id", 0xFFFFFFFFu);
|
||||
status.helper_running = json.value("secure_input_helper_running", false);
|
||||
status.helper_process_id = json.value("secure_input_helper_pid", 0u);
|
||||
status.interactive_stage = json.value("interactive_stage", std::string());
|
||||
status.interactive_desktop =
|
||||
json.value("interactive_input_desktop", std::string());
|
||||
const bool secure_desktop_active =
|
||||
json.value("interactive_secure_desktop_active",
|
||||
json.value("secure_desktop_active", false));
|
||||
status->capture_active =
|
||||
status->active_session_id != 0xFFFFFFFF &&
|
||||
status.capture_active =
|
||||
status.active_session_id != 0xFFFFFFFF &&
|
||||
(secure_desktop_active ||
|
||||
IsSecureDesktopInteractionRequired(status->interactive_stage));
|
||||
return true;
|
||||
IsSecureDesktopInteractionRequired(status.interactive_stage));
|
||||
return status;
|
||||
}
|
||||
|
||||
bool QuerySecureDesktopHelperCommand(DWORD session_id,
|
||||
@@ -322,60 +283,29 @@ bool QuerySecureDesktopHelperCommand(DWORD session_id,
|
||||
return false;
|
||||
}
|
||||
|
||||
response_out->clear();
|
||||
const std::wstring pipe_name =
|
||||
GetCrossDeskSecureInputHelperPipeName(session_id);
|
||||
if (!WaitNamedPipeW(pipe_name.c_str(), kSecureDesktopHelperPipeTimeoutMs)) {
|
||||
if (error_out != nullptr) {
|
||||
*error_out = "pipe_unavailable:" + std::to_string(GetLastError());
|
||||
}
|
||||
return false;
|
||||
std::string error;
|
||||
DWORD code = 0;
|
||||
// The legacy single-frame path includes a full GDI capture and a large
|
||||
// response. Give it a separate deadline from the small control messages.
|
||||
const DWORD timeout_ms =
|
||||
command.rfind(kCrossDeskSecureInputCaptureCommandPrefix, 0) == 0
|
||||
? 1000
|
||||
: kSecureDesktopHelperPipeTimeoutMs;
|
||||
const bool ok = QueryNamedPipeWithDeadline(pipe_name, command, timeout_ms,
|
||||
response_out, &error, &code);
|
||||
if (!ok && error_out != nullptr) {
|
||||
*error_out = error + ":" + std::to_string(code);
|
||||
}
|
||||
|
||||
HANDLE pipe = CreateFileW(pipe_name.c_str(), GENERIC_READ | GENERIC_WRITE, 0,
|
||||
nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
if (pipe == INVALID_HANDLE_VALUE) {
|
||||
if (error_out != nullptr) {
|
||||
*error_out = "pipe_connect_failed:" + std::to_string(GetLastError());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD pipe_mode = PIPE_READMODE_MESSAGE;
|
||||
SetNamedPipeHandleState(pipe, &pipe_mode, nullptr, nullptr);
|
||||
|
||||
DWORD bytes_written = 0;
|
||||
if (!WriteFile(pipe, command.data(), static_cast<DWORD>(command.size()),
|
||||
&bytes_written, nullptr)) {
|
||||
const DWORD error = GetLastError();
|
||||
CloseHandle(pipe);
|
||||
if (error_out != nullptr) {
|
||||
*error_out = "pipe_write_failed:" + std::to_string(error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD read_error = 0;
|
||||
const bool read_ok = ReadPipeMessage(pipe, response_out, &read_error);
|
||||
CloseHandle(pipe);
|
||||
if (!read_ok) {
|
||||
if (error_out != nullptr) {
|
||||
*error_out = "pipe_read_failed:" + std::to_string(read_error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
|
||||
int width, int height, bool show_cursor,
|
||||
const std::string& stage,
|
||||
const std::string& desktop,
|
||||
std::vector<uint8_t>* nv12_frame_out,
|
||||
int* captured_width_out,
|
||||
int* captured_height_out,
|
||||
std::string* error_out) {
|
||||
bool QuerySecureDesktopHelperFrame(
|
||||
DWORD session_id, int left, int top, int width, int height,
|
||||
bool show_cursor, const std::string& stage, const std::string& desktop,
|
||||
std::vector<uint8_t>* nv12_frame_out, int* captured_width_out,
|
||||
int* captured_height_out, std::string* error_out) {
|
||||
if (nv12_frame_out == nullptr || captured_width_out == nullptr ||
|
||||
captured_height_out == nullptr) {
|
||||
return false;
|
||||
@@ -403,8 +333,17 @@ ScreenCapturerWin::~ScreenCapturerWin() { Destroy(); }
|
||||
int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
fps_ = fps;
|
||||
cb_orig_ = cb;
|
||||
native_output_logged_.store(false, std::memory_order_relaxed);
|
||||
native_output_error_logged_.store(false, std::memory_order_relaxed);
|
||||
try {
|
||||
native_frame_pool_ = CapturedNv12FramePool::Create();
|
||||
} catch (...) {
|
||||
LOG_ERROR("Windows capturer: failed to create native NV12 frame pool");
|
||||
return -1;
|
||||
}
|
||||
cb_ = [this](unsigned char* data, int size, int w, int h,
|
||||
const char* reported_stream_id) {
|
||||
const char* reported_stream_id,
|
||||
const MiniRtcNativeVideoFrame* native_frame) {
|
||||
if (secure_desktop_capture_active_.load(std::memory_order_relaxed)) {
|
||||
return;
|
||||
}
|
||||
@@ -450,7 +389,8 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
"elapsed_ms={}",
|
||||
raw_stream_id, mapped_stream_id, w, h, size, elapsed_ms);
|
||||
}
|
||||
if (cb_orig_) cb_orig_(data, size, w, h, mapped_stream_id.c_str());
|
||||
EmitCapturedFrame(data, size, w, h, mapped_stream_id.c_str(),
|
||||
native_frame);
|
||||
};
|
||||
|
||||
int ret = -1;
|
||||
@@ -513,9 +453,52 @@ int ScreenCapturerWin::Destroy() {
|
||||
stream_id_alias_.clear();
|
||||
handle_to_canonical_index_.clear();
|
||||
}
|
||||
native_frame_pool_.reset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::EmitCapturedFrame(
|
||||
unsigned char* data, int size, int width, int height,
|
||||
const char* stream_id, const MiniRtcNativeVideoFrame* native_frame) {
|
||||
if (!cb_orig_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (native_frame) {
|
||||
if (!native_output_logged_.exchange(true, std::memory_order_relaxed)) {
|
||||
LOG_INFO("Windows capturer native frame output enabled (type={})",
|
||||
static_cast<uint32_t>(native_frame->type));
|
||||
}
|
||||
cb_orig_(data, size, width, height, stream_id, native_frame);
|
||||
return;
|
||||
}
|
||||
|
||||
CapturedNv12Frame* owned_frame = nullptr;
|
||||
if (native_frame_pool_ && data && size > 0 && width > 0 && height > 0) {
|
||||
owned_frame = native_frame_pool_->CopyFrom(
|
||||
data, static_cast<size_t>(size), static_cast<uint32_t>(width),
|
||||
static_cast<uint32_t>(height));
|
||||
}
|
||||
if (!owned_frame) {
|
||||
if (!native_output_error_logged_.exchange(true,
|
||||
std::memory_order_relaxed)) {
|
||||
LOG_WARN(
|
||||
"Windows capturer could not retain a native NV12 frame; falling "
|
||||
"back to copied CPU input (size={}x{}, bytes={})",
|
||||
width, height, size);
|
||||
}
|
||||
cb_orig_(data, size, width, height, stream_id, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!native_output_logged_.exchange(true, std::memory_order_relaxed)) {
|
||||
LOG_INFO("Windows capturer native NV12 output enabled");
|
||||
}
|
||||
cb_orig_(nullptr, static_cast<int>(owned_frame->Size()), width, height,
|
||||
stream_id, owned_frame->Descriptor());
|
||||
owned_frame->Release();
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
if (!impl_) return -1;
|
||||
if (running_.load(std::memory_order_relaxed)) {
|
||||
@@ -1108,17 +1091,35 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
const int frame_interval_ms =
|
||||
fps_ > 0 ? (std::min)(kSecureDesktopCaptureMaxIntervalMs, 1000 / fps_)
|
||||
: kSecureDesktopCaptureMaxIntervalMs;
|
||||
ULONGLONG last_status_tick = 0;
|
||||
ULONGLONG last_error_tick = 0;
|
||||
bool last_capture_active = false;
|
||||
bool last_service_available = true;
|
||||
std::string last_stage;
|
||||
std::string last_service_error;
|
||||
ULONGLONG capture_stage_started_tick = 0;
|
||||
bool post_secure_restart_pending = false;
|
||||
ULONGLONG post_secure_restart_deadline_tick = 0;
|
||||
ULONGLONG last_post_secure_restart_tick = 0;
|
||||
SecureDesktopServiceStatus status;
|
||||
SecureDesktopStatusPoller status_poller(
|
||||
QuerySecureDesktopServiceStatus,
|
||||
std::chrono::milliseconds(kSecureDesktopStatusIntervalMs));
|
||||
SecureDesktopFrameSchedule frame_schedule;
|
||||
frame_schedule.Reset(GetTickCount64());
|
||||
ULONGLONG stats_started = GetTickCount64();
|
||||
unsigned shared_frames = 0, fallback_frames = 0, shared_waits = 0;
|
||||
unsigned shared_restarts = 0;
|
||||
int64_t max_status_ms = 0;
|
||||
auto report_stats = [&](bool force) {
|
||||
const ULONGLONG elapsed = GetTickCount64() - stats_started;
|
||||
if (elapsed == 0 || (!force && elapsed < 5000)) return;
|
||||
if (shared_frames || fallback_frames || shared_waits || shared_restarts) {
|
||||
LOG_INFO(
|
||||
"Secure capture delivery: shared_fps={:.1f}, fallback_fps={:.1f}, "
|
||||
"pending_waits={}, shared_restarts={}, status_max_ms={}",
|
||||
shared_frames * 1000.0 / elapsed, fallback_frames * 1000.0 / elapsed,
|
||||
shared_waits, shared_restarts, max_status_ms);
|
||||
}
|
||||
stats_started = GetTickCount64();
|
||||
shared_frames = fallback_frames = shared_waits = shared_restarts = 0;
|
||||
max_status_ms = 0;
|
||||
};
|
||||
std::vector<uint8_t> secure_frame;
|
||||
|
||||
while (running_.load(std::memory_order_relaxed)) {
|
||||
@@ -1128,67 +1129,60 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
}
|
||||
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
if (last_status_tick == 0 ||
|
||||
now - last_status_tick >= kSecureDesktopStatusIntervalMs) {
|
||||
SecureDesktopServiceStatus latest_status;
|
||||
const bool status_ok = QuerySecureDesktopServiceStatus(&latest_status);
|
||||
status = latest_status;
|
||||
if (status_ok) {
|
||||
const bool service_changed =
|
||||
status.service_available != last_service_available;
|
||||
const bool service_error_changed =
|
||||
!status.service_available && status.error != last_service_error;
|
||||
if (service_changed || service_error_changed) {
|
||||
if (status.service_available) {
|
||||
LOG_INFO(
|
||||
"Windows capturer secure desktop service available, polling "
|
||||
"session_id={}",
|
||||
status.active_session_id);
|
||||
} else if (IsTransientWindowsServiceStatusError(status.error)) {
|
||||
LOG_INFO(
|
||||
"Windows capturer secure desktop service temporarily unavailable: "
|
||||
"error={}, code={}",
|
||||
status.error, status.error_code);
|
||||
} else {
|
||||
LOG_WARN(
|
||||
"Windows capturer secure desktop service unavailable: "
|
||||
"error={}, code={}",
|
||||
status.error, status.error_code);
|
||||
}
|
||||
last_service_available = status.service_available;
|
||||
last_service_error = status.error;
|
||||
const auto frame_started = std::chrono::steady_clock::now();
|
||||
report_stats(false);
|
||||
if (auto sample = status_poller.Take()) {
|
||||
const auto previous = std::exchange(status, std::move(sample->status));
|
||||
max_status_ms = (std::max)(max_status_ms, sample->query_ms);
|
||||
if (status.service_available != previous.service_available ||
|
||||
status.error != previous.error) {
|
||||
if (status.service_available) {
|
||||
LOG_INFO(
|
||||
"Windows capturer secure desktop service available, "
|
||||
"polling session_id={}",
|
||||
status.active_session_id);
|
||||
} else if (IsTransientWindowsServiceStatusError(status.error)) {
|
||||
LOG_INFO(
|
||||
"Windows capturer secure desktop service temporarily "
|
||||
"unavailable; "
|
||||
"keeping last capture state: error={}, code={}",
|
||||
status.error, status.error_code);
|
||||
} else {
|
||||
LOG_WARN(
|
||||
"Windows capturer secure desktop service unavailable: "
|
||||
"error={}, code={}",
|
||||
status.error, status.error_code);
|
||||
}
|
||||
} else if (last_service_available ||
|
||||
last_service_error != "invalid_service_status_json") {
|
||||
LOG_WARN("Windows capturer secure desktop service status query failed");
|
||||
last_service_available = false;
|
||||
last_service_error = "invalid_service_status_json";
|
||||
}
|
||||
|
||||
secure_desktop_capture_active_.store(status.capture_active,
|
||||
std::memory_order_relaxed);
|
||||
if (status.capture_active != last_capture_active ||
|
||||
status.interactive_stage != last_stage) {
|
||||
if (status.capture_active != previous.capture_active ||
|
||||
status.interactive_stage != previous.interactive_stage ||
|
||||
status.active_session_id != previous.active_session_id ||
|
||||
status.helper_process_id != previous.helper_process_id) {
|
||||
report_stats(true);
|
||||
// A restarted helper can leave our mapping/event handles alive, but
|
||||
// their old producer is gone. Explicitly establish a fresh stream.
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
frame_schedule.Reset(now);
|
||||
const bool secure_capture_started =
|
||||
!last_capture_active && status.capture_active;
|
||||
!previous.capture_active && status.capture_active;
|
||||
const bool secure_capture_ended =
|
||||
last_capture_active && !status.capture_active;
|
||||
previous.capture_active && !status.capture_active;
|
||||
capture_stage_started_tick = now;
|
||||
LOG_INFO(
|
||||
"Windows capturer secure desktop state: active={}, stage='{}', "
|
||||
"session_id={}",
|
||||
status.capture_active, status.interactive_stage,
|
||||
status.active_session_id);
|
||||
last_capture_active = status.capture_active;
|
||||
last_stage = status.interactive_stage;
|
||||
if (secure_capture_started) {
|
||||
post_secure_restart_pending = false;
|
||||
post_secure_desktop_waiting_for_frame_.store(
|
||||
false, std::memory_order_relaxed);
|
||||
post_secure_desktop_drop_logged_.store(
|
||||
false, std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(
|
||||
0, std::memory_order_relaxed);
|
||||
post_secure_desktop_drop_logged_.store(false,
|
||||
std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
|
||||
} else if (secure_capture_ended) {
|
||||
post_secure_restart_pending = true;
|
||||
post_secure_restart_deadline_tick =
|
||||
@@ -1196,13 +1190,12 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
last_post_secure_restart_tick = 0;
|
||||
post_secure_desktop_waiting_for_frame_.store(
|
||||
true, std::memory_order_relaxed);
|
||||
post_secure_desktop_drop_logged_.store(
|
||||
false, std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(
|
||||
now, std::memory_order_relaxed);
|
||||
post_secure_desktop_drop_logged_.store(false,
|
||||
std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(now,
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
last_status_tick = now;
|
||||
}
|
||||
|
||||
if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) {
|
||||
@@ -1248,41 +1241,64 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
std::string error_message;
|
||||
bool frame_delivered = false;
|
||||
const bool show_cursor = show_cursor_.load(std::memory_order_relaxed);
|
||||
const int shared_fps =
|
||||
fps_ > 0 ? (std::max)(kSecureDesktopCaptureMinFps, fps_)
|
||||
: kSecureDesktopCaptureMinFps;
|
||||
const int shared_fps = fps_ > 0
|
||||
? (std::max)(kSecureDesktopCaptureMinFps, fps_)
|
||||
: kSecureDesktopCaptureMinFps;
|
||||
|
||||
if (StartSecureDesktopSharedCapture(status.active_session_id, left, top,
|
||||
width, height,
|
||||
status.interactive_stage,
|
||||
status.interactive_desktop,
|
||||
show_cursor, shared_fps,
|
||||
&error_message) &&
|
||||
if (secure_shared_capture_started_ &&
|
||||
frame_schedule.SharedCaptureStalled(now)) {
|
||||
LOG_WARN("Secure shared capture stalled; restarting producer");
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
frame_schedule.Reset(GetTickCount64());
|
||||
++shared_restarts;
|
||||
}
|
||||
|
||||
bool shared_ready = false;
|
||||
const bool was_started = secure_shared_capture_started_;
|
||||
if (was_started || frame_schedule.CanStart(now)) {
|
||||
shared_ready = StartSecureDesktopSharedCapture(
|
||||
status.active_session_id, left, top, width, height,
|
||||
status.interactive_stage, status.interactive_desktop, show_cursor,
|
||||
shared_fps, &error_message);
|
||||
if (!shared_ready) {
|
||||
frame_schedule.OnStartFailure(GetTickCount64());
|
||||
} else if (!was_started) {
|
||||
frame_schedule.Reset(GetTickCount64());
|
||||
}
|
||||
}
|
||||
|
||||
if (shared_ready &&
|
||||
ReadSecureDesktopSharedFrame(
|
||||
static_cast<DWORD>(frame_interval_ms + 20), &secure_frame,
|
||||
static_cast<DWORD>((std::max)(50, frame_interval_ms + 20)),
|
||||
&secure_frame, &captured_width, &captured_height, &error_message)) {
|
||||
frame_delivered = true;
|
||||
++shared_frames;
|
||||
frame_schedule.OnSharedFrame(GetTickCount64());
|
||||
}
|
||||
|
||||
const bool frame_pending = shared_ready && !frame_delivered &&
|
||||
IsPendingSecureDesktopFrame(error_message);
|
||||
if (frame_pending) ++shared_waits;
|
||||
if (shared_ready && !frame_delivered && !frame_pending) {
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
frame_schedule.OnStartFailure(GetTickCount64());
|
||||
}
|
||||
if (!frame_delivered && !frame_pending &&
|
||||
QuerySecureDesktopHelperFrame(
|
||||
status.active_session_id, left, top, width, height, show_cursor,
|
||||
status.interactive_stage, status.interactive_desktop, &secure_frame,
|
||||
&captured_width, &captured_height, &error_message)) {
|
||||
if (cb_orig_ && !secure_frame.empty()) {
|
||||
cb_orig_(secure_frame.data(), static_cast<int>(secure_frame.size()),
|
||||
captured_width, captured_height, display_name.c_str());
|
||||
}
|
||||
frame_delivered = true;
|
||||
++fallback_frames;
|
||||
}
|
||||
|
||||
if (!frame_delivered &&
|
||||
QuerySecureDesktopHelperFrame(status.active_session_id, left, top,
|
||||
width, height, show_cursor,
|
||||
status.interactive_stage,
|
||||
status.interactive_desktop,
|
||||
&secure_frame, &captured_width,
|
||||
&captured_height, &error_message)) {
|
||||
if (cb_orig_ && !secure_frame.empty()) {
|
||||
cb_orig_(secure_frame.data(), static_cast<int>(secure_frame.size()),
|
||||
captured_width, captured_height, display_name.c_str());
|
||||
}
|
||||
frame_delivered = true;
|
||||
if (frame_delivered && !secure_frame.empty()) {
|
||||
EmitCapturedFrame(secure_frame.data(),
|
||||
static_cast<int>(secure_frame.size()), captured_width,
|
||||
captured_height, display_name.c_str());
|
||||
}
|
||||
|
||||
if (!frame_delivered) {
|
||||
if (!frame_delivered && !frame_pending) {
|
||||
const bool transient_error =
|
||||
IsTransientSecureDesktopFrameError(error_message);
|
||||
const bool in_grace_period = capture_stage_started_tick != 0 &&
|
||||
@@ -1313,9 +1329,18 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(frame_interval_ms));
|
||||
const auto elapsed_ms =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - frame_started)
|
||||
.count();
|
||||
const int sleep_ms = SecureDesktopFrameSchedule::RemainingFrameDelay(
|
||||
frame_interval_ms, static_cast<uint64_t>(elapsed_ms));
|
||||
if (sleep_ms > 0) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
|
||||
}
|
||||
}
|
||||
|
||||
report_stats(true);
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -22,6 +23,8 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class CapturedNv12FramePool;
|
||||
|
||||
class ScreenCapturerWin : public ScreenCapturer {
|
||||
public:
|
||||
ScreenCapturerWin();
|
||||
@@ -53,6 +56,8 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
std::mutex alias_mutex_;
|
||||
std::vector<DisplayInfo> canonical_displays_;
|
||||
std::atomic<bool> invalid_stream_id_logged_{false};
|
||||
std::atomic<bool> native_output_logged_{false};
|
||||
std::atomic<bool> native_output_error_logged_{false};
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<bool> paused_{false};
|
||||
std::atomic<bool> show_cursor_{true};
|
||||
@@ -77,9 +82,13 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
std::string secure_shared_stage_;
|
||||
std::string secure_shared_desktop_;
|
||||
bool secure_shared_capture_started_ = false;
|
||||
std::shared_ptr<CapturedNv12FramePool> native_frame_pool_;
|
||||
|
||||
void BuildCanonicalFromImpl();
|
||||
void RebuildAliasesFromImpl();
|
||||
void EmitCapturedFrame(unsigned char* data, int size, int width, int height,
|
||||
const char* stream_id,
|
||||
const MiniRtcNativeVideoFrame* native_frame = nullptr);
|
||||
void StopSecureCaptureThread();
|
||||
bool RestartCaptureBackendAfterSecureDesktop();
|
||||
void SecureDesktopCaptureLoop();
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SECURE_DESKTOP_FRAME_SCHEDULE_H_
|
||||
#define _SECURE_DESKTOP_FRAME_SCHEDULE_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline bool IsPendingSecureDesktopFrame(const std::string& error) {
|
||||
return error == "frame_wait_timeout" ||
|
||||
error == "shared_frame_write_in_progress" ||
|
||||
error == "shared_frame_changed_during_read";
|
||||
}
|
||||
|
||||
// A 4K GDI frame can take longer than a single consumer wait. Do not issue
|
||||
// another expensive screenshot while the shared producer is still healthy.
|
||||
class SecureDesktopFrameSchedule {
|
||||
public:
|
||||
void Reset(uint64_t now) {
|
||||
last_shared_frame_ = now;
|
||||
next_start_attempt_ = 0;
|
||||
}
|
||||
|
||||
void OnSharedFrame(uint64_t now) { last_shared_frame_ = now; }
|
||||
bool SharedCaptureStalled(uint64_t now) const {
|
||||
return now - last_shared_frame_ >= 1000;
|
||||
}
|
||||
bool CanStart(uint64_t now) const { return now >= next_start_attempt_; }
|
||||
void OnStartFailure(uint64_t now) { next_start_attempt_ = now + 1000; }
|
||||
|
||||
static int RemainingFrameDelay(int interval_ms, uint64_t elapsed_ms) {
|
||||
return elapsed_ms >= static_cast<uint64_t>(interval_ms)
|
||||
? 0
|
||||
: interval_ms - static_cast<int>(elapsed_ms);
|
||||
}
|
||||
|
||||
private:
|
||||
uint64_t last_shared_frame_ = 0;
|
||||
uint64_t next_start_attempt_ = 0;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SECURE_DESKTOP_STATUS_POLLER_H_
|
||||
#define _SECURE_DESKTOP_STATUS_POLLER_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct SecureDesktopServiceStatus {
|
||||
bool service_available = false;
|
||||
bool capture_active = false;
|
||||
bool helper_running = false;
|
||||
uint32_t helper_process_id = 0;
|
||||
uint32_t active_session_id = 0xFFFFFFFF;
|
||||
uint32_t error_code = 0;
|
||||
std::string interactive_stage;
|
||||
std::string interactive_desktop;
|
||||
std::string error;
|
||||
|
||||
void UpdateFrom(SecureDesktopServiceStatus latest) {
|
||||
if (latest.service_available) {
|
||||
*this = std::move(latest);
|
||||
} else {
|
||||
// Failed IPC says nothing about the desktop. Retain the last confirmed
|
||||
// capture state until a valid response reports a change or an unlock.
|
||||
service_available = false;
|
||||
error = std::move(latest.error);
|
||||
error_code = latest.error_code;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// The query runs without holding the snapshot mutex. Slow service/helper IPC
|
||||
// must not stop delivery of frames that are already available in shared memory.
|
||||
class SecureDesktopStatusPoller {
|
||||
public:
|
||||
using Query = std::function<SecureDesktopServiceStatus()>;
|
||||
|
||||
struct Sample {
|
||||
SecureDesktopServiceStatus status;
|
||||
int64_t query_ms = 0;
|
||||
};
|
||||
|
||||
explicit SecureDesktopStatusPoller(Query query,
|
||||
std::chrono::milliseconds interval)
|
||||
: thread_([this, query = std::move(query), interval]() {
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
while (!stopping_) {
|
||||
lock.unlock();
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
auto latest = query();
|
||||
const auto elapsed =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - started);
|
||||
lock.lock();
|
||||
sample_.status.UpdateFrom(std::move(latest));
|
||||
sample_.query_ms = elapsed.count();
|
||||
pending_ = true;
|
||||
// Even a failed/slow query gets a pause, avoiding a retry storm.
|
||||
wake_.wait_for(lock, interval, [this] { return stopping_; });
|
||||
}
|
||||
}) {}
|
||||
|
||||
~SecureDesktopStatusPoller() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
stopping_ = true;
|
||||
}
|
||||
wake_.notify_one();
|
||||
thread_.join();
|
||||
}
|
||||
|
||||
SecureDesktopStatusPoller(const SecureDesktopStatusPoller&) = delete;
|
||||
SecureDesktopStatusPoller& operator=(const SecureDesktopStatusPoller&) =
|
||||
delete;
|
||||
|
||||
std::optional<Sample> Take() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!pending_) {
|
||||
return std::nullopt;
|
||||
}
|
||||
pending_ = false;
|
||||
return sample_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex mutex_;
|
||||
std::condition_variable wake_;
|
||||
bool stopping_ = false;
|
||||
bool pending_ = false;
|
||||
Sample sample_;
|
||||
std::thread thread_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _INTERACTIVE_DESKTOP_H_
|
||||
#define _INTERACTIVE_DESKTOP_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline std::wstring GetDesktopNameW(HDESK desktop) {
|
||||
DWORD bytes_needed = 0;
|
||||
if (desktop == nullptr) {
|
||||
return {};
|
||||
}
|
||||
GetUserObjectInformationW(desktop, UOI_NAME, nullptr, 0, &bytes_needed);
|
||||
if (bytes_needed == 0) {
|
||||
return {};
|
||||
}
|
||||
std::wstring name(bytes_needed / sizeof(wchar_t), L'\0');
|
||||
if (!GetUserObjectInformationW(desktop, UOI_NAME, name.data(), bytes_needed,
|
||||
&bytes_needed)) {
|
||||
return {};
|
||||
}
|
||||
while (!name.empty() && name.back() == L'\0') {
|
||||
name.pop_back();
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
// Use only on worker threads without windows or hooks. Restore the original
|
||||
// desktop before closing the handle: Windows cannot close a desktop in use.
|
||||
class ScopedInteractiveDesktop {
|
||||
public:
|
||||
ScopedInteractiveDesktop()
|
||||
: original_(GetThreadDesktop(GetCurrentThreadId())) {}
|
||||
ScopedInteractiveDesktop(const ScopedInteractiveDesktop&) = delete;
|
||||
ScopedInteractiveDesktop& operator=(const ScopedInteractiveDesktop&) = delete;
|
||||
|
||||
~ScopedInteractiveDesktop() {
|
||||
if (desktop_ != nullptr) {
|
||||
SetThreadDesktop(original_);
|
||||
CloseDesktop(desktop_);
|
||||
}
|
||||
}
|
||||
|
||||
bool Bind(const std::wstring& fallback_name) {
|
||||
constexpr ACCESS_MASK access =
|
||||
DESKTOP_CREATEWINDOW | DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS |
|
||||
DESKTOP_SWITCHDESKTOP | DESKTOP_JOURNALPLAYBACK;
|
||||
// SendInput requires playback access on the attached desktop; otherwise
|
||||
// it returns ERROR_ACCESS_DENIED even for a correctly attached SYSTEM helper.
|
||||
// The service's stage/name can lag a lock, unlock, or UAC transition.
|
||||
// Opening a cached desktop can succeed even when it is no longer visible.
|
||||
HDESK desktop = OpenInputDesktop(0, FALSE, access);
|
||||
if (desktop == nullptr && !fallback_name.empty()) {
|
||||
desktop = OpenDesktopW(fallback_name.c_str(), 0, FALSE, access);
|
||||
}
|
||||
if (desktop == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (!SetThreadDesktop(desktop)) {
|
||||
const DWORD error = GetLastError();
|
||||
CloseDesktop(desktop);
|
||||
SetLastError(error);
|
||||
return false;
|
||||
}
|
||||
if (desktop_ != nullptr) {
|
||||
CloseDesktop(desktop_);
|
||||
}
|
||||
desktop_ = desktop;
|
||||
name_ = GetDesktopNameW(desktop_);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsInputDesktop() const {
|
||||
HDESK input = OpenInputDesktop(0, FALSE, DESKTOP_READOBJECTS);
|
||||
if (input == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const std::wstring input_name = GetDesktopNameW(input);
|
||||
CloseDesktop(input);
|
||||
return !name_.empty() && _wcsicmp(name_.c_str(), input_name.c_str()) == 0;
|
||||
}
|
||||
|
||||
const std::wstring& name() const { return name_; }
|
||||
|
||||
private:
|
||||
HDESK original_ = nullptr;
|
||||
HDESK desktop_ = nullptr;
|
||||
std::wstring name_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _NAMED_PIPE_DEADLINE_H_
|
||||
#define _NAMED_PIPE_DEADLINE_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace pipe_deadline_detail {
|
||||
struct Handle {
|
||||
explicit Handle(HANDLE handle = INVALID_HANDLE_VALUE) : value(handle) {}
|
||||
Handle(const Handle&) = delete;
|
||||
Handle& operator=(const Handle&) = delete;
|
||||
HANDLE value = INVALID_HANDLE_VALUE;
|
||||
~Handle() {
|
||||
if (value != INVALID_HANDLE_VALUE && value != nullptr) CloseHandle(value);
|
||||
}
|
||||
};
|
||||
|
||||
inline DWORD Remaining(ULONGLONG deadline) {
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
return now >= deadline ? 0 : static_cast<DWORD>(deadline - now);
|
||||
}
|
||||
|
||||
inline bool Transfer(HANDLE pipe, HANDLE event, bool write, void* data,
|
||||
DWORD size, ULONGLONG deadline, DWORD* transferred,
|
||||
DWORD* error) {
|
||||
*transferred = 0;
|
||||
if (Remaining(deadline) == 0) {
|
||||
*error = ERROR_SEM_TIMEOUT;
|
||||
return false;
|
||||
}
|
||||
ResetEvent(event);
|
||||
OVERLAPPED operation{};
|
||||
operation.hEvent = event;
|
||||
const BOOL complete =
|
||||
write ? WriteFile(pipe, data, size, transferred, &operation)
|
||||
: ReadFile(pipe, data, size, transferred, &operation);
|
||||
*error = complete ? ERROR_SUCCESS : GetLastError();
|
||||
if (*error == ERROR_IO_PENDING) {
|
||||
const DWORD wait = WaitForSingleObject(event, Remaining(deadline));
|
||||
if (wait != WAIT_OBJECT_0) {
|
||||
*error = wait == WAIT_TIMEOUT ? ERROR_SEM_TIMEOUT : GetLastError();
|
||||
// Keep the OVERLAPPED and buffer alive until cancellation completes.
|
||||
CancelIoEx(pipe, &operation);
|
||||
DWORD ignored = 0;
|
||||
GetOverlappedResult(pipe, &operation, &ignored, TRUE);
|
||||
return false;
|
||||
}
|
||||
} else if (*error != ERROR_SUCCESS && *error != ERROR_MORE_DATA) {
|
||||
return false;
|
||||
}
|
||||
// Immediate MORE_DATA also needs GetOverlappedResult: ReadFile may leave
|
||||
// its byte-count output at zero even though part of the message was read.
|
||||
const BOOL result = GetOverlappedResult(pipe, &operation, transferred, FALSE);
|
||||
*error = result ? ERROR_SUCCESS : GetLastError();
|
||||
return result != FALSE;
|
||||
}
|
||||
} // namespace pipe_deadline_detail
|
||||
|
||||
// One deadline covers connection, write, and every response chunk. A connect
|
||||
// timeout alone does not bound a synchronous ReadFile on an unresponsive peer.
|
||||
inline bool QueryNamedPipeWithDeadline(const std::wstring& name,
|
||||
const std::string& command,
|
||||
DWORD timeout_ms,
|
||||
std::vector<uint8_t>* response,
|
||||
std::string* error, DWORD* error_code) {
|
||||
using namespace pipe_deadline_detail;
|
||||
response->clear();
|
||||
error->clear();
|
||||
*error_code = ERROR_SUCCESS;
|
||||
const ULONGLONG deadline = GetTickCount64() + timeout_ms;
|
||||
Handle pipe;
|
||||
while (true) {
|
||||
const DWORD remaining = Remaining(deadline);
|
||||
if (remaining == 0) break;
|
||||
if (WaitNamedPipeW(name.c_str(), remaining)) {
|
||||
pipe.value =
|
||||
CreateFileW(name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr,
|
||||
OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr);
|
||||
if (pipe.value != INVALID_HANDLE_VALUE) break;
|
||||
}
|
||||
*error_code = GetLastError();
|
||||
if (*error_code != ERROR_FILE_NOT_FOUND && *error_code != ERROR_PIPE_BUSY &&
|
||||
*error_code != ERROR_SEM_TIMEOUT)
|
||||
break;
|
||||
Sleep((std::min)(DWORD{10}, Remaining(deadline)));
|
||||
}
|
||||
if (pipe.value == INVALID_HANDLE_VALUE) {
|
||||
*error = "pipe_unavailable";
|
||||
if (*error_code == ERROR_SUCCESS) *error_code = ERROR_SEM_TIMEOUT;
|
||||
return false;
|
||||
}
|
||||
DWORD mode = PIPE_READMODE_MESSAGE;
|
||||
if (!SetNamedPipeHandleState(pipe.value, &mode, nullptr, nullptr)) {
|
||||
*error = "pipe_connect_failed";
|
||||
*error_code = GetLastError();
|
||||
return false;
|
||||
}
|
||||
Handle event{CreateEventW(nullptr, TRUE, FALSE, nullptr)};
|
||||
if (event.value == nullptr) {
|
||||
*error = "pipe_connect_failed";
|
||||
*error_code = GetLastError();
|
||||
return false;
|
||||
}
|
||||
DWORD transferred = 0;
|
||||
if (!Transfer(pipe.value, event.value, true,
|
||||
const_cast<char*>(command.data()),
|
||||
static_cast<DWORD>(command.size()), deadline, &transferred,
|
||||
error_code) ||
|
||||
transferred != command.size()) {
|
||||
*error = "pipe_write_failed";
|
||||
return false;
|
||||
}
|
||||
std::vector<uint8_t> chunk(64 * 1024);
|
||||
while (true) {
|
||||
const bool complete = Transfer(pipe.value, event.value, false, chunk.data(),
|
||||
static_cast<DWORD>(chunk.size()), deadline,
|
||||
&transferred, error_code);
|
||||
if (!complete && *error_code != ERROR_MORE_DATA) {
|
||||
response->clear();
|
||||
*error = "pipe_read_failed";
|
||||
return false;
|
||||
}
|
||||
response->insert(response->end(), chunk.begin(),
|
||||
chunk.begin() + transferred);
|
||||
if (complete) return true;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "interactive_state.h"
|
||||
#include "path_manager.h"
|
||||
#include "rd_log.h"
|
||||
#include "session_helper_shared.h"
|
||||
@@ -1459,18 +1460,18 @@ std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
|
||||
|
||||
std::string CrossDeskServiceHost::ResolveInteractiveDesktopLocked(
|
||||
const std::string& interactive_stage) const {
|
||||
if (interactive_stage == "lock-screen") {
|
||||
return "Default";
|
||||
}
|
||||
|
||||
if (session_helper_status_ok_ &&
|
||||
session_helper_report_input_desktop_available_ &&
|
||||
!session_helper_report_input_desktop_.empty() &&
|
||||
(interactive_stage == "credential-ui" ||
|
||||
(IsSecureDesktopInteractionRequired(interactive_stage) ||
|
||||
session_helper_report_consent_ui_visible_)) {
|
||||
return session_helper_report_input_desktop_;
|
||||
}
|
||||
|
||||
if (interactive_stage == "lock-screen") {
|
||||
return "Default";
|
||||
}
|
||||
|
||||
if (input_desktop_available_ && !input_desktop_name_.empty() &&
|
||||
(interactive_stage == "credential-ui" || consent_ui_visible_)) {
|
||||
return input_desktop_name_;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "interactive_desktop.h"
|
||||
#include "path_manager.h"
|
||||
#include "rd_log.h"
|
||||
#include "session_helper_shared.h"
|
||||
@@ -30,6 +31,7 @@ namespace {
|
||||
|
||||
using crossdesk::get_logger;
|
||||
using crossdesk::InitLogger;
|
||||
using crossdesk::ScopedInteractiveDesktop;
|
||||
using Json = nlohmann::json;
|
||||
|
||||
constexpr DWORD kSessionHelperStatePollMs = 1000;
|
||||
@@ -538,123 +540,8 @@ void HelperIpcServerLoop(HANDLE stop_event, DWORD session_id,
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring GetDesktopNameW(HDESK desktop) {
|
||||
if (desktop == nullptr) {
|
||||
return L"";
|
||||
}
|
||||
|
||||
DWORD bytes_needed = 0;
|
||||
GetUserObjectInformationW(desktop, UOI_NAME, nullptr, 0, &bytes_needed);
|
||||
if (bytes_needed == 0) {
|
||||
return L"";
|
||||
}
|
||||
|
||||
std::wstring desktop_name(bytes_needed / sizeof(wchar_t), L'\0');
|
||||
if (!GetUserObjectInformationW(desktop, UOI_NAME, desktop_name.data(),
|
||||
bytes_needed, &bytes_needed)) {
|
||||
return L"";
|
||||
}
|
||||
|
||||
while (!desktop_name.empty() && desktop_name.back() == L'\0') {
|
||||
desktop_name.pop_back();
|
||||
}
|
||||
return desktop_name;
|
||||
}
|
||||
|
||||
std::wstring GetCurrentThreadDesktopNameW() {
|
||||
return GetDesktopNameW(GetThreadDesktop(GetCurrentThreadId()));
|
||||
}
|
||||
|
||||
constexpr ACCESS_MASK kCrossDeskInteractiveDesktopAccess =
|
||||
DESKTOP_CREATEWINDOW | DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS |
|
||||
DESKTOP_SWITCHDESKTOP;
|
||||
|
||||
bool EnsureThreadDesktop(const wchar_t* desktop_name,
|
||||
HDESK* opened_desktop_out = nullptr) {
|
||||
if (desktop_name == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring current_desktop = GetCurrentThreadDesktopNameW();
|
||||
if (!current_desktop.empty() &&
|
||||
_wcsicmp(current_desktop.c_str(), desktop_name) == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
HDESK desktop =
|
||||
OpenDesktopW(desktop_name, 0, FALSE, kCrossDeskInteractiveDesktopAccess);
|
||||
if (desktop == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SetThreadDesktop(desktop)) {
|
||||
CloseDesktop(desktop);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opened_desktop_out != nullptr) {
|
||||
*opened_desktop_out = desktop;
|
||||
} else {
|
||||
CloseDesktop(desktop);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EnsureThreadInputDesktop(HDESK* opened_desktop_out = nullptr) {
|
||||
HDESK desktop =
|
||||
OpenInputDesktop(0, FALSE, kCrossDeskInteractiveDesktopAccess);
|
||||
if (desktop == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::wstring input_desktop = GetDesktopNameW(desktop);
|
||||
const std::wstring current_desktop = GetCurrentThreadDesktopNameW();
|
||||
if (!input_desktop.empty() && !current_desktop.empty() &&
|
||||
_wcsicmp(input_desktop.c_str(), current_desktop.c_str()) == 0) {
|
||||
if (opened_desktop_out != nullptr) {
|
||||
*opened_desktop_out = desktop;
|
||||
} else {
|
||||
CloseDesktop(desktop);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!SetThreadDesktop(desktop)) {
|
||||
CloseDesktop(desktop);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opened_desktop_out != nullptr) {
|
||||
*opened_desktop_out = desktop;
|
||||
} else {
|
||||
CloseDesktop(desktop);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EnsureThreadInteractiveDesktop(HDESK* opened_desktop_out = nullptr) {
|
||||
if (opened_desktop_out != nullptr) {
|
||||
*opened_desktop_out = nullptr;
|
||||
}
|
||||
|
||||
if (EnsureThreadInputDesktop(opened_desktop_out)) {
|
||||
return true;
|
||||
}
|
||||
const DWORD input_desktop_error = GetLastError();
|
||||
|
||||
if (EnsureThreadDesktop(L"Winlogon", opened_desktop_out)) {
|
||||
return true;
|
||||
}
|
||||
const DWORD winlogon_error = GetLastError();
|
||||
|
||||
LOG_WARN(
|
||||
"Failed to switch secure input helper desktop, input_error={}, "
|
||||
"winlogon_error={}, current='{}'",
|
||||
input_desktop_error, winlogon_error,
|
||||
WideToUtf8(GetCurrentThreadDesktopNameW()));
|
||||
SetLastError(winlogon_error != ERROR_SUCCESS ? winlogon_error
|
||||
: input_desktop_error);
|
||||
return false;
|
||||
return crossdesk::GetDesktopNameW(GetThreadDesktop(GetCurrentThreadId()));
|
||||
}
|
||||
|
||||
std::wstring DesktopNameForInteractiveStage(
|
||||
@@ -700,8 +587,10 @@ DesktopSwitchDetails BuildDesktopSwitchDetails(
|
||||
return details;
|
||||
}
|
||||
|
||||
InputInjectionResult BuildInputSuccess() {
|
||||
return {};
|
||||
InputInjectionResult BuildInputSuccess(DesktopSwitchDetails desktop) {
|
||||
InputInjectionResult result;
|
||||
result.desktop = std::move(desktop);
|
||||
return result;
|
||||
}
|
||||
|
||||
InputInjectionResult BuildInputFailure(const char* error, DWORD error_code,
|
||||
@@ -732,48 +621,23 @@ Json BuildInputFailureJson(const InputInjectionResult& result) {
|
||||
bool EnsureThreadInteractiveDesktopForStage(
|
||||
const std::string& interactive_stage,
|
||||
const std::string& interactive_desktop,
|
||||
HDESK* opened_desktop_out = nullptr,
|
||||
ScopedInteractiveDesktop* desktop,
|
||||
DesktopSwitchDetails* switch_details = nullptr) {
|
||||
if (opened_desktop_out != nullptr) {
|
||||
*opened_desktop_out = nullptr;
|
||||
}
|
||||
|
||||
DesktopSwitchDetails local_details =
|
||||
BuildDesktopSwitchDetails(interactive_stage, interactive_desktop);
|
||||
if (switch_details != nullptr) {
|
||||
*switch_details = local_details;
|
||||
}
|
||||
|
||||
const std::wstring desktop_name =
|
||||
std::wstring desktop_name =
|
||||
DesktopNameForInteractiveStage(interactive_stage, interactive_desktop);
|
||||
if (!desktop_name.empty()) {
|
||||
if (EnsureThreadDesktop(desktop_name.c_str(), opened_desktop_out)) {
|
||||
if (switch_details != nullptr) {
|
||||
switch_details->current_desktop =
|
||||
WideToUtf8(GetCurrentThreadDesktopNameW());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const DWORD error = GetLastError();
|
||||
if (switch_details != nullptr) {
|
||||
switch_details->error_code = error;
|
||||
switch_details->current_desktop =
|
||||
WideToUtf8(GetCurrentThreadDesktopNameW());
|
||||
}
|
||||
LOG_WARN(
|
||||
"Failed to switch secure input helper to stage desktop, stage='{}', "
|
||||
"desktop='{}', error={}, current='{}'",
|
||||
interactive_stage, WideToUtf8(desktop_name),
|
||||
error,
|
||||
WideToUtf8(GetCurrentThreadDesktopNameW()));
|
||||
SetLastError(error);
|
||||
return false;
|
||||
if (desktop_name.empty()) {
|
||||
desktop_name = L"Winlogon";
|
||||
}
|
||||
|
||||
if (EnsureThreadInteractiveDesktop(opened_desktop_out)) {
|
||||
if (desktop->Bind(desktop_name)) {
|
||||
if (switch_details != nullptr) {
|
||||
switch_details->current_desktop = WideToUtf8(GetCurrentThreadDesktopNameW());
|
||||
switch_details->target_desktop = WideToUtf8(desktop->name());
|
||||
switch_details->current_desktop = switch_details->target_desktop;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -783,18 +647,15 @@ bool EnsureThreadInteractiveDesktopForStage(
|
||||
switch_details->error_code = error;
|
||||
switch_details->current_desktop = WideToUtf8(GetCurrentThreadDesktopNameW());
|
||||
}
|
||||
LOG_WARN(
|
||||
"Failed to switch secure input helper desktop, stage='{}', "
|
||||
"fallback='{}', error={}, current='{}'",
|
||||
interactive_stage, WideToUtf8(desktop_name), error,
|
||||
WideToUtf8(GetCurrentThreadDesktopNameW()));
|
||||
SetLastError(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct ScopedDesktopHandle {
|
||||
HDESK handle = nullptr;
|
||||
~ScopedDesktopHandle() {
|
||||
if (handle != nullptr) {
|
||||
CloseDesktop(handle);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bool PreferSideSpecificVkInjection(int key_code) {
|
||||
switch (key_code) {
|
||||
case VK_LSHIFT:
|
||||
@@ -815,11 +676,11 @@ InputInjectionResult InjectKeyboardInput(
|
||||
int key_code, bool is_down, uint32_t scan_code, bool extended,
|
||||
const std::string& interactive_stage,
|
||||
const std::string& interactive_desktop) {
|
||||
ScopedDesktopHandle desktop;
|
||||
ScopedInteractiveDesktop desktop;
|
||||
DesktopSwitchDetails desktop_switch;
|
||||
if (!EnsureThreadInteractiveDesktopForStage(interactive_stage,
|
||||
interactive_desktop,
|
||||
&desktop.handle,
|
||||
&desktop,
|
||||
&desktop_switch)) {
|
||||
const DWORD error = GetLastError();
|
||||
return BuildInputFailure("switch_interactive_desktop_failed",
|
||||
@@ -875,7 +736,7 @@ InputInjectionResult InjectKeyboardInput(
|
||||
desktop_switch);
|
||||
}
|
||||
|
||||
return BuildInputSuccess();
|
||||
return BuildInputSuccess(std::move(desktop_switch));
|
||||
}
|
||||
|
||||
void ParseInteractionTail(const std::string& tail,
|
||||
@@ -1192,11 +1053,11 @@ INPUT BuildAbsoluteMouseMoveInput(int x, int y) {
|
||||
}
|
||||
|
||||
InputInjectionResult InjectMouseInput(const SecureMouseRequest& request) {
|
||||
ScopedDesktopHandle desktop;
|
||||
ScopedInteractiveDesktop desktop;
|
||||
DesktopSwitchDetails desktop_switch;
|
||||
if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage,
|
||||
request.interactive_desktop,
|
||||
&desktop.handle,
|
||||
&desktop,
|
||||
&desktop_switch)) {
|
||||
const DWORD error = GetLastError();
|
||||
return BuildInputFailure("switch_interactive_desktop_failed",
|
||||
@@ -1256,13 +1117,91 @@ InputInjectionResult InjectMouseInput(const SecureMouseRequest& request) {
|
||||
desktop_switch);
|
||||
}
|
||||
|
||||
return BuildInputSuccess();
|
||||
return BuildInputSuccess(std::move(desktop_switch));
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BuildTextResponseBytes(const std::string& response) {
|
||||
return std::vector<uint8_t>(response.begin(), response.end());
|
||||
}
|
||||
|
||||
// Own the desktop before its GDI objects, and release those objects before
|
||||
// restoring the original desktop. Both capture paths use the same lifecycle.
|
||||
struct SecureDesktopGdiResources {
|
||||
ScopedInteractiveDesktop desktop;
|
||||
HDC screen_dc = nullptr;
|
||||
HDC mem_dc = nullptr;
|
||||
HBITMAP dib = nullptr;
|
||||
HGDIOBJ old_bitmap = nullptr;
|
||||
void* bits = nullptr;
|
||||
const char* error = nullptr;
|
||||
DWORD error_code = ERROR_SUCCESS;
|
||||
|
||||
~SecureDesktopGdiResources() {
|
||||
if (old_bitmap != nullptr && old_bitmap != HGDI_ERROR) {
|
||||
SelectObject(mem_dc, old_bitmap);
|
||||
}
|
||||
if (dib != nullptr) DeleteObject(dib);
|
||||
if (mem_dc != nullptr) DeleteDC(mem_dc);
|
||||
if (screen_dc != nullptr) ReleaseDC(nullptr, screen_dc);
|
||||
}
|
||||
|
||||
bool Fail(const char* reason) {
|
||||
error = reason;
|
||||
error_code = GetLastError();
|
||||
if (error_code == ERROR_SUCCESS) error_code = ERROR_GEN_FAILURE;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Initialize(const SecureCaptureRequest& request) {
|
||||
const auto fallback = DesktopNameForInteractiveStage(
|
||||
request.interactive_stage, request.interactive_desktop);
|
||||
if (!desktop.Bind(fallback.empty() ? L"Winlogon" : fallback)) {
|
||||
return Fail("switch_interactive_desktop_failed");
|
||||
}
|
||||
screen_dc = GetDC(nullptr);
|
||||
if (screen_dc == nullptr) return Fail("get_dc_failed");
|
||||
mem_dc = CreateCompatibleDC(screen_dc);
|
||||
if (mem_dc == nullptr) return Fail("create_dc_failed");
|
||||
BITMAPINFO bmi{};
|
||||
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bmi.bmiHeader.biWidth = request.width;
|
||||
bmi.bmiHeader.biHeight = -request.height;
|
||||
bmi.bmiHeader.biPlanes = 1;
|
||||
bmi.bmiHeader.biBitCount = 32;
|
||||
bmi.bmiHeader.biCompression = BI_RGB;
|
||||
dib = CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
|
||||
if (dib == nullptr || bits == nullptr) return Fail("create_dib_failed");
|
||||
old_bitmap = SelectObject(mem_dc, dib);
|
||||
if (old_bitmap == nullptr || old_bitmap == HGDI_ERROR) {
|
||||
return Fail("select_bitmap_failed");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Capture(const SecureCaptureRequest& request) {
|
||||
if (!BitBlt(mem_dc, 0, 0, request.width, request.height, screen_dc,
|
||||
request.left, request.top, SRCCOPY | CAPTUREBLT)) {
|
||||
return Fail("bitblt_failed");
|
||||
}
|
||||
if (request.show_cursor) {
|
||||
CURSORINFO cursor{};
|
||||
cursor.cbSize = sizeof(cursor);
|
||||
if (GetCursorInfo(&cursor) && cursor.flags == CURSOR_SHOWING &&
|
||||
cursor.hCursor != nullptr) {
|
||||
const int x = cursor.ptScreenPos.x - request.left;
|
||||
const int y = cursor.ptScreenPos.y - request.top;
|
||||
if (x >= -64 && y >= -64 && x < request.width + 64 &&
|
||||
y < request.height + 64) {
|
||||
DrawIconEx(mem_dc, x, y, cursor.hCursor, 0, 0, 0, nullptr, DI_NORMAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Complete GDI writes before libyuv reads the DIB's pixel memory.
|
||||
GdiFlush();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<uint8_t> CaptureSecureDesktopFrame(
|
||||
const SecureCaptureRequest& request,
|
||||
SecureCaptureBuffers* capture_buffers) {
|
||||
@@ -1270,87 +1209,21 @@ std::vector<uint8_t> CaptureSecureDesktopFrame(
|
||||
return BuildTextResponseBytes(BuildErrorJson("invalid_capture_buffers"));
|
||||
}
|
||||
|
||||
ScopedDesktopHandle desktop;
|
||||
if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage,
|
||||
request.interactive_desktop,
|
||||
&desktop.handle)) {
|
||||
const DWORD error = GetLastError();
|
||||
return BuildTextResponseBytes(BuildErrorJson(
|
||||
"switch_interactive_desktop_failed",
|
||||
error != ERROR_SUCCESS ? error : ERROR_GEN_FAILURE));
|
||||
}
|
||||
|
||||
HDC screen_dc = GetDC(nullptr);
|
||||
if (screen_dc == nullptr) {
|
||||
SecureDesktopGdiResources capture;
|
||||
if (!capture.Initialize(request) || !capture.Capture(request)) {
|
||||
return BuildTextResponseBytes(
|
||||
BuildErrorJson("get_dc_failed", GetLastError()));
|
||||
}
|
||||
|
||||
HDC mem_dc = CreateCompatibleDC(screen_dc);
|
||||
if (mem_dc == nullptr) {
|
||||
const DWORD error = GetLastError();
|
||||
ReleaseDC(nullptr, screen_dc);
|
||||
return BuildTextResponseBytes(BuildErrorJson("create_dc_failed", error));
|
||||
}
|
||||
|
||||
BITMAPINFO bmi{};
|
||||
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bmi.bmiHeader.biWidth = request.width;
|
||||
bmi.bmiHeader.biHeight = -request.height;
|
||||
bmi.bmiHeader.biPlanes = 1;
|
||||
bmi.bmiHeader.biBitCount = 32;
|
||||
bmi.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
void* bits = nullptr;
|
||||
HBITMAP dib =
|
||||
CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
|
||||
if (dib == nullptr || bits == nullptr) {
|
||||
const DWORD error = GetLastError();
|
||||
DeleteDC(mem_dc);
|
||||
ReleaseDC(nullptr, screen_dc);
|
||||
return BuildTextResponseBytes(BuildErrorJson("create_dib_failed", error));
|
||||
}
|
||||
|
||||
HGDIOBJ old_bitmap = SelectObject(mem_dc, dib);
|
||||
if (!BitBlt(mem_dc, 0, 0, request.width, request.height, screen_dc,
|
||||
request.left, request.top, SRCCOPY | CAPTUREBLT)) {
|
||||
const DWORD error = GetLastError();
|
||||
SelectObject(mem_dc, old_bitmap);
|
||||
DeleteObject(dib);
|
||||
DeleteDC(mem_dc);
|
||||
ReleaseDC(nullptr, screen_dc);
|
||||
return BuildTextResponseBytes(BuildErrorJson("bitblt_failed", error));
|
||||
}
|
||||
|
||||
if (request.show_cursor) {
|
||||
CURSORINFO cursor_info{};
|
||||
cursor_info.cbSize = sizeof(CURSORINFO);
|
||||
if (GetCursorInfo(&cursor_info) && cursor_info.flags == CURSOR_SHOWING &&
|
||||
cursor_info.hCursor != nullptr) {
|
||||
const int cursor_x = cursor_info.ptScreenPos.x - request.left;
|
||||
const int cursor_y = cursor_info.ptScreenPos.y - request.top;
|
||||
if (cursor_x >= -64 && cursor_y >= -64 && cursor_x < request.width + 64 &&
|
||||
cursor_y < request.height + 64) {
|
||||
DrawIconEx(mem_dc, cursor_x, cursor_y, cursor_info.hCursor, 0, 0, 0,
|
||||
nullptr, DI_NORMAL);
|
||||
}
|
||||
}
|
||||
BuildErrorJson(capture.error, capture.error_code));
|
||||
}
|
||||
|
||||
const size_t nv12_size =
|
||||
static_cast<size_t>(request.width) * request.height * 3 / 2;
|
||||
capture_buffers->nv12_frame.resize(nv12_size);
|
||||
const int convert_result = libyuv::ARGBToNV12(
|
||||
static_cast<const uint8_t*>(bits), request.width * 4,
|
||||
static_cast<const uint8_t*>(capture.bits), request.width * 4,
|
||||
capture_buffers->nv12_frame.data(), request.width,
|
||||
capture_buffers->nv12_frame.data() + request.width * request.height,
|
||||
request.width, request.width, request.height);
|
||||
|
||||
SelectObject(mem_dc, old_bitmap);
|
||||
DeleteObject(dib);
|
||||
DeleteDC(mem_dc);
|
||||
ReleaseDC(nullptr, screen_dc);
|
||||
|
||||
if (convert_result != 0) {
|
||||
return BuildTextResponseBytes(BuildErrorJson("argb_to_nv12_failed"));
|
||||
}
|
||||
@@ -1397,8 +1270,7 @@ void CloseSecureDesktopSharedCaptureResourcesLocked(
|
||||
capture_state->frame_view_size = 0;
|
||||
}
|
||||
|
||||
void SecureDesktopSharedCaptureThread(
|
||||
SecureSharedCaptureState* capture_state) {
|
||||
void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
||||
if (capture_state == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -1421,79 +1293,65 @@ void SecureDesktopSharedCaptureThread(
|
||||
static_cast<size_t>(request.width) * request.height * 3 / 2;
|
||||
std::vector<uint8_t> nv12_frame(nv12_size);
|
||||
|
||||
ScopedDesktopHandle desktop;
|
||||
if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage,
|
||||
request.interactive_desktop,
|
||||
&desktop.handle)) {
|
||||
LOG_ERROR("Secure shared capture desktop switch failed, error={}",
|
||||
GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
HDC screen_dc = GetDC(nullptr);
|
||||
if (screen_dc == nullptr) {
|
||||
LOG_ERROR("Secure shared capture GetDC failed, error={}", GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
HDC mem_dc = CreateCompatibleDC(screen_dc);
|
||||
if (mem_dc == nullptr) {
|
||||
LOG_ERROR("Secure shared capture CreateCompatibleDC failed, error={}",
|
||||
GetLastError());
|
||||
ReleaseDC(nullptr, screen_dc);
|
||||
return;
|
||||
}
|
||||
|
||||
BITMAPINFO bmi{};
|
||||
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bmi.bmiHeader.biWidth = request.width;
|
||||
bmi.bmiHeader.biHeight = -request.height;
|
||||
bmi.bmiHeader.biPlanes = 1;
|
||||
bmi.bmiHeader.biBitCount = 32;
|
||||
bmi.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
void* bits = nullptr;
|
||||
HBITMAP dib =
|
||||
CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
|
||||
if (dib == nullptr || bits == nullptr) {
|
||||
LOG_ERROR("Secure shared capture CreateDIBSection failed, error={}",
|
||||
GetLastError());
|
||||
DeleteDC(mem_dc);
|
||||
ReleaseDC(nullptr, screen_dc);
|
||||
return;
|
||||
}
|
||||
|
||||
HGDIOBJ old_bitmap = SelectObject(mem_dc, dib);
|
||||
std::unique_ptr<SecureDesktopGdiResources> capture;
|
||||
ULONGLONG last_error_tick = 0;
|
||||
auto stats_started = std::chrono::steady_clock::now();
|
||||
unsigned frame_count = 0;
|
||||
double capture_ms = 0, convert_ms = 0, publish_ms = 0;
|
||||
auto report_stats = [&](bool force) {
|
||||
const auto elapsed = std::chrono::duration<double>(
|
||||
std::chrono::steady_clock::now() - stats_started)
|
||||
.count();
|
||||
if (elapsed <= 0 || (!force && elapsed < 5.0)) return;
|
||||
if (frame_count > 0) {
|
||||
LOG_INFO(
|
||||
"Secure capture producer: fps={:.1f}, capture_avg_ms={:.1f}, "
|
||||
"convert_avg_ms={:.1f}, publish_avg_ms={:.1f}, size={}x{}",
|
||||
frame_count / elapsed, capture_ms / frame_count,
|
||||
convert_ms / frame_count, publish_ms / frame_count, request.width,
|
||||
request.height);
|
||||
}
|
||||
stats_started = std::chrono::steady_clock::now();
|
||||
frame_count = 0;
|
||||
capture_ms = convert_ms = publish_ms = 0;
|
||||
};
|
||||
while (!capture_state->stop_requested.load(std::memory_order_relaxed)) {
|
||||
const auto frame_started = std::chrono::steady_clock::now();
|
||||
if (BitBlt(mem_dc, 0, 0, request.width, request.height, screen_dc,
|
||||
request.left, request.top, SRCCOPY | CAPTUREBLT)) {
|
||||
if (request.show_cursor) {
|
||||
CURSORINFO cursor_info{};
|
||||
cursor_info.cbSize = sizeof(CURSORINFO);
|
||||
if (GetCursorInfo(&cursor_info) &&
|
||||
cursor_info.flags == CURSOR_SHOWING &&
|
||||
cursor_info.hCursor != nullptr) {
|
||||
const int cursor_x = cursor_info.ptScreenPos.x - request.left;
|
||||
const int cursor_y = cursor_info.ptScreenPos.y - request.top;
|
||||
if (cursor_x >= -64 && cursor_y >= -64 &&
|
||||
cursor_x < request.width + 64 &&
|
||||
cursor_y < request.height + 64) {
|
||||
DrawIconEx(mem_dc, cursor_x, cursor_y, cursor_info.hCursor, 0, 0,
|
||||
0, nullptr, DI_NORMAL);
|
||||
}
|
||||
report_stats(false);
|
||||
// A lock/credential/UAC transition can change the desktop before the
|
||||
// service's next status poll. Release its DCs before binding the new one.
|
||||
if (capture && !capture->desktop.IsInputDesktop()) {
|
||||
capture.reset();
|
||||
}
|
||||
if (!capture) {
|
||||
capture = std::make_unique<SecureDesktopGdiResources>();
|
||||
if (!capture->Initialize(request)) {
|
||||
const DWORD error = capture->error_code;
|
||||
capture.reset();
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
if (last_error_tick == 0 || now - last_error_tick >= 5000) {
|
||||
LOG_WARN(
|
||||
"Secure shared capture initialization failed; retrying, "
|
||||
"error={}",
|
||||
error);
|
||||
last_error_tick = now;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (capture->Capture(request)) {
|
||||
const auto captured_at = std::chrono::steady_clock::now();
|
||||
const int convert_result = libyuv::ARGBToNV12(
|
||||
static_cast<const uint8_t*>(bits), request.width * 4,
|
||||
static_cast<const uint8_t*>(capture->bits), request.width * 4,
|
||||
nv12_frame.data(), request.width,
|
||||
nv12_frame.data() + request.width * request.height, request.width,
|
||||
request.width, request.height);
|
||||
if (convert_result == 0) {
|
||||
auto* header =
|
||||
reinterpret_cast<crossdesk::CrossDeskSecureDesktopSharedFrameHeader*>(
|
||||
frame_view);
|
||||
const auto converted_at = std::chrono::steady_clock::now();
|
||||
auto* header = reinterpret_cast<
|
||||
crossdesk::CrossDeskSecureDesktopSharedFrameHeader*>(frame_view);
|
||||
uint8_t* payload = frame_view + sizeof(*header);
|
||||
header->writing = 1;
|
||||
MemoryBarrier();
|
||||
@@ -1509,25 +1367,41 @@ void SecureDesktopSharedCaptureThread(
|
||||
MemoryBarrier();
|
||||
header->writing = 0;
|
||||
SetEvent(capture_state->frame_ready_event);
|
||||
++frame_count;
|
||||
capture_ms += std::chrono::duration<double, std::milli>(captured_at -
|
||||
frame_started)
|
||||
.count();
|
||||
convert_ms += std::chrono::duration<double, std::milli>(converted_at -
|
||||
captured_at)
|
||||
.count();
|
||||
publish_ms += std::chrono::duration<double, std::milli>(
|
||||
std::chrono::steady_clock::now() - converted_at)
|
||||
.count();
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("Secure shared capture BitBlt failed, error={}", GetLastError());
|
||||
const DWORD error = capture->error_code;
|
||||
capture.reset();
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
if (last_error_tick == 0 || now - last_error_tick >= 5000) {
|
||||
LOG_WARN(
|
||||
"Secure shared capture BitBlt failed; recreating capture, "
|
||||
"error={}",
|
||||
error);
|
||||
last_error_tick = now;
|
||||
}
|
||||
}
|
||||
|
||||
const auto elapsed_ms =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - frame_started)
|
||||
.count();
|
||||
if (elapsed_ms < interval_ms) {
|
||||
const int retry_interval_ms = capture ? interval_ms : 100;
|
||||
if (elapsed_ms < retry_interval_ms) {
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(interval_ms - elapsed_ms));
|
||||
std::chrono::milliseconds(retry_interval_ms - elapsed_ms));
|
||||
}
|
||||
}
|
||||
|
||||
SelectObject(mem_dc, old_bitmap);
|
||||
DeleteObject(dib);
|
||||
DeleteDC(mem_dc);
|
||||
ReleaseDC(nullptr, screen_dc);
|
||||
report_stats(true);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> StopSecureDesktopSharedCapture(
|
||||
@@ -1703,7 +1577,7 @@ std::vector<uint8_t> HandleSecureInputHelperCommand(
|
||||
json["extended"] = extended;
|
||||
json["stage"] = interactive_stage;
|
||||
json["interactive_desktop"] = interactive_desktop;
|
||||
json["desktop"] = WideToUtf8(GetCurrentThreadDesktopNameW());
|
||||
json["desktop"] = inject_result.desktop.current_desktop;
|
||||
return BuildTextResponseBytes(json.dump());
|
||||
}
|
||||
|
||||
@@ -1731,7 +1605,7 @@ std::vector<uint8_t> HandleSecureInputHelperCommand(
|
||||
json["flag"] = mouse_request.flag;
|
||||
json["stage"] = mouse_request.interactive_stage;
|
||||
json["interactive_desktop"] = mouse_request.interactive_desktop;
|
||||
json["desktop"] = WideToUtf8(GetCurrentThreadDesktopNameW());
|
||||
json["desktop"] = inject_result.desktop.current_desktop;
|
||||
return BuildTextResponseBytes(json.dump());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include "captured_nv12_frame.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <new>
|
||||
#include <utility>
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
bool GetNv12Size(uint32_t width, uint32_t height, size_t* size) {
|
||||
if (!size || width == 0 || height == 0 || (width & 1U) != 0 ||
|
||||
(height & 1U) != 0 ||
|
||||
static_cast<size_t>(width) >
|
||||
std::numeric_limits<size_t>::max() / height) {
|
||||
return false;
|
||||
}
|
||||
const size_t pixels = static_cast<size_t>(width) * height;
|
||||
if (pixels > std::numeric_limits<size_t>::max() / 3U * 2U) {
|
||||
return false;
|
||||
}
|
||||
*size = pixels * 3U / 2U;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::shared_ptr<CapturedNv12FramePool> CapturedNv12FramePool::Create(
|
||||
size_t max_cached_buffers) {
|
||||
return std::shared_ptr<CapturedNv12FramePool>(
|
||||
new CapturedNv12FramePool(max_cached_buffers));
|
||||
}
|
||||
|
||||
CapturedNv12FramePool::CapturedNv12FramePool(size_t max_cached_buffers)
|
||||
: max_cached_buffers_(max_cached_buffers) {}
|
||||
|
||||
CapturedNv12Frame* CapturedNv12FramePool::CopyFrom(const uint8_t* data,
|
||||
size_t size,
|
||||
uint32_t width,
|
||||
uint32_t height) {
|
||||
size_t required_size = 0;
|
||||
if (!data || !GetNv12Size(width, height, &required_size) ||
|
||||
size < required_size) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> storage;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto best = free_buffers_.end();
|
||||
for (auto it = free_buffers_.begin(); it != free_buffers_.end(); ++it) {
|
||||
if (it->capacity() < required_size) {
|
||||
continue;
|
||||
}
|
||||
if (best == free_buffers_.end() || it->capacity() < best->capacity()) {
|
||||
best = it;
|
||||
}
|
||||
}
|
||||
if (best == free_buffers_.end() && !free_buffers_.empty()) {
|
||||
best = std::max_element(
|
||||
free_buffers_.begin(), free_buffers_.end(),
|
||||
[](const auto& lhs, const auto& rhs) {
|
||||
return lhs.capacity() < rhs.capacity();
|
||||
});
|
||||
}
|
||||
if (best != free_buffers_.end()) {
|
||||
storage = std::move(*best);
|
||||
free_buffers_.erase(best);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
storage.resize(required_size);
|
||||
std::memcpy(storage.data(), data, required_size);
|
||||
return new CapturedNv12Frame(shared_from_this(), std::move(storage), width,
|
||||
height);
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void CapturedNv12FramePool::Recycle(std::vector<uint8_t> storage) {
|
||||
if (max_cached_buffers_ == 0 || storage.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (free_buffers_.size() < max_cached_buffers_) {
|
||||
free_buffers_.push_back(std::move(storage));
|
||||
return;
|
||||
}
|
||||
|
||||
auto smallest = std::min_element(
|
||||
free_buffers_.begin(), free_buffers_.end(),
|
||||
[](const auto& lhs, const auto& rhs) {
|
||||
return lhs.capacity() < rhs.capacity();
|
||||
});
|
||||
if (smallest != free_buffers_.end() &&
|
||||
smallest->capacity() < storage.capacity()) {
|
||||
*smallest = std::move(storage);
|
||||
}
|
||||
}
|
||||
|
||||
CapturedNv12Frame::CapturedNv12Frame(
|
||||
std::shared_ptr<CapturedNv12FramePool> pool,
|
||||
std::vector<uint8_t> storage, uint32_t width, uint32_t height)
|
||||
: pool_(std::move(pool)),
|
||||
storage_(std::move(storage)),
|
||||
width_(width),
|
||||
height_(height) {
|
||||
descriptor_.struct_size = sizeof(descriptor_);
|
||||
descriptor_.type = MiniRtcNativeVideoFrameCpuNv12;
|
||||
descriptor_.width = width_;
|
||||
descriptor_.height = height_;
|
||||
descriptor_.payload.cpu_nv12 = {
|
||||
storage_.data(), storage_.data() + static_cast<size_t>(width_) * height_,
|
||||
width_, width_};
|
||||
descriptor_.owner = this;
|
||||
descriptor_.retain = &RetainOwner;
|
||||
descriptor_.release = &ReleaseOwner;
|
||||
descriptor_.copy_to_nv12 = &CopyToNv12;
|
||||
}
|
||||
|
||||
CapturedNv12Frame::~CapturedNv12Frame() {
|
||||
if (pool_) {
|
||||
pool_->Recycle(std::move(storage_));
|
||||
}
|
||||
}
|
||||
|
||||
void CapturedNv12Frame::AddRef() {
|
||||
references_.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void CapturedNv12Frame::Release() {
|
||||
if (references_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
|
||||
delete this;
|
||||
}
|
||||
}
|
||||
|
||||
void CapturedNv12Frame::RetainOwner(void* owner) {
|
||||
static_cast<CapturedNv12Frame*>(owner)->AddRef();
|
||||
}
|
||||
|
||||
void CapturedNv12Frame::ReleaseOwner(void* owner) {
|
||||
static_cast<CapturedNv12Frame*>(owner)->Release();
|
||||
}
|
||||
|
||||
int CapturedNv12Frame::CopyToNv12(void* owner, uint8_t* destination,
|
||||
size_t destination_size) {
|
||||
auto* frame = static_cast<CapturedNv12Frame*>(owner);
|
||||
if (!frame || !destination || destination_size < frame->Size()) {
|
||||
return -1;
|
||||
}
|
||||
std::memcpy(destination, frame->storage_.data(), frame->Size());
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _CAPTURED_NV12_FRAME_H_
|
||||
#define _CAPTURED_NV12_FRAME_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class CapturedNv12Frame;
|
||||
|
||||
// Owns the pixels behind captured native frames. A frame may outlive the
|
||||
// capture callback while MiniRTC waits for its asynchronous encoder, so capture
|
||||
// buffers cannot be borrowed directly. Released allocations are cached to keep
|
||||
// that ownership transfer inexpensive after the pipeline is warm.
|
||||
class CapturedNv12FramePool final
|
||||
: public std::enable_shared_from_this<CapturedNv12FramePool> {
|
||||
public:
|
||||
static std::shared_ptr<CapturedNv12FramePool> Create(
|
||||
size_t max_cached_buffers = 4);
|
||||
|
||||
CapturedNv12Frame* CopyFrom(const uint8_t* data, size_t size,
|
||||
uint32_t width, uint32_t height);
|
||||
|
||||
private:
|
||||
explicit CapturedNv12FramePool(size_t max_cached_buffers);
|
||||
void Recycle(std::vector<uint8_t> storage);
|
||||
|
||||
friend class CapturedNv12Frame;
|
||||
|
||||
const size_t max_cached_buffers_;
|
||||
std::mutex mutex_;
|
||||
std::vector<std::vector<uint8_t>> free_buffers_;
|
||||
};
|
||||
|
||||
class CapturedNv12Frame final {
|
||||
public:
|
||||
const MiniRtcNativeVideoFrame* Descriptor() const { return &descriptor_; }
|
||||
size_t Size() const { return storage_.size(); }
|
||||
|
||||
void AddRef();
|
||||
void Release();
|
||||
|
||||
private:
|
||||
friend class CapturedNv12FramePool;
|
||||
|
||||
CapturedNv12Frame(std::shared_ptr<CapturedNv12FramePool> pool,
|
||||
std::vector<uint8_t> storage, uint32_t width,
|
||||
uint32_t height);
|
||||
~CapturedNv12Frame();
|
||||
|
||||
static void RetainOwner(void* owner);
|
||||
static void ReleaseOwner(void* owner);
|
||||
static int CopyToNv12(void* owner, uint8_t* destination,
|
||||
size_t destination_size);
|
||||
|
||||
std::atomic<uint32_t> references_{1};
|
||||
std::shared_ptr<CapturedNv12FramePool> pool_;
|
||||
std::vector<uint8_t> storage_;
|
||||
uint32_t width_ = 0;
|
||||
uint32_t height_ = 0;
|
||||
MiniRtcNativeVideoFrame descriptor_{};
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -10,14 +10,18 @@
|
||||
#include <functional>
|
||||
|
||||
#include "display_info.h"
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class ScreenCapturer {
|
||||
public:
|
||||
// The final callback argument is a logical MiniRTC stream ID (DisplayN),
|
||||
// not a platform display name or physical handle.
|
||||
typedef std::function<void(unsigned char*, int, int, int, const char*)>
|
||||
// |stream_id| is a logical MiniRTC stream ID (DisplayN), not a platform
|
||||
// display name or physical handle. |native_frame| is borrowed for the
|
||||
// duration of the callback; retain its owner before using it asynchronously.
|
||||
typedef std::function<void(unsigned char* data, int size, int width,
|
||||
int height, const char* stream_id,
|
||||
const MiniRtcNativeVideoFrame* native_frame)>
|
||||
cb_desktop_data;
|
||||
|
||||
public:
|
||||
|
||||
@@ -34,6 +34,7 @@ function setup_targets()
|
||||
set_kind("object")
|
||||
add_deps("rd_log", "crossdesk_wire")
|
||||
add_packages("libyuv")
|
||||
add_packages("concurrentqueue", {public = true})
|
||||
add_files("apps/desktop/src/common/*.cpp")
|
||||
remove_files("apps/desktop/src/common/rounded_corner_button.cpp")
|
||||
if is_os("windows") then
|
||||
@@ -159,8 +160,10 @@ function setup_targets()
|
||||
set_kind("object")
|
||||
add_deps("rd_log", "common", "crossdesk_wire")
|
||||
add_includedirs("apps/desktop/src/screen_capturer", {public = true})
|
||||
add_includedirs("deps/submodules/minirtc/src/api", {public = true})
|
||||
if is_os("windows") then
|
||||
add_packages("libyuv")
|
||||
add_files("apps/desktop/src/screen_capturer/captured_nv12_frame.cpp")
|
||||
add_files("apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.cpp",
|
||||
"apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.cpp",
|
||||
"apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp",
|
||||
@@ -327,7 +330,11 @@ function setup_targets()
|
||||
add_cxxflags("/bigobj")
|
||||
add_links("opengl32")
|
||||
add_files("apps/desktop/src/platform/windows/gui/tray/win_tray.cpp",
|
||||
"apps/desktop/src/platform/windows/gui/slint_backend.cpp",
|
||||
"apps/desktop/src/platform/windows/gui/slint_renderer_probe.cpp",
|
||||
"apps/desktop/src/platform/common/gui/opengl_video_renderer.cpp",
|
||||
"apps/desktop/src/platform/common/gui/cuda_driver.cpp",
|
||||
"apps/desktop/src/platform/common/gui/cuda_opengl_interop.cpp",
|
||||
"apps/desktop/src/platform/common/gui/video_renderer_factory_opengl.cpp",
|
||||
"apps/desktop/src/platform/windows/gui/runtime/windows_service_runtime.cpp",
|
||||
"apps/desktop/src/platform/windows/gui/application/portable_service_integration.cpp")
|
||||
@@ -345,7 +352,10 @@ function setup_targets()
|
||||
elseif is_os("linux") then
|
||||
add_links("GL")
|
||||
add_files("apps/desktop/src/platform/linux/gui/tray/linux_tray.cpp",
|
||||
"apps/desktop/src/platform/linux/gui/cuda_graphics.cpp",
|
||||
"apps/desktop/src/platform/common/gui/opengl_video_renderer.cpp",
|
||||
"apps/desktop/src/platform/common/gui/cuda_driver.cpp",
|
||||
"apps/desktop/src/platform/common/gui/cuda_opengl_interop.cpp",
|
||||
"apps/desktop/src/platform/common/gui/video_renderer_factory_opengl.cpp")
|
||||
add_includedirs("apps/desktop/src/platform/common/gui",
|
||||
"apps/desktop/src/platform/linux/gui/tray",
|
||||
@@ -367,7 +377,8 @@ function setup_targets()
|
||||
"apps/desktop/src/platform/windows/screen_capturer/wgc_plugin_entry.cpp")
|
||||
add_files("apps/desktop/resources/windows/wgc_plugin.rc")
|
||||
add_includedirs("apps/desktop/src/common", "apps/desktop/src/screen_capturer",
|
||||
"apps/desktop/src/platform/windows/screen_capturer")
|
||||
"apps/desktop/src/platform/windows/screen_capturer",
|
||||
"deps/submodules/minirtc/src/api")
|
||||
|
||||
target("crossdesk_service")
|
||||
set_kind("binary")
|
||||
|
||||
@@ -202,7 +202,7 @@ void DispatchMain(dispatch_block_t block) {
|
||||
generation:(uint64_t)generation;
|
||||
- (BOOL)isControllerGenerationActive:(uint64_t)generation;
|
||||
- (BOOL)isControllerGenerationCurrent:(uint64_t)generation;
|
||||
- (void)handleVideoFrame:(const XVideoFrame *)frame
|
||||
- (void)handleVideoFrame:(const MiniRtcVideoFrame *)frame
|
||||
sourceID:(const char *)sourceID
|
||||
sourceIDSize:(size_t)sourceIDSize;
|
||||
- (void)enqueueVideoPixelBuffer:(CVPixelBufferRef)pixelBuffer
|
||||
@@ -214,7 +214,7 @@ void DispatchMain(dispatch_block_t block) {
|
||||
size:(size_t)size
|
||||
sourceID:(const char *)sourceID
|
||||
sourceIDSize:(size_t)sourceIDSize;
|
||||
- (void)handleStats:(const XNetTrafficStats *)stats mode:(TraversalMode)mode;
|
||||
- (void)handleStats:(const MiniRtcNetTrafficStats *)stats mode:(TraversalMode)mode;
|
||||
- (void)sendMessage:(const std::string &)message
|
||||
reliable:(BOOL)reliable
|
||||
stream:(const char *)stream;
|
||||
@@ -227,7 +227,7 @@ void DispatchMain(dispatch_block_t block) {
|
||||
|
||||
namespace {
|
||||
|
||||
void OnVideoFrame(const XVideoFrame *frame, const char *, size_t,
|
||||
void OnVideoFrame(const MiniRtcVideoFrame *frame, const char *, size_t,
|
||||
const char *source_id, size_t source_id_size,
|
||||
void *user_data) {
|
||||
auto *context = static_cast<CallbackContext *>(user_data);
|
||||
@@ -298,7 +298,7 @@ void OnConnectionState(ConnectionStatus status, const char *remote_id,
|
||||
}
|
||||
|
||||
void OnNetworkStats(const char *peer_id, size_t peer_id_size,
|
||||
TraversalMode mode, const XNetTrafficStats *stats,
|
||||
TraversalMode mode, const MiniRtcNetTrafficStats *stats,
|
||||
const char *, size_t, void *user_data) {
|
||||
auto *context = static_cast<CallbackContext *>(user_data);
|
||||
CrossDeskRTCBridge *owner = context ? context->owner : nil;
|
||||
@@ -1114,7 +1114,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
return active == 0 || active == generation;
|
||||
}
|
||||
|
||||
- (void)handleVideoFrame:(const XVideoFrame *)frame
|
||||
- (void)handleVideoFrame:(const MiniRtcVideoFrame *)frame
|
||||
sourceID:(const char *)sourceID
|
||||
sourceIDSize:(size_t)sourceIDSize {
|
||||
if (!frame || frame->width == 0 || frame->height == 0) return;
|
||||
@@ -1124,12 +1124,17 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
static_cast<size_t>(_selectedDisplay.load()));
|
||||
if (source != expected) return;
|
||||
}
|
||||
const MiniRtcNativeVideoFrame *native_frame = frame->native_frame;
|
||||
const bool has_native_pixel_buffer =
|
||||
frame->native_handle &&
|
||||
frame->native_handle_type == XVideoFrameNativeHandleCVPixelBuffer;
|
||||
native_frame &&
|
||||
native_frame->struct_size >=
|
||||
static_cast<uint32_t>(sizeof(MiniRtcNativeVideoFrame)) &&
|
||||
native_frame->type == MiniRtcNativeVideoFrameCVPixelBuffer &&
|
||||
native_frame->payload.cv_pixel_buffer;
|
||||
if (has_native_pixel_buffer) {
|
||||
CVPixelBufferRef pixel_buffer =
|
||||
static_cast<CVPixelBufferRef>(frame->native_handle);
|
||||
static_cast<CVPixelBufferRef>(
|
||||
native_frame->payload.cv_pixel_buffer);
|
||||
[self enqueueVideoPixelBuffer:pixel_buffer
|
||||
width:frame->width
|
||||
height:frame->height];
|
||||
@@ -1579,7 +1584,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleStats:(const XNetTrafficStats *)stats mode:(TraversalMode)mode {
|
||||
- (void)handleStats:(const MiniRtcNetTrafficStats *)stats mode:(TraversalMode)mode {
|
||||
if (!stats) return;
|
||||
const NSUInteger bitrate = stats->total_inbound_stats.bitrate;
|
||||
const float loss = stats->video_inbound_stats.loss_rate;
|
||||
|
||||
@@ -121,6 +121,7 @@ LINK_DIRS=("${(@f)$(print -r -- "${CLEAN_INFO}" | sed -nE 's|.*-> (/.*)/lib -> p
|
||||
|
||||
REQUIRED_LINKS=(
|
||||
nice glib-2.0 gobject-2.0 gmodule-2.0 gio-2.0 gthread-2.0 intl
|
||||
gupnp-igd-1.6 gupnp-1.6 gssdp-1.6 soup-3.0 nghttp2 sqlite3 psl xml2
|
||||
ffi pcre2-8 pcre2-posix z ssl crypto srtp2 openfec opus yuv kcp
|
||||
datachannel usrsctp openh264 dav1d aom SvtAv1Enc
|
||||
)
|
||||
|
||||
Vendored
+1
-1
Submodule deps/submodules/minirtc updated: a1d221da96...6c39589408
Reference in New Issue
Block a user