LibWebView+UI/AppKit: Present IOSurfaces directly

Instead of copying the Bitmap that wraps the IOSurface, we can just
present the IOSurface directly. This significantly reduces CPU usage in
the UI process, particularly at high refresh rates such as 120Hz where
it would saturate a full CPU core.

This is done by using CAMetalLayer and blitting the IOSurface to the
next drawable buffer, which handles triple buffering, locking the
IOSurface and vsync automatically. This also allows the Metal HUD to
work, but the only accurate stat is the frame intervals/FPS because
it's in the UI layer, not WebContent. However, that's still useful
to detect frame drops.
This commit is contained in:
Luke Wilde 2026-02-06 15:02:27 +00:00 committed by Luke Wilde
parent aeec2c804c
commit 0e958ca8d5
6 changed files with 113 additions and 22 deletions

View file

@ -582,13 +582,18 @@ void ViewImplementation::did_allocate_iosurface_backing_stores(i32 front_id, Cor
auto bytes_per_row = front_iosurface.bytes_per_row();
auto* front_ref = front_iosurface.core_foundation_pointer();
auto* back_ref = back_iosurface.core_foundation_pointer();
auto front_bitmap = Gfx::Bitmap::create_wrapper(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, front_size, bytes_per_row, front_iosurface.data(), [handle = move(front_iosurface)] { });
auto back_bitmap = Gfx::Bitmap::create_wrapper(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, back_size, bytes_per_row, back_iosurface.data(), [handle = move(back_iosurface)] { });
m_client_state.front_bitmap.bitmap = front_bitmap.release_value_but_fixme_should_propagate_errors();
m_client_state.front_bitmap.id = front_id;
m_client_state.front_bitmap.iosurface_ref = front_ref;
m_client_state.back_bitmap.bitmap = back_bitmap.release_value_but_fixme_should_propagate_errors();
m_client_state.back_bitmap.id = back_id;
m_client_state.back_bitmap.iosurface_ref = back_ref;
}
#endif

View file

@ -300,6 +300,9 @@ protected:
i32 id { -1 };
Web::DevicePixelSize last_painted_size;
RefPtr<Gfx::Bitmap const> bitmap;
#ifdef AK_OS_MACOS
void* iosurface_ref { nullptr };
#endif
};
struct ClientState {

View file

@ -26,7 +26,7 @@ target_compile_features(ladybird_impl PUBLIC cxx_std_23)
add_executable(ladybird MACOSX_BUNDLE
main.mm
)
target_link_libraries(ladybird_impl PUBLIC "-framework Cocoa -framework UniformTypeIdentifiers" LibUnicode)
target_link_libraries(ladybird_impl PUBLIC "-framework Cocoa -framework Metal -framework QuartzCore -framework UniformTypeIdentifiers" LibUnicode)
target_link_libraries(ladybird PRIVATE ladybird_impl)
create_ladybird_bundle(ladybird)

View file

@ -13,6 +13,8 @@
#import <Interface/Event.h>
#import <Interface/LadybirdWebView.h>
#import <Interface/Menu.h>
#import <Metal/Metal.h>
#import <QuartzCore/CAMetalLayer.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#import <Utilities/Conversions.h>
@ -41,6 +43,9 @@ struct HideCursor {
Optional<HideCursor> m_hidden_cursor;
id<MTLDevice> m_metal_device;
id<MTLCommandQueue> m_metal_queue;
// We have to send key events for modifer keys, but AppKit does not generate key down/up events when only a modifier
// key is pressed. Instead, we only receive an event that the modifier flags have changed, and we must determine for
// ourselves whether the modifier key was pressed or released.
@ -96,6 +101,9 @@ struct HideCursor {
if (self = [super init]) {
self.observer = observer;
m_metal_device = MTLCreateSystemDefaultDevice();
m_metal_queue = [m_metal_device newCommandQueue];
auto* screens = [NSScreen screens];
Vector<Web::DevicePixelRect> screen_rects;
@ -246,7 +254,10 @@ struct HideCursor {
if (self == nil) {
return;
}
[self setNeedsDisplay:YES];
if (m_metal_device)
[self presentMetalFrame];
else
[self setNeedsDisplay:YES];
};
m_web_view_bridge->on_new_web_view = [weak_self](auto activate_tab, auto, auto page_index) {
@ -909,32 +920,100 @@ struct HideCursor {
#pragma mark - NSView
- (void)drawRect:(NSRect)rect
- (CALayer*)makeBackingLayer
{
CAMetalLayer* layer = [CAMetalLayer layer];
layer.device = m_metal_device;
layer.pixelFormat = MTLPixelFormatBGRA8Unorm;
layer.framebufferOnly = YES;
layer.displaySyncEnabled = YES;
layer.contentsGravity = kCAGravityTopLeft;
return layer;
}
- (BOOL)wantsUpdateLayer
{
return YES;
}
- (void)presentMetalFrame
{
auto paintable = m_web_view_bridge->paintable();
if (!paintable.has_value())
return;
auto [bitmap, bitmap_size, iosurface_ref] = *paintable;
if (!iosurface_ref)
return;
CAMetalLayer* metal_layer = (CAMetalLayer*)self.layer;
metal_layer.drawableSize = CGSizeMake(bitmap_size.width(), bitmap_size.height());
metal_layer.contentsScale = m_web_view_bridge->device_pixel_ratio();
id<CAMetalDrawable> drawable = [metal_layer nextDrawable];
if (!drawable)
return;
MTLTextureDescriptor* desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm
width:bitmap.width()
height:bitmap.height()
mipmapped:NO];
desc.storageMode = MTLStorageModeShared;
desc.usage = MTLTextureUsageShaderRead;
id<MTLTexture> src_texture = [m_metal_device newTextureWithDescriptor:desc
iosurface:(IOSurfaceRef)iosurface_ref
plane:0];
id<MTLCommandBuffer> cmd_buf = [m_metal_queue commandBuffer];
id<MTLBlitCommandEncoder> blit = [cmd_buf blitCommandEncoder];
[blit copyFromTexture:src_texture
sourceSlice:0
sourceLevel:0
sourceOrigin:MTLOriginMake(0, 0, 0)
sourceSize:MTLSizeMake(bitmap_size.width(), bitmap_size.height(), 1)
toTexture:drawable.texture
destinationSlice:0
destinationLevel:0
destinationOrigin:MTLOriginMake(0, 0, 0)];
[blit endEncoding];
[cmd_buf presentDrawable:drawable];
[cmd_buf commit];
}
- (void)viewWillStartLiveResize
{
[super viewWillStartLiveResize];
self.layerContentsPlacement = NSViewLayerContentsPlacementTopLeft;
}
- (void)viewDidEndLiveResize
{
[super viewDidEndLiveResize];
self.layerContentsPlacement = NSViewLayerContentsPlacementScaleAxesIndependently;
}
- (void)updateLayer
{
// Metal path is driven directly by on_ready_to_paint via presentMetalFrame.
if (m_metal_device)
return;
// Fallback for non-IOSurface path (e.g. Intel Macs)
auto paintable = m_web_view_bridge->paintable();
if (!paintable.has_value()) {
[super drawRect:rect];
self.layer.contents = nil;
return;
}
auto [bitmap, bitmap_size] = *paintable;
auto [bitmap, bitmap_size, iosurface_ref] = *paintable;
VERIFY(bitmap.format() == Gfx::BitmapFormat::BGRA8888);
static constexpr size_t BITS_PER_COMPONENT = 8;
static constexpr size_t BITS_PER_PIXEL = 32;
auto* context = [[NSGraphicsContext currentContext] CGContext];
CGContextSaveGState(context);
auto device_pixel_ratio = m_web_view_bridge->device_pixel_ratio();
auto inverse_device_pixel_ratio = m_web_view_bridge->inverse_device_pixel_ratio();
CGContextScaleCTM(context, inverse_device_pixel_ratio, inverse_device_pixel_ratio);
static auto color_space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
auto* provider = CGDataProviderCreateWithData(nil, bitmap.scanline_u8(0), bitmap.size_in_bytes(), nil);
auto image_rect = CGRectMake(rect.origin.x * device_pixel_ratio, rect.origin.y * device_pixel_ratio, bitmap_size.width(), bitmap_size.height());
static auto color_space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
// Ideally, this would be NSBitmapImageRep, but the equivalent factory initWithBitmapDataPlanes: does
// not seem to actually respect endianness. We need NSBitmapFormatThirtyTwoBitLittleEndian, but the
@ -952,14 +1031,15 @@ struct HideCursor {
NO,
kCGRenderingIntentDefault);
auto* image = [[NSImage alloc] initWithCGImage:bitmap_image size:NSZeroSize];
[image drawInRect:image_rect];
self.layer.contentsScale = m_web_view_bridge->device_pixel_ratio();
self.layer.contentsGravity = kCAGravityTopLeft;
self.layer.contentsRect = CGRectMake(0, 0,
(CGFloat)bitmap_size.width() / bitmap.width(),
(CGFloat)bitmap_size.height() / bitmap.height());
self.layer.contents = (__bridge id)bitmap_image;
CGContextRestoreGState(context);
CGDataProviderRelease(provider);
CGImageRelease(bitmap_image);
[super drawRect:rect];
}
- (void)viewDidMoveToWindow

View file

@ -94,10 +94,12 @@ Optional<WebViewBridge::Paintable> WebViewBridge::paintable()
{
Gfx::Bitmap const* bitmap = nullptr;
Gfx::IntSize bitmap_size;
void* iosurface_ref = nullptr;
if (m_client_state.has_usable_bitmap) {
bitmap = m_client_state.front_bitmap.bitmap.ptr();
bitmap_size = m_client_state.front_bitmap.last_painted_size.to_type<int>();
iosurface_ref = m_client_state.front_bitmap.iosurface_ref;
} else {
bitmap = m_backup_bitmap.ptr();
bitmap_size = m_backup_bitmap_size.to_type<int>();
@ -105,7 +107,7 @@ Optional<WebViewBridge::Paintable> WebViewBridge::paintable()
if (!bitmap)
return {};
return Paintable { *bitmap, bitmap_size };
return Paintable { *bitmap, bitmap_size, iosurface_ref };
}
void WebViewBridge::update_zoom()

View file

@ -41,6 +41,7 @@ public:
struct Paintable {
Gfx::Bitmap const& bitmap;
Gfx::IntSize bitmap_size;
void* iosurface_ref { nullptr };
};
Optional<Paintable> paintable();