we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement Metal device, command queue, and CAMetalLayer setup

Add Metal GPU framework bindings to the platform crate and integrate
a CAMetalLayer-backed view into the browser for GPU-accelerated rendering.

- metal.rs: FFI bindings for MTLDevice, MTLCommandQueue, MTLCommandBuffer,
MTLRenderCommandEncoder, CAMetalLayer, and render pass descriptor helpers
- MetalView in appkit.rs: custom NSView subclass (WeMetalView) using
CAMetalLayer with updateLayer for clear-to-color rendering
- Browser falls back to BitmapView when Metal is unavailable
- Double-buffered presentation via CAMetalLayer.nextDrawable
- 6 new Metal tests (device, queue, layer creation and configuration)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+709 -7
+36 -7
crates/browser/src/main.rs
··· 12 12 use we_layout::layout; 13 13 use we_platform::appkit; 14 14 use we_platform::cg::BitmapContext; 15 + use we_platform::metal::ClearColor; 15 16 use we_render::{Renderer, ScrollState}; 16 17 use we_style::computed::resolve_styles; 17 18 use we_text::font::{self, Font}; ··· 35 36 page: PageState, 36 37 font: Font, 37 38 bitmap: Box<BitmapContext>, 38 - view: appkit::BitmapView, 39 + view: ViewKind, 39 40 /// Page-level scroll offset (vertical). 40 41 page_scroll_y: f32, 41 42 /// Total content height from the last layout (for scroll clamping). 42 43 content_height: f32, 43 44 /// Per-element scroll offsets for overflow:scroll/auto containers. 44 45 scroll_offsets: HashMap<NodeId, (f32, f32)>, 46 + } 47 + 48 + /// The view kind: either Metal-backed or software bitmap. 49 + enum ViewKind { 50 + Metal(appkit::MetalView), 51 + Bitmap(appkit::BitmapView), 45 52 } 46 53 47 54 thread_local! { ··· 164 171 let max_scroll = (state.content_height - viewport_height).max(0.0); 165 172 state.page_scroll_y = state.page_scroll_y.clamp(0.0, max_scroll); 166 173 167 - // Swap in the new bitmap and update the view's pointer. 174 + // Swap in the new bitmap and update the view. 168 175 state.bitmap = new_bitmap; 169 - state.view.update_bitmap(&state.bitmap); 176 + match &state.view { 177 + ViewKind::Metal(metal_view) => { 178 + metal_view.update_drawable_size(width, height); 179 + metal_view.set_needs_display(); 180 + } 181 + ViewKind::Bitmap(bitmap_view) => { 182 + bitmap_view.update_bitmap(&state.bitmap); 183 + } 184 + } 170 185 }); 171 186 } 172 187 ··· 194 209 &state.scroll_offsets, 195 210 ); 196 211 state.content_height = content_height; 197 - state.view.set_needs_display(); 212 + match &state.view { 213 + ViewKind::Metal(metal_view) => metal_view.set_needs_display(), 214 + ViewKind::Bitmap(bitmap_view) => bitmap_view.set_needs_display(), 215 + } 198 216 }); 199 217 } 200 218 ··· 341 359 let scroll_offsets: HashMap<NodeId, (f32, f32)> = HashMap::new(); 342 360 let content_height = render_page(&page, &font, &mut bitmap, 0.0, &scroll_offsets); 343 361 344 - // Create the view backed by the rendered bitmap. 362 + // Create the view — try Metal first, fall back to software bitmap. 345 363 let frame = appkit::NSRect::new(0.0, 0.0, 800.0, 600.0); 346 - let view = appkit::BitmapView::new(frame, &bitmap); 347 - window.set_content_view(&view.id()); 364 + let clear_color = ClearColor::new(1.0, 1.0, 1.0, 1.0); // white background 365 + let view = match appkit::MetalView::new(frame, clear_color) { 366 + Some(metal_view) => { 367 + window.set_content_view(&metal_view.id()); 368 + ViewKind::Metal(metal_view) 369 + } 370 + None => { 371 + eprintln!("Metal not available, falling back to software rendering"); 372 + let bitmap_view = appkit::BitmapView::new(frame, &bitmap); 373 + window.set_content_view(&bitmap_view.id()); 374 + ViewKind::Bitmap(bitmap_view) 375 + } 376 + }; 348 377 349 378 // Store state for the resize handler. 350 379 STATE.with(|state| {
+328
crates/platform/src/appkit.rs
··· 10 10 11 11 use crate::cf::CfString; 12 12 use crate::cg::{self, BitmapContext, CGRect}; 13 + use crate::metal::{self, ClearColor, CommandQueue, Device, MetalLayer}; 13 14 use crate::objc::{Class, Id, Imp, Sel}; 14 15 use crate::{class, msg_send}; 15 16 use std::ffi::CStr; ··· 497 498 /// 498 499 /// Call this after modifying the bitmap context's pixels to 499 500 /// schedule a redraw. 501 + pub fn set_needs_display(&self) { 502 + let _: *mut c_void = msg_send![self.view.as_ptr(), setNeedsDisplay: true]; 503 + } 504 + 505 + /// Return the underlying Objective-C view object. 506 + pub fn id(&self) -> Id { 507 + self.view 508 + } 509 + } 510 + 511 + // --------------------------------------------------------------------------- 512 + // MetalView — NSView backed by a CAMetalLayer for GPU rendering 513 + // --------------------------------------------------------------------------- 514 + 515 + /// Ivar name for storing the MetalViewState pointer in WeMetalView. 516 + const METAL_STATE_IVAR: &CStr = c"_metalState"; 517 + 518 + /// Internal state for MetalView, stored as an ivar pointer. 519 + struct MetalViewState { 520 + _device: Device, 521 + queue: CommandQueue, 522 + layer: MetalLayer, 523 + clear_color: ClearColor, 524 + } 525 + 526 + /// Register the `WeMetalView` custom NSView subclass. 527 + /// 528 + /// The class overrides `makeBackingLayer` to provide a CAMetalLayer and 529 + /// `wantsUpdateLayer` to skip drawRect:-based rendering. Event handlers 530 + /// are shared with WeView. 531 + fn register_we_metal_view_class() { 532 + if class!("WeMetalView").is_some() { 533 + return; 534 + } 535 + 536 + let superclass = class!("NSView").expect("NSView not found"); 537 + let view_class = Class::allocate(superclass, c"WeMetalView", 0) 538 + .expect("failed to allocate WeMetalView class"); 539 + 540 + // Ivar for the MetalViewState pointer. 541 + view_class.add_ivar(METAL_STATE_IVAR, 8, 3, c"^v"); 542 + 543 + // wantsLayer -> YES 544 + extern "C" fn wants_layer(_this: *mut c_void, _sel: *mut c_void) -> bool { 545 + true 546 + } 547 + 548 + let sel = Sel::register(c"wantsLayer"); 549 + view_class.add_method( 550 + sel, 551 + unsafe { std::mem::transmute::<*const (), Imp>(wants_layer as *const ()) }, 552 + c"B@:", 553 + ); 554 + 555 + // wantsUpdateLayer -> YES (use updateLayer instead of drawRect:) 556 + extern "C" fn wants_update_layer(_this: *mut c_void, _sel: *mut c_void) -> bool { 557 + true 558 + } 559 + 560 + let sel = Sel::register(c"wantsUpdateLayer"); 561 + view_class.add_method( 562 + sel, 563 + unsafe { std::mem::transmute::<*const (), Imp>(wants_update_layer as *const ()) }, 564 + c"B@:", 565 + ); 566 + 567 + // makeBackingLayer -> CAMetalLayer 568 + extern "C" fn make_backing_layer(this: *mut c_void, _sel: *mut c_void) -> *mut c_void { 569 + let this_id = match unsafe { Id::from_raw(this as *mut _) } { 570 + Some(id) => id, 571 + None => return std::ptr::null_mut(), 572 + }; 573 + 574 + let state_ptr = unsafe { this_id.get_ivar(METAL_STATE_IVAR) }; 575 + if state_ptr.is_null() { 576 + return std::ptr::null_mut(); 577 + } 578 + let state = unsafe { &*(state_ptr as *const MetalViewState) }; 579 + state.layer.id().as_ptr() as *mut c_void 580 + } 581 + 582 + let sel = Sel::register(c"makeBackingLayer"); 583 + view_class.add_method( 584 + sel, 585 + unsafe { std::mem::transmute::<*const (), Imp>(make_backing_layer as *const ()) }, 586 + c"@@:", 587 + ); 588 + 589 + // updateLayer — render a clear-to-color pass via Metal 590 + extern "C" fn update_layer(this: *mut c_void, _sel: *mut c_void) { 591 + let this_id = match unsafe { Id::from_raw(this as *mut _) } { 592 + Some(id) => id, 593 + None => return, 594 + }; 595 + 596 + let state_ptr = unsafe { this_id.get_ivar(METAL_STATE_IVAR) }; 597 + if state_ptr.is_null() { 598 + return; 599 + } 600 + let state = unsafe { &*(state_ptr as *const MetalViewState) }; 601 + 602 + // Get drawable 603 + let drawable = match state.layer.next_drawable() { 604 + Some(d) => d, 605 + None => return, 606 + }; 607 + 608 + // Get drawable texture 609 + let texture = match metal::drawable_texture(drawable) { 610 + Some(t) => t, 611 + None => return, 612 + }; 613 + 614 + // Create render pass descriptor 615 + let desc = match metal::make_clear_pass_descriptor(texture, state.clear_color) { 616 + Some(d) => d, 617 + None => return, 618 + }; 619 + 620 + // Create command buffer 621 + let cmd_buf = match state.queue.command_buffer() { 622 + Some(b) => b, 623 + None => return, 624 + }; 625 + 626 + // Create render encoder, immediately end (clear-only pass) 627 + if let Some(encoder) = cmd_buf.render_command_encoder(desc) { 628 + encoder.end_encoding(); 629 + } 630 + 631 + // Present and commit 632 + cmd_buf.present_drawable(drawable); 633 + cmd_buf.commit(); 634 + } 635 + 636 + let sel = Sel::register(c"updateLayer"); 637 + view_class.add_method( 638 + sel, 639 + unsafe { std::mem::transmute::<*const (), Imp>(update_layer as *const ()) }, 640 + c"v@:", 641 + ); 642 + 643 + // isFlipped -> YES (top-left origin) 644 + extern "C" fn is_flipped(_this: *mut c_void, _sel: *mut c_void) -> bool { 645 + true 646 + } 647 + 648 + let sel = Sel::register(c"isFlipped"); 649 + view_class.add_method( 650 + sel, 651 + unsafe { std::mem::transmute::<*const (), Imp>(is_flipped as *const ()) }, 652 + c"B@:", 653 + ); 654 + 655 + // acceptsFirstResponder -> YES 656 + extern "C" fn accepts_first_responder(_this: *mut c_void, _sel: *mut c_void) -> bool { 657 + true 658 + } 659 + 660 + let sel = Sel::register(c"acceptsFirstResponder"); 661 + view_class.add_method( 662 + sel, 663 + unsafe { std::mem::transmute::<*const (), Imp>(accepts_first_responder as *const ()) }, 664 + c"B@:", 665 + ); 666 + 667 + // keyDown: 668 + extern "C" fn key_down(_this: *mut c_void, _sel: *mut c_void, event: *mut c_void) { 669 + let chars: *mut c_void = msg_send![event, characters]; 670 + if chars.is_null() { 671 + return; 672 + } 673 + let utf8: *const c_char = msg_send![chars, UTF8String]; 674 + if utf8.is_null() { 675 + return; 676 + } 677 + let c_str = unsafe { CStr::from_ptr(utf8) }; 678 + let key_code: u16 = msg_send![event, keyCode]; 679 + if let Ok(s) = c_str.to_str() { 680 + println!("keyDown: '{}' (keyCode: {})", s, key_code); 681 + } 682 + } 683 + 684 + let sel = Sel::register(c"keyDown:"); 685 + view_class.add_method( 686 + sel, 687 + unsafe { std::mem::transmute::<*const (), Imp>(key_down as *const ()) }, 688 + c"v@:@", 689 + ); 690 + 691 + // mouseDown: 692 + extern "C" fn mouse_down(this: *mut c_void, _sel: *mut c_void, event: *mut c_void) { 693 + let raw_loc: NSPoint = msg_send![event, locationInWindow]; 694 + let loc: NSPoint = 695 + msg_send![this, convertPoint: raw_loc, fromView: std::ptr::null_mut::<c_void>()]; 696 + println!("mouseDown: ({:.1}, {:.1})", loc.x, loc.y); 697 + } 698 + 699 + let sel = Sel::register(c"mouseDown:"); 700 + view_class.add_method( 701 + sel, 702 + unsafe { std::mem::transmute::<*const (), Imp>(mouse_down as *const ()) }, 703 + c"v@:@", 704 + ); 705 + 706 + // mouseUp: 707 + extern "C" fn mouse_up(this: *mut c_void, _sel: *mut c_void, event: *mut c_void) { 708 + let raw_loc: NSPoint = msg_send![event, locationInWindow]; 709 + let loc: NSPoint = 710 + msg_send![this, convertPoint: raw_loc, fromView: std::ptr::null_mut::<c_void>()]; 711 + println!("mouseUp: ({:.1}, {:.1})", loc.x, loc.y); 712 + } 713 + 714 + let sel = Sel::register(c"mouseUp:"); 715 + view_class.add_method( 716 + sel, 717 + unsafe { std::mem::transmute::<*const (), Imp>(mouse_up as *const ()) }, 718 + c"v@:@", 719 + ); 720 + 721 + // mouseMoved: 722 + extern "C" fn mouse_moved(this: *mut c_void, _sel: *mut c_void, event: *mut c_void) { 723 + let raw_loc: NSPoint = msg_send![event, locationInWindow]; 724 + let loc: NSPoint = 725 + msg_send![this, convertPoint: raw_loc, fromView: std::ptr::null_mut::<c_void>()]; 726 + println!("mouseMoved: ({:.1}, {:.1})", loc.x, loc.y); 727 + } 728 + 729 + let sel = Sel::register(c"mouseMoved:"); 730 + view_class.add_method( 731 + sel, 732 + unsafe { std::mem::transmute::<*const (), Imp>(mouse_moved as *const ()) }, 733 + c"v@:@", 734 + ); 735 + 736 + // scrollWheel: 737 + extern "C" fn scroll_wheel(this: *mut c_void, _sel: *mut c_void, event: *mut c_void) { 738 + let dx: f64 = msg_send![event, scrollingDeltaX]; 739 + let dy: f64 = msg_send![event, scrollingDeltaY]; 740 + let raw_loc: NSPoint = msg_send![event, locationInWindow]; 741 + let loc: NSPoint = 742 + msg_send![this, convertPoint: raw_loc, fromView: std::ptr::null_mut::<c_void>()]; 743 + unsafe { 744 + if let Some(handler) = SCROLL_HANDLER { 745 + handler(dx, dy, loc.x, loc.y); 746 + } 747 + } 748 + } 749 + 750 + let sel = Sel::register(c"scrollWheel:"); 751 + view_class.add_method( 752 + sel, 753 + unsafe { std::mem::transmute::<*const (), Imp>(scroll_wheel as *const ()) }, 754 + c"v@:@", 755 + ); 756 + 757 + view_class.register(); 758 + } 759 + 760 + /// A Metal-backed NSView using `CAMetalLayer` for GPU rendering. 761 + /// 762 + /// Renders by clearing to a configurable background color. The Metal 763 + /// device, command queue, and layer are owned by the view's internal state. 764 + pub struct MetalView { 765 + view: Id, 766 + /// Boxed state kept alive for the view's ivar pointer. 767 + _state: Box<MetalViewState>, 768 + } 769 + 770 + impl MetalView { 771 + /// Create a new `MetalView` with the given frame and clear color. 772 + /// 773 + /// Returns `None` if Metal is not available on this system. 774 + pub fn new(frame: NSRect, clear_color: ClearColor) -> Option<MetalView> { 775 + register_we_metal_view_class(); 776 + 777 + let device = Device::system_default()?; 778 + let queue = device.new_command_queue()?; 779 + let layer = MetalLayer::new()?; 780 + 781 + layer.set_device(&device); 782 + layer.set_pixel_format(metal::MTL_PIXEL_FORMAT_BGRA8_UNORM); 783 + layer.set_framebuffer_only(true); 784 + 785 + // Set initial drawable size (points × scale factor will be updated on 786 + // first display, but set a sensible default). 787 + layer.set_drawable_size(frame.size.width, frame.size.height); 788 + 789 + let state = Box::new(MetalViewState { 790 + _device: device, 791 + queue, 792 + layer, 793 + clear_color, 794 + }); 795 + 796 + let cls = class!("WeMetalView").expect("WeMetalView class not found"); 797 + let view: *mut c_void = msg_send![cls.as_ptr(), alloc]; 798 + 799 + // Set the state ivar BEFORE initWithFrame so makeBackingLayer can read it. 800 + let view_id = 801 + unsafe { Id::from_raw(view as *mut _) }.expect("WeMetalView alloc returned nil"); 802 + unsafe { 803 + view_id.set_ivar( 804 + METAL_STATE_IVAR, 805 + &*state as *const MetalViewState as *mut c_void, 806 + ); 807 + } 808 + 809 + let view: *mut c_void = msg_send![view_id.as_ptr(), initWithFrame: frame]; 810 + let view_id = 811 + unsafe { Id::from_raw(view as *mut _) }.expect("WeMetalView initWithFrame failed"); 812 + 813 + Some(MetalView { 814 + view: view_id, 815 + _state: state, 816 + }) 817 + } 818 + 819 + /// Update the `CAMetalLayer` drawable size to match the view dimensions. 820 + /// 821 + /// Call this when the window is resized. The width and height should be 822 + /// in pixels (points × backing scale factor). 823 + pub fn update_drawable_size(&self, width: f64, height: f64) { 824 + self._state.layer.set_drawable_size(width, height); 825 + } 826 + 827 + /// Request the view to redraw (triggers `updateLayer`). 500 828 pub fn set_needs_display(&self) { 501 829 let _: *mut c_void = msg_send![self.view.as_ptr(), setNeedsDisplay: true]; 502 830 }
+1
crates/platform/src/lib.rs
··· 3 3 pub mod appkit; 4 4 pub mod cf; 5 5 pub mod cg; 6 + pub mod metal; 6 7 pub mod objc;
+344
crates/platform/src/metal.rs
··· 1 + //! Metal GPU framework FFI bindings for macOS. 2 + //! 3 + //! Provides minimal wrappers around Metal device, command queue, and 4 + //! CAMetalLayer for GPU-accelerated rendering. 5 + //! 6 + //! # Safety 7 + //! 8 + //! This module contains `unsafe` code for FFI with Metal.framework and 9 + //! QuartzCore.framework. The `platform` crate is one of the few crates 10 + //! where `unsafe` is permitted. 11 + 12 + use crate::objc::Id; 13 + use crate::{class, msg_send}; 14 + use std::os::raw::c_void; 15 + 16 + // --------------------------------------------------------------------------- 17 + // Framework links 18 + // --------------------------------------------------------------------------- 19 + 20 + #[link(name = "Metal", kind = "framework")] 21 + extern "C" { 22 + fn MTLCreateSystemDefaultDevice() -> *mut c_void; 23 + } 24 + 25 + #[link(name = "QuartzCore", kind = "framework")] 26 + extern "C" {} 27 + 28 + // --------------------------------------------------------------------------- 29 + // Metal pixel format constants 30 + // --------------------------------------------------------------------------- 31 + 32 + /// `MTLPixelFormatBGRA8Unorm` — 8-bit BGRA, unsigned normalized. 33 + pub const MTL_PIXEL_FORMAT_BGRA8_UNORM: u64 = 80; 34 + 35 + // --------------------------------------------------------------------------- 36 + // MTLLoadAction / MTLStoreAction constants 37 + // --------------------------------------------------------------------------- 38 + 39 + /// `MTLLoadActionClear` — clear the attachment at the start of a render pass. 40 + const MTL_LOAD_ACTION_CLEAR: u64 = 2; 41 + 42 + /// `MTLStoreActionStore` — store the rendered contents. 43 + const MTL_STORE_ACTION_STORE: u64 = 1; 44 + 45 + // --------------------------------------------------------------------------- 46 + // MTLClearColor 47 + // --------------------------------------------------------------------------- 48 + 49 + /// `MTLClearColor` — RGBA clear color for render pass attachments. 50 + #[repr(C)] 51 + #[derive(Debug, Clone, Copy)] 52 + pub struct ClearColor { 53 + pub red: f64, 54 + pub green: f64, 55 + pub blue: f64, 56 + pub alpha: f64, 57 + } 58 + 59 + impl ClearColor { 60 + pub fn new(red: f64, green: f64, blue: f64, alpha: f64) -> ClearColor { 61 + ClearColor { 62 + red, 63 + green, 64 + blue, 65 + alpha, 66 + } 67 + } 68 + } 69 + 70 + // --------------------------------------------------------------------------- 71 + // CGSize (needed for CAMetalLayer drawable size) 72 + // --------------------------------------------------------------------------- 73 + 74 + /// `CGSize` for setting `CAMetalLayer.drawableSize`. 75 + #[repr(C)] 76 + #[derive(Debug, Clone, Copy)] 77 + pub struct MetalSize { 78 + pub width: f64, 79 + pub height: f64, 80 + } 81 + 82 + // --------------------------------------------------------------------------- 83 + // Device — wraps id<MTLDevice> 84 + // --------------------------------------------------------------------------- 85 + 86 + /// Wrapper around `id<MTLDevice>`. 87 + pub struct Device { 88 + id: Id, 89 + } 90 + 91 + impl Device { 92 + /// Obtain the system default Metal GPU device. 93 + /// 94 + /// Returns `None` if no Metal-capable GPU is available. 95 + pub fn system_default() -> Option<Device> { 96 + let ptr = unsafe { MTLCreateSystemDefaultDevice() }; 97 + let id = unsafe { Id::from_raw(ptr as *mut _) }?; 98 + Some(Device { id }) 99 + } 100 + 101 + /// Create a new command queue on this device. 102 + pub fn new_command_queue(&self) -> Option<CommandQueue> { 103 + let queue: *mut c_void = msg_send![self.id.as_ptr(), newCommandQueue]; 104 + let id = unsafe { Id::from_raw(queue as *mut _) }?; 105 + Some(CommandQueue { id }) 106 + } 107 + 108 + /// Return the underlying Objective-C object. 109 + pub fn id(&self) -> Id { 110 + self.id 111 + } 112 + } 113 + 114 + // --------------------------------------------------------------------------- 115 + // CommandQueue — wraps id<MTLCommandQueue> 116 + // --------------------------------------------------------------------------- 117 + 118 + /// Wrapper around `id<MTLCommandQueue>`. 119 + pub struct CommandQueue { 120 + id: Id, 121 + } 122 + 123 + impl CommandQueue { 124 + /// Create a new command buffer from this queue. 125 + pub fn command_buffer(&self) -> Option<CommandBuffer> { 126 + let buf: *mut c_void = msg_send![self.id.as_ptr(), commandBuffer]; 127 + let id = unsafe { Id::from_raw(buf as *mut _) }?; 128 + Some(CommandBuffer { id }) 129 + } 130 + } 131 + 132 + // --------------------------------------------------------------------------- 133 + // CommandBuffer — wraps id<MTLCommandBuffer> 134 + // --------------------------------------------------------------------------- 135 + 136 + /// Wrapper around `id<MTLCommandBuffer>`. 137 + pub struct CommandBuffer { 138 + id: Id, 139 + } 140 + 141 + impl CommandBuffer { 142 + /// Create a render command encoder with the given descriptor. 143 + pub fn render_command_encoder(&self, descriptor: Id) -> Option<RenderCommandEncoder> { 144 + let encoder: *mut c_void = 145 + msg_send![self.id.as_ptr(), renderCommandEncoderWithDescriptor: descriptor.as_ptr()]; 146 + let id = unsafe { Id::from_raw(encoder as *mut _) }?; 147 + Some(RenderCommandEncoder { id }) 148 + } 149 + 150 + /// Schedule presentation of a drawable. 151 + pub fn present_drawable(&self, drawable: Id) { 152 + let _: *mut c_void = msg_send![self.id.as_ptr(), presentDrawable: drawable.as_ptr()]; 153 + } 154 + 155 + /// Commit the command buffer for execution. 156 + pub fn commit(&self) { 157 + let _: *mut c_void = msg_send![self.id.as_ptr(), commit]; 158 + } 159 + } 160 + 161 + // --------------------------------------------------------------------------- 162 + // RenderCommandEncoder — wraps id<MTLRenderCommandEncoder> 163 + // --------------------------------------------------------------------------- 164 + 165 + /// Wrapper around `id<MTLRenderCommandEncoder>`. 166 + pub struct RenderCommandEncoder { 167 + id: Id, 168 + } 169 + 170 + impl RenderCommandEncoder { 171 + /// End encoding commands. 172 + pub fn end_encoding(&self) { 173 + let _: *mut c_void = msg_send![self.id.as_ptr(), endEncoding]; 174 + } 175 + } 176 + 177 + // --------------------------------------------------------------------------- 178 + // MetalLayer — wraps CAMetalLayer 179 + // --------------------------------------------------------------------------- 180 + 181 + /// Wrapper around `CAMetalLayer`. 182 + pub struct MetalLayer { 183 + id: Id, 184 + } 185 + 186 + impl MetalLayer { 187 + /// Create a new `CAMetalLayer`. 188 + pub fn new() -> Option<MetalLayer> { 189 + let cls = class!("CAMetalLayer")?; 190 + let layer: *mut c_void = msg_send![cls.as_ptr(), alloc]; 191 + let layer: *mut c_void = msg_send![layer, init]; 192 + let id = unsafe { Id::from_raw(layer as *mut _) }?; 193 + Some(MetalLayer { id }) 194 + } 195 + 196 + /// Set the Metal device for this layer. 197 + pub fn set_device(&self, device: &Device) { 198 + let _: *mut c_void = msg_send![self.id.as_ptr(), setDevice: device.id().as_ptr()]; 199 + } 200 + 201 + /// Set the pixel format. 202 + pub fn set_pixel_format(&self, format: u64) { 203 + let _: *mut c_void = msg_send![self.id.as_ptr(), setPixelFormat: format]; 204 + } 205 + 206 + /// Set whether the layer's textures are for framebuffer use only. 207 + pub fn set_framebuffer_only(&self, val: bool) { 208 + let _: *mut c_void = msg_send![self.id.as_ptr(), setFramebufferOnly: val]; 209 + } 210 + 211 + /// Set the drawable size in pixels. 212 + pub fn set_drawable_size(&self, width: f64, height: f64) { 213 + let size = MetalSize { width, height }; 214 + let _: *mut c_void = msg_send![self.id.as_ptr(), setDrawableSize: size]; 215 + } 216 + 217 + /// Get the next drawable from the layer. 218 + /// 219 + /// Returns the `CAMetalDrawable` object, or `None` if no drawable is available. 220 + pub fn next_drawable(&self) -> Option<Id> { 221 + let drawable: *mut c_void = msg_send![self.id.as_ptr(), nextDrawable]; 222 + unsafe { Id::from_raw(drawable as *mut _) } 223 + } 224 + 225 + /// Return the underlying Objective-C object. 226 + pub fn id(&self) -> Id { 227 + self.id 228 + } 229 + } 230 + 231 + // --------------------------------------------------------------------------- 232 + // Render pass descriptor helpers 233 + // --------------------------------------------------------------------------- 234 + 235 + /// Create a `MTLRenderPassDescriptor` configured to clear to the given color. 236 + /// 237 + /// Sets up color attachment 0 with: 238 + /// - `texture` from the drawable 239 + /// - `loadAction = MTLLoadActionClear` 240 + /// - `storeAction = MTLStoreActionStore` 241 + /// - `clearColor` as specified 242 + pub fn make_clear_pass_descriptor(drawable_texture: Id, clear_color: ClearColor) -> Option<Id> { 243 + let cls = class!("MTLRenderPassDescriptor")?; 244 + let desc: *mut c_void = msg_send![cls.as_ptr(), renderPassDescriptor]; 245 + let desc_id = unsafe { Id::from_raw(desc as *mut _) }?; 246 + 247 + // Get colorAttachments[0] 248 + let attachments: *mut c_void = msg_send![desc_id.as_ptr(), colorAttachments]; 249 + let attachment: *mut c_void = msg_send![attachments, objectAtIndexedSubscript: 0u64]; 250 + 251 + // Set texture 252 + let _: *mut c_void = msg_send![attachment, setTexture: drawable_texture.as_ptr()]; 253 + 254 + // Set load action = Clear 255 + let _: *mut c_void = msg_send![attachment, setLoadAction: MTL_LOAD_ACTION_CLEAR]; 256 + 257 + // Set store action = Store 258 + let _: *mut c_void = msg_send![attachment, setStoreAction: MTL_STORE_ACTION_STORE]; 259 + 260 + // Set clear color 261 + let _: *mut c_void = msg_send![attachment, setClearColor: clear_color]; 262 + 263 + Some(desc_id) 264 + } 265 + 266 + /// Get the `texture` property of a `CAMetalDrawable`. 267 + pub fn drawable_texture(drawable: Id) -> Option<Id> { 268 + let texture: *mut c_void = msg_send![drawable.as_ptr(), texture]; 269 + unsafe { Id::from_raw(texture as *mut _) } 270 + } 271 + 272 + // --------------------------------------------------------------------------- 273 + // Tests 274 + // --------------------------------------------------------------------------- 275 + 276 + #[cfg(test)] 277 + mod tests { 278 + use super::*; 279 + 280 + #[test] 281 + fn clear_color_new() { 282 + let c = ClearColor::new(0.1, 0.2, 0.3, 1.0); 283 + assert_eq!(c.red, 0.1); 284 + assert_eq!(c.green, 0.2); 285 + assert_eq!(c.blue, 0.3); 286 + assert_eq!(c.alpha, 1.0); 287 + } 288 + 289 + #[test] 290 + fn metal_size_layout() { 291 + let s = MetalSize { 292 + width: 800.0, 293 + height: 600.0, 294 + }; 295 + assert_eq!(s.width, 800.0); 296 + assert_eq!(s.height, 600.0); 297 + } 298 + 299 + #[test] 300 + fn pixel_format_constant() { 301 + assert_eq!(MTL_PIXEL_FORMAT_BGRA8_UNORM, 80); 302 + } 303 + 304 + #[test] 305 + fn system_default_device() { 306 + // On a Mac with Metal support, this should succeed. 307 + // On CI without GPU, it may return None — that's OK. 308 + let device = Device::system_default(); 309 + if let Some(device) = device { 310 + assert!(!device.id().as_ptr().is_null()); 311 + } 312 + } 313 + 314 + #[test] 315 + fn create_command_queue() { 316 + let device = match Device::system_default() { 317 + Some(d) => d, 318 + None => return, // No GPU available 319 + }; 320 + let queue = device.new_command_queue(); 321 + assert!(queue.is_some()); 322 + } 323 + 324 + #[test] 325 + fn metal_layer_create() { 326 + let _pool = crate::appkit::AutoreleasePool::new(); 327 + let layer = MetalLayer::new(); 328 + assert!(layer.is_some()); 329 + } 330 + 331 + #[test] 332 + fn metal_layer_configure() { 333 + let _pool = crate::appkit::AutoreleasePool::new(); 334 + let device = match Device::system_default() { 335 + Some(d) => d, 336 + None => return, 337 + }; 338 + let layer = MetalLayer::new().expect("CAMetalLayer should be available"); 339 + layer.set_device(&device); 340 + layer.set_pixel_format(MTL_PIXEL_FORMAT_BGRA8_UNORM); 341 + layer.set_framebuffer_only(true); 342 + layer.set_drawable_size(800.0, 600.0); 343 + } 344 + }