From 8ea466830521d2a73a4c657685eda2d5bf1191f5 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Tue, 25 Aug 2026 11:45:38 +0800 Subject: [PATCH] [refactor] modularize Slint video rendering backends --- src/gui/application/gui_application.cpp | 638 ++---------------- src/gui/application/gui_application.h | 8 - src/gui/platform/metal_video_renderer.h | 40 +- src/gui/platform/metal_video_renderer.mm | 4 + src/gui/platform/opengl_video_renderer.cpp | 2 + src/gui/platform/opengl_video_renderer.h | 40 +- src/gui/platform/video_renderer.h | 70 ++ .../platform/video_renderer_factory_metal.mm | 13 + .../video_renderer_factory_opengl.cpp | 12 + src/gui/rendering/slint_video_presenter.cpp | 502 ++++++++++++++ src/gui/rendering/slint_video_presenter.h | 75 ++ src/gui/runtime/connection_runtime.cpp | 21 +- src/gui/runtime/gui_runtime.cpp | 7 +- src/gui/runtime/gui_runtime.h | 14 +- src/gui/runtime/peer_event_handler.cpp | 40 +- src/gui/runtime/peer_media_callbacks.cpp | 72 +- src/gui/runtime/remote_session.h | 2 - xmake/targets.lua | 10 +- 18 files changed, 799 insertions(+), 771 deletions(-) create mode 100644 src/gui/platform/video_renderer.h create mode 100644 src/gui/platform/video_renderer_factory_metal.mm create mode 100644 src/gui/platform/video_renderer_factory_opengl.cpp create mode 100644 src/gui/rendering/slint_video_presenter.cpp create mode 100644 src/gui/rendering/slint_video_presenter.h diff --git a/src/gui/application/gui_application.cpp b/src/gui/application/gui_application.cpp index d224c2c..2404965 100644 --- a/src/gui/application/gui_application.cpp +++ b/src/gui/application/gui_application.cpp @@ -1,7 +1,6 @@ #include "application/gui_application.h" #include -#include #include #include @@ -29,15 +28,14 @@ #include "crossdesk_ui.h" #include "fa_solid_900.h" #include "localization.h" -#include "nv12_scaler.h" #include "platform.h" +#include "platform/video_renderer.h" +#include "rendering/slint_video_presenter.h" #if _WIN32 #include -#include #include "platform/tray/win_tray.h" #elif defined(__APPLE__) -#include "platform/metal_video_renderer.h" #include "platform/tray/mac_tray.h" #include "platform/window_drag.h" #elif defined(__linux__) @@ -48,9 +46,6 @@ #include "platform/tray/linux_tray.h" #endif -#if defined(_WIN32) || defined(__linux__) -#include "platform/opengl_video_renderer.h" -#endif #include "rd_log.h" #include "server_window_state.h" #include "ui/ui_localization.h" @@ -62,42 +57,6 @@ namespace { using namespace std::chrono_literals; -#if !defined(__APPLE__) -constexpr GLint kGlClampToEdge = 0x812F; -constexpr float kStreamWindowCornerRadius = 12.0f; -#else -constexpr int kMetalAttachmentAttemptLimit = 30; -#endif - -struct VideoRenderSize { - int width = 0; - int height = 0; - - bool operator==(const VideoRenderSize&) const = default; -}; - -VideoRenderSize FitVideoToRenderArea(int source_width, int source_height, - int area_width, int area_height) { - if (source_width <= 0 || source_height <= 0 || area_width <= 0 || - area_height <= 0) { - return {}; - } - - const double scale = - std::min({1.0, static_cast(area_width) / source_width, - static_cast(area_height) / source_height}); - int width = std::max(2, static_cast(std::floor(source_width * scale))); - int height = - std::max(2, static_cast(std::floor(source_height * scale))); - - // NV12 chroma samples cover 2x2 pixels. Keep the render buffer even-sized - // so libyuv never needs to read a partial chroma sample at the edge. - width &= ~1; - height &= ~1; - return {std::min(width, source_width & ~1), - std::min(height, source_height & ~1)}; -} - std::string CreatePasswordChangeRequestId(uint64_t sequence) { const auto timestamp = std::chrono::system_clock::now() .time_since_epoch() @@ -933,19 +892,7 @@ struct GuiApplication::SlintUi { std::shared_ptr> controller_name_model = std::make_shared>(); - std::unordered_map displayed_frame_sequence; - std::vector scaled_video_frame; - std::vector scaled_video_frame_scratch; -#if !defined(__APPLE__) - std::mutex video_gl_mutex; - std::vector video_gl_conversion_frame; - std::vector video_gl_pending_frame; - uint32_t video_gl_texture = 0; - VideoRenderSize video_gl_texture_size; - VideoRenderSize video_gl_image_size; - VideoRenderSize video_gl_pending_size; - bool video_gl_pending_dirty = false; -#endif + std::unique_ptr video_presenter; std::vector tab_order; std::vector tab_ids; std::string tab_model_signature; @@ -960,9 +907,6 @@ struct GuiApplication::SlintUi { WindowDragState server_drag; int main_native_titlebar_attempts = 30; int stream_live_resize_configuration_attempts = 0; -#if defined(__APPLE__) - int stream_video_attachment_attempts = 0; -#endif int stream_initial_position_attempts = 0; int server_initial_position_attempts = 0; #if _WIN32 @@ -1117,14 +1061,9 @@ void GuiApplication::InitializeModules() { if (modules_inited_) { return; } -#if defined(_WIN32) || defined(__linux__) - opengl_video_renderer_ = std::make_unique(); -#elif defined(__APPLE__) - // Metal setup can log device, shader, or pipeline failures. Initialize it - // here, after Run() has selected the application's log directory, rather - // than from GuiRuntime's constructor before InitializeLogger(). - mac_metal_video_renderer_ = std::make_unique(); -#endif + // Renderer construction can log device, shader, or pipeline failures. + // Initialize it after Run() has selected the application's log directory. + video_renderer_ = CreateVideoRenderer(); devices_.Initialize(); CreateConnectionPeer(); modules_inited_ = true; @@ -1159,6 +1098,8 @@ void GuiApplication::InitializeUi() { } #endif ui_ = std::make_unique(); + ui_->video_presenter = + std::make_unique(*video_renderer_); #if _WIN32 ui_->main->set_custom_titlebar(true); #elif defined(__linux__) @@ -2263,9 +2204,6 @@ void GuiApplication::SyncStreamKeyboardFocus() { } void GuiApplication::SyncStreamWindow() { -#if defined(__APPLE__) - bool stream_window_created_this_sync = false; -#endif bool has_sessions = false; { std::shared_lock lock(remote_sessions_mutex_); @@ -2277,15 +2215,7 @@ void GuiApplication::SyncStreamWindow() { (*ui_->stream)->set_custom_titlebar(use_xwayland_gui_); #endif RegisterFontAwesome((*ui_->stream)->window()); -#if defined(__APPLE__) - // Commit the first visible AppKit window as an ordinary opaque window. - // The process-wide Slint event loop is already running here, so - // appkit_view() may exist before show(); that does not mean the native - // titlebar style has been committed yet. - (*ui_->stream)->set_native_video_enabled(false); -#else ConfigureStreamVideoRenderer(); -#endif // Slint globals belong to a component tree. Force the next localization // pass to initialize the newly-created, independent stream window tree. ui_->localized_language = -1; @@ -2295,15 +2225,11 @@ void GuiApplication::SyncStreamWindow() { (*ui_->stream)->set_stats_rows(ui_->stats_model); BindStreamCallbacks(); (*ui_->stream)->show(); -#if defined(__APPLE__) - stream_window_created_this_sync = true; -#endif // Repeat after the next layout pass because the backend resolves the // initial DPI and preferred size asynchronously on some platforms. ui_->stream_initial_position_attempts = 2; #if defined(__APPLE__) ui_->stream_live_resize_configuration_attempts = 30; - ui_->stream_video_attachment_attempts = kMetalAttachmentAttemptLimit; #endif stream_window_created_ = true; stream_window_inited_ = true; @@ -2323,24 +2249,10 @@ void GuiApplication::SyncStreamWindow() { --ui_->stream_live_resize_configuration_attempts; } } - if (ui_->stream_video_attachment_attempts > 0 && - !stream_window_created_this_sync) { - ConfigureStreamVideoRenderer(); - const bool metal_unavailable = - !mac_metal_video_renderer_ || !mac_metal_video_renderer_->IsReady(); - const bool metal_attached = - mac_metal_video_renderer_ && mac_metal_video_renderer_->IsAttached(); - if (metal_unavailable) { - ui_->stream_video_attachment_attempts = 0; - } else if (metal_attached) { - video_frame_dirty_.store(true, std::memory_order_release); - ui_->stream_video_attachment_attempts = 0; - } else if (--ui_->stream_video_attachment_attempts == 0) { - (*ui_->stream)->set_native_video_enabled(false); - LOG_WARN("Unable to attach the Metal video surface; using CPU rendering"); - } - } #endif + if (ui_->video_presenter && ui_->video_presenter->EnsureAttached()) { + video_frame_dirty_.store(true, std::memory_order_release); + } if (ui_->stream_initial_position_attempts > 0) { if (PositionWindowAtCenter( (*ui_->stream)->window(), ui_->main->window(), @@ -2353,29 +2265,20 @@ void GuiApplication::SyncStreamWindow() { } if (!has_sessions) { SetStreamKeyboardFocus(false); -#if defined(_WIN32) || defined(__linux__) - if (opengl_video_renderer_) { - opengl_video_renderer_->SetSelectedStream({}); - } -#endif #if defined(__APPLE__) void* stream_view = (*ui_->stream)->window().appkit_view(); SetStreamWindowFullscreen(false); UnregisterStreamWindow(stream_view); - if (mac_metal_video_renderer_) { - mac_metal_video_renderer_->SetSelectedStream({}); - mac_metal_video_renderer_->Detach(); - } #endif + if (ui_->video_presenter) { + ui_->video_presenter->Detach(); + } (*ui_->stream)->hide(); ui_->stream.reset(); stream_window_created_ = false; stream_window_inited_ = false; ui_->stream_initial_position_attempts = 0; ui_->stream_live_resize_configuration_attempts = 0; -#if defined(__APPLE__) - ui_->stream_video_attachment_attempts = 0; -#endif focused_remote_id_.clear(); controlled_remote_id_.clear(); ui_->tab_order.clear(); @@ -2592,7 +2495,7 @@ void GuiApplication::SyncStreamWindow() { } void GuiApplication::SyncStreamVideoFrame() { - if (!ui_ || !ui_->stream) { + if (!ui_ || !ui_->stream || !ui_->video_presenter) { return; } auto props = SelectedSession(); @@ -2600,430 +2503,46 @@ void GuiApplication::SyncStreamVideoFrame() { return; } -#if defined(__APPLE__) - if (mac_metal_video_renderer_ && mac_metal_video_renderer_->IsReady() && - mac_metal_video_renderer_->IsAttached()) { - const double top_inset = - !fullscreen_button_pressed_ && ui_->tab_ids.size() > 1 ? 30.0 : 0.0; - const auto render_outcome = mac_metal_video_renderer_->RenderLatest( - props->remote_id_, top_inset, !fullscreen_button_pressed_); - if (render_outcome.result == - MacMetalVideoRenderer::RenderResult::rendered) { - if (!(*ui_->stream)->get_has_frame()) { - (*ui_->stream)->set_has_frame(true); - } - if (!(*ui_->stream)->get_receiving_text().empty()) { - (*ui_->stream)->set_receiving_text(""); - } - if (render_outcome.width > 0 && render_outcome.height > 0 && - render_outcome.sequence > 0) { - (*ui_->stream) - ->set_stats_resolution( - UiText(std::to_string(render_outcome.width) + "x" + - std::to_string(render_outcome.height))); - if (ui_->displayed_frame_sequence[props->remote_id_] != - render_outcome.sequence) { - ui_->displayed_frame_sequence[props->remote_id_] = - render_outcome.sequence; - ++props->frame_count_; - } - } - return; - } - if (render_outcome.result == MacMetalVideoRenderer::RenderResult::empty) { - if ((*ui_->stream)->get_has_frame()) { - (*ui_->stream)->set_has_frame(false); - } - return; - } - if (render_outcome.result == MacMetalVideoRenderer::RenderResult::idle) { - return; - } - // A transient Metal error must not change renderer ownership mid-window. - // Keep the last frame and retry on the next UI tick. - return; - } -#endif - -#if defined(_WIN32) || defined(__linux__) - if (opengl_video_renderer_ && - opengl_video_renderer_->IsReady()) { - opengl_video_renderer_->SetSelectedStream(props->remote_id_); - SubmitCachedFrameToOpenGl(props); - if (!(*ui_->stream)->get_native_video_enabled()) { - (*ui_->stream)->set_native_video_enabled(true); - } - - int width = 0; - int height = 0; - uint64_t sequence = 0; - { - std::lock_guard lock(props->video_frame_mutex_); - width = props->video_width_; - height = props->video_height_; - sequence = props->video_frame_sequence_; - } - const bool has_frame = width > 0 && height > 0 && sequence > 0; - (*ui_->stream)->set_has_frame(has_frame); - if (has_frame) { - (*ui_->stream)->set_receiving_text(""); - (*ui_->stream) - ->set_stats_resolution(UiText(std::to_string(width) + "x" + - std::to_string(height))); - if (ui_->displayed_frame_sequence[props->remote_id_] != sequence) { - ui_->displayed_frame_sequence[props->remote_id_] = sequence; - ++props->frame_count_; - } - } - (*ui_->stream)->window().request_redraw(); - return; - } - if ((*ui_->stream)->get_native_video_enabled()) { - (*ui_->stream)->set_native_video_enabled(false); - } -#endif - - std::shared_ptr> frame; - int width = 0; - int height = 0; - uint64_t sequence = 0; + SlintVideoPresenter::Frame frame; + frame.remote_id = props->remote_id_; { std::lock_guard lock(props->video_frame_mutex_); - frame = props->front_frame_; - width = props->video_width_; - height = props->video_height_; - sequence = props->video_frame_sequence_; + frame.nv12 = props->front_frame_; + frame.width = props->video_width_; + frame.height = props->video_height_; + frame.sequence = props->video_frame_sequence_; } - const size_t nv12_size = static_cast(width) * height * 3 / 2; - if (!frame || width <= 0 || height <= 0 || frame->size() < nv12_size) { - (*ui_->stream)->set_has_frame(false); - return; + const auto result = ui_->video_presenter->Present(frame); + if (result.width > 0 && result.height > 0) { + (*ui_->stream) + ->set_stats_resolution(UiText(std::to_string(result.width) + "x" + + std::to_string(result.height))); } - if (ui_->displayed_frame_sequence[props->remote_id_] == sequence) { - return; - } - - const auto window_size = (*ui_->stream)->window().size(); - const VideoRenderSize render_size = FitVideoToRenderArea( - width, height, static_cast(window_size.width), - static_cast(window_size.height)); - const uint8_t* y_plane = frame->data(); - const uint8_t* uv_plane = - frame->data() + static_cast(width) * height; - int output_width = width; - int output_height = height; - - if (render_size.width > 0 && render_size.height > 0 && - (render_size.width != width || render_size.height != height)) { - const size_t scaled_nv12_size = - static_cast(render_size.width) * render_size.height * 3 / 2; - ui_->scaled_video_frame.resize(scaled_nv12_size); - uint8_t* scaled_y = ui_->scaled_video_frame.data(); - uint8_t* scaled_uv = - scaled_y + - static_cast(render_size.width) * render_size.height; - if (ScaleNv12ViaI420( - y_plane, width, uv_plane, width, width, height, scaled_y, - render_size.width, scaled_uv, render_size.width, render_size.width, - render_size.height, libyuv::kFilterBox, - &ui_->scaled_video_frame_scratch) == 0) { - y_plane = scaled_y; - uv_plane = scaled_uv; - output_width = render_size.width; - output_height = render_size.height; - } - } - -#if !defined(__APPLE__) - uint32_t texture_id = 0; - { - std::lock_guard lock(ui_->video_gl_mutex); - texture_id = ui_->video_gl_texture; - } - if (texture_id != 0) { - ui_->video_gl_conversion_frame.resize( - static_cast(output_width) * output_height * 4); - if (libyuv::NV12ToABGR( - y_plane, output_width, uv_plane, output_width, - ui_->video_gl_conversion_frame.data(), output_width * 4, - output_width, output_height) != 0) { - return; - } - { - std::lock_guard lock(ui_->video_gl_mutex); - if (ui_->video_gl_texture == 0) { - return; - } - texture_id = ui_->video_gl_texture; - ui_->video_gl_pending_frame.swap(ui_->video_gl_conversion_frame); - ui_->video_gl_pending_size = {output_width, output_height}; - ui_->video_gl_pending_dirty = true; - } - const VideoRenderSize output_size{output_width, output_height}; - if (ui_->video_gl_image_size != output_size) { - (*ui_->stream) - ->set_frame(slint::Image::create_from_borrowed_gl_2d_rgba_texture( - texture_id, - slint::Size{static_cast(output_width), - static_cast(output_height)})); - ui_->video_gl_image_size = output_size; - } - (*ui_->stream)->set_has_frame(true); - (*ui_->stream)->set_receiving_text(""); - (*ui_->stream)->window().request_redraw(); - ui_->displayed_frame_sequence[props->remote_id_] = sequence; + if (result.new_frame) { ++props->frame_count_; - return; } -#endif - - slint::SharedPixelBuffer pixels(output_width, - output_height); - if (libyuv::NV12ToRAW( - y_plane, output_width, uv_plane, output_width, - reinterpret_cast(pixels.begin()), output_width * 3, - output_width, output_height) != 0) { - return; - } - - (*ui_->stream)->set_frame(slint::Image(std::move(pixels))); - (*ui_->stream)->set_has_frame(true); - (*ui_->stream)->set_receiving_text(""); - ui_->displayed_frame_sequence[props->remote_id_] = sequence; - ++props->frame_count_; } void GuiApplication::ConfigureStreamVideoRenderer() { -#if defined(__APPLE__) - if (!ui_ || !ui_->stream) { + if (!ui_ || !ui_->stream || !ui_->video_presenter) { return; } - const bool native_video_enabled = - mac_metal_video_renderer_ && mac_metal_video_renderer_->IsReady(); - if (!native_video_enabled) { - (*ui_->stream)->set_native_video_enabled(false); - return; - } - if (void* view = (*ui_->stream)->window().appkit_view()) { - // Slint applies transparency through a property binding. Set it before - // restoring the AppKit titlebar so the later operation wins regardless of - // whether the backend commits the binding synchronously or next tick. - (*ui_->stream)->set_native_video_enabled(true); - if (!mac_metal_video_renderer_->Attach(view)) { - (*ui_->stream)->set_native_video_enabled(false); - } else if (auto selected_session = SelectedSession()) { - // Metal owns a separate monotonic sequence, so discard a CPU-renderer - // sequence recorded during the short window-attachment gap. - ui_->displayed_frame_sequence.erase(selected_session->remote_id_); - if (mac_metal_video_renderer_->SetSelectedStream( - selected_session->remote_id_)) { - video_frame_dirty_.store(true, std::memory_order_release); - } - SubmitCachedFrameToMetal(selected_session); - } - } else { - // Keep the initial window opaque until winit has created its native - // NSWindow. Creating it transparent opts into a full-size titlebar whose - // title field sits underneath the standard traffic-light buttons. - (*ui_->stream)->set_native_video_enabled(false); - } - return; -#else - if (!ui_ || !ui_->stream) { - return; - } - const auto error = (*ui_->stream)->window().set_rendering_notifier( - [this](slint::RenderingState state, slint::GraphicsAPI graphics_api) { - if (!ui_ || graphics_api != slint::GraphicsAPI::NativeOpenGL) { - return; - } - std::lock_guard lock(ui_->video_gl_mutex); - if (state == slint::RenderingState::RenderingSetup) { - GLuint texture = 0; - glGenTextures(1, &texture); - if (texture == 0) { - return; - } - GLint previous_texture = 0; - glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture); - glBindTexture(GL_TEXTURE_2D, texture); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, kGlClampToEdge); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, kGlClampToEdge); - glBindTexture(GL_TEXTURE_2D, - static_cast(previous_texture)); - ui_->video_gl_texture = texture; -#if defined(_WIN32) || defined(__linux__) - if (opengl_video_renderer_ && - opengl_video_renderer_->Setup()) { - opengl_video_renderer_->SetSelectedStream( - focused_remote_id_); - video_frame_dirty_.store(true, std::memory_order_release); - } -#endif - return; + ui_->video_presenter->PrepareWindow( + *ui_->stream, + [this] { + SlintVideoPresenter::SurfaceState state; + state.selected_stream = focused_remote_id_; + state.fullscreen = fullscreen_button_pressed_; + if (ui_) { + state.tab_count = ui_->tab_ids.size(); } - -#if defined(_WIN32) || defined(__linux__) - if (state == slint::RenderingState::BeforeRendering && - opengl_video_renderer_ && - opengl_video_renderer_->IsReady() && ui_->stream && - (*ui_->stream)->get_native_video_enabled()) { - const auto window_size = (*ui_->stream)->window().size(); - const float scale_factor = - std::max(1.0f, (*ui_->stream)->window().scale_factor()); - const int top_inset = - !fullscreen_button_pressed_ && ui_->tab_ids.size() > 1 - ? static_cast(std::lround(30.0f * scale_factor)) - : 0; - const bool rounded_window = - (*ui_->stream)->get_custom_titlebar() && - !fullscreen_button_pressed_ && - !(*ui_->stream)->window().is_maximized(); - const int corner_radius = - rounded_window - ? static_cast(std::lround( - kStreamWindowCornerRadius * scale_factor)) - : 0; - opengl_video_renderer_->RenderLatest( - focused_remote_id_, static_cast(window_size.width), - static_cast(window_size.height), top_inset, corner_radius); - return; - } -#endif - - if (state == slint::RenderingState::BeforeRendering && - ui_->video_gl_texture != 0 && ui_->video_gl_pending_dirty && - !ui_->video_gl_pending_frame.empty()) { - GLint previous_texture = 0; - GLint previous_unpack_alignment = 0; - glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture); - glGetIntegerv(GL_UNPACK_ALIGNMENT, &previous_unpack_alignment); - glBindTexture(GL_TEXTURE_2D, ui_->video_gl_texture); - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - if (ui_->video_gl_texture_size != ui_->video_gl_pending_size) { - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, - ui_->video_gl_pending_size.width, - ui_->video_gl_pending_size.height, 0, GL_RGBA, - GL_UNSIGNED_BYTE, ui_->video_gl_pending_frame.data()); - ui_->video_gl_texture_size = ui_->video_gl_pending_size; - } else { - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, - ui_->video_gl_pending_size.width, - ui_->video_gl_pending_size.height, GL_RGBA, - GL_UNSIGNED_BYTE, - ui_->video_gl_pending_frame.data()); - } - glPixelStorei(GL_UNPACK_ALIGNMENT, previous_unpack_alignment); - glBindTexture(GL_TEXTURE_2D, - static_cast(previous_texture)); - ui_->video_gl_pending_dirty = false; - return; - } - - if (state == slint::RenderingState::RenderingTeardown) { -#if defined(_WIN32) || defined(__linux__) - if (opengl_video_renderer_) { - opengl_video_renderer_->Teardown(); - } -#endif - if (ui_->video_gl_texture != 0) { - const GLuint texture = ui_->video_gl_texture; - glDeleteTextures(1, &texture); - } - ui_->video_gl_texture = 0; - ui_->video_gl_texture_size = {}; - ui_->video_gl_image_size = {}; - ui_->video_gl_pending_size = {}; - ui_->video_gl_pending_dirty = false; - ui_->video_gl_pending_frame.clear(); - } - }); - if (error.has_value()) { - LOG_WARN("Slint OpenGL video renderer unavailable, using pixel buffers"); -#if defined(_WIN32) || defined(__linux__) - } else if (opengl_video_renderer_) { - opengl_video_renderer_->SetSelectedStream(focused_remote_id_); -#endif - } -#endif + return state; + }, + [this] { video_frame_dirty_.store(true, std::memory_order_release); }); } -#if defined(_WIN32) || defined(__linux__) -void GuiApplication::SubmitCachedFrameToOpenGl( - const std::shared_ptr& session) { - if (!session || !opengl_video_renderer_ || - !opengl_video_renderer_->IsReady() || - session->connection_status_.load() != ConnectionStatus::Connected) { - return; - } - - std::shared_ptr> cached_frame; - int cached_width = 0; - int cached_height = 0; - { - std::lock_guard lock(session->video_frame_mutex_); - if (session->front_frame_ && !session->front_frame_->empty()) { - cached_frame = session->front_frame_; - cached_width = session->video_width_; - cached_height = session->video_height_; - } else if (session->thumbnail_frame_ && - !session->thumbnail_frame_->empty()) { - cached_frame = session->thumbnail_frame_; - cached_width = session->thumbnail_width_; - cached_height = session->thumbnail_height_; - } - } - - if (cached_frame && cached_width > 0 && cached_height > 0 && - opengl_video_renderer_->SubmitCachedNv12( - session->remote_id_, cached_frame->data(), cached_frame->size(), - cached_width, cached_height) == - OpenGlVideoRenderer::SubmitResult::submitted) { - video_frame_dirty_.store(true, std::memory_order_release); - } -} -#endif - -#if defined(__APPLE__) -void GuiApplication::SubmitCachedFrameToMetal( - const std::shared_ptr& session) { - if (!session || !mac_metal_video_renderer_ || - !mac_metal_video_renderer_->IsReady() || - session->connection_status_.load() != ConnectionStatus::Connected) { - return; - } - - std::shared_ptr> cached_frame; - int cached_width = 0; - int cached_height = 0; - { - std::lock_guard lock(session->video_frame_mutex_); - if (session->front_frame_ && !session->front_frame_->empty()) { - cached_frame = session->front_frame_; - cached_width = session->video_width_; - cached_height = session->video_height_; - } else if (session->thumbnail_frame_ && - !session->thumbnail_frame_->empty()) { - cached_frame = session->thumbnail_frame_; - cached_width = session->thumbnail_width_; - cached_height = session->thumbnail_height_; - } - } - - if (cached_frame && cached_width > 0 && cached_height > 0 && - mac_metal_video_renderer_->SubmitCachedNv12( - session->remote_id_, cached_frame->data(), cached_frame->size(), - cached_width, cached_height) == - MacMetalVideoRenderer::SubmitResult::submitted) { - video_frame_dirty_.store(true, std::memory_order_release); - } -} -#endif - void GuiApplication::ScheduleNextVideoFrame() { if (!ui_) { return; @@ -3039,28 +2558,19 @@ void GuiApplication::ScheduleNextVideoFrame() { if (now >= next_video_frame_time_) { const bool frame_dirty = video_frame_dirty_.exchange(false, std::memory_order_acq_rel); -#if defined(__APPLE__) - const bool native_render_needed = - mac_metal_video_renderer_ && mac_metal_video_renderer_->IsReady() && - mac_metal_video_renderer_->IsAttached() && ui_->stream && - mac_metal_video_renderer_->NeedsSurfaceRedraw(); + const bool native_render_needed = ui_->stream && ui_->video_presenter && + ui_->video_presenter->NeedsRedraw(); if (frame_dirty || native_render_needed) { SyncStreamVideoFrame(); } -#else - if (frame_dirty) { - SyncStreamVideoFrame(); - } -#endif do { next_video_frame_time_ += frame_interval; } while (next_video_frame_time_ <= now); } const auto after_render = std::chrono::steady_clock::now(); - const auto remaining = - std::max(next_video_frame_time_ - after_render, - std::chrono::steady_clock::duration::zero()); + const auto remaining = std::max(next_video_frame_time_ - after_render, + std::chrono::steady_clock::duration::zero()); const auto delay_ns = std::chrono::duration_cast(remaining).count(); const auto delay_ms = std::max(1, (delay_ns + 999'999) / 1'000'000); @@ -3332,20 +2842,17 @@ void GuiApplication::SelectStreamTab(int index) { return; } const std::string selected_remote_id = ui_->tab_ids[index]; -#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) const bool selection_changed = focused_remote_id_ != selected_remote_id; bool renderer_selection_changed = false; -#endif if (!controlled_remote_id_.empty() && controlled_remote_id_ != selected_remote_id) { keyboard_.ForceReleasePressedKeys(); } focused_remote_id_ = selected_remote_id; controlled_remote_id_ = selected_remote_id; -#if defined(_WIN32) || defined(__linux__) - if (opengl_video_renderer_) { + if (ui_->video_presenter) { renderer_selection_changed = - opengl_video_renderer_->SetSelectedStream(selected_remote_id); + ui_->video_presenter->SelectStream(selected_remote_id); } if (renderer_selection_changed) { video_frame_dirty_.store(true, std::memory_order_release); @@ -3353,18 +2860,6 @@ void GuiApplication::SelectStreamTab(int index) { if (selection_changed && ui_->stream) { (*ui_->stream)->set_has_frame(false); } -#elif defined(__APPLE__) - if (mac_metal_video_renderer_) { - renderer_selection_changed = - mac_metal_video_renderer_->SetSelectedStream(selected_remote_id); - } - if (renderer_selection_changed) { - video_frame_dirty_.store(true, std::memory_order_release); - } - if (selection_changed && ui_->stream) { - (*ui_->stream)->set_has_frame(false); - } -#endif std::shared_ptr selected_session; { std::shared_lock lock(remote_sessions_mutex_); @@ -3377,20 +2872,11 @@ void GuiApplication::SelectStreamTab(int index) { if (selected != remote_sessions_.end()) { selected_session = selected->second; } - start_keyboard_capturer_ = - selected_session && selected_session->control_mouse_ && - selected_session->connection_status_.load() == - ConnectionStatus::Connected; + start_keyboard_capturer_ = selected_session && + selected_session->control_mouse_ && + selected_session->connection_status_.load() == + ConnectionStatus::Connected; } -#if defined(_WIN32) || defined(__linux__) - if (renderer_selection_changed) { - SubmitCachedFrameToOpenGl(selected_session); - } -#elif defined(__APPLE__) - if (renderer_selection_changed) { - SubmitCachedFrameToMetal(selected_session); - } -#endif } void GuiApplication::ReorderStreamTab(int from, float drop_x, float tab_width) { @@ -3434,21 +2920,15 @@ void GuiApplication::CloseStreamTab(const std::string& remote_id) { std::unique_lock lock(remote_sessions_mutex_); remote_sessions_.erase(remote_id); } - ui_->displayed_frame_sequence.erase(remote_id); + if (ui_->video_presenter) { + ui_->video_presenter->ForgetStream(remote_id); + } if (focused_remote_id_ == remote_id) { focused_remote_id_.clear(); controlled_remote_id_.clear(); -#if defined(_WIN32) || defined(__linux__) - if (opengl_video_renderer_ && - opengl_video_renderer_->SetSelectedStream({})) { + if (ui_->video_presenter && ui_->video_presenter->SelectStream({})) { video_frame_dirty_.store(true, std::memory_order_release); } -#elif defined(__APPLE__) - if (mac_metal_video_renderer_ && - mac_metal_video_renderer_->SetSelectedStream({})) { - video_frame_dirty_.store(true, std::memory_order_release); - } -#endif } } @@ -3642,20 +3122,14 @@ void GuiApplication::Cleanup() { WaitForThumbnailSaveTasks(); devices_.DestroyAudioOutput(); if (ui_->stream) { -#if defined(_WIN32) || defined(__linux__) - if (opengl_video_renderer_) { - opengl_video_renderer_->SetSelectedStream({}); - } -#endif #if defined(__APPLE__) void* stream_view = (*ui_->stream)->window().appkit_view(); SetStreamWindowFullscreen(false); UnregisterStreamWindow(stream_view); - if (mac_metal_video_renderer_) { - mac_metal_video_renderer_->SetSelectedStream({}); - mac_metal_video_renderer_->Detach(); - } #endif + if (ui_->video_presenter) { + ui_->video_presenter->Detach(); + } (*ui_->stream)->hide(); ui_->stream.reset(); } diff --git a/src/gui/application/gui_application.h b/src/gui/application/gui_application.h index 6f27f38..9aea77c 100644 --- a/src/gui/application/gui_application.h +++ b/src/gui/application/gui_application.h @@ -41,14 +41,6 @@ private: void SyncStreamVideoFrame(); void ScheduleNextVideoFrame(); void ConfigureStreamVideoRenderer(); -#if defined(_WIN32) || defined(__linux__) - void SubmitCachedFrameToOpenGl( - const std::shared_ptr& session); -#endif -#if defined(__APPLE__) - void SubmitCachedFrameToMetal( - const std::shared_ptr& session); -#endif void SyncStreamKeyboardFocus(); void SetStreamKeyboardFocus(bool focused); void SyncServerWindow(); diff --git a/src/gui/platform/metal_video_renderer.h b/src/gui/platform/metal_video_renderer.h index 03de5b7..f1fdd65 100644 --- a/src/gui/platform/metal_video_renderer.h +++ b/src/gui/platform/metal_video_renderer.h @@ -8,6 +8,8 @@ #include #include +#include "platform/video_renderer.h" + namespace crossdesk { // macOS video compositor used by CrossDesk only. MiniRTC continues to expose @@ -15,51 +17,31 @@ namespace crossdesk { // copy into shared Metal buffers and converts NV12 to RGB in the fragment // shader. AppKit attachment and rendering are main-thread operations, while // SubmitNv12() is safe to call from MiniRTC's decode callback thread. -class MacMetalVideoRenderer { +class MacMetalVideoRenderer final : public VideoRenderer { public: - enum class SubmitResult { - submitted, - not_selected, - dropped, - failed, - }; - - enum class RenderResult { - rendered, - idle, - empty, - failed, - }; - - struct RenderOutcome { - RenderResult result = RenderResult::failed; - int width = 0; - int height = 0; - uint64_t sequence = 0; - }; - MacMetalVideoRenderer(); - ~MacMetalVideoRenderer(); + ~MacMetalVideoRenderer() override; MacMetalVideoRenderer(const MacMetalVideoRenderer&) = delete; MacMetalVideoRenderer& operator=(const MacMetalVideoRenderer&) = delete; - bool IsReady() const; + bool IsReady() const override; + bool IsActive() const override; // The selected stream is the only stream uploaded. Frames for background // tabs are intentionally dropped to avoid wasting memory bandwidth. - bool SetSelectedStream(std::string remote_id); - void DiscardStream(std::string_view remote_id); + bool SetSelectedStream(std::string remote_id) override; + void DiscardStream(std::string_view remote_id) override; // 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); + size_t size, int width, int height) 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); + int height) override; // Attaches a native CAMetalLayer-backed sibling below Slint's NSView and // restores the containing AppKit window's ordinary opaque titlebar. @@ -84,7 +66,7 @@ class MacMetalVideoRenderer { // a second per-frame CPU copy during normal playback. bool CopyLatestNv12(std::string_view remote_id, std::vector* output, int* width, - int* height) const; + int* height) const override; private: SubmitResult SubmitNv12Internal(std::string_view remote_id, diff --git a/src/gui/platform/metal_video_renderer.mm b/src/gui/platform/metal_video_renderer.mm index 070fcee..e3e96ba 100644 --- a/src/gui/platform/metal_video_renderer.mm +++ b/src/gui/platform/metal_video_renderer.mm @@ -602,6 +602,10 @@ bool MacMetalVideoRenderer::IsReady() const { return impl_ && impl_->Ready(); } +bool MacMetalVideoRenderer::IsActive() const { + return IsReady() && IsAttached(); +} + bool MacMetalVideoRenderer::SetSelectedStream(std::string remote_id) { if (!impl_) { return false; diff --git a/src/gui/platform/opengl_video_renderer.cpp b/src/gui/platform/opengl_video_renderer.cpp index c19a850..c287fa6 100644 --- a/src/gui/platform/opengl_video_renderer.cpp +++ b/src/gui/platform/opengl_video_renderer.cpp @@ -541,6 +541,8 @@ bool OpenGlVideoRenderer::IsReady() const { return impl_->ready.load(std::memory_order_acquire); } +bool OpenGlVideoRenderer::IsActive() const { return IsReady(); } + bool OpenGlVideoRenderer::SetSelectedStream(std::string remote_id) { std::lock_guard lock(impl_->frames->mutex); if (impl_->frames->selected_stream == remote_id) { diff --git a/src/gui/platform/opengl_video_renderer.h b/src/gui/platform/opengl_video_renderer.h index 55c1b4c..316567d 100644 --- a/src/gui/platform/opengl_video_renderer.h +++ b/src/gui/platform/opengl_video_renderer.h @@ -8,6 +8,8 @@ #include #include +#include "platform/video_renderer.h" + namespace crossdesk { // NV12 underlay for Slint's FemtoVG OpenGL renderer on Windows and Linux. @@ -15,31 +17,10 @@ namespace crossdesk { // thread. The Slint UI thread uploads the newest frame as Y and UV textures and // performs color conversion and scaling in a fragment shader before Slint // draws its overlay. -class OpenGlVideoRenderer { +class OpenGlVideoRenderer final : public VideoRenderer { public: - enum class SubmitResult { - submitted, - not_selected, - dropped, - failed, - }; - - enum class RenderResult { - rendered, - idle, - empty, - failed, - }; - - struct RenderOutcome { - RenderResult result = RenderResult::failed; - int width = 0; - int height = 0; - uint64_t sequence = 0; - }; - OpenGlVideoRenderer(); - ~OpenGlVideoRenderer(); + ~OpenGlVideoRenderer() override; OpenGlVideoRenderer(const OpenGlVideoRenderer &) = delete; OpenGlVideoRenderer &operator=(const OpenGlVideoRenderer &) = delete; @@ -48,18 +29,19 @@ public: // RenderingSetup and RenderingTeardown notifier states respectively. bool Setup(); void Teardown(); - bool IsReady() const; + bool IsReady() const override; + bool IsActive() const override; // Only the selected stream is queued for upload. Frames for other tabs are // retained by GuiRuntime at a throttled rate for thumbnail/tab restoration. - bool SetSelectedStream(std::string remote_id); - void DiscardStream(std::string_view remote_id); + bool SetSelectedStream(std::string remote_id) override; + void DiscardStream(std::string_view remote_id) override; // 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); + 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); + size_t size, int width, int height) 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 @@ -72,7 +54,7 @@ public: // second per-frame CPU copy during normal playback. bool CopyLatestNv12(std::string_view remote_id, std::vector *output, int *width, - int *height) const; + int *height) const override; private: SubmitResult SubmitNv12Internal(std::string_view remote_id, diff --git a/src/gui/platform/video_renderer.h b/src/gui/platform/video_renderer.h new file mode 100644 index 0000000..3c06011 --- /dev/null +++ b/src/gui/platform/video_renderer.h @@ -0,0 +1,70 @@ +#ifndef CROSSDESK_GUI_PLATFORM_VIDEO_RENDERER_H_ +#define CROSSDESK_GUI_PLATFORM_VIDEO_RENDERER_H_ + +#include +#include +#include +#include +#include +#include + +namespace crossdesk { + +// Platform-independent contract for the native stream video renderer. +// +// Frame submission and snapshot methods may be called from MiniRTC callback +// threads. Surface setup, rendering and teardown remain backend-specific and +// are driven by the Slint presentation module on the UI thread. +class VideoRenderer { +public: + enum class SubmitResult { + submitted, + not_selected, + dropped, + failed, + }; + + enum class RenderResult { + rendered, + idle, + empty, + failed, + }; + + struct RenderOutcome { + RenderResult result = RenderResult::failed; + int width = 0; + int height = 0; + uint64_t sequence = 0; + }; + + virtual ~VideoRenderer() = default; + + virtual bool IsReady() const = 0; + + // True only while the renderer can accept and present native frames. Metal + // additionally requires attachment to an AppKit view; OpenGL requires an + // active Slint rendering context. + virtual bool IsActive() const = 0; + + virtual bool SetSelectedStream(std::string remote_id) = 0; + virtual void DiscardStream(std::string_view remote_id) = 0; + + virtual SubmitResult SubmitNv12(std::string_view remote_id, + const uint8_t* data, size_t size, int width, + int height) = 0; + virtual SubmitResult SubmitCachedNv12(std::string_view remote_id, + const uint8_t* data, size_t size, + int width, int height) = 0; + + virtual bool CopyLatestNv12(std::string_view remote_id, + std::vector* output, int* width, + int* height) const = 0; +}; + +// Exactly one factory implementation is linked for the target platform. +std::unique_ptr CreateVideoRenderer(); + +} // namespace crossdesk + +#endif // CROSSDESK_GUI_PLATFORM_VIDEO_RENDERER_H_ diff --git a/src/gui/platform/video_renderer_factory_metal.mm b/src/gui/platform/video_renderer_factory_metal.mm new file mode 100644 index 0000000..27dfbdd --- /dev/null +++ b/src/gui/platform/video_renderer_factory_metal.mm @@ -0,0 +1,13 @@ +#include "platform/video_renderer.h" + +#include + +#include "platform/metal_video_renderer.h" + +namespace crossdesk { + +std::unique_ptr CreateVideoRenderer() { + return std::make_unique(); +} + +} // namespace crossdesk diff --git a/src/gui/platform/video_renderer_factory_opengl.cpp b/src/gui/platform/video_renderer_factory_opengl.cpp new file mode 100644 index 0000000..9389429 --- /dev/null +++ b/src/gui/platform/video_renderer_factory_opengl.cpp @@ -0,0 +1,12 @@ +#include + +#include "platform/opengl_video_renderer.h" +#include "platform/video_renderer.h" + +namespace crossdesk { + +std::unique_ptr CreateVideoRenderer() { + return std::make_unique(); +} + +} // namespace crossdesk diff --git a/src/gui/rendering/slint_video_presenter.cpp b/src/gui/rendering/slint_video_presenter.cpp new file mode 100644 index 0000000..c2e83bb --- /dev/null +++ b/src/gui/rendering/slint_video_presenter.cpp @@ -0,0 +1,502 @@ +#include "rendering/slint_video_presenter.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include "nv12_scaler.h" +#include "platform/video_renderer.h" +#include "rd_log.h" + +#if defined(__APPLE__) +#include "platform/metal_video_renderer.h" +#else +#if defined(_WIN32) +#include +#endif +#include + +#include "platform/opengl_video_renderer.h" +#endif + +namespace crossdesk { +namespace { + +#if !defined(__APPLE__) +constexpr GLint kGlClampToEdge = 0x812F; +constexpr float kStreamWindowCornerRadius = 12.0f; +#else +constexpr int kMetalAttachmentAttemptLimit = 30; +#endif + +struct VideoRenderSize { + int width = 0; + int height = 0; + + bool operator==(const VideoRenderSize&) const = default; +}; + +VideoRenderSize FitVideoToRenderArea(int source_width, int source_height, + int area_width, int area_height) { + if (source_width <= 0 || source_height <= 0 || area_width <= 0 || + area_height <= 0) { + return {}; + } + + const double scale = + std::min({1.0, static_cast(area_width) / source_width, + static_cast(area_height) / source_height}); + int width = std::max(2, static_cast(std::floor(source_width * scale))); + int height = std::max(2, static_cast(std::floor(source_height * scale))); + + width &= ~1; + height &= ~1; + return {std::min(width, source_width & ~1), + std::min(height, source_height & ~1)}; +} + +} // namespace + +struct SlintVideoPresenter::Impl { + explicit Impl(VideoRenderer& renderer) : renderer(renderer) {} + + VideoRenderer& renderer; + slint::ComponentHandle* stream = nullptr; + StateProvider state_provider; + DirtyCallback dirty_callback; + std::unordered_map displayed_frame_sequence; + std::unordered_map seeded_frame_sequence; + std::vector scaled_video_frame; + std::vector scaled_video_frame_scratch; + +#if defined(__APPLE__) + int attachment_attempts = 0; + bool skip_attachment_once = false; +#else + std::mutex gl_mutex; + std::vector gl_conversion_frame; + std::vector gl_pending_frame; + uint32_t gl_texture = 0; + VideoRenderSize gl_texture_size; + VideoRenderSize gl_image_size; + VideoRenderSize gl_pending_size; + bool gl_pending_dirty = false; +#endif + + SurfaceState CurrentState() const { + return state_provider ? state_provider() : SurfaceState{}; + } + + void MarkDirty() const { + if (dirty_callback) { + dirty_callback(); + } + } + + bool MarkPresented(const std::string& remote_id, uint64_t sequence) { + if (remote_id.empty() || sequence == 0 || + displayed_frame_sequence[remote_id] == sequence) { + return false; + } + displayed_frame_sequence[remote_id] = sequence; + return true; + } + +#if !defined(__APPLE__) + void ConfigureOpenGlNotifier() { + if (!stream) { + return; + } + + const auto error = (*stream)->window().set_rendering_notifier( + [this](slint::RenderingState state, slint::GraphicsAPI graphics_api) { + if (graphics_api != slint::GraphicsAPI::NativeOpenGL) { + return; + } + + std::lock_guard lock(gl_mutex); + auto* opengl_renderer = + dynamic_cast(&renderer); + if (state == slint::RenderingState::RenderingSetup) { + GLuint texture = 0; + glGenTextures(1, &texture); + if (texture == 0) { + return; + } + GLint previous_texture = 0; + glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, kGlClampToEdge); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, kGlClampToEdge); + glBindTexture(GL_TEXTURE_2D, static_cast(previous_texture)); + gl_texture = texture; + if (opengl_renderer && opengl_renderer->Setup()) { + const std::string selected_stream = + CurrentState().selected_stream; + opengl_renderer->SetSelectedStream(selected_stream); + seeded_frame_sequence.erase(selected_stream); + MarkDirty(); + } + return; + } + + if (state == slint::RenderingState::BeforeRendering && stream && + opengl_renderer && opengl_renderer->IsReady() && + (*stream)->get_native_video_enabled()) { + const SurfaceState surface = CurrentState(); + const auto window_size = (*stream)->window().size(); + const float scale_factor = + std::max(1.0f, (*stream)->window().scale_factor()); + const int top_inset = + !surface.fullscreen && surface.tab_count > 1 + ? static_cast(std::lround(30.0f * scale_factor)) + : 0; + const bool rounded_window = (*stream)->get_custom_titlebar() && + !surface.fullscreen && + !(*stream)->window().is_maximized(); + const int corner_radius = + rounded_window ? static_cast(std::lround( + kStreamWindowCornerRadius * scale_factor)) + : 0; + opengl_renderer->RenderLatest( + surface.selected_stream, static_cast(window_size.width), + static_cast(window_size.height), top_inset, corner_radius); + return; + } + + if (state == slint::RenderingState::BeforeRendering && + gl_texture != 0 && gl_pending_dirty && + !gl_pending_frame.empty()) { + GLint previous_texture = 0; + GLint previous_unpack_alignment = 0; + glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture); + glGetIntegerv(GL_UNPACK_ALIGNMENT, &previous_unpack_alignment); + glBindTexture(GL_TEXTURE_2D, gl_texture); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + if (gl_texture_size != gl_pending_size) { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, gl_pending_size.width, + gl_pending_size.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, + gl_pending_frame.data()); + gl_texture_size = gl_pending_size; + } else { + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, gl_pending_size.width, + gl_pending_size.height, GL_RGBA, GL_UNSIGNED_BYTE, + gl_pending_frame.data()); + } + glPixelStorei(GL_UNPACK_ALIGNMENT, previous_unpack_alignment); + glBindTexture(GL_TEXTURE_2D, static_cast(previous_texture)); + gl_pending_dirty = false; + return; + } + + if (state == slint::RenderingState::RenderingTeardown) { + if (opengl_renderer) { + opengl_renderer->Teardown(); + } + if (gl_texture != 0) { + const GLuint texture = gl_texture; + glDeleteTextures(1, &texture); + } + gl_texture = 0; + gl_texture_size = {}; + gl_image_size = {}; + gl_pending_size = {}; + gl_pending_dirty = false; + gl_pending_frame.clear(); + } + }); + + if (error.has_value()) { + LOG_WARN("Slint OpenGL video renderer unavailable, using pixel buffers"); + } else { + renderer.SetSelectedStream(CurrentState().selected_stream); + } + } +#endif +}; + +SlintVideoPresenter::SlintVideoPresenter(VideoRenderer& renderer) + : impl_(std::make_unique(renderer)) {} + +SlintVideoPresenter::~SlintVideoPresenter() = default; + +void SlintVideoPresenter::PrepareWindow( + slint::ComponentHandle& stream, + StateProvider state_provider, DirtyCallback dirty_callback) { + impl_->stream = &stream; + impl_->state_provider = std::move(state_provider); + impl_->dirty_callback = std::move(dirty_callback); +#if defined(__APPLE__) + stream->set_native_video_enabled(false); + impl_->attachment_attempts = kMetalAttachmentAttemptLimit; + impl_->skip_attachment_once = true; +#else + impl_->ConfigureOpenGlNotifier(); +#endif +} + +bool SlintVideoPresenter::EnsureAttached() { +#if defined(__APPLE__) + if (!impl_->stream || impl_->attachment_attempts <= 0) { + return false; + } + if (impl_->skip_attachment_once) { + impl_->skip_attachment_once = false; + return false; + } + + auto* metal_renderer = + dynamic_cast(&impl_->renderer); + if (!metal_renderer) { + impl_->attachment_attempts = 0; + (*impl_->stream)->set_native_video_enabled(false); + return false; + } + if (!metal_renderer->IsReady()) { + impl_->attachment_attempts = 0; + (*impl_->stream)->set_native_video_enabled(false); + return false; + } + if (metal_renderer->IsAttached()) { + impl_->attachment_attempts = 0; + return false; + } + + if (void* view = (*impl_->stream)->window().appkit_view()) { + (*impl_->stream)->set_native_video_enabled(true); + if (metal_renderer->Attach(view)) { + impl_->attachment_attempts = 0; + const SurfaceState surface = impl_->CurrentState(); + impl_->displayed_frame_sequence.erase(surface.selected_stream); + impl_->seeded_frame_sequence.erase(surface.selected_stream); + if (metal_renderer->SetSelectedStream(surface.selected_stream)) { + impl_->MarkDirty(); + } + return true; + } + (*impl_->stream)->set_native_video_enabled(false); + } + + if (--impl_->attachment_attempts == 0) { + (*impl_->stream)->set_native_video_enabled(false); + LOG_WARN("Unable to attach the Metal video surface; using CPU rendering"); + } +#endif + return false; +} + +bool SlintVideoPresenter::NeedsRedraw() const { +#if defined(__APPLE__) + const auto* metal_renderer = + dynamic_cast(&impl_->renderer); + return metal_renderer && metal_renderer->IsActive() && + metal_renderer->NeedsSurfaceRedraw(); +#else + return false; +#endif +} + +bool SlintVideoPresenter::SelectStream(std::string remote_id) { + const bool changed = impl_->renderer.SetSelectedStream(remote_id); + if (changed && !remote_id.empty()) { + impl_->seeded_frame_sequence.erase(remote_id); + } + return changed; +} + +SlintVideoPresenter::PresentResult SlintVideoPresenter::Present( + const Frame& frame) { + PresentResult result; + if (!impl_->stream || frame.remote_id.empty()) { + return result; + } + + if (impl_->renderer.IsActive() && frame.nv12 && frame.width > 0 && + frame.height > 0 && frame.sequence > 0 && + 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); + if (submit_result == VideoRenderer::SubmitResult::submitted || + submit_result == VideoRenderer::SubmitResult::dropped) { + impl_->seeded_frame_sequence[frame.remote_id] = frame.sequence; + } + } + +#if defined(__APPLE__) + auto* metal_renderer = + dynamic_cast(&impl_->renderer); + if (metal_renderer && metal_renderer->IsActive()) { + const SurfaceState surface = impl_->CurrentState(); + const double top_inset = + !surface.fullscreen && surface.tab_count > 1 ? 30.0 : 0.0; + const auto outcome = metal_renderer->RenderLatest( + frame.remote_id, top_inset, !surface.fullscreen); + if (outcome.result == VideoRenderer::RenderResult::rendered) { + (*impl_->stream)->set_has_frame(true); + (*impl_->stream)->set_receiving_text(""); + result.width = outcome.width; + result.height = outcome.height; + result.new_frame = + impl_->MarkPresented(frame.remote_id, outcome.sequence); + } else if (outcome.result == VideoRenderer::RenderResult::empty) { + (*impl_->stream)->set_has_frame(false); + } + return result; + } +#else + auto* opengl_renderer = + dynamic_cast(&impl_->renderer); + if (opengl_renderer && opengl_renderer->IsReady()) { + opengl_renderer->SetSelectedStream(frame.remote_id); + if (!(*impl_->stream)->get_native_video_enabled()) { + (*impl_->stream)->set_native_video_enabled(true); + } + + const bool has_frame = + frame.width > 0 && frame.height > 0 && frame.sequence > 0; + (*impl_->stream)->set_has_frame(has_frame); + if (has_frame) { + (*impl_->stream)->set_receiving_text(""); + result.width = frame.width; + result.height = frame.height; + result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence); + } + (*impl_->stream)->window().request_redraw(); + return result; + } + if ((*impl_->stream)->get_native_video_enabled()) { + (*impl_->stream)->set_native_video_enabled(false); + } +#endif + + const size_t nv12_size = + static_cast(frame.width) * frame.height * 3 / 2; + if (!frame.nv12 || frame.width <= 0 || frame.height <= 0 || + frame.nv12->size() < nv12_size) { + (*impl_->stream)->set_has_frame(false); + return result; + } + if (impl_->displayed_frame_sequence[frame.remote_id] == frame.sequence) { + return result; + } + + const auto window_size = (*impl_->stream)->window().size(); + const VideoRenderSize render_size = FitVideoToRenderArea( + frame.width, frame.height, static_cast(window_size.width), + static_cast(window_size.height)); + const uint8_t* y_plane = frame.nv12->data(); + const uint8_t* uv_plane = + frame.nv12->data() + static_cast(frame.width) * frame.height; + int output_width = frame.width; + int output_height = frame.height; + + if (render_size.width > 0 && render_size.height > 0 && + (render_size.width != frame.width || + render_size.height != frame.height)) { + const size_t scaled_nv12_size = + static_cast(render_size.width) * render_size.height * 3 / 2; + impl_->scaled_video_frame.resize(scaled_nv12_size); + uint8_t* scaled_y = impl_->scaled_video_frame.data(); + uint8_t* scaled_uv = + scaled_y + static_cast(render_size.width) * render_size.height; + if (ScaleNv12ViaI420(y_plane, frame.width, uv_plane, frame.width, + frame.width, frame.height, scaled_y, render_size.width, + scaled_uv, render_size.width, render_size.width, + render_size.height, libyuv::kFilterBox, + &impl_->scaled_video_frame_scratch) == 0) { + y_plane = scaled_y; + uv_plane = scaled_uv; + output_width = render_size.width; + output_height = render_size.height; + } + } + +#if !defined(__APPLE__) + uint32_t texture_id = 0; + { + std::lock_guard lock(impl_->gl_mutex); + texture_id = impl_->gl_texture; + } + if (texture_id != 0) { + impl_->gl_conversion_frame.resize(static_cast(output_width) * + output_height * 4); + if (libyuv::NV12ToABGR(y_plane, output_width, uv_plane, output_width, + impl_->gl_conversion_frame.data(), output_width * 4, + output_width, output_height) != 0) { + return result; + } + { + std::lock_guard lock(impl_->gl_mutex); + if (impl_->gl_texture == 0) { + return result; + } + texture_id = impl_->gl_texture; + impl_->gl_pending_frame.swap(impl_->gl_conversion_frame); + impl_->gl_pending_size = {output_width, output_height}; + impl_->gl_pending_dirty = true; + } + const VideoRenderSize output_size{output_width, output_height}; + if (impl_->gl_image_size != output_size) { + (*impl_->stream) + ->set_frame(slint::Image::create_from_borrowed_gl_2d_rgba_texture( + texture_id, + slint::Size{static_cast(output_width), + static_cast(output_height)})); + impl_->gl_image_size = output_size; + } + (*impl_->stream)->set_has_frame(true); + (*impl_->stream)->set_receiving_text(""); + (*impl_->stream)->window().request_redraw(); + result.width = frame.width; + result.height = frame.height; + result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence); + return result; + } +#endif + + slint::SharedPixelBuffer pixels(output_width, + output_height); + if (libyuv::NV12ToRAW(y_plane, output_width, uv_plane, output_width, + reinterpret_cast(pixels.begin()), + output_width * 3, output_width, output_height) != 0) { + return result; + } + + (*impl_->stream)->set_frame(slint::Image(std::move(pixels))); + (*impl_->stream)->set_has_frame(true); + (*impl_->stream)->set_receiving_text(""); + result.width = frame.width; + result.height = frame.height; + result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence); + return result; +} + +void SlintVideoPresenter::ForgetStream(const std::string& remote_id) { + impl_->displayed_frame_sequence.erase(remote_id); + impl_->seeded_frame_sequence.erase(remote_id); +} + +void SlintVideoPresenter::Detach() { + impl_->renderer.SetSelectedStream({}); +#if defined(__APPLE__) + if (auto* metal_renderer = + dynamic_cast(&impl_->renderer)) { + metal_renderer->Detach(); + } + impl_->attachment_attempts = 0; + impl_->skip_attachment_once = false; +#endif + impl_->stream = nullptr; + impl_->state_provider = {}; + impl_->dirty_callback = {}; +} + +} // namespace crossdesk diff --git a/src/gui/rendering/slint_video_presenter.h b/src/gui/rendering/slint_video_presenter.h new file mode 100644 index 0000000..e7ff6a7 --- /dev/null +++ b/src/gui/rendering/slint_video_presenter.h @@ -0,0 +1,75 @@ +#ifndef CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_ +#define CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_ + +#include +#include +#include +#include +#include +#include + +#include "crossdesk_ui.h" + +namespace crossdesk { + +class VideoRenderer; + +// Owns the bridge between decoded NV12 frames, the selected native graphics +// backend and Slint's StreamWindow. Platform macros and graphics-API lifecycle +// details are intentionally contained in the implementation file. +class SlintVideoPresenter { +public: + struct SurfaceState { + std::string selected_stream; + bool fullscreen = false; + size_t tab_count = 0; + }; + + struct Frame { + std::string remote_id; + std::shared_ptr> nv12; + int width = 0; + int height = 0; + uint64_t sequence = 0; + }; + + struct PresentResult { + int width = 0; + int height = 0; + bool new_frame = false; + }; + + using StateProvider = std::function; + using DirtyCallback = std::function; + + explicit SlintVideoPresenter(VideoRenderer& renderer); + ~SlintVideoPresenter(); + + SlintVideoPresenter(const SlintVideoPresenter&) = delete; + SlintVideoPresenter& operator=(const SlintVideoPresenter&) = delete; + + // Called before the window is shown. OpenGL installs Slint's rendering + // notifier here; macOS deliberately defers native view attachment until a + // later UI pass, after AppKit has committed the ordinary window style. + void PrepareWindow(slint::ComponentHandle& stream, + StateProvider state_provider, + DirtyCallback dirty_callback); + + // Returns true only when a deferred native surface became attached during + // this call, allowing the caller to seed it from a cached frame once. + bool EnsureAttached(); + bool NeedsRedraw() const; + + bool SelectStream(std::string remote_id); + PresentResult Present(const Frame& frame); + void ForgetStream(const std::string& remote_id); + void Detach(); + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace crossdesk + +#endif // CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_ diff --git a/src/gui/runtime/connection_runtime.cpp b/src/gui/runtime/connection_runtime.cpp index de485f2..01f2981 100644 --- a/src/gui/runtime/connection_runtime.cpp +++ b/src/gui/runtime/connection_runtime.cpp @@ -11,12 +11,7 @@ #include "display_stream_id.h" #include "localization.h" #include "platform.h" -#if defined(_WIN32) || defined(__linux__) -#include "platform/opengl_video_renderer.h" -#endif -#ifdef __APPLE__ -#include "platform/metal_video_renderer.h" -#endif +#include "platform/video_renderer.h" #include "rd_log.h" #include "runtime/gui_runtime.h" @@ -229,21 +224,14 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr props) { frame_snapshot = props->front_frame_; video_width = props->video_width_; video_height = props->video_height_; -#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) if ((!frame_snapshot || frame_snapshot->empty()) && props->thumbnail_frame_ && !props->thumbnail_frame_->empty()) { frame_snapshot = props->thumbnail_frame_; video_width = props->thumbnail_width_; video_height = props->thumbnail_height_; } -#endif } -#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) -#if defined(__APPLE__) - auto* native_renderer = mac_metal_video_renderer_.get(); -#else - auto* native_renderer = opengl_video_renderer_.get(); -#endif + auto* native_renderer = video_renderer_.get(); if ((!frame_snapshot || frame_snapshot->empty()) && native_renderer) { auto native_snapshot = std::make_shared>(); if (native_renderer->CopyLatestNv12(props->remote_id_, @@ -252,7 +240,6 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr props) { frame_snapshot = std::move(native_snapshot); } } -#endif if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 && video_height > 0) { @@ -276,12 +263,10 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr props) { } } -#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) if (native_renderer) { native_renderer->DiscardStream(props->remote_id_); video_frame_dirty_.store(true, std::memory_order_release); } -#endif if (props->peer_) { LOG_INFO("[{}] Leave connection [{}]", props->local_id_, props->remote_id_); @@ -343,12 +328,10 @@ void GuiRuntime::ResetRemoteSessionResources( std::lock_guard lock(props->video_frame_mutex_); props->front_frame_.reset(); props->back_frame_.reset(); -#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) props->thumbnail_frame_.reset(); props->thumbnail_width_ = 0; props->thumbnail_height_ = 0; props->background_snapshot_time_ = {}; -#endif props->video_width_ = 0; props->video_height_ = 0; props->video_size_ = 0; diff --git a/src/gui/runtime/gui_runtime.cpp b/src/gui/runtime/gui_runtime.cpp index 2952a58..78123da 100644 --- a/src/gui/runtime/gui_runtime.cpp +++ b/src/gui/runtime/gui_runtime.cpp @@ -12,12 +12,7 @@ #include "display_stream_id.h" #include "localization.h" -#if defined(_WIN32) || defined(__linux__) -#include "platform/opengl_video_renderer.h" -#endif -#ifdef __APPLE__ -#include "platform/metal_video_renderer.h" -#endif +#include "platform/video_renderer.h" #include "rd_log.h" namespace crossdesk { diff --git a/src/gui/runtime/gui_runtime.h b/src/gui/runtime/gui_runtime.h index fd45ae8..fcb3146 100644 --- a/src/gui/runtime/gui_runtime.h +++ b/src/gui/runtime/gui_runtime.h @@ -15,12 +15,7 @@ namespace crossdesk { -#if defined(_WIN32) || defined(__linux__) -class OpenGlVideoRenderer; -#endif -#ifdef __APPLE__ -class MacMetalVideoRenderer; -#endif +class VideoRenderer; // Shared GUI runtime. It owns subsystem controllers and cross-cutting session // state, but no window lifecycle, ImGui view, or transport callback methods. @@ -90,12 +85,7 @@ class GuiRuntime : protected gui_detail::GuiState { KeyboardController keyboard_; PeerEventHandler peer_events_; std::atomic video_frame_dirty_{false}; -#if defined(_WIN32) || defined(__linux__) - std::unique_ptr opengl_video_renderer_; -#endif -#ifdef __APPLE__ - std::unique_ptr mac_metal_video_renderer_; -#endif + std::unique_ptr video_renderer_; private: friend class ClipboardController; diff --git a/src/gui/runtime/peer_event_handler.cpp b/src/gui/runtime/peer_event_handler.cpp index 4cdd49d..c825518 100644 --- a/src/gui/runtime/peer_event_handler.cpp +++ b/src/gui/runtime/peer_event_handler.cpp @@ -16,12 +16,7 @@ #include "file_transfer.h" #include "localization.h" #include "platform.h" -#if defined(_WIN32) || defined(__linux__) -#include "platform/opengl_video_renderer.h" -#endif -#ifdef __APPLE__ -#include "platform/metal_video_renderer.h" -#endif +#include "platform/video_renderer.h" #include "rd_log.h" #include "runtime/gui_runtime.h" #include "runtime/remote_action_codec.h" @@ -319,61 +314,46 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, props->enable_mouse_control_ = false; runtime->ResetRemoteServiceStatus(*props); -#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) std::shared_ptr> native_snapshot; int native_snapshot_width = 0; int native_snapshot_height = 0; bool needs_native_snapshot = false; { std::lock_guard lock(props->video_frame_mutex_); - needs_native_snapshot = !props->thumbnail_frame_ || - props->thumbnail_frame_->empty(); + needs_native_snapshot = + !props->thumbnail_frame_ || props->thumbnail_frame_->empty(); } if (needs_native_snapshot) { -#if defined(__APPLE__) - auto* native_renderer = runtime->mac_metal_video_renderer_.get(); -#else - auto* native_renderer = runtime->opengl_video_renderer_.get(); -#endif + auto* native_renderer = runtime->video_renderer_.get(); auto snapshot = std::make_shared>(); - if (native_renderer && native_renderer->CopyLatestNv12( - remote_id, snapshot.get(), - &native_snapshot_width, - &native_snapshot_height)) { + if (native_renderer && + native_renderer->CopyLatestNv12(remote_id, snapshot.get(), + &native_snapshot_width, + &native_snapshot_height)) { native_snapshot = std::move(snapshot); } } -#endif { std::lock_guard lock(props->video_frame_mutex_); props->front_frame_.reset(); props->back_frame_.reset(); -#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) if (native_snapshot && (!props->thumbnail_frame_ || props->thumbnail_frame_->empty())) { props->thumbnail_frame_ = std::move(native_snapshot); props->thumbnail_width_ = native_snapshot_width; props->thumbnail_height_ = native_snapshot_height; } -#endif props->video_width_ = 0; props->video_height_ = 0; props->video_size_ = 0; props->render_rect_dirty_ = true; props->stream_cleanup_pending_ = true; } -#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) -#if defined(__APPLE__) - auto* native_renderer = runtime->mac_metal_video_renderer_.get(); -#else - auto* native_renderer = runtime->opengl_video_renderer_.get(); -#endif + auto* native_renderer = runtime->video_renderer_.get(); if (native_renderer) { native_renderer->DiscardStream(remote_id); - runtime->video_frame_dirty_.store(true, - std::memory_order_release); + runtime->video_frame_dirty_.store(true, std::memory_order_release); } -#endif runtime->focus_on_stream_window_ = false; diff --git a/src/gui/runtime/peer_media_callbacks.cpp b/src/gui/runtime/peer_media_callbacks.cpp index 7487c42..606adcc 100644 --- a/src/gui/runtime/peer_media_callbacks.cpp +++ b/src/gui/runtime/peer_media_callbacks.cpp @@ -1,36 +1,26 @@ -#include "runtime/peer_event_handler.h" - +#include #include #include #include #include #include +#include "platform/video_renderer.h" #include "runtime/gui_runtime.h" -#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) -#include - -#if defined(__APPLE__) -#include "platform/metal_video_renderer.h" -#else -#include "platform/opengl_video_renderer.h" -#endif -#endif +#include "runtime/peer_event_handler.h" namespace crossdesk { -#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) namespace { constexpr auto kBackgroundSnapshotInterval = std::chrono::seconds(1); } // namespace -#endif void PeerEventHandler::OnReceiveVideoBuffer( - const XVideoFrame *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(user_data); - GuiRuntime *runtime = handler ? &handler->owner_ : nullptr; + const XVideoFrame* 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(user_data); + GuiRuntime* runtime = handler ? &handler->owner_ : nullptr; if (!runtime) { return; } @@ -41,32 +31,20 @@ void PeerEventHandler::OnReceiveVideoBuffer( runtime->remote_sessions_.end()) { return; } - GuiRuntime::RemoteSession *props = + GuiRuntime::RemoteSession* props = runtime->remote_sessions_.find(remote_id)->second.get(); if (props->connection_established_) { -#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) bool background_snapshot_only = false; -#if defined(__APPLE__) - auto* native_renderer = runtime->mac_metal_video_renderer_.get(); - using NativeVideoRenderer = MacMetalVideoRenderer; -#else - auto* native_renderer = runtime->opengl_video_renderer_.get(); - using NativeVideoRenderer = OpenGlVideoRenderer; -#endif - if (native_renderer && native_renderer->IsReady() -#if defined(__APPLE__) - && native_renderer->IsAttached() -#endif - ) { + auto* native_renderer = runtime->video_renderer_.get(); + if (native_renderer && native_renderer->IsActive()) { const auto submit_result = native_renderer->SubmitNv12( remote_id, reinterpret_cast(video_frame->data), video_frame->size, video_frame->width, video_frame->height); - if (submit_result == NativeVideoRenderer::SubmitResult::submitted) { + if (submit_result == VideoRenderer::SubmitResult::submitted) { std::lock_guard lock(props->video_frame_mutex_); - const bool size_changed = - (props->video_width_ != video_frame->width) || - (props->video_height_ != video_frame->height); + const bool size_changed = (props->video_width_ != video_frame->width) || + (props->video_height_ != video_frame->height); if (size_changed) { props->render_rect_dirty_ = true; } @@ -86,22 +64,19 @@ void PeerEventHandler::OnReceiveVideoBuffer( runtime->video_frame_dirty_.store(true, std::memory_order_release); return; } - if (submit_result == NativeVideoRenderer::SubmitResult::dropped || - submit_result == NativeVideoRenderer::SubmitResult::failed) { + if (submit_result == VideoRenderer::SubmitResult::dropped || + submit_result == VideoRenderer::SubmitResult::failed) { // Keep presenting the last native frame. A later decoded frame can // reuse the renderer without changing ownership mid-window. props->streaming_ = true; return; } - if (submit_result == - NativeVideoRenderer::SubmitResult::not_selected) { + if (submit_result == VideoRenderer::SubmitResult::not_selected) { background_snapshot_only = true; } } -#endif { std::lock_guard lock(props->video_frame_mutex_); -#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) const auto now = std::chrono::steady_clock::now(); if (background_snapshot_only && props->thumbnail_frame_ && !props->thumbnail_frame_->empty() && @@ -112,7 +87,6 @@ void PeerEventHandler::OnReceiveVideoBuffer( props->streaming_ = true; return; } -#endif // Allocate a third buffer only while the UI still owns the old snapshot. if (!props->back_frame_ || props->back_frame_.use_count() != 1) { @@ -137,32 +111,28 @@ void PeerEventHandler::OnReceiveVideoBuffer( props->video_size_ = video_frame->size; props->front_frame_.swap(props->back_frame_); -#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) props->thumbnail_frame_ = props->front_frame_; props->thumbnail_width_ = video_frame->width; props->thumbnail_height_ = video_frame->height; if (background_snapshot_only) { props->background_snapshot_time_ = now; } -#endif ++props->video_frame_sequence_; } props->streaming_ = true; -#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) if (background_snapshot_only) { return; } -#endif runtime->video_frame_dirty_.store(true, std::memory_order_release); } } 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(user_data); - GuiRuntime *runtime = handler ? &handler->owner_ : nullptr; + 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(user_data); + GuiRuntime* runtime = handler ? &handler->owner_ : nullptr; if (!runtime) { return; } @@ -172,4 +142,4 @@ void PeerEventHandler::OnReceiveAudioBuffer( runtime->devices_.PushAudio(data, size); } -} // namespace crossdesk +} // namespace crossdesk diff --git a/src/gui/runtime/remote_session.h b/src/gui/runtime/remote_session.h index e90bd40..276e2ff 100644 --- a/src/gui/runtime/remote_session.h +++ b/src/gui/runtime/remote_session.h @@ -110,14 +110,12 @@ struct RemoteSession { std::mutex video_frame_mutex_; std::shared_ptr> front_frame_; std::shared_ptr> back_frame_; -#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) // Retains the most recent background CPU frame across disconnect cleanup // so closing an unselected tab can still update its recent-item thumbnail. std::shared_ptr> thumbnail_frame_; int thumbnail_width_ = 0; int thumbnail_height_ = 0; std::chrono::steady_clock::time_point background_snapshot_time_; -#endif bool render_rect_dirty_ = false; bool stream_cleanup_pending_ = false; float mouse_pos_x_ = 0; diff --git a/xmake/targets.lua b/xmake/targets.lua index a95d95a..21552ec 100644 --- a/xmake/targets.lua +++ b/xmake/targets.lua @@ -270,6 +270,7 @@ function setup_targets() "device_controller", "thumbnail", "version_checker", "tools") add_files("src/gui/render.cpp", "src/gui/application/gui_application.cpp", "src/gui/application/portable_service_integration.cpp", + "src/gui/rendering/*.cpp", "src/gui/runtime/*.cpp", "src/gui/features/devices/*.cpp", "src/gui/features/input/*.cpp", "src/gui/features/clipboard/*.cpp", "src/gui/features/file_transfer/*.cpp", @@ -279,16 +280,19 @@ function setup_targets() add_cxxflags("/bigobj") add_links("opengl32") add_files("src/gui/platform/tray/win_tray.cpp", - "src/gui/platform/opengl_video_renderer.cpp") + "src/gui/platform/opengl_video_renderer.cpp", + "src/gui/platform/video_renderer_factory_opengl.cpp") add_includedirs("src/service/windows", {public = true}) elseif is_os("macosx") then add_files("src/gui/runtime/*.mm", "src/gui/platform/tray/*.mm", "src/gui/platform/window_drag_mac.mm", - "src/gui/platform/metal_video_renderer.mm") + "src/gui/platform/metal_video_renderer.mm", + "src/gui/platform/video_renderer_factory_metal.mm") elseif is_os("linux") then add_links("GL") add_files("src/gui/platform/tray/linux_tray.cpp", - "src/gui/platform/opengl_video_renderer.cpp") + "src/gui/platform/opengl_video_renderer.cpp", + "src/gui/platform/video_renderer_factory_opengl.cpp") end if is_os("windows") then