Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ members = [
"crates/gpui_macros",
"crates/gpui_platform",
"crates/gpui_shared_string",
"crates/gpui_terminal",
"crates/gpui_tokio",
"crates/gpui_util",
"crates/gpui_web",
Expand Down Expand Up @@ -361,6 +362,7 @@ gpui_macos = { path = "crates/gpui_macos", default-features = false }
gpui_macros = { path = "crates/gpui_macros" }
gpui_platform = { path = "crates/gpui_platform", default-features = false }
gpui_shared_string = { path = "crates/gpui_shared_string" }
gpui_terminal = { path = "crates/gpui_terminal" }
gpui_tokio = { path = "crates/gpui_tokio" }
gpui_util = { path = "crates/gpui_util" }
gpui_web = { path = "crates/gpui_web" }
Expand Down
8 changes: 0 additions & 8 deletions crates/gpui/src/gpui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,6 @@ mod platform;
pub mod prelude;
/// Profiling utilities for task, frame, and thread performance tracking.
pub mod profiler;
#[cfg(any(
test,
target_os = "windows",
target_os = "linux",
target_family = "wasm",
feature = "test-support"
))]
#[expect(missing_docs)]
pub mod queue;
mod scene;
Expand Down Expand Up @@ -145,7 +138,6 @@ pub use keymap::*;
pub use path_builder::*;
pub use platform::*;
pub use profiler::*;
#[cfg(any(target_os = "windows", target_os = "linux", target_family = "wasm"))]
pub use queue::{PriorityQueueReceiver, PriorityQueueSender};
pub use refineable::*;
pub use scene::*;
Expand Down
121 changes: 106 additions & 15 deletions crates/gpui_apple/src/metal_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ pub struct MetalRenderer {
/// rendering headlessly without reading pixels back.
#[cfg(any(test, feature = "test-support"))]
headless_render_target: Option<metal::Texture>,
/// CPU-readable render target reused across `render_scene_to_pixels` calls.
readback_target: Option<metal::Texture>,
/// Format of every render target and pipeline output.
pixel_format: MTLPixelFormat,
}

#[repr(C)]
Expand Down Expand Up @@ -173,17 +177,28 @@ impl MetalRenderer {
];
}

Self::new_internal(device, Some(layer), !transparent, instance_buffer_pool)
Self::new_internal(
device,
Some(layer),
!transparent,
instance_buffer_pool,
MTLPixelFormat::BGRA8Unorm,
)
}

/// Creates a new headless MetalRenderer for offscreen rendering without a window.
///
/// This renderer can render scenes to images without requiring a CAMetalLayer,
/// window, or AppKit. Use `render_scene_to_image()` to render scenes.
#[cfg(any(test, feature = "test-support"))]
pub fn new_headless(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>) -> Self {
/// window, or AppKit. Use `render_scene_to_pixels()` to render scenes.
///
/// `pixel_format` is the format of the produced pixels: `BGRA8Unorm` or
/// `RGBA8Unorm`. Rendering in the consumer's format avoids a CPU swizzle.
pub fn new_headless(
instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>,
pixel_format: MTLPixelFormat,
) -> Self {
let device = Self::create_device();
Self::new_internal(device, None, true, instance_buffer_pool)
Self::new_internal(device, None, true, instance_buffer_pool, pixel_format)
}

fn create_device() -> metal::Device {
Expand Down Expand Up @@ -213,6 +228,7 @@ impl MetalRenderer {
layer: Option<metal::MetalLayer>,
opaque: bool,
instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>,
pixel_format: MTLPixelFormat,
) -> Self {
#[cfg(feature = "runtime_shaders")]
let library = device
Expand Down Expand Up @@ -264,7 +280,7 @@ impl MetalRenderer {
"paths_rasterization",
"path_rasterization_vertex",
"path_rasterization_fragment",
MTLPixelFormat::BGRA8Unorm,
pixel_format,
PATH_SAMPLE_COUNT,
);
let path_sprites_pipeline_state = build_path_sprite_pipeline_state(
Expand All @@ -273,55 +289,55 @@ impl MetalRenderer {
"path_sprites",
"path_sprite_vertex",
"path_sprite_fragment",
MTLPixelFormat::BGRA8Unorm,
pixel_format,
);
let shadows_pipeline_state = build_pipeline_state(
&device,
&library,
"shadows",
"shadow_vertex",
"shadow_fragment",
MTLPixelFormat::BGRA8Unorm,
pixel_format,
);
let quads_pipeline_state = build_pipeline_state(
&device,
&library,
"quads",
"quad_vertex",
"quad_fragment",
MTLPixelFormat::BGRA8Unorm,
pixel_format,
);
let underlines_pipeline_state = build_pipeline_state(
&device,
&library,
"underlines",
"underline_vertex",
"underline_fragment",
MTLPixelFormat::BGRA8Unorm,
pixel_format,
);
let monochrome_sprites_pipeline_state = build_pipeline_state(
&device,
&library,
"monochrome_sprites",
"monochrome_sprite_vertex",
"monochrome_sprite_fragment",
MTLPixelFormat::BGRA8Unorm,
pixel_format,
);
let polychrome_sprites_pipeline_state = build_pipeline_state(
&device,
&library,
"polychrome_sprites",
"polychrome_sprite_vertex",
"polychrome_sprite_fragment",
MTLPixelFormat::BGRA8Unorm,
pixel_format,
);
let surfaces_pipeline_state = build_pipeline_state(
&device,
&library,
"surfaces",
"surface_vertex",
"surface_fragment",
MTLPixelFormat::BGRA8Unorm,
pixel_format,
);

let command_queue = device.new_command_queue();
Expand Down Expand Up @@ -354,6 +370,8 @@ impl MetalRenderer {
path_sample_count: PATH_SAMPLE_COUNT,
#[cfg(any(test, feature = "test-support"))]
headless_render_target: None,
readback_target: None,
pixel_format,
}
}

Expand Down Expand Up @@ -408,7 +426,7 @@ impl MetalRenderer {
let texture_descriptor = metal::TextureDescriptor::new();
texture_descriptor.set_width(size.width.0 as u64);
texture_descriptor.set_height(size.height.0 as u64);
texture_descriptor.set_pixel_format(metal::MTLPixelFormat::BGRA8Unorm);
texture_descriptor.set_pixel_format(self.pixel_format);
texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
texture_descriptor
.set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead);
Expand Down Expand Up @@ -648,6 +666,78 @@ impl MetalRenderer {
Ok(())
}

/// Renders a scene offscreen and copies the result into `pixels` as
/// tightly packed rows in the renderer's pixel format, reusing the render
/// target between frames. `pixels` must be `width * height * 4` bytes.
///
/// This is the presentation path for hosts that display frames themselves,
/// such as a terminal, rather than through a `CAMetalLayer`.
pub fn render_scene_to_pixels(
&mut self,
scene: &Scene,
size: Size<DevicePixels>,
pixels: &mut [u8],
) -> Result<()> {
if size.width.0 <= 0 || size.height.0 <= 0 {
anyhow::bail!("Invalid size for render_scene_to_pixels: {:?}", size);
}
let bytes_per_row = size.width.0 as usize * 4;
anyhow::ensure!(
pixels.len() == bytes_per_row * size.height.0 as usize,
"pixel buffer does not match {size:?}"
);

let needs_new_target = self.readback_target.as_ref().is_none_or(|texture| {
texture.width() != size.width.0 as u64 || texture.height() != size.height.0 as u64
});
if needs_new_target {
self.update_path_intermediate_textures(size);
let texture_descriptor = metal::TextureDescriptor::new();
texture_descriptor.set_width(size.width.0 as u64);
texture_descriptor.set_height(size.height.0 as u64);
texture_descriptor.set_pixel_format(self.pixel_format);
texture_descriptor.set_usage(
metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead,
);
// Shared storage lets the CPU read the texture without a blit on
// unified memory. Discrete GPUs cannot share textures.
texture_descriptor.set_storage_mode(if self.is_unified_memory {
metal::MTLStorageMode::Shared
} else {
metal::MTLStorageMode::Managed
});
self.readback_target = Some(self.device.new_texture(&texture_descriptor));
}
let target_texture = self
.readback_target
.clone()
.context("readback target was just created")?;

let command_buffer = self.render_frame(scene, &target_texture, size)?;
if !self.is_unified_memory {
let blit = command_buffer.new_blit_command_encoder();
blit.synchronize_resource(&target_texture);
blit.end_encoding();
}
command_buffer.commit();
command_buffer.wait_until_completed();

target_texture.get_bytes(
pixels.as_mut_ptr() as *mut c_void,
bytes_per_row as u64,
metal::MTLRegion {
origin: metal::MTLOrigin { x: 0, y: 0, z: 0 },
size: metal::MTLSize {
width: size.width.0 as u64,
height: size.height.0 as u64,
depth: 1,
},
},
0,
);
Ok(())
}

fn draw_primitives_to_texture(
&mut self,
scene: &Scene,
Expand Down Expand Up @@ -1604,7 +1694,8 @@ pub struct MetalHeadlessRenderer {
impl MetalHeadlessRenderer {
pub fn new() -> Self {
let instance_buffer_pool = Arc::new(Mutex::new(InstanceBufferPool::default()));
let renderer = MetalRenderer::new_headless(instance_buffer_pool);
let renderer =
MetalRenderer::new_headless(instance_buffer_pool, MTLPixelFormat::BGRA8Unorm);
Self { renderer }
}
}
Expand Down
6 changes: 4 additions & 2 deletions crates/gpui_macos/src/gpui_macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ mod screen_capture;
use gpui_apple::metal_renderer as renderer;

pub mod metal_renderer {
pub use gpui_apple::metal_renderer::{PathRasterizationVertex, PathSprite, SurfaceBounds};
pub use gpui_apple::metal_renderer::{
InstanceBufferPool, MetalRenderer, PathRasterizationVertex, PathSprite, SurfaceBounds,
};

#[cfg(any(test, feature = "test-support"))]
pub use gpui_apple::metal_renderer::MetalHeadlessRenderer;
Expand Down Expand Up @@ -53,7 +55,7 @@ pub(crate) use platform::*;
pub(crate) use window::*;

#[cfg(feature = "font-kit")]
pub(crate) use text_system::*;
pub use text_system::MacTextSystem;

pub use display_link::set_frame_wake_hook;
pub use platform::MacPlatform;
Expand Down
37 changes: 37 additions & 0 deletions crates/gpui_terminal/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[package]
name = "gpui_terminal"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "Apache-2.0"

[lints]
workspace = true

[lib]
path = "src/gpui_terminal.rs"

[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "freebsd"))'.dependencies]
anyhow.workspace = true
base64.workspace = true
flate2 = "1.1.8"
futures.workspace = true
gpui.workspace = true
libc.workspace = true
log.workspace = true
parking_lot.workspace = true
raw-window-handle.workspace = true
smol.workspace = true
uuid.workspace = true

[target.'cfg(target_os = "macos")'.dependencies]
gpui_macos = { workspace = true, features = ["font-kit"] }
metal.workspace = true
objc.workspace = true

[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
gpui_wgpu = { workspace = true, features = ["font-kit"] }

[[example]]
name = "terminal_hello"
path = "examples/terminal_hello.rs"
1 change: 1 addition & 0 deletions crates/gpui_terminal/LICENSE-APACHE
Loading