From ce537e7a8f3b037195d5ef1103446bb98486a1e7 Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Tue, 24 Feb 2026 00:00:32 +0100 Subject: [PATCH] Implement CoreGraphics rendering surface Add platform/src/cg.rs with: - CGColorSpace (device RGB), CGBitmapContext, CGImage wrappers - CGRect/CGPoint/CGSize geometry types - BitmapContext: Rust-owned pixel buffer wrapped in CGBitmapContext - fill_rect(), clear(), create_image(), pixels/pixels_mut accessors - draw_image_in_context() for blitting into a view's CG context - 8 unit tests covering context creation, drawing, and edge cases Add custom NSView subclass (WeView) in appkit.rs: - Overrides drawRect: to blit CGImage from BitmapContext - Overrides isFlipped for top-left coordinate origin - BitmapView wrapper with set_needs_display() for redraw requests - Uses ivar to store BitmapContext pointer per-instance - Window::set_content_view() for installing custom views - 2 new tests for WeView class registration and BitmapView creation Add ivar support to objc.rs: - class_addIvar FFI binding - object_setInstanceVariable / object_getInstanceVariable FFI - Id::set_ivar() and Id::get_ivar() helper methods - Class::add_ivar() helper method Update browser main.rs: - Creates 800x600 BitmapContext - Fills dark gray background with centered blue rectangle - Installs BitmapView as window content view Co-Authored-By: Claude Opus 4.6 --- crates/browser/src/main.rs | 18 +- crates/platform/src/appkit.rs | 170 ++++++++++++++++ crates/platform/src/cg.rs | 369 ++++++++++++++++++++++++++++++++++ crates/platform/src/lib.rs | 1 + crates/platform/src/objc.rs | 61 +++++- 5 files changed, 617 insertions(+), 2 deletions(-) create mode 100644 crates/platform/src/cg.rs diff --git a/crates/browser/src/main.rs b/crates/browser/src/main.rs index 1bdd079..cb86954 100644 --- a/crates/browser/src/main.rs +++ b/crates/browser/src/main.rs @@ -1,4 +1,5 @@ use we_platform::appkit; +use we_platform::cg::{BitmapContext, CGRect}; fn main() { let _pool = appkit::AutoreleasePool::new(); @@ -9,8 +10,23 @@ fn main() { appkit::install_app_delegate(&app); let window = appkit::create_standard_window("we"); - window.make_key_and_order_front(); + // Create a bitmap context for software rendering. + let bitmap = BitmapContext::new(800, 600).expect("failed to create bitmap context"); + + // Draw a colored rectangle as proof of life. + // Clear to dark gray background. + bitmap.clear(0.15, 0.15, 0.15, 1.0); + // Draw a blue rectangle in the center. + bitmap.fill_rect(CGRect::new(200.0, 150.0, 400.0, 300.0), 0.2, 0.4, 0.8, 1.0); + + // Create a custom view backed by the bitmap context and set it as + // the window's content view. + let frame = appkit::NSRect::new(0.0, 0.0, 800.0, 600.0); + let view = appkit::BitmapView::new(frame, &bitmap); + window.set_content_view(&view.id()); + + window.make_key_and_order_front(); app.activate(); app.run(); } diff --git a/crates/platform/src/appkit.rs b/crates/platform/src/appkit.rs index 667b759..ed3c574 100644 --- a/crates/platform/src/appkit.rs +++ b/crates/platform/src/appkit.rs @@ -9,8 +9,10 @@ //! The `platform` crate is one of the few crates where `unsafe` is permitted. use crate::cf::CfString; +use crate::cg::{self, BitmapContext, CGRect}; use crate::objc::{Class, Id, Imp, Sel}; use crate::{class, msg_send}; +use std::ffi::CStr; use std::os::raw::c_void; // --------------------------------------------------------------------------- @@ -219,12 +221,164 @@ impl Window { unsafe { Id::from_raw(view as *mut _) }.expect("contentView returned nil") } + /// Set the window's content view. + pub fn set_content_view(&self, view: &Id) { + let _: *mut c_void = msg_send![self.window.as_ptr(), setContentView: view.as_ptr()]; + } + /// Return the underlying Objective-C object. pub fn id(&self) -> Id { self.window } } +// --------------------------------------------------------------------------- +// BitmapView — custom NSView subclass for rendering a CG bitmap +// --------------------------------------------------------------------------- + +/// The name of the ivar storing the BitmapContext pointer in WeView. +const BITMAP_CTX_IVAR: &CStr = c"_bitmapCtx"; + +/// Register the `WeView` custom NSView subclass. +/// +/// The class overrides `drawRect:` to blit a CGImage from the associated +/// BitmapContext into the view's graphics context, and `isFlipped` to use a +/// top-left coordinate origin. +fn register_we_view_class() { + if class!("WeView").is_some() { + return; + } + + let superclass = class!("NSView").expect("NSView not found"); + let view_class = + Class::allocate(superclass, c"WeView", 0).expect("failed to allocate WeView class"); + + // Add an ivar to hold a raw pointer to a BitmapContext. + // Size = 8 (pointer), alignment = 3 (log2(8)), type = "^v" (pointer to void). + view_class.add_ivar(BITMAP_CTX_IVAR, 8, 3, c"^v"); + + // drawRect: + extern "C" fn draw_rect(this: *mut c_void, _sel: *mut c_void, _dirty_rect: NSRect) { + let this_id = unsafe { Id::from_raw(this as *mut _) }; + let this_id = match this_id { + Some(id) => id, + None => return, + }; + + // Get the BitmapContext pointer from our ivar. + let ctx_ptr = unsafe { this_id.get_ivar(BITMAP_CTX_IVAR) }; + if ctx_ptr.is_null() { + return; + } + let bitmap_ctx = unsafe { &*(ctx_ptr as *const BitmapContext) }; + + // Create a CGImage from the bitmap context. + let image = match bitmap_ctx.create_image() { + Some(img) => img, + None => return, + }; + + // Get the view's bounds: [self bounds] + let bounds: NSRect = msg_send![this, bounds]; + + // Get the current NSGraphicsContext: + // [NSGraphicsContext currentContext] + let gfx_ctx_cls = class!("NSGraphicsContext").expect("NSGraphicsContext not found"); + let gfx_ctx: *mut c_void = msg_send![gfx_ctx_cls.as_ptr(), currentContext]; + if gfx_ctx.is_null() { + return; + } + + // Get the CGContextRef: [gfxCtx CGContext] + let cg_context: *mut c_void = msg_send![gfx_ctx, CGContext]; + if cg_context.is_null() { + return; + } + + // Draw the image into the CG context. + let rect = CGRect::new( + bounds.origin.x, + bounds.origin.y, + bounds.size.width, + bounds.size.height, + ); + unsafe { + cg::draw_image_in_context(cg_context, rect, &image); + } + } + + let sel = Sel::register(c"drawRect:"); + view_class.add_method( + sel, + unsafe { std::mem::transmute::<*const (), Imp>(draw_rect as *const ()) }, + c"v@:{CGRect={CGPoint=dd}{CGSize=dd}}", + ); + + // isFlipped -> YES (top-left origin) + extern "C" fn is_flipped(_this: *mut c_void, _sel: *mut c_void) -> bool { + true + } + + let sel = Sel::register(c"isFlipped"); + view_class.add_method( + sel, + unsafe { std::mem::transmute::<*const (), Imp>(is_flipped as *const ()) }, + c"B@:", + ); + + view_class.register(); +} + +/// A custom NSView backed by a [`BitmapContext`]. +/// +/// When the view needs to draw, it creates a `CGImage` from the bitmap +/// context and blits it into the view's graphics context. +/// +/// The `BitmapContext` must outlive this view. The caller is responsible +/// for ensuring this (typically by keeping the context in a `Box` alongside +/// the view). +pub struct BitmapView { + view: Id, +} + +impl BitmapView { + /// Create a new `BitmapView` with the given frame and bitmap context. + /// + /// The `bitmap_ctx` pointer is stored in the view's instance variable. + /// The caller must ensure the `BitmapContext` outlives this view. + pub fn new(frame: NSRect, bitmap_ctx: &BitmapContext) -> BitmapView { + register_we_view_class(); + + let cls = class!("WeView").expect("WeView class not found"); + let view: *mut c_void = msg_send![cls.as_ptr(), alloc]; + let view: *mut c_void = msg_send![view, initWithFrame: frame]; + let view = unsafe { Id::from_raw(view as *mut _) }.expect("WeView initWithFrame failed"); + + // Store the BitmapContext pointer in the ivar. + unsafe { + view.set_ivar( + BITMAP_CTX_IVAR, + bitmap_ctx as *const BitmapContext as *mut c_void, + ); + } + + BitmapView { view } + } + + /// Request the view to redraw. + /// + /// Call this after modifying the bitmap context's pixels to + /// schedule a redraw. + pub fn set_needs_display(&self) { + let _: *mut c_void = msg_send![self.view.as_ptr(), setNeedsDisplay: true]; + } + + /// Return the underlying Objective-C view object. + pub fn id(&self) -> Id { + self.view + } +} + // --------------------------------------------------------------------------- // App delegate for handling window close -> app termination // --------------------------------------------------------------------------- @@ -347,4 +501,20 @@ mod tests { | NS_WINDOW_STYLE_MASK_RESIZABLE; assert_eq!(style, 0b1111); } + + #[test] + fn we_view_class_registration() { + register_we_view_class(); + let cls = class!("WeView"); + assert!(cls.is_some(), "WeView class should be registered"); + } + + #[test] + fn bitmap_view_create() { + let _pool = AutoreleasePool::new(); + let bitmap = BitmapContext::new(100, 100).expect("should create bitmap context"); + let frame = NSRect::new(0.0, 0.0, 100.0, 100.0); + let view = BitmapView::new(frame, &bitmap); + assert!(!view.id().as_ptr().is_null()); + } } diff --git a/crates/platform/src/cg.rs b/crates/platform/src/cg.rs new file mode 100644 index 0000000..1e822c1 --- /dev/null +++ b/crates/platform/src/cg.rs @@ -0,0 +1,369 @@ +//! CoreGraphics FFI bindings for bitmap rendering. +//! +//! Provides wrappers around `CGColorSpace`, `CGContext` (bitmap contexts), +//! and `CGImage` for software rendering into a Rust-owned pixel buffer. +//! +//! # Safety +//! +//! This module contains `unsafe` code for FFI with CoreGraphics. +//! The `platform` crate is one of the few crates where `unsafe` is permitted. + +use std::os::raw::c_void; +use std::ptr::NonNull; + +// --------------------------------------------------------------------------- +// CoreGraphics framework link +// --------------------------------------------------------------------------- + +#[link(name = "CoreGraphics", kind = "framework")] +extern "C" { + fn CGColorSpaceCreateDeviceRGB() -> *mut c_void; + fn CGColorSpaceRelease(space: *mut c_void); + + fn CGBitmapContextCreate( + data: *mut c_void, + width: usize, + height: usize, + bits_per_component: usize, + bytes_per_row: usize, + colorspace: *mut c_void, + bitmap_info: u32, + ) -> *mut c_void; + + fn CGBitmapContextCreateImage(context: *mut c_void) -> *mut c_void; + + fn CGContextRelease(context: *mut c_void); + fn CGImageRelease(image: *mut c_void); + + fn CGContextDrawImage(context: *mut c_void, rect: CGRect, image: *mut c_void); + fn CGContextSetRGBFillColor(context: *mut c_void, red: f64, green: f64, blue: f64, alpha: f64); + fn CGContextFillRect(context: *mut c_void, rect: CGRect); +} + +// --------------------------------------------------------------------------- +// Geometry (re-export compatible with appkit types) +// --------------------------------------------------------------------------- + +/// `CGRect` — a rectangle defined by origin and size. +/// +/// This is layout-compatible with `NSRect` / the appkit `NSRect` type. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct CGRect { + pub origin: CGPoint, + pub size: CGSize, +} + +/// `CGPoint` — a point in 2D space. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct CGPoint { + pub x: f64, + pub y: f64, +} + +/// `CGSize` — a 2D size. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct CGSize { + pub width: f64, + pub height: f64, +} + +impl CGRect { + /// Create a new rectangle. + pub fn new(x: f64, y: f64, width: f64, height: f64) -> CGRect { + CGRect { + origin: CGPoint { x, y }, + size: CGSize { width, height }, + } + } +} + +// --------------------------------------------------------------------------- +// CGBitmapInfo constants +// --------------------------------------------------------------------------- + +/// Alpha info mask (lower 5 bits of CGBitmapInfo). +pub const K_CG_IMAGE_ALPHA_PREMULTIPLIED_FIRST: u32 = 2; + +/// Byte order mask: 32-bit host byte order. +pub const K_CG_BITMAP_BYTE_ORDER_32_HOST: u32 = 0; + +/// Standard BGRA premultiplied alpha format used by macOS windowing. +/// +/// Pixel layout in memory on little-endian (ARM64): B, G, R, A +/// This matches what CoreGraphics and AppKit expect for display. +pub const BITMAP_INFO_PREMULTIPLIED_FIRST: u32 = + K_CG_IMAGE_ALPHA_PREMULTIPLIED_FIRST | K_CG_BITMAP_BYTE_ORDER_32_HOST; + +// --------------------------------------------------------------------------- +// ColorSpace +// --------------------------------------------------------------------------- + +/// An owned CoreGraphics device RGB color space. +pub struct ColorSpace(NonNull); + +impl ColorSpace { + /// Create a device RGB color space. + pub fn device_rgb() -> Option { + let ptr = unsafe { CGColorSpaceCreateDeviceRGB() }; + NonNull::new(ptr).map(ColorSpace) + } + + /// Return the raw pointer. + #[inline] + pub fn as_ptr(&self) -> *mut c_void { + self.0.as_ptr() + } +} + +impl Drop for ColorSpace { + fn drop(&mut self) { + unsafe { CGColorSpaceRelease(self.as_ptr()) } + } +} + +// --------------------------------------------------------------------------- +// BitmapContext — a CGBitmapContext backed by a Rust-owned pixel buffer +// --------------------------------------------------------------------------- + +/// A CoreGraphics bitmap context backed by a Rust-owned pixel buffer. +/// +/// The pixel buffer is BGRA, 8 bits per component, premultiplied alpha. +/// The buffer is owned by this struct and the `CGBitmapContext` renders into it. +pub struct BitmapContext { + context: NonNull, + buffer: Vec, + width: usize, + height: usize, +} + +impl BitmapContext { + /// Create a new bitmap context with the given dimensions. + /// + /// Allocates a Rust-owned pixel buffer and wraps it in a CGBitmapContext. + /// Pixel format: 4 bytes per pixel (BGRA, premultiplied alpha). + pub fn new(width: usize, height: usize) -> Option { + if width == 0 || height == 0 { + return None; + } + + let bytes_per_row = width * 4; + let mut buffer = vec![0u8; bytes_per_row * height]; + let color_space = ColorSpace::device_rgb()?; + + let ctx = unsafe { + CGBitmapContextCreate( + buffer.as_mut_ptr() as *mut c_void, + width, + height, + 8, // bits per component + bytes_per_row, + color_space.as_ptr(), + BITMAP_INFO_PREMULTIPLIED_FIRST, + ) + }; + + let context = NonNull::new(ctx)?; + Some(BitmapContext { + context, + buffer, + width, + height, + }) + } + + /// Get the width in pixels. + #[inline] + pub fn width(&self) -> usize { + self.width + } + + /// Get the height in pixels. + #[inline] + pub fn height(&self) -> usize { + self.height + } + + /// Get the bytes per row (stride). + #[inline] + pub fn bytes_per_row(&self) -> usize { + self.width * 4 + } + + /// Get a shared reference to the raw pixel buffer. + /// + /// Pixel format: BGRA, 8 bits per component, premultiplied alpha. + /// Layout: row-major, bottom-to-top (CG default coordinate system). + #[inline] + pub fn pixels(&self) -> &[u8] { + &self.buffer + } + + /// Get a mutable reference to the raw pixel buffer. + /// + /// After modifying pixels directly, changes are immediately visible to + /// CoreGraphics since the bitmap context wraps this buffer. + #[inline] + pub fn pixels_mut(&mut self) -> &mut [u8] { + &mut self.buffer + } + + /// Create a `CGImage` from the current bitmap context contents. + /// + /// Returns `None` if the image cannot be created. + pub fn create_image(&self) -> Option { + let img = unsafe { CGBitmapContextCreateImage(self.context.as_ptr()) }; + NonNull::new(img).map(Image) + } + + /// Fill a rectangle with an RGBA color using CoreGraphics. + /// + /// Color components are in 0.0..=1.0 range. + pub fn fill_rect(&self, rect: CGRect, r: f64, g: f64, b: f64, a: f64) { + unsafe { + CGContextSetRGBFillColor(self.context.as_ptr(), r, g, b, a); + CGContextFillRect(self.context.as_ptr(), rect); + } + } + + /// Clear the entire buffer to a given RGBA color. + /// + /// Color components are in 0.0..=1.0 range. This uses CoreGraphics + /// `CGContextFillRect` to fill the entire bitmap. + pub fn clear(&self, r: f64, g: f64, b: f64, a: f64) { + let rect = CGRect::new(0.0, 0.0, self.width as f64, self.height as f64); + self.fill_rect(rect, r, g, b, a); + } + + /// Return the raw CGContextRef pointer. + /// + /// Useful for passing to other CG drawing functions. + #[inline] + pub fn as_ptr(&self) -> *mut c_void { + self.context.as_ptr() + } +} + +impl Drop for BitmapContext { + fn drop(&mut self) { + unsafe { CGContextRelease(self.context.as_ptr()) } + } +} + +// --------------------------------------------------------------------------- +// Image — an owned CGImage +// --------------------------------------------------------------------------- + +/// An owned CoreGraphics image (CGImageRef). +/// +/// Created from a [`BitmapContext`] via [`BitmapContext::create_image`]. +pub struct Image(NonNull); + +impl Image { + /// Return the raw CGImageRef pointer. + #[inline] + pub fn as_ptr(&self) -> *mut c_void { + self.0.as_ptr() + } +} + +impl Drop for Image { + fn drop(&mut self) { + unsafe { CGImageRelease(self.as_ptr()) } + } +} + +// --------------------------------------------------------------------------- +// Drawing a CGImage into a CGContext +// --------------------------------------------------------------------------- + +/// Draw a `CGImage` into a CoreGraphics context at the given rectangle. +/// +/// This is used in `drawRect:` to blit the bitmap into the view's context. +/// +/// # Safety +/// +/// `target_context` must be a valid CGContextRef (e.g., from +/// `NSGraphicsContext.currentContext.CGContext`). +pub unsafe fn draw_image_in_context(target_context: *mut c_void, rect: CGRect, image: &Image) { + CGContextDrawImage(target_context, rect, image.as_ptr()); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cgrect_new() { + let rect = CGRect::new(10.0, 20.0, 300.0, 400.0); + assert_eq!(rect.origin.x, 10.0); + assert_eq!(rect.origin.y, 20.0); + assert_eq!(rect.size.width, 300.0); + assert_eq!(rect.size.height, 400.0); + } + + #[test] + fn bitmap_info_constant() { + assert_eq!(BITMAP_INFO_PREMULTIPLIED_FIRST, 2); + } + + #[test] + fn create_color_space() { + let cs = ColorSpace::device_rgb().expect("should create device RGB color space"); + assert!(!cs.as_ptr().is_null()); + } + + #[test] + fn create_bitmap_context() { + let ctx = BitmapContext::new(100, 100).expect("should create bitmap context"); + assert_eq!(ctx.width(), 100); + assert_eq!(ctx.height(), 100); + assert_eq!(ctx.bytes_per_row(), 400); + assert_eq!(ctx.pixels().len(), 100 * 400); + } + + #[test] + fn bitmap_context_zero_size_returns_none() { + assert!(BitmapContext::new(0, 100).is_none()); + assert!(BitmapContext::new(100, 0).is_none()); + assert!(BitmapContext::new(0, 0).is_none()); + } + + #[test] + fn bitmap_context_pixels_mut() { + let mut ctx = BitmapContext::new(10, 10).expect("should create"); + let pixels = ctx.pixels_mut(); + // Write a red pixel at (0, 0): BGRA format + pixels[0] = 0; // B + pixels[1] = 0; // G + pixels[2] = 255; // R + pixels[3] = 255; // A + assert_eq!(ctx.pixels()[0], 0); + assert_eq!(ctx.pixels()[2], 255); + } + + #[test] + fn bitmap_context_clear_and_create_image() { + let ctx = BitmapContext::new(50, 50).expect("should create"); + ctx.clear(0.0, 0.0, 1.0, 1.0); // blue + let img = ctx.create_image().expect("should create image"); + assert!(!img.as_ptr().is_null()); + } + + #[test] + fn bitmap_context_fill_rect() { + let ctx = BitmapContext::new(100, 100).expect("should create"); + let rect = CGRect::new(10.0, 10.0, 50.0, 50.0); + ctx.fill_rect(rect, 1.0, 0.0, 0.0, 1.0); // red rectangle + let img = ctx + .create_image() + .expect("should create image from filled context"); + assert!(!img.as_ptr().is_null()); + } +} diff --git a/crates/platform/src/lib.rs b/crates/platform/src/lib.rs index ddd494c..205ca38 100644 --- a/crates/platform/src/lib.rs +++ b/crates/platform/src/lib.rs @@ -2,4 +2,5 @@ pub mod appkit; pub mod cf; +pub mod cg; pub mod objc; diff --git a/crates/platform/src/objc.rs b/crates/platform/src/objc.rs index 9732241..1bd8250 100644 --- a/crates/platform/src/objc.rs +++ b/crates/platform/src/objc.rs @@ -10,7 +10,7 @@ use std::ffi::CStr; use std::fmt; -use std::os::raw::c_char; +use std::os::raw::{c_char, c_void}; use std::ptr::NonNull; // --------------------------------------------------------------------------- @@ -58,6 +58,24 @@ extern "C" { types: *const c_char, ) -> bool; fn class_getName(cls: *mut ObjcClass) -> *const c_char; + fn class_addIvar( + cls: *mut ObjcClass, + name: *const c_char, + size: usize, + alignment: u8, + types: *const c_char, + ) -> bool; + + fn object_setInstanceVariable( + obj: *mut ObjcObject, + name: *const c_char, + value: *mut c_void, + ) -> *mut c_void; // returns Ivar (opaque, we don't use it) + fn object_getInstanceVariable( + obj: *mut ObjcObject, + name: *const c_char, + out_value: *mut *mut c_void, + ) -> *mut c_void; // returns Ivar // objc_msgSend has a variadic ABI. On AArch64 the calling convention is // the standard C calling convention (arguments in x0..x7, return in x0). @@ -174,6 +192,26 @@ impl Class { unsafe { class_addMethod(self.as_ptr(), sel.as_ptr(), imp, types.as_ptr()) } } + /// Add an instance variable to a class that has not yet been registered. + /// + /// `name` is the ivar name, `size` is the size in bytes, `alignment` is + /// the log2 alignment (e.g., 3 for 8-byte alignment), and `types` is the + /// ObjC type encoding string. + /// + /// Returns `true` if the ivar was added. + pub fn add_ivar(self, name: &CStr, size: usize, alignment: u8, types: &CStr) -> bool { + // SAFETY: the class pointer is valid and not yet registered. + unsafe { + class_addIvar( + self.as_ptr(), + name.as_ptr(), + size, + alignment, + types.as_ptr(), + ) + } + } + /// Get the name of this class. pub fn name(self) -> &'static str { // SAFETY: class_getName always returns a valid C string for a valid @@ -248,6 +286,27 @@ impl Id { pub unsafe fn cast(self) -> *mut T { self.0.as_ptr().cast() } + + /// Set an instance variable by name. + /// + /// # Safety + /// + /// The object must have an ivar with the given `name` of pointer type. + /// The caller is responsible for the validity and lifetime of `value`. + pub unsafe fn set_ivar(self, name: &CStr, value: *mut c_void) { + object_setInstanceVariable(self.as_ptr(), name.as_ptr(), value); + } + + /// Get an instance variable by name. + /// + /// # Safety + /// + /// The object must have an ivar with the given `name` of pointer type. + pub unsafe fn get_ivar(self, name: &CStr) -> *mut c_void { + let mut out: *mut c_void = std::ptr::null_mut(); + object_getInstanceVariable(self.as_ptr(), name.as_ptr(), &mut out); + out + } } impl fmt::Debug for Id { -- 2.51.2