A client requests a set of commands to be Presented as part of a future Scenic frame. A single Scenic frame can have multiple client "Presents", where each Present represents a Flatland instance's update to the global scene graph. This doc describes the architecture internal to Scenic for how a request becomes pixels.
The diagram below shows the steps a client Present follows when it is requested. Flatland is a multi-threaded system, with Flatland instances each living on its own thread, in addition to a central render thread which handles compositing.
Client makes various calls to the Flatland API to modify its scene graph, creating transforms and rectangles, and adding images. When a client creates an image, the image is registered with several implementations of the
BufferCollectionImporterAPI, which include Vulkan and Display Controller implementations. Each one creates an internal representation of the image to be used for its domain (e.g. Vulkan image for Vulkan) backed by sysmem (this becomes important later -- see step 8). When ready, the client then calls
Present()to commit them.
FrameSchedulerdecides when the
Present()call will be applied. It uses its own internal policy to wake up early enough to present the frame before the next vsync.
Each Flatland instance is associated with its own
UberStruct-- a collection of data local to a particular Flatland instance representing the most recent commit of that instance's presented state. The
UberStructrepresents a snapshot of the local state of a Flatland instance. As such, it contains only data and no references to external resources. The
UberStructfor the Flatland instance is updated at this time after
UberStructdata needs to be compiled by the
UberStructSystemis the central manager of all the uberstructs and is a system for aggregating the local data (uberstructs) from Flatland instances into a snapshot of the entire scene graph to be consumed by the render loop. To be more specific, the
UberStructSystemtakes an atomic snapshot of all of the Uberstructs whose acquire fences have been signaled. The intent is for separate worker threads to own each Flatland instance, compute local data (such as topology vectors) in their local thread, and then commit those vectors to the
UberStructSystemin a concurrent manner.
UberStructSystemdata is sent to the
Enginewhich flattens the data which comes from multiple Flatland instances into long 1-dimensional arrays of data which are suitable for low-level graphics and display processing. The engine is also where we perform occlusion culling on the rectangle renderable data.
At this point the rendering process diverges into two possible options. Flatland is capable of doing both direct-to-display compositing as well as Vulkan-based GPU compositing. The
DisplayCompositoris responsible for managing these two rendering paths, and choosing which to use for the set of render data for a particular frame. The direct-to-display render path is preferred, as it allows content to be directly composited onto device hardware layers, skipping costly rendering computations. However, there are only a limited number of layers (varying depending on the specific device) along with device specific rules as to how images can be applied to the layers. If these conditions are met, we skip directly to step 7. Otherwise we go to step 8.
During direct-to-display compositing, the
DisplayCompositorwill take the
RectangleRenderabledata and decompose each rectangle into
destdata for the layer it will be applied to.
Sourcedata refers to the cropping dimensions in texels of the image to be composited.
Destdata refers to the dimensions in screen-space pixels that the image will occupy on the display. We also attempt direct-to-display color conversion here.
If we are unable to do direct-to-display compositing, we fallback to GPU compositing. The render data is sent from the
VkRenderer. Prior to any of these steps listed here, when the client is setting images as content in their Flatland instance, those images are registered with the
VkRenderer(step 1) which creates Vulkan textures out of them that are backed by sysmem. So once we reach this step here, the work of creating Vulkan textures is already complete. This means that all the
VkRenderer::Render()function needs to do before passing the render data along to the
RectangleCompositor(step 9 below) is create a new
Escherframe, prepare the textures to be rendered by transitioning their image layouts and calculating their proper normalized UV coordinates, and setting up the Vulkan semaphores to signal when rendering has been completed.
The data gets passed from the
VkRendererdown to the
RectangleCompositoris a generic 2D Vulkan compositor, which makes use of Escher, to render arrays of image data onto an output image (framebuffer). For efficiency, opaque and transparent data is handled separately. Opaque data is looped over front-to-back and transparent data back-to-front, with the Vulkan CommandBuffers being updated along the way, along with other data necessary for rendering such as push constants.
Once all the commands have been pushed to the Vulkan command buffer, we go down into the GPU level, where the render data is being processed by our Flatland-specific vertex and fragment shaders. Additionally, if color conversion was not successfully applied when attempting direct-to-display rendering, the Flatland color-conversion shaders will also be invoked here, and will calculate the color correction values directly as a subpass after the main render pass.
Once all data has been applied to the display controller config, we call
ApplyConfigwhich commits the changes. Once this is done, all of the rendered data will become visible on the display.