feat(servo): isolate profiles with hardware sidecars
This commit is contained in:
+72
-1
@@ -5,6 +5,33 @@ use crate::{
|
||||
#[cfg(target_os = "macos")]
|
||||
use core_video::pixel_buffer::CVPixelBuffer;
|
||||
use refineable::Refineable;
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
/// An owner retained until the GPU finishes reading a surface frame.
|
||||
///
|
||||
/// Attach a lease with [`Surface::lease`] when the surface's producer may
|
||||
/// recycle its backing storage independently of the CoreVideo pixel buffer.
|
||||
#[derive(Clone)]
|
||||
pub struct SurfaceLease(Arc<dyn Send + Sync>);
|
||||
|
||||
impl SurfaceLease {
|
||||
/// Create a lease from a thread-safe shared owner.
|
||||
pub fn from_arc<T>(owner: Arc<T>) -> Self
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
Self(owner)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for SurfaceLease {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("SurfaceLease")
|
||||
.field("strong_count", &Arc::strong_count(&self.0))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A source of a surface's content.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -24,6 +51,7 @@ impl From<CVPixelBuffer> for SurfaceSource {
|
||||
/// A surface element.
|
||||
pub struct Surface {
|
||||
source: SurfaceSource,
|
||||
lease: Option<SurfaceLease>,
|
||||
object_fit: ObjectFit,
|
||||
corner_radii: Option<Corners<Pixels>>,
|
||||
style: StyleRefinement,
|
||||
@@ -33,6 +61,7 @@ pub struct Surface {
|
||||
pub fn surface(source: impl Into<SurfaceSource>) -> Surface {
|
||||
Surface {
|
||||
source: source.into(),
|
||||
lease: None,
|
||||
object_fit: ObjectFit::Contain,
|
||||
corner_radii: None,
|
||||
style: Default::default(),
|
||||
@@ -40,6 +69,12 @@ pub fn surface(source: impl Into<SurfaceSource>) -> Surface {
|
||||
}
|
||||
|
||||
impl Surface {
|
||||
/// Retain an owner until the GPU completes this surface frame.
|
||||
pub fn lease(mut self, lease: SurfaceLease) -> Self {
|
||||
self.lease = Some(lease);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the object fit for the image.
|
||||
pub fn object_fit(mut self, object_fit: ObjectFit) -> Self {
|
||||
self.object_fit = object_fit;
|
||||
@@ -110,7 +145,12 @@ impl Element for Surface {
|
||||
.corner_radii
|
||||
.unwrap_or_else(|| style.corner_radii.to_pixels(window.rem_size()))
|
||||
.clamp_radii_for_quad_size(new_bounds.size);
|
||||
window.paint_surface(new_bounds, corner_radii, surface.clone());
|
||||
window.paint_surface_with_lease(
|
||||
new_bounds,
|
||||
corner_radii,
|
||||
surface.clone(),
|
||||
self.lease.clone(),
|
||||
);
|
||||
}
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => {}
|
||||
@@ -131,3 +171,34 @@ impl Styled for Surface {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
use super::SurfaceLease;
|
||||
|
||||
struct DropProbe(Arc<AtomicUsize>);
|
||||
|
||||
impl Drop for DropProbe {
|
||||
fn drop(&mut self) {
|
||||
self.0.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surface_lease_retains_its_owner() {
|
||||
let drops = Arc::new(AtomicUsize::new(0));
|
||||
let owner = Arc::new(DropProbe(drops.clone()));
|
||||
let lease = SurfaceLease::from_arc(owner.clone());
|
||||
|
||||
drop(owner);
|
||||
assert_eq!(drops.load(Ordering::SeqCst), 0);
|
||||
|
||||
drop(lease);
|
||||
assert_eq!(drops.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
}
|
||||
|
||||
+47
-15
@@ -384,19 +384,17 @@ impl MetalRenderer {
|
||||
let mut instance_buffer = self.instance_buffer_pool.lock().acquire(&self.device);
|
||||
|
||||
let command_buffer =
|
||||
self.draw_primitives(scene, &mut instance_buffer, drawable, viewport_size);
|
||||
self.draw_primitives(scene, &mut instance_buffer, drawable.texture(), viewport_size);
|
||||
|
||||
match command_buffer {
|
||||
Ok(command_buffer) => {
|
||||
let instance_buffer_pool = self.instance_buffer_pool.clone();
|
||||
let instance_buffer = Cell::new(Some(instance_buffer));
|
||||
let block = ConcreteBlock::new(move |_| {
|
||||
if let Some(instance_buffer) = instance_buffer.take() {
|
||||
instance_buffer_pool.lock().release(instance_buffer);
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
command_buffer.add_completed_handler(&block);
|
||||
retain_frame_resources_until_completion(
|
||||
command_buffer.as_ref(),
|
||||
scene,
|
||||
instance_buffer,
|
||||
self.instance_buffer_pool.clone(),
|
||||
None,
|
||||
);
|
||||
|
||||
if self.presents_with_transaction {
|
||||
command_buffer.commit();
|
||||
@@ -433,7 +431,7 @@ impl MetalRenderer {
|
||||
&mut self,
|
||||
scene: &Scene,
|
||||
instance_buffer: &mut InstanceBuffer,
|
||||
drawable: &metal::MetalDrawableRef,
|
||||
target_texture: &metal::TextureRef,
|
||||
viewport_size: Size<DevicePixels>,
|
||||
) -> Result<metal::CommandBuffer> {
|
||||
let command_queue = self.command_queue.clone();
|
||||
@@ -443,7 +441,7 @@ impl MetalRenderer {
|
||||
|
||||
let mut command_encoder = new_command_encoder(
|
||||
command_buffer,
|
||||
drawable,
|
||||
target_texture,
|
||||
viewport_size,
|
||||
|color_attachment| {
|
||||
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
|
||||
@@ -480,7 +478,7 @@ impl MetalRenderer {
|
||||
|
||||
command_encoder = new_command_encoder(
|
||||
command_buffer,
|
||||
drawable,
|
||||
target_texture,
|
||||
viewport_size,
|
||||
|color_attachment| {
|
||||
color_attachment.set_load_action(metal::MTLLoadAction::Load);
|
||||
@@ -1197,9 +1195,36 @@ impl MetalRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
fn retain_frame_resources_until_completion(
|
||||
command_buffer: &metal::CommandBufferRef,
|
||||
scene: &Scene,
|
||||
instance_buffer: InstanceBuffer,
|
||||
instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>,
|
||||
completion_tx: Option<std::sync::mpsc::Sender<()>>,
|
||||
) {
|
||||
let instance_buffer = Cell::new(Some(instance_buffer));
|
||||
let surface_leases = Cell::new(Some(
|
||||
scene
|
||||
.surfaces
|
||||
.iter()
|
||||
.filter_map(|surface| surface.lease.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
));
|
||||
let block = ConcreteBlock::new(move |_| {
|
||||
let _ = surface_leases.take();
|
||||
if let Some(instance_buffer) = instance_buffer.take() {
|
||||
instance_buffer_pool.lock().release(instance_buffer);
|
||||
}
|
||||
if let Some(completion_tx) = completion_tx.as_ref() {
|
||||
let _ = completion_tx.send(());
|
||||
}
|
||||
});
|
||||
command_buffer.add_completed_handler(&block.copy());
|
||||
}
|
||||
|
||||
fn new_command_encoder<'a>(
|
||||
command_buffer: &'a metal::CommandBufferRef,
|
||||
drawable: &'a metal::MetalDrawableRef,
|
||||
target_texture: &'a metal::TextureRef,
|
||||
viewport_size: Size<DevicePixels>,
|
||||
configure_color_attachment: impl Fn(&RenderPassColorAttachmentDescriptorRef),
|
||||
) -> &'a metal::RenderCommandEncoderRef {
|
||||
@@ -1208,7 +1233,7 @@ fn new_command_encoder<'a>(
|
||||
.color_attachments()
|
||||
.object_at(0)
|
||||
.unwrap();
|
||||
color_attachment.set_texture(Some(drawable.texture()));
|
||||
color_attachment.set_texture(Some(target_texture));
|
||||
color_attachment.set_store_action(metal::MTLStoreAction::Store);
|
||||
configure_color_attachment(color_attachment);
|
||||
|
||||
@@ -1224,6 +1249,13 @@ fn new_command_encoder<'a>(
|
||||
command_encoder
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
#[path = "metal_renderer_test_support.rs"]
|
||||
mod test_support;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub use test_support::{MetalSurfaceTestSubmission, submit_surface_to_metal_for_test};
|
||||
|
||||
fn build_pipeline_state(
|
||||
device: &metal::DeviceRef,
|
||||
library: &metal::LibraryRef,
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
use std::{
|
||||
ffi::CStr,
|
||||
os::raw::c_char,
|
||||
sync::{Arc, mpsc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use core_video::pixel_buffer::CVPixelBuffer;
|
||||
use metal::{MTLCommandBufferStatus, MTLPixelFormat, MTLTextureUsage, TextureDescriptor};
|
||||
use parking_lot::Mutex;
|
||||
use objc::{msg_send, runtime::Object, sel, sel_impl};
|
||||
|
||||
use super::{
|
||||
InstanceBufferPool, MetalRenderer, retain_frame_resources_until_completion,
|
||||
};
|
||||
use crate::{
|
||||
Bounds, ContentMask, Corners, DevicePixels, PaintSurface, ScaledPixels, Scene, SurfaceLease,
|
||||
point, size,
|
||||
};
|
||||
|
||||
const COMPLETION_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const COMPLETION_GATE_VALUE: u64 = 1;
|
||||
|
||||
/// A gated offscreen Metal submission that uses GPUI's production surface pipeline.
|
||||
pub struct MetalSurfaceTestSubmission {
|
||||
gate: metal::SharedEvent,
|
||||
command_buffer: metal::CommandBuffer,
|
||||
completion_rx: mpsc::Receiver<()>,
|
||||
_target_texture: metal::Texture,
|
||||
finished: bool,
|
||||
}
|
||||
|
||||
impl MetalSurfaceTestSubmission {
|
||||
/// Releases the GPU gate and waits for the production completion handler.
|
||||
pub fn finish(mut self) -> crate::Result<()> {
|
||||
self.gate.set_signaled_value(COMPLETION_GATE_VALUE);
|
||||
self.command_buffer.wait_until_completed();
|
||||
self.completion_rx.recv_timeout(COMPLETION_TIMEOUT)?;
|
||||
anyhow::ensure!(
|
||||
self.command_buffer.status() == MTLCommandBufferStatus::Completed,
|
||||
"Metal surface test submission ended with status {:?}: {}",
|
||||
self.command_buffer.status(),
|
||||
command_buffer_error(self.command_buffer.as_ref()),
|
||||
);
|
||||
self.finished = true;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MetalSurfaceTestSubmission {
|
||||
fn drop(&mut self) {
|
||||
if !self.finished {
|
||||
self.gate.set_signaled_value(COMPLETION_GATE_VALUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Submits a CoreVideo surface through GPUI's production BGRA Metal pipeline.
|
||||
pub fn submit_surface_to_metal_for_test(
|
||||
image_buffer: CVPixelBuffer,
|
||||
lease: SurfaceLease,
|
||||
) -> crate::Result<MetalSurfaceTestSubmission> {
|
||||
let width = u32::try_from(image_buffer.get_width())?;
|
||||
let height = u32::try_from(image_buffer.get_height())?;
|
||||
anyhow::ensure!(width > 0 && height > 0, "surface dimensions must be positive");
|
||||
let viewport_size = size(
|
||||
DevicePixels(i32::try_from(width)?),
|
||||
DevicePixels(i32::try_from(height)?),
|
||||
);
|
||||
let instance_buffer_pool = Arc::new(Mutex::new(InstanceBufferPool::default()));
|
||||
let mut renderer = MetalRenderer::new(instance_buffer_pool.clone());
|
||||
let target_texture = build_target_texture(&renderer.device, width, height);
|
||||
let mut scene = build_surface_scene(image_buffer, lease, width, height);
|
||||
scene.finish();
|
||||
let mut instance_buffer = instance_buffer_pool.lock().acquire(&renderer.device);
|
||||
let command_buffer = renderer.draw_primitives(
|
||||
&scene,
|
||||
&mut instance_buffer,
|
||||
target_texture.as_ref(),
|
||||
viewport_size,
|
||||
)?;
|
||||
let gate = renderer.device.new_shared_event();
|
||||
gate.set_signaled_value(0);
|
||||
command_buffer.encode_wait_for_event(&gate, COMPLETION_GATE_VALUE);
|
||||
let (completion_tx, completion_rx) = mpsc::channel();
|
||||
retain_frame_resources_until_completion(
|
||||
command_buffer.as_ref(),
|
||||
&scene,
|
||||
instance_buffer,
|
||||
instance_buffer_pool,
|
||||
Some(completion_tx),
|
||||
);
|
||||
command_buffer.commit();
|
||||
anyhow::ensure!(
|
||||
!matches!(
|
||||
command_buffer.status(),
|
||||
MTLCommandBufferStatus::Completed | MTLCommandBufferStatus::Error
|
||||
),
|
||||
"gated Metal surface submission ended early with status {:?}: {error}",
|
||||
command_buffer.status(),
|
||||
error = command_buffer_error(command_buffer.as_ref()),
|
||||
);
|
||||
|
||||
Ok(MetalSurfaceTestSubmission {
|
||||
gate,
|
||||
command_buffer,
|
||||
completion_rx,
|
||||
_target_texture: target_texture,
|
||||
finished: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn command_buffer_error(command_buffer: &metal::CommandBufferRef) -> String {
|
||||
#[expect(unsafe_code)]
|
||||
unsafe {
|
||||
let error: *mut Object = msg_send![command_buffer, error];
|
||||
if error.is_null() {
|
||||
return "no Metal error description".to_string();
|
||||
}
|
||||
let description: *mut Object = msg_send![error, localizedDescription];
|
||||
if description.is_null() {
|
||||
return "Metal error description was null".to_string();
|
||||
}
|
||||
let utf8: *const c_char = msg_send![description, UTF8String];
|
||||
if utf8.is_null() {
|
||||
return "Metal error description had no UTF-8 representation".to_string();
|
||||
}
|
||||
CStr::from_ptr(utf8).to_string_lossy().into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn build_target_texture(device: &metal::DeviceRef, width: u32, height: u32) -> metal::Texture {
|
||||
let descriptor = TextureDescriptor::new();
|
||||
descriptor.set_width(u64::from(width));
|
||||
descriptor.set_height(u64::from(height));
|
||||
descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
|
||||
descriptor.set_storage_mode(metal::MTLStorageMode::Private);
|
||||
descriptor.set_usage(MTLTextureUsage::RenderTarget | MTLTextureUsage::ShaderRead);
|
||||
device.new_texture(&descriptor)
|
||||
}
|
||||
|
||||
fn build_surface_scene(
|
||||
image_buffer: CVPixelBuffer,
|
||||
lease: SurfaceLease,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Scene {
|
||||
let bounds = Bounds::new(
|
||||
point(ScaledPixels::default(), ScaledPixels::default()),
|
||||
size(ScaledPixels::from(width as f32), ScaledPixels::from(height as f32)),
|
||||
);
|
||||
let mut scene = Scene::default();
|
||||
scene.insert_primitive(PaintSurface {
|
||||
order: 0,
|
||||
bounds,
|
||||
content_mask: ContentMask { bounds },
|
||||
corner_radii: Corners::default(),
|
||||
image_buffer,
|
||||
lease: Some(lease),
|
||||
});
|
||||
scene
|
||||
}
|
||||
Vendored
+2
@@ -661,6 +661,8 @@ pub(crate) struct PaintSurface {
|
||||
pub corner_radii: Corners<ScaledPixels>,
|
||||
#[cfg(target_os = "macos")]
|
||||
pub image_buffer: core_video::pixel_buffer::CVPixelBuffer,
|
||||
#[cfg(target_os = "macos")]
|
||||
pub lease: Option<crate::SurfaceLease>,
|
||||
}
|
||||
|
||||
impl From<PaintSurface> for Primitive {
|
||||
|
||||
Vendored
+5
@@ -35,6 +35,11 @@ use std::{
|
||||
pin::Pin,
|
||||
};
|
||||
|
||||
#[cfg(all(target_os = "macos", feature = "test-support", not(feature = "macos-blade")))]
|
||||
pub use crate::metal_renderer::{
|
||||
MetalSurfaceTestSubmission, submit_surface_to_metal_for_test,
|
||||
};
|
||||
|
||||
/// Run the given test function with the configured parameters.
|
||||
/// This is intended for use with the `gpui::test` macro
|
||||
/// and generally should not be used directly.
|
||||
|
||||
Vendored
+12
@@ -3184,6 +3184,17 @@ impl Window {
|
||||
bounds: Bounds<Pixels>,
|
||||
corner_radii: Corners<Pixels>,
|
||||
image_buffer: CVPixelBuffer,
|
||||
) {
|
||||
self.paint_surface_with_lease(bounds, corner_radii, image_buffer, None);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) fn paint_surface_with_lease(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
corner_radii: Corners<Pixels>,
|
||||
image_buffer: CVPixelBuffer,
|
||||
lease: Option<crate::SurfaceLease>,
|
||||
) {
|
||||
use crate::PaintSurface;
|
||||
|
||||
@@ -3199,6 +3210,7 @@ impl Window {
|
||||
content_mask,
|
||||
corner_radii,
|
||||
image_buffer,
|
||||
lease,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user