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.
260 lines
9.7 KiB
Text
260 lines
9.7 KiB
Text
/*
|
|
* Copyright (c) 2026-present, the Ladybird developers.
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/Debug.h>
|
|
#include <AK/Math.h>
|
|
#include <LibGfx/SharedImageBuffer.h>
|
|
#include <UI/Qt/WebContentView.h>
|
|
|
|
#include <QColor>
|
|
#include <rhi/qrhi.h>
|
|
#include <rhi/qrhi_platform.h>
|
|
|
|
#import <IOSurface/IOSurface.h>
|
|
#import <Metal/Metal.h>
|
|
|
|
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<QRhiMetalNativeHandles const*>(rhi()->nativeHandles());
|
|
if (!rhi_native_handles || !rhi_native_handles->dev)
|
|
return false;
|
|
|
|
auto* device = (id<MTLDevice>)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 <metal_stdlib>
|
|
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<half> 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<MTLPixelFormat>(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<MTLDevice>)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<MTLTexture>)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<float>(target_size.width()), static_cast<float>(target_size.height()) },
|
|
{ static_cast<float>(content_width), static_cast<float>(content_height) },
|
|
{ static_cast<float>(paintable->shared_image_buffer->iosurface_handle().width()), static_cast<float>(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<QRhiMetalCommandBufferNativeHandles const*>(command_buffer->nativeHandles());
|
|
if (command_buffer_native_handles && command_buffer_native_handles->encoder) {
|
|
auto* encoder = command_buffer_native_handles->encoder;
|
|
[encoder setRenderPipelineState:(id<MTLRenderPipelineState>)m_metal_pipeline_state];
|
|
[encoder setVertexBytes:&uniforms length:sizeof(uniforms) atIndex:0];
|
|
[encoder setFragmentTexture:(id<MTLTexture>)m_imported_iosurface_texture atIndex:0];
|
|
[encoder setFragmentSamplerState:(id<MTLSamplerState>)m_metal_sampler_state atIndex:0];
|
|
[encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
|
|
}
|
|
command_buffer->endExternal();
|
|
|
|
command_buffer->endPass();
|
|
}
|
|
|
|
}
|