| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //! This file contains for creating and serving a `Flatland` view using a `Framebuffer`. |
| //! |
| //! A lot of the code in this file is temporary to enable developers to see the contents of a |
| //! `Framebuffer` in the workstation UI (e.g., using `ffx session add`). |
| //! |
| //! To display the `Framebuffer` as its view, a component must add the `framebuffer` feature to its |
| //! `.cml`. |
| |
| use anyhow::anyhow; |
| use fidl::HandleBased; |
| use fidl::endpoints::{create_proxy, create_request_stream}; |
| use flatland_frame_scheduling_lib::{ |
| PresentationInfo, PresentedInfo, SchedulingLib, ThroughputScheduler, |
| }; |
| use fuchsia_component::client::{ |
| connect_to_protocol, connect_to_protocol_at_dir_root, connect_to_protocol_sync, |
| }; |
| use fuchsia_framebuffer::FrameUsage; |
| use fuchsia_framebuffer::sysmem::BufferCollectionAllocator; |
| use fuchsia_scenic::BufferCollectionTokenPair; |
| use fuchsia_scenic::flatland::ViewCreationTokenPair; |
| use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded}; |
| use futures::{FutureExt, StreamExt}; |
| use starnix_core::mm::memory::MemoryObject; |
| use starnix_core::task::dynamic_thread_spawner::SpawnRequestBuilder; |
| use starnix_core::task::{Kernel, LockedAndTask}; |
| use starnix_lifecycle::AtomicU64Counter; |
| use starnix_logging::log_error; |
| use starnix_sync::Mutex; |
| use starnix_uapi::errno; |
| use starnix_uapi::errors::Errno; |
| use std::ops::{Deref, DerefMut}; |
| use std::sync::Arc; |
| use std::sync::mpsc::channel; |
| use { |
| fidl_fuchsia_element as felement, fidl_fuchsia_images2 as fimages2, fidl_fuchsia_math as fmath, |
| fidl_fuchsia_ui_composition as fuicomposition, fidl_fuchsia_ui_views as fuiviews, |
| fuchsia_async as fasync, |
| }; |
| |
| /// The offset at which the framebuffer will be placed. |
| pub const TRANSLATION_X: i32 = 0; |
| |
| /// The Flatland identifier for the transform associated with the framebuffer. |
| const ROOT_TRANSFORM_ID: fuicomposition::TransformId = fuicomposition::TransformId { value: 1 }; |
| |
| /// The Flatland identifier for the framebuffer image. |
| const FB_IMAGE_ID: fuicomposition::ContentId = fuicomposition::ContentId { value: 1 }; |
| |
| /// The Scene states that `FramebufferServer` may serve. |
| #[derive(Copy, Clone)] |
| pub enum SceneState { |
| Fb, |
| Viewport, |
| } |
| |
| /// Unbounded sender used for presentation messages. |
| pub type PresentationSender = UnboundedSender<SceneState>; |
| |
| /// Unbounded receiver used for presentation messages. |
| pub type PresentationReceiver = UnboundedReceiver<SceneState>; |
| |
| /// A `FramebufferServer` contains initialized proxies to Flatland, as well as a buffer collection |
| /// that is registered with Flatland. |
| pub struct FramebufferServer { |
| /// The Flatland proxy associated with this server. |
| flatland: fuicomposition::FlatlandProxy, |
| |
| /// The width of the display and framebuffer image. |
| image_width: u32, |
| |
| /// The height of the display and framebuffer image. |
| image_height: u32, |
| |
| /// Keeps track if this class is serving FB or a Viewport. |
| scene_state: Arc<Mutex<SceneState>>, |
| |
| /// Keeps track of the Flatland viewport ID. |
| viewport_id: AtomicU64Counter, |
| |
| /// Channel to send Present requests on. |
| presentation_sender: PresentationSender, |
| |
| /// Channel to receive Present requests on. |
| presentation_receiver: Arc<Mutex<Option<PresentationReceiver>>>, |
| } |
| |
| impl FramebufferServer { |
| /// Returns a `FramebufferServer` that has created a scene and registered a buffer with |
| /// Flatland. |
| pub fn new(width: u32, height: u32) -> Result<(Self, Arc<MemoryObject>), Errno> { |
| let allocator = connect_to_protocol_sync::<fuicomposition::AllocatorMarker>() |
| .map_err(|_| errno!(ENOENT))?; |
| let flatland = |
| connect_to_protocol::<fuicomposition::FlatlandMarker>().map_err(|_| errno!(ENOENT))?; |
| flatland.set_debug_name("StarnixFrameBufferServer").map_err(|_| errno!(EINVAL))?; |
| |
| let memory = |
| init_fb_scene(&flatland, &allocator, width, height).map_err(|_| errno!(EINVAL))?; |
| |
| let (presentation_sender, presentation_receiver) = unbounded(); |
| Ok(( |
| Self { |
| flatland, |
| image_width: width, |
| image_height: height, |
| scene_state: Arc::new(Mutex::new(SceneState::Fb)), |
| viewport_id: (FB_IMAGE_ID.value + 1).into(), |
| presentation_sender: presentation_sender, |
| presentation_receiver: Arc::new(Mutex::new(Some(presentation_receiver))), |
| }, |
| memory.into(), |
| )) |
| } |
| |
| // Present according to the scene state. |
| pub fn present(&self) { |
| let scene_state = self.scene_state.lock(); |
| let scene_state = scene_state.deref(); |
| self.presentation_sender.unbounded_send(*scene_state).expect("send failed"); |
| } |
| } |
| |
| /// Initializes the flatland scene, and returns the associated buffer collection. |
| /// |
| /// SAFETY: This function `.expect`'s a lot, because it isn't meant to be used in the long time and |
| /// most of the failures would be unexpected and unrecoverable. |
| |
| fn init_fb_scene( |
| flatland: &fuicomposition::FlatlandProxy, |
| allocator: &fuicomposition::AllocatorSynchronousProxy, |
| width: u32, |
| height: u32, |
| ) -> Result<MemoryObject, anyhow::Error> { |
| flatland |
| .create_transform(&ROOT_TRANSFORM_ID) |
| .map_err(|_| anyhow!("error creating transform"))?; |
| flatland |
| .set_root_transform(&ROOT_TRANSFORM_ID) |
| .map_err(|_| anyhow!("error setting root transform"))?; |
| |
| let (collection_sender, collection_receiver) = channel(); |
| let (allocation_sender, allocation_receiver) = channel(); |
| // This thread is spawned to deal with the mix of asynchronous and synchronous proxies. |
| // In particular, we want to keep Framebuffer creation synchronous, while still making use of |
| // BufferCollectionAllocator (which exposes an async api). |
| // |
| // The spawned thread will execute the futures and send results back to this thread via a |
| // channel. |
| std::thread::Builder::new() |
| .name("kthread-fb-alloc".to_string()) |
| .spawn(move || -> Result<(), anyhow::Error> { |
| let mut executor = fasync::LocalExecutor::default(); |
| |
| let mut buffer_allocator = BufferCollectionAllocator::new( |
| width, |
| height, |
| fimages2::PixelFormat::R8G8B8A8, |
| FrameUsage::Cpu, |
| 1, |
| )?; |
| buffer_allocator.set_name(100, "Starnix View")?; |
| |
| let sysmem_buffer_collection_token = |
| executor.run_singlethreaded(buffer_allocator.duplicate_token())?; |
| // Notify the async code that the sysmem buffer collection token is available. |
| collection_sender |
| .send(sysmem_buffer_collection_token) |
| .expect("Failed to send collection"); |
| |
| let allocation = |
| executor.run_singlethreaded(buffer_allocator.allocate_buffers(true))?; |
| // Notify the async code that the buffer allocation completed. |
| allocation_sender.send(allocation).expect("Failed to send allocation"); |
| |
| Ok(()) |
| }) |
| .expect("able to create threads"); |
| |
| // Wait for the async code to generate the buffer collection token. |
| let sysmem_buffer_collection_token = collection_receiver |
| .recv() |
| .map_err(|_| anyhow!("Error receiving buffer collection token"))?; |
| |
| let buffer_tokens = BufferCollectionTokenPair::new(); |
| let args = fuicomposition::RegisterBufferCollectionArgs { |
| export_token: Some(buffer_tokens.export_token), |
| buffer_collection_token2: Some(sysmem_buffer_collection_token), |
| ..Default::default() |
| }; |
| |
| allocator |
| .register_buffer_collection(args, zx::MonotonicInstant::INFINITE) |
| .map_err(|_| anyhow!("FIDL error registering buffer collection"))? |
| .map_err(|_| anyhow!("Error registering buffer collection"))?; |
| |
| // Now that the buffer collection is registered, wait for the buffer allocation to happen. |
| let allocation = |
| allocation_receiver.recv().map_err(|_| anyhow!("Error receiving buffer allocation"))?; |
| |
| let image_props = fuicomposition::ImageProperties { |
| size: Some(fmath::SizeU { width, height }), |
| ..Default::default() |
| }; |
| flatland |
| .create_image(&FB_IMAGE_ID, buffer_tokens.import_token, 0, &image_props) |
| .map_err(|_| anyhow!("FIDL error creating image"))?; |
| flatland |
| .set_image_destination_size(&FB_IMAGE_ID, &fmath::SizeU { width, height }) |
| .expect("FIDL error resizing image"); |
| flatland |
| .set_content(&ROOT_TRANSFORM_ID, &FB_IMAGE_ID) |
| .map_err(|_| anyhow!("error setting content"))?; |
| flatland |
| .set_translation(&ROOT_TRANSFORM_ID, &fmath::Vec_ { x: TRANSLATION_X, y: 0 }) |
| .map_err(|_| anyhow!("error setting translation"))?; |
| |
| allocation.buffers.as_ref().unwrap()[0] |
| .vmo |
| .as_ref() |
| .ok_or_else(|| anyhow!("Failed to get VMO from allocation"))? |
| .duplicate_handle(zx::Rights::SAME_RIGHTS) |
| .map(MemoryObject::from) |
| .map_err(|_| anyhow!("Failed to create MemoryObject from allocation")) |
| } |
| |
| /// Initializes a flatland scene where only the child view is presented through |
| /// `ViewportCreationToken`. |
| pub fn init_viewport_scene( |
| server: Arc<FramebufferServer>, |
| viewport_token: fuiviews::ViewportCreationToken, |
| ) { |
| let (_, child_view_watcher_request) = create_proxy::<fuicomposition::ChildViewWatcherMarker>(); |
| let viewport_properties = fuicomposition::ViewportProperties { |
| logical_size: Some(fmath::SizeU { width: server.image_width, height: server.image_height }), |
| ..Default::default() |
| }; |
| let new_viewport = fuicomposition::ContentId { value: server.viewport_id.next() }; |
| let old_viewport = fuicomposition::ContentId { value: new_viewport.value - 1 }; |
| server |
| .flatland |
| .create_viewport( |
| &new_viewport, |
| viewport_token, |
| &viewport_properties, |
| child_view_watcher_request, |
| ) |
| .expect("failed to create child viewport"); |
| server.flatland.set_content(&ROOT_TRANSFORM_ID, &new_viewport).expect("error setting content"); |
| |
| { |
| let mut scene_state = server.scene_state.lock(); |
| let scene_state = scene_state.deref_mut(); |
| match scene_state { |
| // We are switching from Fb presentation to Viewport. We can clean up resources as this |
| // change only happens once and there is no switching back to Fb. |
| SceneState::Fb => { |
| server.flatland.release_image(&FB_IMAGE_ID).expect("failed to release image"); |
| } |
| SceneState::Viewport => { |
| let _ = server |
| .flatland |
| .release_viewport(&old_viewport) |
| .check() |
| .expect("failed to release child viewport"); |
| } |
| } |
| *scene_state = SceneState::Viewport; |
| } |
| server.present(); |
| } |
| |
| pub fn start_presentation_loop( |
| kernel: &Kernel, |
| server: Arc<FramebufferServer>, |
| view_bound_protocols: fuicomposition::ViewBoundProtocols, |
| view_identity: fuiviews::ViewIdentityOnCreation, |
| incoming_dir: Option<fidl_fuchsia_io::DirectoryProxy>, |
| ) { |
| let flatland = server.flatland.clone(); |
| let mut flatland_event_stream = flatland.take_event_stream(); |
| let mut presentation_receiver = server.presentation_receiver.lock().deref_mut().take().unwrap(); |
| let closure = async move |locked_and_task: LockedAndTask<'_>| { |
| let kernel = locked_and_task.current_task().kernel(); |
| let scheduler = ThroughputScheduler::new(); |
| let mut view_bound_protocols = Some(view_bound_protocols); |
| let mut view_identity = Some(view_identity); |
| let mut maybe_view_controller_proxy = None; |
| let mut scene_state = None; |
| let link_token_pair = |
| ViewCreationTokenPair::new().expect("failed to create ViewCreationTokenPair"); |
| // We don't actually care about the parent viewport at the moment, because we don't resize. |
| let (_parent_viewport_watcher, parent_viewport_watcher_request) = |
| create_proxy::<fuicomposition::ParentViewportWatcherMarker>(); |
| server |
| .flatland |
| .create_view2( |
| link_token_pair.view_creation_token, |
| view_identity |
| .take() |
| .expect("cannot create view because view identity has been consumed"), |
| view_bound_protocols |
| .take() |
| .expect("cannot create view because view bound protocols have been consumed"), |
| parent_viewport_watcher_request, |
| ) |
| .expect("FIDL error"); |
| |
| // Now that the view has been created, start presenting to Flatland. |
| // We must do this first because GraphicalPresenter can only |
| // service `present_view` once a child view is attached to the view |
| // tree. In order to attach, the child must have presented at least |
| // once. |
| server.present(); |
| let message = presentation_receiver.next().await; |
| if message.is_some() { |
| scene_state = message; |
| scheduler.request_present(); |
| let present_parameters = scheduler.wait_to_update().await; |
| flatland |
| .present(fuicomposition::PresentArgs { |
| requested_presentation_time: Some( |
| present_parameters.requested_presentation_time.into_nanos(), |
| ), |
| acquire_fences: None, |
| release_fences: None, |
| unsquashable: Some(present_parameters.unsquashable), |
| ..Default::default() |
| }) |
| .unwrap_or(()); |
| }; |
| |
| let graphical_presenter = if let Some(incoming_dir) = incoming_dir { |
| connect_to_protocol_at_dir_root::<felement::GraphicalPresenterMarker>(&incoming_dir) |
| .map_err(|_| errno!(ENOENT)) |
| .expect("Failed to connect to GraphicalPresenter") |
| } else { |
| kernel |
| .connect_to_protocol_at_container_svc::<felement::GraphicalPresenterMarker>() |
| .map_err(|_| errno!(ENOENT)) |
| .expect("Failed to connect to GraphicalPresenter") |
| .into_proxy() |
| }; |
| |
| let (view_controller_proxy, view_controller_server_end) = |
| fidl::endpoints::create_proxy::<felement::ViewControllerMarker>(); |
| let _ = maybe_view_controller_proxy.insert(view_controller_proxy); |
| |
| let view_spec = felement::ViewSpec { |
| annotations: Some(vec![felement::Annotation { |
| key: felement::AnnotationKey { |
| namespace: "window_manager".to_string(), |
| value: "view_id".to_string(), |
| }, |
| value: felement::AnnotationValue::Text("starnix_framebuffer".to_string()), |
| }]), |
| viewport_creation_token: Some(link_token_pair.viewport_creation_token), |
| ..Default::default() |
| }; |
| |
| // TODO: b/307790211 - Service annotation controller stream. |
| let (annotation_controller_client_end, _annotation_controller_stream) = |
| create_request_stream::<felement::AnnotationControllerMarker>(); |
| |
| // Wait for present_view before processing Flatland events. |
| graphical_presenter |
| .present_view( |
| view_spec, |
| Some(annotation_controller_client_end), |
| Some(view_controller_server_end), |
| ) |
| .await |
| .expect("failed to present view") |
| .unwrap_or_else(|e| println!("{:?}", e)); |
| |
| // Start presentation loop to prepare for display updates. |
| loop { |
| futures::select! { |
| message = presentation_receiver.next() => { |
| if message.is_some() { |
| scene_state = message; |
| scheduler.request_present(); |
| } |
| } |
| flatland_event = flatland_event_stream.next() => { |
| match flatland_event { |
| Some(Ok(fuicomposition::FlatlandEvent::OnNextFrameBegin{ values })) => { |
| let credits = values |
| .additional_present_credits |
| .expect("Present credits must exist"); |
| let infos = values |
| .future_presentation_infos |
| .expect("Future presentation infos must exist") |
| .iter() |
| .map( |
| |x| PresentationInfo{ |
| latch_point: zx::MonotonicInstant::from_nanos(x.latch_point.unwrap()), |
| presentation_time: zx::MonotonicInstant::from_nanos( |
| x.presentation_time.unwrap()) |
| }) |
| .collect(); |
| scheduler.on_next_frame_begin(credits, infos); |
| // Keep presenting as long as we are in Fb state. |
| match scene_state { |
| Some(SceneState::Fb) => { |
| scheduler.request_present(); |
| } |
| _ => {} |
| } |
| } |
| Some(Ok(fuicomposition::FlatlandEvent::OnFramePresented{ frame_presented_info })) => { |
| let actual_presentation_time = |
| zx::MonotonicInstant::from_nanos(frame_presented_info.actual_presentation_time); |
| let presented_infos: Vec<PresentedInfo> = |
| frame_presented_info.presentation_infos |
| .into_iter() |
| .map(|x| x.into()) |
| .collect(); |
| scheduler.on_frame_presented(actual_presentation_time, presented_infos); |
| } |
| Some(Ok(fuicomposition::FlatlandEvent::OnError{ error })) => { |
| log_error!( |
| "Received FlatlandError code: {}; exiting listener loop", |
| error.into_primitive() |
| ); |
| return; |
| } |
| _ => {} |
| } |
| } |
| present_parameters = scheduler.wait_to_update().fuse() => { |
| flatland |
| .present(fuicomposition::PresentArgs { |
| requested_presentation_time: Some( |
| present_parameters.requested_presentation_time.into_nanos(), |
| ), |
| acquire_fences: None, |
| release_fences: None, |
| unsquashable: Some(present_parameters.unsquashable), |
| ..Default::default() |
| }) |
| .unwrap_or(()); |
| } |
| } |
| } |
| }; |
| let req = SpawnRequestBuilder::new() |
| .with_debug_name("framebuffer-present") |
| .with_async_closure(closure) |
| .build(); |
| kernel.kthreads.spawner().spawn_from_request(req); |
| } |