Lumiera is envisioned as a heavyweight yet coherent »Application« — not so much as platform, framework or operating system. And, in accordance with this focus, we place no emphasis on possible configuration changes and state transitions within the running application as a whole. And, while in fact it is possible to close and load a new Session, most of the time we assume the application just to be “up and running” — all required services to be available and interfaces to be accessible. Without doubt, these are simplifications, but serve us well to cut down complexity — yet still there needs to be one dedicated realm to deal with these specific concerns of configuration, dependency and lifecyle.
The Application Realm
So we treat all these concerns within a small and self contained structure, clearly
set apart from all the other layers, services and subsystems. This dedicated
Application Realm is organised around the “Application main object”.
[
This is the singleton lumiera::AppState
, which is triggered by the main
function
of the Lumiera Application. The sourcecode is kept in a separate folder src/common
and linked into the shared library liblumieracommon.so]
It serves the purpose of pulling up and tearing down the application in a controlled
fashion. And additionally, it provides the Interface and Config core services.
The act of building or tearing down this core realm and main object is what creates
the Lifecycle of the application. This is a succession of “lifecycle phases” — and almost all activities happen within the operational phase, when everything
is “up and running” or “just available”.
Subsystems
However, initially the application needs to be brought up, and at the end, all parts need to be unwound cleanly. To organise this process, we identify a limited number of Subsystems within the Application, which are more or less independent. Each Subsystem is self contained and groups some other parts and services, which typically work together and may be mutually dependent. These subsystems represent a grouping, applied for the purpose of starting and stopping the application in a regular way; they rather do not correspond 1:1 to a layer, an interface, a class or a plugin. As a matter of fact, they are rather irrelevant outside the mentioned »Application realm«. A subsystem may depend on other subsystems, which comprises a clear startup- and shutdown-ordering. However, once the application is in normal operational mode, the subsystems turn into a passive, guarding and managing role; the activities relevant for the application’s purpose rather rely on components, interfaces, services, all aggregated into the three Layers »Stage«, »Steam« and »Vault«.
We expect the following subsystems to be built eventually:
Engine, Session, PlayOut, GUI, Script runner, Renderfarm node.
Organisation of Subsystems
Not all subsystems need to be started for any use of the application. A script-driven use, or a renderfarm node does not need a GUI. So there is an overall global operation mode of the application, controlled through the launching options, and determined during the startup phase. It is the responsibility of the Application main object to pull up required functionality, which in turn might result in pulling up further subsystems as dependencies.
Subsystems are defined by implementing the interface lumiera::Subsys
, which acts
as façade to conduct the lifecycle, find out about dependencies and shut down
the subsystem in the end. So this interface, together with the Subsystem Runner,
define a lifecycle protocol; each subsystem is free to implement this as it
sees fit. Typically, this façade will load plugins, register and provide further
business interfaces, and especially set up the Layer separation interfaces
which canalise any communication going on between the layers.
The GUI Façade is special, while in compliance with this protocol. The actual
UI is loaded from a plug-in at runtime,
[This corresponds to the vision
to allow for different Lumiera UI’s — maybe to support different working styles
or target audiences. If such is actually feasible remains to be clarified as of
2020; even while decoupled on a technical level, the session still needs to make
a lot of assumptions regarding the UI’s capabilities and needs.]
and so the implementation of this façade needs to reside in the application core
realm; it will start a GuiRunner
to load and activate the GUI plug-in, which
then in turn has to open the public GUI Notification façade. The latter is
one of the Layer separation interfaces and comprises the actual way for the
lower layers to activate and interact with the user interface.
Parallelism
Actually this scheme builds on the assumption that starting each subsystem will not block the overall start/main/shutdown thread. Any subsystem is supposed to spawn its own control/event threads if necessary. The Lumiera application works fundamentally asynchronous. The user interface operates single threaded, in accordance to long established best practices of UI programming. However, any user interaction is translated into commands, sent down into the session and handled there one by one. The result of processing such commands will be pushed back up into the UI later and detached from the immediate interaction. Likewise, the re-rendering caused by changes in the session is carried out within the engine independently, relying on worker jobs and a thread pool.
Initialisation and Lifecycle
After some discussion,
[See the
GlobalInitialisation RfC
from spring 2008. In the beginning, we all agreed to “keep matters simple”
and build an init()
function within one central piece of code everyone knows
and hooks into. However, while the outline of the application emerged, there
was a tension between the concern about over-engineering versus the concern
about tangled and unmanageable complexity. At some point, an alternative
implementation based on lifecycle callbacks was elaborated, which then turned
into the solution described here. Lumiera then ceased to be the typical UI
application started by GTK, and the existing GTK code was retrofitted to
launch from within a plug-in.]
the design leaned toward loosely coupled parts and a formal lifecycle; which
saves us from investigating and coding up the various interdependencies
explicitly. Rather, the parts of the application have to comply to
Lifecycle Phases,
and each part has to care for its own state transitions, invoked through
lifecycle callbacks. We can distinguish two distinct models how to deal
with lifecycle, and both are equally acceptable:
-
Assuming that operations happen in response to some client’s request, this activation should go through a service interface. Interfaces can be opened and closed in Lumiera, and this is accomplished by hooking them up below some subsystem.
-
However, some parts carry out continuous activities, and in that case a lifecycle hook should be registered, to limit activities to the appropriate lifecycle phase.
Application Start
-
some fundamental language-level facilities will be prepared during static initialisation. At some point, execution enters
main(argc,arvv)
. -
AppState::init()
brings up the plugin loader and opens the config-interface. -
…followed by triggering ON_GLOBAL_INIT
-
the main thread then pulls up the subsystems (
AppState::maybeStart(subsystem)
), according to the command line options. -
within each subsystem, façade interfaces will be opened through the interface/plug-in system.
-
At this point, the GUI plug-in is loaded and launched, the windows created, the UI event loop starts and the application becomes live.
-
shutdown or failure of any given subsystem initiates the shutdown sequence by requesting all other running subsystems to terminate. In the typical case, the UI subsystem will trigger this shutdown sequence, in response to closing the main window.
-
there is an ON_GLOBAL_SHUTDOWN event, which can be used for normal cleanup; In case of an emergency exit, the ON_EMERGENCY_EXIT event is triggered alternatively.
-
the AppState destructor tears down the core systems (config-interface and pluginloader).
-
we establish a policy to prohibit any non-local and non-trivial activities during the tear-down phase after leaving the
main()
function.