At a high level Hummingbird does the following things to render your UI:
- Load and parse HTML files.
- Build a DOM of the page.
- Load and parse CSS styles. Styling allows having very rich interfaces with complex layout and visuals.
- Load image files and fonts.
- Load and execute JavaScript code. JavaScript is used to code more advanced logic for the user interface.
- Communicate with the client application to send/receive commands or input.
- Render the page in a texture. Hummingbird uses the powerful Coherent Labs' "Renoir" rendering library that draws the page in a texture that can be used by the client application.
In Hummingbird speak, a page along with its styles, DOM, JS code is called a View (the class cohtml::View
). You can have as many Views as you need in your application.
Hummingbird has an advanced task-based architecture that under the hood tries to make best use of all cores and computational power of the device, while saving battery at the same time. Key technical features of Hummingbird include:
- Task-based architecture. Many operations in Hummingbird run on work-threads, which improves scalability, overall performance and battery duration.
- Data-oriented design. Internally Hummingbird owes much of its performance to a modern data-oriented design.
- High-performance rendering with advanced batching. An additional performance boost is given by the fact that only changed parts of a page are re-drawn each frame - not the whole UI.
- Asynchronous resource loading. All resources are loaded in work-threads in order to not stall the main UI thread.
- Full control over memory allocations, file loading, logging, rendering. Users can control how memory and files are loaded. Custom rendering backends can be easily developed with the provided interfaces.
- Support for iOS, Android, Windows, PlayStation 4, Xbox One, UWP and Mac OS X. Linux support coming soon.
- Note
- Due to the asynchronous resource loading, styles are applied in the order in which the CSS declarations are parsed. This means that styles from external CSS files will be applied later and you may see the intermediate styles for the first few frames.
Multithreading
Key to the scalability and performance of Hummingbird is its multithreaded architecture. At the moment Hummingbird designates a "Main" or "UI" thread. This is the thread where the user has to interact with the UI logic - send events, input and execute JavaScript. The other thread the user interacts on is the "render" thread. This is usually the thread that submits commands on the GPU. On that thread the methods of the ViewRenderer
s should be called that in turn will execute the needed rendering commands. Additional work includes View resource loading, JavaScript garbage collection, View Layout etc. All these operations must be scheduled by the user in worker threads or dedicated threads should be created to handle those tasks.
Hummingbird will not create threads behind the scenes as they might interfere with the workload of the application.
The different work "channels" are:
- Main (UI): holds the DOM and executes the JavaScript code. The user submits input and sends events to the UI in this thread. This is the thread on which
Library::Initialize
was called. Often this is also the "main" thread of the application.
- Render thread: the
ViewRenderer
methods are called on this thread. It should have access to the rendering subsystem and the rendering commands will be executed on it. This is usually the render thread of the application. It can change, but you can only execute one "Paint" concurrently.
- N Worker threads & Layout threads: Hummingbird generates work for worker threads for many activities including: resource loading; parsing, layout etc. The Layout operation can also happen in the Main thread in the
View::Advance
method. This is controlled by the LibraryParams::UseDedicatedLayoutThread
flag. The Layout is a heavy operation and it's strongly recommended to let it happen on a worker thread. Using a layout thread is highly recommended as it will significantly reduce the View::Advance
time and improve scalability. All these operations must be executed by the application through the Library::ExecuteWork
method.
Worker threads execute Resources and Layout tasks. Resources tasks are global for all Hummingbird Views as they share the image loading and caching, parsing etc. resources. However the Layout tasks can be scheduled per-View. This allows better scalability and having a separate thread per View Layout is thus possible although not recommended.
- Note
- Both Resources and Layout work must be executed by the application at some point after it is notified, otherwise deadlock might happen. It is not possible to execute work in the same call stack of the notification method of the work
LibraryParams::OnWorkAvailable
, it will lead to a crash, work has to be scheduled. If you choose to dedicate threads only to Hummingbird, this is simpler as they'll automatically pick-up any new work.
The Hummingbird samples contain a TaskScheduler
sample class that shows different approaches on the threading integration of the library.
Integrating the task system of Hummingbird in an application
- Note
- The
TaskScheduler
class in the samples shows the different approaches to the integration.
The integration flow can go in two main ways:
- Dedicate one or more worker threads to Hummingbird's resource and Layout tasks. This is by far the easiest way to integrate, but might not be optimal in some situations. In this case the user only has to:
- Create one or more worker threads. If you'll be letting Layout happen on a separate worker thread, you'll need to create at least 2 threads - one for the Resources work and one for the Layout.
- Run on them the
Library::ExecuteWork
methods with WorkExecutionMode
set to WEM_UntilQuit
. The methods will not return until Library::StopWorkers
is called, it should happen just before Library::Uninitialize
.
- Integrate with the engine task system. The second option gives more control over scheduling. Most modern game engines have a task-based architecture and task systems that allow running work on separate threads. The Hummingbird work can be integrated with such a system and its task interleaved with the other engine work.
- Make sure to set a callback to receive notification when new Hummingbird work is available through
LibraryParams::OnWorkAvailable
- When the callback is called - inspect the type of work (Resources or Layout) in the
type
parameter
- Schedule a call to
Library::ExecuteWork
with the according WorkType
and WorkExecutionMode
WEM_UntilQueueEmpty
- Make sure the work is done in another call stack (usually a different thread). Failing to execute the work might lead to deadlocks.
The following diagram shows the approaches to the execution models described: