Repository navigation
Expand file tree
/
Copy pathcompositor.c
More file actions
3078 lines (2760 loc) · 126 KB
/
Copy pathcompositor.c
File metadata and controls
3078 lines (2760 loc) · 126 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include "utils.h"
#include <float.h>
#include <libplacebo/compositor.h>
#include <libplacebo/dummy.h>
#include <libplacebo/vulkan.h>
// Tests for pl_compositor: input placement (fit modes, rects, rotation, focus),
// rendering (canvas params, equivalence with the renderer, status reporting),
// compositing order, color overrides, queue management, and the scheduling
// semantics of pl_input_update() on a synthetic output vsync grid.
//
// Reference models are written from <libplacebo/compositor.h> rather than
// mirrored from the implementation, so that the two can disagree and say which
// one is wrong. Set PL_TEST_VERBOSE=1 for trace logging.
#define MAX_SRC 256
#define MAX_OUT 192
#define MAX_INPUTS 3
#define EPS 1e-9
#define FAIL(...) \
do { \
fprintf(stderr, "=== FAILED: "); \
fprintf(stderr, __VA_ARGS__); \
exit(1); \
} while (0)
//----------------------------------------------------------------------------
// Reference model
//----------------------------------------------------------------------------
// Round `pts` to a boundary on the sorted grid `grid`. A PTS already lying on
// a boundary is never adjusted; values outside the grid clamp to its ends.
static double round_grid(double pts, const double *grid, int n,
enum pl_pts_round_mode mode)
{
pl_assert(n > 0);
if (pts <= grid[0] + EPS)
return grid[0];
if (pts >= grid[n - 1] - EPS)
return grid[n - 1];
int i = 0;
while (i + 1 < n && grid[i + 1] <= pts + EPS)
i++;
if (fabs(grid[i] - pts) < EPS)
return grid[i];
const double lo = grid[i], hi = grid[i + 1];
switch (mode) {
case PL_PTS_ROUND_DOWN: return lo;
case PL_PTS_ROUND_UP: return hi;
case PL_PTS_ROUND_NEAREST: return (pts - lo) >= (hi - pts) ? hi : lo; // ties up
case PL_PTS_ROUND_COUNT: break;
}
pl_unreachable();
}
// Round an output timestamp. Both ends of a cut are quantized by the same
// function against the same grid, which is what makes the sequential playback
// guarantee hold. Without a known vsync duration only UP is resolvable, so
// everything else degrades to UP - uniformly, preserving the guarantee.
static double round_out(double pts, const double *grid, int n,
enum pl_pts_round_mode mode, bool have_vsync)
{
if (!have_vsync)
mode = PL_PTS_ROUND_UP;
return round_grid(pts, grid, n, mode);
}
struct resolved {
double start, stop;
};
//----------------------------------------------------------------------------
// Scenario description
//----------------------------------------------------------------------------
struct input_spec {
// Synthetic source stream
double first_pts; // PTS of the first source frame
double fps; // source frame rate
int count; // number of source frames
// Scheduling. `start`/`stop` are output timestamps, `seek` is an input one.
double start, stop, seek;
enum pl_pts_round_mode start_mode, stop_mode;
bool trickle; // feed the queue lazily rather than pre-pushing
};
struct scenario {
const char *name;
double out_fps;
int num_out;
const struct pl_filter_config *mixer; // NULL = no frame mixing
bool no_vsync_hint; // leave pl_queue_params.vsync_duration unset
struct input_spec in[MAX_INPUTS];
int num_in;
};
//----------------------------------------------------------------------------
// Simulator
//----------------------------------------------------------------------------
struct sim_input {
const struct input_spec *spec;
struct pl_frame frames[MAX_SRC];
struct pl_source_frame srcs[MAX_SRC];
double pts[MAX_SRC];
int pushed;
pl_input input;
// Reference model state
struct resolved ref;
// Per-output-frame records
char ref_line[MAX_OUT + 1];
char act_line[MAX_OUT + 1];
double act_pts[MAX_OUT];
bool act_vis[MAX_OUT];
enum pl_queue_status act_status[MAX_OUT];
// Values reported via pl_input_status, per output frame. The implementation
// only sees frames up to the current instant, so these are not known up
// front; NAN means "not yet determinable" and the checks below verify
// monotone refinement rather than eager equality.
double act_start[MAX_OUT];
double act_stop[MAX_OUT];
};
static bool map_frame(pl_gpu gpu, pl_tex *tex, const struct pl_source_frame *src,
struct pl_frame *out_frame)
{
*out_frame = *(const struct pl_frame *) src->frame_data;
return true;
}
static char advance_char(int prev, int cur, bool visible)
{
if (!visible)
return '.';
if (prev < 0)
return '^'; // first visible frame of a run
if (cur == prev)
return '='; // duplicated source frame
if (cur == prev + 1)
return '>'; // advanced by exactly one source frame
if (cur > prev + 1)
return '+'; // skipped one or more source frames
return '<'; // went backwards
}
// The source frame the renderer displays at `pts`, or -1 if none is available
// yet. This models pl_render_image_mix(): a frame only becomes available once
// `pts` reaches it, but among the frames actually in the mix the renderer picks
// refimg via pl_frame_mix_nearest() - nearest neighbour, not zero-order hold.
//
// The availability gate applies to still images too, so a single frame whose
// PTS lies ahead of the mapped input time never becomes visible at all. Only
// its *duration* is unbounded; see `build_reference`.
static int nearest_index(const struct sim_input *si, double pts)
{
if (!si->spec->count || pts + EPS < si->pts[0])
return -1; // nothing to show yet
int best = 0;
for (int i = 1; i < si->spec->count; i++) {
if (fabs(si->pts[i] - pts) < fabs(si->pts[best] - pts))
best = i;
}
return best;
}
static void build_reference(struct sim_input *si, const double *out_grid,
int out_n, int num_out, double vsync,
bool have_vsync)
{
const struct input_spec *s = si->spec;
const double frame_dur = 1.0 / s->fps;
struct resolved *r = &si->ref;
r->start = round_out(s->start, out_grid, out_n, s->start_mode, have_vsync);
if (s->stop < DBL_MAX) {
r->stop = round_out(s->stop, out_grid, out_n, s->stop_mode, have_vsync);
} else if (s->count == 1) {
r->stop = DBL_MAX; // still image: unbounded, never runs out
} else {
// "Until the stream runs out": the queue holds the last frame for one
// extra frame duration before reporting EOF.
r->stop = r->start - s->seek + si->pts[s->count - 1] + frame_dur;
// A seek past the end of the stream never becomes visible at all, in
// which case it stops at the same instant it was scheduled to start.
r->stop = PL_MAX(r->stop, r->start);
}
int prev = -1;
for (int o = 0; o < num_out; o++) {
const double p = o * vsync;
// Being scheduled is not sufficient: the input also needs a frame to
// show, which it lacks while `seek_pts` maps to a point preceding the
// stream. Such an input simply appears late.
const bool sched = p >= r->start - EPS && p < r->stop - EPS;
const int cur = sched ? nearest_index(si, p - r->start + s->seek) : -1;
const bool vis = sched && cur >= 0;
si->ref_line[o] = advance_char(prev, cur, vis);
prev = vis ? cur : -1;
}
si->ref_line[num_out] = '\0';
}
// Run the scenario, filling in each input's `act_*` records. `repeat` controls
// how many times pl_input_update() is called per output frame, which must not
// affect the result.
static void simulate(const struct scenario *sc, struct sim_input *sis,
int repeat, pl_log log, pl_gpu gpu)
{
pl_compositor comp = pl_compositor_create(log);
pl_compositor_init(comp, gpu);
struct pl_render_params rparams = { PL_RENDER_DEFAULTS };
rparams.frame_mixer = sc->mixer;
struct pl_queue_params params = { PL_QUEUE_DEFAULTS
.vsync_duration = sc->no_vsync_hint ? 0.0 : 1.0 / sc->out_fps,
};
const double vsync = 1.0 / sc->out_fps;
for (int n = 0; n < sc->num_in; n++) {
struct sim_input *si = &sis[n];
const struct input_spec *s = si->spec;
si->input = pl_compositor_input_create(comp);
si->input->start_pts = s->start;
si->input->stop_pts = s->stop;
si->input->seek_pts = s->seek;
si->input->start_mode = s->start_mode;
si->input->stop_mode = s->stop_mode;
si->input->params = &rparams;
si->pushed = 0;
if (!s->trickle) {
for (int i = 0; i < s->count; i++)
pl_queue_push(si->input->queue, &si->srcs[i]);
pl_queue_push(si->input->queue, NULL);
si->pushed = s->count + 1;
}
}
int prev_idx[MAX_INPUTS];
for (int n = 0; n < MAX_INPUTS; n++)
prev_idx[n] = -1;
for (int o = 0; o < sc->num_out; o++) {
const double p = o * vsync;
params.pts = p;
for (int n = 0; n < sc->num_in; n++) {
struct sim_input *si = &sis[n];
const struct input_spec *s = si->spec;
// Keep the queue fed relative to the *reference* timeline, so the
// feed schedule does not depend on the implementation.
if (s->trickle) {
const double want = p - si->ref.start + s->seek + 5.0 / s->fps;
while (si->pushed < s->count && si->pts[si->pushed] < want)
pl_queue_push(si->input->queue, &si->srcs[si->pushed++]);
if (si->pushed == s->count) {
pl_queue_push(si->input->queue, NULL);
si->pushed++;
}
}
struct pl_input_status st = {0};
enum pl_queue_status ret = PL_QUEUE_OK;
for (int rep = 0; rep < repeat; rep++)
ret = pl_input_update(si->input, &st, ¶ms);
if (ret == PL_QUEUE_ERR)
FAIL("input %d: PL_QUEUE_ERR at out[%d]\n", n, o);
const bool vis = st.mix != NULL;
int cur = -1;
if (vis) {
// Documented guarantee: a non-NULL mix holds at least one frame
if (!st.mix->num_frames)
FAIL("input %d: mix non-NULL but empty at out[%d]\n", n, o);
const struct pl_frame *f = pl_frame_mix_nearest(st.mix);
cur = (int) (intptr_t) f->user_data;
}
si->act_status[o] = ret;
si->act_vis[o] = vis;
si->act_pts[o] = st.pts;
si->act_start[o] = st.start_pts;
si->act_stop[o] = st.stop_pts;
si->act_line[o] = advance_char(prev_idx[n], cur, vis);
prev_idx[n] = vis ? cur : -1;
}
}
for (int n = 0; n < sc->num_in; n++)
sis[n].act_line[sc->num_out] = '\0';
pl_compositor_destroy(&comp);
}
//----------------------------------------------------------------------------
// Checks
//----------------------------------------------------------------------------
static const char *fmt_pts(double pts, char buf[32])
{
if (pts >= DBL_MAX)
snprintf(buf, 32, "+inf");
else
snprintf(buf, 32, "%.4f", pts);
return buf;
}
// Invariants that hold regardless of the rounding semantics in force.
static void check_invariants(const struct scenario *sc,
const struct sim_input *si, int idx)
{
// The visible set must be a single contiguous run. An input that flickers
// on and off, or comes back after stopping, is always a bug.
int runs = 0;
for (int o = 0; o < sc->num_out; o++) {
if (si->act_vis[o] && (o == 0 || !si->act_vis[o - 1]))
runs++;
}
if (runs > 1) {
FAIL("input %d visible in %d disjoint runs\n timeline = %s\n",
idx, runs, si->act_line);
}
// NAN means "not yet determinable", and each value becomes determinable at
// a specific, observable moment:
//
// start_pts => the input has been visible at least once
// stop_pts <=> pl_input_update() has returned PL_QUEUE_EOF
//
// `start_pts` is only an implication: it latches when the *scheduled* start
// is reached, which can precede the first visible frame if the queue has
// nothing to show yet. The converse - visible but unresolved - is a bug,
// since the output-to-input mapping is anchored on it.
bool was_visible = false, was_eof = false;
for (int o = 0; o < sc->num_out; o++) {
was_visible |= si->act_vis[o];
was_eof |= si->act_status[o] == PL_QUEUE_EOF;
if (was_visible && isnan(si->act_start[o])) {
FAIL("input %d start_pts is NAN at out[%d], but the input has been "
"visible\n timeline = %s\n", idx, o, si->act_line);
}
if (isnan(si->act_stop[o]) == was_eof) {
FAIL("input %d stop_pts is %s at out[%d], but EOF has%s been "
"reported\n timeline = %s\n", idx, was_eof ? "NAN" : "set",
o, was_eof ? "" : " not", si->act_line);
}
}
// The reported input-domain PTS advances monotonically while visible.
for (int o = 1; o < sc->num_out; o++) {
if (!si->act_vis[o] || !si->act_vis[o - 1])
continue;
if (si->act_pts[o] < si->act_pts[o - 1] - EPS) {
FAIL("input %d PTS went backwards at out[%d]: %f -> %f\n",
idx, o, si->act_pts[o - 1], si->act_pts[o]);
}
}
}
// Verify one reported value across the run: NAN for as long as it is not yet
// determinable, but once it commits to a concrete value that value must be
// correct and must never change or revert.
//
// `need_by` is the output frame by which it must have resolved, or -1 if the
// run never reaches that point.
static void check_value(const struct scenario *sc, int idx, const char *name,
const double *got, double ref, int need_by)
{
char b0[32], b1[32];
int first = -1;
for (int o = 0; o < sc->num_out; o++) {
if (!isnan(got[o])) {
first = o;
break;
}
}
if (first < 0) {
if (need_by >= 0)
FAIL("input %d: %s never resolved (needed by out[%d])\n",
idx, name, need_by);
return;
}
for (int o = first + 1; o < sc->num_out; o++) {
if (isnan(got[o])) {
FAIL("input %d: %s reverted to NAN at out[%d] after resolving at "
"out[%d]\n", idx, name, o, first);
}
if (fabs(got[o] - got[first]) > EPS) {
FAIL("input %d: %s changed after resolving: %s at out[%d] -> %s at "
"out[%d]\n", idx, name, fmt_pts(got[first], b0), first,
fmt_pts(got[o], b1), o);
}
}
if (fabs(got[first] - ref) > EPS) {
FAIL("input %d: %s resolved to %s, reference says %s\n",
idx, name, fmt_pts(got[first], b0), fmt_pts(ref, b1));
}
if (first > need_by && need_by >= 0) {
FAIL("input %d: %s only resolved at out[%d], but was needed by "
"out[%d]\n", idx, name, first, need_by);
}
}
static void check_resolved(const struct scenario *sc, const struct sim_input *si,
int idx)
{
// `start_pts` must be resolved by the time the input first appears, and
// `stop_pts` by the time it disappears again. If the run never reaches
// those points, leaving them unresolved is legitimate.
int first_vis = -1, last_vis = -1;
for (int o = 0; o < sc->num_out; o++) {
if (si->act_vis[o]) {
if (first_vis < 0)
first_vis = o;
last_vis = o;
}
}
const int stop_at = (last_vis >= 0 && last_vis + 1 < sc->num_out)
? last_vis + 1 : -1;
check_value(sc, idx, "start_pts", si->act_start, si->ref.start, first_vis);
check_value(sc, idx, "stop_pts", si->act_stop, si->ref.stop, stop_at);
}
static void check_reference(const struct scenario *sc, const struct sim_input *si,
int idx)
{
char b0[32], b1[32];
if (strcmp(si->ref_line, si->act_line) != 0) {
int at = 0;
while (si->ref_line[at] && si->ref_line[at] == si->act_line[at])
at++;
FAIL("input %d: timeline diverges from reference at out[%d]\n"
" ref: %s\n act: %s\n %*s^\n",
idx, at, si->ref_line, si->act_line, at, "");
}
printf(" input %d: %s (start=%s stop=%s)\n", idx, si->act_line,
fmt_pts(si->ref.start, b0), fmt_pts(si->ref.stop, b1));
check_resolved(sc, si, idx);
}
// The sequential playback guarantee: for any input pair where one's `stop_pts`
// equals the other's `start_pts` under matching rounding modes, the handover
// must be exact - no output frame with both visible, none with neither.
//
// This holds by construction: both ends are quantized on the same grid by the
// same function.
static void check_sequential(const struct scenario *sc,
const struct sim_input *sis)
{
for (int a = 0; a < sc->num_in; a++) {
for (int b = 0; b < sc->num_in; b++) {
if (a == b)
continue;
const struct input_spec *sa = sis[a].spec, *sb = sis[b].spec;
if (sa->stop >= DBL_MAX || fabs(sa->stop - sb->start) > EPS)
continue;
if (sa->stop_mode != sb->start_mode)
continue;
for (int o = 0; o < sc->num_out; o++) {
if (sis[a].act_vis[o] && sis[b].act_vis[o]) {
FAIL("inputs %d and %d overlap at out[%d], but %d->stop_pts"
" == %d->start_pts == %f\n %d = %s\n %d = %s\n",
a, b, o, a, b, sa->stop, a, sis[a].act_line,
b, sis[b].act_line);
}
}
// No gap either: b must pick up on exactly the frame a drops out.
int last_a = -1, first_b = -1;
for (int o = 0; o < sc->num_out; o++) {
if (sis[a].act_vis[o])
last_a = o;
if (sis[b].act_vis[o] && first_b < 0)
first_b = o;
}
if (last_a >= 0 && first_b >= 0 && first_b != last_a + 1) {
FAIL("gap between inputs %d and %d: %d ends at out[%d], %d "
"starts at out[%d]\n %d = %s\n %d = %s\n", a, b, a,
last_a, b, first_b, a, sis[a].act_line, b, sis[b].act_line);
}
}
}
}
static void run_scenario(const struct scenario *sc, pl_log log, pl_gpu gpu)
{
static struct sim_input sis[MAX_INPUTS];
static struct sim_input sis2[MAX_INPUTS];
const double vsync = 1.0 / sc->out_fps;
pl_assert(sc->name && sc->out_fps > 0);
pl_assert(sc->num_in > 0 && sc->num_in <= MAX_INPUTS);
pl_assert(sc->num_out > 0 && sc->num_out <= MAX_OUT);
// Output vsync grid, extended past the simulated range so that a stop can
// round up beyond the last simulated frame.
static double out_grid[MAX_OUT * 2];
const int out_n = MAX_OUT * 2;
for (int i = 0; i < out_n; i++)
out_grid[i] = i * vsync;
printf("--- %s (%g fps x%d)\n", sc->name, sc->out_fps, sc->num_out);
for (int n = 0; n < sc->num_in; n++) {
const struct input_spec *s = &sc->in[n];
pl_assert(s->count > 0 && s->count <= MAX_SRC);
struct sim_input *si = &sis[n];
*si = (struct sim_input) { .spec = s };
for (int i = 0; i < s->count; i++) {
si->pts[i] = s->first_pts + i / s->fps;
si->frames[i] = (struct pl_frame) {
.user_data = (void *) (intptr_t) i,
};
si->srcs[i] = (struct pl_source_frame) {
.pts = si->pts[i],
.duration = 1.0 / s->fps,
.map = map_frame,
.frame_data = &si->frames[i],
};
}
build_reference(si, out_grid, out_n, sc->num_out, vsync,
!sc->no_vsync_hint);
}
simulate(sc, sis, 1, log, gpu);
for (int n = 0; n < sc->num_in; n++) {
check_invariants(sc, &sis[n], n);
check_reference(sc, &sis[n], n);
}
check_sequential(sc, sis);
// Repeated pl_input_update() calls at an identical PTS must not change the
// outcome; plplay issues 2-3 per output frame.
for (int n = 0; n < sc->num_in; n++)
sis2[n] = sis[n];
simulate(sc, sis2, 3, log, gpu);
for (int n = 0; n < sc->num_in; n++) {
if (strcmp(sis[n].act_line, sis2[n].act_line) != 0) {
FAIL("input %d not idempotent under repeated pl_input_update()\n"
" 1x = %s\n 3x = %s\n", n, sis[n].act_line, sis2[n].act_line);
}
}
}
//----------------------------------------------------------------------------
// Scenarios
//----------------------------------------------------------------------------
#define SRC(fps_, first_, count_) .fps = fps_, .first_pts = first_, .count = count_
#define NOSTOP .stop = DBL_MAX
static const struct scenario scenarios[] = {
{
.name = "baseline: 30 -> 30, fully aligned",
.out_fps = 30, .num_out = 12, .num_in = 1,
.in = {{ SRC(30, 0.0, 20), NOSTOP }},
}, {
.name = "start aligned to the output grid (0.1 = 3 vsyncs)",
.out_fps = 30, .num_out = 12, .num_in = 1,
.in = {{ SRC(30, 0.0, 20), NOSTOP, .start = 0.1 }},
}, {
.name = "start misaligned by half a vsync, NEAREST (ties round up)",
.out_fps = 30, .num_out = 12, .num_in = 1,
.in = {{ SRC(30, 0.0, 20), NOSTOP, .start = 1/60.0 }},
}, {
.name = "start misaligned by half a vsync, DOWN",
.out_fps = 30, .num_out = 12, .num_in = 1,
.in = {{ SRC(30, 0.0, 20), NOSTOP, .start = 1/60.0,
.start_mode = PL_PTS_ROUND_DOWN }},
}, {
.name = "start misaligned by half a vsync, UP",
.out_fps = 30, .num_out = 12, .num_in = 1,
.in = {{ SRC(30, 0.0, 20), NOSTOP, .start = 1/60.0,
.start_mode = PL_PTS_ROUND_UP }},
}, {
.name = "start misaligned, DOWN but no vsync_duration (falls back to UP)",
.out_fps = 30, .num_out = 12, .num_in = 1, .no_vsync_hint = true,
.in = {{ SRC(30, 0.0, 20), NOSTOP, .start = 1/60.0,
.start_mode = PL_PTS_ROUND_DOWN }},
}, {
.name = "seek mid-frame: input phase preserved exactly",
.out_fps = 30, .num_out = 12, .num_in = 1,
.in = {{ SRC(30, 0.0, 20), NOSTOP, .seek = 0.055 }},
}, {
// Deliberate: the renderer selects frames by nearest neighbour in all
// cases, so a mid-frame seek into a low-fps source shifts every cut by
// up to half an input frame (0.125s here) and can skip the first frame
// outright - at 4 fps a seek to 0.13 is already closer to frame 1 than
// frame 0. ZOH would hold each frame for its full interval instead;
// that trade was made knowingly in favour of a single uniform rule.
.name = "very low fps input: nearest selection shifts cut points",
.out_fps = 30, .num_out = 24, .num_in = 1,
.in = {{ SRC(4, 0.0, 8), NOSTOP, .seek = 0.13 }},
}, {
// A single frame is a still image of unbounded duration: it never runs
// out, regardless of any tagged frame duration, and so is visible for
// exactly [start_pts, stop_pts). Its own PTS still gates when it first
// becomes available, as for any other frame.
.name = "still: single frame, no stop -> visible for the whole run",
.out_fps = 30, .num_out = 12, .num_in = 1,
.in = {{ SRC(30, 0.0, 1), NOSTOP }},
}, {
.name = "still: low tagged frame duration must not end it",
.out_fps = 30, .num_out = 12, .num_in = 1,
.in = {{ SRC(4, 0.0, 1), NOSTOP }},
}, {
.name = "still: visible only within [start, stop)",
.out_fps = 30, .num_out = 12, .num_in = 1,
.in = {{ SRC(30, 0.0, 1), .start = 0.1, .stop = 0.2 }},
}, {
// No clamping: the caller is responsible for normalizing the timebase,
// since libplacebo cannot tell an unnormalized stream apart from one
// that genuinely begins late.
.name = "unnormalized stream @3.7, seek = 3.7 (caller normalizes)",
.out_fps = 30, .num_out = 8, .num_in = 1,
.in = {{ SRC(30, 3.7, 20), NOSTOP, .seek = 3.7 }},
}, {
.name = "stream @0.1, seek = 0: appears 3 vsyncs late, no clamp",
.out_fps = 30, .num_out = 12, .num_in = 1,
.in = {{ SRC(30, 0.1, 20), NOSTOP }},
}, {
.name = "seek past the end of the stream: never becomes visible",
.out_fps = 30, .num_out = 8, .num_in = 1,
.in = {{ SRC(30, 0.0, 5), NOSTOP, .seek = 100.0 }},
}, {
.name = "stop aligned to the output grid (3 frames)",
.out_fps = 30, .num_out = 8, .num_in = 1,
.in = {{ SRC(30, 0.0, 20), .stop = 0.1 }},
}, {
.name = "stop misaligned, NEAREST",
.out_fps = 30, .num_out = 10, .num_in = 1,
.in = {{ SRC(30, 0.0, 20), .stop = 0.115 }},
}, {
.name = "stop misaligned, UP",
.out_fps = 30, .num_out = 10, .num_in = 1,
.in = {{ SRC(30, 0.0, 20), .stop = 0.115,
.stop_mode = PL_PTS_ROUND_UP }},
}, {
.name = "stop = DBL_MAX: plays until the stream runs out",
.out_fps = 30, .num_out = 28, .num_in = 1,
.in = {{ SRC(30, 0.0, 15), NOSTOP }},
}, {
.name = "rate mismatch: 24 -> 60, no mixer",
.out_fps = 60, .num_out = 16, .num_in = 1,
.in = {{ SRC(24, 0.0, 20), NOSTOP }},
}, {
.name = "rate mismatch: 24 -> 60, mitchell mixer",
.out_fps = 60, .num_out = 16, .num_in = 1,
.mixer = &pl_filter_mitchell_clamp,
.in = {{ SRC(24, 0.0, 20), NOSTOP }},
}, {
.name = "trickle-fed queue instead of pre-pushed",
.out_fps = 30, .num_out = 12, .num_in = 1,
.in = {{ SRC(30, 0.0, 20), NOSTOP, .trickle = true }},
},
// The sequential playback guarantee: input0->stop_pts == input1->start_pts
// with matching modes must hand over with no gap and no overlap.
{
.name = "SEQ: cut on a vsync boundary",
.out_fps = 30, .num_out = 16, .num_in = 2,
.in = {
{ SRC(30, 0.0, 20), .stop = 0.2 },
{ SRC(30, 0.0, 20), NOSTOP, .start = 0.2 },
},
}, {
.name = "SEQ: cut mid-vsync, NEAREST both sides",
.out_fps = 30, .num_out = 16, .num_in = 2,
.in = {
{ SRC(30, 0.0, 20), .stop = 0.215 },
{ SRC(30, 0.0, 20), NOSTOP, .start = 0.215 },
},
}, {
.name = "SEQ: cut mid-vsync, DOWN both sides",
.out_fps = 30, .num_out = 16, .num_in = 2,
.in = {
{ SRC(30, 0.0, 20), .stop = 0.215,
.stop_mode = PL_PTS_ROUND_DOWN },
{ SRC(30, 0.0, 20), NOSTOP, .start = 0.215,
.start_mode = PL_PTS_ROUND_DOWN },
},
}, {
.name = "SEQ: cut mid-vsync, UP both sides",
.out_fps = 30, .num_out = 16, .num_in = 2,
.in = {
{ SRC(30, 0.0, 20), .stop = 0.215,
.stop_mode = PL_PTS_ROUND_UP },
{ SRC(30, 0.0, 20), NOSTOP, .start = 0.215,
.start_mode = PL_PTS_ROUND_UP },
},
}, {
.name = "SEQ: cut mid-vsync, mismatched frame rates either side",
.out_fps = 30, .num_out = 16, .num_in = 2,
.in = {
{ SRC(24, 0.0, 20), .stop = 0.215 },
{ SRC(50, 0.0, 30), NOSTOP, .start = 0.215 },
},
}, {
.name = "SEQ: cut mid-vsync, no vsync_duration (both fall back to UP)",
.out_fps = 30, .num_out = 16, .num_in = 2, .no_vsync_hint = true,
.in = {
{ SRC(30, 0.0, 20), .stop = 0.215 },
{ SRC(30, 0.0, 20), NOSTOP, .start = 0.215 },
},
}, {
// Degenerate case of the same guarantee: a zero-length cut is empty
// regardless of where it falls or which mode quantizes it.
.name = "EMPTY: start == stop on a vsync boundary",
.out_fps = 30, .num_out = 10, .num_in = 1,
.in = {{ SRC(30, 0.0, 20), .start = 0.1, .stop = 0.1 }},
}, {
.name = "EMPTY: start == stop mid-vsync, NEAREST both",
.out_fps = 30, .num_out = 10, .num_in = 1,
.in = {{ SRC(30, 0.0, 20), .start = 0.215, .stop = 0.215 }},
}, {
.name = "EMPTY: start == stop mid-vsync, UP both",
.out_fps = 30, .num_out = 10, .num_in = 1,
.in = {{ SRC(30, 0.0, 20), .start = 0.215, .stop = 0.215,
.start_mode = PL_PTS_ROUND_UP,
.stop_mode = PL_PTS_ROUND_UP }},
}, {
.name = "EMPTY: start == stop mid-vsync, DOWN both",
.out_fps = 30, .num_out = 10, .num_in = 1,
.in = {{ SRC(30, 0.0, 20), .start = 0.215, .stop = 0.215,
.start_mode = PL_PTS_ROUND_DOWN,
.stop_mode = PL_PTS_ROUND_DOWN }},
}, {
.name = "EMPTY: start == stop at 0.0",
.out_fps = 30, .num_out = 10, .num_in = 1,
.in = {{ SRC(30, 0.0, 20), .start = 0.0, .stop = 0.0 }},
},
};
//----------------------------------------------------------------------------
// Fit modes
//----------------------------------------------------------------------------
// pl_input_place() is a pure function of the fields below, so these cases need
// no queue, no renderer and no GPU work - just two bare frames.
struct fit_case {
const char *name;
enum pl_fit_mode fit;
float image_w, image_h, image_par;
pl_rotation image_rot;
float canvas_w, canvas_h, canvas_par;
pl_rotation canvas_rot;
pl_rotation rotation; // pl_input.rotation
struct pl_rect2df src_rect, dst_rect; // pl_input.*_rect, {0} = full frame
pl_vec2df focus; // pl_input.focus, if `has_focus`
bool has_focus;
int focus_centered; // FOCUS_X/Y: axes on which the focus
// must land on the dst_rect center
float expect_aspect; // physical aspect, 0 = unchecked
};
enum { FOCUS_X = 1 << 0, FOCUS_Y = 1 << 1 };
#define FOCUS(fx, fy) .focus = { fx, fy }, .has_focus = true
static pl_vec2df focus_of(const struct fit_case *c)
{
return c->has_focus ? c->focus : (pl_vec2df) { 0.5f, 0.5f };
}
static const struct pl_rect2df full_rect = { 0, 0, 1, 1 };
static struct pl_rect2df rect_or_full(struct pl_rect2df rc)
{
return pl_rect2d_eq(rc, (struct pl_rect2df) {0}) ? full_rect : rc;
}
// Map `rc`, given relative to `outer`, into the frame `outer` is relative to.
static struct pl_rect2df mix_rect(struct pl_rect2df outer, struct pl_rect2df rc)
{
return (struct pl_rect2df) {
.x0 = PL_MIX(outer.x0, outer.x1, rc.x0),
.y0 = PL_MIX(outer.y0, outer.y1, rc.y0),
.x1 = PL_MIX(outer.x0, outer.x1, rc.x1),
.y1 = PL_MIX(outer.y0, outer.y1, rc.y1),
};
}
// Mirror `rc` within `bounds`, along every axis on which `ref` is flipped.
static void flip_like(struct pl_rect2df *rc, struct pl_rect2df ref,
struct pl_rect2df bounds)
{
if (ref.x0 > ref.x1) {
rc->x0 = bounds.x0 + bounds.x1 - rc->x0;
rc->x1 = bounds.x0 + bounds.x1 - rc->x1;
}
if (ref.y0 > ref.y1) {
rc->y0 = bounds.y0 + bounds.y1 - rc->y0;
rc->y1 = bounds.y0 + bounds.y1 - rc->y1;
}
}
// Clip `dst` to `bounds`, cutting `src` down by the same fraction so that the
// mapping between the two stays exactly the same.
static void clip_rect(struct pl_rect2df *src, struct pl_rect2df *dst,
struct pl_rect2df bounds)
{
const float sx = pl_rect_w(*src) / pl_rect_w(*dst);
const float sy = pl_rect_h(*src) / pl_rect_h(*dst);
if (dst->x0 < bounds.x0) {
src->x0 += (bounds.x0 - dst->x0) * sx;
dst->x0 = bounds.x0;
}
if (dst->x1 > bounds.x1) {
src->x1 -= (dst->x1 - bounds.x1) * sx;
dst->x1 = bounds.x1;
}
if (dst->y0 < bounds.y0) {
src->y0 += (bounds.y0 - dst->y0) * sy;
dst->y0 = bounds.y0;
}
if (dst->y1 > bounds.y1) {
src->y1 -= (dst->y1 - bounds.y1) * sy;
dst->y1 = bounds.y1;
}
}
// "The smaller of the two is centered on the focus point of the larger, as far
// as possible without exceeding it": the offset of a span of `size`, relative
// to the larger one, centered on `focus`.
static float align(float size, float focus)
{
return PL_CLAMP(focus - size / 2, 0.0f, 1.0f - size);
}
// Logical size, i.e. after folding in the pixel aspect ratio and rotation.
static void logical(float *w, float *h, float pw, float ph, float par,
pl_rotation rot)
{
*w = pw;
*h = ph;
if (par > 1.0f)
*w *= par;
else if (par > 0.0f && par < 1.0f)
*h /= par;
if (rot % PL_ROTATION_180 == PL_ROTATION_90)
PL_SWAP(*w, *h);
}
// Frame of reference: everything is expressed in the *displayed* orientation
// of each frame. For the image, that is after both its own rotation and
// `pl_input.rotation`; for the canvas, after its own rotation. A 16:9 camera
// frame tagged 90 degrees fits like a 9:16 one, `src_rect = {0, 0, 0.5, 1}`
// crops away the right half of what the viewer would see, and the rects that
// pl_input_place() returns are in that same orientation. Mapping all of this
// back onto the actual pixels is pl_compositor_render()'s job, which is what
// the rendered variant of these tests below checks.
// Reference model, written from the documented meaning of each fit mode in
// <libplacebo/compositor.h> rather than from pl_input_place().
static void expect_fit(const struct fit_case *c, struct pl_rect2df *src,
struct pl_rect2df *dst)
{
// A flipped rect is the same rect with its contents mirrored, so fit
// using the plain rects, and mirror the result within them at the very
// end. The focus is thus measured from the `x0`, `y0` corner of whichever
// rect it is relative to.
const struct pl_rect2df sr = rect_or_full(c->src_rect), dr = rect_or_full(c->dst_rect);
struct pl_rect2df src_rect = sr, dst_rect = dr;
pl_rect2df_normalize(&src_rect);
pl_rect2df_normalize(&dst_rect);
const pl_vec2df focus = focus_of(c);
// "Like PL_FIT_PLACE, but rounding to exact pixels": raw pixels means the
// pixel aspect ratio of neither side applies.
float ipar = c->image_par, cpar = c->canvas_par;
if (c->fit == PL_FIT_RAW)
ipar = cpar = 1.0f;
float iw, ih, cw, ch;
logical(&iw, &ih, c->image_w, c->image_h, ipar, c->image_rot);
logical(&cw, &ch, c->canvas_w, c->canvas_h, cpar, c->canvas_rot);
if (c->rotation % PL_ROTATION_180 == PL_ROTATION_90)
PL_SWAP(iw, ih);
const float full_cw = cw, full_ch = ch;
if (c->fit == PL_FIT_RAW) {
// "Rounding to exact pixels": image pixels map 1:1 onto canvas
// pixels, so both rects are cut down to whole pixels first
src_rect = (struct pl_rect2df) {
roundf(src_rect.x0 * iw) / iw, roundf(src_rect.y0 * ih) / ih,
roundf(src_rect.x1 * iw) / iw, roundf(src_rect.y1 * ih) / ih,
};
dst_rect = (struct pl_rect2df) {
roundf(dst_rect.x0 * cw) / cw, roundf(dst_rect.y0 * ch) / ch,
roundf(dst_rect.x1 * cw) / cw, roundf(dst_rect.y1 * ch) / ch,
};
}
// The fit is performed as though `src_rect` were the entire image, and
// `dst_rect` the entire canvas
iw *= pl_rect_w(src_rect);
ih *= pl_rect_h(src_rect);
cw *= pl_rect_w(dst_rect);
ch *= pl_rect_h(dst_rect);
enum pl_fit_mode mode = c->fit;
if (mode == PL_FIT_SCALE_DOWN) // "scaled only as needed to fit inside"
mode = (iw > cw || ih > ch) ? PL_FIT_CONTAIN : PL_FIT_PLACE;
*src = *dst = full_rect;
switch (mode) {
case PL_FIT_FILL: // "stretched to the exact output size, ignoring aspect"
break;
case PL_FIT_CONTAIN: { // "largest size contained within the canvas"
// The image is the smaller one, aligned within the canvas
const float scale = fminf(cw / iw, ch / ih);
const float dw = iw * scale / cw, dh = ih * scale / ch;
dst->x0 = align(dw, focus.x);
dst->y0 = align(dh, focus.y);
dst->x1 = dst->x0 + dw;
dst->y1 = dst->y0 + dh;
break;
}
case PL_FIT_COVER: { // "smallest size that covers the canvas"
// The canvas is the smaller one, aligned within the image
const float scale = fmaxf(cw / iw, ch / ih);
const float sw = cw / (iw * scale), sh = ch / (ih * scale);
src->x0 = align(sw, focus.x);
src->y0 = align(sh, focus.y);
src->x1 = src->x0 + sw;
src->y1 = src->y0 + sh;
break;
}
case PL_FIT_PLACE: // "source is unscaled (as-is)"
case PL_FIT_RAW: {
const float dw = iw / cw, dh = ih / ch;
if (c->fit == PL_FIT_SCALE_DOWN) {
// The image fits inside the canvas, and is aligned within it
dst->x0 = align(dw, focus.x);
dst->y0 = align(dh, focus.y);
} else {
// The free parameter is the position on the canvas: the image is
// centered on the focus point, however far off the canvas that is
dst->x0 = focus.x - dw / 2;
dst->y0 = focus.y - dh / 2;
}
dst->x1 = dst->x0 + dw;
dst->y1 = dst->y0 + dh;
break;
}
default:
pl_unreachable();
}
*src = mix_rect(src_rect, *src);
*dst = mix_rect(dst_rect, *dst);
if (c->fit == PL_FIT_RAW) {
// ... and placed, at that exact size, on whole canvas pixels. Exact
// (half pixel) ties may be broken either way, so this is only
// compared to within a pixel.
const float dx = roundf(dst->x0 * full_cw) / full_cw - dst->x0;
const float dy = roundf(dst->y0 * full_ch) / full_ch - dst->y0;
*dst = (struct pl_rect2df) {
dst->x0 + dx, dst->y0 + dy, dst->x1 + dx, dst->y1 + dy,
};
}
// Hard invariant, for every mode: nothing is ever drawn outside of
// `dst_rect`. An image that would overflow it is cropped instead.
clip_rect(src, dst, dst_rect);
if (dst->x0 >= dst->x1 || dst->y0 >= dst->y1) {
// Entirely outside of `dst_rect`, so nothing remains at all
*src = *dst = (struct pl_rect2df) {0};
return;
}