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 render pipeline: 2D shaders and vertex format

Add MSL shaders (vertex + fragment) for 2D quad rendering with pixel-to-NDC
coordinate transform and dual-mode fragment shader (solid color + textured).
Create MTLRenderPipelineState with alpha blending, define Vertex/Uniforms/Quad
structs, and implement MetalRenderer for batched draw call encoding.

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

+801
+801
crates/platform/src/metal.rs
··· 9 9 //! QuartzCore.framework. The `platform` crate is one of the few crates 10 10 //! where `unsafe` is permitted. 11 11 12 + use crate::cf::CfString; 12 13 use crate::objc::Id; 13 14 use crate::{class, msg_send}; 14 15 use std::os::raw::c_void; ··· 80 81 } 81 82 82 83 // --------------------------------------------------------------------------- 84 + // MTLPrimitiveType constants 85 + // --------------------------------------------------------------------------- 86 + 87 + /// `MTLPrimitiveTypeTriangle` — render triangles from triples of vertices. 88 + const MTL_PRIMITIVE_TYPE_TRIANGLE: u64 = 3; 89 + 90 + // --------------------------------------------------------------------------- 91 + // MTLBlendFactor constants 92 + // --------------------------------------------------------------------------- 93 + 94 + /// `MTLBlendFactorSourceAlpha` 95 + const MTL_BLEND_FACTOR_SOURCE_ALPHA: u64 = 4; 96 + 97 + /// `MTLBlendFactorOneMinusSourceAlpha` 98 + const MTL_BLEND_FACTOR_ONE_MINUS_SOURCE_ALPHA: u64 = 5; 99 + 100 + // --------------------------------------------------------------------------- 101 + // MTLRegion (for texture upload) 102 + // --------------------------------------------------------------------------- 103 + 104 + #[repr(C)] 105 + #[derive(Clone, Copy)] 106 + struct MtlOrigin { 107 + x: u64, 108 + y: u64, 109 + z: u64, 110 + } 111 + 112 + #[repr(C)] 113 + #[derive(Clone, Copy)] 114 + struct MtlSize { 115 + width: u64, 116 + height: u64, 117 + depth: u64, 118 + } 119 + 120 + #[repr(C)] 121 + #[derive(Clone, Copy)] 122 + struct MtlRegion { 123 + origin: MtlOrigin, 124 + size: MtlSize, 125 + } 126 + 127 + // --------------------------------------------------------------------------- 128 + // Vertex format for 2D rendering 129 + // --------------------------------------------------------------------------- 130 + 131 + /// A vertex for 2D quad rendering via the Metal pipeline. 132 + /// 133 + /// Layout matches the MSL `Vertex` struct (packed floats, 36 bytes total). 134 + #[repr(C)] 135 + #[derive(Debug, Clone, Copy)] 136 + pub struct Vertex { 137 + /// Position in pixel coordinates. 138 + pub position: [f32; 2], 139 + /// RGBA color (0.0–1.0). 140 + pub color: [f32; 4], 141 + /// UV texture coordinates (unused for solid-color quads). 142 + pub tex_coord: [f32; 2], 143 + /// 0.0 = solid color, 1.0 = sample from texture. 144 + pub use_texture: f32, 145 + } 146 + 147 + /// Uniform data passed to the vertex shader. 148 + #[repr(C)] 149 + #[derive(Debug, Clone, Copy)] 150 + pub struct Uniforms { 151 + /// Viewport dimensions in pixels (width, height). 152 + pub viewport_size: [f32; 2], 153 + } 154 + 155 + /// A 2D axis-aligned rectangle for batched rendering. 156 + #[derive(Debug, Clone, Copy)] 157 + pub struct Quad { 158 + /// X position in pixels. 159 + pub x: f32, 160 + /// Y position in pixels. 161 + pub y: f32, 162 + /// Width in pixels. 163 + pub width: f32, 164 + /// Height in pixels. 165 + pub height: f32, 166 + /// RGBA color (0.0–1.0). 167 + pub color: [f32; 4], 168 + } 169 + 170 + impl Quad { 171 + /// Decompose this quad into 6 vertices (2 triangles) and append to `out`. 172 + pub fn triangulate(&self, out: &mut Vec<Vertex>) { 173 + let x0 = self.x; 174 + let y0 = self.y; 175 + let x1 = self.x + self.width; 176 + let y1 = self.y + self.height; 177 + let c = self.color; 178 + let tc = [0.0, 0.0]; 179 + let ut = 0.0; 180 + 181 + // Triangle 1: top-left, top-right, bottom-left 182 + out.push(Vertex { 183 + position: [x0, y0], 184 + color: c, 185 + tex_coord: tc, 186 + use_texture: ut, 187 + }); 188 + out.push(Vertex { 189 + position: [x1, y0], 190 + color: c, 191 + tex_coord: tc, 192 + use_texture: ut, 193 + }); 194 + out.push(Vertex { 195 + position: [x0, y1], 196 + color: c, 197 + tex_coord: tc, 198 + use_texture: ut, 199 + }); 200 + 201 + // Triangle 2: top-right, bottom-right, bottom-left 202 + out.push(Vertex { 203 + position: [x1, y0], 204 + color: c, 205 + tex_coord: tc, 206 + use_texture: ut, 207 + }); 208 + out.push(Vertex { 209 + position: [x1, y1], 210 + color: c, 211 + tex_coord: tc, 212 + use_texture: ut, 213 + }); 214 + out.push(Vertex { 215 + position: [x0, y1], 216 + color: c, 217 + tex_coord: tc, 218 + use_texture: ut, 219 + }); 220 + } 221 + } 222 + 223 + // --------------------------------------------------------------------------- 224 + // MSL shader source 225 + // --------------------------------------------------------------------------- 226 + 227 + /// Metal Shading Language source for the 2D rendering pipeline. 228 + /// 229 + /// Vertex shader: converts pixel coordinates to clip space using a viewport 230 + /// uniform. Fragment shader: solid-color fill or textured sampling with 231 + /// color tint (selected per-vertex via `use_texture`). 232 + const SHADER_SOURCE: &str = r#" 233 + #include <metal_stdlib> 234 + using namespace metal; 235 + 236 + struct Vertex { 237 + packed_float2 position; 238 + packed_float4 color; 239 + packed_float2 tex_coord; 240 + float use_texture; 241 + }; 242 + 243 + struct Uniforms { 244 + float2 viewport_size; 245 + }; 246 + 247 + struct VertexOut { 248 + float4 position [[position]]; 249 + float4 color; 250 + float2 tex_coord; 251 + float use_texture; 252 + }; 253 + 254 + vertex VertexOut vertex_main( 255 + const device Vertex* vertices [[buffer(0)]], 256 + constant Uniforms& uniforms [[buffer(1)]], 257 + uint vid [[vertex_id]] 258 + ) { 259 + VertexOut out; 260 + float2 pos = float2(vertices[vid].position); 261 + out.position = float4( 262 + (pos.x / uniforms.viewport_size.x) * 2.0 - 1.0, 263 + 1.0 - (pos.y / uniforms.viewport_size.y) * 2.0, 264 + 0.0, 265 + 1.0 266 + ); 267 + out.color = float4(vertices[vid].color); 268 + out.tex_coord = float2(vertices[vid].tex_coord); 269 + out.use_texture = vertices[vid].use_texture; 270 + return out; 271 + } 272 + 273 + fragment float4 fragment_main( 274 + VertexOut in [[stage_in]], 275 + texture2d<float> tex [[texture(0)]], 276 + sampler samp [[sampler(0)]] 277 + ) { 278 + if (in.use_texture > 0.5) { 279 + float4 tex_color = tex.sample(samp, in.tex_coord); 280 + return tex_color * in.color; 281 + } 282 + return in.color; 283 + } 284 + "#; 285 + 286 + // --------------------------------------------------------------------------- 287 + // Buffer — wraps id<MTLBuffer> 288 + // --------------------------------------------------------------------------- 289 + 290 + /// Wrapper around `id<MTLBuffer>`. 291 + pub struct Buffer { 292 + id: Id, 293 + length: usize, 294 + } 295 + 296 + impl Buffer { 297 + /// Return the underlying Objective-C object. 298 + pub fn id(&self) -> Id { 299 + self.id 300 + } 301 + 302 + /// Return the buffer size in bytes. 303 + pub fn length(&self) -> usize { 304 + self.length 305 + } 306 + } 307 + 308 + // --------------------------------------------------------------------------- 309 + // RenderPipeline — wraps id<MTLRenderPipelineState> 310 + // --------------------------------------------------------------------------- 311 + 312 + /// Wrapper around `id<MTLRenderPipelineState>`. 313 + pub struct RenderPipeline { 314 + id: Id, 315 + } 316 + 317 + // --------------------------------------------------------------------------- 318 + // Texture — wraps id<MTLTexture> 319 + // --------------------------------------------------------------------------- 320 + 321 + /// Wrapper around `id<MTLTexture>`. 322 + pub struct Texture { 323 + id: Id, 324 + } 325 + 326 + // --------------------------------------------------------------------------- 327 + // SamplerState — wraps id<MTLSamplerState> 328 + // --------------------------------------------------------------------------- 329 + 330 + /// Wrapper around `id<MTLSamplerState>`. 331 + pub struct SamplerState { 332 + id: Id, 333 + } 334 + 335 + // --------------------------------------------------------------------------- 83 336 // Device — wraps id<MTLDevice> 84 337 // --------------------------------------------------------------------------- 85 338 ··· 109 362 pub fn id(&self) -> Id { 110 363 self.id 111 364 } 365 + 366 + /// Compile MSL source into a library. 367 + pub fn new_library_with_source(&self, source: &str) -> Option<Id> { 368 + let ns_source = CfString::new(source)?; 369 + let mut error_ptr: *mut c_void = std::ptr::null_mut(); 370 + let library: *mut c_void = msg_send![ 371 + self.id.as_ptr(), 372 + newLibraryWithSource: ns_source.as_void_ptr() as *mut c_void, 373 + options: std::ptr::null_mut::<c_void>(), 374 + error: &mut error_ptr as *mut *mut c_void 375 + ]; 376 + unsafe { Id::from_raw(library as *mut _) } 377 + } 378 + 379 + /// Create a render pipeline state from a descriptor. 380 + pub fn new_render_pipeline_state(&self, descriptor: Id) -> Option<RenderPipeline> { 381 + let mut error_ptr: *mut c_void = std::ptr::null_mut(); 382 + let state: *mut c_void = msg_send![ 383 + self.id.as_ptr(), 384 + newRenderPipelineStateWithDescriptor: descriptor.as_ptr(), 385 + error: &mut error_ptr as *mut *mut c_void 386 + ]; 387 + let id = unsafe { Id::from_raw(state as *mut _) }?; 388 + Some(RenderPipeline { id }) 389 + } 390 + 391 + /// Create a buffer from vertex data. 392 + pub fn new_buffer_with_vertices(&self, vertices: &[Vertex]) -> Option<Buffer> { 393 + let byte_len = std::mem::size_of_val(vertices); 394 + if byte_len == 0 { 395 + return None; 396 + } 397 + let buf: *mut c_void = msg_send![ 398 + self.id.as_ptr(), 399 + newBufferWithBytes: vertices.as_ptr() as *mut c_void, 400 + length: byte_len as u64, 401 + options: 0u64 402 + ]; 403 + let id = unsafe { Id::from_raw(buf as *mut _) }?; 404 + Some(Buffer { 405 + id, 406 + length: byte_len, 407 + }) 408 + } 409 + 410 + /// Create a 1×1 white BGRA texture (used as a dummy when no texture is bound). 411 + pub fn new_dummy_texture(&self) -> Option<Texture> { 412 + // Create texture descriptor via class method 413 + let cls = class!("MTLTextureDescriptor")?; 414 + let desc: *mut c_void = msg_send![ 415 + cls.as_ptr(), 416 + texture2DDescriptorWithPixelFormat: MTL_PIXEL_FORMAT_BGRA8_UNORM, 417 + width: 1u64, 418 + height: 1u64, 419 + mipmapped: false 420 + ]; 421 + let desc_id = unsafe { Id::from_raw(desc as *mut _) }?; 422 + 423 + // Set usage to ShaderRead (1) 424 + let _: *mut c_void = msg_send![desc_id.as_ptr(), setUsage: 1u64]; 425 + 426 + // Create texture 427 + let tex: *mut c_void = 428 + msg_send![self.id.as_ptr(), newTextureWithDescriptor: desc_id.as_ptr()]; 429 + let tex_id = unsafe { Id::from_raw(tex as *mut _) }?; 430 + 431 + // Upload 1 white pixel (BGRA: 0xFF 0xFF 0xFF 0xFF) 432 + let pixel: [u8; 4] = [0xFF, 0xFF, 0xFF, 0xFF]; 433 + let region = MtlRegion { 434 + origin: MtlOrigin { x: 0, y: 0, z: 0 }, 435 + size: MtlSize { 436 + width: 1, 437 + height: 1, 438 + depth: 1, 439 + }, 440 + }; 441 + let _: *mut c_void = msg_send![ 442 + tex_id.as_ptr(), 443 + replaceRegion: region, 444 + mipmapLevel: 0u64, 445 + withBytes: pixel.as_ptr() as *mut c_void, 446 + bytesPerRow: 4u64 447 + ]; 448 + 449 + Some(Texture { id: tex_id }) 450 + } 451 + 452 + /// Create a sampler state with linear filtering. 453 + pub fn new_sampler_state(&self) -> Option<SamplerState> { 454 + let cls = class!("MTLSamplerDescriptor")?; 455 + let desc: *mut c_void = msg_send![cls.as_ptr(), alloc]; 456 + let desc: *mut c_void = msg_send![desc, init]; 457 + let desc_id = unsafe { Id::from_raw(desc as *mut _) }?; 458 + 459 + // MTLSamplerMinMagFilterLinear = 1 460 + let _: *mut c_void = msg_send![desc_id.as_ptr(), setMinFilter: 1u64]; 461 + let _: *mut c_void = msg_send![desc_id.as_ptr(), setMagFilter: 1u64]; 462 + 463 + let sampler: *mut c_void = 464 + msg_send![self.id.as_ptr(), newSamplerStateWithDescriptor: desc_id.as_ptr()]; 465 + let id = unsafe { Id::from_raw(sampler as *mut _) }?; 466 + Some(SamplerState { id }) 467 + } 112 468 } 113 469 114 470 // --------------------------------------------------------------------------- ··· 168 524 } 169 525 170 526 impl RenderCommandEncoder { 527 + /// Set the render pipeline state for subsequent draw calls. 528 + pub fn set_render_pipeline_state(&self, pipeline: &RenderPipeline) { 529 + let _: *mut c_void = 530 + msg_send![self.id.as_ptr(), setRenderPipelineState: pipeline.id.as_ptr()]; 531 + } 532 + 533 + /// Bind a vertex buffer at the given index. 534 + pub fn set_vertex_buffer(&self, buffer: &Buffer, offset: u64, index: u64) { 535 + let _: *mut c_void = msg_send![ 536 + self.id.as_ptr(), 537 + setVertexBuffer: buffer.id.as_ptr(), 538 + offset: offset, 539 + atIndex: index 540 + ]; 541 + } 542 + 543 + /// Pass uniform data directly to the vertex shader at the given index. 544 + pub fn set_vertex_bytes<T>(&self, data: &T, index: u64) { 545 + let length = std::mem::size_of::<T>() as u64; 546 + let _: *mut c_void = msg_send![ 547 + self.id.as_ptr(), 548 + setVertexBytes: data as *const T as *mut c_void, 549 + length: length, 550 + atIndex: index 551 + ]; 552 + } 553 + 554 + /// Bind a texture to the fragment shader at the given index. 555 + pub fn set_fragment_texture(&self, texture: &Texture, index: u64) { 556 + let _: *mut c_void = msg_send![ 557 + self.id.as_ptr(), 558 + setFragmentTexture: texture.id.as_ptr(), 559 + atIndex: index 560 + ]; 561 + } 562 + 563 + /// Bind a sampler state to the fragment shader at the given index. 564 + pub fn set_fragment_sampler_state(&self, sampler: &SamplerState, index: u64) { 565 + let _: *mut c_void = msg_send![ 566 + self.id.as_ptr(), 567 + setFragmentSamplerState: sampler.id.as_ptr(), 568 + atIndex: index 569 + ]; 570 + } 571 + 572 + /// Issue a draw call for non-indexed primitives. 573 + pub fn draw_primitives(&self, primitive_type: u64, vertex_start: u64, vertex_count: u64) { 574 + let _: *mut c_void = msg_send![ 575 + self.id.as_ptr(), 576 + drawPrimitives: primitive_type, 577 + vertexStart: vertex_start, 578 + vertexCount: vertex_count 579 + ]; 580 + } 581 + 171 582 /// End encoding commands. 172 583 pub fn end_encoding(&self) { 173 584 let _: *mut c_void = msg_send![self.id.as_ptr(), endEncoding]; ··· 270 681 } 271 682 272 683 // --------------------------------------------------------------------------- 684 + // Render pipeline creation 685 + // --------------------------------------------------------------------------- 686 + 687 + /// Create the 2D render pipeline state from compiled MSL shaders. 688 + /// 689 + /// Configures alpha blending: `source * sourceAlpha + dest * (1 - sourceAlpha)`. 690 + fn create_render_pipeline(device: &Device) -> Option<RenderPipeline> { 691 + // Compile shader source 692 + let library = device.new_library_with_source(SHADER_SOURCE)?; 693 + 694 + // Get vertex and fragment functions 695 + let vert_name = CfString::new("vertex_main")?; 696 + let frag_name = CfString::new("fragment_main")?; 697 + 698 + let vert_fn: *mut c_void = 699 + msg_send![library.as_ptr(), newFunctionWithName: vert_name.as_void_ptr() as *mut c_void]; 700 + let vert_fn_id = unsafe { Id::from_raw(vert_fn as *mut _) }?; 701 + 702 + let frag_fn: *mut c_void = 703 + msg_send![library.as_ptr(), newFunctionWithName: frag_name.as_void_ptr() as *mut c_void]; 704 + let frag_fn_id = unsafe { Id::from_raw(frag_fn as *mut _) }?; 705 + 706 + // Create pipeline descriptor 707 + let cls = class!("MTLRenderPipelineDescriptor")?; 708 + let desc: *mut c_void = msg_send![cls.as_ptr(), alloc]; 709 + let desc: *mut c_void = msg_send![desc, init]; 710 + let desc_id = unsafe { Id::from_raw(desc as *mut _) }?; 711 + 712 + // Set vertex and fragment functions 713 + let _: *mut c_void = msg_send![desc_id.as_ptr(), setVertexFunction: vert_fn_id.as_ptr()]; 714 + let _: *mut c_void = msg_send![desc_id.as_ptr(), setFragmentFunction: frag_fn_id.as_ptr()]; 715 + 716 + // Configure color attachment 0 717 + let attachments: *mut c_void = msg_send![desc_id.as_ptr(), colorAttachments]; 718 + let attachment: *mut c_void = msg_send![attachments, objectAtIndexedSubscript: 0u64]; 719 + 720 + // Pixel format = BGRA8Unorm 721 + let _: *mut c_void = msg_send![attachment, setPixelFormat: MTL_PIXEL_FORMAT_BGRA8_UNORM]; 722 + 723 + // Enable alpha blending 724 + let _: *mut c_void = msg_send![attachment, setBlendingEnabled: true]; 725 + 726 + // source * sourceAlpha + dest * (1 - sourceAlpha) 727 + let _: *mut c_void = 728 + msg_send![attachment, setSourceRGBBlendFactor: MTL_BLEND_FACTOR_SOURCE_ALPHA]; 729 + let _: *mut c_void = msg_send![ 730 + attachment, 731 + setDestinationRGBBlendFactor: MTL_BLEND_FACTOR_ONE_MINUS_SOURCE_ALPHA 732 + ]; 733 + let _: *mut c_void = 734 + msg_send![attachment, setSourceAlphaBlendFactor: MTL_BLEND_FACTOR_SOURCE_ALPHA]; 735 + let _: *mut c_void = msg_send![ 736 + attachment, 737 + setDestinationAlphaBlendFactor: MTL_BLEND_FACTOR_ONE_MINUS_SOURCE_ALPHA 738 + ]; 739 + 740 + device.new_render_pipeline_state(desc_id) 741 + } 742 + 743 + // --------------------------------------------------------------------------- 744 + // MetalRenderer — batched 2D quad renderer 745 + // --------------------------------------------------------------------------- 746 + 747 + /// A batched 2D renderer using the Metal GPU pipeline. 748 + /// 749 + /// Renders solid-color and textured quads via a single vertex buffer and 750 + /// draw call per batch. Alpha blending is enabled for semi-transparent fills. 751 + pub struct MetalRenderer { 752 + device: Device, 753 + pipeline: RenderPipeline, 754 + dummy_texture: Texture, 755 + sampler: SamplerState, 756 + } 757 + 758 + impl MetalRenderer { 759 + /// Create a new `MetalRenderer` on the given device. 760 + /// 761 + /// Compiles the MSL shaders, creates the pipeline state, and sets up a 762 + /// dummy 1×1 white texture for solid-color rendering. 763 + /// 764 + /// Returns `None` if shader compilation or pipeline creation fails. 765 + pub fn new(device: Device) -> Option<MetalRenderer> { 766 + let pipeline = create_render_pipeline(&device)?; 767 + let dummy_texture = device.new_dummy_texture()?; 768 + let sampler = device.new_sampler_state()?; 769 + Some(MetalRenderer { 770 + device, 771 + pipeline, 772 + dummy_texture, 773 + sampler, 774 + }) 775 + } 776 + 777 + /// Build a vertex buffer from a batch of quads. 778 + /// 779 + /// Each quad is decomposed into 2 triangles (6 vertices). Returns the 780 + /// buffer and the total vertex count. 781 + pub fn build_vertex_buffer(&self, quads: &[Quad]) -> Option<(Buffer, usize)> { 782 + let mut vertices = Vec::with_capacity(quads.len() * 6); 783 + for quad in quads { 784 + quad.triangulate(&mut vertices); 785 + } 786 + let count = vertices.len(); 787 + let buffer = self.device.new_buffer_with_vertices(&vertices)?; 788 + Some((buffer, count)) 789 + } 790 + 791 + /// Encode draw commands for a batch of quads into a render command encoder. 792 + /// 793 + /// The encoder must already be created from a command buffer with an 794 + /// appropriate render pass descriptor. 795 + pub fn encode_draw( 796 + &self, 797 + encoder: &RenderCommandEncoder, 798 + vertex_buffer: &Buffer, 799 + vertex_count: usize, 800 + viewport_width: f32, 801 + viewport_height: f32, 802 + ) { 803 + let uniforms = Uniforms { 804 + viewport_size: [viewport_width, viewport_height], 805 + }; 806 + 807 + encoder.set_render_pipeline_state(&self.pipeline); 808 + encoder.set_vertex_buffer(vertex_buffer, 0, 0); 809 + encoder.set_vertex_bytes(&uniforms, 1); 810 + encoder.set_fragment_texture(&self.dummy_texture, 0); 811 + encoder.set_fragment_sampler_state(&self.sampler, 0); 812 + encoder.draw_primitives(MTL_PRIMITIVE_TYPE_TRIANGLE, 0, vertex_count as u64); 813 + } 814 + 815 + /// Return a reference to the underlying device. 816 + pub fn device(&self) -> &Device { 817 + &self.device 818 + } 819 + } 820 + 821 + // --------------------------------------------------------------------------- 273 822 // Tests 274 823 // --------------------------------------------------------------------------- 275 824 ··· 340 889 layer.set_pixel_format(MTL_PIXEL_FORMAT_BGRA8_UNORM); 341 890 layer.set_framebuffer_only(true); 342 891 layer.set_drawable_size(800.0, 600.0); 892 + } 893 + 894 + // -- Vertex layout tests ------------------------------------------------ 895 + 896 + #[test] 897 + fn vertex_size_and_alignment() { 898 + // The Vertex struct must be 36 bytes to match the MSL packed layout. 899 + assert_eq!(std::mem::size_of::<Vertex>(), 36); 900 + assert_eq!(std::mem::align_of::<Vertex>(), 4); 901 + } 902 + 903 + #[test] 904 + fn uniforms_size() { 905 + assert_eq!(std::mem::size_of::<Uniforms>(), 8); 906 + } 907 + 908 + // -- Quad triangulation tests ------------------------------------------- 909 + 910 + #[test] 911 + fn quad_triangulate_produces_6_vertices() { 912 + let quad = Quad { 913 + x: 10.0, 914 + y: 20.0, 915 + width: 100.0, 916 + height: 50.0, 917 + color: [1.0, 0.0, 0.0, 1.0], 918 + }; 919 + let mut verts = Vec::new(); 920 + quad.triangulate(&mut verts); 921 + assert_eq!(verts.len(), 6); 922 + } 923 + 924 + #[test] 925 + fn quad_triangulate_covers_corners() { 926 + let quad = Quad { 927 + x: 0.0, 928 + y: 0.0, 929 + width: 100.0, 930 + height: 50.0, 931 + color: [1.0, 1.0, 1.0, 1.0], 932 + }; 933 + let mut verts = Vec::new(); 934 + quad.triangulate(&mut verts); 935 + 936 + // Collect all unique positions 937 + let positions: Vec<[f32; 2]> = verts.iter().map(|v| v.position).collect(); 938 + 939 + // Should have all 4 corners represented 940 + assert!(positions.contains(&[0.0, 0.0])); 941 + assert!(positions.contains(&[100.0, 0.0])); 942 + assert!(positions.contains(&[0.0, 50.0])); 943 + assert!(positions.contains(&[100.0, 50.0])); 944 + } 945 + 946 + #[test] 947 + fn quad_triangulate_solid_color() { 948 + let quad = Quad { 949 + x: 0.0, 950 + y: 0.0, 951 + width: 10.0, 952 + height: 10.0, 953 + color: [0.5, 0.3, 0.1, 0.8], 954 + }; 955 + let mut verts = Vec::new(); 956 + quad.triangulate(&mut verts); 957 + 958 + for v in &verts { 959 + assert_eq!(v.color, [0.5, 0.3, 0.1, 0.8]); 960 + assert_eq!(v.use_texture, 0.0); 961 + assert_eq!(v.tex_coord, [0.0, 0.0]); 962 + } 963 + } 964 + 965 + #[test] 966 + fn multiple_quads_batch() { 967 + let quads = vec![ 968 + Quad { 969 + x: 0.0, 970 + y: 0.0, 971 + width: 50.0, 972 + height: 50.0, 973 + color: [1.0, 0.0, 0.0, 1.0], 974 + }, 975 + Quad { 976 + x: 60.0, 977 + y: 0.0, 978 + width: 50.0, 979 + height: 50.0, 980 + color: [0.0, 1.0, 0.0, 1.0], 981 + }, 982 + Quad { 983 + x: 120.0, 984 + y: 0.0, 985 + width: 50.0, 986 + height: 50.0, 987 + color: [0.0, 0.0, 1.0, 1.0], 988 + }, 989 + ]; 990 + let mut verts = Vec::new(); 991 + for q in &quads { 992 + q.triangulate(&mut verts); 993 + } 994 + assert_eq!(verts.len(), 18); // 3 quads × 6 vertices 995 + 996 + // First quad vertices should be red 997 + for v in &verts[..6] { 998 + assert_eq!(v.color, [1.0, 0.0, 0.0, 1.0]); 999 + } 1000 + // Second quad vertices should be green 1001 + for v in &verts[6..12] { 1002 + assert_eq!(v.color, [0.0, 1.0, 0.0, 1.0]); 1003 + } 1004 + } 1005 + 1006 + // -- GPU tests (require Metal device) ----------------------------------- 1007 + 1008 + #[test] 1009 + fn compile_shader_source() { 1010 + let device = match Device::system_default() { 1011 + Some(d) => d, 1012 + None => return, 1013 + }; 1014 + let library = device.new_library_with_source(SHADER_SOURCE); 1015 + assert!( 1016 + library.is_some(), 1017 + "MSL shader should compile without errors" 1018 + ); 1019 + } 1020 + 1021 + #[test] 1022 + fn create_pipeline_state() { 1023 + let _pool = crate::appkit::AutoreleasePool::new(); 1024 + let device = match Device::system_default() { 1025 + Some(d) => d, 1026 + None => return, 1027 + }; 1028 + let pipeline = create_render_pipeline(&device); 1029 + assert!( 1030 + pipeline.is_some(), 1031 + "render pipeline state should be created" 1032 + ); 1033 + } 1034 + 1035 + #[test] 1036 + fn create_vertex_buffer() { 1037 + let device = match Device::system_default() { 1038 + Some(d) => d, 1039 + None => return, 1040 + }; 1041 + let quad = Quad { 1042 + x: 0.0, 1043 + y: 0.0, 1044 + width: 100.0, 1045 + height: 100.0, 1046 + color: [1.0, 0.0, 0.0, 1.0], 1047 + }; 1048 + let mut verts = Vec::new(); 1049 + quad.triangulate(&mut verts); 1050 + 1051 + let buffer = device.new_buffer_with_vertices(&verts); 1052 + assert!(buffer.is_some()); 1053 + let buffer = buffer.unwrap(); 1054 + assert_eq!(buffer.length(), 6 * std::mem::size_of::<Vertex>()); 1055 + } 1056 + 1057 + #[test] 1058 + fn create_dummy_texture() { 1059 + let _pool = crate::appkit::AutoreleasePool::new(); 1060 + let device = match Device::system_default() { 1061 + Some(d) => d, 1062 + None => return, 1063 + }; 1064 + let texture = device.new_dummy_texture(); 1065 + assert!(texture.is_some(), "dummy texture should be created"); 1066 + } 1067 + 1068 + #[test] 1069 + fn create_sampler_state() { 1070 + let _pool = crate::appkit::AutoreleasePool::new(); 1071 + let device = match Device::system_default() { 1072 + Some(d) => d, 1073 + None => return, 1074 + }; 1075 + let sampler = device.new_sampler_state(); 1076 + assert!(sampler.is_some(), "sampler state should be created"); 1077 + } 1078 + 1079 + #[test] 1080 + fn metal_renderer_new() { 1081 + let _pool = crate::appkit::AutoreleasePool::new(); 1082 + let device = match Device::system_default() { 1083 + Some(d) => d, 1084 + None => return, 1085 + }; 1086 + let renderer = MetalRenderer::new(device); 1087 + assert!(renderer.is_some(), "MetalRenderer should be created"); 1088 + } 1089 + 1090 + #[test] 1091 + fn metal_renderer_build_vertex_buffer() { 1092 + let _pool = crate::appkit::AutoreleasePool::new(); 1093 + let device = match Device::system_default() { 1094 + Some(d) => d, 1095 + None => return, 1096 + }; 1097 + let renderer = MetalRenderer::new(device).expect("renderer should be created"); 1098 + 1099 + let quads = vec![ 1100 + Quad { 1101 + x: 10.0, 1102 + y: 10.0, 1103 + width: 200.0, 1104 + height: 100.0, 1105 + color: [1.0, 0.0, 0.0, 1.0], 1106 + }, 1107 + Quad { 1108 + x: 50.0, 1109 + y: 50.0, 1110 + width: 100.0, 1111 + height: 100.0, 1112 + color: [0.0, 0.0, 1.0, 0.5], 1113 + }, 1114 + ]; 1115 + 1116 + let result = renderer.build_vertex_buffer(&quads); 1117 + assert!(result.is_some()); 1118 + let (buffer, count) = result.unwrap(); 1119 + assert_eq!(count, 12); // 2 quads × 6 vertices 1120 + assert_eq!(buffer.length(), 12 * std::mem::size_of::<Vertex>()); 1121 + } 1122 + 1123 + #[test] 1124 + fn empty_quad_list() { 1125 + let _pool = crate::appkit::AutoreleasePool::new(); 1126 + let device = match Device::system_default() { 1127 + Some(d) => d, 1128 + None => return, 1129 + }; 1130 + let renderer = MetalRenderer::new(device).expect("renderer should be created"); 1131 + let result = renderer.build_vertex_buffer(&[]); 1132 + assert!(result.is_none(), "empty quad list should return None"); 1133 + } 1134 + 1135 + #[test] 1136 + fn primitive_type_constant() { 1137 + assert_eq!(MTL_PRIMITIVE_TYPE_TRIANGLE, 3); 1138 + } 1139 + 1140 + #[test] 1141 + fn blend_factor_constants() { 1142 + assert_eq!(MTL_BLEND_FACTOR_SOURCE_ALPHA, 4); 1143 + assert_eq!(MTL_BLEND_FACTOR_ONE_MINUS_SOURCE_ALPHA, 5); 343 1144 } 344 1145 }