| ... | @@ -1,5 +1,5 @@ | ... | @@ -1,5 +1,5 @@ |
| 1 | //! A canvas page behind a C surface for native mobile shells: notebooks and their sections | 1 | //! A canvas page behind a C surface for native mobile shells: notebooks and their sections |
| 2 | //! (`library`), one page drawn into a `CAMetalLayer`, and the active outline's text as the | 2 | //! (`library`), one page drawn into a native window, and the active outline's text as the |
| 3 | //! flat UTF-16 model UIKit's `UITextInput` speaks (see `TextOutline::shown_text`). Every | 3 | //! flat UTF-16 model UIKit's `UITextInput` speaks (see `TextOutline::shown_text`). Every |
| 4 | //! edit a view takes is stored at once through its section. | 4 | //! edit a view takes is stored at once through its section. |
| 5 | //! | 5 | //! |
| ... | @@ -616,9 +616,10 @@ struct Gpu { | ... | @@ -616,9 +616,10 @@ struct Gpu { |
| 616 | instance: wgpu::Instance, | 616 | instance: wgpu::Instance, |
| 617 | adapter: wgpu::Adapter, | 617 | adapter: wgpu::Adapter, |
| 618 | renderer: draw::Renderer, | 618 | renderer: draw::Renderer, |
| | 619 | format: wgpu::TextureFormat, |
| 619 | } | 620 | } |
| 620 | | 621 | |
| 621 | /// Made with the first view's surface, whose adapter every later layer can use. | 622 | /// Made with the first view's surface and reused when the host recreates its window. |
| 622 | static GPU: Mutex<Option<Gpu>> = Mutex::new(None); | 623 | static GPU: Mutex<Option<Gpu>> = Mutex::new(None); |
| 623 | | 624 | |
| 624 | fn gpu() -> std::sync::MutexGuard<'static, Option<Gpu>> { | 625 | fn gpu() -> std::sync::MutexGuard<'static, Option<Gpu>> { |
| ... | @@ -677,37 +678,74 @@ pub extern "C" fn sb_set_markdown_shortcuts(on: bool) { | ... | @@ -677,37 +678,74 @@ pub extern "C" fn sb_set_markdown_shortcuts(on: bool) { |
| 677 | options().2 = on; | 678 | options().2 = on; |
| 678 | } | 679 | } |
| 679 | | 680 | |
| 680 | pub struct View { | 681 | #[cfg(target_os = "android")] |
| 681 | /// Taken apart on drop, which returns its text engine to `ENGINES`. | 682 | #[link(name = "android")] |
| 682 | canvas: ManuallyDrop<Canvas>, | 683 | unsafe extern "C" { |
| | 684 | fn ANativeWindow_acquire(window: *mut c_void); |
| | 685 | fn ANativeWindow_release(window: *mut c_void); |
| | 686 | } |
| | 687 | |
| | 688 | #[cfg(target_os = "android")] |
| | 689 | struct NativeWindow(std::ptr::NonNull<c_void>); |
| | 690 | |
| | 691 | #[cfg(target_os = "android")] |
| | 692 | impl Drop for NativeWindow { |
| | 693 | fn drop(&mut self) { |
| | 694 | // SAFETY: this guard owns one reference acquired by `Presentation::new`. |
| | 695 | unsafe { ANativeWindow_release(self.0.as_ptr()) }; |
| | 696 | } |
| | 697 | } |
| | 698 | |
| | 699 | fn view_pixels(size: [f32; 2], scale: f32, limit: u32) -> Result<[u32; 2]> { |
| | 700 | if !scale.is_finite() |
| | 701 | || scale <= 0.0 |
| | 702 | || size.iter().any(|side| !side.is_finite() || *side <= 0.0) |
| | 703 | { |
| | 704 | return Err("Canvas dimensions and scale must be finite and positive".into()); |
| | 705 | } |
| | 706 | let pixels = size.map(|side| (side * scale).round().max(1.0)); |
| | 707 | if pixels |
| | 708 | .iter() |
| | 709 | .any(|side| f64::from(*side) > f64::from(limit)) |
| | 710 | { |
| | 711 | return Err("Canvas dimensions exceed the GPU texture limit".into()); |
| | 712 | } |
| | 713 | Ok(pixels.map(|side| side as u32)) |
| | 714 | } |
| | 715 | |
| | 716 | struct Presentation { |
| 683 | surface: wgpu::Surface<'static>, | 717 | surface: wgpu::Surface<'static>, |
| | 718 | /// Field order keeps the acquired window alive until the surface has dropped. |
| | 719 | #[cfg(target_os = "android")] |
| | 720 | _window: NativeWindow, |
| 684 | config: wgpu::SurfaceConfiguration, | 721 | config: wgpu::SurfaceConfiguration, |
| 685 | frame: Arc<Frame>, | | |
| 686 | section: Arc<Shared>, | | |
| 687 | /// A conflict page, which shows what is stored and takes no edits. | | |
| 688 | read_only: bool, | | |
| 689 | /// The column, in page points, a reading view reflows the page into; it takes no edits. | | |
| 690 | reading: Option<f32>, | | |
| 691 | } | 722 | } |
| 692 | | 723 | |
| 693 | impl View { | 724 | impl Presentation { |
| 694 | fn new( | 725 | /// # Safety |
| 695 | layer: *mut c_void, | 726 | /// `window` is null or a live platform window; on Apple it outlives the presentation. |
| 696 | section: &Section, | 727 | unsafe fn new(window: *mut c_void, pixels: [u32; 2]) -> Result<Self> { |
| 697 | space: ExGuid, | 728 | let window = std::ptr::NonNull::new(window).ok_or("No native window")?; |
| 698 | size: [f32; 2], | 729 | #[cfg(target_os = "android")] |
| 699 | scale: f32, | 730 | let window = { |
| 700 | ) -> Result<Self> { | 731 | // SAFETY: the caller lends a live ANativeWindow for this call. |
| 701 | let pixels = size.map(|side| (side * scale).round().max(1.0) as u32); | 732 | unsafe { ANativeWindow_acquire(window.as_ptr()) }; |
| | 733 | NativeWindow(window) |
| | 734 | }; |
| | 735 | #[cfg(target_os = "android")] |
| | 736 | let target = wgpu::SurfaceTargetUnsafe::RawHandle { |
| | 737 | raw_display_handle: Some(wgpu::rwh::AndroidDisplayHandle::new().into()), |
| | 738 | raw_window_handle: wgpu::rwh::AndroidNdkWindowHandle::new(window.0).into(), |
| | 739 | }; |
| | 740 | #[cfg(not(target_os = "android"))] |
| | 741 | let target = wgpu::SurfaceTargetUnsafe::CoreAnimationLayer(window.as_ptr()); |
| 702 | let mut gpu = gpu(); | 742 | let mut gpu = gpu(); |
| 703 | let instance = match &*gpu { | 743 | let instance = match &*gpu { |
| 704 | Some(gpu) => gpu.instance.clone(), | 744 | Some(gpu) => gpu.instance.clone(), |
| 705 | None => wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()), | 745 | None => wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()), |
| 706 | }; | 746 | }; |
| 707 | // SAFETY: the host passes a live CAMetalLayer that outlives the view. | 747 | // SAFETY: Android retains its window; the Apple caller keeps its layer alive. |
| 708 | let surface = unsafe { | 748 | let surface = unsafe { instance.create_surface_unsafe(target) }?; |
| 709 | instance.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::CoreAnimationLayer(layer)) | | |
| 710 | }?; | | |
| 711 | if gpu.is_none() { | 749 | if gpu.is_none() { |
| 712 | let adapter = | 750 | let adapter = |
| 713 | pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { | 751 | pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { |
| ... | @@ -725,19 +763,71 @@ impl View { | ... | @@ -725,19 +763,71 @@ impl View { |
| 725 | .ok_or("No supported surface configuration")? | 763 | .ok_or("No supported surface configuration")? |
| 726 | .format; | 764 | .format; |
| 727 | if let Ok(mut renderer) = crash_report::RENDERER.lock() { | 765 | if let Ok(mut renderer) = crash_report::RENDERER.lock() { |
| 728 | *renderer = format!("Metal ({})", adapter.get_info().name); | 766 | let info = adapter.get_info(); |
| | 767 | *renderer = format!("{:?} ({})", info.backend, info.name); |
| 729 | } | 768 | } |
| 730 | *gpu = Some(Gpu { | 769 | *gpu = Some(Gpu { |
| 731 | instance, | 770 | instance, |
| 732 | renderer: draw::Renderer::new(device, queue, format), | 771 | renderer: draw::Renderer::new(device, queue, format), |
| 733 | adapter, | 772 | adapter, |
| | 773 | format, |
| 734 | }); | 774 | }); |
| 735 | } | 775 | } |
| 736 | let gpu = gpu.as_ref().ok_or("No GPU")?; | 776 | let gpu = gpu.as_ref().ok_or("No GPU")?; |
| 737 | let config = surface | 777 | if pixels |
| | 778 | .iter() |
| | 779 | .any(|side| *side > gpu.renderer.device().limits().max_texture_dimension_2d) |
| | 780 | { |
| | 781 | return Err("Canvas dimensions exceed the GPU texture limit".into()); |
| | 782 | } |
| | 783 | if !surface |
| | 784 | .get_capabilities(&gpu.adapter) |
| | 785 | .formats |
| | 786 | .contains(&gpu.format) |
| | 787 | { |
| | 788 | return Err( |
| | 789 | "The native window does not support the shared renderer's texture format".into(), |
| | 790 | ); |
| | 791 | } |
| | 792 | let mut config = surface |
| 738 | .get_default_config(&gpu.adapter, pixels[0], pixels[1]) | 793 | .get_default_config(&gpu.adapter, pixels[0], pixels[1]) |
| 739 | .ok_or("No supported surface configuration")?; | 794 | .ok_or("No supported surface configuration")?; |
| | 795 | config.format = gpu.format; |
| 740 | surface.configure(gpu.renderer.device(), &config); | 796 | surface.configure(gpu.renderer.device(), &config); |
| | 797 | Ok(Self { |
| | 798 | surface, |
| | 799 | #[cfg(target_os = "android")] |
| | 800 | _window: window, |
| | 801 | config, |
| | 802 | }) |
| | 803 | } |
| | 804 | } |
| | 805 | |
| | 806 | pub struct View { |
| | 807 | /// Taken apart on drop, which returns its text engine to `ENGINES`. |
| | 808 | canvas: ManuallyDrop<Canvas>, |
| | 809 | presentation: Option<Presentation>, |
| | 810 | frame: Arc<Frame>, |
| | 811 | section: Arc<Shared>, |
| | 812 | /// A conflict page, which shows what is stored and takes no edits. |
| | 813 | read_only: bool, |
| | 814 | /// The column, in page points, a reading view reflows the page into; it takes no edits. |
| | 815 | reading: Option<f32>, |
| | 816 | } |
| | 817 | |
| | 818 | impl View { |
| | 819 | /// # Safety |
| | 820 | /// `window` is null or a live platform window; on Apple it outlives the returned view. |
| | 821 | unsafe fn new( |
| | 822 | window: *mut c_void, |
| | 823 | section: &Section, |
| | 824 | space: ExGuid, |
| | 825 | size: [f32; 2], |
| | 826 | scale: f32, |
| | 827 | ) -> Result<Self> { |
| | 828 | let pixels = view_pixels(size, scale, u32::MAX)?; |
| | 829 | // SAFETY: the caller supplies the platform window's lifetime contract. |
| | 830 | let presentation = unsafe { Presentation::new(window, pixels) }?; |
| 741 | let (mut page, read_only) = section.shared.page(space)?; | 831 | let (mut page, read_only) = section.shared.page(space)?; |
| 742 | // The page wears its theme: style objects it gives otherwise are restyled first. | 832 | // The page wears its theme: style objects it gives otherwise are restyled first. |
| 743 | let sheet = section.sheet(page.identity); | 833 | let sheet = section.sheet(page.identity); |
| ... | @@ -758,8 +848,7 @@ impl View { | ... | @@ -758,8 +848,7 @@ impl View { |
| 758 | canvas.page.editor.styles = sheet; | 848 | canvas.page.editor.styles = sheet; |
| 759 | Ok(Self { | 849 | Ok(Self { |
| 760 | canvas: ManuallyDrop::new(canvas), | 850 | canvas: ManuallyDrop::new(canvas), |
| 761 | surface, | 851 | presentation: Some(presentation), |
| 762 | config, | | |
| 763 | frame, | 852 | frame, |
| 764 | section: Arc::clone(&section.shared), | 853 | section: Arc::clone(&section.shared), |
| 765 | read_only, | 854 | read_only, |
| ... | @@ -768,13 +857,18 @@ impl View { | ... | @@ -768,13 +857,18 @@ impl View { |
| 768 | } | 857 | } |
| 769 | | 858 | |
| 770 | fn render(&mut self) -> Result<()> { | 859 | fn render(&mut self) -> Result<()> { |
| | 860 | let Some(presentation) = &mut self.presentation else { |
| | 861 | return Ok(()); |
| | 862 | }; |
| 771 | let mut gpu = gpu(); | 863 | let mut gpu = gpu(); |
| 772 | let renderer = &mut gpu.as_mut().ok_or("No GPU")?.renderer; | 864 | let renderer = &mut gpu.as_mut().ok_or("No GPU")?.renderer; |
| 773 | let frame = match self.surface.get_current_texture() { | 865 | let frame = match presentation.surface.get_current_texture() { |
| 774 | wgpu::CurrentSurfaceTexture::Success(frame) | 866 | wgpu::CurrentSurfaceTexture::Success(frame) |
| 775 | | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame, | 867 | | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame, |
| 776 | wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => { | 868 | wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => { |
| 777 | self.surface.configure(renderer.device(), &self.config); | 869 | presentation |
| | 870 | .surface |
| | 871 | .configure(renderer.device(), &presentation.config); |
| 778 | return Ok(()); | 872 | return Ok(()); |
| 779 | } | 873 | } |
| 780 | _ => return Ok(()), | 874 | _ => return Ok(()), |
| ... | @@ -785,10 +879,13 @@ impl View { | ... | @@ -785,10 +879,13 @@ impl View { |
| 785 | .page | 879 | .page |
| 786 | .update_pictures(paper, &Waker::from(self.frame.clone())); | 880 | .update_pictures(paper, &Waker::from(self.frame.clone())); |
| 787 | let primitives = canvas.page.primitives(canvas.colors())?; | 881 | let primitives = canvas.page.primitives(canvas.colors())?; |
| | 882 | let target = frame.texture.create_view(&Default::default()); |
| | 883 | #[cfg(target_os = "macos")] |
| | 884 | let target = target.into(); |
| 788 | renderer | 885 | renderer |
| 789 | .draw( | 886 | .draw( |
| 790 | &frame.texture.create_view(&Default::default()).into(), | 887 | &target, |
| 791 | [self.config.width, self.config.height], | 888 | [presentation.config.width, presentation.config.height], |
| 792 | paper.color, | 889 | paper.color, |
| 793 | &[canvas.page.viewport.layer(&primitives)], | 890 | &[canvas.page.viewport.layer(&primitives)], |
| 794 | ) | 891 | ) |
| ... | @@ -853,7 +950,7 @@ impl View { | ... | @@ -853,7 +950,7 @@ impl View { |
| 853 | }; | 950 | }; |
| 854 | pool_engine(engine); | 951 | pool_engine(engine); |
| 855 | self.reading = column.filter(|_| reflowed.is_some()); | 952 | self.reading = column.filter(|_| reflowed.is_some()); |
| 856 | let pixels = [self.config.width, self.config.height]; | 953 | let pixels = self.canvas.page.viewport.size; |
| 857 | let mut canvas = Canvas::new( | 954 | let mut canvas = Canvas::new( |
| 858 | self.canvas.space, | 955 | self.canvas.space, |
| 859 | reflowed.unwrap_or(page), | 956 | reflowed.unwrap_or(page), |
| ... | @@ -899,14 +996,18 @@ fn owned(text: String) -> *mut c_char { | ... | @@ -899,14 +996,18 @@ fn owned(text: String) -> *mut c_char { |
| 899 | CString::new(text.replace('\0', "")).map_or(std::ptr::null_mut(), CString::into_raw) | 996 | CString::new(text.replace('\0', "")).map_or(std::ptr::null_mut(), CString::into_raw) |
| 900 | } | 997 | } |
| 901 | | 998 | |
| 902 | /// A view drawing page `id` of `section` into `layer`, a `CAMetalLayer`, `size` points at | 999 | /// A view drawing page `id` of `section`, `width` by `height` host points at `scale` |
| 903 | /// `scale` pixels per point; null if it cannot. | 1000 | /// pixels per point; null if it cannot. Android exports this as `sb_view_new_android`. |
| 904 | /// | 1001 | /// |
| 905 | /// # Safety | 1002 | /// # Safety |
| 906 | /// `layer` is a live `CAMetalLayer` that outlives the view; `id` is NUL-terminated UTF-8. | 1003 | /// `window` is null or a live `ANativeWindow` on Android, or a `CAMetalLayer` that outlives the |
| 907 | #[unsafe(no_mangle)] | 1004 | /// view on Apple. Android acquires its own reference without consuming the caller's. |
| | 1005 | /// The host serializes view calls and detaches Android views before destroying their surfaces. |
| | 1006 | /// `id` is NUL-terminated UTF-8. |
| | 1007 | #[cfg_attr(target_os = "android", unsafe(export_name = "sb_view_new_android"))] |
| | 1008 | #[cfg_attr(not(target_os = "android"), unsafe(no_mangle))] |
| 908 | pub unsafe extern "C" fn sb_view_new( | 1009 | pub unsafe extern "C" fn sb_view_new( |
| 909 | layer: *mut c_void, | 1010 | window: *mut c_void, |
| 910 | section: &Section, | 1011 | section: &Section, |
| 911 | id: *const c_char, | 1012 | id: *const c_char, |
| 912 | width: f32, | 1013 | width: f32, |
| ... | @@ -917,11 +1018,40 @@ pub unsafe extern "C" fn sb_view_new( | ... | @@ -917,11 +1018,40 @@ pub unsafe extern "C" fn sb_view_new( |
| 917 | string(id) | 1018 | string(id) |
| 918 | .parse() | 1019 | .parse() |
| 919 | .map_err(Into::into) | 1020 | .map_err(Into::into) |
| 920 | .and_then(|space| View::new(layer, section, space, [width, height], scale)), | 1021 | .and_then(|space| unsafe { View::new(window, section, space, [width, height], scale) }), |
| 921 | ) | 1022 | ) |
| 922 | .map_or(std::ptr::null_mut(), |view| Box::into_raw(Box::new(view))) | 1023 | .map_or(std::ptr::null_mut(), |view| Box::into_raw(Box::new(view))) |
| 923 | } | 1024 | } |
| 924 | | 1025 | |
| | 1026 | /// Stops presentation without changing the editor, selection or Undo history. |
| | 1027 | #[cfg(any(target_os = "android", test))] |
| | 1028 | #[cfg_attr(target_os = "android", unsafe(no_mangle))] |
| | 1029 | pub extern "C" fn sb_view_detach_android(view: &mut View) { |
| | 1030 | view.presentation = None; |
| | 1031 | } |
| | 1032 | |
| | 1033 | /// Attaches a replacement window at the last size and scale; resize after SurfaceChanged. |
| | 1034 | /// Failure leaves a detached editor unchanged; an attached view must be detached first. |
| | 1035 | /// |
| | 1036 | /// # Safety |
| | 1037 | /// `window` is null or a live ANativeWindow. The view acquires its own reference. |
| | 1038 | /// The host serializes view calls and detaches before SurfaceDestroyed returns. |
| | 1039 | #[cfg(any(target_os = "android", test))] |
| | 1040 | #[cfg_attr(target_os = "android", unsafe(no_mangle))] |
| | 1041 | pub unsafe extern "C" fn sb_view_attach_android(view: &mut View, window: *mut c_void) -> bool { |
| | 1042 | report(if view.presentation.is_some() { |
| | 1043 | Err("Detach the current surface before attaching another".into()) |
| | 1044 | } else { |
| | 1045 | // SAFETY: the caller supplies the platform window's lifetime contract. |
| | 1046 | unsafe { Presentation::new(window, view.canvas.page.viewport.size) } |
| | 1047 | }) |
| | 1048 | .map(|presentation| { |
| | 1049 | view.presentation = Some(presentation); |
| | 1050 | view.frame.0.store(true, Ordering::Release); |
| | 1051 | }) |
| | 1052 | .is_some() |
| | 1053 | } |
| | 1054 | |
| 925 | /// # Safety | 1055 | /// # Safety |
| 926 | /// `view` came from `sb_view_new` and is not used again. | 1056 | /// `view` came from `sb_view_new` and is not used again. |
| 927 | #[unsafe(no_mangle)] | 1057 | #[unsafe(no_mangle)] |
| ... | @@ -931,17 +1061,27 @@ pub unsafe extern "C" fn sb_view_free(view: *mut View) { | ... | @@ -931,17 +1061,27 @@ pub unsafe extern "C" fn sb_view_free(view: *mut View) { |
| 931 | | 1061 | |
| 932 | #[unsafe(no_mangle)] | 1062 | #[unsafe(no_mangle)] |
| 933 | pub extern "C" fn sb_view_resize(view: &mut View, width: f32, height: f32, scale: f32) { | 1063 | pub extern "C" fn sb_view_resize(view: &mut View, width: f32, height: f32, scale: f32) { |
| 934 | let pixels = [width, height].map(|side| (side * scale).round().max(1.0) as u32); | 1064 | let gpu = gpu(); |
| | 1065 | let Some(gpu) = gpu.as_ref() else { return }; |
| | 1066 | let Some(pixels) = report(view_pixels( |
| | 1067 | [width, height], |
| | 1068 | scale, |
| | 1069 | gpu.renderer.device().limits().max_texture_dimension_2d, |
| | 1070 | )) else { |
| | 1071 | return; |
| | 1072 | }; |
| 935 | let canvas = &mut view.canvas; | 1073 | let canvas = &mut view.canvas; |
| 936 | if scale != canvas.scale { | 1074 | if scale != canvas.scale { |
| 937 | canvas.scale = scale; | 1075 | canvas.scale = scale; |
| 938 | let _ = report(canvas.page.scale_factor_changed(scale)); | 1076 | let _ = report(canvas.page.scale_factor_changed(scale)); |
| 939 | } | 1077 | } |
| 940 | let _ = report(canvas.page.resized(pixels)); | 1078 | let _ = report(canvas.page.resized(pixels)); |
| 941 | view.config.width = pixels[0]; | 1079 | if let Some(presentation) = &mut view.presentation { |
| 942 | view.config.height = pixels[1]; | 1080 | presentation.config.width = pixels[0]; |
| 943 | if let Some(gpu) = &*gpu() { | 1081 | presentation.config.height = pixels[1]; |
| 944 | view.surface.configure(gpu.renderer.device(), &view.config); | 1082 | presentation |
| | 1083 | .surface |
| | 1084 | .configure(gpu.renderer.device(), &presentation.config); |
| 945 | } | 1085 | } |
| 946 | } | 1086 | } |
| 947 | | 1087 | |