metal_render_pass.rs

  1use crate::{DevicePixels, PaintMetalView, PrimitiveBatch, ScaledPixels, Scene, Size};
  2use metal::{
  3    CommandBufferRef, CommandQueue, Device, MTLLoadAction, MTLStoreAction, RenderCommandEncoderRef,
  4};
  5
  6/// Represents a single render command in the rendering pipeline
  7#[derive(Debug)]
  8pub enum RenderCommand<'a> {
  9    /// Begin a new render pass with the specified configuration
 10    BeginRenderPass { descriptor: RenderPassDescriptor },
 11    /// Draw a batch of GPUI primitives
 12    DrawPrimitives {
 13        batch: PrimitiveBatch<'a>,
 14        viewport_size: Size<DevicePixels>,
 15    },
 16    /// Execute custom Metal rendering
 17    ExecuteMetalCallback {
 18        metal_view: &'a PaintMetalView,
 19        viewport_size: Size<DevicePixels>,
 20    },
 21    /// End the current render pass
 22    EndRenderPass,
 23}
 24
 25/// Configuration for a render pass
 26#[derive(Clone, Debug)]
 27pub struct RenderPassDescriptor {
 28    pub texture: metal::Texture,
 29    pub load_action: MTLLoadAction,
 30    pub store_action: MTLStoreAction,
 31    pub clear_color: metal::MTLClearColor,
 32    pub viewport: metal::MTLViewport,
 33}
 34
 35/// State that needs to be preserved across render pass breaks
 36#[derive(Clone, Debug)]
 37pub struct RenderState {
 38    pub viewport: metal::MTLViewport,
 39    pub blend_mode: Option<BlendMode>,
 40    // Add other state that needs to be preserved
 41}
 42
 43#[derive(Clone, Copy, Debug, PartialEq)]
 44pub enum BlendMode {
 45    Normal,
 46    Multiply,
 47    Screen,
 48    // Add other blend modes as needed
 49}
 50
 51/// Context provided to Metal render callbacks
 52pub struct MetalRenderContext<'a> {
 53    pub command_buffer: &'a CommandBufferRef,
 54    pub drawable_texture: &'a metal::TextureRef,
 55    pub viewport_size: Size<DevicePixels>,
 56    pub device: &'a Device,
 57    pub bounds: crate::Bounds<ScaledPixels>,
 58    pub scale_factor: f32,
 59}
 60
 61/// Manages the rendering pipeline with support for render pass breaks
 62pub struct RenderPassManager {
 63    device: Device,
 64    command_queue: CommandQueue,
 65    current_state: RenderState,
 66}
 67
 68impl RenderPassManager {
 69    pub fn new(device: Device, command_queue: CommandQueue) -> Self {
 70        Self {
 71            device,
 72            command_queue,
 73            current_state: RenderState {
 74                viewport: metal::MTLViewport {
 75                    originX: 0.0,
 76                    originY: 0.0,
 77                    width: 0.0,
 78                    height: 0.0,
 79                    znear: 0.0,
 80                    zfar: 1.0,
 81                },
 82                blend_mode: None,
 83            },
 84        }
 85    }
 86
 87    /// Convert a scene into a list of render commands
 88    pub fn build_render_commands<'a>(
 89        &self,
 90        scene: &'a Scene,
 91        drawable_texture: &metal::TextureRef,
 92        viewport_size: Size<DevicePixels>,
 93        is_opaque: bool,
 94    ) -> Vec<RenderCommand<'a>> {
 95        let mut commands = Vec::new();
 96
 97        // Initial render pass configuration
 98        let alpha = if is_opaque { 1.0 } else { 0.0 };
 99        let descriptor = RenderPassDescriptor {
100            texture: drawable_texture.to_owned(),
101            load_action: MTLLoadAction::Clear,
102            store_action: MTLStoreAction::Store,
103            clear_color: metal::MTLClearColor::new(0.0, 0.0, 0.0, alpha),
104            viewport: metal::MTLViewport {
105                originX: 0.0,
106                originY: 0.0,
107                width: i32::from(viewport_size.width) as f64,
108                height: i32::from(viewport_size.height) as f64,
109                znear: 0.0,
110                zfar: 1.0,
111            },
112        };
113
114        commands.push(RenderCommand::BeginRenderPass { descriptor });
115
116        // Process batches, inserting render pass breaks for MetalViews
117        let mut in_render_pass = true;
118
119        for batch in scene.batches() {
120            match batch {
121                #[cfg(target_os = "macos")]
122                PrimitiveBatch::MetalViews(metal_views) => {
123                    // End current render pass
124                    if in_render_pass {
125                        commands.push(RenderCommand::EndRenderPass);
126                        in_render_pass = false;
127                    }
128
129                    // Add commands for each MetalView
130                    for metal_view in metal_views {
131                        commands.push(RenderCommand::ExecuteMetalCallback {
132                            metal_view,
133                            viewport_size,
134                        });
135                    }
136                }
137                _ => {
138                    // Ensure we're in a render pass
139                    if !in_render_pass {
140                        let descriptor = RenderPassDescriptor {
141                            texture: drawable_texture.to_owned(),
142                            load_action: MTLLoadAction::Load, // Load existing content
143                            store_action: MTLStoreAction::Store,
144                            clear_color: metal::MTLClearColor::new(0.0, 0.0, 0.0, 0.0),
145                            viewport: self.current_state.viewport,
146                        };
147                        commands.push(RenderCommand::BeginRenderPass { descriptor });
148                        in_render_pass = true;
149                    }
150
151                    // Add primitive drawing command
152                    commands.push(RenderCommand::DrawPrimitives {
153                        batch,
154                        viewport_size,
155                    });
156                }
157            }
158        }
159
160        // Ensure we end the final render pass
161        if in_render_pass {
162            commands.push(RenderCommand::EndRenderPass);
163        }
164
165        commands
166    }
167
168    /// Execute a list of render commands
169    pub fn execute_commands<F>(
170        &mut self,
171        commands: &[RenderCommand],
172        command_buffer: &CommandBufferRef,
173        drawable_texture: &metal::TextureRef,
174        mut draw_primitives: F,
175    ) -> Result<(), anyhow::Error>
176    where
177        F: FnMut(
178            PrimitiveBatch,
179            &RenderCommandEncoderRef,
180            Size<DevicePixels>,
181        ) -> Result<(), anyhow::Error>,
182    {
183        let mut current_encoder: Option<metal::RenderCommandEncoder> = None;
184
185        for command in commands {
186            match command {
187                RenderCommand::BeginRenderPass { descriptor } => {
188                    // End any existing encoder
189                    if let Some(encoder) = current_encoder.take() {
190                        encoder.end_encoding();
191                    }
192
193                    // Create new render pass
194                    let render_pass_descriptor = metal::RenderPassDescriptor::new();
195                    let color_attachment = render_pass_descriptor
196                        .color_attachments()
197                        .object_at(0)
198                        .unwrap();
199
200                    color_attachment.set_texture(Some(&descriptor.texture));
201                    color_attachment.set_load_action(descriptor.load_action);
202                    color_attachment.set_store_action(descriptor.store_action);
203                    color_attachment.set_clear_color(descriptor.clear_color);
204
205                    let encoder =
206                        command_buffer.new_render_command_encoder(&render_pass_descriptor);
207                    encoder.set_viewport(descriptor.viewport);
208                    self.current_state.viewport = descriptor.viewport;
209
210                    current_encoder = Some(encoder);
211                }
212
213                RenderCommand::DrawPrimitives {
214                    batch,
215                    viewport_size,
216                } => {
217                    if let Some(ref encoder) = current_encoder {
218                        draw_primitives(*batch, encoder, *viewport_size)?;
219                    }
220                }
221
222                RenderCommand::ExecuteMetalCallback {
223                    metal_view,
224                    viewport_size,
225                } => {
226                    // End current encoder if any
227                    if let Some(encoder) = current_encoder.take() {
228                        encoder.end_encoding();
229                    }
230
231                    // Create context for the callback
232                    let context = MetalRenderContext {
233                        command_buffer,
234                        drawable_texture,
235                        viewport_size: *viewport_size,
236                        device: &self.device,
237                        bounds: metal_view.bounds.clone(),
238                        scale_factor: 2.0, // TODO: Get actual scale factor
239                    };
240
241                    // Create a new render command encoder for the callback
242                    let render_pass_descriptor = metal::RenderPassDescriptor::new();
243                    let color_attachment = render_pass_descriptor
244                        .color_attachments()
245                        .object_at(0)
246                        .unwrap();
247
248                    color_attachment.set_texture(Some(drawable_texture));
249                    color_attachment.set_load_action(MTLLoadAction::Load);
250                    color_attachment.set_store_action(MTLStoreAction::Store);
251
252                    let encoder =
253                        command_buffer.new_render_command_encoder(&render_pass_descriptor);
254
255                    // Invoke the callback
256                    (metal_view.render_callback)(
257                        &encoder,
258                        drawable_texture,
259                        context.bounds.into(),
260                        context.scale_factor,
261                    );
262
263                    encoder.end_encoding();
264                }
265
266                RenderCommand::EndRenderPass => {
267                    if let Some(encoder) = current_encoder.take() {
268                        encoder.end_encoding();
269                    }
270                }
271            }
272        }
273
274        // Ensure any remaining encoder is ended
275        if let Some(encoder) = current_encoder {
276            encoder.end_encoding();
277        }
278
279        Ok(())
280    }
281
282    /// Save the current render state
283    pub fn save_state(&self) -> RenderState {
284        self.current_state.clone()
285    }
286
287    /// Restore a previously saved render state
288    pub fn restore_state(&mut self, state: RenderState) {
289        self.current_state = state;
290    }
291}
292
293/// Builder for constructing render command lists
294pub struct RenderCommandBuilder<'a> {
295    commands: Vec<RenderCommand<'a>>,
296}
297
298impl<'a> RenderCommandBuilder<'a> {
299    pub fn new() -> Self {
300        Self {
301            commands: Vec::new(),
302        }
303    }
304
305    pub fn begin_render_pass(mut self, descriptor: RenderPassDescriptor) -> Self {
306        self.commands
307            .push(RenderCommand::BeginRenderPass { descriptor });
308        self
309    }
310
311    pub fn draw_primitives(
312        mut self,
313        batch: PrimitiveBatch<'a>,
314        viewport_size: Size<DevicePixels>,
315    ) -> Self {
316        self.commands.push(RenderCommand::DrawPrimitives {
317            batch,
318            viewport_size,
319        });
320        self
321    }
322
323    pub fn execute_metal_callback(
324        mut self,
325        metal_view: &'a PaintMetalView,
326        viewport_size: Size<DevicePixels>,
327    ) -> Self {
328        self.commands.push(RenderCommand::ExecuteMetalCallback {
329            metal_view,
330            viewport_size,
331        });
332        self
333    }
334
335    pub fn end_render_pass(mut self) -> Self {
336        self.commands.push(RenderCommand::EndRenderPass);
337        self
338    }
339
340    pub fn build(self) -> Vec<RenderCommand<'a>> {
341        self.commands
342    }
343}