mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-23 04:53:06 +08:00
Compare commits
3
Commits
e0cd761bdc
...
d381cbaa9b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d381cbaa9b | ||
|
|
5aedb09c55 | ||
|
|
fe1178e26f |
@@ -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
|
||||
@@ -48,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"
|
||||
@@ -1030,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
|
||||
@@ -1738,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_);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1748,6 +1750,7 @@ void GuiApplication::Tick() {
|
||||
slint::quit_event_loop();
|
||||
return;
|
||||
}
|
||||
HandleSessionCleanup();
|
||||
HandlePasswordChangeResult();
|
||||
HandleCredentialRecovery();
|
||||
if (!peer_) {
|
||||
@@ -2008,8 +2011,7 @@ void GuiApplication::HandlePasswordChangeResult() {
|
||||
}
|
||||
show_offline_warning_window_ = true;
|
||||
if (uncertain && peer_) {
|
||||
LeaveConnection(peer_, client_id_);
|
||||
DestroyPeer(&peer_);
|
||||
CloseConnectionPeer();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -2022,8 +2024,7 @@ void GuiApplication::HandlePasswordChangeResult() {
|
||||
|
||||
LOG_INFO("Password changed successfully for [{}]", client_id_);
|
||||
if (peer_) {
|
||||
LeaveConnection(peer_, client_id_);
|
||||
DestroyPeer(&peer_);
|
||||
CloseConnectionPeer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2047,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;
|
||||
}
|
||||
@@ -3083,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_);
|
||||
@@ -3290,7 +3288,7 @@ void GuiApplication::Cleanup() {
|
||||
devices_.DestroyDevices();
|
||||
devices_.DestroyFactories();
|
||||
CloseAllRemoteSessions();
|
||||
WaitForThumbnailSaveTasks();
|
||||
WaitForSessionCleanup();
|
||||
devices_.DestroyAudioOutput();
|
||||
if (ui_->stream) {
|
||||
#if defined(__APPLE__)
|
||||
|
||||
@@ -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() ==
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,9 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
sizeof(params_.log_path) - 1);
|
||||
params_.log_path[sizeof(params_.log_path) - 1] = '\0';
|
||||
params_.hardware_acceleration = config_center_->IsHardwareVideoCodec();
|
||||
#if defined(_WIN32) || defined(__APPLE__)
|
||||
// Windows renderers retain pooled CPU NV12 frames or CUDA device frames;
|
||||
// macOS retains VideoToolbox CVPixelBuffers for direct Metal sampling. Both
|
||||
#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
|
||||
@@ -189,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_);
|
||||
@@ -240,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_,
|
||||
|
||||
@@ -61,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);
|
||||
|
||||
@@ -89,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_;
|
||||
|
||||
@@ -104,4 +107,4 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#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;
|
||||
@@ -496,6 +500,9 @@ void PeerEventHandler::OnNetStatusReport(
|
||||
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_));
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <string>
|
||||
|
||||
#include "minirtc.h"
|
||||
#include "runtime/session_lifecycle.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -21,6 +22,10 @@ class PeerEventHandler {
|
||||
public:
|
||||
explicit PeerEventHandler(GuiRuntime &owner);
|
||||
|
||||
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,
|
||||
@@ -49,6 +54,7 @@ private:
|
||||
static void SendClientInfo(PeerPtr *peer, const std::string &client_id);
|
||||
|
||||
GuiRuntime &owner_;
|
||||
gui_detail::SessionCallbackGate callbacks_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -32,19 +32,17 @@ void PeerEventHandler::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) {
|
||||
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;
|
||||
@@ -174,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;
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
#include "display_info.h"
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk { class PeerEventHandler; }
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
|
||||
struct FileTransferState {
|
||||
@@ -67,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;
|
||||
|
||||
@@ -22,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 {
|
||||
@@ -184,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_;
|
||||
|
||||
@@ -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
|
||||
@@ -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,98 +27,25 @@
|
||||
#include <utility>
|
||||
|
||||
#include "rd_log.h"
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
#include "nvcodec_api.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 defined(_WIN32) && USE_CUDA
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
constexpr size_t kCudaUploadSlotCount = 3;
|
||||
|
||||
#endif
|
||||
|
||||
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 *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;
|
||||
}
|
||||
|
||||
enum class SlotUse {
|
||||
available,
|
||||
pending,
|
||||
@@ -194,7 +122,7 @@ struct OpenGlFunctions {
|
||||
PFNGLUNIFORM4FPROC uniform_4f = nullptr;
|
||||
PFNGLUSEPROGRAMPROC use_program = nullptr;
|
||||
PFNGLVERTEXATTRIBPOINTERPROC vertex_attrib_pointer = nullptr;
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
PFNGLFENCESYNCPROC fence_sync = nullptr;
|
||||
PFNGLCLIENTWAITSYNCPROC client_wait_sync = nullptr;
|
||||
PFNGLDELETESYNCPROC delete_sync = nullptr;
|
||||
@@ -235,7 +163,7 @@ struct OpenGlFunctions {
|
||||
LoadOpenGlFunction(&vertex_attrib_pointer, "glVertexAttribPointer");
|
||||
}
|
||||
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
bool LoadSyncFunctions() {
|
||||
return LoadOpenGlFunction(&fence_sync, "glFenceSync") &&
|
||||
LoadOpenGlFunction(&client_wait_sync, "glClientWaitSync") &&
|
||||
@@ -276,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);
|
||||
@@ -346,10 +289,14 @@ 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 defined(_WIN32) && USE_CUDA
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
enum class CudaUploadResult {
|
||||
ready,
|
||||
busy,
|
||||
@@ -363,17 +310,14 @@ struct OpenGlVideoRenderer::Impl {
|
||||
size_t capacity = 0;
|
||||
int texture_width = 0;
|
||||
int texture_height = 0;
|
||||
CUgraphicsResource resource = nullptr;
|
||||
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;
|
||||
CUcontext cuda_context = nullptr;
|
||||
CUstream cuda_upload_stream = nullptr;
|
||||
NativeVideoFrameRef cuda_context_owner;
|
||||
bool cuda_sync_functions_available = false;
|
||||
void *cuda_context = nullptr;
|
||||
bool cuda_interop_disabled = false;
|
||||
std::atomic<bool> cuda_cpu_fallback{false};
|
||||
bool cuda_native_logged = false;
|
||||
@@ -392,6 +336,7 @@ struct OpenGlVideoRenderer::Impl {
|
||||
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;
|
||||
}
|
||||
@@ -419,28 +364,10 @@ struct OpenGlVideoRenderer::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
if (cuda_context && minirtc::cuCtxPushCurrent_ld &&
|
||||
minirtc::cuCtxPushCurrent_ld(cuda_context) == CUDA_SUCCESS) {
|
||||
if (cuda_upload_stream && minirtc::cuStreamSynchronize_ld) {
|
||||
minirtc::cuStreamSynchronize_ld(cuda_upload_stream);
|
||||
}
|
||||
for (auto &slot : cuda_upload_slots) {
|
||||
if (slot.resource && minirtc::cuGraphicsUnregisterResource_ld) {
|
||||
minirtc::cuGraphicsUnregisterResource_ld(slot.resource);
|
||||
slot.resource = nullptr;
|
||||
}
|
||||
}
|
||||
if (cuda_upload_stream && minirtc::cuStreamDestroy_ld) {
|
||||
minirtc::cuStreamDestroy_ld(cuda_upload_stream);
|
||||
}
|
||||
CUcontext popped_context = nullptr;
|
||||
minirtc::cuCtxPopCurrent_ld(&popped_context);
|
||||
}
|
||||
cuda_upload_stream = nullptr;
|
||||
cuda_context = nullptr;
|
||||
cuda_context_owner.Reset();
|
||||
|
||||
for (auto &slot : cuda_upload_slots) {
|
||||
DestroyCudaOpenGlVideoBuffer(slot.resource);
|
||||
if (slot.buffer != 0 && gl.delete_buffers) {
|
||||
gl.delete_buffers(1, &slot.buffer);
|
||||
}
|
||||
@@ -458,57 +385,32 @@ struct OpenGlVideoRenderer::Impl {
|
||||
|
||||
bool PrepareCudaContext(const NativeVideoFrameRef &native_frame) {
|
||||
const MiniRtcNativeVideoFrame *frame = native_frame.Get();
|
||||
if (!frame || !frame->payload.cuda_nv12.context ||
|
||||
cuda_interop_disabled ||
|
||||
!cuda_sync_functions_available ||
|
||||
minirtc::LoadCudaGraphicsInterop() != 0) {
|
||||
if (!frame || !frame->payload.cuda_nv12.context || cuda_interop_disabled) {
|
||||
DisableCudaInterop();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto frame_context =
|
||||
static_cast<CUcontext>(frame->payload.cuda_nv12.context);
|
||||
void *frame_context = frame->payload.cuda_nv12.context;
|
||||
if (cuda_context && cuda_context != frame_context) {
|
||||
ReleaseCudaInterop();
|
||||
}
|
||||
if (!cuda_context) {
|
||||
cuda_context_owner = native_frame;
|
||||
cuda_context = frame_context;
|
||||
if (minirtc::cuCtxPushCurrent_ld(cuda_context) != CUDA_SUCCESS) {
|
||||
ReleaseCudaInterop();
|
||||
DisableCudaInterop();
|
||||
return false;
|
||||
}
|
||||
const CUresult create_result = minirtc::cuStreamCreate_ld(
|
||||
&cuda_upload_stream, CU_STREAM_NON_BLOCKING);
|
||||
CUcontext popped_context = nullptr;
|
||||
const CUresult pop_result =
|
||||
minirtc::cuCtxPopCurrent_ld(&popped_context);
|
||||
if (create_result != CUDA_SUCCESS || pop_result != CUDA_SUCCESS) {
|
||||
ReleaseCudaInterop();
|
||||
DisableCudaInterop();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
cuda_context = frame_context;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PrepareCudaSlot(CudaUploadSlot &slot, size_t required_size) {
|
||||
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) {
|
||||
if (slot.resource) {
|
||||
if (minirtc::cuGraphicsUnregisterResource_ld(slot.resource) !=
|
||||
CUDA_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
slot.resource = nullptr;
|
||||
}
|
||||
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);
|
||||
@@ -517,21 +419,18 @@ struct OpenGlVideoRenderer::Impl {
|
||||
GL_STREAM_DRAW);
|
||||
gl.bind_buffer(GL_PIXEL_UNPACK_BUFFER,
|
||||
static_cast<GLuint>(previous_unpack_buffer));
|
||||
if (glGetError() != GL_NO_ERROR) {
|
||||
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) {
|
||||
if (minirtc::cuGraphicsGLRegisterBuffer_ld(
|
||||
&slot.resource, slot.buffer,
|
||||
CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD) != CUDA_SUCCESS) {
|
||||
slot.resource = nullptr;
|
||||
return false;
|
||||
}
|
||||
slot.resource = CreateCudaOpenGlVideoBuffer(frame, slot.buffer);
|
||||
}
|
||||
return true;
|
||||
return slot.resource != nullptr;
|
||||
}
|
||||
|
||||
bool PrepareCudaTextures(CudaUploadSlot &slot) {
|
||||
@@ -577,6 +476,9 @@ struct OpenGlVideoRenderer::Impl {
|
||||
selected_index = index;
|
||||
break;
|
||||
}
|
||||
if (cuda_interop_disabled) {
|
||||
return CudaUploadResult::unavailable;
|
||||
}
|
||||
}
|
||||
if (selected_index == kCudaUploadSlotCount) {
|
||||
++cuda_busy_drop_count;
|
||||
@@ -587,68 +489,11 @@ struct OpenGlVideoRenderer::Impl {
|
||||
return CudaUploadResult::busy;
|
||||
}
|
||||
|
||||
if (minirtc::cuCtxPushCurrent_ld(cuda_context) != CUDA_SUCCESS) {
|
||||
DisableCudaInterop();
|
||||
return CudaUploadResult::unavailable;
|
||||
}
|
||||
|
||||
CudaUploadSlot &slot = cuda_upload_slots[selected_index];
|
||||
const size_t packed_size =
|
||||
static_cast<size_t>(frame->width) * frame->height * 3U / 2U;
|
||||
CUresult result = PrepareCudaSlot(slot, packed_size)
|
||||
? CUDA_SUCCESS
|
||||
: CUDA_ERROR_INVALID_VALUE;
|
||||
CUgraphicsResource resource = slot.resource;
|
||||
bool mapped = false;
|
||||
CUdeviceptr destination = 0;
|
||||
size_t mapped_size = 0;
|
||||
if (result == CUDA_SUCCESS) {
|
||||
result = minirtc::cuGraphicsMapResources_ld(
|
||||
1, &resource, cuda_upload_stream);
|
||||
mapped = result == CUDA_SUCCESS;
|
||||
}
|
||||
if (result == CUDA_SUCCESS) {
|
||||
result = minirtc::cuGraphicsResourceGetMappedPointer_ld(
|
||||
&destination, &mapped_size, slot.resource);
|
||||
}
|
||||
if (result == CUDA_SUCCESS && mapped_size < packed_size) {
|
||||
result = CUDA_ERROR_INVALID_VALUE;
|
||||
}
|
||||
if (result == CUDA_SUCCESS) {
|
||||
CUDA_MEMCPY2D copy{};
|
||||
copy.srcMemoryType = CU_MEMORYTYPE_DEVICE;
|
||||
copy.srcDevice =
|
||||
static_cast<CUdeviceptr>(
|
||||
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;
|
||||
result = minirtc::cuMemcpy2DAsync_ld(©, cuda_upload_stream);
|
||||
if (result == CUDA_SUCCESS) {
|
||||
copy.srcDevice =
|
||||
static_cast<CUdeviceptr>(
|
||||
frame->payload.cuda_nv12.uv_device_pointer);
|
||||
copy.srcPitch = frame->payload.cuda_nv12.uv_stride;
|
||||
copy.dstDevice = destination +
|
||||
static_cast<CUdeviceptr>(frame->width) * frame->height;
|
||||
copy.Height = frame->height / 2U;
|
||||
result = minirtc::cuMemcpy2DAsync_ld(©, cuda_upload_stream);
|
||||
}
|
||||
}
|
||||
if (mapped) {
|
||||
const CUresult unmap_result = minirtc::cuGraphicsUnmapResources_ld(
|
||||
1, &resource, cuda_upload_stream);
|
||||
if (result == CUDA_SUCCESS) {
|
||||
result = unmap_result;
|
||||
}
|
||||
}
|
||||
CUcontext popped_context = nullptr;
|
||||
const CUresult pop_result =
|
||||
minirtc::cuCtxPopCurrent_ld(&popped_context);
|
||||
if (result != CUDA_SUCCESS || pop_result != CUDA_SUCCESS) {
|
||||
if (!PrepareCudaSlot(slot, packed_size, frame) ||
|
||||
UploadCudaFrameToOpenGlBuffer(slot.resource, frame) != 0) {
|
||||
DisableCudaInterop();
|
||||
return CudaUploadResult::unavailable;
|
||||
}
|
||||
@@ -674,9 +519,11 @@ 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 defined(_WIN32) && USE_CUDA
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
displayed_cuda_upload_slot = kCudaUploadSlotCount;
|
||||
#endif
|
||||
}
|
||||
@@ -697,14 +544,37 @@ bool OpenGlVideoRenderer::Setup() {
|
||||
LOG_WARN("OpenGL NV12 underlay unavailable: required functions missing");
|
||||
return false;
|
||||
}
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
impl_->cuda_sync_functions_available = impl_->gl.LoadSyncFunctions();
|
||||
impl_->cuda_interop_disabled = !impl_->cuda_sync_functions_available;
|
||||
impl_->cuda_cpu_fallback.store(!impl_->cuda_sync_functions_available,
|
||||
std::memory_order_release);
|
||||
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(
|
||||
@@ -815,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 =
|
||||
@@ -939,14 +807,17 @@ 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 defined(_WIN32) && USE_CUDA
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
impl_->ReleaseCudaInterop();
|
||||
#endif
|
||||
if (impl_->y_texture != 0)
|
||||
@@ -1143,7 +1014,7 @@ OpenGlVideoRenderer::SubmitNativeFrame(std::string_view remote_id,
|
||||
return SubmitResult::dropped;
|
||||
}
|
||||
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
if (native->type == MiniRtcNativeVideoFrameCudaNv12 &&
|
||||
impl_->cuda_cpu_fallback.load(std::memory_order_acquire)) {
|
||||
const size_t required_size =
|
||||
@@ -1152,27 +1023,18 @@ OpenGlVideoRenderer::SubmitNativeFrame(std::string_view remote_id,
|
||||
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();
|
||||
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;
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
|
||||
NativeVideoFrameRef retained(native);
|
||||
if (!retained) {
|
||||
return SubmitResult::failed;
|
||||
{
|
||||
target->bytes.clear();
|
||||
target->native_frame = NativeVideoFrameRef(native);
|
||||
}
|
||||
target->bytes.clear();
|
||||
target->native_frame = std::move(retained);
|
||||
target->width = static_cast<int>(native->width);
|
||||
target->height = static_cast<int>(native->height);
|
||||
target->remote_id.assign(remote_id);
|
||||
@@ -1235,10 +1097,11 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
GLint previous_program = 0;
|
||||
GLint previous_active_texture = GL_TEXTURE0;
|
||||
GLint previous_array_buffer = 0;
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
GLint previous_unpack_buffer = 0;
|
||||
#endif
|
||||
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] = {};
|
||||
@@ -1246,9 +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 defined(_WIN32) && USE_CUDA
|
||||
glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &previous_unpack_buffer);
|
||||
#endif
|
||||
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);
|
||||
@@ -1302,7 +1174,7 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
y_data = native->payload.cpu_nv12.y_plane;
|
||||
uv_data = native->payload.cpu_nv12.uv_plane;
|
||||
} else if (native->type == MiniRtcNativeVideoFrameCudaNv12) {
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
const auto cuda_result = impl_->CopyCudaFrameToUploadBuffer(
|
||||
active_native_frame, &cuda_upload_slot);
|
||||
use_pixel_unpack_buffer =
|
||||
@@ -1310,7 +1182,7 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
should_upload =
|
||||
cuda_result != OpenGlVideoRenderer::Impl::CudaUploadResult::busy;
|
||||
if (use_pixel_unpack_buffer && !impl_->cuda_native_logged) {
|
||||
LOG_INFO("Windows native NV12 using asynchronous CUDA/OpenGL ring");
|
||||
LOG_INFO("Native NV12 using asynchronous CUDA/OpenGL ring");
|
||||
impl_->cuda_native_logged = true;
|
||||
}
|
||||
if (should_upload && !use_pixel_unpack_buffer &&
|
||||
@@ -1338,7 +1210,7 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
|
||||
GLuint upload_y_texture = impl_->y_texture;
|
||||
GLuint upload_uv_texture = impl_->uv_texture;
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
#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);
|
||||
@@ -1356,7 +1228,7 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, upload_uv_texture);
|
||||
if (use_pixel_unpack_buffer) {
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
#if CROSSDESK_OPENGL_CUDA_INTEROP
|
||||
impl_->gl.bind_buffer(
|
||||
GL_PIXEL_UNPACK_BUFFER,
|
||||
impl_->cuda_upload_slots[cuda_upload_slot].buffer);
|
||||
@@ -1369,9 +1241,9 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
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_->texture_width;
|
||||
int uploaded_texture_height = impl_->texture_height;
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
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;
|
||||
@@ -1398,12 +1270,6 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
source_height / 2, GL_LUMINANCE_ALPHA,
|
||||
GL_UNSIGNED_BYTE, uv_pixels);
|
||||
}
|
||||
if (use_pixel_unpack_buffer) {
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
impl_->gl.bind_buffer(GL_PIXEL_UNPACK_BUFFER,
|
||||
static_cast<GLuint>(previous_unpack_buffer));
|
||||
#endif
|
||||
}
|
||||
const GLenum upload_error = glGetError();
|
||||
upload_succeeded = upload_error == GL_NO_ERROR;
|
||||
if (upload_succeeded) {
|
||||
@@ -1411,7 +1277,11 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
impl_->texture_height = source_height;
|
||||
impl_->uploaded_stream.assign(remote_id);
|
||||
impl_->uploaded_sequence = sequence;
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
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;
|
||||
@@ -1423,6 +1293,13 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
#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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1470,7 +1347,7 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
}
|
||||
GLuint displayed_y_texture = impl_->y_texture;
|
||||
GLuint displayed_uv_texture = impl_->uv_texture;
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
#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];
|
||||
@@ -1510,7 +1387,7 @@ 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 defined(_WIN32) && USE_CUDA
|
||||
#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];
|
||||
@@ -1539,10 +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 defined(_WIN32) && USE_CUDA
|
||||
impl_->gl.bind_buffer(GL_PIXEL_UNPACK_BUFFER,
|
||||
static_cast<GLuint>(previous_unpack_buffer));
|
||||
#endif
|
||||
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);
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
@@ -332,6 +333,8 @@ function setup_targets()
|
||||
"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")
|
||||
@@ -349,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",
|
||||
|
||||
Vendored
+1
-1
Submodule deps/submodules/minirtc updated: 5d8657e63e...6c39589408
Reference in New Issue
Block a user