Start on image rendering

Antonio Scandurra created

Change summary

Cargo.lock                                  |  65 ++++++++
gpui/Cargo.toml                             |   1 
gpui/src/elements.rs                        |  24 --
gpui/src/elements/image.rs                  |  65 ++++++++
gpui/src/image_data.rs                      |  31 ++++
gpui/src/lib.rs                             |   2 
gpui/src/platform/mac/atlas.rs              |  84 +++++++++-
gpui/src/platform/mac/renderer.rs           | 173 ++++++++++++++++++++--
gpui/src/platform/mac/shaders/shaders.h     |  51 +++++-
gpui/src/platform/mac/shaders/shaders.metal |  33 ++++
gpui/src/scene.rs                           |  22 ++
zed/Cargo.toml                              |   1 
zed/src/workspace.rs                        |  33 ++-
13 files changed, 510 insertions(+), 75 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -814,7 +814,7 @@ dependencies = [
  "error-chain",
  "glob 0.2.11",
  "icns",
- "image",
+ "image 0.12.4",
  "libflate",
  "md5",
  "msi",
@@ -2102,6 +2102,16 @@ dependencies = [
  "lzw",
 ]
 
+[[package]]
+name = "gif"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a668f699973d0f573d15749b7002a9ac9e1f9c6b220e7b165601334c173d8de"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
 [[package]]
 name = "gimli"
 version = "0.23.0"
@@ -2167,6 +2177,7 @@ dependencies = [
  "font-kit",
  "foreign-types",
  "gpui_macros",
+ "image 0.23.14",
  "lazy_static",
  "log",
  "metal",
@@ -2462,15 +2473,34 @@ checksum = "d95816db758249fe16f23a4e23f1a3a817fe11892dbfd1c5836f625324702158"
 dependencies = [
  "byteorder",
  "enum_primitive",
- "gif",
+ "gif 0.9.2",
  "jpeg-decoder",
  "num-iter",
- "num-rational",
+ "num-rational 0.1.42",
  "num-traits 0.1.43",
  "png 0.6.2",
  "scoped_threadpool",
 ]
 
+[[package]]
+name = "image"
+version = "0.23.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "gif 0.11.2",
+ "jpeg-decoder",
+ "num-iter",
+ "num-rational 0.3.2",
+ "num-traits 0.2.14",
+ "png 0.16.8",
+ "scoped_threadpool",
+ "tiff",
+]
+
 [[package]]
 name = "indexmap"
 version = "1.6.2"
@@ -3014,6 +3044,17 @@ dependencies = [
  "num-traits 0.2.14",
 ]
 
+[[package]]
+name = "num-rational"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
+dependencies = [
+ "autocfg 1.0.1",
+ "num-integer",
+ "num-traits 0.2.14",
+]
+
 [[package]]
 name = "num-traits"
 version = "0.1.43"
@@ -5129,6 +5170,17 @@ dependencies = [
  "tide",
 ]
 
+[[package]]
+name = "tiff"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437"
+dependencies = [
+ "jpeg-decoder",
+ "miniz_oxide 0.4.4",
+ "weezl",
+]
+
 [[package]]
 name = "time"
 version = "0.1.44"
@@ -5694,6 +5746,12 @@ dependencies = [
  "webpki",
 ]
 
+[[package]]
+name = "weezl"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e"
+
 [[package]]
 name = "wepoll-sys"
 version = "3.0.1"
@@ -5836,6 +5894,7 @@ dependencies = [
  "gpui",
  "http-auth-basic",
  "ignore",
+ "image 0.23.14",
  "lazy_static",
  "libc",
  "log",

gpui/Cargo.toml 🔗

@@ -11,6 +11,7 @@ backtrace = "0.3"
 ctor = "0.1"
 etagere = "0.2"
 gpui_macros = { path = "../gpui_macros" }
+image = "0.23"
 lazy_static = "1.4.0"
 log = "0.4"
 num_cpus = "1.13"

gpui/src/elements.rs 🔗

@@ -6,6 +6,7 @@ mod empty;
 mod event_handler;
 mod flex;
 mod hook;
+mod image;
 mod label;
 mod line_box;
 mod list;
@@ -16,25 +17,12 @@ mod svg;
 mod text;
 mod uniform_list;
 
+pub use self::{
+    align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*,
+    hook::*, image::*, label::*, line_box::*, list::*, mouse_event_handler::*, overlay::*,
+    stack::*, svg::*, text::*, uniform_list::*,
+};
 pub use crate::presenter::ChildView;
-pub use align::*;
-pub use canvas::*;
-pub use constrained_box::*;
-pub use container::*;
-pub use empty::*;
-pub use event_handler::*;
-pub use flex::*;
-pub use hook::*;
-pub use label::*;
-pub use line_box::*;
-pub use list::*;
-pub use mouse_event_handler::*;
-pub use overlay::*;
-pub use stack::*;
-pub use svg::*;
-pub use text::*;
-pub use uniform_list::*;
-
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json, DebugContext, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,

gpui/src/elements/image.rs 🔗

@@ -0,0 +1,65 @@
+use crate::{
+    geometry::{rect::RectF, vector::Vector2F},
+    json::{json, ToJson},
+    scene, DebugContext, Element, Event, EventContext, ImageData, LayoutContext, PaintContext,
+    SizeConstraint,
+};
+use std::sync::Arc;
+
+pub struct Image(Arc<ImageData>);
+
+impl Image {
+    pub fn new(data: Arc<ImageData>) -> Self {
+        Self(data)
+    }
+}
+
+impl Element for Image {
+    type LayoutState = ();
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: SizeConstraint,
+        _: &mut LayoutContext,
+    ) -> (Vector2F, Self::LayoutState) {
+        (constraint.max, ())
+    }
+
+    fn paint(
+        &mut self,
+        bounds: RectF,
+        _: RectF,
+        _: &mut Self::LayoutState,
+        cx: &mut PaintContext,
+    ) -> Self::PaintState {
+        cx.scene.push_image(scene::Image {
+            bounds,
+            data: self.0.clone(),
+        });
+    }
+
+    fn dispatch_event(
+        &mut self,
+        _: &Event,
+        _: RectF,
+        _: &mut Self::LayoutState,
+        _: &mut Self::PaintState,
+        _: &mut EventContext,
+    ) -> bool {
+        false
+    }
+
+    fn debug(
+        &self,
+        bounds: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &DebugContext,
+    ) -> serde_json::Value {
+        json!({
+            "type": "Image",
+            "bounds": bounds.to_json(),
+        })
+    }
+}

gpui/src/image_data.rs 🔗

@@ -0,0 +1,31 @@
+use crate::geometry::vector::{vec2i, Vector2I};
+use image::{Bgra, ImageBuffer};
+use std::sync::{
+    atomic::{AtomicUsize, Ordering::SeqCst},
+    Arc,
+};
+
+pub struct ImageData {
+    pub id: usize,
+    data: ImageBuffer<Bgra<u8>, Vec<u8>>,
+}
+
+impl ImageData {
+    pub fn new(data: ImageBuffer<Bgra<u8>, Vec<u8>>) -> Arc<Self> {
+        static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
+
+        Arc::new(Self {
+            id: NEXT_ID.fetch_add(1, SeqCst),
+            data,
+        })
+    }
+
+    pub fn as_bytes(&self) -> &[u8] {
+        &self.data
+    }
+
+    pub fn size(&self) -> Vector2I {
+        let (width, height) = self.data.dimensions();
+        vec2i(width as i32, height as i32)
+    }
+}

gpui/src/lib.rs 🔗

@@ -7,6 +7,8 @@ mod test;
 pub use assets::*;
 pub mod elements;
 pub mod font_cache;
+mod image_data;
+pub use crate::image_data::ImageData;
 pub mod views;
 pub use font_cache::FontCache;
 mod clipboard;

gpui/src/platform/mac/atlas.rs 🔗

@@ -1,4 +1,7 @@
-use crate::geometry::vector::{vec2i, Vector2I};
+use crate::geometry::{
+    rect::RectI,
+    vector::{vec2i, Vector2I},
+};
 use etagere::BucketedAtlasAllocator;
 use foreign_types::ForeignType;
 use metal::{self, Device, TextureDescriptor};
@@ -11,6 +14,12 @@ pub struct AtlasAllocator {
     free_atlases: Vec<Atlas>,
 }
 
+#[derive(Copy, Clone)]
+pub struct AllocId {
+    pub atlas_id: usize,
+    alloc_id: etagere::AllocId,
+}
+
 impl AtlasAllocator {
     pub fn new(device: Device, texture_descriptor: TextureDescriptor) -> Self {
         let mut me = Self {
@@ -31,20 +40,40 @@ impl AtlasAllocator {
         )
     }
 
-    pub fn allocate(&mut self, requested_size: Vector2I) -> anyhow::Result<(usize, Vector2I)> {
-        let origin = self
+    pub fn allocate(&mut self, requested_size: Vector2I) -> (AllocId, Vector2I) {
+        let (alloc_id, origin) = self
             .atlases
             .last_mut()
             .unwrap()
             .allocate(requested_size)
             .unwrap_or_else(|| {
                 let mut atlas = self.new_atlas(requested_size);
-                let origin = atlas.allocate(requested_size).unwrap();
+                let (id, origin) = atlas.allocate(requested_size).unwrap();
                 self.atlases.push(atlas);
-                origin
+                (id, origin)
             });
 
-        Ok((self.atlases.len() - 1, origin))
+        let id = AllocId {
+            atlas_id: self.atlases.len() - 1,
+            alloc_id,
+        };
+        (id, origin)
+    }
+
+    pub fn upload(&mut self, size: Vector2I, bytes: &[u8]) -> (AllocId, RectI) {
+        let (alloc_id, origin) = self.allocate(size);
+        let bounds = RectI::new(origin, size);
+        self.atlases[alloc_id.atlas_id].upload(bounds, bytes);
+        (alloc_id, bounds)
+    }
+
+    pub fn deallocate(&mut self, id: AllocId) {
+        if let Some(atlas) = self.atlases.get_mut(id.atlas_id) {
+            atlas.deallocate(id.alloc_id);
+            if atlas.is_empty() {
+                self.free_atlases.push(self.atlases.remove(id.atlas_id));
+            }
+        }
     }
 
     pub fn clear(&mut self) {
@@ -102,13 +131,44 @@ impl Atlas {
         vec2i(size.width, size.height)
     }
 
-    fn allocate(&mut self, size: Vector2I) -> Option<Vector2I> {
-        let origin = self
+    fn allocate(&mut self, size: Vector2I) -> Option<(etagere::AllocId, Vector2I)> {
+        let alloc = self
             .allocator
-            .allocate(etagere::Size::new(size.x(), size.y()))?
-            .rectangle
-            .min;
-        Some(vec2i(origin.x, origin.y))
+            .allocate(etagere::Size::new(size.x(), size.y()))?;
+        let origin = alloc.rectangle.min;
+        Some((alloc.id, vec2i(origin.x, origin.y)))
+    }
+
+    fn upload(&mut self, bounds: RectI, bytes: &[u8]) {
+        let region = metal::MTLRegion::new_2d(
+            bounds.origin().x() as u64,
+            bounds.origin().y() as u64,
+            bounds.size().x() as u64,
+            bounds.size().y() as u64,
+        );
+        self.texture.replace_region(
+            region,
+            0,
+            bytes.as_ptr() as *const _,
+            (bounds.size().x() * self.bytes_per_pixel() as i32) as u64,
+        );
+    }
+
+    fn bytes_per_pixel(&self) -> u8 {
+        use metal::MTLPixelFormat::*;
+        match self.texture.pixel_format() {
+            A8Unorm | R8Unorm => 1,
+            RGBA8Unorm | BGRA8Unorm => 4,
+            _ => unimplemented!(),
+        }
+    }
+
+    fn deallocate(&mut self, id: etagere::AllocId) {
+        self.allocator.deallocate(id);
+    }
+
+    fn is_empty(&self) -> bool {
+        self.allocator.is_empty()
     }
 
     fn clear(&mut self) {

gpui/src/platform/mac/renderer.rs 🔗

@@ -1,13 +1,15 @@
-use super::{atlas::AtlasAllocator, sprite_cache::SpriteCache};
+use super::{
+    atlas::{self, AtlasAllocator},
+    sprite_cache::SpriteCache,
+};
 use crate::{
     color::Color,
     geometry::{
-        rect::RectF,
+        rect::{RectF, RectI},
         vector::{vec2f, vec2i, Vector2F},
     },
     platform,
-    scene::{Glyph, Icon, Layer, Quad, Shadow},
-    Scene,
+    scene::{Glyph, Icon, Image, Layer, Quad, Scene, Shadow},
 };
 use cocoa::foundation::NSUInteger;
 use metal::{MTLPixelFormat, MTLResourceOptions, NSRange};
@@ -21,9 +23,13 @@ const INSTANCE_BUFFER_SIZE: usize = 1024 * 1024; // This is an arbitrary decisio
 pub struct Renderer {
     sprite_cache: SpriteCache,
     path_atlases: AtlasAllocator,
+    image_atlases: AtlasAllocator,
+    prev_rendered_images: HashMap<usize, (atlas::AllocId, RectI)>,
+    curr_rendered_images: HashMap<usize, (atlas::AllocId, RectI)>,
     quad_pipeline_state: metal::RenderPipelineState,
     shadow_pipeline_state: metal::RenderPipelineState,
     sprite_pipeline_state: metal::RenderPipelineState,
+    image_pipeline_state: metal::RenderPipelineState,
     path_atlas_pipeline_state: metal::RenderPipelineState,
     unit_vertices: metal::Buffer,
     instances: metal::Buffer,
@@ -64,7 +70,10 @@ impl Renderer {
         );
 
         let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), fonts);
-        let path_atlases = build_path_atlas_allocator(MTLPixelFormat::R8Unorm, &device);
+        let path_atlases =
+            AtlasAllocator::new(device.clone(), build_path_atlas_texture_descriptor());
+        let image_atlases =
+            AtlasAllocator::new(device.clone(), build_image_atlas_texture_descriptor());
         let quad_pipeline_state = build_pipeline_state(
             &device,
             &library,
@@ -89,6 +98,14 @@ impl Renderer {
             "sprite_fragment",
             pixel_format,
         );
+        let image_pipeline_state = build_pipeline_state(
+            &device,
+            &library,
+            "image",
+            "image_vertex",
+            "image_fragment",
+            pixel_format,
+        );
         let path_atlas_pipeline_state = build_path_atlas_pipeline_state(
             &device,
             &library,
@@ -100,9 +117,13 @@ impl Renderer {
         Self {
             sprite_cache,
             path_atlases,
+            image_atlases,
+            prev_rendered_images: Default::default(),
+            curr_rendered_images: Default::default(),
             quad_pipeline_state,
             shadow_pipeline_state,
             sprite_pipeline_state,
+            image_pipeline_state,
             path_atlas_pipeline_state,
             unit_vertices,
             instances,
@@ -117,6 +138,12 @@ impl Renderer {
         output: &metal::TextureRef,
     ) {
         let mut offset = 0;
+
+        mem::swap(
+            &mut self.curr_rendered_images,
+            &mut self.prev_rendered_images,
+        );
+
         let path_sprites = self.render_path_atlases(scene, &mut offset, command_buffer);
         self.render_layers(
             scene,
@@ -130,6 +157,11 @@ impl Renderer {
             location: 0,
             length: offset as NSUInteger,
         });
+
+        for (id, _) in self.prev_rendered_images.values() {
+            self.image_atlases.deallocate(*id);
+        }
+        self.prev_rendered_images.clear();
     }
 
     fn render_path_atlases(
@@ -146,11 +178,11 @@ impl Renderer {
             for path in layer.paths() {
                 let origin = path.bounds.origin() * scene.scale_factor();
                 let size = (path.bounds.size() * scene.scale_factor()).ceil();
-                let (atlas_id, atlas_origin) = self.path_atlases.allocate(size.to_i32()).unwrap();
+                let (alloc_id, atlas_origin) = self.path_atlases.allocate(size.to_i32());
                 let atlas_origin = atlas_origin.to_f32();
                 sprites.push(PathSprite {
                     layer_id,
-                    atlas_id,
+                    atlas_id: alloc_id.atlas_id,
                     shader_data: shaders::GPUISprite {
                         origin: origin.floor().to_float2(),
                         target_size: size.to_float2(),
@@ -162,7 +194,7 @@ impl Renderer {
                 });
 
                 if let Some(current_atlas_id) = current_atlas_id {
-                    if atlas_id != current_atlas_id {
+                    if alloc_id.atlas_id != current_atlas_id {
                         self.render_paths_to_atlas(
                             offset,
                             &vertices,
@@ -173,7 +205,7 @@ impl Renderer {
                     }
                 }
 
-                current_atlas_id = Some(atlas_id);
+                current_atlas_id = Some(alloc_id.atlas_id);
 
                 for vertex in &path.vertices {
                     let xy_position =
@@ -316,6 +348,13 @@ impl Renderer {
                 drawable_size,
                 command_encoder,
             );
+            self.render_images(
+                layer.images(),
+                scale_factor,
+                offset,
+                drawable_size,
+                command_encoder,
+            );
             self.render_quads(
                 layer.underlines(),
                 scale_factor,
@@ -602,6 +641,97 @@ impl Renderer {
         }
     }
 
+    fn render_images(
+        &mut self,
+        images: &[Image],
+        scale_factor: f32,
+        offset: &mut usize,
+        drawable_size: Vector2F,
+        command_encoder: &metal::RenderCommandEncoderRef,
+    ) {
+        if images.is_empty() {
+            return;
+        }
+
+        let mut images_by_atlas = HashMap::new();
+        for image in images {
+            let origin = image.bounds.origin() * scale_factor;
+            let target_size = image.bounds.size() * scale_factor;
+            let (alloc_id, atlas_bounds) = self
+                .prev_rendered_images
+                .remove(&image.data.id)
+                .or_else(|| self.curr_rendered_images.get(&image.data.id).copied())
+                .unwrap_or_else(|| {
+                    self.image_atlases
+                        .upload(image.data.size(), image.data.as_bytes())
+                });
+            self.curr_rendered_images
+                .insert(image.data.id, (alloc_id, atlas_bounds));
+            images_by_atlas
+                .entry(alloc_id.atlas_id)
+                .or_insert_with(Vec::new)
+                .push(shaders::GPUIImage {
+                    origin: origin.to_float2(),
+                    target_size: target_size.to_float2(),
+                    source_size: atlas_bounds.size().to_float2(),
+                    atlas_origin: atlas_bounds.origin().to_float2(),
+                });
+        }
+
+        command_encoder.set_render_pipeline_state(&self.image_pipeline_state);
+        command_encoder.set_vertex_buffer(
+            shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexVertices as u64,
+            Some(&self.unit_vertices),
+            0,
+        );
+        command_encoder.set_vertex_bytes(
+            shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexViewportSize as u64,
+            mem::size_of::<shaders::vector_float2>() as u64,
+            [drawable_size.to_float2()].as_ptr() as *const c_void,
+        );
+
+        for (atlas_id, images) in images_by_atlas {
+            align_offset(offset);
+            let next_offset = *offset + images.len() * mem::size_of::<shaders::GPUIImage>();
+            assert!(
+                next_offset <= INSTANCE_BUFFER_SIZE,
+                "instance buffer exhausted"
+            );
+
+            let texture = self.image_atlases.texture(atlas_id).unwrap();
+            command_encoder.set_vertex_buffer(
+                shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexImages as u64,
+                Some(&self.instances),
+                *offset as u64,
+            );
+            command_encoder.set_vertex_bytes(
+                shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexAtlasSize as u64,
+                mem::size_of::<shaders::vector_float2>() as u64,
+                [vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr()
+                    as *const c_void,
+            );
+            command_encoder.set_fragment_texture(
+                shaders::GPUIImageFragmentInputIndex_GPUIImageFragmentInputIndexAtlas as u64,
+                Some(texture),
+            );
+
+            unsafe {
+                let buffer_contents = (self.instances.contents() as *mut u8)
+                    .offset(*offset as isize)
+                    as *mut shaders::GPUIImage;
+                std::ptr::copy_nonoverlapping(images.as_ptr(), buffer_contents, images.len());
+            }
+
+            command_encoder.draw_primitives_instanced(
+                metal::MTLPrimitiveType::Triangle,
+                0,
+                6,
+                images.len() as u64,
+            );
+            *offset = next_offset;
+        }
+    }
+
     fn render_path_sprites(
         &mut self,
         layer_id: usize,
@@ -708,19 +838,23 @@ impl Renderer {
     }
 }
 
-fn build_path_atlas_allocator(
-    pixel_format: MTLPixelFormat,
-    device: &metal::Device,
-) -> AtlasAllocator {
+fn build_path_atlas_texture_descriptor() -> metal::TextureDescriptor {
     let texture_descriptor = metal::TextureDescriptor::new();
     texture_descriptor.set_width(2048);
     texture_descriptor.set_height(2048);
-    texture_descriptor.set_pixel_format(pixel_format);
+    texture_descriptor.set_pixel_format(MTLPixelFormat::R8Unorm);
     texture_descriptor
         .set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead);
     texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
-    let path_atlases = AtlasAllocator::new(device.clone(), texture_descriptor);
-    path_atlases
+    texture_descriptor
+}
+
+fn build_image_atlas_texture_descriptor() -> metal::TextureDescriptor {
+    let texture_descriptor = metal::TextureDescriptor::new();
+    texture_descriptor.set_width(2048);
+    texture_descriptor.set_height(2048);
+    texture_descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
+    texture_descriptor
 }
 
 fn align_offset(offset: &mut usize) {
@@ -803,9 +937,10 @@ mod shaders {
     #![allow(non_camel_case_types)]
     #![allow(non_snake_case)]
 
-    use pathfinder_geometry::vector::Vector2I;
-
-    use crate::{color::Color, geometry::vector::Vector2F};
+    use crate::{
+        color::Color,
+        geometry::vector::{Vector2F, Vector2I},
+    };
     use std::mem;
 
     include!(concat!(env!("OUT_DIR"), "/shaders.rs"));

gpui/src/platform/mac/shaders/shaders.h 🔗

@@ -1,16 +1,19 @@
 #include <simd/simd.h>
 
-typedef struct {
+typedef struct
+{
     vector_float2 viewport_size;
 } GPUIUniforms;
 
-typedef enum {
+typedef enum
+{
     GPUIQuadInputIndexVertices = 0,
     GPUIQuadInputIndexQuads = 1,
     GPUIQuadInputIndexUniforms = 2,
 } GPUIQuadInputIndex;
 
-typedef struct {
+typedef struct
+{
     vector_float2 origin;
     vector_float2 size;
     vector_uchar4 background_color;
@@ -22,13 +25,15 @@ typedef struct {
     float corner_radius;
 } GPUIQuad;
 
-typedef enum {
+typedef enum
+{
     GPUIShadowInputIndexVertices = 0,
     GPUIShadowInputIndexShadows = 1,
     GPUIShadowInputIndexUniforms = 2,
 } GPUIShadowInputIndex;
 
-typedef struct {
+typedef struct
+{
     vector_float2 origin;
     vector_float2 size;
     float corner_radius;
@@ -36,18 +41,21 @@ typedef struct {
     vector_uchar4 color;
 } GPUIShadow;
 
-typedef enum {
+typedef enum
+{
     GPUISpriteVertexInputIndexVertices = 0,
     GPUISpriteVertexInputIndexSprites = 1,
     GPUISpriteVertexInputIndexViewportSize = 2,
     GPUISpriteVertexInputIndexAtlasSize = 3,
 } GPUISpriteVertexInputIndex;
 
-typedef enum {
+typedef enum
+{
     GPUISpriteFragmentInputIndexAtlas = 0,
 } GPUISpriteFragmentInputIndex;
 
-typedef struct {
+typedef struct
+{
     vector_float2 origin;
     vector_float2 target_size;
     vector_float2 source_size;
@@ -56,14 +64,37 @@ typedef struct {
     uint8_t compute_winding;
 } GPUISprite;
 
-typedef enum {
+typedef enum
+{
     GPUIPathAtlasVertexInputIndexVertices = 0,
     GPUIPathAtlasVertexInputIndexAtlasSize = 1,
 } GPUIPathAtlasVertexInputIndex;
 
-typedef struct {
+typedef struct
+{
     vector_float2 xy_position;
     vector_float2 st_position;
     vector_float2 clip_rect_origin;
     vector_float2 clip_rect_size;
 } GPUIPathVertex;
+
+typedef enum
+{
+    GPUIImageVertexInputIndexVertices = 0,
+    GPUIImageVertexInputIndexImages = 1,
+    GPUIImageVertexInputIndexViewportSize = 2,
+    GPUIImageVertexInputIndexAtlasSize = 3,
+} GPUIImageVertexInputIndex;
+
+typedef enum
+{
+    GPUIImageFragmentInputIndexAtlas = 0,
+} GPUIImageFragmentInputIndex;
+
+typedef struct
+{
+    vector_float2 origin;
+    vector_float2 target_size;
+    vector_float2 source_size;
+    vector_float2 atlas_origin;
+} GPUIImage;

gpui/src/platform/mac/shaders/shaders.metal 🔗

@@ -217,6 +217,39 @@ fragment float4 sprite_fragment(
     return color;
 }
 
+struct ImageFragmentInput {
+    float4 position [[position]];
+    float2 atlas_position;
+};
+
+vertex ImageFragmentInput image_vertex(
+    uint unit_vertex_id [[vertex_id]],
+    uint image_id [[instance_id]],
+    constant float2 *unit_vertices [[buffer(GPUIImageVertexInputIndexVertices)]],
+    constant GPUIImage *images [[buffer(GPUIImageVertexInputIndexImages)]],
+    constant float2 *viewport_size [[buffer(GPUIImageVertexInputIndexViewportSize)]],
+    constant float2 *atlas_size [[buffer(GPUIImageVertexInputIndexAtlasSize)]]
+) {
+    float2 unit_vertex = unit_vertices[unit_vertex_id];
+    GPUIImage image = images[image_id];
+    float2 position = unit_vertex * image.target_size + image.origin;
+    float4 device_position = to_device_position(position, *viewport_size);
+    float2 atlas_position = (unit_vertex * image.source_size + image.atlas_origin) / *atlas_size;
+
+    return ImageFragmentInput {
+        device_position,
+        atlas_position,
+    };
+}
+
+fragment float4 image_fragment(
+    ImageFragmentInput input [[stage_in]],
+    texture2d<float> atlas [[ texture(GPUIImageFragmentInputIndexAtlas) ]]
+) {
+    constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
+    return atlas.sample(atlas_sampler, input.atlas_position);
+}
+
 struct PathAtlasVertexOutput {
     float4 position [[position]];
     float2 st_position;

gpui/src/scene.rs 🔗

@@ -1,12 +1,13 @@
 use serde::Deserialize;
 use serde_json::json;
-use std::borrow::Cow;
+use std::{borrow::Cow, sync::Arc};
 
 use crate::{
     color::Color,
     fonts::{FontId, GlyphId},
     geometry::{rect::RectF, vector::Vector2F},
     json::ToJson,
+    ImageData,
 };
 
 pub struct Scene {
@@ -25,6 +26,7 @@ pub struct Layer {
     clip_bounds: Option<RectF>,
     quads: Vec<Quad>,
     underlines: Vec<Quad>,
+    images: Vec<Image>,
     shadows: Vec<Shadow>,
     glyphs: Vec<Glyph>,
     icons: Vec<Icon>,
@@ -124,6 +126,11 @@ pub struct PathVertex {
     pub st_position: Vector2F,
 }
 
+pub struct Image {
+    pub bounds: RectF,
+    pub data: Arc<ImageData>,
+}
+
 impl Scene {
     pub fn new(scale_factor: f32) -> Self {
         let stacking_context = StackingContext::new(None);
@@ -166,6 +173,10 @@ impl Scene {
         self.active_layer().push_quad(quad)
     }
 
+    pub fn push_image(&mut self, image: Image) {
+        self.active_layer().push_image(image)
+    }
+
     pub fn push_underline(&mut self, underline: Quad) {
         self.active_layer().push_underline(underline)
     }
@@ -240,6 +251,7 @@ impl Layer {
             clip_bounds,
             quads: Vec::new(),
             underlines: Vec::new(),
+            images: Vec::new(),
             shadows: Vec::new(),
             glyphs: Vec::new(),
             icons: Vec::new(),
@@ -267,6 +279,14 @@ impl Layer {
         self.underlines.as_slice()
     }
 
+    fn push_image(&mut self, image: Image) {
+        self.images.push(image);
+    }
+
+    pub fn images(&self) -> &[Image] {
+        self.images.as_slice()
+    }
+
     fn push_shadow(&mut self, shadow: Shadow) {
         self.shadows.push(shadow);
     }

zed/Cargo.toml 🔗

@@ -30,6 +30,7 @@ futures = "0.3"
 gpui = { path = "../gpui" }
 http-auth-basic = "0.1.3"
 ignore = "0.4"
+image = "0.23"
 lazy_static = "1.4.0"
 libc = "0.2"
 log = "0.4"

zed/src/workspace.rs 🔗

@@ -21,7 +21,7 @@ use gpui::{
     json::to_string_pretty,
     keymap::Binding,
     platform::WindowOptions,
-    AnyViewHandle, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext,
+    AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelHandle, MutableAppContext,
     PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
     WeakModelHandle,
 };
@@ -354,10 +354,19 @@ pub struct Workspace {
         (usize, Arc<Path>),
         postage::watch::Receiver<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
     >,
+    image: Arc<ImageData>,
 }
 
 impl Workspace {
     pub fn new(app_state: &AppState, cx: &mut ViewContext<Self>) -> Self {
+        let image_bytes = crate::assets::Assets::get("images/as-cii.jpeg").unwrap();
+        let image = image::io::Reader::new(std::io::Cursor::new(&*image_bytes.data))
+            .with_guessed_format()
+            .unwrap()
+            .decode()
+            .unwrap()
+            .into_bgra8();
+
         let pane = cx.add_view(|_| Pane::new(app_state.settings.clone()));
         let pane_id = pane.id();
         cx.subscribe(&pane, move |me, _, event, cx| {
@@ -401,6 +410,7 @@ impl Workspace {
             worktrees: Default::default(),
             items: Default::default(),
             loading_items: Default::default(),
+            image: ImageData::new(image),
         }
     }
 
@@ -954,17 +964,16 @@ impl View for Workspace {
             Flex::column()
                 .with_child(
                     ConstrainedBox::new(
-                        Container::new(
-                            Align::new(
-                                Label::new(
-                                    "zed".into(), 
-                                    theme.workspace.titlebar.label.clone()
-                                ).boxed()
-                            )
-                            .boxed()
-                        )
-                        .with_style(&theme.workspace.titlebar.container)
-                        .boxed(),
+                        Image::new(self.image.clone()).boxed()
+                        // Container::new(
+                        //     Align::new(
+                        //         Label::new("zed".into(), theme.workspace.titlebar.label.clone())
+                        //             .boxed(),
+                        //     )
+                        //     .boxed(),
+                        // )
+                        // .with_style(&theme.workspace.titlebar.container)
+                        // .boxed(),
                     )
                     .with_height(32.)
                     .named("titlebar"),