From 7cc81327a1ede2af48ed694e623d03005589de11 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Mon, 1 Jun 2026 02:29:15 +0200 Subject: [PATCH] UI/Qt: Present macOS frames with QRhiWidget Use QRhiWidget for the Qt web content view on macOS and render the current IOSurface-backed shared image into the widget's Metal render target. This keeps normal presentation on the GPU instead of painting a QImage wrapper over the shared bitmap. Force Qt Widgets' RHI backing store to Metal before QApplication is created so QRhiWidget can obtain the top-level QRhi even when the native window is created before the web content widget enters the hierarchy. Other platforms keep the existing QWidget and QPainter path. --- UI/Qt/CMakeLists.txt | 14 +- UI/Qt/WebContentView.cpp | 66 +++++++--- UI/Qt/WebContentView.h | 46 ++++++- UI/Qt/WebContentViewMac.mm | 260 +++++++++++++++++++++++++++++++++++++ UI/Qt/main.cpp | 9 ++ 5 files changed, 371 insertions(+), 24 deletions(-) create mode 100644 UI/Qt/WebContentViewMac.mm diff --git a/UI/Qt/CMakeLists.txt b/UI/Qt/CMakeLists.txt index 912a35d3bc..7b4cfb585c 100644 --- a/UI/Qt/CMakeLists.txt +++ b/UI/Qt/CMakeLists.txt @@ -3,6 +3,13 @@ set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) find_package(Qt6 REQUIRED COMPONENTS Core Widgets) +if (APPLE) + # The macOS QRhiWidget presentation path uses QRhi's private Metal native + # handle types to import IOSurfaces without CPU readback. + set(QT_NO_PRIVATE_MODULE_WARNING ON) + find_package(Qt6 REQUIRED COMPONENTS GuiPrivate) +endif() + qt_add_executable(ladybird main.cpp) target_sources(ladybird PRIVATE Application.cpp @@ -27,8 +34,11 @@ target_sources(ladybird PRIVATE target_link_libraries(ladybird PRIVATE Qt::Core Qt::Gui Qt::Widgets) if (APPLE) - target_sources(ladybird PRIVATE MacWindow.mm) - target_link_libraries(ladybird PRIVATE "-framework Cocoa" "-framework QuartzCore") + target_sources(ladybird PRIVATE + MacWindow.mm + WebContentViewMac.mm + ) + target_link_libraries(ladybird PRIVATE Qt::GuiPrivate "-framework Cocoa" "-framework IOSurface" "-framework Metal" "-framework QuartzCore") endif() create_ladybird_bundle(ladybird) diff --git a/UI/Qt/WebContentView.cpp b/UI/Qt/WebContentView.cpp index 384583fae7..185e35f9e6 100644 --- a/UI/Qt/WebContentView.cpp +++ b/UI/Qt/WebContentView.cpp @@ -53,8 +53,12 @@ namespace Ladybird { bool is_using_dark_system_theme(QWidget&); WebContentView::WebContentView(QWidget* window, RefPtr parent_client, size_t page_index, WebContentViewInitialState initial_state) - : QWidget(window) + : WebContentViewBase(window) { +#ifdef AK_OS_MACOS + setApi(QRhiWidget::Api::Metal); +#endif + m_client_state.client = parent_client; m_client_state.page_index = page_index; @@ -186,7 +190,12 @@ WebContentView::WebContentView(QWidget* window, RefPtr WebContentView::current_paintable() const +{ + Gfx::SharedImageBuffer const* shared_image_buffer = nullptr; + Gfx::IntSize bitmap_size; + + if (m_client_state.has_usable_bitmap) { + VERIFY(m_client_state.front_bitmap.shared_image_buffer); + shared_image_buffer = m_client_state.front_bitmap.shared_image_buffer.ptr(); + bitmap_size = m_client_state.front_bitmap.last_painted_size.to_type(); + } else if (m_backup_shared_image_buffer) { + shared_image_buffer = m_backup_shared_image_buffer.ptr(); + bitmap_size = m_backup_bitmap_size.to_type(); + } + + if (!shared_image_buffer) + return {}; + return Paintable { shared_image_buffer, bitmap_size }; +} + +#ifndef AK_OS_MACOS void WebContentView::paintEvent(QPaintEvent*) { QPainter painter(this); painter.scale(1 / m_device_pixel_ratio, 1 / m_device_pixel_ratio); + auto paintable = current_paintable(); Gfx::Bitmap const* bitmap = nullptr; Gfx::IntSize bitmap_size; - - if (m_client_state.has_usable_bitmap) { - VERIFY(m_client_state.front_bitmap.shared_image_buffer); - bitmap = m_client_state.front_bitmap.shared_image_buffer->bitmap().ptr(); - bitmap_size = m_client_state.front_bitmap.last_painted_size.to_type(); - } else if (m_backup_shared_image_buffer) { - bitmap = m_backup_shared_image_buffer->bitmap().ptr(); - bitmap_size = m_backup_bitmap_size.to_type(); + if (paintable.has_value()) { + bitmap = paintable->shared_image_buffer->bitmap().ptr(); + bitmap_size = paintable->bitmap_size; } if (bitmap) { @@ -652,10 +677,11 @@ void WebContentView::paintEvent(QPaintEvent*) auto background_color = page_background_color(); painter.fillRect(QRect(0, 0, m_viewport_size.width(), m_viewport_size.height()), QColor(background_color.red(), background_color.green(), background_color.blue())); } +#endif void WebContentView::resizeEvent(QResizeEvent* event) { - QWidget::resizeEvent(event); + WebContentViewBase::resizeEvent(event); update_viewport_size(); handle_resize(); } @@ -719,13 +745,13 @@ void WebContentView::update_zoom() void WebContentView::showEvent(QShowEvent* event) { - QWidget::showEvent(event); + WebContentViewBase::showEvent(event); set_system_visibility_state(Web::HTML::VisibilityState::Visible); } void WebContentView::hideEvent(QHideEvent* event) { - QWidget::hideEvent(event); + WebContentViewBase::hideEvent(event); set_system_visibility_state(Web::HTML::VisibilityState::Hidden); } @@ -926,7 +952,7 @@ bool WebContentView::event(QEvent* event) update_palette(); update(); }); - return QWidget::event(event); + return WebContentViewBase::event(event); } if (event->type() == QEvent::ShortcutOverride) { @@ -940,7 +966,7 @@ bool WebContentView::event(QEvent* event) return true; } - return QWidget::event(event); + return WebContentViewBase::event(event); } void WebContentView::enqueue_native_event(Web::MouseEvent::Type type, QSinglePointEvent const& event) @@ -1075,10 +1101,10 @@ void WebContentView::finish_handling_key_event(Web::KeyEvent const& key_event) switch (key_event.type) { case Web::KeyEvent::Type::KeyDown: - QWidget::keyPressEvent(&event); + WebContentViewBase::keyPressEvent(&event); break; case Web::KeyEvent::Type::KeyUp: - QWidget::keyReleaseEvent(&event); + WebContentViewBase::keyReleaseEvent(&event); break; } diff --git a/UI/Qt/WebContentView.h b/UI/Qt/WebContentView.h index 46fc4d47f7..dd9a4df20c 100644 --- a/UI/Qt/WebContentView.h +++ b/UI/Qt/WebContentView.h @@ -21,27 +21,40 @@ #include #include #include -#include + +#ifdef AK_OS_MACOS +# include +#else +# include +#endif class QKeyEvent; class QSinglePointEvent; namespace Ladybird { +#ifdef AK_OS_MACOS +using WebContentViewBase = QRhiWidget; +#else +using WebContentViewBase = QWidget; +#endif + struct WebContentViewInitialState { double maximum_frames_per_second { 60.0 }; Optional display_id; }; class WebContentView final - : public QWidget + : public WebContentViewBase , public WebView::ViewImplementation { Q_OBJECT public: WebContentView(QWidget* window, RefPtr parent_client = nullptr, size_t page_index = 0, WebContentViewInitialState initial_state = {}); virtual ~WebContentView() override; +#ifndef AK_OS_MACOS virtual void paintEvent(QPaintEvent*) override; +#endif virtual void resizeEvent(QResizeEvent*) override; virtual void leaveEvent(QEvent* event) override; virtual void mouseMoveEvent(QMouseEvent*) override; @@ -93,6 +106,20 @@ private: virtual Gfx::IntPoint to_content_position(Gfx::IntPoint widget_position) const override; virtual Gfx::IntPoint to_widget_position(Gfx::IntPoint content_position) const override; +#ifdef AK_OS_MACOS + // ^QRhiWidget + virtual void initialize(QRhiCommandBuffer*) override; + virtual void render(QRhiCommandBuffer*) override; + virtual void releaseResources() override; +#endif + + struct Paintable { + Gfx::SharedImageBuffer const* shared_image_buffer { nullptr }; + Gfx::IntSize bitmap_size; + }; + + Optional current_paintable() const; + void update_viewport_size(); void update_cursor(Gfx::Cursor cursor); void update_compositor_display_metadata(); @@ -120,6 +147,21 @@ private: int m_click_count { 0 }; QMenu* m_select_dropdown { nullptr }; + +#ifdef AK_OS_MACOS + bool prepare_metal_renderer(unsigned long render_target_pixel_format); + bool update_imported_iosurface_texture(Gfx::SharedImageBuffer const&); + void release_metal_resources(); + void release_imported_iosurface_texture(); + + void* m_metal_device { nullptr }; + void* m_metal_library { nullptr }; + void* m_metal_pipeline_state { nullptr }; + void* m_metal_sampler_state { nullptr }; + void* m_imported_iosurface_texture { nullptr }; + Gfx::SharedImageBuffer const* m_imported_shared_image_buffer { nullptr }; + unsigned long m_render_target_pixel_format { 0 }; +#endif }; } diff --git a/UI/Qt/WebContentViewMac.mm b/UI/Qt/WebContentViewMac.mm new file mode 100644 index 0000000000..fd6e7dbf8d --- /dev/null +++ b/UI/Qt/WebContentViewMac.mm @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +#include +#include +#include + +#import +#import + +namespace Ladybird { + +static void release_metal_object(void*& object) +{ + if (!object) + return; + + [(id)object release]; + object = nullptr; +} + +void WebContentView::release_imported_iosurface_texture() +{ + release_metal_object(m_imported_iosurface_texture); + m_imported_shared_image_buffer = nullptr; +} + +void WebContentView::release_metal_resources() +{ + release_imported_iosurface_texture(); + release_metal_object(m_metal_sampler_state); + release_metal_object(m_metal_pipeline_state); + release_metal_object(m_metal_library); + m_metal_device = nullptr; + m_render_target_pixel_format = 0; +} + +bool WebContentView::prepare_metal_renderer(unsigned long render_target_pixel_format) +{ + auto const* rhi_native_handles = static_cast(rhi()->nativeHandles()); + if (!rhi_native_handles || !rhi_native_handles->dev) + return false; + + auto* device = (id)rhi_native_handles->dev; + if (m_metal_device != device || m_render_target_pixel_format != render_target_pixel_format) { + release_metal_resources(); + m_metal_device = device; + m_render_target_pixel_format = render_target_pixel_format; + } + + if (m_metal_pipeline_state && m_metal_sampler_state) + return true; + + // NB: QRhiWidget gives us a Metal render target for the widget, but QRhi does + // not expose a way to wrap the IOSurface-backed texture from WebContent as a + // QRhiTexture. Importing the IOSurface with Metal and drawing a textured quad + // lets us present it without doing a CPU readback through QImage/QPainter. + // + // The shader deliberately keeps the geometry tiny: it maps the painted content + // rectangle into the QRhiWidget render target and samples the corresponding + // sub-rectangle from the IOSurface. The IOSurface is imported as BGRA below; + // Metal texture sampling exposes those pixels to the shader as logical RGBA + // components, so the fragment shader does not need an explicit swizzle. + auto const* shader_source = R"( +#include +using namespace metal; + +struct Uniforms { + float2 target_size; + float2 content_size; + float2 source_size; +}; + +struct VertexOut { + float4 position [[position]]; + float2 texture_coordinate; +}; + +vertex VertexOut vertex_main(uint vertex_id [[vertex_id]], constant Uniforms& uniforms [[buffer(0)]]) +{ + float2 unit_positions[4] = { + float2(0.0, 0.0), + float2(1.0, 0.0), + float2(0.0, 1.0), + float2(1.0, 1.0), + }; + + float2 unit_position = unit_positions[vertex_id]; + float2 pixel_position = unit_position * uniforms.content_size; + + VertexOut out; + out.position = float4( + pixel_position.x / uniforms.target_size.x * 2.0 - 1.0, + 1.0 - pixel_position.y / uniforms.target_size.y * 2.0, + 0.0, + 1.0); + out.texture_coordinate = pixel_position / uniforms.source_size; + return out; +} + +fragment half4 fragment_main(VertexOut in [[stage_in]], texture2d texture [[texture(0)]], sampler texture_sampler [[sampler(0)]]) +{ + return texture.sample(texture_sampler, in.texture_coordinate); +} +)"; + + NSError* error = nil; + auto* library = [device newLibraryWithSource:[NSString stringWithUTF8String:shader_source] options:nil error:&error]; + if (!library) { + dbgln("Failed to create Metal shader library for Qt WebContentView"); + return false; + } + m_metal_library = library; + + auto* vertex_function = [library newFunctionWithName:@"vertex_main"]; + auto* fragment_function = [library newFunctionWithName:@"fragment_main"]; + if (!vertex_function || !fragment_function) { + [vertex_function release]; + [fragment_function release]; + dbgln("Failed to create Metal shader functions for Qt WebContentView"); + return false; + } + + auto* pipeline_descriptor = [[MTLRenderPipelineDescriptor alloc] init]; + pipeline_descriptor.vertexFunction = vertex_function; + pipeline_descriptor.fragmentFunction = fragment_function; + pipeline_descriptor.colorAttachments[0].pixelFormat = static_cast(render_target_pixel_format); + + auto* pipeline_state = [device newRenderPipelineStateWithDescriptor:pipeline_descriptor error:&error]; + [pipeline_descriptor release]; + [vertex_function release]; + [fragment_function release]; + + if (!pipeline_state) { + dbgln("Failed to create Metal render pipeline for Qt WebContentView"); + return false; + } + m_metal_pipeline_state = pipeline_state; + + auto* sampler_descriptor = [[MTLSamplerDescriptor alloc] init]; + sampler_descriptor.minFilter = MTLSamplerMinMagFilterNearest; + sampler_descriptor.magFilter = MTLSamplerMinMagFilterNearest; + sampler_descriptor.sAddressMode = MTLSamplerAddressModeClampToEdge; + sampler_descriptor.tAddressMode = MTLSamplerAddressModeClampToEdge; + m_metal_sampler_state = [device newSamplerStateWithDescriptor:sampler_descriptor]; + [sampler_descriptor release]; + + if (!m_metal_sampler_state) { + dbgln("Failed to create Metal sampler for Qt WebContentView"); + return false; + } + + return true; +} + +bool WebContentView::update_imported_iosurface_texture(Gfx::SharedImageBuffer const& shared_image_buffer) +{ + if (m_imported_shared_image_buffer == &shared_image_buffer && m_imported_iosurface_texture) + return true; + + release_imported_iosurface_texture(); + + auto* device = (id)m_metal_device; + if (!device) + return false; + + auto const& iosurface_handle = shared_image_buffer.iosurface_handle(); + // LibGfx IOSurfaces are BGRA in memory. Match that storage format when + // importing the IOSurface so Metal can do the channel mapping while sampling. + auto* descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm + width:iosurface_handle.width() + height:iosurface_handle.height() + mipmapped:NO]; + descriptor.storageMode = MTLStorageModeShared; + descriptor.usage = MTLTextureUsageShaderRead; + + m_imported_iosurface_texture = [device newTextureWithDescriptor:descriptor + iosurface:(IOSurfaceRef)iosurface_handle.core_foundation_pointer() + plane:0]; + if (!m_imported_iosurface_texture) + return false; + + m_imported_shared_image_buffer = &shared_image_buffer; + return true; +} + +void WebContentView::initialize(QRhiCommandBuffer*) +{ + release_metal_resources(); +} + +void WebContentView::releaseResources() +{ + release_metal_resources(); +} + +void WebContentView::render(QRhiCommandBuffer* command_buffer) +{ + auto background_color = page_background_color(); + auto clear_color = QColor(background_color.red(), background_color.green(), background_color.blue()); + + command_buffer->beginPass(renderTarget(), clear_color, { 1.0f, 0 }, nullptr, QRhiCommandBuffer::ExternalContent); + + auto paintable = current_paintable(); + if (!paintable.has_value() || paintable->bitmap_size.is_empty() || !rhi() || rhi()->backend() != QRhi::Metal) { + command_buffer->endPass(); + return; + } + + auto native_color_texture = colorTexture()->nativeTexture(); + auto* render_target_texture = (id)native_color_texture.object; + if (!render_target_texture || !prepare_metal_renderer(render_target_texture.pixelFormat) || !update_imported_iosurface_texture(*paintable->shared_image_buffer)) { + command_buffer->endPass(); + return; + } + + auto target_size = colorTexture()->pixelSize(); + auto content_width = min(paintable->bitmap_size.width(), target_size.width()); + auto content_height = min(paintable->bitmap_size.height(), target_size.height()); + if (content_width <= 0 || content_height <= 0) { + command_buffer->endPass(); + return; + } + + struct Uniforms { + float target_size[2]; + float content_size[2]; + float source_size[2]; + } uniforms { + { static_cast(target_size.width()), static_cast(target_size.height()) }, + { static_cast(content_width), static_cast(content_height) }, + { static_cast(paintable->shared_image_buffer->iosurface_handle().width()), static_cast(paintable->shared_image_buffer->iosurface_handle().height()) }, + }; + + // The pass was opened with ExternalContent so we can encode the Metal draw + // directly into Qt's command buffer. + command_buffer->beginExternal(); + auto const* command_buffer_native_handles = static_cast(command_buffer->nativeHandles()); + if (command_buffer_native_handles && command_buffer_native_handles->encoder) { + auto* encoder = command_buffer_native_handles->encoder; + [encoder setRenderPipelineState:(id)m_metal_pipeline_state]; + [encoder setVertexBytes:&uniforms length:sizeof(uniforms) atIndex:0]; + [encoder setFragmentTexture:(id)m_imported_iosurface_texture atIndex:0]; + [encoder setFragmentSamplerState:(id)m_metal_sampler_state atIndex:0]; + [encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; + } + command_buffer->endExternal(); + + command_buffer->endPass(); +} + +} diff --git a/UI/Qt/main.cpp b/UI/Qt/main.cpp index b585f7f70f..9c3e01d0b4 100644 --- a/UI/Qt/main.cpp +++ b/UI/Qt/main.cpp @@ -13,6 +13,8 @@ #include #include +#include + #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) # include #endif @@ -43,6 +45,13 @@ ErrorOr ladybird_main(Main::Arguments arguments) { AK::set_rich_debug_enabled(true); +#ifdef AK_OS_MACOS + if (!qEnvironmentVariableIsSet("QT_WIDGETS_RHI")) + qputenv("QT_WIDGETS_RHI", "1"); + if (!qEnvironmentVariableIsSet("QT_WIDGETS_RHI_BACKEND")) + qputenv("QT_WIDGETS_RHI_BACKEND", "metal"); +#endif + auto app = TRY(Ladybird::Application::create(arguments)); WebView::BrowserProcess browser_process;