Branch data Line data Source code
1 : : /*
2 : : * This file is part of libplacebo.
3 : : *
4 : : * libplacebo is free software; you can redistribute it and/or
5 : : * modify it under the terms of the GNU Lesser General Public
6 : : * License as published by the Free Software Foundation; either
7 : : * version 2.1 of the License, or (at your option) any later version.
8 : : *
9 : : * libplacebo is distributed in the hope that it will be useful,
10 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : : * GNU Lesser General Public License for more details.
13 : : *
14 : : * You should have received a copy of the GNU Lesser General Public
15 : : * License along with libplacebo. If not, see <http://www.gnu.org/licenses/>.
16 : : */
17 : :
18 : : #include <errno.h>
19 : : #include <math.h>
20 : :
21 : : #include "common.h"
22 : : #include "log.h"
23 : : #include "pl_thread.h"
24 : :
25 : : #include <libplacebo/utils/frame_queue.h>
26 : :
27 : : struct cache_entry {
28 : : pl_tex tex[4];
29 : : };
30 : :
31 : : struct entry {
32 : : pl_rc_t rc;
33 : : double pts;
34 : : struct cache_entry cache;
35 : : struct pl_source_frame src;
36 : : struct pl_frame frame;
37 : : uint64_t signature;
38 : : bool mapped;
39 : : bool ok;
40 : :
41 : : // for interlaced frames
42 : : enum pl_field field;
43 : : struct entry *primary;
44 : : struct entry *prev, *next;
45 : : bool dirty;
46 : : };
47 : :
48 : : // Hard limits for vsync timing validity
49 : : #define MIN_FPS 10
50 : : #define MAX_FPS 400
51 : :
52 : : // Limits for FPS estimation state
53 : : #define MAX_SAMPLES 32
54 : : #define MIN_SAMPLES 4
55 : :
56 : : // Stickiness to prevent `interpolation_threshold` oscillation
57 : : #define THRESHOLD_MAX_RATIO 0.3
58 : : #define THRESHOLD_FRAMES 5
59 : :
60 : : // Maximum number of not-yet-mapped frames to allow queueing in advance
61 : : #define PREFETCH_FRAMES 2
62 : :
63 : : struct pool {
64 : : float samples[MAX_SAMPLES];
65 : : float estimate;
66 : : float sum;
67 : : int idx;
68 : : int num;
69 : : int total;
70 : : };
71 : :
72 : : struct pl_queue_t {
73 : : pl_gpu gpu;
74 : : pl_log log;
75 : :
76 : : // For multi-threading, we use two locks. The `lock_weak` guards the queue
77 : : // state itself. The `lock_strong` has a bigger scope and should be held
78 : : // for the duration of any functions that expect the queue state to
79 : : // remain more or less valid (with the exception of adding new members).
80 : : //
81 : : // In particular, `pl_queue_reset` and `pl_queue_update` will take
82 : : // the strong lock, while `pl_queue_push_*` will only take the weak
83 : : // lock.
84 : : pl_mutex lock_strong;
85 : : pl_mutex lock_weak;
86 : : pl_cond wakeup;
87 : :
88 : : // Frame queue and state
89 : : PL_ARRAY(struct entry *) queue;
90 : : uint64_t signature;
91 : : int threshold_frames;
92 : : bool want_frame;
93 : : bool eof;
94 : :
95 : : // Average vsync/frame fps estimation state
96 : : struct pool vps, fps;
97 : : float reported_vps;
98 : : float reported_fps;
99 : : double prev_pts;
100 : : double pts_offset;
101 : :
102 : : // Storage for temporary arrays
103 : : PL_ARRAY(uint64_t) tmp_sig;
104 : : PL_ARRAY(float) tmp_ts;
105 : : PL_ARRAY(const struct pl_frame *) tmp_frame;
106 : :
107 : : // Queue of GPU objects to reuse
108 : : PL_ARRAY(struct cache_entry) cache;
109 : : };
110 : :
111 : 4 : pl_queue pl_queue_create(pl_gpu gpu)
112 : : {
113 : 4 : pl_queue p = pl_alloc_ptr(NULL, p);
114 : 4 : *p = (struct pl_queue_t) {
115 : : .gpu = gpu,
116 : 4 : .log = gpu->log,
117 : : };
118 : :
119 [ - + ]: 4 : pl_mutex_init(&p->lock_strong);
120 [ - + ]: 4 : pl_mutex_init(&p->lock_weak);
121 : 4 : int ret = pl_cond_init(&p->wakeup);
122 [ - + ]: 4 : if (ret) {
123 : 0 : PL_ERR(p, "Failed to init conditional variable: %d", ret);
124 : 0 : return NULL;
125 : : }
126 : : return p;
127 : : }
128 : :
129 : 320 : static void recycle_cache(pl_queue p, struct cache_entry *cache, bool recycle)
130 : : {
131 : : bool has_textures = false;
132 [ + + ]: 1600 : for (int i = 0; i < PL_ARRAY_SIZE(cache->tex); i++) {
133 [ + - ]: 1280 : if (!cache->tex[i])
134 : 1280 : continue;
135 : :
136 : : has_textures = true;
137 [ # # ]: 0 : if (recycle) {
138 : 0 : pl_tex_invalidate(p->gpu, cache->tex[i]);
139 : : } else {
140 : 0 : pl_tex_destroy(p->gpu, &cache->tex[i]);
141 : : }
142 : : }
143 : :
144 [ - + ]: 320 : if (recycle && has_textures)
145 [ # # # # : 0 : PL_ARRAY_APPEND(p, p->cache, *cache);
# # ]
146 : :
147 : : memset(cache, 0, sizeof(*cache)); // sanity
148 : 320 : }
149 : :
150 : 1464 : static void entry_deref(pl_queue p, struct entry **pentry, bool recycle)
151 : : {
152 : 1464 : struct entry *entry = *pentry;
153 : 1464 : *pentry = NULL;
154 [ + + + + ]: 1464 : if (!entry || !pl_rc_deref(&entry->rc))
155 : 1144 : return;
156 : :
157 [ + + - + ]: 320 : if (!entry->mapped && entry->src.discard) {
158 : 0 : PL_TRACE(p, "Discarding unused frame id %"PRIu64" with PTS %f",
159 : : entry->signature, entry->src.pts);
160 : 0 : entry->src.discard(&entry->src);
161 : : }
162 : :
163 [ + + + - : 320 : if (entry->mapped && entry->ok && entry->src.unmap) {
- + ]
164 : 0 : PL_TRACE(p, "Unmapping frame id %"PRIu64" with PTS %f",
165 : : entry->signature, entry->src.pts);
166 : 0 : entry->src.unmap(p->gpu, &entry->frame, &entry->src);
167 : : }
168 : :
169 : 320 : recycle_cache(p, &entry->cache, recycle);
170 : 320 : pl_free(entry);
171 : : }
172 : :
173 : : static struct entry *entry_ref(struct entry *entry)
174 : : {
175 : 480 : pl_rc_ref(&entry->rc);
176 : : return entry;
177 : : }
178 : :
179 : 320 : static void entry_cull(pl_queue p, struct entry *entry, bool recycle)
180 : : {
181 : : // Forcibly clean up references to prev/next frames, even if `entry` has
182 : : // remaining refs pointing at it. This is to prevent cyclic references.
183 : 320 : entry_deref(p, &entry->primary, recycle);
184 : 320 : entry_deref(p, &entry->prev, recycle);
185 : 320 : entry_deref(p, &entry->next, recycle);
186 : 320 : entry_deref(p, &entry, recycle);
187 : 320 : }
188 : :
189 : 4 : void pl_queue_destroy(pl_queue *queue)
190 : : {
191 : 4 : pl_queue p = *queue;
192 [ + - ]: 4 : if (!p)
193 : : return;
194 : :
195 [ - + ]: 4 : for (int n = 0; n < p->queue.num; n++)
196 : 0 : entry_cull(p, p->queue.elem[n], false);
197 [ - + ]: 4 : for (int n = 0; n < p->cache.num; n++) {
198 [ # # ]: 0 : for (int i = 0; i < PL_ARRAY_SIZE(p->cache.elem[n].tex); i++)
199 : 0 : pl_tex_destroy(p->gpu, &p->cache.elem[n].tex[i]);
200 : : }
201 : :
202 : 4 : pl_cond_destroy(&p->wakeup);
203 : 4 : pl_mutex_destroy(&p->lock_weak);
204 : 4 : pl_mutex_destroy(&p->lock_strong);
205 : 4 : pl_free(p);
206 : 4 : *queue = NULL;
207 : : }
208 : :
209 : 12 : void pl_queue_reset(pl_queue p)
210 : : {
211 : 12 : pl_mutex_lock(&p->lock_strong);
212 : 12 : pl_mutex_lock(&p->lock_weak);
213 : :
214 [ + + ]: 20 : for (int i = 0; i < p->queue.num; i++)
215 : 8 : entry_cull(p, p->queue.elem[i], false);
216 : :
217 : 12 : *p = (struct pl_queue_t) {
218 : 12 : .gpu = p->gpu,
219 : 12 : .log = p->log,
220 : :
221 : : // Reuse lock objects
222 : 12 : .lock_strong = p->lock_strong,
223 : 12 : .lock_weak = p->lock_weak,
224 : 12 : .wakeup = p->wakeup,
225 : :
226 : : // Explicitly preserve allocations
227 : 12 : .queue.elem = p->queue.elem,
228 : 12 : .tmp_sig.elem = p->tmp_sig.elem,
229 : 12 : .tmp_ts.elem = p->tmp_ts.elem,
230 : 12 : .tmp_frame.elem = p->tmp_frame.elem,
231 : :
232 : : // Reuse GPU object cache entirely
233 : 12 : .cache = p->cache,
234 : : };
235 : :
236 : 12 : pl_cond_signal(&p->wakeup);
237 : 12 : pl_mutex_unlock(&p->lock_weak);
238 : 12 : pl_mutex_unlock(&p->lock_strong);
239 : 12 : }
240 : :
241 : : static inline float delta(float old, float new)
242 : : {
243 : 3056 : return fabsf((new - old) / PL_MIN(new, old));
244 : : }
245 : :
246 : : static inline void default_estimate(struct pool *pool, float val)
247 : : {
248 [ + - + - : 256 : if (!pool->estimate && isnormal(val) && val > 0.0)
+ + + - +
- ]
249 : 28 : pool->estimate = val;
250 : : }
251 : :
252 : 764 : static inline void update_estimate(struct pool *pool, float cur)
253 : : {
254 [ + + ]: 764 : if (pool->num) {
255 : : static const float max_delta = 0.3;
256 [ + + + + ]: 1024 : if (delta(pool->sum / pool->num, cur) > max_delta) {
257 : 4 : pool->sum = 0.0;
258 : 4 : pool->num = pool->idx = 0;
259 : : }
260 : : }
261 : :
262 [ + + ]: 764 : if (pool->num++ == MAX_SAMPLES) {
263 : 216 : pool->sum -= pool->samples[pool->idx];
264 : 216 : pool->num--;
265 : : }
266 : :
267 : 764 : pool->sum += pool->samples[pool->idx] = cur;
268 : 764 : pool->idx = (pool->idx + 1) % MAX_SAMPLES;
269 : 764 : pool->total++;
270 : :
271 [ + + + + ]: 764 : if (pool->total < MIN_SAMPLES || pool->num >= MIN_SAMPLES)
272 : 760 : pool->estimate = pool->sum / pool->num;
273 : 764 : }
274 : :
275 : 256 : static void queue_push(pl_queue p, const struct pl_source_frame *src)
276 : : {
277 [ - + - - ]: 256 : if (p->eof && !src)
278 : : return; // ignore duplicate EOF
279 : :
280 [ - + - - ]: 256 : if (p->eof && src) {
281 : 0 : PL_INFO(p, "Received frame after EOF signaled... discarding frame!");
282 [ # # ]: 0 : if (src->discard)
283 : 0 : src->discard(src);
284 : 0 : return;
285 : : }
286 : :
287 : 256 : pl_cond_signal(&p->wakeup);
288 : :
289 [ + + ]: 256 : if (!src) {
290 : 16 : PL_TRACE(p, "Received EOF, draining frame queue...");
291 : 16 : p->eof = true;
292 : 16 : p->want_frame = false;
293 : 16 : return;
294 : : }
295 : :
296 : : // Update FPS estimates if possible/reasonable
297 [ + + ]: 240 : default_estimate(&p->fps, src->first_field ? src->duration / 2 : src->duration);
298 [ + + ]: 240 : if (p->queue.num) {
299 : 228 : double last_pts = p->queue.elem[p->queue.num - 1]->pts;
300 : 228 : float delta = src->pts - last_pts;
301 [ + + ]: 228 : if (delta <= 0.0f) {
302 : 64 : PL_DEBUG(p, "Non monotonically increasing PTS %f -> %f", last_pts, src->pts);
303 [ + - + + ]: 164 : } else if (p->fps.estimate && delta > 10.0 * p->fps.estimate) {
304 : 4 : PL_DEBUG(p, "Discontinuous source PTS jump %f -> %f", last_pts, src->pts);
305 : : } else {
306 : 160 : update_estimate(&p->fps, delta);
307 : : }
308 [ - + ]: 12 : } else if (src->pts != 0) {
309 : 0 : PL_DEBUG(p, "First frame received with non-zero PTS %f", src->pts);
310 : : }
311 : :
312 : 240 : struct entry *entry = pl_alloc_ptr(NULL, entry);
313 : 240 : *entry = (struct entry) {
314 : 240 : .signature = p->signature++,
315 : 240 : .pts = src->pts,
316 : 240 : .src = *src,
317 : : };
318 : 240 : pl_rc_init(&entry->rc);
319 [ - + ]: 240 : PL_ARRAY_POP(p->cache, &entry->cache);
320 : 240 : PL_TRACE(p, "Added new frame id %"PRIu64" with PTS %f",
321 : : entry->signature, entry->pts);
322 : :
323 : : // Insert new entry into the correct spot in the queue, sorted by PTS
324 : 672 : for (int i = p->queue.num;; i--) {
325 [ + + + + ]: 672 : if (i == 0 || p->queue.elem[i - 1]->pts <= entry->pts) {
326 [ + + ]: 240 : if (src->first_field == PL_FIELD_NONE) {
327 : : // Progressive
328 [ - + - - : 160 : PL_ARRAY_INSERT_AT(p, p->queue, i, entry);
- + + + +
+ - + ]
329 : 160 : break;
330 : : } else {
331 : : // Interlaced
332 [ + + ]: 80 : struct entry *prev = i > 0 ? p->queue.elem[i - 1] : NULL;
333 [ + + ]: 80 : struct entry *next = i < p->queue.num ? p->queue.elem[i] : NULL;
334 : 80 : struct entry *entry2 = pl_zalloc_ptr(NULL, entry2);
335 : 80 : pl_rc_init(&entry2->rc);
336 [ + + ]: 80 : if (next) {
337 : 32 : entry2->pts = (entry->pts + next->pts) / 2;
338 [ + - ]: 48 : } else if (src->duration) {
339 : 48 : entry2->pts = entry->pts + src->duration / 2;
340 [ # # ]: 0 : } else if (p->fps.estimate) {
341 : 0 : entry2->pts = entry->pts + p->fps.estimate;
342 : : } else {
343 : 0 : PL_ERR(p, "Frame with PTS %f specified as interlaced, but "
344 : : "no FPS information known yet! Please specify a "
345 : : "valid `pl_source_frame.duration`. Treating as "
346 : : "progressive...", src->pts);
347 [ # # # # : 0 : PL_ARRAY_INSERT_AT(p, p->queue, i, entry);
# # # # #
# # # ]
348 : 0 : pl_free(entry2);
349 : 0 : break;
350 : : }
351 : :
352 : 80 : entry->field = src->first_field;
353 : 80 : entry2->primary = entry_ref(entry);
354 [ - - + ]: 80 : entry2->field = pl_field_other(entry->field);
355 : 80 : entry2->signature = p->signature++;
356 : :
357 : 80 : PL_TRACE(p, "Added second field id %"PRIu64" with PTS %f",
358 : : entry2->signature, entry2->pts);
359 : :
360 : : // Link previous/next frames
361 [ + + ]: 80 : if (prev) {
362 [ + - ]: 76 : entry->prev = entry_ref(PL_DEF(prev->primary, prev));
363 [ + - ]: 76 : entry2->prev = entry_ref(PL_DEF(prev->primary, prev));
364 : : // Retroactively re-link the previous frames that should
365 : : // be referencing this frame
366 [ + + ]: 228 : for (int j = i - 1; j >= 0; --j) {
367 : 224 : struct entry *e = p->queue.elem[j];
368 [ + + + + ]: 224 : if (e != prev && e != prev->primary)
369 : : break;
370 : 152 : entry_deref(p, &e->next, true);
371 : 152 : e->next = entry_ref(entry);
372 [ - + ]: 152 : if (e->dirty) { // reset signature to signal change
373 : 0 : e->signature = p->signature++;
374 : 0 : e->dirty = false;
375 : : }
376 : : }
377 : : }
378 : :
379 [ + + ]: 80 : if (next) {
380 [ - + ]: 32 : entry->next = entry_ref(PL_DEF(next->primary, next));
381 [ - + ]: 32 : entry2->next = entry_ref(PL_DEF(next->primary, next));
382 [ + - ]: 64 : for (int j = i; j < p->queue.num; j++) {
383 : 64 : struct entry *e = p->queue.elem[j];
384 [ + + - + ]: 64 : if (e != next && e != next->primary)
385 : : break;
386 : 32 : entry_deref(p, &e->prev, true);
387 : 32 : e->prev = entry_ref(entry);
388 [ - + ]: 32 : if (e->dirty) {
389 : 0 : e->signature = p->signature++;
390 : 0 : e->dirty = false;
391 : : }
392 : : }
393 : : }
394 : :
395 [ - + - - : 80 : PL_ARRAY_INSERT_AT(p, p->queue, i, entry);
- + - + +
+ - + -
+ ]
396 [ - + - - : 80 : PL_ARRAY_INSERT_AT(p, p->queue, i+1, entry2);
- + - + +
+ - + ]
397 : 80 : break;
398 : : }
399 : : }
400 : : }
401 : :
402 : 240 : p->want_frame = false;
403 : : }
404 : :
405 : 248 : void pl_queue_push(pl_queue p, const struct pl_source_frame *frame)
406 : : {
407 : 248 : pl_mutex_lock(&p->lock_weak);
408 : 248 : queue_push(p, frame);
409 : 248 : pl_mutex_unlock(&p->lock_weak);
410 : 248 : }
411 : :
412 : : static inline bool entry_mapped(struct entry *entry)
413 : : {
414 [ - + - - ]: 148 : return entry->mapped || (entry->primary && entry->primary->mapped);
415 : : }
416 : :
417 : 80 : static bool queue_has_room(pl_queue p)
418 : : {
419 [ - + ]: 80 : if (p->want_frame)
420 : : return true;
421 : :
422 : : int wanted_frames = PREFETCH_FRAMES;
423 [ + + - + : 80 : if (p->fps.estimate && p->vps.estimate && p->vps.estimate <= 1.0f / MIN_FPS)
- - ]
424 : 0 : wanted_frames += ceilf(p->vps.estimate / p->fps.estimate) - 1;
425 : :
426 : : // Examine the queue tail
427 [ + + ]: 156 : for (int i = p->queue.num - 1; i >= 0; i--) {
428 [ - + ]: 148 : if (entry_mapped(p->queue.elem[i]))
429 : : return true;
430 [ + + ]: 148 : if (p->queue.num - i >= wanted_frames)
431 : : return false;
432 : : }
433 : :
434 : : return true;
435 : : }
436 : :
437 : 80 : bool pl_queue_push_block(pl_queue p, uint64_t timeout,
438 : : const struct pl_source_frame *frame)
439 : : {
440 : 80 : pl_mutex_lock(&p->lock_weak);
441 [ + - - + ]: 80 : if (!timeout || !frame || p->eof)
442 : 0 : goto skip_blocking;
443 : :
444 [ + + + - ]: 80 : while (!queue_has_room(p) && !p->eof) {
445 [ + - ]: 72 : if (pl_cond_timedwait(&p->wakeup, &p->lock_weak, timeout) == ETIMEDOUT) {
446 : 72 : pl_mutex_unlock(&p->lock_weak);
447 : 72 : return false;
448 : : }
449 : : }
450 : :
451 : 8 : skip_blocking:
452 : :
453 : 8 : queue_push(p, frame);
454 : 8 : pl_mutex_unlock(&p->lock_weak);
455 : 8 : return true;
456 : : }
457 : :
458 : 604 : static void report_estimates(pl_queue p)
459 : : {
460 [ + + + + ]: 604 : if (p->fps.total >= MIN_SAMPLES && p->vps.total >= MIN_SAMPLES) {
461 [ + + + - ]: 540 : if (p->reported_fps && p->reported_vps) {
462 : : // Only re-report the estimates if they've changed considerably
463 : : // from the previously reported values
464 : : static const float report_delta = 0.3f;
465 [ + - ]: 528 : float delta_fps = delta(p->reported_fps, p->fps.estimate);
466 [ + - ]: 528 : float delta_vps = delta(p->reported_vps, p->vps.estimate);
467 [ + - - + ]: 528 : if (delta_fps < report_delta && delta_vps < report_delta)
468 : : return;
469 : : }
470 : :
471 : 12 : PL_INFO(p, "Estimated source FPS: %.3f, display FPS: %.3f",
472 : : 1.0 / p->fps.estimate, 1.0 / p->vps.estimate);
473 : :
474 : 12 : p->reported_fps = p->fps.estimate;
475 : 12 : p->reported_vps = p->vps.estimate;
476 : : }
477 : : }
478 : :
479 : : // note: may add more than one frame, since it releases the lock
480 : 148 : static enum pl_queue_status get_frame(pl_queue p, const struct pl_queue_params *params)
481 : : {
482 [ + + ]: 148 : if (p->eof)
483 : : return PL_QUEUE_EOF;
484 : :
485 [ + + ]: 92 : if (!params->get_frame) {
486 [ + - ]: 4 : if (!params->timeout)
487 : : return PL_QUEUE_MORE;
488 : :
489 : 0 : p->want_frame = true;
490 : 0 : pl_cond_signal(&p->wakeup);
491 : :
492 [ # # ]: 0 : while (p->want_frame) {
493 [ # # ]: 0 : if (pl_cond_timedwait(&p->wakeup, &p->lock_weak, params->timeout) == ETIMEDOUT)
494 : : return PL_QUEUE_MORE;
495 : : }
496 : :
497 : 0 : return p->eof ? PL_QUEUE_EOF : PL_QUEUE_OK;
498 : : }
499 : :
500 : : // Don't hold the weak mutex while calling into `get_frame`, to allow
501 : : // `pl_queue_push` to run concurrently while we're waiting for frames
502 : 88 : pl_mutex_unlock(&p->lock_weak);
503 : :
504 : : struct pl_source_frame src;
505 : : enum pl_queue_status ret;
506 [ + + - ]: 88 : switch ((ret = params->get_frame(&src, params))) {
507 : 80 : case PL_QUEUE_OK:
508 : 80 : pl_queue_push(p, &src);
509 : 80 : break;
510 : 8 : case PL_QUEUE_EOF:
511 : 8 : pl_queue_push(p, NULL);
512 : 8 : break;
513 : : case PL_QUEUE_MORE:
514 : : case PL_QUEUE_ERR:
515 : : break;
516 : : }
517 : :
518 : 88 : pl_mutex_lock(&p->lock_weak);
519 : 88 : return ret;
520 : : }
521 : :
522 : 2512 : static inline bool map_frame(pl_queue p, struct entry *entry)
523 : : {
524 [ + + ]: 2512 : if (!entry->mapped) {
525 : 240 : PL_TRACE(p, "Mapping frame id %"PRIu64" with PTS %f",
526 : : entry->signature, entry->pts);
527 : 240 : entry->mapped = true;
528 : 480 : entry->ok = entry->src.map(p->gpu, entry->cache.tex,
529 : 240 : &entry->src, &entry->frame);
530 [ - + ]: 240 : if (!entry->ok)
531 : 0 : PL_ERR(p, "Failed mapping frame id %"PRIu64" with PTS %f",
532 : : entry->signature, entry->pts);
533 : : }
534 : :
535 : 2512 : return entry->ok;
536 : : }
537 : :
538 : 1756 : static bool map_entry(pl_queue p, struct entry *entry)
539 : : {
540 [ + + ]: 1956 : bool ok = map_frame(p, entry->primary ? entry->primary : entry);
541 [ + + ]: 1756 : if (entry->prev)
542 : 376 : ok &= map_frame(p, entry->prev);
543 [ + + ]: 1756 : if (entry->next)
544 : 380 : ok &= map_frame(p, entry->next);
545 [ + - ]: 1756 : if (!ok)
546 : : return false;
547 : :
548 [ + + ]: 1756 : if (entry->primary)
549 : 200 : entry->frame = entry->primary->frame;
550 : :
551 [ + + ]: 1756 : if (entry->field) {
552 : 396 : entry->frame.field = entry->field;
553 [ + + ]: 396 : entry->frame.first_field = PL_DEF(entry->primary, entry)->src.first_field;
554 [ + + ]: 396 : entry->frame.prev = entry->prev ? &entry->prev->frame : NULL;
555 [ + + ]: 396 : entry->frame.next = entry->next ? &entry->next->frame : NULL;
556 : 396 : entry->dirty = true;
557 : : }
558 : :
559 : : return true;
560 : : }
561 : :
562 : : static bool entry_complete(struct entry *entry)
563 : : {
564 [ - + - - : 368 : return entry->field ? !!entry->next : true;
+ + ]
565 : : }
566 : :
567 : : // Advance the queue as needed to make sure idx 0 is the last frame before
568 : : // `pts`, and idx 1 is the first frame after `pts` (unless this is the last).
569 : : //
570 : : // Returns PL_QUEUE_OK only if idx 0 is still legal under ZOH semantics.
571 : 620 : static enum pl_queue_status advance(pl_queue p, double pts,
572 : : const struct pl_queue_params *params)
573 : : {
574 : : // Cull all frames except the last frame before `pts`
575 : : int culled = 0;
576 [ + + ]: 7320 : for (int i = 1; i < p->queue.num; i++) {
577 [ + + ]: 6700 : if (p->queue.elem[i]->pts <= pts) {
578 : 304 : entry_cull(p, p->queue.elem[i - 1], true);
579 : 304 : culled++;
580 : : }
581 : : }
582 [ - + ]: 620 : PL_ARRAY_REMOVE_RANGE(p->queue, 0, culled);
583 : :
584 : : // Keep adding new frames until we find one in the future, or EOF
585 : : enum pl_queue_status ret = PL_QUEUE_OK;
586 [ + + ]: 1312 : while (p->queue.num < 2) {
587 [ + + - - ]: 96 : switch ((ret = get_frame(p, params))) {
588 : : case PL_QUEUE_ERR:
589 : : return ret;
590 : 24 : case PL_QUEUE_EOF:
591 [ + + ]: 24 : if (!p->queue.num)
592 : : return ret;
593 : 20 : goto done;
594 : : case PL_QUEUE_MORE:
595 : : case PL_QUEUE_OK:
596 [ + - - + ]: 72 : while (p->queue.num > 1 && p->queue.elem[1]->pts <= pts) {
597 : 0 : entry_cull(p, p->queue.elem[0], true);
598 [ # # ]: 0 : PL_ARRAY_REMOVE_AT(p->queue, 0);
599 : : }
600 [ + - ]: 72 : if (ret == PL_QUEUE_MORE)
601 : : return ret;
602 : 72 : continue;
603 : : }
604 : : }
605 : :
606 [ + + + + ]: 1192 : if (!entry_complete(p->queue.elem[1])) {
607 [ - - + - ]: 8 : switch (get_frame(p, params)) {
608 : : case PL_QUEUE_ERR:
609 : : return PL_QUEUE_ERR;
610 : 0 : case PL_QUEUE_MORE:
611 : : ret = PL_QUEUE_MORE;
612 : : // fall through
613 : 8 : case PL_QUEUE_EOF:
614 : : case PL_QUEUE_OK:
615 : 8 : goto done;
616 : : }
617 : : }
618 : :
619 : 588 : done:
620 [ + + + + ]: 616 : if (p->eof && p->queue.num == 1) {
621 [ + - + - ]: 20 : if (p->queue.elem[0]->pts == 0.0 || !p->fps.estimate) {
622 : : // If the last frame has PTS 0.0, or we have no FPS estimate, then
623 : : // this is probably a single-frame file, in which case we want to
624 : : // extend the ZOH to infinity, rather than returning. Not a perfect
625 : : // heuristic, but w/e
626 : : return PL_QUEUE_OK;
627 : : }
628 : :
629 : : // Last frame is held for an extra `p->fps.estimate` duration,
630 : : // afterwards this function just returns EOF.
631 [ + + ]: 20 : if (pts < p->queue.elem[0]->pts + p->fps.estimate) {
632 : : ret = PL_QUEUE_OK;
633 : : } else {
634 : 8 : entry_cull(p, p->queue.elem[0], true);
635 : 8 : p->queue.num = 0;
636 : 8 : return PL_QUEUE_EOF;
637 : : }
638 : : }
639 : :
640 [ - + ]: 608 : pl_assert(p->queue.num);
641 : : return ret;
642 : : }
643 : :
644 : 12 : static inline enum pl_queue_status point(pl_queue p, struct pl_frame_mix *mix,
645 : : const struct pl_queue_params *params)
646 : : {
647 [ - + ]: 12 : if (!p->queue.num) {
648 : 0 : *mix = (struct pl_frame_mix) {0};
649 : : return PL_QUEUE_MORE;
650 : : }
651 : :
652 : : // Find closest frame (nearest neighbour semantics)
653 : 12 : struct entry *entry = p->queue.elem[0];
654 [ - + ]: 12 : if (entry->pts > params->pts) { // first frame not visible yet
655 : 0 : *mix = (struct pl_frame_mix) {0};
656 : : return PL_QUEUE_OK;
657 : : }
658 : :
659 : 12 : double best = fabs(entry->pts - params->pts);
660 [ - + ]: 12 : for (int i = 1; i < p->queue.num; i++) {
661 : 0 : double dist = fabs(p->queue.elem[i]->pts - params->pts);
662 [ # # ]: 0 : if (dist < best) {
663 : : entry = p->queue.elem[i];
664 : : best = dist;
665 : : continue;
666 : : } else {
667 : : break;
668 : : }
669 : : }
670 : :
671 [ + - ]: 12 : if (!map_entry(p, entry))
672 : : return PL_QUEUE_ERR;
673 : :
674 : : // Return a mix containing only this single frame
675 : 12 : p->tmp_sig.num = p->tmp_ts.num = p->tmp_frame.num = 0;
676 [ - + - + : 12 : PL_ARRAY_APPEND(p, p->tmp_sig, entry->signature);
- + ]
677 [ - + - + : 12 : PL_ARRAY_APPEND(p, p->tmp_frame, &entry->frame);
- + ]
678 [ - + - + : 12 : PL_ARRAY_APPEND(p, p->tmp_ts, 0.0);
- + ]
679 : 12 : *mix = (struct pl_frame_mix) {
680 : : .num_frames = 1,
681 : 12 : .frames = p->tmp_frame.elem,
682 : 12 : .signatures = p->tmp_sig.elem,
683 : : .timestamps = p->tmp_ts.elem,
684 : : .vsync_duration = 1.0,
685 : : };
686 : :
687 : 12 : PL_TRACE(p, "Showing single frame id %"PRIu64" with PTS %f for target PTS %f",
688 : : entry->signature, entry->pts, params->pts);
689 : :
690 : 12 : report_estimates(p);
691 : 12 : return PL_QUEUE_OK;
692 : : }
693 : :
694 : : // Present a single frame as appropriate for `pts`
695 : 4 : static enum pl_queue_status nearest(pl_queue p, struct pl_frame_mix *mix,
696 : : const struct pl_queue_params *params)
697 : : {
698 : : enum pl_queue_status ret;
699 [ - - + ]: 4 : switch ((ret = advance(p, params->pts, params))) {
700 : : case PL_QUEUE_ERR:
701 : : case PL_QUEUE_EOF:
702 : : return ret;
703 : 0 : case PL_QUEUE_OK:
704 : : case PL_QUEUE_MORE:
705 [ # # # # ]: 0 : if (mix && point(p, mix, params) == PL_QUEUE_ERR)
706 : : return PL_QUEUE_ERR;
707 : : return ret;
708 : : }
709 : :
710 : 0 : pl_unreachable();
711 : : }
712 : :
713 : : // Special case of `interpolate` for radius = 0, in which case we need exactly
714 : : // the previous frame and the following frame
715 : 408 : static enum pl_queue_status oversample(pl_queue p, struct pl_frame_mix *mix,
716 : : const struct pl_queue_params *params)
717 : : {
718 : : enum pl_queue_status ret;
719 [ + - + ]: 408 : switch ((ret = advance(p, params->pts, params))) {
720 : : case PL_QUEUE_ERR:
721 : : case PL_QUEUE_EOF:
722 : : return ret;
723 : : case PL_QUEUE_OK:
724 : : break;
725 : 0 : case PL_QUEUE_MORE:
726 [ # # ]: 0 : if (!p->queue.num) {
727 [ # # ]: 0 : if (mix)
728 : 0 : *mix = (struct pl_frame_mix) {0};
729 : 0 : return ret;
730 : : }
731 : : break;
732 : : }
733 : :
734 [ + - ]: 400 : if (!mix)
735 : : return PL_QUEUE_OK;
736 : :
737 : : // Can't oversample with only a single frame, fall back to point sampling
738 [ + + - + ]: 400 : if (p->queue.num < 2 || p->queue.elem[0]->pts > params->pts) {
739 [ + - ]: 12 : if (point(p, mix, params) != PL_QUEUE_OK)
740 : : return PL_QUEUE_ERR;
741 : : return ret;
742 : : }
743 : :
744 : 388 : struct entry *entries[2] = { p->queue.elem[0], p->queue.elem[1] };
745 [ - + ]: 388 : pl_assert(entries[0]->pts <= params->pts);
746 [ - + ]: 388 : pl_assert(entries[1]->pts >= params->pts);
747 : :
748 : : // Returning a mix containing both of these two frames
749 : 388 : p->tmp_sig.num = p->tmp_ts.num = p->tmp_frame.num = 0;
750 [ + + ]: 1164 : for (int i = 0; i < 2; i++) {
751 [ - + ]: 776 : if (!map_entry(p, entries[i]))
752 : : return PL_QUEUE_ERR;
753 : 776 : float ts = (entries[i]->pts - params->pts) / p->fps.estimate;
754 [ - + - + : 776 : PL_ARRAY_APPEND(p, p->tmp_sig, entries[i]->signature);
- + ]
755 [ - + - + : 776 : PL_ARRAY_APPEND(p, p->tmp_frame, &entries[i]->frame);
- + ]
756 [ - + - + : 776 : PL_ARRAY_APPEND(p, p->tmp_ts, ts);
- + ]
757 : : }
758 : :
759 : 388 : *mix = (struct pl_frame_mix) {
760 : : .num_frames = 2,
761 : 388 : .frames = p->tmp_frame.elem,
762 : 388 : .signatures = p->tmp_sig.elem,
763 : 388 : .timestamps = p->tmp_ts.elem,
764 : 388 : .vsync_duration = p->vps.estimate / p->fps.estimate,
765 : : };
766 : :
767 : 388 : PL_TRACE(p, "Oversampling 2 frames for target PTS %f:", params->pts);
768 [ + + ]: 1164 : for (int i = 0; i < mix->num_frames; i++)
769 : 776 : PL_TRACE(p, " id %"PRIu64" ts %f", mix->signatures[i], mix->timestamps[i]);
770 : :
771 : 388 : report_estimates(p);
772 : 388 : return ret;
773 : : }
774 : :
775 : : // Present a mixture of frames, relative to the vsync ratio
776 : 620 : static enum pl_queue_status interpolate(pl_queue p, struct pl_frame_mix *mix,
777 : : const struct pl_queue_params *params)
778 : : {
779 : : // No FPS estimate available, possibly source contains only a single frame,
780 : : // or this is the first frame to be rendered. Fall back to point sampling.
781 [ + + ]: 620 : if (!p->fps.estimate)
782 : 4 : return nearest(p, mix, params);
783 : :
784 : : // Silently disable interpolation if the ratio dips lower than the
785 : : // configured threshold
786 : 616 : float ratio = fabs(p->fps.estimate / p->vps.estimate - 1.0);
787 [ - + ]: 616 : if (ratio < params->interpolation_threshold) {
788 [ # # ]: 0 : if (!p->threshold_frames) {
789 : 0 : PL_INFO(p, "Detected fps ratio %.4f below threshold %.4f, "
790 : : "disabling interpolation",
791 : : ratio, params->interpolation_threshold);
792 : : }
793 : :
794 : 0 : p->threshold_frames = THRESHOLD_FRAMES + 1;
795 : 0 : return nearest(p, mix, params);
796 [ + + - + ]: 616 : } else if (ratio < THRESHOLD_MAX_RATIO && p->threshold_frames > 1) {
797 : 0 : p->threshold_frames--;
798 : 0 : return nearest(p, mix, params);
799 : : } else {
800 [ - + ]: 616 : if (p->threshold_frames) {
801 : 0 : PL_INFO(p, "Detected fps ratio %.4f exceeds threshold %.4f, "
802 : : "re-enabling interpolation",
803 : : ratio, params->interpolation_threshold);
804 : : }
805 : 616 : p->threshold_frames = 0;
806 : : }
807 : :
808 : : // No radius information, special case in which we only need the previous
809 : : // and next frames.
810 [ + + ]: 616 : if (!params->radius)
811 : 408 : return oversample(p, mix, params);
812 : :
813 [ + - - + ]: 208 : pl_assert(p->fps.estimate && p->vps.estimate);
814 : 208 : float radius = params->radius * fmaxf(1.0f, p->vps.estimate / p->fps.estimate);
815 : 208 : double min_pts = params->pts - radius * p->fps.estimate,
816 : 208 : max_pts = params->pts + radius * p->fps.estimate;
817 : :
818 : : enum pl_queue_status ret;
819 [ - + - ]: 208 : switch ((ret = advance(p, min_pts, params))) {
820 : : case PL_QUEUE_ERR:
821 : : case PL_QUEUE_EOF:
822 : : return ret;
823 : 0 : case PL_QUEUE_MORE:
824 : 0 : goto done;
825 : : case PL_QUEUE_OK:
826 : : break;
827 : : }
828 : :
829 : : // Keep adding new frames until we've covered the range we care about
830 [ - + ]: 208 : pl_assert(p->queue.num);
831 [ + + ]: 208 : while (p->queue.elem[p->queue.num - 1]->pts < max_pts) {
832 [ - + + - : 36 : switch ((ret = get_frame(p, params))) {
- ]
833 : : case PL_QUEUE_ERR:
834 : : return ret;
835 : 4 : case PL_QUEUE_MORE:
836 : 4 : goto done;
837 : 32 : case PL_QUEUE_EOF:;
838 : : // Don't forward EOF until we've held the last frame for the
839 : : // desired ZOH hold duration
840 : 32 : double last_pts = p->queue.elem[p->queue.num - 1]->pts;
841 [ + - + + ]: 32 : if (last_pts && params->pts >= last_pts + p->fps.estimate)
842 : : return ret;
843 : : ret = PL_QUEUE_OK;
844 : 28 : goto done;
845 : 0 : case PL_QUEUE_OK:
846 : 0 : continue;
847 : : }
848 : : }
849 : :
850 [ + - ]: 172 : if (!entry_complete(p->queue.elem[p->queue.num - 1])) {
851 [ # # ]: 0 : switch ((ret = get_frame(p, params))) {
852 : : case PL_QUEUE_MORE:
853 : : case PL_QUEUE_OK:
854 : : break;
855 : : case PL_QUEUE_ERR:
856 : : case PL_QUEUE_EOF:
857 : : return ret;
858 : : }
859 : : }
860 : :
861 : 172 : done: ;
862 : :
863 [ + - ]: 204 : if (!mix)
864 : : return PL_QUEUE_OK;
865 : :
866 : : // Construct a mix object representing the current queue state, starting at
867 : : // the last frame before `min_pts` to make sure there's a fallback frame
868 : : // available for ZOH semantics.
869 : 204 : p->tmp_sig.num = p->tmp_ts.num = p->tmp_frame.num = 0;
870 [ + + ]: 1164 : for (int i = 0; i < p->queue.num; i++) {
871 : 1132 : struct entry *entry = p->queue.elem[i];
872 [ + + ]: 1132 : if (entry->pts > max_pts)
873 : : break;
874 [ - + ]: 960 : if (!map_entry(p, entry))
875 : : return PL_QUEUE_ERR;
876 : 960 : float ts = (entry->pts - params->pts) / p->fps.estimate;
877 [ + + - + : 960 : PL_ARRAY_APPEND(p, p->tmp_sig, entry->signature);
- + ]
878 [ + + - + : 960 : PL_ARRAY_APPEND(p, p->tmp_frame, &entry->frame);
- + ]
879 [ + + - + : 960 : PL_ARRAY_APPEND(p, p->tmp_ts, ts);
- + ]
880 : : }
881 : :
882 : 204 : *mix = (struct pl_frame_mix) {
883 : 204 : .num_frames = p->tmp_frame.num,
884 : 204 : .frames = p->tmp_frame.elem,
885 : 204 : .signatures = p->tmp_sig.elem,
886 : 204 : .timestamps = p->tmp_ts.elem,
887 : 204 : .vsync_duration = p->vps.estimate / p->fps.estimate,
888 : : };
889 : :
890 : 204 : PL_TRACE(p, "Showing mix of %d frames for target PTS %f:",
891 : : mix->num_frames, params->pts);
892 [ + + ]: 1164 : for (int i = 0; i < mix->num_frames; i++)
893 : 960 : PL_TRACE(p, " id %"PRIu64" ts %f", mix->signatures[i], mix->timestamps[i]);
894 : :
895 : 204 : report_estimates(p);
896 : 204 : return ret;
897 : : }
898 : :
899 : 4 : static bool prefill(pl_queue p, const struct pl_queue_params *params)
900 : : {
901 : 4 : int min_frames = 2 * ceilf(params->radius);
902 [ - + - - : 4 : if (p->fps.estimate && p->vps.estimate && p->vps.estimate <= 1.0f / MIN_FPS)
- - ]
903 : 0 : min_frames *= ceilf(p->vps.estimate / p->fps.estimate);
904 : 4 : min_frames = PL_MAX(min_frames, PREFETCH_FRAMES);
905 : :
906 [ + + ]: 12 : while (p->queue.num < min_frames) {
907 [ - - + - ]: 8 : switch (get_frame(p, params)) {
908 : : case PL_QUEUE_ERR:
909 : : return false;
910 : : case PL_QUEUE_EOF:
911 : : case PL_QUEUE_MORE:
912 : : return true;
913 : 8 : case PL_QUEUE_OK:
914 : 8 : continue;
915 : : }
916 : : }
917 : :
918 : : // In the most likely case, the first few frames will all be required. So
919 : : // force-map them all to initialize GPU state on initial rendering. This is
920 : : // better than the alternative of missing the cache later, when timing is
921 : : // more relevant.
922 [ + + ]: 12 : for (int i = 0; i < min_frames; i++) {
923 [ - + ]: 8 : if (!map_entry(p, p->queue.elem[i]))
924 : : return false;
925 : : }
926 : :
927 : : return true;
928 : : }
929 : :
930 : 620 : enum pl_queue_status pl_queue_update(pl_queue p, struct pl_frame_mix *out_mix,
931 : : const struct pl_queue_params *params)
932 : : {
933 : : struct pl_queue_params fixed;
934 : 620 : pl_mutex_lock(&p->lock_strong);
935 : 620 : pl_mutex_lock(&p->lock_weak);
936 [ + + ]: 620 : default_estimate(&p->vps, params->vsync_duration);
937 : :
938 : 620 : float delta = params->pts - p->prev_pts;
939 [ - + ]: 620 : if (delta < 0.0f) {
940 : :
941 : : // This is a backwards PTS jump. This is something we can handle
942 : : // semi-gracefully, but only if we haven't culled past the current
943 : : // frame yet.
944 [ # # # # ]: 0 : if (p->queue.num && p->queue.elem[0]->pts > params->pts) {
945 : 0 : PL_ERR(p, "Requested PTS %f is lower than the oldest frame "
946 : : "PTS %f. This is not supported, PTS must be monotonically "
947 : : "increasing! Please use `pl_queue_reset` to reset the frame "
948 : : "queue on discontinuous PTS jumps.",
949 : : params->pts, p->queue.elem[0]->pts);
950 : 0 : pl_mutex_unlock(&p->lock_weak);
951 : 0 : pl_mutex_unlock(&p->lock_strong);
952 : 0 : return PL_QUEUE_ERR;
953 : : }
954 : :
955 [ - + ]: 620 : } else if (delta > 1.0f) {
956 : :
957 : : // A jump of more than a second is probably the result of a
958 : : // discontinuous jump after a suspend. To prevent this from exploding
959 : : // the FPS estimate, treat this as a new frame.
960 : 0 : PL_TRACE(p, "Discontinuous target PTS jump %f -> %f, ignoring...",
961 : : p->prev_pts, params->pts);
962 : 0 : p->pts_offset = 0.0;
963 : :
964 [ + + ]: 620 : } else if (delta > 0) {
965 : :
966 : 604 : update_estimate(&p->vps, params->pts - p->prev_pts);
967 : :
968 : : }
969 : :
970 : 620 : p->prev_pts = params->pts;
971 : :
972 [ - + ]: 620 : if (params->drift_compensation > 0.0f) {
973 : : // Adjust PTS offset if PTS is near-match for existing frame
974 : 0 : double pts = params->pts + p->pts_offset;
975 [ # # ]: 0 : for (int i = 0; i < p->queue.num; i++) {
976 [ # # ]: 0 : if (fabs(p->queue.elem[i]->pts - pts) < params->drift_compensation) {
977 : 0 : p->pts_offset = p->queue.elem[i]->pts - params->pts;
978 : : pts = p->queue.elem[i]->pts;
979 : 0 : break;
980 : : }
981 : : }
982 : :
983 : 0 : fixed = *params;
984 : 0 : fixed.pts = pts;
985 : : params = &fixed;
986 : : }
987 : :
988 : : // As a special case, prefill the queue if this is the first frame
989 [ + + + + ]: 620 : if (!params->pts && !p->queue.num) {
990 [ - + ]: 4 : if (!prefill(p, params)) {
991 : 0 : pl_mutex_unlock(&p->lock_weak);
992 : 0 : pl_mutex_unlock(&p->lock_strong);
993 : 0 : return PL_QUEUE_ERR;
994 : : }
995 : : }
996 : :
997 : : // Ignore unrealistically high or low FPS, common near start of playback
998 : : static const float max_vsync = 1.0 / MIN_FPS;
999 : : static const float min_vsync = 1.0 / MAX_FPS;
1000 [ + - + + ]: 620 : bool estimation_ok = p->vps.estimate > min_vsync && p->vps.estimate < max_vsync;
1001 : : enum pl_queue_status ret;
1002 : :
1003 [ + - ]: 4 : if (estimation_ok || params->vsync_duration > 0) {
1004 : : // We know the vsync duration, so construct an interpolation mix
1005 : 620 : ret = interpolate(p, out_mix, params);
1006 : : } else {
1007 : : // We don't know the vsync duration (yet), so just point-sample
1008 : 0 : ret = nearest(p, out_mix, params);
1009 : : }
1010 : :
1011 : 620 : pl_cond_signal(&p->wakeup);
1012 : 620 : pl_mutex_unlock(&p->lock_weak);
1013 : 620 : pl_mutex_unlock(&p->lock_strong);
1014 : 620 : return ret;
1015 : : }
1016 : :
1017 : 0 : float pl_queue_estimate_fps(pl_queue p)
1018 : : {
1019 : 0 : pl_mutex_lock(&p->lock_weak);
1020 : 0 : float estimate = p->fps.estimate;
1021 : 0 : pl_mutex_unlock(&p->lock_weak);
1022 [ # # ]: 0 : return estimate ? 1.0f / estimate : 0.0f;
1023 : : }
1024 : :
1025 : 0 : float pl_queue_estimate_vps(pl_queue p)
1026 : : {
1027 : 0 : pl_mutex_lock(&p->lock_weak);
1028 : 0 : float estimate = p->vps.estimate;
1029 : 0 : pl_mutex_unlock(&p->lock_weak);
1030 [ # # ]: 0 : return estimate ? 1.0f / estimate : 0.0f;
1031 : : }
1032 : :
1033 : 0 : int pl_queue_num_frames(pl_queue p)
1034 : : {
1035 : 0 : pl_mutex_lock(&p->lock_weak);
1036 : 0 : int count = p->queue.num;
1037 : 0 : pl_mutex_unlock(&p->lock_weak);
1038 : 0 : return count;
1039 : : }
1040 : :
1041 : 0 : double pl_queue_pts_offset(pl_queue p)
1042 : : {
1043 : 0 : pl_mutex_lock(&p->lock_weak);
1044 : 0 : double offset = p->pts_offset;
1045 : 0 : pl_mutex_unlock(&p->lock_weak);
1046 : 0 : return offset;
1047 : : }
1048 : :
1049 : 0 : bool pl_queue_peek(pl_queue p, int idx, struct pl_source_frame *out)
1050 : : {
1051 : 0 : pl_mutex_lock(&p->lock_weak);
1052 [ # # # # ]: 0 : bool ok = idx >= 0 && idx < p->queue.num;
1053 : : if (ok)
1054 : 0 : *out = p->queue.elem[idx]->src;
1055 : 0 : pl_mutex_unlock(&p->lock_weak);
1056 : 0 : return ok;
1057 : : }
|