]> git.sesse.net Git - nageru/commitdiff
Rework the chain concept.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Thu, 13 Jun 2019 20:59:18 +0000 (22:59 +0200)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Thu, 13 Jun 2019 20:59:18 +0000 (22:59 +0200)
For complex themes, building the multitude of chains one might need
has become very bothersome, with tricky Lua scripting and non-typesafe
multidimensional tables.

To alleviate this somewhat, we introduce a concept called Scenes.
A Scene is pretty much an EffectChain with a better name and significantly
more functionality. In particular, scenes don't consist of single Effects;
they consist of blocks, which can hold any number of alternatives for
Effects. On finalize, we will instantiate all possible variants of
EffectChains behind-the-scenes, like the Lua code used to have to do itself,
but this is transparent to the theme.

In particular, this means that inputs are much more flexible. Instead of
having to make separate chains for regular inputs, deinterlaced inputs,
video inputs and CEF inputs, you now just make an input, and can connect
any type to it runtime (or “display”, as it's now called). Output is also
flexible; by default, any scene will get both Y'CbCr and RGBA versions
compiled. (In both cases, you can make non-flexible versions to reduce
the number of different instantiations. This can be a good idea in
complex chains.)

This also does away with the concept of the prepare function for a chain;
any effect settings are snapshotted when you return from get_scene() (the new
name for get_chain(), obviously), so you don't need to worry about capturing
anything or get threading issues like you used to.

All existing themes will continue to work unmodified for the time being,
but it is strongly recommended to migrate from EffectChain to Scene.

meson.build
nageru/image_input.cpp
nageru/image_input.h
nageru/scene.cpp [new file with mode: 0644]
nageru/scene.h [new file with mode: 0644]
nageru/simple.lua
nageru/theme.cpp
nageru/theme.h
nageru/theme.lua

index 24491e727a99b06f26b45e79e3f1fa48d93dca48..1540b192b29b4017f4263287946b1463bf0d44c0 100644 (file)
@@ -195,7 +195,7 @@ nageru_link_with += audio
 
 # Mixer objects.
 nageru_srcs += ['nageru/chroma_subsampler.cpp', 'nageru/v210_converter.cpp', 'nageru/mixer.cpp', 'nageru/pbo_frame_allocator.cpp',
-       'nageru/theme.cpp', 'nageru/image_input.cpp', 'nageru/alsa_output.cpp',
+       'nageru/theme.cpp', 'nageru/scene.cpp', 'nageru/image_input.cpp', 'nageru/alsa_output.cpp',
        'nageru/timecode_renderer.cpp', 'nageru/tweaked_inputs.cpp', 'nageru/mjpeg_encoder.cpp']
 
 # Streaming and encoding objects (largely the set that is shared between Nageru and Kaeru).
index c8b10371b4d381eec9535246f1e216ba6b6f3bb6..6a2c5abc846b913123ef00647373bd6267ee1d4b 100644 (file)
@@ -41,6 +41,11 @@ struct SwsContext;
 
 using namespace std;
 
+ImageInput::ImageInput()
+       : sRGBSwitchingFlatInput({movit::COLORSPACE_sRGB, movit::GAMMA_sRGB}, movit::FORMAT_RGBA_POSTMULTIPLIED_ALPHA,
+                                GL_UNSIGNED_BYTE, 1280, 720)  // Resolution will be overwritten.
+{}
+
 ImageInput::ImageInput(const string &filename)
        : sRGBSwitchingFlatInput({movit::COLORSPACE_sRGB, movit::GAMMA_sRGB}, movit::FORMAT_RGBA_POSTMULTIPLIED_ALPHA,
                                 GL_UNSIGNED_BYTE, 1280, 720),  // Resolution will be overwritten.
@@ -66,6 +71,7 @@ void ImageInput::set_gl_state(GLuint glsl_program_num, const string& prefix, uns
        // is mostly there to save startup time, not RAM).
        {
                lock_guard<mutex> lock(all_images_lock);
+               assert(all_images.count(pathname));
                if (all_images[pathname] != current_image) {
                        current_image = all_images[pathname];
                        set_texture_num(*current_image->tex);
@@ -288,6 +294,15 @@ void ImageInput::update_thread_func(QSurface *surface)
        }
 }
 
+void ImageInput::switch_image(const string &pathname)
+{
+#ifndef NDEBUG
+       lock_guard<mutex> lock(all_images_lock);
+       assert(all_images.count(pathname));
+#endif
+       this->pathname = pathname;
+}
+
 void ImageInput::start_update_thread(QSurface *surface)
 {
        update_thread = thread(update_thread_func, surface);
index 32fd52913a6cf2b930e6866772c0592ab004e89f..6ccdd128cdd15159f4806f2f98f87f60a0f3ba80 100644 (file)
@@ -23,26 +23,35 @@ class QSurface;
 // from disk about every second.
 class ImageInput : public sRGBSwitchingFlatInput {
 public:
+       // For loading images.
        // NOTE: You will need to call start_update_thread() yourself, once per program.
+       struct Image {
+               unsigned width, height;
+               RefCountedTexture tex;
+               timespec last_modified;
+       };
+       static std::shared_ptr<const Image> load_image(const std::string &filename, const std::string &pathname);
+
+       // Actual members.
+
+       ImageInput();  // Construct an empty input, which can't be used until you call switch_image().
        ImageInput(const std::string &filename);
 
        std::string effect_type_id() const override { return "ImageInput"; }
        void set_gl_state(GLuint glsl_program_num, const std::string& prefix, unsigned *sampler_num) override;
 
+       // Switch to a different image. The image must be previously loaded using load_image().
+       void switch_image(const std::string &pathname);
+
+       std::string get_pathname() const { return pathname; }
+
        static void start_update_thread(QSurface *surface);
        static void end_update_thread();
        
 private:
-       struct Image {
-               unsigned width, height;
-               RefCountedTexture tex;
-               timespec last_modified;
-       };
-
        std::string pathname;
        std::shared_ptr<const Image> current_image;
 
-       static std::shared_ptr<const Image> load_image(const std::string &filename, const std::string &pathname);
        static std::shared_ptr<const Image> load_image_raw(const std::string &pathname);
        static void update_thread_func(QSurface *surface);
        static std::mutex all_images_lock;
diff --git a/nageru/scene.cpp b/nageru/scene.cpp
new file mode 100644 (file)
index 0000000..d7da7db
--- /dev/null
@@ -0,0 +1,595 @@
+#include <assert.h>
+extern "C" {
+#include <lauxlib.h>
+#include <lua.hpp>
+}
+
+#ifdef HAVE_CEF
+#include "cef_capture.h"
+#endif
+#include "ffmpeg_capture.h"
+#include "flags.h"
+#include "image_input.h"
+#include "input_state.h"
+#include "lua_utils.h"
+#include "scene.h"
+#include "theme.h"
+
+using namespace movit;
+using namespace std;
+
+bool display(Block *block, lua_State *L, int idx);
+
+EffectType current_type(const Block *block)
+{
+       return block->alternatives[block->currently_chosen_alternative]->effect_type;
+}
+
+int find_index_of(const Block *block, EffectType val)
+{
+       for (size_t idx = 0; idx < block->alternatives.size(); ++idx) {
+               if (block->alternatives[idx]->effect_type == val) {
+                       return idx;
+               }
+       }
+       return -1;
+}
+
+Scene::Scene(Theme *theme, float aspect_nom, float aspect_denom)
+       : theme(theme), aspect_nom(aspect_nom), aspect_denom(aspect_denom), resource_pool(theme->get_resource_pool()) {}
+
+size_t Scene::compute_chain_number(bool is_main_chain) const
+{
+       assert(chains.size() > 0);
+       assert(chains.size() % 2 == 0);
+       size_t chain_number = compute_chain_number_for_block(blocks.size() - 1);
+       assert(chain_number < chains.size() / 2);
+       if (is_main_chain) {
+               chain_number += chains.size() / 2;
+       }
+       return chain_number;
+}
+
+size_t Scene::compute_chain_number_for_block(size_t block_idx) const
+{
+       Block *block = blocks[block_idx];
+       size_t chain_number;
+       if (block_idx == 0) {
+               assert(block->cardinality_base == 1);
+               chain_number = block->currently_chosen_alternative;
+       } else {
+               chain_number = compute_chain_number_for_block(block_idx - 1) + block->cardinality_base * block->currently_chosen_alternative;
+       }
+       assert(block->currently_chosen_alternative < int(block->alternatives.size()));
+       return chain_number;
+}
+
+int Scene::add_input(lua_State* L)
+{
+       assert(lua_gettop(L) == 1 || lua_gettop(L) == 2);
+       Scene *chain = (Scene *)luaL_checkudata(L, 1, "Scene");
+
+       Block *block = new Block;
+       block->idx = chain->blocks.size();
+       if (lua_gettop(L) == 1) {
+               // No parameter given, so a flexible input.
+               block->alternatives.emplace_back(new EffectBlueprint(LIVE_INPUT_YCBCR));
+               block->alternatives.emplace_back(new EffectBlueprint(LIVE_INPUT_YCBCR_WITH_DEINTERLACE));
+               block->alternatives.emplace_back(new EffectBlueprint(LIVE_INPUT_YCBCR_PLANAR));
+               block->alternatives.emplace_back(new EffectBlueprint(LIVE_INPUT_BGRA));
+               block->alternatives.emplace_back(new EffectBlueprint(IMAGE_INPUT));
+       } else {
+               // Input of a given type. We'll specialize it here, plus connect the input as given.
+               if (lua_isnumber(L, 2)) {
+                       block->alternatives.emplace_back(new EffectBlueprint(LIVE_INPUT_YCBCR));
+                       block->alternatives.emplace_back(new EffectBlueprint(LIVE_INPUT_YCBCR_WITH_DEINTERLACE));
+#ifdef HAVE_CEF
+               } else if (luaL_testudata(L, 2, "HTMLInput")) {
+                       block->alternatives.emplace_back(new EffectBlueprint(LIVE_INPUT_BGRA));
+#endif
+               } else if (luaL_testudata(L, 2, "VideoInput")) {
+                       FFmpegCapture *capture = *(FFmpegCapture **)luaL_checkudata(L, 2, "VideoInput");
+                       if (capture->get_current_pixel_format() == bmusb::PixelFormat_8BitYCbCrPlanar) {
+                               block->alternatives.emplace_back(new EffectBlueprint(LIVE_INPUT_YCBCR_PLANAR));
+                       } else {
+                               assert(capture->get_current_pixel_format() == bmusb::PixelFormat_8BitBGRA);
+                               block->alternatives.emplace_back(new EffectBlueprint(LIVE_INPUT_BGRA));
+                       }
+               } else if (luaL_testudata(L, 2, "ImageInput")) {
+                       block->alternatives.emplace_back(new EffectBlueprint(IMAGE_INPUT));
+               } else {
+                       luaL_error(L, "add_input() called with something that's not a signal (a signal number, a HTML input, or a VideoInput)");
+               }
+               bool ok = display(block, L, 2);
+               assert(ok);
+       }
+       block->is_input = true;
+       chain->blocks.push_back(block);
+
+       return wrap_lua_existing_object_nonowned<Block>(L, "Block", block);
+}
+
+int Scene::add_effect(lua_State* L)
+{
+       assert(lua_gettop(L) >= 2);
+       Scene *chain = (Scene *)luaL_checkudata(L, 1, "Scene");
+
+       Block *block = new Block;
+       block->idx = chain->blocks.size();
+
+       if (lua_istable(L, 2)) {
+               size_t len = lua_objlen(L, 2);
+               for (size_t i = 0; i < len; ++i) {
+                       lua_rawgeti(L, 2, i + 1);
+                       EffectBlueprint *blueprint = *(EffectBlueprint **)luaL_checkudata(L, -1, "EffectBlueprint");
+                       block->alternatives.push_back(blueprint);
+                       lua_settop(L, -2);
+               }
+       } else {
+               EffectBlueprint *blueprint = *(EffectBlueprint **)luaL_checkudata(L, 2, "EffectBlueprint");
+               block->alternatives.push_back(blueprint);
+       }
+
+       // Find the inputs.
+       if (lua_gettop(L) == 2) {
+               assert(!chain->blocks.empty());
+               block->inputs.push_back(chain->blocks.size() - 1);
+       } else {
+               for (int idx = 3; idx <= lua_gettop(L); ++idx) {
+                       Block *input_block = nullptr;
+                       if (luaL_testudata(L, idx, "Block")) {
+                               input_block = *(Block **)luaL_checkudata(L, idx, "Block");
+                       } else {
+                               EffectBlueprint *blueprint = *(EffectBlueprint **)luaL_checkudata(L, idx, "EffectBlueprint");
+
+                               // Search through all the blocks to figure out which one contains this effect.
+                               for (Block *block : chain->blocks) {
+                                       if (find(block->alternatives.begin(), block->alternatives.end(), blueprint) != block->alternatives.end()) {
+                                               input_block = block;
+                                               break;
+                                       }
+                               }
+                               if (input_block == nullptr) {
+                                       luaL_error(L, "Input effect in parameter #%d has not been added to this chain", idx - 1);
+                               }
+                       }
+                       block->inputs.push_back(input_block->idx);
+               }
+       }
+
+       chain->blocks.push_back(block);
+
+       return wrap_lua_existing_object_nonowned<Block>(L, "Block", block);
+}
+
+int Scene::add_optional_effect(lua_State* L)
+{
+       assert(lua_gettop(L) >= 2);
+       Scene *chain = (Scene *)luaL_checkudata(L, 1, "Scene");
+
+       // NOTE: We only support effects with a single parent, since that's what IdentityEffect does.
+       Block *block = new Block;
+       block->idx = chain->blocks.size();
+
+       EffectBlueprint *blueprint = *(EffectBlueprint **)luaL_checkudata(L, 2, "EffectBlueprint");
+       block->alternatives.push_back(blueprint);
+
+       // An IdentityEffect will be the alternative for when the effect is disabled.
+       block->alternatives.push_back(new EffectBlueprint(IDENTITY_EFFECT));
+
+       block->inputs.push_back(chain->blocks.size() - 1);
+       chain->blocks.push_back(block);
+
+       return wrap_lua_existing_object_nonowned<Block>(L, "Block", block);
+}
+
+Effect *Scene::instantiate_effects(const Block *block, size_t chain_idx, Scene::Instantiation *instantiation)
+{
+       vector<Effect *> inputs;
+       for (size_t input_idx : block->inputs) {
+               inputs.push_back(instantiate_effects(blocks[input_idx], chain_idx, instantiation));
+       }
+
+       // Find the chosen alternative for this block in this instance.
+       size_t chosen_alternative = (chain_idx / block->cardinality_base) % block->alternatives.size();
+       EffectType chosen_type = block->alternatives[chosen_alternative]->effect_type;
+
+       Effect *effect;
+       switch (chosen_type) {
+       case LIVE_INPUT_YCBCR:
+       case LIVE_INPUT_YCBCR_WITH_DEINTERLACE:
+       case LIVE_INPUT_YCBCR_PLANAR:
+       case LIVE_INPUT_BGRA: {
+               bool deinterlace = (chosen_type == LIVE_INPUT_YCBCR_WITH_DEINTERLACE);
+               bool override_bounce = !deinterlace;  // For most chains, this will be fine. Reconsider if we see real problems somewhere; it's better than having the user try to understand it.
+               bmusb::PixelFormat pixel_format;
+               if (chosen_type == LIVE_INPUT_BGRA) {
+                       pixel_format = bmusb::PixelFormat_8BitBGRA;
+               } else if (chosen_type == LIVE_INPUT_YCBCR_PLANAR) {
+                       pixel_format = bmusb::PixelFormat_8BitYCbCrPlanar;
+               } else if (global_flags.ten_bit_input) {
+                       pixel_format = bmusb::PixelFormat_10BitYCbCr;
+               } else {
+                       pixel_format = bmusb::PixelFormat_8BitYCbCr;
+               }
+               LiveInputWrapper *input = new LiveInputWrapper(theme, instantiation->chain.get(), pixel_format, override_bounce, deinterlace, /*user_connectable=*/true);
+               effect = input->get_effect();  // Adds itself to the chain, so no need to call add_effect().
+               instantiation->inputs.emplace(block->idx, input);
+               break;
+       }
+       case IMAGE_INPUT: {
+               ImageInput *input = new ImageInput;
+               instantiation->chain->add_input(input);
+               instantiation->image_inputs.emplace(block->idx, input);
+               effect = input;
+               break;
+       }
+       default:
+               effect = instantiate_effect(instantiation->chain.get(), block->alternatives[chosen_alternative]->effect_type);
+               instantiation->chain->add_effect(effect, inputs);
+               break;
+       }
+       instantiation->effects.emplace(block->idx, effect);
+       return effect;
+}
+
+int Scene::finalize(lua_State* L)
+{
+       bool only_one_mode = false;
+       bool chosen_mode = false;
+       if (lua_gettop(L) == 2) {
+               only_one_mode = true;
+               chosen_mode = checkbool(L, 2);
+       } else {
+               assert(lua_gettop(L) == 1);
+       }
+       Scene *chain = (Scene *)luaL_checkudata(L, 1, "Scene");
+       Theme *theme = get_theme_updata(L);
+
+       size_t base = 1;
+       for (Block *block : chain->blocks) {
+               block->cardinality_base = base;
+               base *= block->alternatives.size();
+       }
+
+       const size_t cardinality = base;
+       const size_t total_cardinality = cardinality * (only_one_mode ? 1 : 2);
+       if (total_cardinality > 200) {
+               print_warning(L, "The given Scene will instantiate %zu different versions. This will take a lot of time and RAM to compile; see if you could limit some options by e.g. locking the input type in some cases (by giving a fixed input to add_input()).\n",
+                       total_cardinality);
+       }
+
+       Block *output_block = chain->blocks.back();
+       for (bool is_main_chain : { false, true }) {
+               for (size_t chain_idx = 0; chain_idx < cardinality; ++chain_idx) {
+                       if (only_one_mode && is_main_chain != chosen_mode) {
+                               chain->chains.emplace_back();
+                               continue;
+                       }
+
+                       Scene::Instantiation instantiation;
+                       instantiation.chain.reset(new EffectChain(chain->aspect_nom, chain->aspect_denom, theme->get_resource_pool()));
+                       chain->instantiate_effects(output_block, chain_idx, &instantiation);
+
+                       add_outputs_and_finalize(instantiation.chain.get(), is_main_chain);
+                       chain->chains.emplace_back(move(instantiation));
+               }
+       }
+       return 0;
+}
+
+std::pair<movit::EffectChain *, std::function<void()>>
+Scene::get_chain(Theme *theme, lua_State *L, unsigned num, const InputState &input_state)
+{
+       // For video inputs, pick the right interlaced/progressive version
+       // based on the current state of the signals.
+       InputStateInfo info(input_state);
+       for (Block *block : blocks) {
+               if (block->is_input && block->signal_type_to_connect == Block::CONNECT_SIGNAL) {
+                       EffectType chosen_type = current_type(block);
+                       assert(chosen_type == LIVE_INPUT_YCBCR || chosen_type == LIVE_INPUT_YCBCR_WITH_DEINTERLACE);
+                       if (info.last_interlaced[block->signal_to_connect]) {
+                               block->currently_chosen_alternative = find_index_of(block, LIVE_INPUT_YCBCR_WITH_DEINTERLACE);
+                       } else {
+                               block->currently_chosen_alternative = find_index_of(block, LIVE_INPUT_YCBCR);
+                       }
+               }
+       }
+
+       // Pick out the right chain based on the current selections,
+       // and snapshot all the set variables so that we can set them
+       // in the prepare function even if they're being changed by
+       // the Lua code later.
+       bool is_main_chain = (num == 0);
+       size_t chain_idx = compute_chain_number(is_main_chain);
+       const Scene::Instantiation &instantiation = chains[chain_idx];
+       EffectChain *effect_chain = instantiation.chain.get();
+
+       map<LiveInputWrapper *, int> signals_to_connect;
+       map<ImageInput *, string> images_to_select;
+       map<pair<Effect *, string>, int> int_to_set;
+       map<pair<Effect *, string>, float> float_to_set;
+       map<pair<Effect *, string>, array<float, 3>> vec3_to_set;
+       map<pair<Effect *, string>, array<float, 4>> vec4_to_set;
+       for (const auto &index_and_input : instantiation.inputs) {
+               Block *block = blocks[index_and_input.first];
+               EffectType chosen_type = current_type(block);
+               LiveInputWrapper *input = index_and_input.second;
+               if (chosen_type == LIVE_INPUT_YCBCR ||
+                   chosen_type == LIVE_INPUT_YCBCR_WITH_DEINTERLACE ||
+                   chosen_type == LIVE_INPUT_YCBCR_PLANAR ||
+                   chosen_type == LIVE_INPUT_BGRA) {
+                       if (block->signal_type_to_connect == Block::CONNECT_SIGNAL) {
+                               signals_to_connect.emplace(input, block->signal_to_connect);
+#ifdef HAVE_CEF
+                       } else if (block->signal_type_to_connect == Block::CONNECT_CEF) {
+                               signals_to_connect.emplace(input, block->cef_to_connect->get_card_index());
+#endif
+                       } else if (block->signal_type_to_connect == Block::CONNECT_VIDEO) {
+                               signals_to_connect.emplace(input, block->video_to_connect->get_card_index());
+                       } else {
+                               assert(false);
+                       }
+               }
+       }
+       for (const auto &index_and_input : instantiation.image_inputs) {
+               Block *block = blocks[index_and_input.first];
+               ImageInput *input = index_and_input.second;
+               if (current_type(block) == IMAGE_INPUT) {
+                       images_to_select.emplace(input, block->pathname);
+               }
+       }
+       for (const auto &index_and_effect : instantiation.effects) {
+               Block *block = blocks[index_and_effect.first];
+               Effect *effect = index_and_effect.second;
+
+               // Get the effects currently set on the block.
+               if (current_type(block) != IDENTITY_EFFECT) {  // Ignore settings on optional effects.
+                       for (const auto &key_and_tuple : block->int_parameters) {
+                               int_to_set.emplace(make_pair(effect, key_and_tuple.first), key_and_tuple.second);
+                       }
+                       for (const auto &key_and_tuple : block->float_parameters) {
+                               float_to_set.emplace(make_pair(effect, key_and_tuple.first), key_and_tuple.second);
+                       }
+                       for (const auto &key_and_tuple : block->vec3_parameters) {
+                               vec3_to_set.emplace(make_pair(effect, key_and_tuple.first), key_and_tuple.second);
+                       }
+                       for (const auto &key_and_tuple : block->vec4_parameters) {
+                               vec4_to_set.emplace(make_pair(effect, key_and_tuple.first), key_and_tuple.second);
+                       }
+               }
+
+               // Parameters set on the blueprint itself override those that are set for the block,
+               // so they are set afterwards.
+               if (!block->alternatives.empty()) {
+                       EffectBlueprint *blueprint = block->alternatives[block->currently_chosen_alternative];
+                       for (const auto &key_and_tuple : blueprint->int_parameters) {
+                               int_to_set.emplace(make_pair(effect, key_and_tuple.first), key_and_tuple.second);
+                       }
+                       for (const auto &key_and_tuple : blueprint->float_parameters) {
+                               float_to_set.emplace(make_pair(effect, key_and_tuple.first), key_and_tuple.second);
+                       }
+                       for (const auto &key_and_tuple : blueprint->vec3_parameters) {
+                               vec3_to_set.emplace(make_pair(effect, key_and_tuple.first), key_and_tuple.second);
+                       }
+                       for (const auto &key_and_tuple : blueprint->vec4_parameters) {
+                               vec4_to_set.emplace(make_pair(effect, key_and_tuple.first), key_and_tuple.second);
+                       }
+               }
+       }
+
+       lua_pop(L, 1);
+
+       auto setup_chain = [L, theme, signals_to_connect, images_to_select, int_to_set, float_to_set, vec3_to_set, vec4_to_set, input_state]{
+               lock_guard<mutex> lock(theme->m);
+
+               assert(theme->input_state == nullptr);
+               theme->input_state = &input_state;
+
+               // Set up state, including connecting signals.
+               for (const auto &input_and_signal : signals_to_connect) {
+                       LiveInputWrapper *input = input_and_signal.first;
+                       input->connect_signal_raw(input_and_signal.second, input_state);
+               }
+               for (const auto &input_and_filename : images_to_select) {
+                       input_and_filename.first->switch_image(input_and_filename.second);
+               }
+               for (const auto &effect_and_key_and_value : int_to_set) {
+                       Effect *effect = effect_and_key_and_value.first.first;
+                       const string &key = effect_and_key_and_value.first.second;
+                       const int value = effect_and_key_and_value.second;
+                       if (!effect->set_int(key, value)) {
+                               luaL_error(L, "Effect refused set_int(\"%s\", %d) (invalid key?)", key.c_str(), value);
+                       }
+               }
+               for (const auto &effect_and_key_and_value : float_to_set) {
+                       Effect *effect = effect_and_key_and_value.first.first;
+                       const string &key = effect_and_key_and_value.first.second;
+                       const float value = effect_and_key_and_value.second;
+                       if (!effect->set_float(key, value)) {
+                               luaL_error(L, "Effect refused set_float(\"%s\", %f) (invalid key?)", key.c_str(), value);
+                       }
+               }
+               for (const auto &effect_and_key_and_value : vec3_to_set) {
+                       Effect *effect = effect_and_key_and_value.first.first;
+                       const string &key = effect_and_key_and_value.first.second;
+                       const float *value = effect_and_key_and_value.second.data();
+                       if (!effect->set_vec3(key, value)) {
+                               luaL_error(L, "Effect refused set_vec3(\"%s\", %f, %f, %f) (invalid key?)", key.c_str(),
+                                               value[0], value[1], value[2]);
+                       }
+               }
+               for (const auto &effect_and_key_and_value : vec4_to_set) {
+                       Effect *effect = effect_and_key_and_value.first.first;
+                       const string &key = effect_and_key_and_value.first.second;
+                       const float *value = effect_and_key_and_value.second.data();
+                       if (!effect->set_vec4(key, value)) {
+                               luaL_error(L, "Effect refused set_vec4(\"%s\", %f, %f, %f, %f) (invalid key?)", key.c_str(),
+                                               value[0], value[1], value[2], value[3]);
+                       }
+               }
+
+               theme->input_state = nullptr;
+       };
+       return make_pair(effect_chain, move(setup_chain));
+}
+
+bool display(Block *block, lua_State *L, int idx)
+{
+       if (lua_isnumber(L, idx)) {
+               Theme *theme = get_theme_updata(L);
+               int signal_idx = luaL_checknumber(L, idx);
+               block->signal_type_to_connect = Block::CONNECT_SIGNAL;
+               block->signal_to_connect = theme->map_signal(signal_idx);
+               block->currently_chosen_alternative = find_index_of(block, LIVE_INPUT_YCBCR);  // Will be changed to deinterlaced at get_chain() time if needed.
+               return true;
+#ifdef HAVE_CEF
+       } else if (luaL_testudata(L, idx, "HTMLInput")) {
+               CEFCapture *capture = *(CEFCapture **)luaL_checkudata(L, idx, "HTMLInput");
+               block->signal_type_to_connect = Block::CONNECT_CEF;
+               block->cef_to_connect = capture;
+               block->currently_chosen_alternative = find_index_of(block, LIVE_INPUT_BGRA);
+               assert(capture->get_current_pixel_format() == bmusb::PixelFormat_8BitBGRA);
+               return true;
+#endif
+       } else if (luaL_testudata(L, idx, "VideoInput")) {
+               FFmpegCapture *capture = *(FFmpegCapture **)luaL_checkudata(L, idx, "VideoInput");
+               block->signal_type_to_connect = Block::CONNECT_VIDEO;
+               block->video_to_connect = capture;
+               if (capture->get_current_pixel_format() == bmusb::PixelFormat_8BitYCbCrPlanar) {
+                       block->currently_chosen_alternative = find_index_of(block, LIVE_INPUT_YCBCR_PLANAR);
+               } else {
+                       assert(capture->get_current_pixel_format() == bmusb::PixelFormat_8BitBGRA);
+                       block->currently_chosen_alternative = find_index_of(block, LIVE_INPUT_BGRA);
+               }
+               return true;
+       } else if (luaL_testudata(L, idx, "ImageInput")) {
+               ImageInput *image = *(ImageInput **)luaL_checkudata(L, idx, "ImageInput");
+               block->signal_type_to_connect = Block::CONNECT_NONE;
+               block->currently_chosen_alternative = find_index_of(block, IMAGE_INPUT);
+               block->pathname = image->get_pathname();
+               return true;
+       } else {
+               return false;
+       }
+}
+
+int Block_display(lua_State* L)
+{
+       assert(lua_gettop(L) == 2);
+       Block *block = *(Block **)luaL_checkudata(L, 1, "Block");
+       if (!block->is_input) {
+               luaL_error(L, "display() called on something that isn't an input");
+       }
+
+       bool ok = display(block, L, 2);
+       if (!ok) {
+               luaL_error(L, "display() called with something that's not a signal (a signal number, a HTML input, or a VideoInput)");
+       }
+
+       if (block->currently_chosen_alternative == -1) {
+               luaL_error(L, "display() called on an input whose type was fixed at construction time, with a signal of different type");
+       }
+
+       return 0;
+}
+
+int Block_choose_alternative(lua_State* L)
+{
+       assert(lua_gettop(L) == 2);
+       Block *block = *(Block **)luaL_checkudata(L, 1, "Block");
+       int alternative_idx = luaL_checknumber(L, 2);
+
+       assert(alternative_idx >= 0);
+       assert(size_t(alternative_idx) < block->alternatives.size());
+       block->currently_chosen_alternative = alternative_idx;
+
+       return 0;
+}
+
+int Block_enable(lua_State *L)
+{
+       assert(lua_gettop(L) == 1);
+       Block *block = *(Block **)luaL_checkudata(L, 1, "Block");
+
+       if (block->alternatives.size() != 2 ||
+           block->alternatives[1]->effect_type != IDENTITY_EFFECT) {
+               luaL_error(L, "enable() called on something that wasn't added with add_optional_effect()");
+       }
+       block->currently_chosen_alternative = 0;  // The actual effect.
+       return 0;
+}
+
+int Block_disable(lua_State *L)
+{
+       assert(lua_gettop(L) == 1);
+       Block *block = *(Block **)luaL_checkudata(L, 1, "Block");
+
+       if (block->alternatives.size() != 2 ||
+           block->alternatives[1]->effect_type != IDENTITY_EFFECT) {
+               luaL_error(L, "disable() called on something that wasn't added with add_optional_effect()");
+       }
+       block->currently_chosen_alternative = find_index_of(block, IDENTITY_EFFECT);
+       assert(block->currently_chosen_alternative != -1);
+       return 0;
+}
+
+int Block_set_int(lua_State *L)
+{
+       assert(lua_gettop(L) == 3);
+       Block *block = *(Block **)luaL_checkudata(L, 1, "Block");
+       string key = checkstdstring(L, 2);
+       float value = luaL_checknumber(L, 3);
+
+       // TODO: check validity already here, if possible?
+       block->int_parameters[key] = value;
+
+       return 0;
+}
+
+int Block_set_float(lua_State *L)
+{
+       assert(lua_gettop(L) == 3);
+       Block *block = *(Block **)luaL_checkudata(L, 1, "Block");
+       string key = checkstdstring(L, 2);
+       float value = luaL_checknumber(L, 3);
+
+       // TODO: check validity already here, if possible?
+       block->float_parameters[key] = value;
+
+       return 0;
+}
+
+int Block_set_vec3(lua_State *L)
+{
+       assert(lua_gettop(L) == 5);
+       Block *block = *(Block **)luaL_checkudata(L, 1, "Block");
+       string key = checkstdstring(L, 2);
+       array<float, 3> v;
+       v[0] = luaL_checknumber(L, 3);
+       v[1] = luaL_checknumber(L, 4);
+       v[2] = luaL_checknumber(L, 5);
+
+       // TODO: check validity already here, if possible?
+       block->vec3_parameters[key] = v;
+
+       return 0;
+}
+
+int Block_set_vec4(lua_State *L)
+{
+       assert(lua_gettop(L) == 6);
+       Block *block = *(Block **)luaL_checkudata(L, 1, "Block");
+       string key = checkstdstring(L, 2);
+       array<float, 4> v;
+       v[0] = luaL_checknumber(L, 3);
+       v[1] = luaL_checknumber(L, 4);
+       v[2] = luaL_checknumber(L, 5);
+       v[3] = luaL_checknumber(L, 6);
+
+       // TODO: check validity already here, if possible?
+       block->vec4_parameters[key] = v;
+
+       return 0;
+}
+
diff --git a/nageru/scene.h b/nageru/scene.h
new file mode 100644 (file)
index 0000000..5d5d783
--- /dev/null
@@ -0,0 +1,145 @@
+#ifndef _SCENE_H
+#define _SCENE_H 1
+
+// A Scene is an equivalent of an EffectChain, but each part is not a single
+// Effect. (The name itself does not carry any specific meaning above that
+// of what an EffectChain is; it was just chosen as a more intuitive name than
+// an EffectChain when we had to change anyway.) Instead, it is a “block”,
+// which can hold one or more effect alternatives, e.g., one block could hold
+// ResizeEffect or IdentityEffect (effectively doing nothing), or many
+// different input types. On finalization, every different combination of
+// block alternatives are tried, and one EffectChain is generated for each.
+// This also goes for whether the chain is destined for preview outputs
+// (directly to screen, RGBA) or live (Y'CbCr output).
+
+#include <stddef.h>
+#include <functional>
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+class CEFCapture;
+struct EffectBlueprint;
+class FFmpegCapture;
+class ImageInput;
+struct InputState;
+class LiveInputWrapper;
+class Theme;
+struct lua_State;
+
+namespace movit {
+class Effect;
+class EffectChain;
+class ResourcePool;
+}  // namespace movit
+
+struct Block {
+       // Index into the parent Scene's list of blocks.
+       using Index = size_t;
+       Index idx = 0;
+
+       // Each instantiation is indexed by the chosen alternative for each block.
+       // These are combined into one big variable-base number, ranging from 0
+       // to (B_0*B_1*B_2*...*B_n)-1, where B_i is the number of alternatives for
+       // block number i and n is the index of the last block.
+       //
+       // The actual index, given alternatives A_0, A_1, A_2, ..., is given as
+       //
+       //   A_0 + B_0 * (A_1 + B_1 * (A_2 + B_2 * (...)))
+       //
+       // where each A_i can of course range from 0 to B_i-1. In other words,
+       // the first block gets the lowest “bits” (or trits, or quats...) of the
+       // index number, the second block gets the ones immediately above,
+       // and so on. Thus, there are no holes in the sequence.
+       //
+       // Expanding the formula above gives the equivalent index
+       //
+       //   A_0 + A_1 * B_0 + A_2 * B_0 * B_1 + A_3 * ...
+       //
+       // or
+       //
+       //   A_0 * C_0 + A_1 * C_1 + A_2 * C_2 + A_3 * ...
+       //
+       // where C_0 = 0 and C_(i+1) = C_i * B_i. In other words, C_i is
+       // the product of the cardinalities of each previous effect; if we
+       // are e.g. at the third index and there have been C_2 = 3 * 5 = 15
+       // different alternatives for constructing the chain so far
+       // (with possible indexes 0..14), it is only logical that if we
+       // want three new options (B_2 = 3), we must add 0, 15 or 30 to
+       // the index. (Then the local possible indexes become 0..44 and
+       // C_3 = 45, of course.) Given an index number k, we can then get our
+       // own local “bits” of the index, giving the alternative for this
+       // block, by doing (k / 15) % 3.
+       //
+       // This specific member contains the value of C_i for this block.
+       // (B_i is alternatives.size().) Not set before finalize() has run.
+       size_t cardinality_base = 0;
+
+       std::vector<EffectBlueprint *> alternatives;  // Must all have the same amount of inputs. Pointers to make things easier for Lua.
+       std::vector<Index> inputs;  // One for each input of alternatives[0] (ie., typically 0 or 1, occasionally 2).
+       int currently_chosen_alternative = 0;
+       bool is_input = false;
+
+       // For LIVE_INPUT* only. We can't just always populate signal_to_connect,
+       // since when we set this, CEF and video signals may not have numbers yet.
+       // FIXME: Perhaps it would be simpler if they just did?
+       enum { CONNECT_NONE, CONNECT_SIGNAL, CONNECT_CEF, CONNECT_VIDEO } signal_type_to_connect = CONNECT_NONE;
+       int signal_to_connect = 0;  // For CONNECT_SIGNAL.
+#ifdef HAVE_CEF
+       CEFCapture *cef_to_connect = nullptr;  // For CONNECT_CEF.
+#endif
+       FFmpegCapture *video_to_connect = nullptr;  // For CONNECT_VIDEO.
+
+       std::string pathname;  // For IMAGE_INPUT only.
+
+       // Parameters to set on the effect prior to render.
+       // Will be set _before_ the ones from the EffectBlueprint, so that
+       // the latter takes priority.
+       std::map<std::string, int> int_parameters;
+       std::map<std::string, float> float_parameters;
+       std::map<std::string, std::array<float, 3>> vec3_parameters;
+       std::map<std::string, std::array<float, 4>> vec4_parameters;
+};
+
+int Block_display(lua_State* L);
+int Block_choose_alternative(lua_State* L);
+int Block_enable(lua_State *L);
+int Block_disable(lua_State *L);
+int Block_set_int(lua_State *L);
+int Block_set_float(lua_State *L);
+int Block_set_vec3(lua_State *L);
+int Block_set_vec4(lua_State *L);
+
+class Scene {
+private:
+       std::vector<Block *> blocks;  // The last one represents the output node (after finalization). Pointers to make things easier for Lua.
+       struct Instantiation {
+               std::unique_ptr<movit::EffectChain> chain;
+               std::map<Block::Index, movit::Effect *> effects;  // So that we can set parameters.
+               std::map<Block::Index, LiveInputWrapper *> inputs;  // So that we can connect signals.
+               std::map<Block::Index, ImageInput *> image_inputs;  // So that we can connect signals.
+       };
+       std::vector<Instantiation> chains;  // Indexed by combination of each block's chosen alternative. See Block for information.
+
+       Theme *theme;
+       float aspect_nom, aspect_denom;
+       movit::ResourcePool *resource_pool;
+
+       movit::Effect *instantiate_effects(const Block *block, size_t chain_idx, Instantiation *instantiation);
+       size_t compute_chain_number_for_block(size_t block_idx) const;
+
+public:
+       Scene(Theme *theme, float aspect_nom, float aspect_denom);
+       size_t compute_chain_number(bool is_main_chain) const;
+
+       std::pair<movit::EffectChain *, std::function<void()>>
+       get_chain(Theme *theme, lua_State *L, unsigned num, const InputState &input_state);
+
+       static int add_input(lua_State *L);
+       static int add_effect(lua_State *L);
+       static int add_optional_effect(lua_State *L);
+       static int finalize(lua_State *L);
+};
+
+#endif   // !defined(_SCENE_H)
index 2bff8f341a09387f643d92bc397b14b1d92ea501..3960505175e9637d1a2f627e1586c83691c59d41 100644 (file)
@@ -1,11 +1,13 @@
 -- The theme is what decides what's actually shown on screen, what kind of
 -- transitions are available (if any), and what kind of inputs there are,
 -- if any. In general, it drives the entire display logic by creating Movit
--- chains, setting their parameters and then deciding which to show when.
+-- chains (called “scenes”), setting their parameters and then deciding which
+-- to show when.
 --
 -- Themes are written in Lua, which reflects a simplified form of the Movit API
--- where all the low-level details (such as texture formats) are handled by the
--- C++ side and you generally just build chains.
+-- where all the low-level details (such as texture formats) and alternatives
+-- (e.g. turning scaling on or off) are handled by the C++ side and you
+-- generally just build scenes.
 --
 -- This is a much simpler theme than the default theme; it only allows you to
 -- switch between inputs and set white balance, no transitions or the likes.
@@ -16,30 +18,12 @@ local input_neutral_color = {{0.5, 0.5, 0.5}, {0.5, 0.5, 0.5}}
 local live_signal_num = 0
 local preview_signal_num = 1
 
--- A chain to show a single input, with white balance. In a real example,
--- we'd probably want to support deinterlacing and high-quality scaling
--- (if the input isn't exactly what we want). However, we don't want these
--- things always on, so we'd need to generate more chains for the various
--- cases. In such a simple example, just having two is fine.
-function make_simple_chain(hq)
-       local chain = EffectChain.new(16, 9)
-
-       local input = chain:add_live_input(false, false)  -- No deinterlacing, no bounce override.
-       input:connect_signal(0)  -- First input card. Can be changed whenever you want.
-       local wb_effect = chain:add_effect(WhiteBalanceEffect.new())
-       chain:finalize(hq)
-
-       return {
-               chain = chain,
-               input = input,
-               wb_effect = wb_effect,
-       }
-end
+local img = ImageInput.new("bg.jpeg")
 
--- We only make two chains; one for the live view and one for the previews.
--- (Since they have different outputs, you cannot mix and match them.)
-local simple_hq_chain = make_simple_chain(true)
-local simple_lq_chain = make_simple_chain(false)
+local scene = Scene.new(16, 9)
+local input = scene:add_input()
+local wb_effect = scene:add_effect(WhiteBalanceEffect.new())
+scene:finalize()
 
 -- API ENTRY POINT
 -- Returns the number of outputs in addition to the live (0) and preview (1).
@@ -130,7 +114,7 @@ function channel_clicked(num)
 end
 
 -- API ENTRY POINT
--- Called every frame. Get the chain for displaying at input <num>,
+-- Called every frame. Get the scene for displaying at input <num>,
 -- where 0 is live, 1 is preview, 2 is the first channel to display
 -- in the bottom bar, and so on up to num_channels()+1. t is the
 -- current time in seconds. width and height are the dimensions of
@@ -140,40 +124,29 @@ end
 -- <signals> is basically an exposed InputState, which you can use to
 -- query for information about the signals at the point of the current
 -- frame. In particular, you can call get_width() and get_height()
--- for any signal number, and use that to e.g. assist in chain selection.
---
--- You should return two objects; the chain itself, and then a
--- function (taking no parameters) that is run just before rendering.
--- The function needs to call connect_signal on any inputs, so that
--- it gets updated video data for the given frame. (You are allowed
--- to switch which input your input is getting from between frames,
--- but not calling connect_signal results in undefined behavior.)
--- If you want to change any parameters in the chain, this is also
--- the right place.
+-- for any signal number, and use that to e.g. assist in scene selection.
 --
--- NOTE: The chain returned must be finalized with the Y'CbCr flag
--- if and only if num==0.
-function get_chain(num, t, width, height, signals)
-       local chain, signal_num
+-- You should return the scene to use, after having set any parameters you
+-- want to set (through set_int() etc.). The parameters will be snapshot
+-- at return time and used during rendering.
+function get_scene(num, t, width, height, signals)
+       local signal_num
        if num == 0 then  -- Live (right pane).
-               chain = simple_hq_chain
                signal_num = live_signal_num
        elseif num == 1 then  -- Preview (left pane).
-               chain = simple_lq_chain
                signal_num = preview_signal_num
        else  -- One of the two previews (bottom panes).
-               chain = simple_lq_chain
                signal_num = num - 2
        end
 
-       -- Make a copy of the current neutral color before returning, so that the
-       -- returned prepare function is unaffected by state changes made by the UI
-       -- before it is rendered.
+       if num == 3 then
+               input:display(img)
+       else
+               input:display(signal_num)
+       end
+
        local color = input_neutral_color[signal_num + 1]
+       wb_effect:set_vec3("neutral_color", color[1], color[2], color[3])
 
-       local prepare = function()
-               chain.input:connect_signal(signal_num)
-               chain.wb_effect:set_vec3("neutral_color", color[1], color[2], color[3])
-       end
-       return chain.chain, prepare
+       return scene
 end
index 71a59a24525a8c123da024212e40d9395f5d1769..fb1fa2fd7380c5422fc00dba6eb221dccc350f8c 100644 (file)
@@ -39,6 +39,7 @@
 #include "input_state.h"
 #include "lua_utils.h"
 #include "pbo_frame_allocator.h"
+#include "scene.h"
 
 class Mixer;
 
@@ -77,21 +78,6 @@ int ThemeMenu_set(lua_State *L)
        return theme->set_theme_menu(L);
 }
 
-namespace {
-
-// Contains basically the same data as InputState, but does not hold on to
-// a reference to the frames. This is important so that we can release them
-// without having to wait for Lua's GC.
-struct InputStateInfo {
-       InputStateInfo(const InputState& input_state);
-
-       unsigned last_width[MAX_VIDEO_CARDS], last_height[MAX_VIDEO_CARDS];
-       bool last_interlaced[MAX_VIDEO_CARDS], last_has_signal[MAX_VIDEO_CARDS], last_is_connected[MAX_VIDEO_CARDS];
-       unsigned last_frame_rate_nom[MAX_VIDEO_CARDS], last_frame_rate_den[MAX_VIDEO_CARDS];
-       bool has_last_subtitle[MAX_VIDEO_CARDS];
-       std::string last_subtitle[MAX_VIDEO_CARDS];
-};
-
 InputStateInfo::InputStateInfo(const InputState &input_state)
 {
        for (unsigned signal_num = 0; signal_num < MAX_VIDEO_CARDS; ++signal_num) {
@@ -116,21 +102,19 @@ InputStateInfo::InputStateInfo(const InputState &input_state)
        }
 }
 
-enum EffectType {
-       WHITE_BALANCE_EFFECT,
-       RESAMPLE_EFFECT,
-       PADDING_EFFECT,
-       INTEGRAL_PADDING_EFFECT,
-       OVERLAY_EFFECT,
-       RESIZE_EFFECT,
-       MULTIPLY_EFFECT,
-       MIX_EFFECT,
-       LIFT_GAMMA_GAIN_EFFECT
+// An effect that does nothing.
+class IdentityEffect : public Effect {
+public:
+        IdentityEffect() {}
+        string effect_type_id() const override { return "IdentityEffect"; }
+        string output_fragment_shader() override { return read_file("identity.frag"); }
 };
 
 Effect *instantiate_effect(EffectChain *chain, EffectType effect_type)
 {
        switch (effect_type) {
+       case IDENTITY_EFFECT:
+               return new IdentityEffect;
        case WHITE_BALANCE_EFFECT:
                return new WhiteBalanceEffect;
        case RESAMPLE_EFFECT:
@@ -155,27 +139,12 @@ Effect *instantiate_effect(EffectChain *chain, EffectType effect_type)
        }
 }
 
-// An EffectBlueprint refers to an Effect before it's being added to the graph.
-// It contains enough information to instantiate the effect, including any
-// parameters that were set before it was added to the graph. Once it is
-// instantiated, it forwards its calls on to the real Effect instead.
-struct EffectBlueprint {
-       EffectBlueprint(EffectType effect_type) : effect_type(effect_type) {}
-
-       EffectType effect_type;
-       map<string, int> int_parameters;
-       map<string, float> float_parameters;
-       map<string, array<float, 3>> vec3_parameters;
-       map<string, array<float, 4>> vec4_parameters;
-
-       Effect *effect = nullptr;  // Gets filled out when it's instantiated.
-};
+namespace {
 
 Effect *get_effect_from_blueprint(EffectChain *chain, lua_State *L, int idx)
 {
        EffectBlueprint *blueprint = *(EffectBlueprint **)luaL_checkudata(L, idx, "EffectBlueprint");
        if (blueprint->effect != nullptr) {
-               // NOTE: This will change in the future.
                luaL_error(L, "An effect can currently only be added to one chain.\n");
        }
 
@@ -217,6 +186,8 @@ InputStateInfo *get_input_state_info(lua_State *L, int idx)
        return nullptr;
 }
 
+}  // namespace
+
 bool checkbool(lua_State* L, int idx)
 {
        luaL_checktype(L, idx, LUA_TBOOLEAN);
@@ -230,6 +201,28 @@ string checkstdstring(lua_State *L, int index)
        return string(cstr, len);
 }
 
+namespace {
+
+int Scene_new(lua_State* L)
+{
+       assert(lua_gettop(L) == 2);
+       Theme *theme = get_theme_updata(L);
+       int aspect_w = luaL_checknumber(L, 1);
+       int aspect_h = luaL_checknumber(L, 2);
+
+       return wrap_lua_object<Scene>(L, "Scene", theme, aspect_w, aspect_h);
+}
+
+int Scene_gc(lua_State* L)
+{
+       assert(lua_gettop(L) == 1);
+       Scene *chain = (Scene *)luaL_checkudata(L, 1, "Scene");
+       chain->~Scene();
+       return 0;
+}
+
+}  // namespace
+
 void add_outputs_and_finalize(EffectChain *chain, bool is_main_chain)
 {
        // Add outputs as needed.
@@ -281,6 +274,8 @@ void add_outputs_and_finalize(EffectChain *chain, bool is_main_chain)
        chain->finalize();
 }
 
+namespace {
+
 int EffectChain_new(lua_State* L)
 {
        assert(lua_gettop(L) == 2);
@@ -560,6 +555,12 @@ int HTMLInput_get_signal_num(lua_State* L)
 }
 #endif
 
+int IdentityEffect_new(lua_State* L)
+{
+       assert(lua_gettop(L) == 0);
+       return wrap_lua_object_nonowned<EffectBlueprint>(L, "EffectBlueprint", IDENTITY_EFFECT);
+}
+
 int WhiteBalanceEffect_new(lua_State* L)
 {
        assert(lua_gettop(L) == 0);
@@ -618,6 +619,7 @@ int InputStateInfo_get_width(lua_State* L)
 {
        assert(lua_gettop(L) == 2);
        InputStateInfo *input_state_info = get_input_state_info(L, 1);
+
        Theme *theme = get_theme_updata(L);
        int signal_num = theme->map_signal(luaL_checknumber(L, 2));
        lua_pushnumber(L, input_state_info->last_width[signal_num]);
@@ -777,6 +779,28 @@ int EffectBlueprint_set_vec4(lua_State *L)
        return 0;
 }
 
+const luaL_Reg Scene_funcs[] = {
+       { "new", Scene_new },
+       { "__gc", Scene_gc },
+       { "add_input", Scene::add_input },
+       { "add_effect", Scene::add_effect },
+       { "add_optional_effect", Scene::add_optional_effect },
+       { "finalize", Scene::finalize },
+       { NULL, NULL }
+};
+
+const luaL_Reg Block_funcs[] = {
+       { "display", Block_display },
+       { "choose_alternative", Block_choose_alternative },
+       { "enable", Block_enable },
+       { "disable", Block_disable },
+       { "set_int", Block_set_int },
+       { "set_float", Block_set_float },
+       { "set_vec3", Block_set_vec3 },
+       { "set_vec4", Block_set_vec4 },
+       { NULL, NULL }
+};
+
 const luaL_Reg EffectBlueprint_funcs[] = {
        // NOTE: No new() function; that's for the individual effects.
        { "set_int", EffectBlueprint_set_int },
@@ -835,6 +859,11 @@ const luaL_Reg HTMLInput_funcs[] = {
 // All of these are solely for new(); the returned metatable will be that of
 // EffectBlueprint, and Effect (returned from add_effect()) is its own type.
 
+const luaL_Reg IdentityEffect_funcs[] = {
+       { "new", IdentityEffect_new },
+       { NULL, NULL }
+};
+
 const luaL_Reg WhiteBalanceEffect_funcs[] = {
        { "new", WhiteBalanceEffect_new },
        { NULL, NULL }
@@ -1200,12 +1229,15 @@ Theme::Theme(const string &filename, const vector<string> &search_dirs, Resource
 
        // Set up the API we provide.
        register_constants();
+       register_class("Scene", Scene_funcs);
+       register_class("Block", Block_funcs);
        register_class("EffectBlueprint", EffectBlueprint_funcs);
        register_class("EffectChain", EffectChain_funcs);
        register_class("LiveInputWrapper", LiveInputWrapper_funcs);
        register_class("ImageInput", ImageInput_funcs);
        register_class("VideoInput", VideoInput_funcs);
        register_class("HTMLInput", HTMLInput_funcs);
+       register_class("IdentityEffect", IdentityEffect_funcs);
        register_class("WhiteBalanceEffect", WhiteBalanceEffect_funcs);
        register_class("ResampleEffect", ResampleEffect_funcs);
        register_class("PaddingEffect", PaddingEffect_funcs);
@@ -1276,31 +1308,8 @@ void Theme::register_class(const char *class_name, const luaL_Reg *funcs)
        assert(lua_gettop(L) == 0);
 }
 
-Theme::Chain Theme::get_chain(unsigned num, float t, unsigned width, unsigned height, const InputState &input_state) 
+Theme::Chain Theme::get_chain_from_effect_chain(EffectChain *effect_chain, unsigned num, const InputState &input_state)
 {
-       Chain chain;
-
-       lock_guard<mutex> lock(m);
-       assert(lua_gettop(L) == 0);
-       lua_getglobal(L, "get_chain");  /* function to be called */
-       lua_pushnumber(L, num);
-       lua_pushnumber(L, t);
-       lua_pushnumber(L, width);
-       lua_pushnumber(L, height);
-       wrap_lua_object<InputStateInfo>(L, "InputStateInfo", input_state);
-
-       if (lua_pcall(L, 5, 2, 0) != 0) {
-               fprintf(stderr, "error running function `get_chain': %s\n", lua_tostring(L, -1));
-               abort();
-       }
-
-       EffectChain *effect_chain = (EffectChain *)luaL_testudata(L, -2, "EffectChain");
-       if (effect_chain == nullptr) {
-               fprintf(stderr, "get_chain() for chain number %d did not return an EffectChain\n",
-                       num);
-               abort();
-       }
-       chain.chain = effect_chain;
        if (!lua_isfunction(L, -1)) {
                fprintf(stderr, "Argument #-1 should be a function\n");
                abort();
@@ -1308,8 +1317,9 @@ Theme::Chain Theme::get_chain(unsigned num, float t, unsigned width, unsigned he
        lua_pushvalue(L, -1);
        shared_ptr<LuaRefWithDeleter> funcref(new LuaRefWithDeleter(&m, L, luaL_ref(L, LUA_REGISTRYINDEX)));
        lua_pop(L, 2);
-       assert(lua_gettop(L) == 0);
 
+       Chain chain;
+       chain.chain = effect_chain;
        chain.setup_chain = [this, funcref, input_state, effect_chain]{
                lock_guard<mutex> lock(m);
 
@@ -1341,6 +1351,53 @@ Theme::Chain Theme::get_chain(unsigned num, float t, unsigned width, unsigned he
 
                this->input_state = nullptr;
        };
+       return chain;
+}
+
+Theme::Chain Theme::get_chain(unsigned num, float t, unsigned width, unsigned height, const InputState &input_state)
+{
+       const char *func_name = "get_scene";  // For error reporting.
+       Chain chain;
+
+       lock_guard<mutex> lock(m);
+       assert(lua_gettop(L) == 0);
+       lua_getglobal(L, "get_scene");  /* function to be called */
+       if (lua_isnil(L, -1)) {
+               // Try the pre-1.9.0 name for compatibility.
+               lua_pop(L, 1);
+               lua_getglobal(L, "get_chain");
+               func_name = "get_chain";
+       }
+       lua_pushnumber(L, num);
+       lua_pushnumber(L, t);
+       lua_pushnumber(L, width);
+       lua_pushnumber(L, height);
+       wrap_lua_object<InputStateInfo>(L, "InputStateInfo", input_state);
+
+       if (lua_pcall(L, 5, LUA_MULTRET, 0) != 0) {
+               fprintf(stderr, "error running function “%s”: %s\n", func_name, lua_tostring(L, -1));
+               abort();
+       }
+
+       if (luaL_testudata(L, -1, "Scene") != nullptr) {
+               if (lua_gettop(L) != 1) {
+                       luaL_error(L, "%s() for chain number %d returned an Scene, but also other items", func_name);
+               }
+               Scene *auto_effect_chain = (Scene *)luaL_testudata(L, -1, "Scene");
+               auto chain_and_setup = auto_effect_chain->get_chain(this, L, num, input_state);
+               chain.chain = chain_and_setup.first;
+               chain.setup_chain = move(chain_and_setup.second);
+       } else if (luaL_testudata(L, -2, "EffectChain") != nullptr) {
+               // Old-style (pre-Nageru 1.9.0) return of a single chain and prepare function.
+               if (lua_gettop(L) != 2) {
+                       luaL_error(L, "%s() for chain number %d returned an EffectChain, but needs to also return a prepare function (or use Scene)", func_name);
+               }
+               EffectChain *effect_chain = (EffectChain *)luaL_testudata(L, -2, "EffectChain");
+               chain = get_chain_from_effect_chain(effect_chain, num, input_state);
+       } else {
+               luaL_error(L, "%s() for chain number %d did not return an EffectChain or Scene\n", func_name, num);
+       }
+       assert(lua_gettop(L) == 0);
 
        // TODO: Can we do better, e.g. by running setup_chain() and seeing what it references?
        // Actually, setup_chain does maybe hold all the references we need now anyway?
index 3d57750c9d9cd9ac39c272a1b66997637f15edbf..7c6b3243c85e5d61a1bd7ef42c77d64e7444c0a0 100644 (file)
 #include <vector>
 
 #include "bmusb/bmusb.h"
+#include "defs.h"
 #include "ref_counted_frame.h"
 #include "tweaked_inputs.h"
 
+class Scene;
 class CEFCapture;
 class FFmpegCapture;
 class LiveInputWrapper;
@@ -27,6 +29,55 @@ class EffectChain;
 class ResourcePool;
 }  // namespace movit
 
+enum EffectType {
+       // LIVE_INPUT_* also covers CEF and video inputs.
+       LIVE_INPUT_YCBCR,
+       LIVE_INPUT_YCBCR_WITH_DEINTERLACE,
+       LIVE_INPUT_YCBCR_PLANAR,
+       LIVE_INPUT_BGRA,
+       IMAGE_INPUT,
+
+       IDENTITY_EFFECT,
+       WHITE_BALANCE_EFFECT,
+       RESAMPLE_EFFECT,
+       PADDING_EFFECT,
+       INTEGRAL_PADDING_EFFECT,
+       OVERLAY_EFFECT,
+       RESIZE_EFFECT,
+       MULTIPLY_EFFECT,
+       MIX_EFFECT,
+       LIFT_GAMMA_GAIN_EFFECT
+};
+
+// An EffectBlueprint refers to an Effect before it's being added to the graph.
+// It contains enough information to instantiate the effect, including any
+// parameters that were set before it was added to the graph. Once it is
+// instantiated, it forwards its calls on to the real Effect instead.
+struct EffectBlueprint {
+       EffectBlueprint(EffectType effect_type) : effect_type(effect_type) {}
+
+       EffectType effect_type;
+       std::map<std::string, int> int_parameters;
+       std::map<std::string, float> float_parameters;
+       std::map<std::string, std::array<float, 3>> vec3_parameters;
+       std::map<std::string, std::array<float, 4>> vec4_parameters;
+
+       movit::Effect *effect = nullptr;  // Gets filled out when it's instantiated.
+};
+
+// Contains basically the same data as InputState, but does not hold on to
+// a reference to the frames. This is important so that we can release them
+// without having to wait for Lua's GC.
+struct InputStateInfo {
+       explicit InputStateInfo(const InputState& input_state);
+
+       unsigned last_width[MAX_VIDEO_CARDS], last_height[MAX_VIDEO_CARDS];
+       bool last_interlaced[MAX_VIDEO_CARDS], last_has_signal[MAX_VIDEO_CARDS], last_is_connected[MAX_VIDEO_CARDS];
+       unsigned last_frame_rate_nom[MAX_VIDEO_CARDS], last_frame_rate_den[MAX_VIDEO_CARDS];
+       bool has_last_subtitle[MAX_VIDEO_CARDS];
+       std::string last_subtitle[MAX_VIDEO_CARDS];
+};
+
 class Theme {
 public:
        Theme(const std::string &filename, const std::vector<std::string> &search_dirs, movit::ResourcePool *resource_pool, unsigned num_cards);
@@ -113,6 +164,7 @@ private:
        void register_constants();
        void register_class(const char *class_name, const luaL_Reg *funcs);
        int set_theme_menu(lua_State *L);
+       Chain get_chain_from_effect_chain(movit::EffectChain *effect_chain, unsigned num, const InputState &input_state);
 
        std::string theme_path;
 
@@ -147,6 +199,7 @@ private:
        std::function<void()> theme_menu_callback;
 
        friend class LiveInputWrapper;
+       friend class Scene;
        friend int ThemeMenu_set(lua_State *L);
 };
 
@@ -184,4 +237,12 @@ private:
        bool user_connectable;
 };
 
+// Utility functions used by Scene.
+void add_outputs_and_finalize(movit::EffectChain *chain, bool is_main_chain);
+Theme *get_theme_updata(lua_State* L);
+bool checkbool(lua_State* L, int idx);
+std::string checkstdstring(lua_State *L, int index);
+movit::Effect *instantiate_effect(movit::EffectChain *chain, EffectType effect_type);
+void print_warning(lua_State* L, const char *format, ...);
+
 #endif  // !defined(_THEME_H)
index 401cbd9b842d822d74d0fbbbd2f437e630e3e95e..0bbb7194a6eb8c2ff9151739c47fb5e55d9c12aa 100644 (file)
@@ -1,11 +1,13 @@
 -- The theme is what decides what's actually shown on screen, what kind of
 -- transitions are available (if any), and what kind of inputs there are,
 -- if any. In general, it drives the entire display logic by creating Movit
--- chains, setting their parameters and then deciding which to show when.
+-- chains (called “scenes”), setting their parameters and then deciding which
+-- to show when.
 --
 -- Themes are written in Lua, which reflects a simplified form of the Movit API
--- where all the low-level details (such as texture formats) are handled by the
--- C++ side and you generally just build chains.
+-- where all the low-level details (such as texture formats) and alternatives
+-- (e.g. turning scaling on or off) are handled by the C++ side and you
+-- generally just build scenes.
 
 local state = {
        transition_start = -2.0,
@@ -39,221 +41,83 @@ local FADE_TRANSITION = 2
 -- frame and not per field, since we deinterlace.
 local last_resolution = {}
 
--- Utility function to help creating many similar chains that can differ
--- in a free set of chosen parameters.
-function make_cartesian_product(parms, callback)
-       return make_cartesian_product_internal(parms, callback, 1, {})
-end
-
-function make_cartesian_product_internal(parms, callback, index, args)
-       if index > #parms then
-               return callback(unpack(args))
-       end
-       local ret = {}
-       for _, value in ipairs(parms[index]) do
-               args[index] = value
-               ret[value] = make_cartesian_product_internal(parms, callback, index + 1, args)
-       end
-       return ret
-end
-
-function make_sbs_input(chain, signal, deint, hq)
-       local input = chain:add_live_input(not deint, deint)  -- Override bounce only if not deinterlacing.
-       input:connect_signal(signal)
-
-       local resample_effect = nil
-       local resize_effect = nil
-       if (hq) then
-               resample_effect = chain:add_effect(ResampleEffect.new())
-       else
-               resize_effect = chain:add_effect(ResizeEffect.new())
-       end
-       local wb_effect = chain:add_effect(WhiteBalanceEffect.new())
-
-       local padding_effect = chain:add_effect(IntegralPaddingEffect.new())
-
+function make_sbs_input(scene)
+       local resample_effect = ResampleEffect.new()
+       local resize_effect = ResizeEffect.new()
        return {
-               input = input,
-               wb_effect = wb_effect,
+               input = scene:add_input(),
                resample_effect = resample_effect,
                resize_effect = resize_effect,
-               padding_effect = padding_effect
+               resample_switcher = scene:add_effect({resample_effect, resize_effect}),
+               wb_effect = scene:add_effect(WhiteBalanceEffect.new()),
+               padding_effect = scene:add_effect(IntegralPaddingEffect.new())
        }
 end
 
--- The main live chain.
-function make_sbs_chain(input0_type, input1_type, hq)
-       local chain = EffectChain.new(16, 9)
-
-       local input0 = make_sbs_input(chain, INPUT0_SIGNAL_NUM, input0_type == "livedeint", hq)
-       local input1 = make_sbs_input(chain, INPUT1_SIGNAL_NUM, input1_type == "livedeint", hq)
+-- The main live scene.
+function make_sbs_scene()
+       local scene = Scene.new(16, 9)
 
+       local input0 = make_sbs_input(scene)
+       input0.input:display(0)
        input0.padding_effect:set_vec4("border_color", 0.0, 0.0, 0.0, 1.0)
+
+       local input1 = make_sbs_input(scene)
+       input1.input:display(1)
        input1.padding_effect:set_vec4("border_color", 0.0, 0.0, 0.0, 0.0)
 
-       chain:add_effect(OverlayEffect.new(), input0.padding_effect, input1.padding_effect)
-       chain:finalize(hq)
+       scene:add_effect(OverlayEffect.new(), input0.padding_effect, input1.padding_effect)
+       scene:finalize()
 
        return {
-               chain = chain,
+               scene = scene,
                input0 = input0,
                input1 = input1
        }
 end
+local sbs_scene = make_sbs_scene()
 
--- Make all possible combinations of side-by-side chains.
-local sbs_chains = make_cartesian_product({
-       {"live", "livedeint"},  -- input0_type
-       {"live", "livedeint"},  -- input1_type
-       {true, false}           -- hq
-}, function(input0_type, input1_type, hq)
-       return make_sbs_chain(input0_type, input1_type, hq)
-end)
-
-function make_fade_input(chain, signal, live, deint, scale)
-       local input, wb_effect, resample_effect, last
-       if live then
-               input = chain:add_live_input(false, deint)
-               input:connect_signal(signal)
-               last = input
-       else
-               input = chain:add_effect(ImageInput.new("bg.jpeg"))
-               last = input
-       end
-
-       -- If we cared about this for the non-main inputs, we would have
-       -- checked hq here and invoked ResizeEffect instead.
-       if scale then
-               resample_effect = chain:add_effect(ResampleEffect.new())
-               last = resample_effect
-       end
-
-       -- Make sure to put the white balance after the scaling (usually more efficient).
-       if live then
-               wb_effect = chain:add_effect(WhiteBalanceEffect.new())
-               last = wb_effect
-       end
-
+function make_fade_input(scene)
        return {
-               input = input,
-               wb_effect = wb_effect,
-               resample_effect = resample_effect,
-               last = last
+               input = scene:add_input(),
+               resample_effect = scene:add_optional_effect(ResampleEffect.new()),  -- Activated if scaling.
+               wb_effect = scene:add_optional_effect(WhiteBalanceEffect.new())  -- Activated for video inputs.
        }
 end
 
--- A chain to fade between two inputs, of which either can be a picture
--- or a live input. In practice only used live, but we still support the
--- hq parameter.
-function make_fade_chain(input0_live, input0_deint, input0_scale, input1_live, input1_deint, input1_scale, hq)
-       local chain = EffectChain.new(16, 9)
-
-       local input0 = make_fade_input(chain, INPUT0_SIGNAL_NUM, input0_live, input0_deint, input0_scale)
-       local input1 = make_fade_input(chain, INPUT1_SIGNAL_NUM, input1_live, input1_deint, input1_scale)
-
-       local mix_effect = chain:add_effect(MixEffect.new(), input0.last, input1.last)
-       chain:finalize(hq)
+-- A scene to fade between two inputs, of which either can be a picture
+-- or a live input. Only used live.
+function make_fade_scene()
+       local scene = Scene.new(16, 9)
+       local input0 = make_fade_input(scene)
+       local input1 = make_fade_input(scene)
+       local mix_effect = scene:add_effect(MixEffect.new(), input0.wb_effect, input1.wb_effect)
+       scene:finalize(true)  -- Only used live.
 
        return {
-               chain = chain,
+               scene = scene,
                input0 = input0,
                input1 = input1,
                mix_effect = mix_effect
        }
 end
+local fade_scene = make_fade_scene()
 
--- Chains to fade between two inputs, in various configurations.
-local fade_chains = make_cartesian_product({
-       {"static", "live", "livedeint"},  -- input0_type
-       {true, false},                    -- input0_scale
-       {"static", "live", "livedeint"},  -- input1_type
-       {true, false},                    -- input1_scale
-       {true}                            -- hq
-}, function(input0_type, input0_scale, input1_type, input1_scale, hq)
-       local input0_live = (input0_type ~= "static")
-       local input1_live = (input1_type ~= "static")
-       local input0_deint = (input0_type == "livedeint")
-       local input1_deint = (input1_type == "livedeint")
-       return make_fade_chain(input0_live, input0_deint, input0_scale, input1_live, input1_deint, input1_scale, hq)
-end)
-
--- A chain to show a single input on screen.
-function make_simple_chain(input_deint, input_scale, hq)
-       local chain = EffectChain.new(16, 9)
-
-       local input = chain:add_live_input(false, input_deint)
-       input:connect_signal(0)  -- First input card. Can be changed whenever you want.
-
-       local resample_effect, resize_effect
-       if input_scale then
-               if hq then
-                       resample_effect = chain:add_effect(ResampleEffect.new())
-               else
-                       resize_effect = chain:add_effect(ResizeEffect.new())
-               end
-       end
-
-       local wb_effect = chain:add_effect(WhiteBalanceEffect.new())
-       chain:finalize(hq)
-
-       return {
-               chain = chain,
-               input = input,
-               wb_effect = wb_effect,
-               resample_effect = resample_effect,
-               resize_effect = resize_effect
-       }
-end
-
--- Make all possible combinations of single-input chains.
-local simple_chains = make_cartesian_product({
-       {"live", "livedeint"},  -- input_type
-       {true, false},          -- input_scale
-       {true, false}           -- hq
-}, function(input_type, input_scale, hq)
-       local input_deint = (input_type == "livedeint")
-       return make_simple_chain(input_deint, input_scale, hq)
-end)
-
--- A chain to show a single static picture on screen (HQ version).
-local static_chain_hq = EffectChain.new(16, 9)
-local static_chain_hq_input = static_chain_hq:add_effect(ImageInput.new("bg.jpeg"))
-static_chain_hq:finalize(true)
-
--- A chain to show a single static picture on screen (LQ version).
-local static_chain_lq = EffectChain.new(16, 9)
-local static_chain_lq_input = static_chain_lq:add_effect(ImageInput.new("bg.jpeg"))
-static_chain_lq:finalize(false)
-
--- Used for indexing into the tables of chains.
-function get_input_type(signals, signal_num)
-       if signal_num == STATIC_SIGNAL_NUM then
-               return "static"
-       elseif signals:get_interlaced(signal_num) then
-               return "livedeint"
-       else
-               return "live"
-       end
-end
-
-function needs_scale(signals, signal_num, width, height)
-       if signal_num == STATIC_SIGNAL_NUM then
-               -- We assume this is already correctly scaled at load time.
-               return false
-       end
-       assert(is_plain_signal(signal_num))
-       return (signals:get_width(signal_num) ~= width or signals:get_height(signal_num) ~= height)
-end
+-- A scene to show a single input on screen.
+local scene = Scene.new(16, 9)
+local simple_scene = {
+       scene = scene,
+       input = scene:add_input(),
+       resample_effect = scene:add_effect({ResampleEffect.new(), ResizeEffect.new(), IdentityEffect.new()}),
+       wb_effect = scene:add_effect(WhiteBalanceEffect.new())
+}
+scene:finalize()
 
-function set_scale_parameters_if_needed(chain_or_input, width, height)
-       if chain_or_input.resample_effect then
-               chain_or_input.resample_effect:set_int("width", width)
-               chain_or_input.resample_effect:set_int("height", height)
-       elseif chain_or_input.resize_effect then
-               chain_or_input.resize_effect:set_int("width", width)
-               chain_or_input.resize_effect:set_int("height", height)
-       end
-end
+-- A scene to show a single static picture on screen.
+local static_image = ImageInput.new("bg.jpeg")  -- Also used as input to other scenes.
+local static_scene = Scene.new(16, 9)
+static_scene:add_input(static_image)  -- Note: Locks this input to images only.
+static_scene:finalize()
 
 -- API ENTRY POINT
 -- Returns the number of outputs in addition to the live (0) and preview (1).
@@ -492,7 +356,7 @@ function transition_clicked(num, t)
                    state.preview_signal_num == STATIC_SIGNAL_NUM) then
                        start_transition(FADE_TRANSITION, t, 1.0)
                else
-                       -- Fades involving SBS are ignored (we have no chain for it).
+                       -- Fades involving SBS are ignored (we have no scene for it).
                end
        end
 end
@@ -502,40 +366,55 @@ function channel_clicked(num)
        state.preview_signal_num = num
 end
 
-function get_fade_chain(state, signals, t, width, height, input_resolution)
-       local input0_type = get_input_type(signals, state.transition_src_signal)
-       local input0_scale = needs_scale(signals, state.transition_src_signal, width, height)
-       local input1_type = get_input_type(signals, state.transition_dst_signal)
-       local input1_scale = needs_scale(signals, state.transition_dst_signal, width, height)
-       local chain = fade_chains[input0_type][input0_scale][input1_type][input1_scale][true]
-       local prepare = function()
-               if input0_type == "live" or input0_type == "livedeint" then
-                       chain.input0.input:connect_signal(state.transition_src_signal)
-                       set_neutral_color_from_signal(state, chain.input0.wb_effect, state.transition_src_signal)
-               end
-               set_scale_parameters_if_needed(chain.input0, width, height)
-               if input1_type == "live" or input1_type == "livedeint" then
-                       chain.input1.input:connect_signal(state.transition_dst_signal)
-                       set_neutral_color_from_signal(state, chain.input1.wb_effect, state.transition_dst_signal)
+function setup_fade_input(state, input, signals, signal_num, width, height)
+       if signal_num == STATIC_SIGNAL_NUM then
+               input.input:display(static_image)
+               input.wb_effect:disable()
+
+               -- We assume this is already correctly scaled at load time.
+               input.resample_effect:disable()
+       else
+               input.input:display(signal_num)
+               input.wb_effect:enable()
+               set_neutral_color(input.wb_effect, state.neutral_colors[signal_num - INPUT0_SIGNAL_NUM + 1])
+
+               if (signals:get_width(signal_num) ~= width or signals:get_height(signal_num) ~= height) then
+                       input.resample_effect:enable()
+                       input.resample_effect:set_int("width", width)
+                       input.resample_effect:set_int("height", height)
+               else
+                       input.resample_effect:disable()
                end
-               set_scale_parameters_if_needed(chain.input1, width, height)
-               local tt = calc_fade_progress(t, state.transition_start, state.transition_end)
+       end
+end
 
-               chain.mix_effect:set_float("strength_first", 1.0 - tt)
-               chain.mix_effect:set_float("strength_second", tt)
+function needs_scale(signals, signal_num, width, height)
+       if signal_num == STATIC_SIGNAL_NUM then
+               -- We assume this is already correctly scaled at load time.
+               return false
        end
-       return chain.chain, prepare
+       assert(is_plain_signal(signal_num))
+       return (signals:get_width(signal_num) ~= width or signals:get_height(signal_num) ~= height)
 end
 
--- SBS code (live_signal_num == SBS_SIGNAL_NUM, or in a transition to/from it).
-function get_sbs_chain(signals, t, width, height, input_resolution)
-       local input0_type = get_input_type(signals, INPUT0_SIGNAL_NUM)
-       local input1_type = get_input_type(signals, INPUT1_SIGNAL_NUM)
-       return sbs_chains[input0_type][input1_type][true]
+function setup_simple_input(state, signals, signal_num, width, height, hq)
+       simple_scene.input:display(signal_num)
+       if needs_scale(signals, signal_num, width, height) then
+               if hq then
+                       simple_scene.resample_effect:choose_alternative(0)  -- High-quality resampling (ResampleEffect)
+               else
+                       simple_scene.resample_effect:choose_alternative(1)  -- Low-quality resampling (ResizeEffect)
+               end
+               simple_scene.resample_effect:set_int("width", width)
+               simple_scene.resample_effect:set_int("height", height)
+       else
+               simple_scene.resample_effect:choose_alternative(2)  -- No scaling.
+       end
+       set_neutral_color_from_signal(state, simple_scene.wb_effect, signal_num)
 end
 
 -- API ENTRY POINT
--- Called every frame. Get the chain for displaying at input <num>,
+-- Called every frame. Get the scene for displaying at input <num>,
 -- where 0 is live, 1 is preview, 2 is the first channel to display
 -- in the bottom bar, and so on up to num_channels()+1. t is the
 -- current time in seconds. width and height are the dimensions of
@@ -545,20 +424,12 @@ end
 -- <signals> is basically an exposed InputState, which you can use to
 -- query for information about the signals at the point of the current
 -- frame. In particular, you can call get_width() and get_height()
--- for any signal number, and use that to e.g. assist in chain selection.
+-- for any signal number, and use that to e.g. assist in scene selection.
 --
--- You should return two objects; the chain itself, and then a
--- function (taking no parameters) that is run just before rendering.
--- The function needs to call connect_signal on any inputs, so that
--- it gets updated video data for the given frame. (You are allowed
--- to switch which input your input is getting from between frames,
--- but not calling connect_signal results in undefined behavior.)
--- If you want to change any parameters in the chain, this is also
--- the right place.
---
--- NOTE: The chain returned must be finalized with the Y'CbCr flag
--- if and only if num==0.
-function get_chain(num, t, width, height, signals)
+-- You should return scene to use, after having set any parameters you
+-- want to set (through set_int() etc.). The parameters will be snapshot
+-- at return time and used during rendering.
+function get_scene(num, t, width, height, signals)
        local input_resolution = {}
        for signal_num=0,1 do
                local res = {
@@ -586,46 +457,30 @@ function get_chain(num, t, width, height, signals)
        end
        last_resolution = input_resolution
 
-       -- Make a (semi-shallow) copy of the current state, so that the returned prepare function
-       -- is unaffected by state changes made by the UI before it is rendered.
-       local state_copy = {}
-       for key, value in pairs(state) do
-               state_copy[key] = value
-       end
-       state_copy.neutral_colors = { unpack(state.neutral_colors) }
-
        if num == 0 then  -- Live.
                finish_transitions(t)
                if state.transition_type == ZOOM_TRANSITION then
                        -- Transition in or out of SBS.
-                       local chain = get_sbs_chain(signals, t, width, height, input_resolution)
-                       local prepare = function()
-                               prepare_sbs_chain(state_copy, chain, calc_zoom_progress(state_copy, t), state_copy.transition_type, state_copy.transition_src_signal, state_copy.transition_dst_signal, width, height, input_resolution)
-                       end
-                       return chain.chain, prepare
+                       prepare_sbs_scene(state, calc_zoom_progress(state, t), state.transition_type, state.transition_src_signal, state.transition_dst_signal, width, height, input_resolution, true)
+                       return sbs_scene.scene
                elseif state.transition_type == NO_TRANSITION and state.live_signal_num == SBS_SIGNAL_NUM then
                        -- Static SBS view.
-                       local chain = get_sbs_chain(signals, t, width, height, input_resolution)
-                       local prepare = function()
-                               prepare_sbs_chain(state_copy, chain, 0.0, NO_TRANSITION, 0, SBS_SIGNAL_NUM, width, height, input_resolution)
-                       end
-                       return chain.chain, prepare
+                       prepare_sbs_scene(state, 0.0, NO_TRANSITION, 0, SBS_SIGNAL_NUM, width, height, input_resolution, true)
+                       return sbs_scene.scene
                elseif state.transition_type == FADE_TRANSITION then
-                       return get_fade_chain(state_copy, signals, t, width, height, input_resolution)
+                       setup_fade_input(state, fade_scene.input0, signals, state.transition_src_signal, width, height)
+                       setup_fade_input(state, fade_scene.input1, signals, state.transition_dst_signal, width, height)
+
+                       local tt = calc_fade_progress(t, state.transition_start, state.transition_end)
+                       fade_scene.mix_effect:set_float("strength_first", 1.0 - tt)
+                       fade_scene.mix_effect:set_float("strength_second", tt)
+
+                       return fade_scene.scene
                elseif is_plain_signal(state.live_signal_num) then
-                       local input_type = get_input_type(signals, state.live_signal_num)
-                       local input_scale = needs_scale(signals, state.live_signal_num, width, height)
-                       local chain = simple_chains[input_type][input_scale][true]
-                       local prepare = function()
-                               chain.input:connect_signal(state_copy.live_signal_num)
-                               set_scale_parameters_if_needed(chain, width, height)
-                               set_neutral_color_from_signal(state_copy, chain.wb_effect, state_copy.live_signal_num)
-                       end
-                       return chain.chain, prepare
+                       setup_simple_input(state, signals, state.live_signal_num, width, height, true)
+                       return simple_scene.scene
                elseif state.live_signal_num == STATIC_SIGNAL_NUM then  -- Static picture.
-                       local prepare = function()
-                       end
-                       return static_chain_hq, prepare
+                       return static_scene
                else
                        assert(false)
                end
@@ -636,58 +491,37 @@ function get_chain(num, t, width, height, signals)
 
        -- Individual preview inputs.
        if is_plain_signal(num - 2) then
-               local signal_num = num - 2
-               local input_type = get_input_type(signals, signal_num)
-               local input_scale = needs_scale(signals, signal_num, width, height)
-               local chain = simple_chains[input_type][input_scale][false]
-               local prepare = function()
-                       chain.input:connect_signal(signal_num)
-                       set_scale_parameters_if_needed(chain, width, height)
-                       set_neutral_color(chain.wb_effect, state_copy.neutral_colors[signal_num + 1])
-               end
-               return chain.chain, prepare
+               setup_simple_input(state, signals, num - 2, width, height, false)
+               return simple_scene.scene
        end
        if num == SBS_SIGNAL_NUM + 2 then
-               local input0_type = get_input_type(signals, INPUT0_SIGNAL_NUM)
-               local input1_type = get_input_type(signals, INPUT1_SIGNAL_NUM)
-               local chain = sbs_chains[input0_type][input1_type][false]
-               local prepare = function()
-                       prepare_sbs_chain(state_copy, chain, 0.0, NO_TRANSITION, 0, SBS_SIGNAL_NUM, width, height, input_resolution)
-               end
-               return chain.chain, prepare
+               prepare_sbs_scene(state, 0.0, NO_TRANSITION, 0, SBS_SIGNAL_NUM, width, height, input_resolution, false)
+               return sbs_scene.scene
        end
        if num == STATIC_SIGNAL_NUM + 2 then
-               local prepare = function()
-               end
-               return static_chain_lq, prepare
+               return static_scene
        end
 end
 
-function place_rectangle(resample_effect, resize_effect, padding_effect, x0, y0, x1, y1, screen_width, screen_height, input_width, input_height)
-       local srcx0 = 0.0
-       local srcx1 = 1.0
-       local srcy0 = 0.0
-       local srcy1 = 1.0
-
-       padding_effect:set_int("width", screen_width)
-       padding_effect:set_int("height", screen_height)
+function place_rectangle(input, x0, y0, x1, y1, screen_width, screen_height, input_width, input_height, hq)
+       input.padding_effect:set_int("width", screen_width)
+       input.padding_effect:set_int("height", screen_height)
 
        -- Cull.
        if x0 > screen_width or x1 < 0.0 or y0 > screen_height or y1 < 0.0 then
-               if resample_effect ~= nil then
-                       resample_effect:set_int("width", 1)
-                       resample_effect:set_int("height", 1)
-                       resample_effect:set_float("zoom_x", screen_width)
-                       resample_effect:set_float("zoom_y", screen_height)
-               else
-                       resize_effect:set_int("width", 1)
-                       resize_effect:set_int("height", 1)
-               end
-               padding_effect:set_int("left", screen_width + 100)
-               padding_effect:set_int("top", screen_height + 100)
+               input.resample_switcher:choose_alternative(1)  -- Low-quality resizing.
+               input.resize_effect:set_int("width", 1)
+               input.resize_effect:set_int("height", 1)
+               input.padding_effect:set_int("left", screen_width + 100)
+               input.padding_effect:set_int("top", screen_height + 100)
                return
        end
 
+       local srcx0 = 0.0
+       local srcx1 = 1.0
+       local srcy0 = 0.0
+       local srcy1 = 1.0
+
        -- Clip.
        if x0 < 0 then
                srcx0 = -x0 / (x1 - x0)
@@ -706,8 +540,10 @@ function place_rectangle(resample_effect, resize_effect, padding_effect, x0, y0,
                y1 = screen_height
        end
 
-       if resample_effect ~= nil then
+       if hq then
                -- High-quality resampling.
+               input.resample_switcher:choose_alternative(0)
+
                local x_subpixel_offset = x0 - math.floor(x0)
                local y_subpixel_offset = y0 - math.floor(y0)
 
@@ -715,41 +551,49 @@ function place_rectangle(resample_effect, resize_effect, padding_effect, x0, y0,
                -- and then add an extra pixel so we have some leeway for the border.
                local width = math.ceil(x1 - x0) + 1
                local height = math.ceil(y1 - y0) + 1
-               resample_effect:set_int("width", width)
-               resample_effect:set_int("height", height)
+               input.resample_effect:set_int("width", width)
+               input.resample_effect:set_int("height", height)
 
                -- Correct the discrepancy with zoom. (This will leave a small
                -- excess edge of pixels and subpixels, which we'll correct for soon.)
                local zoom_x = (x1 - x0) / (width * (srcx1 - srcx0))
                local zoom_y = (y1 - y0) / (height * (srcy1 - srcy0))
-               resample_effect:set_float("zoom_x", zoom_x)
-               resample_effect:set_float("zoom_y", zoom_y)
-               resample_effect:set_float("zoom_center_x", 0.0)
-               resample_effect:set_float("zoom_center_y", 0.0)
+               input.resample_effect:set_float("zoom_x", zoom_x)
+               input.resample_effect:set_float("zoom_y", zoom_y)
+               input.resample_effect:set_float("zoom_center_x", 0.0)
+               input.resample_effect:set_float("zoom_center_y", 0.0)
 
                -- Padding must also be to a whole-pixel offset.
-               padding_effect:set_int("left", math.floor(x0))
-               padding_effect:set_int("top", math.floor(y0))
+               input.padding_effect:set_int("left", math.floor(x0))
+               input.padding_effect:set_int("top", math.floor(y0))
 
                -- Correct _that_ discrepancy by subpixel offset in the resampling.
-               resample_effect:set_float("left", srcx0 * input_width - x_subpixel_offset / zoom_x)
-               resample_effect:set_float("top", srcy0 * input_height - y_subpixel_offset / zoom_y)
+               input.resample_effect:set_float("left", srcx0 * input_width - x_subpixel_offset / zoom_x)
+               input.resample_effect:set_float("top", srcy0 * input_height - y_subpixel_offset / zoom_y)
 
                -- Finally, adjust the border so it is exactly where we want it.
-               padding_effect:set_float("border_offset_left", x_subpixel_offset)
-               padding_effect:set_float("border_offset_right", x1 - (math.floor(x0) + width))
-               padding_effect:set_float("border_offset_top", y_subpixel_offset)
-               padding_effect:set_float("border_offset_bottom", y1 - (math.floor(y0) + height))
+               input.padding_effect:set_float("border_offset_left", x_subpixel_offset)
+               input.padding_effect:set_float("border_offset_right", x1 - (math.floor(x0) + width))
+               input.padding_effect:set_float("border_offset_top", y_subpixel_offset)
+               input.padding_effect:set_float("border_offset_bottom", y1 - (math.floor(y0) + height))
        else
                -- Lower-quality simple resizing.
+               input.resample_switcher:choose_alternative(1)
+
                local width = round(x1 - x0)
                local height = round(y1 - y0)
-               resize_effect:set_int("width", width)
-               resize_effect:set_int("height", height)
+               input.resize_effect:set_int("width", width)
+               input.resize_effect:set_int("height", height)
 
                -- Padding must also be to a whole-pixel offset.
-               padding_effect:set_int("left", math.floor(x0))
-               padding_effect:set_int("top", math.floor(y0))
+               input.padding_effect:set_int("left", math.floor(x0))
+               input.padding_effect:set_int("top", math.floor(y0))
+
+               -- No subpixel stuff.
+               input.padding_effect:set_float("border_offset_left", 0.0)
+               input.padding_effect:set_float("border_offset_right", 0.0)
+               input.padding_effect:set_float("border_offset_top", 0.0)
+               input.padding_effect:set_float("border_offset_bottom", 0.0)
        end
 end
 
@@ -782,11 +626,9 @@ function pos_from_top_left(x, y, width, height, screen_width, screen_height)
        }
 end
 
-function prepare_sbs_chain(state, chain, t, transition_type, src_signal, dst_signal, screen_width, screen_height, input_resolution)
-       chain.input0.input:connect_signal(0)
-       chain.input1.input:connect_signal(1)
-       set_neutral_color(chain.input0.wb_effect, state.neutral_colors[1])
-       set_neutral_color(chain.input1.wb_effect, state.neutral_colors[2])
+function prepare_sbs_scene(state, t, transition_type, src_signal, dst_signal, screen_width, screen_height, input_resolution, hq)
+       set_neutral_color(sbs_scene.input0.wb_effect, state.neutral_colors[1])
+       set_neutral_color(sbs_scene.input1.wb_effect, state.neutral_colors[2])
 
        -- First input is positioned (16,48) from top-left.
        -- Second input is positioned (16,48) from the bottom-right.
@@ -819,8 +661,8 @@ function prepare_sbs_chain(state, chain, t, transition_type, src_signal, dst_sig
        end
 
        -- NOTE: input_resolution is not 1-indexed, unlike usual Lua arrays.
-       place_rectangle_with_affine(chain.input0.resample_effect, chain.input0.resize_effect, chain.input0.padding_effect, pos0, affine_param, screen_width, screen_height, input_resolution[0].width, input_resolution[0].height)
-       place_rectangle_with_affine(chain.input1.resample_effect, chain.input1.resize_effect, chain.input1.padding_effect, pos1, affine_param, screen_width, screen_height, input_resolution[1].width, input_resolution[1].height)
+       place_rectangle_with_affine(sbs_scene.input0, pos0, affine_param, screen_width, screen_height, input_resolution[0].width, input_resolution[0].height, hq)
+       place_rectangle_with_affine(sbs_scene.input1, pos1, affine_param, screen_width, screen_height, input_resolution[1].width, input_resolution[1].height, hq)
 end
 
 -- Find the transformation that changes the first rectangle to the second one.
@@ -835,13 +677,13 @@ function find_affine_param(a, b)
        }
 end
 
-function place_rectangle_with_affine(resample_effect, resize_effect, padding_effect, pos, aff, screen_width, screen_height, input_width, input_height)
+function place_rectangle_with_affine(input, pos, aff, screen_width, screen_height, input_width, input_height, hq)
        local x0 = pos.x0 * aff.sx + aff.tx
        local x1 = pos.x1 * aff.sx + aff.tx
        local y0 = pos.y0 * aff.sy + aff.ty
        local y1 = pos.y1 * aff.sy + aff.ty
 
-       place_rectangle(resample_effect, resize_effect, padding_effect, x0, y0, x1, y1, screen_width, screen_height, input_width, input_height)
+       place_rectangle(input, x0, y0, x1, y1, screen_width, screen_height, input_width, input_height, hq)
 end
 
 function set_neutral_color(effect, color)