Document the new 1.9.0 scenes (nothing about locking yet).
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Fri, 19 Jul 2019 19:45:03 +0000 (21:45 +0200)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Fri, 19 Jul 2019 19:45:03 +0000 (21:45 +0200)
theme.rst

index 04146e8..39424bd 100644 (file)
--- a/theme.rst
+++ b/theme.rst
@@ -1,6 +1,14 @@
 The theme
 =========
 
+**NOTE**: Nageru 1.9.0 made significant improvements to themes
+and how scenes work. If you use an older version, you may want
+to look at `the 1.8.6 documentation <https://nageru.sesse.net/doc-1.8.6/>`_;
+themes written for older versions still work without modification in
+1.9.0, but are not documented here, and you are advised to change
+to use the new interfaces, as they are equally powerful and much simpler
+to work with.
+
 In Nageru, most of the business logic around how your stream
 ends up looking is governed by the **theme**, much like how a
 theme works on a blog or a CMS. Most importantly, the theme
@@ -21,17 +29,18 @@ setup with side-by-side for e.g. conferences, and a minimal one
 that is easier for new users to understand.
 
 
-Introduction to chains
+Introduction to scenes
 ----------------------
 
 Anything that's shown on the stream, or on one of the preview displays,
-is created by a **Movit chain**. `Movit <https://movit.sesse.net/>`_
+is created by a **Movit chain**, instantiated by a Nageru **scene**.
+`Movit <https://movit.sesse.net/>`_
 is a library for high-quality, high-performance video filters,
 and Nageru's themes can use a simplified version of Movit's API where
 most of the low-level details are abstracted away.
 
-Every frame, the theme chooses a chain and a set of parameters to it,
-based on what it thinks the picture should look like. Every chain
+Every frame, the theme chooses a **scene** and a set of parameters to it,
+based on what it thinks the picture should look like. Every scene
 consists of a set of *inputs* (which can be either live video streams
 or static pictures) and then a set of operators or *effects* to combine
 or modify each other. Movit compiles these down to a set of shaders
@@ -41,55 +50,46 @@ especially if you use `LuaJIT <http://luajit.org/>`_) will not matter
 much.
 
 
-High- and low-quality chains
-----------------------------
+Basic scenes
+------------
 
-The simplest possible chain takes only in an input and sends it on
+The simplest possible scene takes only in an input and sends it on
 to the display (the output of the last added node is always sent to
 the screen, and in this case, that would be the input)::
 
-  local chain = EffectChain.new(16, 9)  -- Aspect ratio.
-  local input = chain:add_live_input(false, false)  -- No bounce override, no deinterlacing.
-  input:connect_signal(0)  -- First input card. Can be changed whenever you want.
-  chain:finalize(hq)
-
-Note the “hq” parameter. Every chain needs to be able to run in two
-situations: Both for the stream output (the ”live” pane) and for the preview
-displays (the big “preview” pane and the :ref:`channels <channels>`). The details have
-to do with Nageru internals (high-quality chains need to have an additional
-Y'CbCr output), but the distinction is also useful for themes. In particular,
-some operations, like scaling, can be done in various quality levels,
-and for a low-resolution preview display, you don't need maximum quality.
-Thus, in a preview chain (hq=false), you can safely take shortcuts.
-
-The live chain is always processed in full resolution (typically 720p)
-and then scaled down for the GUI. Preview chains are rendered in exactly
+  local scene = Scene.new(16, 9)  -- Aspect ratio.
+  local input = scene:add_input()
+  input:display(0)  -- First input card. Can be changed whenever you want.
+  scene:finalize()
+
+The live scene is always processed in full resolution (typically 720p)
+and then scaled down for the GUI. Preview scenes are rendered in exactly
 the resolution required, although of course, intermediate steps could be
 bigger.
 
 
-Setting parameters, and the get_chain entry point
+Setting parameters, and the get_scene entry point
 -------------------------------------------------
 
 Many effects support parameters that can vary per-frame. Imagine,
 for instance, a theme where you want to supports two inputs and fading between
-them. This means you will need a chain that produces two inputs and
+them. This means you will need a scene that produces two inputs and
 produces a mix of them; Movit's *MixEffect* is exactly what you want here::
 
-  local chain = EffectChain.new(16, 9)
+  local scene = EffectChain.new(16, 9)
 
-  local input0 = chain:add_live_input(false, false)
-  input0:connect_signal(0)
-  local input1 = chain:add_live_input(false, false)
-  input1:connect_signal(1)
+  local input0 = scene:add_input()
+  input0:display(0)
+  local input1 = scene:add_input()
+  input1:display(1)
 
-  local mix_effect = chain:add_effect(MixEffect.new(), input0, input1)
-  chain:finalize(hq)
+  local mix_effect = scene:add_effect(MixEffect.new(), input0, input1)
+  scene:finalize()
 
-Every frame, Movit will call your **get_chain** function, which has
+Every frame, Movit will call your **get_scene** function, which has
 this signature:
 
-  function get_chain(num, t, width, height, signals)
+  function get_scene(num, t, width, height, signals)
 
 “width” and “height” are what you'd expect (the output resolution).
 t contains the current stream time in seconds. “num” contains 0
@@ -98,100 +98,105 @@ of the individual stream previews. “signals“ contains a bit of
 information about each input signal, like its current resolution
 or frame rate.
 
-get_chain is in turn responsible for returning two values:
-
-  * The first return value is a Movit chain, as described in these
-    sections. For the live stream (num=0), you should return a high-quality
-    chain; for all others, you should return a low-quality chain.
-  * The second parameter is an *closure* that will be called just before
-    the chain is to be rendered. (The same chain could be used in
-    multiple OpenGL contexts at the same time, so you can't just set the values
-    immediately before returning. If you set them in the closure,
-    Nageru and Movit will deal with all the required threading for you.)
+get_scene in return should return a scene. However, before you do that,
+you can set the parameters **strength_first** and **strength_second**
+on it; for instance like this::
 
-In the returned closure, you can set the parameters **strength_first**
-and **strength_second**; for instance like this::
-
-  function get_chain(num, t, width, height, signals)
+  function get_scene(num, t, width, height, signals)
     -- Assume num is 0 here; you will need to handle the other
     -- cases, too.
-    prepare = function()
-      input0:connect_signal(0)
-      input1:connect_signal(1)
-
-      local fade_progress = 0.0
-      if t >= 1.0 and t >= 2.0:  -- Between 1 and 2 seconds; do the fade.
-        fade_progress = t - 1.0
-      elseif t >= 2.0:
-        fade_progress = 1.0
-      end
-
-      mix_effect:set_float("strength_first", 1.0 - fade_progress)
-      mix_effect:set_float("strength_second", fade_progress)
+    local fade_progress = 0.0
+    if t >= 1.0 and t >= 2.0:  -- Between 1 and 2 seconds; do the fade.
+      fade_progress = t - 1.0
+    elseif t >= 2.0:
+      fade_progress = 1.0
     end
-    return chain, prepare
+
+    mix_effect:set_float("strength_first", 1.0 - fade_progress)
+    mix_effect:set_float("strength_second", fade_progress)
+    return scene
   end
 
 Note that in the case where fade_progress is 0.0 or 1.0 (you are just
 showing one of the inputs), you are wasting GPU power by using the
-fade chain; you should just return a simpler one-input chain instead.
+fade scene; you should just return a simpler one-input scene instead.
 
-The get_chain function is the backbone of every Nageru theme.
+The get_scene function is the backbone of every Nageru theme.
 As we shall see, however, it may end up dealing with a fair bit
 of complexity as the theme grows.
 
 
-Chain precalculation
---------------------
+Scene variants and effect alternatives
+--------------------------------------
 
-Setting up and finalizing a chain is relatively fast, but it still
+Setting up and finalizing a scene is relatively fast, but it still
 takes a measurable amount of CPU time, since it needs to create an OpenGL
 shader and have it optimized by the driver; 50–100 ms is not uncommon.
-Given that 60 fps means each frame is 16.7 ms, you cannot create new chains in
-get_chain; every chain you could be using must be created at program start,
+Given that 60 fps means each frame is 16.7 ms, you cannot create new scenes in
+get_scene; every scene you could be using must be created at program start,
 when your theme is initialized.
 
-For any nontrivial theme, there are a lot of possible chains. Let's
-return to the case of the MixEffect chain from the previous section.
+For any nontrivial theme, there are a lot of possible scenes. Let's
+return to the case of the MixEffect scene from the previous section.
 Now let us assume that we could deal with signals that come in at
 1080p instead of the native 720p. In this case, we will want a high-quality
 scaler before mixing; *ResampleEffect* provides one::
 
-  local chain = EffectChain.new(16, 9)
+  local scene = EffectChain.new(16, 9)
 
-  local input0 = chain:add_live_input(false, false)
-  input0:connect_signal(0)
-  local input0_scaled = chain:add_effect(ResampleEffect.new())  -- Implicitly uses input0.
-  chain_or_input.resample_effect:set_int("width", 1280)  -- Would normally be set in the prepare function.
-  chain_or_input.resample_effect:set_int("height", 720)
+  local input0 = scene:add_input()
+  input0:display(0)
+  local input0_scaled = scene:add_optional_effect(ResampleEffect.new())  -- Implicitly uses input0.
+  scene_or_input.resample_effect:set_int("width", 1280)  -- Could also be set in get_scene().
+  scene_or_input.resample_effect:set_int("height", 720)
+  input0_scaled:enable()  -- Enable or disable as needed.
 
-  local input1 = chain:add_live_input(false, false)
-  input1:connect_signal(1)
+  local input1 = scene:add_input()
+  input1:display(1)
+  local input1_scaled = ... -- Similarly here and the rest.
 
   -- The rest is unchanged.
 
 Clearly, there are four options here; both inputs could be unscaled,
 input0 could be scaled but not input1, input1 could be scaled but not input0,
-or both could be scaled. That means four chains.
-
-Now remember that we need to create all your chains both in high-
-and low-quality versions. In particular, this determines the “hq”
-parameter to finalize(), but in our case, we would want to replace
-ResampleEffect by *ResizeEffect* (a simpler scaling algorithm provided
-directly by the GPU) for the low-quality versions. This makes for
-eight chains.
-
-Now also consider that we would want to deal with *interlaced*
-inputs. (You can check if you get an interlaced input on the Nth
-input by calling “signals:get_deinterlaced(n)” from get_chain.)
-This further quadruples the number of chains you'd need to write,
-and this isn't even including that you'd want the static chains.
-It is obvious that this should not be done by hand. The default
-included theme contains a handy Lua shortcut called
-**make_cartesian_product** where you can declare all the dimensions
-you would want to specialize your chain over, and have a callback
-function called for each possible combination. Movit will make sure
-each and every of those generated chains runs optimally on your GPU.
+or both could be scaled. That means four scenes. However, you don't need to
+care about this; behind the scenes (no pun intended), Nageru will make all
+four versions for you and choose the right one as you call enable() or
+disable() on each effect.
+
+Beyond simple on/off switches, an effect can have many *alternatives*,
+by giving in an array of effects. For instance, it is usually pointless to use
+the high-quality resampling provided by ResampleEffect for the on-screen
+outputs; we can use *ResizeEffect* (a simpler scaling algorithm provided
+directly by the GPU) that instead. The scaling is set up like this::
+
+  local input0 = scene:add_input()
+  input0:display(0)
+  local input0_scaled = scene:add_effect({ResampleEffect.new(), ResizeEffect.new()})  -- Implicitly uses input0.
+  scene_or_input.resample_effect:set_int("width", 1280)  -- Just like before.
+  scene_or_input.resample_effect:set_int("height", 720)
+
+  -- Pick one in get_scene() like this:
+  input0_scaled:choose(ResizeEffect)
+
+  -- Or by numerical index:
+  input0_scaled:choose(1)  -- Chooses ResizeEffect
+
+Note that add_effect returns its input for convenience.
+
+Actually, add_optional_effect() is just a wrapper around add_effect() with
+IdentityEffect as the other alternative, and disable() is a convenience version of
+choose(IdentityEffect).
+
+Actually, more versions are created than you'd immediately expect.
+In particular, the output format for the live output and all previews are
+different (Y'CbCr versus RGBA), which is also handled transparently for you.
+Also, the inputs could be interlaced, or they could be images, or videos (see
+:ref:`images` and :doc:`video`), creating many more options. Again, you
+generally don't need to care about this; Movit will make sure each and every of
+those generated scenes runs optimally on your GPU. However, if the
+combinatorial explosion increases startup time beyond what you are comfortable
+with, see :ref:`locking`.
 
 
 Transitions
@@ -223,8 +228,8 @@ In this case, it can use two of the buttons to offer “Cut“ or “Fade”
 transitions to the user. If the user clicks the cut button, the theme
 can simply switch input and previews, which will take immediate
 effect on the next frame. However, if the user clicks the fade button,
-state will need to be set up so that next time get_chain() runs,
-it will return the chain with the MixEffect, until it determines
+state will need to be set up so that next time get_scene() runs,
+it will return the scene with the MixEffect, until it determines
 the transition is over and changes back to showing only one input
 (presumably the new one).
 
@@ -256,7 +261,7 @@ by calling this function::
 Here, channel is 2, 3, 4, etc.—0 is always called “Live” and
 1 is always called “Preview”.
 
-Each channel has its own chain, starting from number 2 for the first one
+Each channel has its own scene, starting from number 2 for the first one
 (since 0 is live and 1 is preview). The simplest form is simply a direct copy
 of an input, and most themes will include one such channel for each input.
 (Below, we will see that there are more types of channels, however.)
@@ -301,22 +306,22 @@ The typical way to to this is to have a *WhiteBalanceEffect* on each input
 and set its “neutral_color” parameter using the “set_vec3” function.
 
 
-More complicated channels: Scenes
----------------------------------
+More complicated channels: Composites
+-------------------------------------
 
-Direct inputs are not the only kind of channels possible; again, any chain
-can be output. The most common case is different kinds of **scenes**,
+Direct inputs are not the only kind of channels possible; again, any scene
+can be output. The most common case is different kinds of **composites**,
 typically showing side-by-side or something similar. The typical UI presented
 to the user in this case is that you create a channel that consists of the
-finished setup; you use ResampleEffect (or ResizeEffect for low-quality chains),
+finished setup; you use ResampleEffect (or ResizeEffect for preview scenes),
 *PaddingEffect* (to place the rectangles on the screen, one of them with a
 transparent border) and then *OverlayEffect* (to get both on the screen at
 the same time). Optionally, you can have a background image at the bottom,
 and perhaps a logo at the top. This allows the operator to select a pre-made
-scene, and then transition to and from it from a single camera view (or even
-between different scenes) as needed.
+composits, and then transition to and from it from a single camera view (or even
+between different composites) as needed.
 
-Transitions involving scenes tend to be the most complicated parts of the theme
+Transitions involving composites tend to be the most complicated parts of the theme
 logic, but also make for the most distinct parts of your visual look.
 
 
@@ -326,18 +331,16 @@ Image inputs
 ------------
 
 In addition to video inputs, Nageru supports static **image inputs**.
-These work pretty much the same way as live video inputs; however,
-they need to be instantiated in a different way. Recall that live inputs
-were created like this::
+These work pretty much the same way as live video inputs. Recall that
+you chose what input to display like this::
 
-  input = chain:add_live_input(false, deint)
+  input:display(0)
 
 Image inputs are instead created by instantiating *ImageInput* and
-adding them manually to the chain::
+displaying that::
 
-  input = chain:add_effect(ImageInput.new("bg.jpeg"))
-
-Note that add_effect returns its input for convenience.
+  bg = ImageInput.new("bg.jpeg")  -- Once, at the start of the program.
+  input:display(bg)  -- In get_scene().
 
 All image types supported by FFmpeg are supported; if you give in a video,
 only the first frame is used. The file is checked once every second,