Compare commits

...
5 Commits
34 changed files with 788 additions and 134 deletions
+76 -32
View File
@@ -23,6 +23,7 @@
#include "config_center.h"
#include "daemon.h"
#include "path_manager.h"
#include "platform/single_instance.h"
#include "rd_log.h"
#include "render.h"
@@ -183,28 +184,34 @@ int HandleServiceCliCommand(const std::string& command) {
} // namespace
#endif
static int RunApplication(int argc, char* argv[]) {
// Service operations and config migration can log before the GUI starts.
// Configure the directory first, including CLI and daemon child paths.
static bool IsDaemonChild(int argc, char* argv[]) {
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--child") == 0) return true;
}
return false;
}
static crossdesk::platform::InstanceResult AcquireInstance(
crossdesk::platform::SingleInstanceGuard& guard,
crossdesk::platform::InstanceRole role) {
std::string error;
const auto result = guard.TryAcquire(role, error);
if (result == crossdesk::platform::InstanceResult::kAlreadyRunning) {
std::cerr << "CrossDesk is already running for this user.\n";
} else if (result == crossdesk::platform::InstanceResult::kError) {
std::cerr << "CrossDesk cannot start: " << error << '\n';
}
return result;
}
static int RunApplication(
int argc, char* argv[],
crossdesk::platform::SingleInstanceGuard& client_instance) {
// The instance guards are held before any configuration or logger is opened.
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
crossdesk::InitLogger(path_manager->GetLogPath().string());
#ifdef _WIN32
if (argc > 1 && IsServiceCliCommand(argv[1])) {
return HandleServiceCliCommand(argv[1]);
}
#endif
// check if running as child process
bool is_child = false;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--child") == 0) {
is_child = true;
break;
}
}
if (is_child) {
if (IsDaemonChild(argc, argv)) {
// child process: run render directly
crossdesk::Render render;
return render.Run();
@@ -230,11 +237,23 @@ static int RunApplication(int argc, char* argv[]) {
#endif
if (enable_daemon) {
// The launcher lease stays in main for the supervisor's entire lifetime.
// Each --child must acquire its own client lease, so it is neither blocked
// by its supervisor nor able to bypass the single-client restriction.
client_instance.Release();
// start daemon with restart monitoring
Daemon daemon("CrossDesk");
// define main loop function: run render and stop daemon on normal exit
Daemon::MainLoopFunc main_loop = [&daemon]() {
Daemon::MainLoopFunc main_loop = [&daemon, &client_instance]() {
// Executable discovery can fail and make the supervisor run the GUI in
// process. This fallback must retain the same client ownership rule.
if (AcquireInstance(client_instance,
crossdesk::platform::InstanceRole::kClient) !=
crossdesk::platform::InstanceResult::kAcquired) {
daemon.stop();
return;
}
crossdesk::Render render;
render.Run();
daemon.stop();
@@ -267,26 +286,51 @@ int main(int argc, char* argv[]) {
std::cout << console.Text("console_cli_help");
return 0;
}
const int result = crossdesk::RunWithLinuxDisplay(
argc, argv, [&] { return RunApplication(argc, argv); }, [&] {
#endif
#ifdef _WIN32
if (argc == 2 &&
std::strcmp(argv[1], crossdesk::kSlintRendererProbeArgument) == 0) {
return crossdesk::RunSlintRendererProbe();
}
if (argc > 1 && IsServiceCliCommand(argv[1])) {
EnsureConsoleForCli();
crossdesk::platform::SingleInstanceGuard cli_instance;
if (AcquireInstance(cli_instance, crossdesk::platform::InstanceRole::kServiceCli) !=
crossdesk::platform::InstanceResult::kAcquired) return 1;
crossdesk::PathManager paths("CrossDesk");
crossdesk::InitLogger(paths.GetLogPath().string(), "crossdesk-service-cli");
return HandleServiceCliCommand(argv[1]);
}
#endif
using crossdesk::platform::InstanceResult;
using crossdesk::platform::InstanceRole;
crossdesk::platform::SingleInstanceGuard launcher_instance;
crossdesk::platform::SingleInstanceGuard client_instance;
if (!IsDaemonChild(argc, argv)) {
const auto result = AcquireInstance(launcher_instance, InstanceRole::kLauncher);
if (result != InstanceResult::kAcquired)
return result == InstanceResult::kAlreadyRunning ? 0 : 1;
}
const auto result = AcquireInstance(client_instance, InstanceRole::kClient);
if (result != InstanceResult::kAcquired)
return result == InstanceResult::kAlreadyRunning ? 0 : 1;
#ifdef __linux__
const int exit_code = crossdesk::RunWithLinuxDisplay(
argc, argv, [&] { return RunApplication(argc, argv, client_instance); }, [&] {
crossdesk::PathManager paths("CrossDesk");
if (console.Enable(paths.GetLogPath(), paths.GetCachePath() / "config.ini")) return true;
std::cerr << "无法初始化日志或控制台,启动失败。\n";
return false;
});
if (console.active()) {
console.Notify(result == 0 ? console.Text("console_exited")
: console.Text("console_stopped") + " " + std::to_string(result) +
console.Notify(exit_code == 0 ? console.Text("console_exited")
: console.Text("console_stopped") + " " + std::to_string(exit_code) +
"; " + console.Text("console_log_file") + ": " + console.diagnostic_path());
}
return result;
return exit_code;
#else
#ifdef _WIN32
if (argc == 2 &&
std::strcmp(argv[1], crossdesk::kSlintRendererProbeArgument) == 0) {
return crossdesk::RunSlintRendererProbe();
}
#endif
return RunApplication(argc, argv);
return RunApplication(argc, argv, client_instance);
#endif
}
@@ -2029,7 +2029,7 @@ void GuiApplication::ShareLocalCursorState() {
if (!delivery.has_sent ||
delivery.last_sent.render_mode != outgoing.render_mode ||
delivery.last_sent.hidden_reason != outgoing.hidden_reason) {
LOG_INFO("Send cursor presentation to [{}]: mode={}, reason={}",
LOG_DEBUG("Send cursor presentation to [{}]: mode={}, reason={}",
recipient.remote_id, static_cast<int>(outgoing.render_mode),
static_cast<int>(outgoing.hidden_reason));
}
@@ -2803,7 +2803,7 @@ void GuiApplication::SyncStreamWindow() {
ui_->display_model->set_vector(std::move(displays));
(*ui_->stream)->set_selected_display(props->selected_display_);
const auto& net = props->net_traffic_stats_;
const auto net = props->net_traffic_stats_.Load();
(*ui_->stream)->set_stats_encryption_enabled(
status == ConnectionStatus::Connected && net.srtp_active);
std::vector<ui::NetworkStatsRow> stats_rows;
@@ -2892,12 +2892,14 @@ void GuiApplication::SyncStreamWindow() {
props->file_transfer_.file_transfer_window_visible_);
const auto fps_now = std::chrono::steady_clock::now();
bool refresh_stats = false;
if (!props->net_traffic_stats_button_pressed_) {
props->fps_ = 0;
props->frame_count_ = 0;
props->last_time_ = {};
} else if (props->last_time_.time_since_epoch().count() == 0) {
props->last_time_ = fps_now;
refresh_stats = true;
} else {
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
fps_now - props->last_time_)
@@ -2906,9 +2908,42 @@ void GuiApplication::SyncStreamWindow() {
props->fps_ = static_cast<int>(props->frame_count_ * 1000 / elapsed);
props->frame_count_ = 0;
props->last_time_ = fps_now;
refresh_stats = true;
}
}
(*ui_->stream)->set_stats_fps(UiText(std::to_string(props->fps_)));
if (!props->net_traffic_stats_button_pressed_ ||
status != ConnectionStatus::Connected) {
props->video_latency_snapshot_.reset();
} else if (refresh_stats) {
props->video_latency_snapshot_ = props->video_latency_->Get(fps_now);
}
const auto& latency = props->video_latency_snapshot_;
(*ui_->stream)
->set_stats_video_latency(UiText(
latency ? std::to_string(
static_cast<int>(std::lround(latency->average_ms))) +
" ms"
: "—"));
const auto display_rtt = props->net_traffic_stats_.SmoothedRttMs(fps_now);
const bool rtt_available = props->net_traffic_stats_button_pressed_ &&
status == ConnectionStatus::Connected &&
display_rtt.has_value();
std::string connection_latency = "—";
if (rtt_available) {
connection_latency =
*display_rtt < 1
? "<1 ms"
: std::to_string(static_cast<int>(std::lround(*display_rtt))) + " ms";
}
(*ui_->stream)->set_stats_connection_latency(UiText(connection_latency));
if (refresh_stats && status == ConnectionStatus::Connected) {
LOG_DEBUG("Latency stats: remote={} video_ms={} video_samples={} "
"rtt_raw_ms={} rtt_display_ms={}",
props->remote_id_, latency ? latency->average_ms : -1,
latency ? latency->samples : 0, net.rtt_ms,
display_rtt.value_or(-1));
}
}
void GuiApplication::SyncStreamVideoFrame() {
@@ -2928,6 +2963,7 @@ void GuiApplication::SyncStreamVideoFrame() {
frame.width = props->video_width_;
frame.height = props->video_height_;
frame.sequence = props->video_frame_sequence_;
frame.timing = props->video_frame_timing_;
}
const auto result = ui_->video_presenter->Present(frame);
@@ -305,6 +305,8 @@ struct TranslationRow {
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
X(video_latency, u8"视频延时", "Video delay", u8"Задержка") \
X(connection_latency, u8"连接延时", "Connection RTT", u8"Задержка соединения") \
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \
@@ -82,7 +82,7 @@ void SessionDeviceManager::RecordCaptureCadence(
: capture_secure_callbacks_ == capture_callbacks_
? "secure_desktop"
: "mixed";
LOG_INFO(
LOG_DEBUG(
"Capture delivery: source={} normal_callbacks={} secure_callbacks={} "
"target_fps={} input_fps={:.2f} "
"forwarded_fps={:.2f} callbacks={} rate_dropped={} "
@@ -225,7 +225,7 @@ int SessionDeviceManager::InitializeScreenCapturer() {
last_video_frame_stream_id_ != stream_id;
if (resumed_after_gap || stream_changed) {
if (RequestVideoKeyFrame(owner_.peer_, stream_id.c_str()) == 0) {
LOG_INFO("Request video key frame before sending captured frame, "
LOG_DEBUG("Request video key frame before sending captured frame, "
"stream='{}', gap_ms={}, stream_changed={}",
stream_id, duration_ms, stream_changed);
}
@@ -298,7 +298,7 @@ void FileTransferManager::HandleAck(const char *data, size_t size) {
uint32_t rate_bps = 0;
if (props) {
const uint32_t bitrate =
props->net_traffic_stats_.data_outbound_stats.bitrate;
props->net_traffic_stats_.DataOutboundBitrate();
if (bitrate > 0 && state->file_sending_.load()) {
rate_bps = static_cast<uint32_t>(bitrate * 0.99f);
const uint32_t current_rate = state->file_send_rate_bps_.load();
@@ -87,6 +87,7 @@ struct SlintVideoPresenter::Impl {
VideoRenderSize gl_image_size;
VideoRenderSize gl_pending_size;
bool gl_pending_dirty = false;
VideoLatencyFrame gl_pending_timing;
#endif
SurfaceState CurrentState() const {
@@ -192,6 +193,7 @@ struct SlintVideoPresenter::Impl {
glPixelStorei(GL_UNPACK_ALIGNMENT, previous_unpack_alignment);
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture));
gl_pending_dirty = false;
gl_pending_timing.MarkSubmitted();
return;
}
@@ -322,7 +324,7 @@ SlintVideoPresenter::PresentResult SlintVideoPresenter::Present(
impl_->seeded_frame_sequence[frame.remote_id] != frame.sequence) {
const auto submit_result = impl_->renderer.SubmitCachedNv12(
frame.remote_id, frame.nv12->data(), frame.nv12->size(), frame.width,
frame.height);
frame.height, frame.timing);
if (submit_result == VideoRenderer::SubmitResult::submitted ||
submit_result == VideoRenderer::SubmitResult::dropped) {
impl_->seeded_frame_sequence[frame.remote_id] = frame.sequence;
@@ -440,6 +442,7 @@ SlintVideoPresenter::PresentResult SlintVideoPresenter::Present(
impl_->gl_pending_frame.swap(impl_->gl_conversion_frame);
impl_->gl_pending_size = {output_width, output_height};
impl_->gl_pending_dirty = true;
impl_->gl_pending_timing = frame.timing;
}
const VideoRenderSize output_size{output_width, output_height};
if (impl_->gl_image_size != output_size) {
@@ -469,6 +472,7 @@ SlintVideoPresenter::PresentResult SlintVideoPresenter::Present(
}
(*impl_->stream)->set_frame(slint::Image(std::move(pixels)));
frame.timing.MarkSubmitted();
(*impl_->stream)->set_has_frame(true);
(*impl_->stream)->set_receiving_text("");
result.width = frame.width;
@@ -14,6 +14,8 @@
#include <string>
#include <vector>
#include "rendering/video_latency.h"
namespace slint {
template <typename T> class ComponentHandle;
}
@@ -42,6 +44,7 @@ public:
int width = 0;
int height = 0;
uint64_t sequence = 0;
VideoLatencyFrame timing;
};
struct PresentResult {
@@ -0,0 +1,110 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-26
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _VIDEO_LATENCY_H_
#define _VIDEO_LATENCY_H_
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <memory>
#include <mutex>
#include <optional>
namespace crossdesk {
class VideoLatencyStats;
// Travels with the exact queued pixels, including native frames. Its clock is
// translated once at the receive callback; rendering needs no live PeerPtr.
struct VideoLatencyFrame {
using Clock = std::chrono::steady_clock;
std::shared_ptr<VideoLatencyStats> stats;
uint64_t id = 0;
Clock::time_point capture_time{};
void MarkPresented(Clock::time_point now = Clock::now()) const;
// Fallback for renderers which expose submission but no presentation time.
void MarkSubmitted(Clock::time_point now = Clock::now()) const;
};
class VideoLatencyStats
: public std::enable_shared_from_this<VideoLatencyStats> {
public:
using Clock = VideoLatencyFrame::Clock;
struct Snapshot {
double average_ms;
size_t samples;
};
VideoLatencyFrame Frame(uint64_t captured_us, int64_t local_now_us,
Clock::time_point now = Clock::now()) {
if (captured_us == 0 || local_now_us <= 0 ||
captured_us > uint64_t(local_now_us) ||
uint64_t(local_now_us) - captured_us > 5'000'000)
return {};
std::lock_guard lock(mutex_);
return {shared_from_this(), ++issued_id_,
now - std::chrono::microseconds(local_now_us - captured_us)};
}
void Record(const VideoLatencyFrame& frame, Clock::time_point now) {
std::lock_guard lock(mutex_);
// Redraws, cached snapshots and late completions must not count twice.
if (frame.id <= last_id_) return;
last_id_ = frame.id;
const double ms =
std::chrono::duration<double, std::milli>(now - frame.capture_time)
.count();
if (ms < 0 || ms > 5000) return;
samples_.push_back({now, ms});
Prune(now);
}
std::optional<Snapshot> Get(Clock::time_point now = Clock::now()) {
std::lock_guard lock(mutex_);
Prune(now);
if (samples_.empty()) return std::nullopt;
double sum = 0;
for (const auto& sample : samples_) {
sum += sample.ms;
}
return Snapshot{sum / samples_.size(), samples_.size()};
}
void Reset() {
std::lock_guard lock(mutex_);
samples_.clear();
last_id_ = issued_id_; // Invalidate frames still held by a renderer.
}
private:
void Prune(Clock::time_point now) {
while (!samples_.empty() &&
(now - samples_.front().time > std::chrono::seconds(1) ||
samples_.size() > 240))
samples_.pop_front();
}
struct Sample {
Clock::time_point time;
double ms;
};
std::mutex mutex_;
uint64_t issued_id_ = 0;
uint64_t last_id_ = 0;
std::deque<Sample> samples_;
};
inline void VideoLatencyFrame::MarkSubmitted(Clock::time_point now) const {
MarkPresented(now);
}
inline void VideoLatencyFrame::MarkPresented(Clock::time_point now) const {
if (stats) stats->Record(*this, now);
}
} // namespace crossdesk
#endif
@@ -0,0 +1,59 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-26
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CONNECTION_LATENCY_H_
#define _CONNECTION_LATENCY_H_
#include <chrono>
#include <cmath>
#include <mutex>
#include <optional>
namespace crossdesk::gui_detail {
// Presentation-only smoothing. The transport's raw RTT remains available for
// recovery, congestion control and diagnostics. A missing one-second report
// does not flicker, but a disconnected/stalled peer cannot leave a stale value.
class ConnectionLatencyStats {
public:
using Clock = std::chrono::steady_clock;
void Update(double rtt_ms, Clock::time_point now = Clock::now()) {
if (!std::isfinite(rtt_ms) || rtt_ms < 0 || rtt_ms > 2000) return;
std::lock_guard lock(mutex_);
if (average_ms_ && now < updated_) return;
if (!Fresh(now)) {
average_ms_ = rtt_ms;
} else {
*average_ms_ += 0.25 * (rtt_ms - *average_ms_);
}
updated_ = now;
}
std::optional<double> Get(Clock::time_point now = Clock::now()) const {
std::lock_guard lock(mutex_);
return Fresh(now) ? average_ms_ : std::nullopt;
}
void Reset() {
std::lock_guard lock(mutex_);
average_ms_.reset();
}
private:
bool Fresh(Clock::time_point now) const {
// The UI may snapshot `now` just before the report thread updates us.
// A newer valid report must not cause a one-frame unavailable flicker.
return average_ms_ && now - updated_ < std::chrono::seconds(3);
}
mutable std::mutex mutex_;
std::optional<double> average_ms_;
Clock::time_point updated_{};
};
} // namespace crossdesk::gui_detail
#endif
@@ -417,8 +417,11 @@ void GuiRuntime::WaitForSessionCleanup() {
void GuiRuntime::ResetRemoteSessionResources(
std::shared_ptr<RemoteSession> props) {
props->net_traffic_stats_.Reset();
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->video_latency_->Reset();
props->video_frame_timing_ = {};
props->front_frame_.reset();
props->back_frame_.reset();
props->thumbnail_frame_.reset();
@@ -0,0 +1,122 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-26
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _NET_TRAFFIC_STATS_CACHE_H_
#define _NET_TRAFFIC_STATS_CACHE_H_
#include <atomic>
#include "minirtc.h"
#include "runtime/connection_latency.h"
namespace crossdesk::gui_detail {
// Fields are independent observations and may come from different reports.
// Relaxed accesses keep each field safe without publishing any other state.
class NetTrafficStatsCache {
static_assert(std::atomic<uint32_t>::is_always_lock_free &&
std::atomic<float>::is_always_lock_free &&
std::atomic<double>::is_always_lock_free &&
std::atomic<bool>::is_always_lock_free,
"Network statistics require lock-free scalar atomics");
public:
void Store(const MiniRtcNetTrafficStats& stats) {
video_inbound_stats_.Store(stats.video_inbound_stats);
video_outbound_stats_.Store(stats.video_outbound_stats);
audio_inbound_stats_.Store(stats.audio_inbound_stats);
audio_outbound_stats_.Store(stats.audio_outbound_stats);
data_inbound_stats_.Store(stats.data_inbound_stats);
data_outbound_stats_.Store(stats.data_outbound_stats);
total_inbound_stats_.Store(stats.total_inbound_stats);
total_outbound_stats_.Store(stats.total_outbound_stats);
srtp_active_.store(stats.srtp_active, std::memory_order_relaxed);
rtt_ms_.store(stats.rtt_ms, std::memory_order_relaxed);
connection_latency_.Update(stats.rtt_ms);
}
MiniRtcNetTrafficStats Load() const {
MiniRtcNetTrafficStats stats{};
stats.video_inbound_stats = video_inbound_stats_.Load();
stats.video_outbound_stats = video_outbound_stats_.Load();
stats.audio_inbound_stats = audio_inbound_stats_.Load();
stats.audio_outbound_stats = audio_outbound_stats_.Load();
stats.data_inbound_stats = data_inbound_stats_.Load();
stats.data_outbound_stats = data_outbound_stats_.Load();
stats.total_inbound_stats = total_inbound_stats_.Load();
stats.total_outbound_stats = total_outbound_stats_.Load();
stats.srtp_active = srtp_active_.load(std::memory_order_relaxed);
stats.rtt_ms = rtt_ms_.load(std::memory_order_relaxed);
return stats;
}
uint32_t DataOutboundBitrate() const {
return data_outbound_stats_.bitrate.load(std::memory_order_relaxed);
}
std::optional<double> SmoothedRttMs(
ConnectionLatencyStats::Clock::time_point now =
ConnectionLatencyStats::Clock::now()) const {
return connection_latency_.Get(now);
}
void ResetLatency() { connection_latency_.Reset(); }
void Reset() {
Store(MiniRtcNetTrafficStats{});
ResetLatency();
}
private:
struct InboundStats {
void Store(const MiniRtcInboundStats& stats) {
bitrate.store(stats.bitrate, std::memory_order_relaxed);
rtp_packet_count.store(stats.rtp_packet_count, std::memory_order_relaxed);
loss_rate.store(stats.loss_rate, std::memory_order_relaxed);
}
MiniRtcInboundStats Load() const {
return {bitrate.load(std::memory_order_relaxed),
rtp_packet_count.load(std::memory_order_relaxed),
loss_rate.load(std::memory_order_relaxed)};
}
std::atomic<uint32_t> bitrate{0};
std::atomic<uint32_t> rtp_packet_count{0};
std::atomic<float> loss_rate{0};
};
struct OutboundStats {
void Store(const MiniRtcOutboundStats& stats) {
bitrate.store(stats.bitrate, std::memory_order_relaxed);
rtp_packet_count.store(stats.rtp_packet_count, std::memory_order_relaxed);
}
MiniRtcOutboundStats Load() const {
return {bitrate.load(std::memory_order_relaxed),
rtp_packet_count.load(std::memory_order_relaxed)};
}
std::atomic<uint32_t> bitrate{0};
std::atomic<uint32_t> rtp_packet_count{0};
};
InboundStats video_inbound_stats_;
OutboundStats video_outbound_stats_;
InboundStats audio_inbound_stats_;
OutboundStats audio_outbound_stats_;
InboundStats data_inbound_stats_;
OutboundStats data_outbound_stats_;
InboundStats total_inbound_stats_;
OutboundStats total_outbound_stats_;
std::atomic<bool> srtp_active_{false};
std::atomic<double> rtt_ms_{-1};
ConnectionLatencyStats connection_latency_;
};
} // namespace crossdesk::gui_detail
#endif
@@ -294,7 +294,7 @@ void PeerEventHandler::OnReceiveDataBuffer(
props->remote_cursor_state_received_ = true;
props->cursor_presentation_.Observe(merged, SDL_GetTicks());
if (changed) {
LOG_INFO("Received cursor state: seq={}, visible={}, shape={}, "
LOG_DEBUG("Received cursor state: seq={}, visible={}, shape={}, "
"mode={}, reason={}",
remote_action.cs.seq, remote_action.cs.visible,
static_cast<int>(remote_action.cs.shape),
@@ -300,6 +300,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
"connection_closed");
props->connection_established_ = false;
props->net_traffic_stats_.Reset();
props->enable_mouse_control_ = false;
runtime->ResetRemoteServiceStatus(*props);
{
@@ -336,6 +337,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
}
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->video_latency_->Reset();
props->video_frame_timing_ = {};
props->front_frame_.reset();
props->back_frame_.reset();
if (native_snapshot &&
@@ -508,6 +511,7 @@ void PeerEventHandler::OnNetStatusReport(
auto props = runtime->FindRemoteSession(remote_id);
if (!props) return;
if (props->traversal_mode_ != mode) {
props->net_traffic_stats_.ResetLatency();
props->traversal_mode_ = mode;
LOG_INFO("Net mode: [{}]", int(props->traversal_mode_));
}
@@ -518,7 +522,7 @@ void PeerEventHandler::OnNetStatusReport(
// only display client side net status if connected to itself
if (!(runtime->peer_reserved_ && !strstr(client_id, "C-"))) {
props->net_traffic_stats_ = *net_traffic_stats;
props->net_traffic_stats_.Store(*net_traffic_stats);
}
}
} // namespace crossdesk
@@ -36,7 +36,7 @@ void PeerEventHandler::OnReceiveVideoBuffer(
auto callback = handler->EnterCallback();
if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
if (!runtime || !video_frame) {
return;
}
@@ -45,13 +45,18 @@ void PeerEventHandler::OnReceiveVideoBuffer(
if (!props) return;
if (props->connection_established_) {
const auto timing_now = VideoLatencyFrame::Clock::now();
const auto timing = props->video_latency_->Frame(
video_frame->captured_timestamp, GetSystemTimeMicros(props->peer_),
timing_now);
bool background_snapshot_only = false;
auto* native_renderer = runtime->video_renderer_.get();
if (native_renderer && native_renderer->IsActive()) {
VideoRenderer::SubmitResult submit_result =
VideoRenderer::SubmitResult::failed;
if (const auto* native = GetNativeVideoFrame(video_frame)) {
submit_result = native_renderer->SubmitNativeFrame(remote_id, *native);
submit_result =
native_renderer->SubmitNativeFrame(remote_id, *native, timing);
// A CPU NV12 native frame is valid MiniRTC input, but the macOS Metal
// fast path only accepts CVPixelBuffer. Materialize it when that path
// rejects a software decoder's descriptor. This also supports peers
@@ -69,14 +74,14 @@ void PeerEventHandler::OnReceiveVideoBuffer(
submit_result = native_renderer->SubmitNv12(
remote_id, fallback.data(), fallback.size(),
static_cast<int>(native->width),
static_cast<int>(native->height));
static_cast<int>(native->height), timing);
}
}
} else
{
submit_result = native_renderer->SubmitNv12(
remote_id, reinterpret_cast<const uint8_t*>(video_frame->data),
video_frame->size, video_frame->width, video_frame->height);
video_frame->size, video_frame->width, video_frame->height, timing);
}
if (submit_result == VideoRenderer::SubmitResult::submitted) {
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
@@ -170,6 +175,7 @@ void PeerEventHandler::OnReceiveVideoBuffer(
props->video_size_ = frame_size;
props->front_frame_.swap(props->back_frame_);
props->video_frame_timing_ = timing;
props->thumbnail_frame_ = props->front_frame_;
props->thumbnail_width_ = video_frame->width;
props->thumbnail_height_ = video_frame->height;
@@ -14,6 +14,7 @@
#include <filesystem>
#include <memory>
#include <mutex>
#include <optional>
#include <queue>
#include <string>
#include <vector>
@@ -23,6 +24,8 @@
#include "display_info.h"
#include "cursor_presentation.h"
#include "minirtc.h"
#include "rendering/video_latency.h"
#include "runtime/net_traffic_stats_cache.h"
namespace crossdesk { class PeerEventHandler; }
@@ -115,6 +118,9 @@ struct RemoteSession {
int selected_display_ = 0;
size_t video_size_ = 0;
uint64_t video_frame_sequence_ = 0;
VideoLatencyFrame video_frame_timing_;
std::shared_ptr<VideoLatencyStats> video_latency_ =
std::make_shared<VideoLatencyStats>();
bool tab_selected_ = false;
bool tab_opened_ = true;
std::string remote_host_name_;
@@ -150,7 +156,9 @@ struct RemoteSession {
int fps_ = 0;
int frame_count_ = 0;
std::chrono::steady_clock::time_point last_time_;
MiniRtcNetTrafficStats net_traffic_stats_{};
// UI-thread snapshot refreshed with FPS once per second.
std::optional<VideoLatencyStats::Snapshot> video_latency_snapshot_;
NetTrafficStatsCache net_traffic_stats_;
using QueuedFile = FileTransferState::QueuedFile;
using FileTransferStatus = FileTransferState::FileTransferStatus;
+23 -7
View File
@@ -56,6 +56,8 @@ export global StreamStrings {
in-out property <string> stats-out: "Out";
in-out property <string> stats-loss-rate: "Loss Rate";
in-out property <string> stats-resolution: "Res";
in-out property <string> stats-video-latency: "Video delay";
in-out property <string> stats-connection-latency: "Connection RTT";
in-out property <string> stats-connection-mode: "Mode";
in-out property <string> stats-encryption: "Encryption";
in-out property <string> stats-encryption-enabled: "Enabled";
@@ -218,6 +220,8 @@ export component StreamWindow inherits Window {
in property <[NetworkStatsRow]> stats-rows;
in property <string> stats-fps: "0";
in property <string> stats-resolution: "";
in property <string> stats-video-latency: "—";
in property <string> stats-connection-latency: "—";
in property <string> stats-connection-mode: "";
in property <bool> stats-encryption-enabled: false;
in property <bool> file-transfer-visible: false;
@@ -253,7 +257,7 @@ export component StreamWindow inherits Window {
private property <length> stats-row-height: 18px;
private property <length> stats-header-height: 18px;
private property <length> stats-content-height:
root.stats-header-height + root.stats-rows.length * root.stats-row-height + 44px;
root.stats-header-height + root.stats-rows.length * root.stats-row-height + 65px;
private property <bool> display-menu-open: false;
private property <bool> display-menu-animation-enabled: false;
private property <float> display-menu-progress: 0.0;
@@ -903,18 +907,22 @@ export component StreamWindow inherits Window {
private property <length> details-right-width:
right-label-width-probe.preferred-width + 6px
+ max(resolution-width-probe.preferred-width,
connection-latency-width-probe.preferred-width,
encryption-width-probe.preferred-width
+ (root.stats-encryption-enabled ? 14px : 0px));
private property <length> details-right-x: self.width / 2;
private property <length> details-right-x:
min(self.width / 2, self.width - self.details-right-width - 6px);
left-label-width-probe := Text {
visible: false;
text: "FPS\n" + StreamStrings.stats-connection-mode;
text: "FPS\n" + StreamStrings.stats-video-latency
+ "\n" + StreamStrings.stats-connection-mode;
font-size: ImGuiFontStyle.small;
}
right-label-width-probe := Text {
visible: false;
text: StreamStrings.stats-resolution + "\n" + StreamStrings.stats-encryption;
text: StreamStrings.stats-resolution + "\n" + StreamStrings.stats-connection-latency
+ "\n" + StreamStrings.stats-encryption;
font-size: ImGuiFontStyle.small;
}
resolution-width-probe := Text {
@@ -923,6 +931,12 @@ export component StreamWindow inherits Window {
font-size: ImGuiFontStyle.body;
font-weight: 600;
}
connection-latency-width-probe := Text {
visible: false;
text: root.stats-connection-latency;
font-size: ImGuiFontStyle.body;
font-weight: 600;
}
encryption-width-probe := Text {
visible: false;
text: root.stats-encryption-enabled
@@ -1029,12 +1043,14 @@ export component StreamWindow inherits Window {
for metric[index] in [
{ label: "FPS", value: root.stats-fps },
{ label: StreamStrings.stats-resolution, value: root.stats-resolution },
{ label: StreamStrings.stats-video-latency, value: root.stats-video-latency },
{ label: StreamStrings.stats-connection-latency, value: root.stats-connection-latency },
{ label: StreamStrings.stats-connection-mode, value: root.stats-connection-mode },
{ label: StreamStrings.stats-encryption, value: root.stats-encryption-enabled
? StreamStrings.stats-encryption-enabled : StreamStrings.stats-encryption-disabled },
]: Rectangle {
private property <length> encryption-icon-width:
index == 3 && root.stats-encryption-enabled ? 14px : 0px;
index == 5 && root.stats-encryption-enabled ? 14px : 0px;
x: mod(index, 2) == 1 ? stats-table.details-right-x : 6px;
y: root.stats-header-height + root.stats-rows.length * stats-table.row-height
+ 2px + floor(index / 2) * 21px;
@@ -1058,14 +1074,14 @@ export component StreamWindow inherits Window {
height: parent.height;
width: parent.width - self.x - parent.encryption-icon-width;
text: metric.value;
color: index == 3 && root.stats-encryption-enabled ? #18794e : #253247;
color: index == 5 && root.stats-encryption-enabled ? #18794e : #253247;
font-size: ImGuiFontStyle.body;
font-weight: 600;
horizontal-alignment: left;
vertical-alignment: center;
overflow: elide;
}
if index == 3 && root.stats-encryption-enabled: Text {
if index == 5 && root.stats-encryption-enabled: Text {
x: metric-value.x + min(metric-value.preferred-width, metric-value.width) + 4px;
y: metric-value.y + (metric-value.height - self.height) / 2;
width: 10px;
@@ -175,6 +175,8 @@ inline int ApplyStreamWindowStrings(
strings.set_stats_out(Text(localization::out[language]));
strings.set_stats_loss_rate(Text(localization::loss_rate[language]));
strings.set_stats_resolution(Text(localization::resolution[language]));
strings.set_stats_video_latency(Text(localization::video_latency[language]));
strings.set_stats_connection_latency(Text(localization::connection_latency[language]));
strings.set_stats_connection_mode(
Text(localization::connection_mode[language]));
strings.set_stats_encryption(
+16 -28
View File
@@ -1,11 +1,7 @@
#include "rd_log.h"
#include <chrono>
#include <ctime>
#include <filesystem>
#include <iomanip>
#include <mutex>
#include <sstream>
#include <vector>
#include "spdlog/sinks/rotating_file_sink.h"
@@ -16,6 +12,7 @@ namespace crossdesk {
namespace {
std::string g_log_dir = "logs";
std::string g_logger_name = LOGGER_NAME;
std::once_flag g_logger_once_flag;
std::mutex g_logger_mutex;
std::shared_ptr<spdlog::logger> g_logger;
@@ -29,20 +26,20 @@ std::string NormalizeLogDirectory(const std::string& log_dir) {
} // namespace
void InitLogger(const std::string& log_dir) {
void InitLogger(const std::string& log_dir, const std::string& logger_name) {
const auto directory = NormalizeLogDirectory(log_dir);
std::lock_guard<std::mutex> lock(g_logger_mutex);
if (g_logger) {
if (directory != g_log_dir) {
if (directory != g_log_dir || logger_name != g_logger_name) {
g_logger->warn(
"InitLogger called after logger initialized. Ignoring log_dir: {}, "
"using previous log_dir: {}",
directory, g_log_dir);
"InitLogger called after logger initialized. Keeping logger {} in {}",
g_logger_name, g_log_dir);
}
return;
}
g_log_dir = directory;
g_logger_name = logger_name;
}
std::shared_ptr<spdlog::logger> get_logger() {
@@ -53,32 +50,23 @@ std::shared_ptr<spdlog::logger> get_logger() {
std::error_code ec;
std::filesystem::create_directories(g_log_dir, ec);
auto now = std::chrono::system_clock::now() + std::chrono::hours(8);
auto now_time = std::chrono::system_clock::to_time_t(now);
std::tm tm_info;
#ifdef _WIN32
gmtime_s(&tm_info, &now_time);
#else
gmtime_r(&now_time, &tm_info);
#endif
std::stringstream ss;
ss << LOGGER_NAME;
ss << std::put_time(&tm_info, "-%Y%m%d-%H%M%S.log");
std::string filename = g_log_dir + "/" + ss.str();
// Reuse the same rotation set across restarts. Timestamped base names
// give each launch its own backups and let the directory grow forever.
auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
g_log_dir + "/" + g_logger_name + ".log", 5 * 1024 * 1024, 3);
std::vector<spdlog::sink_ptr> sinks;
sinks.push_back(std::make_shared<spdlog::sinks::stdout_color_sink_mt>());
sinks.push_back(std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
filename, 5 * 1024 * 1024, 3));
sinks.push_back(file_sink);
auto logger = std::make_shared<spdlog::logger>(LOGGER_NAME, sinks.begin(),
auto logger = std::make_shared<spdlog::logger>(g_logger_name, sinks.begin(),
sinks.end());
logger->set_level(SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_DEBUG
? spdlog::level::debug
: spdlog::level::info);
logger->flush_on(spdlog::level::info);
spdlog::register_logger(logger);
logger->info("Logger initialized: path={}", filename);
logger->info("Logger initialized: path={}", file_sink->filename());
g_logger = std::move(logger);
});
+9 -4
View File
@@ -10,21 +10,26 @@
#include <memory>
#include <string>
#include "spdlog/spdlog.h"
#ifndef SPDLOG_ACTIVE_LEVEL
#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_INFO
#endif
#include "spdlog/spdlog.h"
namespace crossdesk {
constexpr auto LOGGER_NAME = "crossdesk";
void InitLogger(const std::string& log_dir);
void InitLogger(const std::string& log_dir,
const std::string& logger_name = LOGGER_NAME);
std::shared_ptr<spdlog::logger> get_logger();
#define LOG_TRACE(...) SPDLOG_LOGGER_TRACE(get_logger(), __VA_ARGS__)
#define LOG_DEBUG(...) SPDLOG_LOGGER_DEBUG(get_logger(), __VA_ARGS__)
#define LOG_INFO(...) SPDLOG_LOGGER_INFO(get_logger(), __VA_ARGS__)
#define LOG_WARN(...) SPDLOG_LOGGER_WARN(get_logger(), __VA_ARGS__)
#define LOG_ERROR(...) SPDLOG_LOGGER_ERROR(get_logger(), __VA_ARGS__)
#define LOG_FATAL(...) SPDLOG_LOGGER_CRITICAL(get_logger(), __VA_ARGS__)
} // namespace crossdesk
#endif
#endif
@@ -59,6 +59,7 @@ struct FrameSlot {
int height = 0;
std::string remote_id;
uint64_t sequence = 0;
VideoLatencyFrame timing;
SlotUse use = SlotUse::available;
bool valid = false;
};
@@ -320,6 +321,7 @@ struct OpenGlVideoRenderer::Impl {
bool unpack_subimage_available = false;
std::string uploaded_stream;
uint64_t uploaded_sequence = 0;
VideoLatencyFrame uploaded_timing;
#if CROSSDESK_OPENGL_CUDA_INTEROP
enum class CudaUploadResult {
@@ -1023,25 +1025,22 @@ void OpenGlVideoRenderer::DiscardStream(std::string_view remote_id) {
}
}
OpenGlVideoRenderer::SubmitResult
OpenGlVideoRenderer::SubmitNv12(std::string_view remote_id,
const uint8_t *data, size_t size,
int width, int height) {
return SubmitNv12Internal(remote_id, data, size, width, height, true);
OpenGlVideoRenderer::SubmitResult OpenGlVideoRenderer::SubmitNv12(
std::string_view remote_id, const uint8_t *data, size_t size, int width,
int height, VideoLatencyFrame timing) {
return SubmitNv12Internal(remote_id, data, size, width, height, true, timing);
}
OpenGlVideoRenderer::SubmitResult
OpenGlVideoRenderer::SubmitCachedNv12(std::string_view remote_id,
const uint8_t *data, size_t size,
int width, int height) {
return SubmitNv12Internal(remote_id, data, size, width, height, false);
OpenGlVideoRenderer::SubmitResult OpenGlVideoRenderer::SubmitCachedNv12(
std::string_view remote_id, const uint8_t *data, size_t size, int width,
int height, VideoLatencyFrame timing) {
return SubmitNv12Internal(remote_id, data, size, width, height, false,
timing);
}
OpenGlVideoRenderer::SubmitResult
OpenGlVideoRenderer::SubmitNv12Internal(std::string_view remote_id,
const uint8_t *data, size_t size,
int width, int height,
bool replace_pending) {
OpenGlVideoRenderer::SubmitResult OpenGlVideoRenderer::SubmitNv12Internal(
std::string_view remote_id, const uint8_t *data, size_t size, int width,
int height, bool replace_pending, VideoLatencyFrame timing) {
if (!IsReady()) {
return SubmitResult::failed;
}
@@ -1104,14 +1103,15 @@ OpenGlVideoRenderer::SubmitNv12Internal(std::string_view remote_id,
target->height = height;
target->remote_id.assign(remote_id);
target->sequence = frames.next_sequence++;
target->timing = timing;
target->use = SlotUse::pending;
target->valid = true;
return SubmitResult::submitted;
}
OpenGlVideoRenderer::SubmitResult
OpenGlVideoRenderer::SubmitNativeFrame(std::string_view remote_id,
const MiniRtcNativeVideoFrame &frame) {
OpenGlVideoRenderer::SubmitResult OpenGlVideoRenderer::SubmitNativeFrame(
std::string_view remote_id, const MiniRtcNativeVideoFrame &frame,
VideoLatencyFrame timing) {
if (!IsReady()) {
return SubmitResult::failed;
}
@@ -1183,6 +1183,7 @@ OpenGlVideoRenderer::SubmitNativeFrame(std::string_view remote_id,
target->height = static_cast<int>(native->height);
target->remote_id.assign(remote_id);
target->sequence = frames.next_sequence++;
target->timing = timing;
target->use = SlotUse::pending;
target->valid = true;
return SubmitResult::submitted;
@@ -1204,6 +1205,7 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
const unsigned char *y_data = nullptr;
const unsigned char *uv_data = nullptr;
NativeVideoFrameRef active_native_frame;
VideoLatencyFrame timing;
{
std::lock_guard lock(impl_->frames->mutex);
for (size_t index = 0; index < impl_->frames->slots.size(); ++index) {
@@ -1221,6 +1223,7 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
source_width = selected.width;
source_height = selected.height;
active_native_frame = selected.native_frame;
timing = selected.timing;
if (!selected.bytes.empty()) {
const size_t y_size = static_cast<size_t>(source_width) * source_height;
y_data = selected.bytes.data();
@@ -1421,6 +1424,7 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
impl_->texture_height = source_height;
impl_->uploaded_stream.assign(remote_id);
impl_->uploaded_sequence = sequence;
impl_->uploaded_timing = timing;
if (!use_pixel_unpack_buffer) {
impl_->cpu_texture_width = source_width;
impl_->cpu_texture_height = source_height;
@@ -1549,6 +1553,8 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
}
#endif
const GLenum draw_error = glGetError();
if (has_video && draw_error == GL_NO_ERROR)
impl_->uploaded_timing.MarkSubmitted();
RenderOutcome outcome{
!upload_succeeded || draw_error != GL_NO_ERROR
? RenderResult::failed
@@ -39,11 +39,14 @@ public:
// Thread-safe; called from MiniRTC's decode callback thread.
SubmitResult SubmitNv12(std::string_view remote_id, const uint8_t *data,
size_t size, int width, int height) override;
size_t size, int width, int height,
VideoLatencyFrame timing = {}) override;
SubmitResult SubmitCachedNv12(std::string_view remote_id, const uint8_t *data,
size_t size, int width, int height) override;
size_t size, int width, int height,
VideoLatencyFrame timing = {}) override;
SubmitResult SubmitNativeFrame(std::string_view remote_id,
const MiniRtcNativeVideoFrame &frame) override;
const MiniRtcNativeVideoFrame &frame,
VideoLatencyFrame timing = {}) override;
// Draws below Slint while its OpenGL context is current. All dimensions are
// physical pixels in the window client area. corner_radius_pixels clips the
@@ -61,7 +64,8 @@ public:
private:
SubmitResult SubmitNv12Internal(std::string_view remote_id,
const uint8_t *data, size_t size, int width,
int height, bool replace_pending);
int height, bool replace_pending,
VideoLatencyFrame timing);
struct Impl;
std::unique_ptr<Impl> impl_;
@@ -0,0 +1,68 @@
#include "platform/single_instance.h"
#include <cerrno>
#include <cstring>
#include <fcntl.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <unistd.h>
#include <utility>
namespace crossdesk::platform {
SingleInstanceGuard::SingleInstanceGuard(std::string application)
: application_(std::move(application)) {}
SingleInstanceGuard::~SingleInstanceGuard() { Release(); }
InstanceResult SingleInstanceGuard::TryAcquire(InstanceRole role,
std::string& error) {
error.clear();
if (handle_ != -1) return InstanceResult::kAcquired;
if (!ValidInstanceApplication(application_)) {
error = "Invalid single-instance application name";
return InstanceResult::kError;
}
// A fixed local path also coordinates launches with different TMPDIR/XDG
// settings. O_CLOEXEC prevents unrelated desktop/helper processes retaining it.
const std::string path = "/tmp/" + application_ + "-" +
std::to_string(geteuid()) + "-" +
InstanceRoleName(role) + ".lock";
const int fd = open(path.c_str(), O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600);
if (fd < 0) {
error = "Cannot open single-instance lock: " + std::string(std::strerror(errno));
return InstanceResult::kError;
}
struct stat info {};
if (fstat(fd, &info) != 0 || !S_ISREG(info.st_mode) || info.st_uid != geteuid()) {
close(fd);
error = "Single-instance lock is not a regular file owned by this user";
return InstanceResult::kError;
}
int result;
do {
result = flock(fd, LOCK_EX | LOCK_NB);
} while (result != 0 && errno == EINTR);
if (result == 0) {
handle_ = fd;
return InstanceResult::kAcquired;
}
const int lock_error = errno;
close(fd);
if (lock_error == EWOULDBLOCK || lock_error == EAGAIN)
return InstanceResult::kAlreadyRunning;
error = "Cannot acquire single-instance lock: " +
std::string(std::strerror(lock_error));
return InstanceResult::kError;
}
void SingleInstanceGuard::Release() {
if (handle_ == -1) return;
// Close rather than LOCK_UN: a forked headless application may still own the
// same open file description. Never unlink a lock that another launch opened.
close(static_cast<int>(handle_));
handle_ = -1;
}
} // namespace crossdesk::platform
@@ -78,7 +78,7 @@ bool HeadlessConsole::Enable(const std::filesystem::path& log_directory,
error_ = err;
interactive_ = isatty(STDIN_FILENO) && isatty(output_);
// Initialize after redirecting output so even the logger's first message
// stays in its original crossdesk-YYYYMMDD-HHMMSS.log file.
// stays in crossdesk.log.
try {
InitLogger(log_directory.string());
for (const auto& sink : get_logger()->sinks()) {
@@ -35,18 +35,20 @@ class MacMetalVideoRenderer final : public VideoRenderer {
// Copies one tightly packed NV12 frame into a free shared Metal slot.
SubmitResult SubmitNv12(std::string_view remote_id, const uint8_t* data,
size_t size, int width, int height) override;
size_t size, int width, int height,
VideoLatencyFrame timing = {}) override;
// Retains an IOSurface-backed VideoToolbox CVPixelBuffer until the Metal
// command buffer using it has completed.
SubmitResult SubmitNativeFrame(std::string_view remote_id,
const MiniRtcNativeVideoFrame& frame) override;
const MiniRtcNativeVideoFrame& frame,
VideoLatencyFrame timing = {}) override;
// Seeds a newly selected stream from its retained CPU snapshot without
// replacing a newer decoded frame that is already queued or rendering.
SubmitResult SubmitCachedNv12(std::string_view remote_id,
const uint8_t* data, size_t size, int width,
int height) override;
SubmitResult SubmitCachedNv12(std::string_view remote_id, const uint8_t* data,
size_t size, int width, int height,
VideoLatencyFrame timing = {}) override;
// Attaches a native CAMetalLayer-backed sibling below Slint's NSView and
// restores the containing AppKit window's ordinary opaque titlebar.
@@ -76,7 +78,8 @@ class MacMetalVideoRenderer final : public VideoRenderer {
private:
SubmitResult SubmitNv12Internal(std::string_view remote_id,
const uint8_t* data, size_t size, int width,
int height, bool replace_pending);
int height, bool replace_pending,
VideoLatencyFrame timing);
struct Impl;
std::unique_ptr<Impl> impl_;
@@ -3,11 +3,13 @@
#import <AppKit/AppKit.h>
#import <CoreVideo/CVMetalTextureCache.h>
#import <Metal/Metal.h>
#import <QuartzCore/CABase.h>
#import <QuartzCore/CAMetalLayer.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstring>
#include <limits>
#include <mutex>
@@ -340,6 +342,7 @@ struct FrameSlot {
int height = 0;
std::string remote_id;
uint64_t sequence = 0;
VideoLatencyFrame timing;
SlotUse use = SlotUse::available;
bool valid = false;
@@ -744,14 +747,16 @@ void MacMetalVideoRenderer::DiscardStream(std::string_view remote_id) {
}
}
MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitNv12(
std::string_view remote_id, const uint8_t* data, size_t size, int width,
int height) {
return SubmitNv12Internal(remote_id, data, size, width, height, true);
MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitNv12(std::string_view remote_id,
const uint8_t* data,
size_t size, int width,
int height,
VideoLatencyFrame timing) {
return SubmitNv12Internal(remote_id, data, size, width, height, true, timing);
}
MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitNativeFrame(
std::string_view remote_id, const MiniRtcNativeVideoFrame& frame) {
std::string_view remote_id, const MiniRtcNativeVideoFrame& frame, VideoLatencyFrame timing) {
if (!IsReady()) {
return SubmitResult::failed;
}
@@ -781,8 +786,7 @@ MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitNativeFrame(
packed_nv12.size()) != 0) {
return SubmitResult::failed;
}
return SubmitNv12(remote_id, packed_nv12.data(), packed_nv12.size(), width,
height);
return SubmitNv12(remote_id, packed_nv12.data(), packed_nv12.size(), width, height, timing);
};
CVPixelBufferRef pixel_buffer =
@@ -878,6 +882,7 @@ MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitNativeFrame(
target->height = height;
target->remote_id.assign(remote_id);
target->sequence = frames.next_sequence++;
target->timing = timing;
target->use = SlotUse::pending;
target->valid = true;
if (!impl_->native_path_logged.exchange(true, std::memory_order_acq_rel)) {
@@ -887,14 +892,14 @@ MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitNativeFrame(
}
MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitCachedNv12(
std::string_view remote_id, const uint8_t* data, size_t size, int width,
int height) {
return SubmitNv12Internal(remote_id, data, size, width, height, false);
std::string_view remote_id, const uint8_t* data, size_t size, int width, int height,
VideoLatencyFrame timing) {
return SubmitNv12Internal(remote_id, data, size, width, height, false, timing);
}
MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitNv12Internal(
std::string_view remote_id, const uint8_t* data, size_t size, int width,
int height, bool replace_pending) {
std::string_view remote_id, const uint8_t* data, size_t size, int width, int height,
bool replace_pending, VideoLatencyFrame timing) {
if (!IsReady()) {
return SubmitResult::failed;
}
@@ -968,6 +973,7 @@ MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitNv12Internal(
target->remote_id.assign(remote_id);
target->sequence = frames.next_sequence++;
target->timing = timing;
target->use = SlotUse::pending;
target->valid = true;
return SubmitResult::submitted;
@@ -1189,6 +1195,7 @@ MacMetalVideoRenderer::RenderOutcome MacMetalVideoRenderer::RenderLatest(
id<MTLTexture> y_texture = nil;
id<MTLTexture> uv_texture = nil;
VideoLatencyFrame timing;
{
std::lock_guard lock(impl_->frames->mutex);
auto& slot = impl_->frames->slots[slot_index];
@@ -1202,6 +1209,7 @@ MacMetalVideoRenderer::RenderOutcome MacMetalVideoRenderer::RenderLatest(
slot.use = SlotUse::in_flight;
y_texture = slot.y_texture;
uv_texture = slot.uv_texture;
timing = slot.timing;
}
MTLRenderPassDescriptor* pass = [MTLRenderPassDescriptor renderPassDescriptor];
@@ -1259,6 +1267,19 @@ MacMetalVideoRenderer::RenderOutcome MacMetalVideoRenderer::RenderLatest(
slot.use = SlotUse::available;
}
}];
[drawable addPresentedHandler:^(id<MTLDrawable> presented) {
// presentedTime uses the Core Animation host clock. Translate its age
// into our steady clock, excluding callback scheduling delay. Capturing
// timing by value keeps this safe after a session/renderer is destroyed.
const double presented_time = presented.presentedTime;
const auto now = VideoLatencyFrame::Clock::now();
const double age = CACurrentMediaTime() - presented_time;
if (presented_time > 0 && std::isfinite(age) && age >= 0 && age <= 5) {
timing.MarkPresented(
now - std::chrono::duration_cast<VideoLatencyFrame::Clock::duration>(
std::chrono::duration<double>(age)));
}
}];
[command_buffer presentDrawable:drawable];
[command_buffer commit];
impl_->rendered_stream.assign(remote_id);
@@ -0,0 +1,53 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-26
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SINGLE_INSTANCE_H_
#define _SINGLE_INSTANCE_H_
#include <cstdint>
#include <string>
namespace crossdesk::platform {
enum class InstanceRole { kLauncher, kClient, kServiceCli };
enum class InstanceResult { kAcquired, kAlreadyRunning, kError };
// Per-user ownership, independent of the install/portable directory. The OS
// releases ownership on process exit, including crashes; no PID files to age out.
class SingleInstanceGuard {
public:
explicit SingleInstanceGuard(std::string application = "CrossDesk");
~SingleInstanceGuard();
SingleInstanceGuard(const SingleInstanceGuard&) = delete;
SingleInstanceGuard& operator=(const SingleInstanceGuard&) = delete;
InstanceResult TryAcquire(InstanceRole role, std::string& error);
void Release();
private:
std::string application_;
std::intptr_t handle_ = -1;
};
inline const char* InstanceRoleName(InstanceRole role) {
switch (role) {
case InstanceRole::kLauncher: return "launcher";
case InstanceRole::kClient: return "client";
case InstanceRole::kServiceCli: return "service-cli";
}
return "client";
}
inline bool ValidInstanceApplication(const std::string& application) {
return !application.empty() && application.size() <= 100 &&
application.find_first_not_of(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") ==
std::string::npos;
}
} // namespace crossdesk::platform
#endif
+7 -3
View File
@@ -9,6 +9,7 @@
#include <vector>
#include "minirtc.h"
#include "rendering/video_latency.h"
namespace crossdesk {
@@ -54,14 +55,17 @@ public:
virtual SubmitResult SubmitNv12(std::string_view remote_id,
const uint8_t* data, size_t size, int width,
int height) = 0;
int height,
VideoLatencyFrame timing = {}) = 0;
virtual SubmitResult SubmitCachedNv12(std::string_view remote_id,
const uint8_t* data, size_t size,
int width, int height) = 0;
int width, int height,
VideoLatencyFrame timing = {}) = 0;
// Retains a platform-native decoded frame when the backend supports its
// storage and handle types. Unsupported backends keep the default fallback.
virtual SubmitResult SubmitNativeFrame(std::string_view,
const MiniRtcNativeVideoFrame&) {
const MiniRtcNativeVideoFrame&,
VideoLatencyFrame = {}) {
return SubmitResult::failed;
}
@@ -547,7 +547,7 @@ void ScreenCapturerDxgi::CaptureLoop() {
if (last_logged_cursor_embedding_ < 0 ||
(software_cursor_output_ &&
last_logged_cursor_embedding_ != static_cast<int>(cursor_embedded))) {
LOG_INFO("DXGI cursor: monitor={} usbmmidd={} mouse_update={} "
LOG_DEBUG("DXGI cursor: monitor={} usbmmidd={} mouse_update={} "
"separate_pointer_visible={} pointer_shape_bytes={} "
"compose_cursor={} embedded={}",
frame_monitor, software_cursor_output_,
@@ -1638,7 +1638,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
const ULONGLONG elapsed = GetTickCount64() - stats_started;
if (elapsed == 0 || (!force && elapsed < 5000)) return;
if (shared_frames || fallback_frames || shared_waits || shared_restarts) {
LOG_INFO(
LOG_DEBUG(
"Secure capture delivery: shared_fps={:.1f}, fallback_fps={:.1f}, "
"pending_waits={}, shared_restarts={}, status_max_ms={}",
shared_frames * 1000.0 / elapsed, fallback_frames * 1000.0 / elapsed,
@@ -12,7 +12,7 @@ void InitializePluginLogger() {
static std::once_flag once;
std::call_once(once, []() {
crossdesk::PathManager path_manager("CrossDesk");
crossdesk::InitLogger(path_manager.GetLogPath().string());
crossdesk::InitLogger(path_manager.GetLogPath().string(), "crossdesk-wgc");
});
}
@@ -0,0 +1,71 @@
#include "platform/single_instance.h"
#include <windows.h>
#include <sddl.h>
#include <utility>
#include <vector>
namespace crossdesk::platform {
SingleInstanceGuard::SingleInstanceGuard(std::string application)
: application_(std::move(application)) {}
SingleInstanceGuard::~SingleInstanceGuard() { Release(); }
InstanceResult SingleInstanceGuard::TryAcquire(InstanceRole role,
std::string& error) {
error.clear();
if (handle_ != -1) return InstanceResult::kAcquired;
if (!ValidInstanceApplication(application_)) {
error = "Invalid single-instance application name";
return InstanceResult::kError;
}
HANDLE token = nullptr;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
error = "Cannot read current user token: " + std::to_string(GetLastError());
return InstanceResult::kError;
}
DWORD size = 0;
GetTokenInformation(token, TokenUser, nullptr, 0, &size);
std::vector<unsigned char> buffer(size);
if (!GetTokenInformation(token, TokenUser, buffer.data(), size, &size)) {
const DWORD code = GetLastError();
CloseHandle(token);
error = "Cannot read current user SID: " + std::to_string(code);
return InstanceResult::kError;
}
CloseHandle(token);
wchar_t* sid = nullptr;
if (!ConvertSidToStringSidW(reinterpret_cast<TOKEN_USER*>(buffer.data())->User.Sid,
&sid)) {
error = "Cannot format current user SID: " + std::to_string(GetLastError());
return InstanceResult::kError;
}
const std::string role_name = InstanceRoleName(role);
const std::wstring name = L"Global\\" +
std::wstring(application_.begin(), application_.end()) + L"." + sid + L"." +
std::wstring(role_name.begin(), role_name.end());
LocalFree(sid);
// Object lifetime is the lease. No thread-owned mutex to transfer to a daemon.
HANDLE mutex = CreateMutexW(nullptr, FALSE, name.c_str());
const DWORD code = GetLastError();
if (!mutex) {
error = "Cannot create single-instance mutex: " + std::to_string(code);
return InstanceResult::kError;
}
if (code == ERROR_ALREADY_EXISTS) {
CloseHandle(mutex);
return InstanceResult::kAlreadyRunning;
}
handle_ = reinterpret_cast<std::intptr_t>(mutex);
return InstanceResult::kAcquired;
}
void SingleInstanceGuard::Release() {
if (handle_ == -1) return;
CloseHandle(reinterpret_cast<HANDLE>(handle_));
handle_ = -1;
}
} // namespace crossdesk::platform
+3 -1
View File
@@ -38,7 +38,9 @@ function setup_options_and_dependencies()
add_defines("USE_WAYLAND=" .. (is_config("USE_WAYLAND", true) and "1" or "0"))
add_defines("USE_DRM=" .. (is_config("USE_DRM", true) and "1" or "0"))
if is_mode("debug") then
add_defines("CROSSDESK_DEBUG")
add_defines("CROSSDESK_DEBUG", "SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_DEBUG")
else
add_defines("SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_INFO")
end
add_requireconfs("**.python", {version = "3.12", override = true, configs = {pgo = false}})
+11 -1
View File
@@ -135,6 +135,16 @@ function setup_targets()
"apps/desktop/src/gui/runtime")
add_files("apps/desktop/tests/cursor_position_test.cpp")
target("single_instance")
set_kind("object")
add_includedirs("apps/desktop/src", {public = true})
if is_os("windows") then
add_files("apps/desktop/src/platform/windows/single_instance.cpp")
add_syslinks("Advapi32")
else
add_files("apps/desktop/src/platform/common/posix_single_instance.cpp")
end
target("repository_structure_test")
set_kind("binary")
set_default(false)
@@ -540,7 +550,7 @@ function setup_targets()
target("crossdesk")
set_kind("binary")
add_deps("rd_log", "desktop_common", "gui")
add_deps("rd_log", "desktop_common", "gui", "single_instance")
add_files("apps/desktop/src/app/*.cpp")
add_includedirs("apps/desktop/src", "apps/desktop/src/app", {public = true})
if is_os("windows") then