]> git.sesse.net Git - nageru/blob - theme.lua
Make make_sbs_chain() take type instead of a yes/no deinterlaced flag, for more flexi...
[nageru] / theme.lua
1 -- The theme is what decides what's actually shown on screen, what kind of
2 -- transitions are available (if any), and what kind of inputs there are,
3 -- if any. In general, it drives the entire display logic by creating Movit
4 -- chains, setting their parameters and then deciding which to show when.
5 --
6 -- Themes are written in Lua, which reflects a simplified form of the Movit API
7 -- where all the low-level details (such as texture formats) are handled by the
8 -- C++ side and you generally just build chains.
9
10 local transition_start = -2.0
11 local transition_end = -1.0
12 local zoom_src = 0.0
13 local zoom_dst = 1.0
14 local zoom_poi = 0   -- which input to zoom in on
15 local fade_src_signal = 0
16 local fade_dst_signal = 0
17
18 local neutral_colors = {
19         {0.5, 0.5, 0.5},  -- Input 0.
20         {0.5, 0.5, 0.5}   -- Input 1.
21 }
22
23 local live_signal_num = 0
24 local preview_signal_num = 1
25
26 -- Valid values for live_signal_num and preview_signal_num.
27 local INPUT0_SIGNAL_NUM = 0
28 local INPUT1_SIGNAL_NUM = 1
29 local SBS_SIGNAL_NUM = 2
30 local STATIC_SIGNAL_NUM = 3
31
32 -- “fake” signal number that signifies that we are fading from one input
33 -- to the next.
34 local FADE_SIGNAL_NUM = 4
35
36 -- Last width/height/frame rate for each channel, if we have it.
37 -- Note that unlike the values we get from Nageru, the resolution is per
38 -- frame and not per field, since we deinterlace.
39 local last_resolution = {}
40
41 -- Utility function to help creating many similar chains that can differ
42 -- in a free set of chosen parameters.
43 function make_cartesian_product(parms, callback)
44         return make_cartesian_product_internal(parms, callback, 1, {})
45 end
46
47 function make_cartesian_product_internal(parms, callback, index, args)
48         if index > #parms then
49                 return callback(unpack(args))
50         end
51         local ret = {}
52         for _, value in ipairs(parms[index]) do
53                 args[index] = value
54                 ret[value] = make_cartesian_product_internal(parms, callback, index + 1, args)
55         end
56         return ret
57 end
58
59 function make_sbs_input(chain, signal, deint, hq)
60         local input = chain:add_live_input(not deint, deint)  -- Override bounce only if not deinterlacing.
61         input:connect_signal(signal)
62
63         local resample_effect = nil
64         local resize_effect = nil
65         if (hq) then
66                 resample_effect = chain:add_effect(ResampleEffect.new())
67         else
68                 resize_effect = chain:add_effect(ResizeEffect.new())
69         end
70         local wb_effect = chain:add_effect(WhiteBalanceEffect.new())
71
72         local padding_effect = chain:add_effect(IntegralPaddingEffect.new())
73
74         return {
75                 input = input,
76                 wb_effect = wb_effect,
77                 resample_effect = resample_effect,
78                 resize_effect = resize_effect,
79                 padding_effect = padding_effect
80         }
81 end
82
83 -- The main live chain.
84 function make_sbs_chain(input0_type, input1_type, hq)
85         local chain = EffectChain.new(16, 9)
86
87         local input0 = make_sbs_input(chain, INPUT0_SIGNAL_NUM, input0_type == "livedeint", hq)
88         local input1 = make_sbs_input(chain, INPUT1_SIGNAL_NUM, input1_type == "livedeint", hq)
89
90         input0.padding_effect:set_vec4("border_color", 0.0, 0.0, 0.0, 1.0)
91         input1.padding_effect:set_vec4("border_color", 0.0, 0.0, 0.0, 0.0)
92
93         chain:add_effect(OverlayEffect.new(), input0.padding_effect, input1.padding_effect)
94         chain:finalize(hq)
95
96         return {
97                 chain = chain,
98                 input0 = input0,
99                 input1 = input1
100         }
101 end
102
103 -- Make all possible combinations of side-by-side chains.
104 local sbs_chains = make_cartesian_product({
105         {"live", "livedeint"},  -- input0_type
106         {"live", "livedeint"},  -- input1_type
107         {true, false}           -- hq
108 }, function(input0_type, input1_type, hq)
109         return make_sbs_chain(input0_type, input1_type, hq)
110 end)
111
112 function make_fade_input(chain, signal, live, deint, scale)
113         local input, wb_effect, resample_effect, last
114         if live then
115                 input = chain:add_live_input(false, deint)
116                 input:connect_signal(signal)
117                 last = input
118         else
119                 input = chain:add_effect(ImageInput.new("bg.jpeg"))
120                 last = input
121         end
122
123         -- If we cared about this for the non-main inputs, we would have
124         -- checked hq here and invoked ResizeEffect instead.
125         if scale then
126                 resample_effect = chain:add_effect(ResampleEffect.new())
127                 last = resample_effect
128         end
129
130         -- Make sure to put the white balance after the scaling (usually more efficient).
131         if live then
132                 wb_effect = chain:add_effect(WhiteBalanceEffect.new())
133                 last = wb_effect
134         end
135
136         return {
137                 input = input,
138                 wb_effect = wb_effect,
139                 resample_effect = resample_effect,
140                 last = last
141         }
142 end
143
144 -- A chain to fade between two inputs, of which either can be a picture
145 -- or a live input. In practice only used live, but we still support the
146 -- hq parameter.
147 function make_fade_chain(input0_live, input0_deint, input0_scale, input1_live, input1_deint, input1_scale, hq)
148         local chain = EffectChain.new(16, 9)
149
150         local input0 = make_fade_input(chain, INPUT0_SIGNAL_NUM, input0_live, input0_deint, input0_scale)
151         local input1 = make_fade_input(chain, INPUT1_SIGNAL_NUM, input1_live, input1_deint, input1_scale)
152
153         local mix_effect = chain:add_effect(MixEffect.new(), input0.last, input1.last)
154         chain:finalize(hq)
155
156         return {
157                 chain = chain,
158                 input0 = input0,
159                 input1 = input1,
160                 mix_effect = mix_effect
161         }
162 end
163
164 -- Chains to fade between two inputs, in various configurations.
165 local fade_chains = make_cartesian_product({
166         {"static", "live", "livedeint"},  -- input0_type
167         {true, false},                    -- input0_scale
168         {"static", "live", "livedeint"},  -- input1_type
169         {true, false},                    -- input1_scale
170         {true}                            -- hq
171 }, function(input0_type, input0_scale, input1_type, input1_scale, hq)
172         local input0_live = (input0_type ~= "static")
173         local input1_live = (input1_type ~= "static")
174         local input0_deint = (input0_type == "livedeint")
175         local input1_deint = (input1_type == "livedeint")
176         return make_fade_chain(input0_live, input0_deint, input0_scale, input1_live, input1_deint, input1_scale, hq)
177 end)
178
179 -- A chain to show a single input on screen.
180 function make_simple_chain(input_deint, input_scale, hq)
181         local chain = EffectChain.new(16, 9)
182
183         local input = chain:add_live_input(false, input_deint)
184         input:connect_signal(0)  -- First input card. Can be changed whenever you want.
185
186         local resample_effect, resize_effect
187         if scale then
188                 if hq then
189                         resample_effect = chain:add_effect(ResampleEffect.new())
190                 else
191                         resize_effect = chain:add_effect(ResizeEffect.new())
192                 end
193         end
194
195         local wb_effect = chain:add_effect(WhiteBalanceEffect.new())
196         chain:finalize(hq)
197
198         return {
199                 chain = chain,
200                 input = input,
201                 wb_effect = wb_effect,
202                 resample_effect = resample_effect,
203                 resize_effect = resize_effect
204         }
205 end
206
207 -- Make all possible combinations of single-input chains.
208 local simple_chains = make_cartesian_product({
209         {"live", "livedeint"},  -- input_type
210         {true, false},          -- input_scale
211         {true, false}           -- hq
212 }, function(input_type, input_scale, hq)
213         local input_deint = (input_type == "livedeint")
214         return make_simple_chain(input_deint, input_scale, hq)
215 end)
216
217 -- A chain to show a single static picture on screen (HQ version).
218 local static_chain_hq = EffectChain.new(16, 9)
219 local static_chain_hq_input = static_chain_hq:add_effect(ImageInput.new("bg.jpeg"))
220 static_chain_hq:finalize(true)
221
222 -- A chain to show a single static picture on screen (LQ version).
223 local static_chain_lq = EffectChain.new(16, 9)
224 local static_chain_lq_input = static_chain_lq:add_effect(ImageInput.new("bg.jpeg"))
225 static_chain_lq:finalize(false)
226
227 -- Used for indexing into the tables of chains.
228 function get_input_type(signals, signal_num)
229         if signal_num == STATIC_SIGNAL_NUM then
230                 return "static"
231         elseif signals:get_interlaced(signal_num) then
232                 return "livedeint"
233         else
234                 return "live"
235         end
236 end
237
238 function needs_scale(signals, signal_num, width, height)
239         if signal_num == STATIC_SIGNAL_NUM then
240                 -- We assume this is already correctly scaled at load time.
241                 return false
242         end
243         assert(is_plain_signal(signal_num))
244         return (signals:get_width(signal_num) ~= width or signals:get_height(signal_num) ~= height)
245 end
246
247 function set_scale_parameters_if_needed(chain_or_input, width, height)
248         if chain_or_input.resample_effect then
249                 chain_or_input.resample_effect:set_int("width", width)
250                 chain_or_input.resample_effect:set_int("height", height)
251         elseif chain_or_input.resize_effect then
252                 chain_or_input.resize_effect:set_int("width", width)
253                 chain_or_input.resize_effect:set_int("height", height)
254         end
255 end
256
257 -- API ENTRY POINT
258 -- Returns the number of outputs in addition to the live (0) and preview (1).
259 -- Called only once, at the start of the program.
260 function num_channels()
261         return 4
262 end
263
264 function is_plain_signal(num)
265         return num == INPUT0_SIGNAL_NUM or num == INPUT1_SIGNAL_NUM
266 end
267
268 -- Helper function to write e.g. “720p60”. The difference between this
269 -- and get_channel_resolution_raw() is that this one also can say that
270 -- there's no signal.
271 function get_channel_resolution(signal_num)
272         res = last_resolution[signal_num]
273         if (not res) or res.height <= 0 then
274                 return "no signal"
275         end
276         if not res.has_signal then
277                 if res.height == 525 then
278                         -- Special mode for the USB3 cards.
279                         return "no signal"
280                 end
281                 return get_channel_resolution_raw(res) .. ", no signal"
282         else
283                 return get_channel_resolution_raw(res)
284         end
285 end
286
287 -- Helper function to write e.g. “60” or “59.94”.
288 function get_frame_rate(res)
289         local nom = res.frame_rate_nom
290         local den = res.frame_rate_den
291         if nom % den == 0 then
292                 return nom / den
293         else
294                 return string.format("%.2f", nom / den)
295         end
296 end
297
298 -- Helper function to write e.g. “720p60”.
299 function get_channel_resolution_raw(res)
300         if res.interlaced then
301                 return res.height .. "i" .. get_frame_rate(res)
302         else
303                 return res.height .. "p" .. get_frame_rate(res)
304         end
305 end
306
307 -- API ENTRY POINT
308 -- Returns the name for each additional channel (starting from 2).
309 -- Called at the start of the program, and then each frame for live
310 -- channels in case they change resolution.
311 function channel_name(channel)
312         local signal_num = channel - 2
313         if is_plain_signal(signal_num) then
314                 return "Input " .. (signal_num + 1) .. " (" .. get_channel_resolution(signal_num) .. ")"
315         elseif signal_num == SBS_SIGNAL_NUM then
316                 return "Side-by-side"
317         elseif signal_num == STATIC_SIGNAL_NUM then
318                 return "Static picture"
319         end
320 end
321
322 -- API ENTRY POINT
323 -- Returns, given a channel number, which signal it corresponds to (starting from 0).
324 -- Should return -1 if the channel does not correspond to a simple signal.
325 -- (The information is used for whether right-click on the channel should bring up
326 -- an input selector or not.)
327 -- Called once for each channel, at the start of the program.
328 -- Will never be called for live (0) or preview (1).
329 function channel_signal(channel)
330         if channel == 2 then
331                 return 0
332         elseif channel == 3 then
333                 return 1
334         else
335                 return -1
336         end
337 end
338
339 -- API ENTRY POINT
340 -- Called every frame. Returns the color (if any) to paint around the given
341 -- channel. Returns a CSS color (typically to mark live and preview signals);
342 -- "transparent" is allowed.
343 -- Will never be called for live (0) or preview (1).
344 function channel_color(channel)
345         if channel_involved_in(channel, live_signal_num) then
346                 return "#f00"
347         end
348         if channel_involved_in(channel, preview_signal_num) then
349                 return "#0f0"
350         end
351         return "transparent"
352 end
353
354 function channel_involved_in(channel, signal_num)
355         if is_plain_signal(signal_num) then
356                 return channel == (signal_num + 2)
357         end
358         if signal_num == SBS_SIGNAL_NUM then
359                 return (channel == 2 or channel == 3)
360         end
361         if signal_num == STATIC_SIGNAL_NUM then
362                 return (channel == 5)
363         end
364         if signal_num == FADE_SIGNAL_NUM then
365                 return (channel_involved_in(channel, fade_src_signal) or
366                         channel_involved_in(channel, fade_dst_signal))
367         end
368         return false
369 end
370
371 -- API ENTRY POINT
372 -- Returns if a given channel supports setting white balance (starting from 2).
373 -- Called only once for each channel, at the start of the program.
374 function supports_set_wb(channel)
375         return is_plain_signal(channel - 2)
376 end
377
378 -- API ENTRY POINT
379 -- Gets called with a new gray point when the white balance is changing.
380 -- The color is in linear light (not sRGB gamma).
381 function set_wb(channel, red, green, blue)
382         if is_plain_signal(channel - 2) then
383                 neutral_colors[channel - 2 + 1] = { red, green, blue }
384         end
385 end
386
387 function finish_transitions(t)
388         -- If live is SBS but de-facto single, make it so.
389         if live_signal_num == SBS_SIGNAL_NUM and t >= transition_end and zoom_dst == 1.0 then
390                 live_signal_num = zoom_poi
391         end
392
393         -- If live is fade but de-facto single, make it so.
394         if live_signal_num == FADE_SIGNAL_NUM and t >= transition_end then
395                 live_signal_num = fade_dst_signal
396         end
397 end
398
399 -- API ENTRY POINT
400 -- Called every frame.
401 function get_transitions(t)
402         finish_transitions(t)
403
404         if live_signal_num == preview_signal_num then
405                 -- No transitions possible.
406                 return {}
407         end
408
409         if live_signal_num == SBS_SIGNAL_NUM and t >= transition_start and t <= transition_end then
410                 -- Zoom in progress.
411                 return {"Cut"}
412         end
413
414         if (is_plain_signal(live_signal_num) or live_signal_num == STATIC_SIGNAL_NUM) and
415            (is_plain_signal(preview_signal_num) or preview_signal_num == STATIC_SIGNAL_NUM) then
416                 return {"Cut", "", "Fade"}
417         end
418
419         -- Various zooms.
420         if live_signal_num == SBS_SIGNAL_NUM and is_plain_signal(preview_signal_num) then
421                 return {"Cut", "Zoom in"}
422         elseif is_plain_signal(live_signal_num) and preview_signal_num == SBS_SIGNAL_NUM then
423                 return {"Cut", "Zoom out"}
424         end
425
426         return {"Cut"}
427 end
428
429 -- API ENTRY POINT
430 -- Called when the user clicks a transition button.
431 function transition_clicked(num, t)
432         if num == 0 then
433                 -- Cut.
434                 if live_signal_num == FADE_SIGNAL_NUM then
435                         -- Ongoing fade; finish it immediately.
436                         finish_transitions(transition_end)
437                 end
438
439                 local temp = live_signal_num
440                 live_signal_num = preview_signal_num
441                 preview_signal_num = temp
442
443                 if live_signal_num == SBS_SIGNAL_NUM then
444                         -- Just cut to SBS, we need to reset any zooms.
445                         zoom_src = 1.0
446                         zoom_dst = 0.0
447                         transition_start = -2.0
448                         transition_end = -1.0
449                 end
450         elseif num == 1 then
451                 -- Zoom.
452
453                 finish_transitions(t)
454
455                 if live_signal_num == preview_signal_num then
456                         -- Nothing to do.
457                         return
458                 end
459
460                 if (is_plain_signal(live_signal_num) and is_plain_signal(preview_signal_num)) then
461                         -- We can't zoom between these. Just make a cut.
462                         io.write("Cutting from " .. live_signal_num .. " to " .. live_signal_num .. "\n")
463                         local temp = live_signal_num
464                         live_signal_num = preview_signal_num
465                         preview_signal_num = temp
466                         return
467                 end
468
469                 if live_signal_num == SBS_SIGNAL_NUM and is_plain_signal(preview_signal_num) then
470                         -- Zoom in from SBS to single.
471                         transition_start = t
472                         transition_end = t + 1.0
473                         zoom_src = 0.0
474                         zoom_dst = 1.0
475                         zoom_poi = preview_signal_num
476                         preview_signal_num = SBS_SIGNAL_NUM
477                 elseif is_plain_signal(live_signal_num) and preview_signal_num == SBS_SIGNAL_NUM then
478                         -- Zoom out from single to SBS.
479                         transition_start = t
480                         transition_end = t + 1.0
481                         zoom_src = 1.0
482                         zoom_dst = 0.0
483                         preview_signal_num = live_signal_num
484                         zoom_poi = live_signal_num
485                         live_signal_num = SBS_SIGNAL_NUM
486                 end
487         elseif num == 2 then
488                 finish_transitions(t)
489
490                 -- Fade.
491                 if (live_signal_num ~= preview_signal_num) and
492                    (is_plain_signal(live_signal_num) or
493                     live_signal_num == STATIC_SIGNAL_NUM) and
494                    (is_plain_signal(preview_signal_num) or
495                     preview_signal_num == STATIC_SIGNAL_NUM) then
496                         transition_start = t
497                         transition_end = t + 1.0
498                         fade_src_signal = live_signal_num
499                         fade_dst_signal = preview_signal_num
500                         preview_signal_num = live_signal_num
501                         live_signal_num = FADE_SIGNAL_NUM
502                 else
503                         -- Fades involving SBS are ignored (we have no chain for it).
504                 end
505         end
506 end
507
508 -- API ENTRY POINT
509 function channel_clicked(num)
510         preview_signal_num = num
511 end
512
513 -- API ENTRY POINT
514 -- Called every frame. Get the chain for displaying at input <num>,
515 -- where 0 is live, 1 is preview, 2 is the first channel to display
516 -- in the bottom bar, and so on up to num_channels()+1. t is the
517 -- current time in seconds. width and height are the dimensions of
518 -- the output, although you can ignore them if you don't need them
519 -- (they're useful if you want to e.g. know what to resample by).
520 --
521 -- <signals> is basically an exposed InputState, which you can use to
522 -- query for information about the signals at the point of the current
523 -- frame. In particular, you can call get_width() and get_height()
524 -- for any signal number, and use that to e.g. assist in chain selection.
525 --
526 -- You should return two objects; the chain itself, and then a
527 -- function (taking no parameters) that is run just before rendering.
528 -- The function needs to call connect_signal on any inputs, so that
529 -- it gets updated video data for the given frame. (You are allowed
530 -- to switch which input your input is getting from between frames,
531 -- but not calling connect_signal results in undefined behavior.)
532 -- If you want to change any parameters in the chain, this is also
533 -- the right place.
534 --
535 -- NOTE: The chain returned must be finalized with the Y'CbCr flag
536 -- if and only if num==0.
537 function get_chain(num, t, width, height, signals)
538         local input_resolution = {}
539         for signal_num=0,1 do
540                 local res = {
541                         width = signals:get_width(signal_num),
542                         height = signals:get_height(signal_num),
543                         interlaced = signals:get_interlaced(signal_num),
544                         has_signal = signals:get_has_signal(signal_num),
545                         frame_rate_nom = signals:get_frame_rate_nom(signal_num),
546                         frame_rate_den = signals:get_frame_rate_den(signal_num)
547                 }
548
549                 if res.interlaced then
550                         -- Convert height from frame height to field height.
551                         -- (Needed for e.g. place_rectangle.)
552                         res.height = res.height * 2
553
554                         -- Show field rate instead of frame rate; really for cosmetics only
555                         -- (and actually contrary to EBU recommendations, although in line
556                         -- with typical user expectations).
557                         res.frame_rate_nom = res.frame_rate_nom * 2
558                 end
559
560                 input_resolution[signal_num] = res
561         end
562         last_resolution = input_resolution
563
564         if num == 0 then  -- Live.
565                 if is_plain_signal(live_signal_num) then  -- Plain inputs.
566                         local input_type = get_input_type(signals, live_signal_num)
567                         local input_scale = needs_scale(signals, live_signal_num, width, height)
568                         local chain = simple_chains[input_type][input_scale][true]
569                         prepare = function()
570                                 chain.input:connect_signal(live_signal_num)
571                                 set_scale_parameters_if_needed(chain, width, height)
572                                 set_neutral_color_from_signal(chain.wb_effect, live_signal_num)
573                         end
574                         return chain.chain, prepare
575                 elseif live_signal_num == STATIC_SIGNAL_NUM then  -- Static picture.
576                         prepare = function()
577                         end
578                         return static_chain_hq, prepare
579                 elseif live_signal_num == FADE_SIGNAL_NUM then  -- Fade.
580                         local input0_type = get_input_type(signals, fade_src_signal)
581                         local input0_scale = needs_scale(signals, fade_src_signal, width, height)
582                         local input1_type = get_input_type(signals, fade_dst_signal)
583                         local input1_scale = needs_scale(signals, fade_dst_signal, width, height)
584                         local chain = fade_chains[input0_type][input0_scale][input1_type][input1_scale][true]
585                         prepare = function()
586                                 if input0_type == "live" or input0_type == "livedeint" then
587                                         chain.input0.input:connect_signal(fade_src_signal)
588                                         set_neutral_color_from_signal(chain.input0.wb_effect, fade_src_signal)
589                                 end
590                                 set_scale_parameters_if_needed(chain.input0, width, height)
591                                 if input1_type == "live" or input1_type == "livedeint" then
592                                         chain.input1.input:connect_signal(fade_dst_signal)
593                                         set_neutral_color_from_signal(chain.input1.wb_effect, fade_dst_signal)
594                                 end
595                                 set_scale_parameters_if_needed(chain.input1, width, height)
596                                 local tt = calc_fade_progress(t, transition_start, transition_end)
597
598                                 chain.mix_effect:set_float("strength_first", 1.0 - tt)
599                                 chain.mix_effect:set_float("strength_second", tt)
600                         end
601                         return chain.chain, prepare
602                 end
603
604                 -- SBS code (live_signal_num == SBS_SIGNAL_NUM).
605                 local input0_type = get_input_type(signals, INPUT0_SIGNAL_NUM)
606                 local input1_type = get_input_type(signals, INPUT1_SIGNAL_NUM)
607                 if t > transition_end and zoom_dst == 1.0 then
608                         -- Special case: Show only the single image on screen.
609                         local input0_scale = needs_scale(signals, fade_src_signal, width, height)
610                         local chain = simple_chains[input0_type][input0_scale][true]
611                         prepare = function()
612                                 chain.input:connect_signal(INPUT0_SIGNAL_NUM)
613                                 set_scale_parameters_if_needed(chain, width, height)
614                                 set_neutral_color(chain.wb_effect, neutral_colors[1])
615                         end
616                         return chain.chain, prepare
617                 end
618                 local chain = sbs_chains[input0_type][input1_type][true]
619                 prepare = function()
620                         if t < transition_start then
621                                 prepare_sbs_chain(chain, zoom_src, width, height, input_resolution)
622                         elseif t > transition_end then
623                                 prepare_sbs_chain(chain, zoom_dst, width, height, input_resolution)
624                         else
625                                 local tt = (t - transition_start) / (transition_end - transition_start)
626                                 -- Smooth it a bit.
627                                 tt = math.sin(tt * 3.14159265358 * 0.5)
628                                 prepare_sbs_chain(chain, zoom_src + (zoom_dst - zoom_src) * tt, width, height, input_resolution)
629                         end
630                 end
631                 return chain.chain, prepare
632         end
633         if num == 1 then  -- Preview.
634                 num = preview_signal_num + 2
635         end
636
637         -- Individual preview inputs.
638         if is_plain_signal(num - 2) then
639                 local signal_num = num - 2
640                 local input_type = get_input_type(signals, signal_num)
641                 local input_scale = needs_scale(signals, signal_num, width, height)
642                 local chain = simple_chains[input_type][input_scale][false]
643                 prepare = function()
644                         chain.input:connect_signal(signal_num)
645                         set_scale_parameters_if_needed(chain, width, height)
646                         set_neutral_color(chain.wb_effect, neutral_colors[signal_num + 1])
647                 end
648                 return chain.chain, prepare
649         end
650         if num == SBS_SIGNAL_NUM + 2 then
651                 local input0_type = get_input_type(signals, INPUT0_SIGNAL_NUM)
652                 local input1_type = get_input_type(signals, INPUT1_SIGNAL_NUM)
653                 local chain = sbs_chains[input0_type][input1_type][false]
654                 prepare = function()
655                         prepare_sbs_chain(chain, 0.0, width, height, input_resolution)
656                 end
657                 return chain.chain, prepare
658         end
659         if num == STATIC_SIGNAL_NUM + 2 then
660                 prepare = function()
661                 end
662                 return static_chain_lq, prepare
663         end
664 end
665
666 function place_rectangle(resample_effect, resize_effect, padding_effect, x0, y0, x1, y1, screen_width, screen_height, input_width, input_height)
667         local srcx0 = 0.0
668         local srcx1 = 1.0
669         local srcy0 = 0.0
670         local srcy1 = 1.0
671
672         padding_effect:set_int("width", screen_width)
673         padding_effect:set_int("height", screen_height)
674
675         -- Cull.
676         if x0 > screen_width or x1 < 0.0 or y0 > screen_height or y1 < 0.0 then
677                 if resample_effect ~= nil then
678                         resample_effect:set_int("width", 1)
679                         resample_effect:set_int("height", 1)
680                         resample_effect:set_float("zoom_x", screen_width)
681                         resample_effect:set_float("zoom_y", screen_height)
682                 else
683                         resize_effect:set_int("width", 1)
684                         resize_effect:set_int("height", 1)
685                 end
686                 padding_effect:set_int("left", screen_width + 100)
687                 padding_effect:set_int("top", screen_height + 100)
688                 return
689         end
690
691         -- Clip.
692         if x0 < 0 then
693                 srcx0 = -x0 / (x1 - x0)
694                 x0 = 0
695         end
696         if y0 < 0 then
697                 srcy0 = -y0 / (y1 - y0)
698                 y0 = 0
699         end
700         if x1 > screen_width then
701                 srcx1 = (screen_width - x0) / (x1 - x0)
702                 x1 = screen_width
703         end
704         if y1 > screen_height then
705                 srcy1 = (screen_height - y0) / (y1 - y0)
706                 y1 = screen_height
707         end
708
709         if resample_effect ~= nil then
710                 -- High-quality resampling.
711                 local x_subpixel_offset = x0 - math.floor(x0)
712                 local y_subpixel_offset = y0 - math.floor(y0)
713
714                 -- Resampling must be to an integral number of pixels. Round up,
715                 -- and then add an extra pixel so we have some leeway for the border.
716                 local width = math.ceil(x1 - x0) + 1
717                 local height = math.ceil(y1 - y0) + 1
718                 resample_effect:set_int("width", width)
719                 resample_effect:set_int("height", height)
720
721                 -- Correct the discrepancy with zoom. (This will leave a small
722                 -- excess edge of pixels and subpixels, which we'll correct for soon.)
723                 local zoom_x = (x1 - x0) / (width * (srcx1 - srcx0))
724                 local zoom_y = (y1 - y0) / (height * (srcy1 - srcy0))
725                 resample_effect:set_float("zoom_x", zoom_x)
726                 resample_effect:set_float("zoom_y", zoom_y)
727                 resample_effect:set_float("zoom_center_x", 0.0)
728                 resample_effect:set_float("zoom_center_y", 0.0)
729
730                 -- Padding must also be to a whole-pixel offset.
731                 padding_effect:set_int("left", math.floor(x0))
732                 padding_effect:set_int("top", math.floor(y0))
733
734                 -- Correct _that_ discrepancy by subpixel offset in the resampling.
735                 resample_effect:set_float("left", srcx0 * input_width - x_subpixel_offset / zoom_x)
736                 resample_effect:set_float("top", srcy0 * input_height - y_subpixel_offset / zoom_y)
737
738                 -- Finally, adjust the border so it is exactly where we want it.
739                 padding_effect:set_float("border_offset_left", x_subpixel_offset)
740                 padding_effect:set_float("border_offset_right", x1 - (math.floor(x0) + width))
741                 padding_effect:set_float("border_offset_top", y_subpixel_offset)
742                 padding_effect:set_float("border_offset_bottom", y1 - (math.floor(y0) + height))
743         else
744                 -- Lower-quality simple resizing.
745                 local width = round(x1 - x0)
746                 local height = round(y1 - y0)
747                 resize_effect:set_int("width", width)
748                 resize_effect:set_int("height", height)
749
750                 -- Padding must also be to a whole-pixel offset.
751                 padding_effect:set_int("left", math.floor(x0))
752                 padding_effect:set_int("top", math.floor(y0))
753         end
754 end
755
756 -- This is broken, of course (even for positive numbers), but Lua doesn't give us access to real rounding.
757 function round(x)
758         return math.floor(x + 0.5)
759 end
760
761 function lerp(a, b, t)
762         return a + (b - a) * t
763 end
764
765 function lerp_pos(a, b, t)
766         return {
767                 x0 = lerp(a.x0, b.x0, t),
768                 y0 = lerp(a.y0, b.y0, t),
769                 x1 = lerp(a.x1, b.x1, t),
770                 y1 = lerp(a.y1, b.y1, t)
771         }
772 end
773
774 function pos_from_top_left(x, y, width, height, screen_width, screen_height)
775         local xs = screen_width / 1280.0
776         local ys = screen_height / 720.0
777         return {
778                 x0 = round(xs * x),
779                 y0 = round(ys * y),
780                 x1 = round(xs * (x + width)),
781                 y1 = round(ys * (y + height))
782         }
783 end
784
785 function prepare_sbs_chain(chain, t, screen_width, screen_height, input_resolution)
786         chain.input0.input:connect_signal(0)
787         chain.input1.input:connect_signal(1)
788         set_neutral_color(chain.input0.wb_effect, neutral_colors[1])
789         set_neutral_color(chain.input1.wb_effect, neutral_colors[2])
790
791         -- First input is positioned (16,48) from top-left.
792         -- Second input is positioned (16,48) from the bottom-right.
793         local pos0 = pos_from_top_left(16, 48, 848, 477, screen_width, screen_height)
794         local pos1 = pos_from_top_left(1280 - 384 - 16, 720 - 216 - 48, 384, 216, screen_width, screen_height)
795
796         local pos_fs = { x0 = 0, y0 = 0, x1 = screen_width, y1 = screen_height }
797         local affine_param
798         if zoom_poi == INPUT0_SIGNAL_NUM then
799                 affine_param = find_affine_param(pos0, lerp_pos(pos0, pos_fs, t))
800         else
801                 affine_param = find_affine_param(pos1, lerp_pos(pos1, pos_fs, t))
802         end
803
804         -- NOTE: input_resolution is not 1-indexed, unlike usual Lua arrays.
805         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)
806         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)
807 end
808
809 -- Find the transformation that changes the first rectangle to the second one.
810 function find_affine_param(a, b)
811         local sx = (b.x1 - b.x0) / (a.x1 - a.x0)
812         local sy = (b.y1 - b.y0) / (a.y1 - a.y0)
813         return {
814                 sx = sx,
815                 sy = sy,
816                 tx = b.x0 - a.x0 * sx,
817                 ty = b.y0 - a.y0 * sy
818         }
819 end
820
821 function place_rectangle_with_affine(resample_effect, resize_effect, padding_effect, pos, aff, screen_width, screen_height, input_width, input_height)
822         local x0 = pos.x0 * aff.sx + aff.tx
823         local x1 = pos.x1 * aff.sx + aff.tx
824         local y0 = pos.y0 * aff.sy + aff.ty
825         local y1 = pos.y1 * aff.sy + aff.ty
826
827         place_rectangle(resample_effect, resize_effect, padding_effect, x0, y0, x1, y1, screen_width, screen_height, input_width, input_height)
828 end
829
830 function set_neutral_color(effect, color)
831         effect:set_vec3("neutral_color", color[1], color[2], color[3])
832 end
833
834 function set_neutral_color_from_signal(effect, signal)
835         if is_plain_signal(signal) then
836                 set_neutral_color(effect, neutral_colors[signal - INPUT0_SIGNAL_NUM + 1])
837         end
838 end
839
840 function calc_fade_progress(t, transition_start, transition_end)
841         local tt = (t - transition_start) / (transition_end - transition_start)
842         if tt < 0.0 then
843                 return 0.0
844         elseif tt > 1.0 then
845                 return 1.0
846         end
847
848         -- Make the fade look maybe a tad more natural, by pumping it
849         -- through a sigmoid function.
850         tt = 10.0 * tt - 5.0
851         tt = 1.0 / (1.0 + math.exp(-tt))
852
853         return tt
854 end