diff --git a/src/gui/application/gui_application.cpp b/src/gui/application/gui_application.cpp index 5cee3bd..d224c2c 100644 --- a/src/gui/application/gui_application.cpp +++ b/src/gui/application/gui_application.cpp @@ -36,7 +36,6 @@ #include #include "platform/tray/win_tray.h" -#include "platform/windows_opengl_video_renderer.h" #elif defined(__APPLE__) #include "platform/metal_video_renderer.h" #include "platform/tray/mac_tray.h" @@ -49,6 +48,9 @@ #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,6 +64,7 @@ using namespace std::chrono_literals; #if !defined(__APPLE__) constexpr GLint kGlClampToEdge = 0x812F; +constexpr float kStreamWindowCornerRadius = 12.0f; #else constexpr int kMetalAttachmentAttemptLimit = 30; #endif @@ -1114,9 +1117,8 @@ void GuiApplication::InitializeModules() { if (modules_inited_) { return; } -#if defined(_WIN32) - windows_opengl_video_renderer_ = - std::make_unique(); +#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 @@ -2351,9 +2353,9 @@ void GuiApplication::SyncStreamWindow() { } if (!has_sessions) { SetStreamKeyboardFocus(false); -#if defined(_WIN32) - if (windows_opengl_video_renderer_) { - windows_opengl_video_renderer_->SetSelectedStream({}); +#if defined(_WIN32) || defined(__linux__) + if (opengl_video_renderer_) { + opengl_video_renderer_->SetSelectedStream({}); } #endif #if defined(__APPLE__) @@ -2643,10 +2645,10 @@ void GuiApplication::SyncStreamVideoFrame() { } #endif -#if defined(_WIN32) - if (windows_opengl_video_renderer_ && - windows_opengl_video_renderer_->IsReady()) { - windows_opengl_video_renderer_->SetSelectedStream(props->remote_id_); +#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); @@ -2854,10 +2856,10 @@ void GuiApplication::ConfigureStreamVideoRenderer() { glBindTexture(GL_TEXTURE_2D, static_cast(previous_texture)); ui_->video_gl_texture = texture; -#if defined(_WIN32) - if (windows_opengl_video_renderer_ && - windows_opengl_video_renderer_->Setup()) { - windows_opengl_video_renderer_->SetSelectedStream( +#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); } @@ -2865,10 +2867,10 @@ void GuiApplication::ConfigureStreamVideoRenderer() { return; } -#if defined(_WIN32) +#if defined(_WIN32) || defined(__linux__) if (state == slint::RenderingState::BeforeRendering && - windows_opengl_video_renderer_ && - windows_opengl_video_renderer_->IsReady() && ui_->stream && + 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 = @@ -2877,9 +2879,18 @@ void GuiApplication::ConfigureStreamVideoRenderer() { !fullscreen_button_pressed_ && ui_->tab_ids.size() > 1 ? static_cast(std::lround(30.0f * scale_factor)) : 0; - windows_opengl_video_renderer_->RenderLatest( + 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); + static_cast(window_size.height), top_inset, corner_radius); return; } #endif @@ -2914,9 +2925,9 @@ void GuiApplication::ConfigureStreamVideoRenderer() { } if (state == slint::RenderingState::RenderingTeardown) { -#if defined(_WIN32) - if (windows_opengl_video_renderer_) { - windows_opengl_video_renderer_->Teardown(); +#if defined(_WIN32) || defined(__linux__) + if (opengl_video_renderer_) { + opengl_video_renderer_->Teardown(); } #endif if (ui_->video_gl_texture != 0) { @@ -2933,19 +2944,19 @@ void GuiApplication::ConfigureStreamVideoRenderer() { }); if (error.has_value()) { LOG_WARN("Slint OpenGL video renderer unavailable, using pixel buffers"); -#if defined(_WIN32) - } else if (windows_opengl_video_renderer_) { - windows_opengl_video_renderer_->SetSelectedStream(focused_remote_id_); +#if defined(_WIN32) || defined(__linux__) + } else if (opengl_video_renderer_) { + opengl_video_renderer_->SetSelectedStream(focused_remote_id_); #endif } #endif } -#if defined(_WIN32) +#if defined(_WIN32) || defined(__linux__) void GuiApplication::SubmitCachedFrameToOpenGl( const std::shared_ptr& session) { - if (!session || !windows_opengl_video_renderer_ || - !windows_opengl_video_renderer_->IsReady() || + if (!session || !opengl_video_renderer_ || + !opengl_video_renderer_->IsReady() || session->connection_status_.load() != ConnectionStatus::Connected) { return; } @@ -2968,10 +2979,10 @@ void GuiApplication::SubmitCachedFrameToOpenGl( } if (cached_frame && cached_width > 0 && cached_height > 0 && - windows_opengl_video_renderer_->SubmitCachedNv12( + opengl_video_renderer_->SubmitCachedNv12( session->remote_id_, cached_frame->data(), cached_frame->size(), cached_width, cached_height) == - WindowsOpenGlVideoRenderer::SubmitResult::submitted) { + OpenGlVideoRenderer::SubmitResult::submitted) { video_frame_dirty_.store(true, std::memory_order_release); } } @@ -3321,7 +3332,7 @@ void GuiApplication::SelectStreamTab(int index) { return; } const std::string selected_remote_id = ui_->tab_ids[index]; -#if defined(__APPLE__) || defined(_WIN32) +#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) const bool selection_changed = focused_remote_id_ != selected_remote_id; bool renderer_selection_changed = false; #endif @@ -3331,10 +3342,10 @@ void GuiApplication::SelectStreamTab(int index) { } focused_remote_id_ = selected_remote_id; controlled_remote_id_ = selected_remote_id; -#if defined(_WIN32) - if (windows_opengl_video_renderer_) { +#if defined(_WIN32) || defined(__linux__) + if (opengl_video_renderer_) { renderer_selection_changed = - windows_opengl_video_renderer_->SetSelectedStream(selected_remote_id); + opengl_video_renderer_->SetSelectedStream(selected_remote_id); } if (renderer_selection_changed) { video_frame_dirty_.store(true, std::memory_order_release); @@ -3371,7 +3382,7 @@ void GuiApplication::SelectStreamTab(int index) { selected_session->connection_status_.load() == ConnectionStatus::Connected; } -#if defined(_WIN32) +#if defined(_WIN32) || defined(__linux__) if (renderer_selection_changed) { SubmitCachedFrameToOpenGl(selected_session); } @@ -3427,9 +3438,9 @@ void GuiApplication::CloseStreamTab(const std::string& remote_id) { if (focused_remote_id_ == remote_id) { focused_remote_id_.clear(); controlled_remote_id_.clear(); -#if defined(_WIN32) - if (windows_opengl_video_renderer_ && - windows_opengl_video_renderer_->SetSelectedStream({})) { +#if defined(_WIN32) || defined(__linux__) + if (opengl_video_renderer_ && + opengl_video_renderer_->SetSelectedStream({})) { video_frame_dirty_.store(true, std::memory_order_release); } #elif defined(__APPLE__) @@ -3631,9 +3642,9 @@ void GuiApplication::Cleanup() { WaitForThumbnailSaveTasks(); devices_.DestroyAudioOutput(); if (ui_->stream) { -#if defined(_WIN32) - if (windows_opengl_video_renderer_) { - windows_opengl_video_renderer_->SetSelectedStream({}); +#if defined(_WIN32) || defined(__linux__) + if (opengl_video_renderer_) { + opengl_video_renderer_->SetSelectedStream({}); } #endif #if defined(__APPLE__) diff --git a/src/gui/application/gui_application.h b/src/gui/application/gui_application.h index d0cbd3f..6f27f38 100644 --- a/src/gui/application/gui_application.h +++ b/src/gui/application/gui_application.h @@ -41,7 +41,7 @@ private: void SyncStreamVideoFrame(); void ScheduleNextVideoFrame(); void ConfigureStreamVideoRenderer(); -#if defined(_WIN32) +#if defined(_WIN32) || defined(__linux__) void SubmitCachedFrameToOpenGl( const std::shared_ptr& session); #endif diff --git a/src/gui/platform/windows_opengl_video_renderer.cpp b/src/gui/platform/opengl_video_renderer.cpp similarity index 75% rename from src/gui/platform/windows_opengl_video_renderer.cpp rename to src/gui/platform/opengl_video_renderer.cpp index c571cab..c19a850 100644 --- a/src/gui/platform/windows_opengl_video_renderer.cpp +++ b/src/gui/platform/opengl_video_renderer.cpp @@ -1,13 +1,20 @@ -#include "platform/windows_opengl_video_renderer.h" +#include "platform/opengl_video_renderer.h" -#if !defined(_WIN32) -#error "WindowsOpenGlVideoRenderer is only available on Windows" +#if !defined(_WIN32) && !defined(__linux__) +#error "OpenGlVideoRenderer is only available on Windows and Linux" #endif +#if defined(_WIN32) #include +#elif defined(__linux__) +#include +#endif #include #include +#if defined(__linux__) +#include +#endif #include #include @@ -49,6 +56,7 @@ struct SharedFrameState { }; template bool LoadOpenGlFunction(T *function, const char *name) { +#if defined(_WIN32) PROC address = wglGetProcAddress(name); if (address == nullptr || address == reinterpret_cast(1) || address == reinterpret_cast(2) || @@ -58,6 +66,14 @@ template bool LoadOpenGlFunction(T *function, const char *name) { address = module ? GetProcAddress(module, name) : nullptr; } *function = reinterpret_cast(address); +#elif defined(__linux__) + void *address = dlsym(RTLD_DEFAULT, name); + if (address == nullptr) { + address = reinterpret_cast( + glXGetProcAddressARB(reinterpret_cast(name))); + } + *function = reinterpret_cast(address); +#endif return *function != nullptr; } @@ -85,7 +101,10 @@ struct OpenGlFunctions { PFNGLGETVERTEXATTRIBPOINTERVPROC get_vertex_attrib_pointer_v = nullptr; PFNGLLINKPROGRAMPROC link_program = nullptr; PFNGLSHADERSOURCEPROC shader_source = nullptr; + PFNGLUNIFORM1FPROC uniform_1f = nullptr; PFNGLUNIFORM1IPROC uniform_1i = nullptr; + PFNGLUNIFORM2FPROC uniform_2f = nullptr; + PFNGLUNIFORM4FPROC uniform_4f = nullptr; PFNGLUSEPROGRAMPROC use_program = nullptr; PFNGLVERTEXATTRIBPOINTERPROC vertex_attrib_pointer = nullptr; @@ -116,7 +135,10 @@ struct OpenGlFunctions { "glGetVertexAttribPointerv") && LoadOpenGlFunction(&link_program, "glLinkProgram") && LoadOpenGlFunction(&shader_source, "glShaderSource") && + LoadOpenGlFunction(&uniform_1f, "glUniform1f") && LoadOpenGlFunction(&uniform_1i, "glUniform1i") && + LoadOpenGlFunction(&uniform_2f, "glUniform2f") && + LoadOpenGlFunction(&uniform_4f, "glUniform4f") && LoadOpenGlFunction(&use_program, "glUseProgram") && LoadOpenGlFunction(&vertex_attrib_pointer, "glVertexAttribPointer"); } @@ -205,7 +227,7 @@ void RestoreVertexAttrib(const OpenGlFunctions &gl, GLuint index, } // namespace -struct WindowsOpenGlVideoRenderer::Impl { +struct OpenGlVideoRenderer::Impl { OpenGlFunctions gl; std::shared_ptr frames = std::make_shared(); @@ -218,6 +240,10 @@ struct WindowsOpenGlVideoRenderer::Impl { GLint texcoord_location = -1; GLint y_texture_location = -1; GLint uv_texture_location = -1; + GLint target_size_location = -1; + GLint video_rect_location = -1; + GLint corner_radius_location = -1; + GLint video_enabled_location = -1; int texture_width = 0; int texture_height = 0; std::string uploaded_stream; @@ -232,6 +258,10 @@ struct WindowsOpenGlVideoRenderer::Impl { texcoord_location = -1; y_texture_location = -1; uv_texture_location = -1; + target_size_location = -1; + video_rect_location = -1; + corner_radius_location = -1; + video_enabled_location = -1; texture_width = 0; texture_height = 0; uploaded_stream.clear(); @@ -239,12 +269,12 @@ struct WindowsOpenGlVideoRenderer::Impl { } }; -WindowsOpenGlVideoRenderer::WindowsOpenGlVideoRenderer() +OpenGlVideoRenderer::OpenGlVideoRenderer() : impl_(std::make_unique()) {} -WindowsOpenGlVideoRenderer::~WindowsOpenGlVideoRenderer() = default; +OpenGlVideoRenderer::~OpenGlVideoRenderer() = default; -bool WindowsOpenGlVideoRenderer::Setup() { +bool OpenGlVideoRenderer::Setup() { if (IsReady()) { return true; } @@ -274,15 +304,41 @@ precision mediump float; #endif uniform sampler2D y_texture; uniform sampler2D uv_texture; +uniform vec2 target_size; +uniform vec4 video_rect; +uniform float corner_radius; +uniform float video_enabled; varying vec2 video_texcoord; void main() { - float y = 1.16438356 * - (texture2D(y_texture, video_texcoord).r - 16.0 / 255.0); - vec2 uv = texture2D(uv_texture, video_texcoord).ra - vec2(0.5, 0.5); - vec3 rgb = vec3(y + 1.59602678 * uv.y, - y - 0.39176229 * uv.x - 0.81296764 * uv.y, - y + 2.01723214 * uv.x); - gl_FragColor = vec4(clamp(rgb, 0.0, 1.0), 1.0); + vec2 surface_point = video_texcoord * target_size; + vec3 rgb = vec3(0.0); + if (video_enabled > 0.5 && + surface_point.x >= video_rect.x && + surface_point.x <= video_rect.x + video_rect.z && + surface_point.y >= video_rect.y && + surface_point.y <= video_rect.y + video_rect.w) { + vec2 frame_texcoord = + (surface_point - video_rect.xy) / video_rect.zw; + float y = 1.16438356 * + (texture2D(y_texture, frame_texcoord).r - 16.0 / 255.0); + vec2 uv = + texture2D(uv_texture, frame_texcoord).ra - vec2(0.5, 0.5); + rgb = clamp(vec3(y + 1.59602678 * uv.y, + y - 0.39176229 * uv.x - 0.81296764 * uv.y, + y + 2.01723214 * uv.x), + 0.0, 1.0); + } + float coverage = 1.0; + if (corner_radius > 0.0) { + vec2 half_size = target_size * 0.5; + vec2 corner = abs(surface_point - half_size) - + (half_size - vec2(corner_radius)); + float distance_to_edge = + length(max(corner, vec2(0.0))) + + min(max(corner.x, corner.y), 0.0) - corner_radius; + coverage = clamp(0.5 - distance_to_edge, 0.0, 1.0); + } + gl_FragColor = vec4(rgb * coverage, coverage); } )glsl"; static constexpr char kDesktopVertexShader[] = R"glsl( @@ -299,15 +355,41 @@ void main() { #version 110 uniform sampler2D y_texture; uniform sampler2D uv_texture; +uniform vec2 target_size; +uniform vec4 video_rect; +uniform float corner_radius; +uniform float video_enabled; varying vec2 video_texcoord; void main() { - float y = 1.16438356 * - (texture2D(y_texture, video_texcoord).r - 16.0 / 255.0); - vec2 uv = texture2D(uv_texture, video_texcoord).ra - vec2(0.5, 0.5); - vec3 rgb = vec3(y + 1.59602678 * uv.y, - y - 0.39176229 * uv.x - 0.81296764 * uv.y, - y + 2.01723214 * uv.x); - gl_FragColor = vec4(clamp(rgb, 0.0, 1.0), 1.0); + vec2 surface_point = video_texcoord * target_size; + vec3 rgb = vec3(0.0); + if (video_enabled > 0.5 && + surface_point.x >= video_rect.x && + surface_point.x <= video_rect.x + video_rect.z && + surface_point.y >= video_rect.y && + surface_point.y <= video_rect.y + video_rect.w) { + vec2 frame_texcoord = + (surface_point - video_rect.xy) / video_rect.zw; + float y = 1.16438356 * + (texture2D(y_texture, frame_texcoord).r - 16.0 / 255.0); + vec2 uv = + texture2D(uv_texture, frame_texcoord).ra - vec2(0.5, 0.5); + rgb = clamp(vec3(y + 1.59602678 * uv.y, + y - 0.39176229 * uv.x - 0.81296764 * uv.y, + y + 2.01723214 * uv.x), + 0.0, 1.0); + } + float coverage = 1.0; + if (corner_radius > 0.0) { + vec2 half_size = target_size * 0.5; + vec2 corner = abs(surface_point - half_size) - + (half_size - vec2(corner_radius)); + float distance_to_edge = + length(max(corner, vec2(0.0))) + + min(max(corner.x, corner.y), 0.0) - corner_radius; + coverage = clamp(0.5 - distance_to_edge, 0.0, 1.0); + } + gl_FragColor = vec4(rgb * coverage, coverage); } )glsl"; @@ -363,8 +445,19 @@ void main() { impl_->gl.get_uniform_location(impl_->program, "y_texture"); impl_->uv_texture_location = impl_->gl.get_uniform_location(impl_->program, "uv_texture"); + impl_->target_size_location = + impl_->gl.get_uniform_location(impl_->program, "target_size"); + impl_->video_rect_location = + impl_->gl.get_uniform_location(impl_->program, "video_rect"); + impl_->corner_radius_location = + impl_->gl.get_uniform_location(impl_->program, "corner_radius"); + impl_->video_enabled_location = + impl_->gl.get_uniform_location(impl_->program, "video_enabled"); if (impl_->position_location < 0 || impl_->texcoord_location < 0 || - impl_->y_texture_location < 0 || impl_->uv_texture_location < 0) { + impl_->y_texture_location < 0 || impl_->uv_texture_location < 0 || + impl_->target_size_location < 0 || impl_->video_rect_location < 0 || + impl_->corner_radius_location < 0 || + impl_->video_enabled_location < 0) { LOG_ERROR("OpenGL NV12 program is missing required shader bindings"); impl_->gl.delete_program(impl_->program); impl_->ResetGlHandles(); @@ -429,7 +522,7 @@ void main() { return true; } -void WindowsOpenGlVideoRenderer::Teardown() { +void OpenGlVideoRenderer::Teardown() { impl_->ready.store(false, std::memory_order_release); if (impl_->y_texture != 0) glDeleteTextures(1, &impl_->y_texture); @@ -444,11 +537,11 @@ void WindowsOpenGlVideoRenderer::Teardown() { impl_->ResetGlHandles(); } -bool WindowsOpenGlVideoRenderer::IsReady() const { +bool OpenGlVideoRenderer::IsReady() const { return impl_->ready.load(std::memory_order_acquire); } -bool WindowsOpenGlVideoRenderer::SetSelectedStream(std::string remote_id) { +bool OpenGlVideoRenderer::SetSelectedStream(std::string remote_id) { std::lock_guard lock(impl_->frames->mutex); if (impl_->frames->selected_stream == remote_id) { return false; @@ -463,7 +556,7 @@ bool WindowsOpenGlVideoRenderer::SetSelectedStream(std::string remote_id) { return true; } -void WindowsOpenGlVideoRenderer::DiscardStream(std::string_view remote_id) { +void OpenGlVideoRenderer::DiscardStream(std::string_view remote_id) { std::lock_guard lock(impl_->frames->mutex); for (auto &slot : impl_->frames->slots) { if (slot.remote_id == remote_id && slot.use != SlotUse::uploading) { @@ -477,22 +570,22 @@ void WindowsOpenGlVideoRenderer::DiscardStream(std::string_view remote_id) { } } -WindowsOpenGlVideoRenderer::SubmitResult -WindowsOpenGlVideoRenderer::SubmitNv12(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); } -WindowsOpenGlVideoRenderer::SubmitResult -WindowsOpenGlVideoRenderer::SubmitCachedNv12(std::string_view remote_id, +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); } -WindowsOpenGlVideoRenderer::SubmitResult -WindowsOpenGlVideoRenderer::SubmitNv12Internal(std::string_view remote_id, +OpenGlVideoRenderer::SubmitResult +OpenGlVideoRenderer::SubmitNv12Internal(std::string_view remote_id, const uint8_t *data, size_t size, int width, int height, bool replace_pending) { @@ -562,10 +655,11 @@ WindowsOpenGlVideoRenderer::SubmitNv12Internal(std::string_view remote_id, return SubmitResult::submitted; } -WindowsOpenGlVideoRenderer::RenderOutcome -WindowsOpenGlVideoRenderer::RenderLatest(std::string_view remote_id, - int target_width, int target_height, - int top_inset_pixels) { +OpenGlVideoRenderer::RenderOutcome +OpenGlVideoRenderer::RenderLatest(std::string_view remote_id, + int target_width, int target_height, + int top_inset_pixels, + int corner_radius_pixels) { if (!IsReady() || target_width <= 0 || target_height <= 0) { return {RenderResult::failed}; } @@ -643,7 +737,10 @@ WindowsOpenGlVideoRenderer::RenderLatest(std::string_view remote_id, glDisable(GL_STENCIL_TEST); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); glViewport(0, 0, target_width, target_height); - glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + // The Slint items above this underlay are transparent around the custom + // window corners. Keep those pixels transparent so the compositor can show + // the desktop instead of an opaque black framebuffer clear. + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); bool upload_succeeded = true; @@ -696,60 +793,77 @@ WindowsOpenGlVideoRenderer::RenderLatest(std::string_view remote_id, } } - RenderOutcome outcome{RenderResult::empty}; - if (upload_succeeded && impl_->uploaded_stream == remote_id && - impl_->texture_width > 0 && impl_->texture_height > 0) { + const int content_y = std::clamp(top_inset_pixels, 0, target_height); + const int content_height = target_height - content_y; + const bool has_video = + upload_succeeded && impl_->uploaded_stream == remote_id && + impl_->texture_width > 0 && impl_->texture_height > 0 && + content_height > 0; + int video_x = 0; + int video_y = content_y; + int video_width = target_width; + int video_height = content_height; + if (has_video) { source_width = impl_->texture_width; source_height = impl_->texture_height; sequence = impl_->uploaded_sequence; - const int video_height = std::max( - 0, target_height - std::clamp(top_inset_pixels, 0, target_height)); - if (video_height > 0) { - const double source_aspect = - static_cast(source_width) / source_height; - const double target_aspect = - static_cast(target_width) / video_height; - int viewport_width = target_width; - int viewport_height = video_height; - int viewport_x = 0; - int viewport_y = 0; - if (source_aspect > target_aspect) { - viewport_height = - std::max(1, static_cast(target_width / source_aspect + 0.5)); - viewport_y = (video_height - viewport_height) / 2; - } else { - viewport_width = - std::max(1, static_cast(video_height * source_aspect + 0.5)); - viewport_x = (target_width - viewport_width) / 2; - } - - glViewport(viewport_x, viewport_y, viewport_width, viewport_height); - impl_->gl.use_program(impl_->program); - impl_->gl.active_texture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, impl_->y_texture); - impl_->gl.uniform_1i(impl_->y_texture_location, 0); - impl_->gl.active_texture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, impl_->uv_texture); - impl_->gl.uniform_1i(impl_->uv_texture_location, 1); - impl_->gl.bind_buffer(GL_ARRAY_BUFFER, impl_->vertex_buffer); - impl_->gl.vertex_attrib_pointer( - static_cast(impl_->position_location), 2, GL_FLOAT, GL_FALSE, - 4 * static_cast(sizeof(GLfloat)), nullptr); - impl_->gl.vertex_attrib_pointer( - static_cast(impl_->texcoord_location), 2, GL_FLOAT, GL_FALSE, - 4 * static_cast(sizeof(GLfloat)), - reinterpret_cast(2 * sizeof(GLfloat))); - impl_->gl.enable_vertex_attrib_array( - static_cast(impl_->position_location)); - impl_->gl.enable_vertex_attrib_array( - static_cast(impl_->texcoord_location)); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - outcome = {glGetError() == GL_NO_ERROR ? RenderResult::rendered - : RenderResult::failed, - source_width, source_height, sequence}; + const double source_aspect = + static_cast(source_width) / source_height; + const double target_aspect = + static_cast(target_width) / content_height; + if (source_aspect > target_aspect) { + video_height = + std::max(1, static_cast(target_width / source_aspect + 0.5)); + video_y = content_y + (content_height - video_height) / 2; + } else { + video_width = + std::max(1, static_cast(content_height * source_aspect + 0.5)); + video_x = (target_width - video_width) / 2; } } + while (glGetError() != GL_NO_ERROR) { + } + impl_->gl.use_program(impl_->program); + impl_->gl.active_texture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, impl_->y_texture); + impl_->gl.uniform_1i(impl_->y_texture_location, 0); + impl_->gl.active_texture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, impl_->uv_texture); + impl_->gl.uniform_1i(impl_->uv_texture_location, 1); + impl_->gl.uniform_2f(impl_->target_size_location, + static_cast(target_width), + static_cast(target_height)); + impl_->gl.uniform_4f( + impl_->video_rect_location, static_cast(video_x), + static_cast(video_y), static_cast(video_width), + static_cast(video_height)); + impl_->gl.uniform_1f( + impl_->corner_radius_location, + static_cast(std::clamp( + corner_radius_pixels, 0, std::min(target_width, target_height) / 2))); + impl_->gl.uniform_1f(impl_->video_enabled_location, has_video ? 1.0f : 0.0f); + impl_->gl.bind_buffer(GL_ARRAY_BUFFER, impl_->vertex_buffer); + impl_->gl.vertex_attrib_pointer( + static_cast(impl_->position_location), 2, GL_FLOAT, GL_FALSE, + 4 * static_cast(sizeof(GLfloat)), nullptr); + impl_->gl.vertex_attrib_pointer( + static_cast(impl_->texcoord_location), 2, GL_FLOAT, GL_FALSE, + 4 * static_cast(sizeof(GLfloat)), + reinterpret_cast(2 * sizeof(GLfloat))); + impl_->gl.enable_vertex_attrib_array( + static_cast(impl_->position_location)); + impl_->gl.enable_vertex_attrib_array( + static_cast(impl_->texcoord_location)); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + const GLenum draw_error = glGetError(); + RenderOutcome outcome{ + !upload_succeeded || draw_error != GL_NO_ERROR + ? RenderResult::failed + : has_video ? RenderResult::rendered : RenderResult::empty, + has_video ? source_width : 0, has_video ? source_height : 0, + has_video ? sequence : 0}; + RestoreVertexAttrib(impl_->gl, static_cast(impl_->position_location), position_state); RestoreVertexAttrib(impl_->gl, static_cast(impl_->texcoord_location), @@ -786,7 +900,7 @@ WindowsOpenGlVideoRenderer::RenderLatest(std::string_view remote_id, return outcome; } -bool WindowsOpenGlVideoRenderer::CopyLatestNv12( +bool OpenGlVideoRenderer::CopyLatestNv12( std::string_view remote_id, std::vector *output, int *width, int *height) const { if (!output || !width || !height) { diff --git a/src/gui/platform/windows_opengl_video_renderer.h b/src/gui/platform/opengl_video_renderer.h similarity index 67% rename from src/gui/platform/windows_opengl_video_renderer.h rename to src/gui/platform/opengl_video_renderer.h index fa96284..55c1b4c 100644 --- a/src/gui/platform/windows_opengl_video_renderer.h +++ b/src/gui/platform/opengl_video_renderer.h @@ -1,5 +1,5 @@ -#ifndef CROSSDESK_GUI_PLATFORM_WINDOWS_OPENGL_VIDEO_RENDERER_H_ -#define CROSSDESK_GUI_PLATFORM_WINDOWS_OPENGL_VIDEO_RENDERER_H_ +#ifndef CROSSDESK_GUI_PLATFORM_OPENGL_VIDEO_RENDERER_H_ +#define CROSSDESK_GUI_PLATFORM_OPENGL_VIDEO_RENDERER_H_ #include #include @@ -10,11 +10,12 @@ namespace crossdesk { -// Windows NV12 underlay for Slint's FemtoVG OpenGL renderer. MiniRTC submits -// tightly packed CPU NV12 frames from its decode callback thread. The Slint UI -// thread uploads the newest frame as Y and UV textures and performs color -// conversion and scaling in a fragment shader before Slint draws its overlay. -class WindowsOpenGlVideoRenderer { +// NV12 underlay for Slint's FemtoVG OpenGL renderer on Windows and Linux. +// MiniRTC submits tightly packed CPU NV12 frames from its decode callback +// thread. The Slint UI thread uploads the newest frame as Y and UV textures and +// performs color conversion and scaling in a fragment shader before Slint +// draws its overlay. +class OpenGlVideoRenderer { public: enum class SubmitResult { submitted, @@ -37,12 +38,11 @@ public: uint64_t sequence = 0; }; - WindowsOpenGlVideoRenderer(); - ~WindowsOpenGlVideoRenderer(); + OpenGlVideoRenderer(); + ~OpenGlVideoRenderer(); - WindowsOpenGlVideoRenderer(const WindowsOpenGlVideoRenderer &) = delete; - WindowsOpenGlVideoRenderer & - operator=(const WindowsOpenGlVideoRenderer &) = delete; + OpenGlVideoRenderer(const OpenGlVideoRenderer &) = delete; + OpenGlVideoRenderer &operator=(const OpenGlVideoRenderer &) = delete; // These methods must run while Slint's OpenGL context is current, from the // RenderingSetup and RenderingTeardown notifier states respectively. @@ -61,10 +61,12 @@ public: SubmitResult SubmitCachedNv12(std::string_view remote_id, const uint8_t *data, size_t size, int width, int height); - // Draws below Slint while its OpenGL context is current. target dimensions - // and top_inset_pixels are physical pixels in the window client area. + // Draws below Slint while its OpenGL context is current. All dimensions are + // physical pixels in the window client area. corner_radius_pixels clips the + // underlay to Slint's custom rounded window surface. RenderOutcome RenderLatest(std::string_view remote_id, int target_width, - int target_height, int top_inset_pixels); + int target_height, int top_inset_pixels, + int corner_radius_pixels); // Used during disconnect cleanup so thumbnail generation does not require a // second per-frame CPU copy during normal playback. @@ -83,4 +85,4 @@ private: } // namespace crossdesk -#endif // CROSSDESK_GUI_PLATFORM_WINDOWS_OPENGL_VIDEO_RENDERER_H_ +#endif // CROSSDESK_GUI_PLATFORM_OPENGL_VIDEO_RENDERER_H_ diff --git a/src/gui/runtime/connection_runtime.cpp b/src/gui/runtime/connection_runtime.cpp index d6ec5de..de485f2 100644 --- a/src/gui/runtime/connection_runtime.cpp +++ b/src/gui/runtime/connection_runtime.cpp @@ -11,8 +11,8 @@ #include "display_stream_id.h" #include "localization.h" #include "platform.h" -#if defined(_WIN32) -#include "platform/windows_opengl_video_renderer.h" +#if defined(_WIN32) || defined(__linux__) +#include "platform/opengl_video_renderer.h" #endif #ifdef __APPLE__ #include "platform/metal_video_renderer.h" @@ -229,7 +229,7 @@ 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) +#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_; @@ -238,11 +238,11 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr props) { } #endif } -#if defined(__APPLE__) || defined(_WIN32) -#if defined(_WIN32) - auto* native_renderer = windows_opengl_video_renderer_.get(); -#else +#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 if ((!frame_snapshot || frame_snapshot->empty()) && native_renderer) { auto native_snapshot = std::make_shared>(); @@ -276,7 +276,7 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr props) { } } -#if defined(__APPLE__) || defined(_WIN32) +#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); @@ -343,7 +343,7 @@ void GuiRuntime::ResetRemoteSessionResources( std::lock_guard lock(props->video_frame_mutex_); props->front_frame_.reset(); props->back_frame_.reset(); -#if defined(__APPLE__) || defined(_WIN32) +#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) props->thumbnail_frame_.reset(); props->thumbnail_width_ = 0; props->thumbnail_height_ = 0; diff --git a/src/gui/runtime/gui_runtime.cpp b/src/gui/runtime/gui_runtime.cpp index 9070dd3..2952a58 100644 --- a/src/gui/runtime/gui_runtime.cpp +++ b/src/gui/runtime/gui_runtime.cpp @@ -12,8 +12,8 @@ #include "display_stream_id.h" #include "localization.h" -#if defined(_WIN32) -#include "platform/windows_opengl_video_renderer.h" +#if defined(_WIN32) || defined(__linux__) +#include "platform/opengl_video_renderer.h" #endif #ifdef __APPLE__ #include "platform/metal_video_renderer.h" diff --git a/src/gui/runtime/gui_runtime.h b/src/gui/runtime/gui_runtime.h index 635f184..fd45ae8 100644 --- a/src/gui/runtime/gui_runtime.h +++ b/src/gui/runtime/gui_runtime.h @@ -15,8 +15,8 @@ namespace crossdesk { -#if defined(_WIN32) -class WindowsOpenGlVideoRenderer; +#if defined(_WIN32) || defined(__linux__) +class OpenGlVideoRenderer; #endif #ifdef __APPLE__ class MacMetalVideoRenderer; @@ -90,8 +90,8 @@ class GuiRuntime : protected gui_detail::GuiState { KeyboardController keyboard_; PeerEventHandler peer_events_; std::atomic video_frame_dirty_{false}; -#if defined(_WIN32) - std::unique_ptr windows_opengl_video_renderer_; +#if defined(_WIN32) || defined(__linux__) + std::unique_ptr opengl_video_renderer_; #endif #ifdef __APPLE__ std::unique_ptr mac_metal_video_renderer_; diff --git a/src/gui/runtime/peer_event_handler.cpp b/src/gui/runtime/peer_event_handler.cpp index d7dcbea..4cdd49d 100644 --- a/src/gui/runtime/peer_event_handler.cpp +++ b/src/gui/runtime/peer_event_handler.cpp @@ -16,8 +16,8 @@ #include "file_transfer.h" #include "localization.h" #include "platform.h" -#if defined(_WIN32) -#include "platform/windows_opengl_video_renderer.h" +#if defined(_WIN32) || defined(__linux__) +#include "platform/opengl_video_renderer.h" #endif #ifdef __APPLE__ #include "platform/metal_video_renderer.h" @@ -319,7 +319,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, props->enable_mouse_control_ = false; runtime->ResetRemoteServiceStatus(*props); -#if defined(__APPLE__) || defined(_WIN32) +#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) std::shared_ptr> native_snapshot; int native_snapshot_width = 0; int native_snapshot_height = 0; @@ -330,11 +330,10 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, props->thumbnail_frame_->empty(); } if (needs_native_snapshot) { -#if defined(_WIN32) - auto* native_renderer = - runtime->windows_opengl_video_renderer_.get(); -#else +#if defined(__APPLE__) auto* native_renderer = runtime->mac_metal_video_renderer_.get(); +#else + auto* native_renderer = runtime->opengl_video_renderer_.get(); #endif auto snapshot = std::make_shared>(); if (native_renderer && native_renderer->CopyLatestNv12( @@ -349,7 +348,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, std::lock_guard lock(props->video_frame_mutex_); props->front_frame_.reset(); props->back_frame_.reset(); -#if defined(__APPLE__) || defined(_WIN32) +#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) if (native_snapshot && (!props->thumbnail_frame_ || props->thumbnail_frame_->empty())) { props->thumbnail_frame_ = std::move(native_snapshot); @@ -363,11 +362,11 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, props->render_rect_dirty_ = true; props->stream_cleanup_pending_ = true; } -#if defined(__APPLE__) || defined(_WIN32) -#if defined(_WIN32) - auto* native_renderer = runtime->windows_opengl_video_renderer_.get(); -#else +#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 if (native_renderer) { native_renderer->DiscardStream(remote_id); diff --git a/src/gui/runtime/peer_media_callbacks.cpp b/src/gui/runtime/peer_media_callbacks.cpp index 8964aeb..7487c42 100644 --- a/src/gui/runtime/peer_media_callbacks.cpp +++ b/src/gui/runtime/peer_media_callbacks.cpp @@ -7,18 +7,18 @@ #include #include "runtime/gui_runtime.h" -#if defined(__APPLE__) || defined(_WIN32) +#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) #include -#if defined(_WIN32) -#include "platform/windows_opengl_video_renderer.h" -#else +#if defined(__APPLE__) #include "platform/metal_video_renderer.h" +#else +#include "platform/opengl_video_renderer.h" #endif #endif namespace crossdesk { -#if defined(__APPLE__) || defined(_WIN32) +#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) namespace { constexpr auto kBackgroundSnapshotInterval = std::chrono::seconds(1); @@ -45,14 +45,14 @@ void PeerEventHandler::OnReceiveVideoBuffer( runtime->remote_sessions_.find(remote_id)->second.get(); if (props->connection_established_) { -#if defined(__APPLE__) || defined(_WIN32) +#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) bool background_snapshot_only = false; -#if defined(_WIN32) - auto* native_renderer = runtime->windows_opengl_video_renderer_.get(); - using NativeVideoRenderer = WindowsOpenGlVideoRenderer; -#else +#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__) @@ -101,7 +101,7 @@ void PeerEventHandler::OnReceiveVideoBuffer( #endif { std::lock_guard lock(props->video_frame_mutex_); -#if defined(__APPLE__) || defined(_WIN32) +#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() && @@ -137,7 +137,7 @@ void PeerEventHandler::OnReceiveVideoBuffer( props->video_size_ = video_frame->size; props->front_frame_.swap(props->back_frame_); -#if defined(__APPLE__) || defined(_WIN32) +#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; @@ -149,7 +149,7 @@ void PeerEventHandler::OnReceiveVideoBuffer( } props->streaming_ = true; -#if defined(__APPLE__) || defined(_WIN32) +#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__) if (background_snapshot_only) { return; } diff --git a/src/gui/runtime/remote_session.h b/src/gui/runtime/remote_session.h index 4305e68..e90bd40 100644 --- a/src/gui/runtime/remote_session.h +++ b/src/gui/runtime/remote_session.h @@ -110,7 +110,7 @@ struct RemoteSession { std::mutex video_frame_mutex_; std::shared_ptr> front_frame_; std::shared_ptr> back_frame_; -#if defined(__APPLE__) || defined(_WIN32) +#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_; diff --git a/xmake/targets.lua b/xmake/targets.lua index 3e445d5..a95d95a 100644 --- a/xmake/targets.lua +++ b/xmake/targets.lua @@ -279,7 +279,7 @@ function setup_targets() add_cxxflags("/bigobj") add_links("opengl32") add_files("src/gui/platform/tray/win_tray.cpp", - "src/gui/platform/windows_opengl_video_renderer.cpp") + "src/gui/platform/opengl_video_renderer.cpp") add_includedirs("src/service/windows", {public = true}) elseif is_os("macosx") then add_files("src/gui/runtime/*.mm", "src/gui/platform/tray/*.mm", @@ -287,7 +287,8 @@ function setup_targets() "src/gui/platform/metal_video_renderer.mm") elseif is_os("linux") then add_links("GL") - add_files("src/gui/platform/tray/linux_tray.cpp") + add_files("src/gui/platform/tray/linux_tray.cpp", + "src/gui/platform/opengl_video_renderer.cpp") end if is_os("windows") then