/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use constellation::SendableFrameTree; use compositing::compositor_layer::CompositorLayer; use compositing::*; use pipeline::CompositionPipeline; use platform::{Application, Window}; use windowing::{FinishedWindowEvent, IdleWindowEvent, LoadUrlWindowEvent, MouseWindowClickEvent}; use windowing::{MouseWindowEvent, MouseWindowEventClass, MouseWindowMouseDownEvent}; use windowing::{MouseWindowMouseUpEvent, MouseWindowMoveEventClass, NavigationWindowEvent}; use windowing::{QuitWindowEvent, RefreshWindowEvent, ResizeWindowEvent, ScrollWindowEvent}; use windowing::{WindowEvent, WindowMethods, WindowNavigateMsg, ZoomWindowEvent}; use azure::azure_hl::{SourceSurfaceMethods, Color}; use azure::azure_hl; use geom::matrix::identity; use geom::point::Point2D; use geom::rect::Rect; use geom::size::Size2D; use layers::layers::{ContainerLayer, ContainerLayerKind}; use layers::platform::surface::NativeCompositingGraphicsContext; use layers::rendergl; use layers::rendergl::RenderContext; use layers::scene::Scene; use opengles::gl2; use png; use servo_msg::compositor_msg::{Blank, Epoch, FinishedLoading, IdleRenderState, LayerBufferSet}; use servo_msg::compositor_msg::{LayerId, ReadyState, RenderState, ScrollPolicy, Scrollable}; use servo_msg::constellation_msg::{ConstellationChan, ExitMsg, LoadUrlMsg, NavigateMsg}; use servo_msg::constellation_msg::{PipelineId, ResizedWindowMsg}; use servo_msg::constellation_msg; use servo_util::opts::Opts; use servo_util::time::{profile, ProfilerChan}; use servo_util::{time, url}; use std::comm::{Empty, Disconnected, Data, Sender, Receiver}; use std::io::timer::sleep; use std::path::Path; use std::rc::Rc; use time::precise_time_s; pub struct IOCompositor { /// The application window. pub window: Rc, /// The port on which we receive messages. pub port: Receiver, /// The render context. pub context: RenderContext, /// The root ContainerLayer. pub root_layer: Rc, /// The root pipeline. pub root_pipeline: Option, /// The canvas to paint a page. pub scene: Scene, /// The application window size. pub window_size: Size2D, /// The platform-specific graphics context. pub graphics_context: NativeCompositingGraphicsContext, /// Tracks whether the renderer has finished its first rendering pub composite_ready: bool, /// Tracks whether we are in the process of shutting down. pub shutting_down: bool, /// Tracks whether we should close compositor. pub done: bool, /// Tracks whether we need to re-composite a page. pub recomposite: bool, /// Keeps track of the current zoom factor. pub world_zoom: f32, /// Tracks whether the zoom action has happend recently. pub zoom_action: bool, /// The time of the last zoom action has started. pub zoom_time: f64, /// Current display/reflow status of the page pub ready_state: ReadyState, /// Whether the page being rendered has loaded completely. /// Differs from ReadyState because we can finish loading (ready) /// many times for a single page. pub load_complete: bool, /// The command line option flags. pub opts: Opts, /// The root CompositorLayer pub compositor_layer: Option, /// The channel on which messages can be sent to the constellation. pub constellation_chan: ConstellationChan, /// The channel on which messages can be sent to the profiler. pub profiler_chan: ProfilerChan, /// Pending scroll to fragment event, if any pub fragment_point: Option> } impl IOCompositor { pub fn new(app: &Application, opts: Opts, port: Receiver, constellation_chan: ConstellationChan, profiler_chan: ProfilerChan) -> IOCompositor { let window: Rc = WindowMethods::new(app); // Create an initial layer tree. // // TODO: There should be no initial layer tree until the renderer creates one from the display // list. This is only here because we don't have that logic in the renderer yet. let root_layer = Rc::new(ContainerLayer()); let window_size = window.size(); let hidpi_factor = window.hidpi_factor(); root_layer.common.borrow_mut().set_transform(identity().scale(hidpi_factor, hidpi_factor, 1f32)); IOCompositor { window: window, port: port, opts: opts, context: rendergl::init_render_context(), root_layer: root_layer.clone(), root_pipeline: None, scene: Scene(ContainerLayerKind(root_layer), window_size, identity()), window_size: Size2D(window_size.width as uint, window_size.height as uint), graphics_context: CompositorTask::create_graphics_context(), composite_ready: false, shutting_down: false, done: false, recomposite: false, world_zoom: hidpi_factor, zoom_action: false, zoom_time: 0f64, ready_state: Blank, load_complete: false, compositor_layer: None, constellation_chan: constellation_chan, profiler_chan: profiler_chan, fragment_point: None } } pub fn create(app: &Application, opts: Opts, port: Receiver, constellation_chan: ConstellationChan, profiler_chan: ProfilerChan) { let mut compositor = IOCompositor::new(app, opts, port, constellation_chan, profiler_chan); // Starts the compositor, which listens for messages on the specified port. compositor.run(); } fn run (&mut self) { // Tell the constellation about the initial window size. { let ConstellationChan(ref chan) = self.constellation_chan; chan.send(ResizedWindowMsg(self.window_size)); } // Enter the main event loop. while !self.done { // Check for new messages coming from the rendering task. self.handle_message(); if self.done { // We have exited the compositor and passing window // messages to script may crash. debug!("Exiting the compositor due to a request from script."); break; } // Check for messages coming from the windowing system. let msg = self.window.recv(); self.handle_window_message(msg); // If asked to recomposite and renderer has run at least once if self.recomposite && self.composite_ready { self.recomposite = false; self.composite(); } sleep(10); // If a pinch-zoom happened recently, ask for tiles at the new resolution if self.zoom_action && precise_time_s() - self.zoom_time > 0.3 { self.zoom_action = false; self.ask_for_tiles(); } } // Clear out the compositor layers so that painting tasks can destroy the buffers. match self.compositor_layer { None => {} Some(ref mut layer) => layer.forget_all_tiles(), } // Drain compositor port, sometimes messages contain channels that are blocking // another task from finishing (i.e. SetIds) loop { match self.port.try_recv() { Empty | Disconnected => break, Data(_) => {}, } } // Tell the profiler to shut down. let ProfilerChan(ref chan) = self.profiler_chan; chan.send(time::ExitMsg); } fn handle_message(&mut self) { loop { match (self.port.try_recv(), self.shutting_down) { (Empty, _) => break, (Disconnected, _) => break, (Data(Exit(chan)), _) => { debug!("shutting down the constellation"); let ConstellationChan(ref con_chan) = self.constellation_chan; con_chan.send(ExitMsg); chan.send(()); self.shutting_down = true; } (Data(ShutdownComplete), _) => { debug!("constellation completed shutdown"); self.done = true; } (Data(ChangeReadyState(ready_state)), false) => { self.window.set_ready_state(ready_state); self.ready_state = ready_state; } (Data(ChangeRenderState(render_state)), false) => { self.change_render_state(render_state); } (Data(SetUnRenderedColor(pipeline_id, layer_id, color)), false) => { self.set_unrendered_color(pipeline_id, layer_id, color); } (Data(SetIds(frame_tree, response_chan, new_constellation_chan)), _) => { self.set_ids(frame_tree, response_chan, new_constellation_chan); } (Data(GetGraphicsMetadata(chan)), false) => { chan.send(Some(azure_hl::current_graphics_metadata())); } (Data(CreateRootCompositorLayerIfNecessary(pipeline_id, layer_id, size)), false) => { self.create_root_compositor_layer_if_necessary(pipeline_id, layer_id, size); } (Data(CreateDescendantCompositorLayerIfNecessary(pipeline_id, layer_id, rect, scroll_behavior)), false) => { self.create_descendant_compositor_layer_if_necessary(pipeline_id, layer_id, rect, scroll_behavior); } (Data(SetLayerPageSize(pipeline_id, layer_id, new_size, epoch)), false) => { self.set_layer_page_size(pipeline_id, layer_id, new_size, epoch); } (Data(SetLayerClipRect(pipeline_id, layer_id, new_rect)), false) => { self.set_layer_clip_rect(pipeline_id, layer_id, new_rect); } (Data(DeleteLayerGroup(id)), _) => { self.delete_layer(id); } (Data(Paint(pipeline_id, layer_id, new_layer_buffer_set, epoch)), false) => { self.paint(pipeline_id, layer_id, new_layer_buffer_set, epoch); } (Data(ScrollFragmentPoint(pipeline_id, layer_id, point)), false) => { self.scroll_fragment_to_point(pipeline_id, layer_id, point); } (Data(LoadComplete(..)), false) => { self.load_complete = true; } // When we are shutting_down, we need to avoid performing operations // such as Paint that may crash because we have begun tearing down // the rest of our resources. (_, true) => { } } } } fn change_render_state(&mut self, render_state: RenderState) { self.window.set_render_state(render_state); if render_state == IdleRenderState { self.composite_ready = true; } } // FIXME(#2004, pcwalton): Take the pipeline ID and layer ID into account. fn set_unrendered_color(&mut self, _: PipelineId, _: LayerId, color: Color) { match self.compositor_layer { Some(ref mut layer) => layer.unrendered_color = color, None => {} } } fn set_ids(&mut self, frame_tree: SendableFrameTree, response_chan: Sender<()>, new_constellation_chan: ConstellationChan) { response_chan.send(()); self.root_pipeline = Some(frame_tree.pipeline.clone()); // Initialize the new constellation channel by sending it the root window size. let window_size = self.window.size(); let window_size = Size2D(window_size.width as uint, window_size.height as uint); { let ConstellationChan(ref chan) = new_constellation_chan; chan.send(ResizedWindowMsg(window_size)); } self.constellation_chan = new_constellation_chan; } fn create_root_compositor_layer_if_necessary(&mut self, id: PipelineId, layer_id: LayerId, size: Size2D) { let (root_pipeline, root_layer_id) = match self.compositor_layer { Some(ref compositor_layer) if compositor_layer.pipeline.id == id => { (compositor_layer.pipeline.clone(), compositor_layer.id_of_first_child()) } _ => { match self.root_pipeline { Some(ref root_pipeline) => (root_pipeline.clone(), LayerId::null()), None => fail!("Compositor: Received new layer without initialized pipeline"), } } }; if layer_id != root_layer_id { let root_pipeline_id = root_pipeline.id; let mut new_layer = CompositorLayer::new_root(root_pipeline, size, self.opts.tile_size, self.opts.cpu_painting); let first_child = self.root_layer.first_child.borrow().clone(); match first_child { None => {} Some(old_layer) => { ContainerLayer::remove_child(self.root_layer.clone(), old_layer) } } assert!(new_layer.add_child_if_necessary(self.root_layer.clone(), root_pipeline_id, new_layer.id, layer_id, Rect(Point2D(0f32, 0f32), size), size, Scrollable)); ContainerLayer::add_child_start(self.root_layer.clone(), ContainerLayerKind(new_layer.root_layer.clone())); // Release all tiles from the layer before dropping it. for layer in self.compositor_layer.mut_iter() { layer.clear_all_tiles(); } self.compositor_layer = Some(new_layer); } self.ask_for_tiles(); } fn create_descendant_compositor_layer_if_necessary(&mut self, pipeline_id: PipelineId, layer_id: LayerId, rect: Rect, scroll_policy: ScrollPolicy) { match self.compositor_layer { Some(ref mut compositor_layer) => { assert!(compositor_layer.add_child_if_necessary(self.root_layer.clone(), pipeline_id, compositor_layer.id, layer_id, rect, compositor_layer.page_size .unwrap(), scroll_policy)) } None => fail!("Compositor: Received new layer without initialized pipeline"), }; self.ask_for_tiles(); } fn set_layer_page_size(&mut self, pipeline_id: PipelineId, layer_id: LayerId, new_size: Size2D, epoch: Epoch) { let (ask, move): (bool, bool) = match self.compositor_layer { Some(ref mut layer) => { let window_size = &self.window_size; let world_zoom = self.world_zoom; let page_window = Size2D(window_size.width as f32 / world_zoom, window_size.height as f32 / world_zoom); layer.resize(pipeline_id, layer_id, new_size, page_window, epoch); let move = self.fragment_point.take().map_or(false, |point| { layer.move(pipeline_id, layer_id, point, page_window) }); (true, move) } None => (false, false) }; if ask { self.recomposite_if(move); self.ask_for_tiles(); } } fn set_layer_clip_rect(&mut self, pipeline_id: PipelineId, layer_id: LayerId, new_rect: Rect) { let ask: bool = match self.compositor_layer { Some(ref mut layer) => { assert!(layer.set_clipping_rect(pipeline_id, layer_id, new_rect)); true } None => { false } }; if ask { self.ask_for_tiles(); } } fn delete_layer(&mut self, id: PipelineId) { let ask: bool = match self.compositor_layer { Some(ref mut layer) => { assert!(layer.delete(&self.graphics_context, id)); true } None => { false } }; if ask { self.ask_for_tiles(); } } fn paint(&mut self, pipeline_id: PipelineId, layer_id: LayerId, new_layer_buffer_set: ~LayerBufferSet, epoch: Epoch) { debug!("compositor received new frame"); // From now on, if we destroy the buffers, they will leak. let mut new_layer_buffer_set = new_layer_buffer_set; new_layer_buffer_set.mark_will_leak(); match self.compositor_layer { Some(ref mut layer) => { assert!(layer.add_buffers(&self.graphics_context, pipeline_id, layer_id, new_layer_buffer_set, epoch).is_none()); self.recomposite = true; } None => { fail!("compositor given paint command with no CompositorLayer initialized"); } } // TODO: Recycle the old buffers; send them back to the renderer to reuse if // it wishes. } fn scroll_fragment_to_point(&mut self, pipeline_id: PipelineId, layer_id: LayerId, point: Point2D) { let world_zoom = self.world_zoom; let page_window = Size2D(self.window_size.width as f32 / world_zoom, self.window_size.height as f32 / world_zoom); let (ask, move): (bool, bool) = match self.compositor_layer { Some(ref mut layer) if layer.pipeline.id == pipeline_id && !layer.hidden => { (true, layer.move(pipeline_id, layer_id, point, page_window)) } Some(_) | None => { self.fragment_point = Some(point); (false, false) } }; if ask { self.recomposite_if(move); self.ask_for_tiles(); } } fn handle_window_message(&mut self, event: WindowEvent) { match event { IdleWindowEvent => {} RefreshWindowEvent => { self.recomposite = true; } ResizeWindowEvent(width, height) => { self.on_resize_window_event(width, height); } LoadUrlWindowEvent(url_string) => { self.on_load_url_window_event(url_string); } MouseWindowEventClass(mouse_window_event) => { self.on_mouse_window_event_class(mouse_window_event); } MouseWindowMoveEventClass(cursor) => { self.on_mouse_window_move_event_class(cursor); } ScrollWindowEvent(delta, cursor) => { self.on_scroll_window_event(delta, cursor); } ZoomWindowEvent(magnification) => { self.on_zoom_window_event(magnification); } NavigationWindowEvent(direction) => { self.on_navigation_window_event(direction); } FinishedWindowEvent => { let exit = self.opts.exit_after_load; if exit { debug!("shutting down the constellation for FinishedWindowEvent"); let ConstellationChan(ref chan) = self.constellation_chan; chan.send(ExitMsg); self.shutting_down = true; } } QuitWindowEvent => { debug!("shutting down the constellation for QuitWindowEvent"); let ConstellationChan(ref chan) = self.constellation_chan; chan.send(ExitMsg); self.shutting_down = true; } } } fn on_resize_window_event(&mut self, width: uint, height: uint) { let new_size = Size2D(width, height); if self.window_size != new_size { debug!("osmain: window resized to {:u}x{:u}", width, height); self.window_size = new_size; let ConstellationChan(ref chan) = self.constellation_chan; chan.send(ResizedWindowMsg(new_size)) } else { debug!("osmain: dropping window resize since size is still {:u}x{:u}", width, height); } } fn on_load_url_window_event(&mut self, url_string: ~str) { debug!("osmain: loading URL `{:s}`", url_string); self.load_complete = false; let root_pipeline_id = match self.compositor_layer { Some(ref layer) => layer.pipeline.id.clone(), None => fail!("Compositor: Received LoadUrlWindowEvent without initialized compositor layers"), }; let msg = LoadUrlMsg(root_pipeline_id, url::parse_url(url_string, None)); let ConstellationChan(ref chan) = self.constellation_chan; chan.send(msg); } fn on_mouse_window_event_class(&self, mouse_window_event: MouseWindowEvent) { let world_zoom = self.world_zoom; let point = match mouse_window_event { MouseWindowClickEvent(_, p) => Point2D(p.x / world_zoom, p.y / world_zoom), MouseWindowMouseDownEvent(_, p) => Point2D(p.x / world_zoom, p.y / world_zoom), MouseWindowMouseUpEvent(_, p) => Point2D(p.x / world_zoom, p.y / world_zoom), }; for layer in self.compositor_layer.iter() { layer.send_mouse_event(mouse_window_event, point); } } fn on_mouse_window_move_event_class(&self, cursor: Point2D) { for layer in self.compositor_layer.iter() { layer.send_mouse_move_event(cursor); } } fn on_scroll_window_event(&mut self, delta: Point2D, cursor: Point2D) { let world_zoom = self.world_zoom; // TODO: modify delta to snap scroll to pixels. let page_delta = Point2D(delta.x as f32 / world_zoom, delta.y as f32 / world_zoom); let page_cursor: Point2D = Point2D(cursor.x as f32 / world_zoom, cursor.y as f32 / world_zoom); let page_window = Size2D(self.window_size.width as f32 / world_zoom, self.window_size.height as f32 / world_zoom); let mut scroll = false; for layer in self.compositor_layer.mut_iter() { scroll = layer.handle_scroll_event(page_delta, page_cursor, page_window) || scroll; } self.recomposite_if(scroll); self.ask_for_tiles(); } fn on_zoom_window_event(&mut self, magnification: f32) { self.zoom_action = true; self.zoom_time = precise_time_s(); let old_world_zoom = self.world_zoom; let window_size = &self.window_size; // Determine zoom amount self.world_zoom = (self.world_zoom * magnification).max(1.0); let world_zoom = self.world_zoom; { self.root_layer.common.borrow_mut().set_transform(identity().scale(world_zoom, world_zoom, 1f32)); } // Scroll as needed let page_delta = Point2D(window_size.width as f32 * (1.0 / world_zoom - 1.0 / old_world_zoom) * 0.5, window_size.height as f32 * (1.0 / world_zoom - 1.0 / old_world_zoom) * 0.5); // TODO: modify delta to snap scroll to pixels. let page_cursor = Point2D(-1f32, -1f32); // Make sure this hits the base layer let page_window = Size2D(window_size.width as f32 / world_zoom, window_size.height as f32 / world_zoom); for layer in self.compositor_layer.mut_iter() { layer.handle_scroll_event(page_delta, page_cursor, page_window); } self.recomposite = true; } fn on_navigation_window_event(&self, direction: WindowNavigateMsg) { let direction = match direction { windowing::Forward => constellation_msg::Forward, windowing::Back => constellation_msg::Back, }; let ConstellationChan(ref chan) = self.constellation_chan; chan.send(NavigateMsg(direction)) } /// Get BufferRequests from each layer. fn ask_for_tiles(&mut self) { let world_zoom = self.world_zoom; let window_size_page = Size2D(self.window_size.width as f32 / world_zoom, self.window_size.height as f32 / world_zoom); for layer in self.compositor_layer.mut_iter() { if !layer.hidden { let rect = Rect(Point2D(0f32, 0f32), window_size_page); let recomposite = layer.get_buffer_request(&self.graphics_context, rect, world_zoom) || self.recomposite; self.recomposite = recomposite; } else { debug!("Compositor: root layer is hidden!"); } } } fn composite(&mut self) { profile(time::CompositingCategory, self.profiler_chan.clone(), || { debug!("compositor: compositing"); // Adjust the layer dimensions as necessary to correspond to the size of the window. self.scene.size = self.window.size(); // Render the scene. match self.compositor_layer { Some(ref mut layer) => { self.scene.background_color.r = layer.unrendered_color.r; self.scene.background_color.g = layer.unrendered_color.g; self.scene.background_color.b = layer.unrendered_color.b; self.scene.background_color.a = layer.unrendered_color.a; } None => {} } rendergl::render_scene(self.context, &self.scene); }); // Render to PNG. We must read from the back buffer (ie, before // self.window.present()) as OpenGL ES 2 does not have glReadBuffer(). if self.load_complete && self.ready_state == FinishedLoading && self.opts.output_file.is_some() { let (width, height) = (self.window_size.width as uint, self.window_size.height as uint); let path = from_str::(*self.opts.output_file.get_ref()).unwrap(); let mut pixels = gl2::read_pixels(0, 0, width as gl2::GLsizei, height as gl2::GLsizei, gl2::RGB, gl2::UNSIGNED_BYTE); // flip image vertically (texture is upside down) let orig_pixels = pixels.clone(); let stride = width * 3; for y in range(0, height) { let dst_start = y * stride; let src_start = (height - y - 1) * stride; unsafe { pixels.mut_slice(dst_start, dst_start + stride) .copy_memory(orig_pixels.slice(src_start, src_start + stride).slice_to(stride)); } } let img = png::Image { width: width as u32, height: height as u32, color_type: png::RGB8, pixels: pixels, }; let res = png::store_png(&img, &path); assert!(res.is_ok()); debug!("shutting down the constellation after generating an output file"); let ConstellationChan(ref chan) = self.constellation_chan; chan.send(ExitMsg); self.shutting_down = true; } self.window.present(); let exit = self.opts.exit_after_load; if exit { debug!("shutting down the constellation for exit_after_load"); let ConstellationChan(ref chan) = self.constellation_chan; chan.send(ExitMsg); } } fn recomposite_if(&mut self, result: bool) { self.recomposite = result || self.recomposite; } }