metal_view.rs

  1use gpui::{prelude::*, *};
  2use std::sync::Arc;
  3use std::time::Instant;
  4
  5#[cfg(target_os = "macos")]
  6use metal::{Device, MTLPrimitiveType, RenderCommandEncoderRef, RenderPipelineState, TextureRef};
  7
  8struct MetalViewExample {
  9    start_time: Instant,
 10    #[cfg(target_os = "macos")]
 11    pipeline_state: Option<RenderPipelineState>,
 12    #[cfg(target_os = "macos")]
 13    device: Option<Device>,
 14}
 15
 16impl MetalViewExample {
 17    fn new() -> Self {
 18        Self {
 19            #[cfg(target_os = "macos")]
 20            pipeline_state: None,
 21            #[cfg(target_os = "macos")]
 22            device: None,
 23            start_time: Instant::now(),
 24        }
 25    }
 26
 27    #[cfg(target_os = "macos")]
 28    fn setup_metal(&mut self) {
 29        let device = Device::system_default().expect("no Metal device");
 30
 31        // Simplified shader for debugging
 32        let shader_source = r#"
 33            #include <metal_stdlib>
 34            using namespace metal;
 35
 36            struct Uniforms {
 37                float time;
 38            };
 39
 40            struct VertexOut {
 41                float4 position [[position]];
 42                float4 color;
 43            };
 44
 45            vertex VertexOut vertex_main(
 46                uint vid [[vertex_id]],
 47                constant Uniforms& uniforms [[buffer(0)]]
 48            ) {
 49                VertexOut out;
 50
 51                // Define triangle vertices in normalized device coordinates
 52                float2 positions[3] = {
 53                    float2( 0.0,  0.5),  // Top
 54                    float2(-0.5, -0.5),  // Bottom left
 55                    float2( 0.5, -0.5)   // Bottom right
 56                };
 57
 58                float3 colors[3] = {
 59                    float3(1.0, 0.0, 0.0),  // Red
 60                    float3(0.0, 1.0, 0.0),  // Green
 61                    float3(0.0, 0.0, 1.0)   // Blue
 62                };
 63
 64                // Apply rotation
 65                float2 pos = positions[vid];
 66                float c = cos(uniforms.time);
 67                float s = sin(uniforms.time);
 68                float2 rotated = float2(
 69                    pos.x * c - pos.y * s,
 70                    pos.x * s + pos.y * c
 71                );
 72
 73                out.position = float4(rotated, 0.0, 1.0);
 74                out.color = float4(colors[vid], 1.0);
 75                return out;
 76            }
 77
 78            fragment float4 fragment_main(VertexOut in [[stage_in]]) {
 79                return in.color;
 80            }
 81        "#;
 82
 83        let library = device
 84            .new_library_with_source(shader_source, &metal::CompileOptions::new())
 85            .expect("Failed to create shader library");
 86
 87        let vertex_function = library.get_function("vertex_main", None).unwrap();
 88        let fragment_function = library.get_function("fragment_main", None).unwrap();
 89
 90        // Create pipeline state - no vertex descriptor needed for vertex_id based rendering
 91        let pipeline_descriptor = metal::RenderPipelineDescriptor::new();
 92        pipeline_descriptor.set_vertex_function(Some(&vertex_function));
 93        pipeline_descriptor.set_fragment_function(Some(&fragment_function));
 94
 95        let color_attachment = pipeline_descriptor
 96            .color_attachments()
 97            .object_at(0)
 98            .unwrap();
 99        color_attachment.set_pixel_format(metal::MTLPixelFormat::BGRA8Unorm);
100
101        // Note: Depth testing is not enabled for now as it requires proper depth buffer setup
102        // in the GPUI rendering pipeline
103
104        // Enable blending
105        color_attachment.set_blending_enabled(true);
106        color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::SourceAlpha);
107        color_attachment
108            .set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
109        color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
110        color_attachment
111            .set_destination_alpha_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
112
113        let pipeline_state = device
114            .new_render_pipeline_state(&pipeline_descriptor)
115            .expect("Failed to create pipeline state");
116
117        self.device = Some(device);
118        self.pipeline_state = Some(pipeline_state);
119    }
120
121    #[cfg(target_os = "macos")]
122    fn create_render_callback(&self, time_delta: f32) -> MetalRenderCallback {
123        let pipeline_state = self.pipeline_state.clone().unwrap();
124
125        Arc::new(
126            move |encoder: &RenderCommandEncoderRef,
127                  _target: &TextureRef,
128                  bounds: Bounds<Pixels>,
129                  scale_factor: f32| {
130                // Set the pipeline state
131                encoder.set_render_pipeline_state(&pipeline_state);
132
133                // Set viewport to match element bounds
134                let viewport = metal::MTLViewport {
135                    originX: bounds.origin.x.0 as f64 * scale_factor as f64,
136                    originY: bounds.origin.y.0 as f64 * scale_factor as f64,
137                    width: bounds.size.width.0 as f64 * scale_factor as f64,
138                    height: bounds.size.height.0 as f64 * scale_factor as f64,
139                    znear: 0.0,
140                    zfar: 1.0,
141                };
142                encoder.set_viewport(viewport);
143
144                // Set scissor rectangle to clip to bounds
145                let scissor_rect = metal::MTLScissorRect {
146                    x: (bounds.origin.x.0 * scale_factor) as u64,
147                    y: (bounds.origin.y.0 * scale_factor) as u64,
148                    width: (bounds.size.width.0 * scale_factor) as u64,
149                    height: (bounds.size.height.0 * scale_factor) as u64,
150                };
151                encoder.set_scissor_rect(scissor_rect);
152
153                // Pass time as uniform
154                let time = time_delta * 2.0; // Scale for reasonable rotation speed
155                #[repr(C)]
156                struct Uniforms {
157                    time: f32,
158                }
159                let uniforms = Uniforms { time };
160                encoder.set_vertex_bytes(
161                    0,
162                    std::mem::size_of::<Uniforms>() as u64,
163                    &uniforms as *const Uniforms as *const _,
164                );
165
166                // Draw triangle using vertex_id - no vertex buffer needed
167                encoder.draw_primitives(MTLPrimitiveType::Triangle, 0, 3);
168            },
169        )
170    }
171}
172
173impl Render for MetalViewExample {
174    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
175        // Initialize Metal on first render if on macOS
176        #[cfg(target_os = "macos")]
177        if self.pipeline_state.is_none() {
178            self.setup_metal();
179        }
180
181        // Request animation frame
182        window.request_animation_frame();
183
184        div()
185            .flex()
186            .flex_col()
187            .bg(rgb(0x1e1e1e))
188            .size_full()
189            .p_8()
190            .gap_6()
191            .child(
192                div()
193                    .child("Metal View Element")
194                    .text_2xl()
195                    .text_color(rgb(0xffffff)),
196            )
197            .child(
198                div()
199                    .child("While GPUI normally handles all Metal rendering for you, the metal_view() element gives you direct access to write custom Metal shaders and GPU drawing commands")
200                    .text_color(rgb(0xaaaaaa)),
201            )
202            .child(
203                div()
204                    .child("This example shows a rotating 3D cube - the 'Hello World' of 3D graphics programming")
205                    .text_sm()
206                    .text_color(rgb(0x888888)),
207            )
208            .child(div().overflow_hidden().child(
209                #[cfg(target_os = "macos")]
210                {
211                    let elapsed = self.start_time.elapsed().as_secs_f32();
212                    let callback = self.create_render_callback(elapsed);
213                    metal_view()
214                        .render_with_shared(callback)
215                        .w(px(600.0))
216                        .h(px(400.0))
217                        .bg(rgb(0x000000))
218                },
219                #[cfg(not(target_os = "macos"))]
220                {
221                    div()
222                        .w(px(600.0))
223                        .h(px(400.0))
224                        .bg(rgb(0x222222))
225                        .flex()
226                        .items_center()
227                        .justify_center()
228                        .child(div().child("Metal (macOS only)").text_color(rgb(0x666666)))
229                },
230            ))
231    }
232}
233
234fn main() {
235    Application::new().run(|cx: &mut App| {
236        let _ = cx.open_window(
237            WindowOptions {
238                window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
239                    None,
240                    size(px(900.0), px(600.0)),
241                    cx,
242                ))),
243                titlebar: Some(TitlebarOptions {
244                    title: Some("Metal View Element".into()),
245                    ..Default::default()
246                }),
247                ..Default::default()
248            },
249            |_window, cx| cx.new(|_cx| MetalViewExample::new()),
250        );
251
252        cx.activate(false);
253    });
254}