/* * This file is part of libplacebo. * * libplacebo is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * libplacebo is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with libplacebo. If not, see . */ #include #include #include "common.h" #include "log.h" #include "pl_thread.h" #include struct cache_entry { pl_tex tex[4]; }; struct entry { pl_rc_t rc; double pts; struct cache_entry cache; struct pl_source_frame src; struct pl_frame frame; uint64_t signature; bool mapped; bool ok; // for interlaced frames enum pl_field field; struct entry *primary; struct entry *prev, *next; bool dirty; }; // Hard limits for vsync timing validity #define MIN_FPS 10 #define MAX_FPS 400 // Limits for FPS estimation state #define MAX_SAMPLES 32 #define MIN_SAMPLES 4 // Stickiness to prevent `interpolation_threshold` oscillation #define THRESHOLD_MAX_RATIO 0.3 #define THRESHOLD_FRAMES 5 // Maximum number of not-yet-mapped frames to allow queueing in advance #define PREFETCH_FRAMES 2 struct pool { float samples[MAX_SAMPLES]; float estimate; float sum; int idx; int num; int total; }; struct pl_queue_t { pl_gpu gpu; pl_log log; // For multi-threading, we use two locks. The `lock_weak` guards the queue // state itself. The `lock_strong` has a bigger scope and should be held // for the duration of any functions that expect the queue state to // remain more or less valid (with the exception of adding new members). // // In particular, `pl_queue_reset` and `pl_queue_update` will take // the strong lock, while `pl_queue_push_*` will only take the weak // lock. pl_mutex lock_strong; pl_mutex lock_weak; pl_cond wakeup; // Frame queue and state PL_ARRAY(struct entry *) queue; uint64_t signature; int threshold_frames; bool want_frame; bool eof; // Average vsync/frame fps estimation state struct pool vps, fps; float reported_vps; float reported_fps; double prev_pts; // Storage for temporary arrays PL_ARRAY(uint64_t) tmp_sig; PL_ARRAY(float) tmp_ts; PL_ARRAY(const struct pl_frame *) tmp_frame; // Queue of GPU objects to reuse PL_ARRAY(struct cache_entry) cache; }; pl_queue pl_queue_create(pl_gpu gpu) { pl_queue p = pl_alloc_ptr(NULL, p); *p = (struct pl_queue_t) { .gpu = gpu, .log = gpu->log, }; pl_mutex_init(&p->lock_strong); pl_mutex_init(&p->lock_weak); int ret = pl_cond_init(&p->wakeup); if (ret) { PL_ERR(p, "Failed to init conditional variable: %d", ret); return NULL; } return p; } static void recycle_cache(pl_queue p, struct cache_entry *cache, bool recycle) { bool has_textures = false; for (int i = 0; i < PL_ARRAY_SIZE(cache->tex); i++) { if (!cache->tex[i]) continue; has_textures = true; if (recycle) { pl_tex_invalidate(p->gpu, cache->tex[i]); } else { pl_tex_destroy(p->gpu, &cache->tex[i]); } } if (recycle && has_textures) PL_ARRAY_APPEND(p, p->cache, *cache); memset(cache, 0, sizeof(*cache)); // sanity } static void entry_deref(pl_queue p, struct entry **pentry, bool recycle) { struct entry *entry = *pentry; *pentry = NULL; if (!entry || !pl_rc_deref(&entry->rc)) return; if (!entry->mapped && entry->src.discard) { PL_TRACE(p, "Discarding unused frame id %"PRIu64" with PTS %f", entry->signature, entry->src.pts); entry->src.discard(&entry->src); } if (entry->mapped && entry->ok && entry->src.unmap) { PL_TRACE(p, "Unmapping frame id %"PRIu64" with PTS %f", entry->signature, entry->src.pts); entry->src.unmap(p->gpu, &entry->frame, &entry->src); } recycle_cache(p, &entry->cache, recycle); pl_free(entry); } static struct entry *entry_ref(struct entry *entry) { pl_rc_ref(&entry->rc); return entry; } static void entry_cull(pl_queue p, struct entry *entry, bool recycle) { // Forcibly clean up references to prev/next frames, even if `entry` has // remaining refs pointing at it. This is to prevent cyclic references. entry_deref(p, &entry->primary, recycle); entry_deref(p, &entry->prev, recycle); entry_deref(p, &entry->next, recycle); entry_deref(p, &entry, recycle); } void pl_queue_destroy(pl_queue *queue) { pl_queue p = *queue; if (!p) return; for (int n = 0; n < p->queue.num; n++) entry_cull(p, p->queue.elem[n], false); for (int n = 0; n < p->cache.num; n++) { for (int i = 0; i < PL_ARRAY_SIZE(p->cache.elem[n].tex); i++) pl_tex_destroy(p->gpu, &p->cache.elem[n].tex[i]); } pl_cond_destroy(&p->wakeup); pl_mutex_destroy(&p->lock_weak); pl_mutex_destroy(&p->lock_strong); pl_free(p); *queue = NULL; } void pl_queue_reset(pl_queue p) { pl_mutex_lock(&p->lock_strong); pl_mutex_lock(&p->lock_weak); for (int i = 0; i < p->queue.num; i++) entry_cull(p, p->queue.elem[i], false); *p = (struct pl_queue_t) { .gpu = p->gpu, .log = p->log, // Reuse lock objects .lock_strong = p->lock_strong, .lock_weak = p->lock_weak, .wakeup = p->wakeup, // Explicitly preserve allocations .queue.elem = p->queue.elem, .tmp_sig.elem = p->tmp_sig.elem, .tmp_ts.elem = p->tmp_ts.elem, .tmp_frame.elem = p->tmp_frame.elem, // Reuse GPU object cache entirely .cache = p->cache, }; pl_cond_signal(&p->wakeup); pl_mutex_unlock(&p->lock_weak); pl_mutex_unlock(&p->lock_strong); } static inline float delta(float old, float new) { return fabsf((new - old) / PL_MIN(new, old)); } static inline void default_estimate(struct pool *pool, float val) { if (!pool->estimate && isnormal(val) && val > 0.0) pool->estimate = val; } static inline void update_estimate(struct pool *pool, float cur) { if (pool->num) { static const float max_delta = 0.3; if (delta(pool->sum / pool->num, cur) > max_delta) { pool->sum = 0.0; pool->num = pool->idx = 0; } } if (pool->num++ == MAX_SAMPLES) { pool->sum -= pool->samples[pool->idx]; pool->num--; } pool->sum += pool->samples[pool->idx] = cur; pool->idx = (pool->idx + 1) % MAX_SAMPLES; pool->total++; if (pool->total < MIN_SAMPLES || pool->num >= MIN_SAMPLES) pool->estimate = pool->sum / pool->num; } static void queue_push(pl_queue p, const struct pl_source_frame *src) { if (p->eof && !src) return; // ignore duplicate EOF if (p->eof && src) { PL_INFO(p, "Received frame after EOF signaled... discarding frame!"); if (src->discard) src->discard(src); return; } pl_cond_signal(&p->wakeup); if (!src) { PL_TRACE(p, "Received EOF, draining frame queue..."); p->eof = true; p->want_frame = false; return; } // Update FPS estimates if possible/reasonable default_estimate(&p->fps, src->first_field ? src->duration / 2 : src->duration); if (p->queue.num) { double last_pts = p->queue.elem[p->queue.num - 1]->pts; float delta = src->pts - last_pts; if (delta <= 0.0f) { PL_DEBUG(p, "Non monotonically increasing PTS %f -> %f", last_pts, src->pts); } else if (p->fps.estimate && delta > 10.0 * p->fps.estimate) { PL_DEBUG(p, "Discontinuous source PTS jump %f -> %f", last_pts, src->pts); } else { update_estimate(&p->fps, delta); } } else if (src->pts != 0) { PL_DEBUG(p, "First frame received with non-zero PTS %f", src->pts); } struct entry *entry = pl_alloc_ptr(NULL, entry); *entry = (struct entry) { .signature = p->signature++, .pts = src->pts, .src = *src, }; pl_rc_init(&entry->rc); PL_ARRAY_POP(p->cache, &entry->cache); PL_TRACE(p, "Added new frame id %"PRIu64" with PTS %f", entry->signature, entry->pts); // Insert new entry into the correct spot in the queue, sorted by PTS for (int i = p->queue.num;; i--) { if (i == 0 || p->queue.elem[i - 1]->pts <= entry->pts) { if (src->first_field == PL_FIELD_NONE) { // Progressive PL_ARRAY_INSERT_AT(p, p->queue, i, entry); break; } else { // Interlaced struct entry *prev = i > 0 ? p->queue.elem[i - 1] : NULL; struct entry *next = i < p->queue.num ? p->queue.elem[i] : NULL; struct entry *entry2 = pl_zalloc_ptr(NULL, entry2); pl_rc_init(&entry2->rc); if (next) { entry2->pts = (entry->pts + next->pts) / 2; } else if (src->duration) { entry2->pts = entry->pts + src->duration / 2; } else if (p->fps.estimate) { entry2->pts = entry->pts + p->fps.estimate; } else { PL_ERR(p, "Frame with PTS %f specified as interlaced, but " "no FPS information known yet! Please specify a " "valid `pl_source_frame.duration`. Treating as " "progressive...", src->pts); PL_ARRAY_INSERT_AT(p, p->queue, i, entry); pl_free(entry2); break; } entry->field = src->first_field; entry2->primary = entry_ref(entry); entry2->field = pl_field_other(entry->field); entry2->signature = p->signature++; PL_TRACE(p, "Added second field id %"PRIu64" with PTS %f", entry2->signature, entry2->pts); // Link previous/next frames if (prev) { entry->prev = entry_ref(PL_DEF(prev->primary, prev)); entry2->prev = entry_ref(PL_DEF(prev->primary, prev)); // Retroactively re-link the previous frames that should // be referencing this frame for (int j = i - 1; j >= 0; --j) { struct entry *e = p->queue.elem[j]; if (e != prev && e != prev->primary) break; entry_deref(p, &e->next, true); e->next = entry_ref(entry); if (e->dirty) { // reset signature to signal change e->signature = p->signature++; e->dirty = false; } } } if (next) { entry->next = entry_ref(PL_DEF(next->primary, next)); entry2->next = entry_ref(PL_DEF(next->primary, next)); for (int j = i; j < p->queue.num; j++) { struct entry *e = p->queue.elem[j]; if (e != next && e != next->primary) break; entry_deref(p, &e->prev, true); e->prev = entry_ref(entry); if (e->dirty) { e->signature = p->signature++; e->dirty = false; } } } PL_ARRAY_INSERT_AT(p, p->queue, i, entry); PL_ARRAY_INSERT_AT(p, p->queue, i+1, entry2); break; } } } p->want_frame = false; } void pl_queue_push(pl_queue p, const struct pl_source_frame *frame) { pl_mutex_lock(&p->lock_weak); queue_push(p, frame); pl_mutex_unlock(&p->lock_weak); } static inline bool entry_mapped(struct entry *entry) { return entry->mapped || (entry->primary && entry->primary->mapped); } static bool queue_has_room(pl_queue p) { if (p->want_frame) return true; int wanted_frames = PREFETCH_FRAMES; if (p->fps.estimate && p->vps.estimate && p->vps.estimate <= 1.0f / MIN_FPS) wanted_frames += ceilf(p->vps.estimate / p->fps.estimate) - 1; // Examine the queue tail for (int i = p->queue.num - 1; i >= 0; i--) { if (entry_mapped(p->queue.elem[i])) return true; if (p->queue.num - i >= wanted_frames) return false; } return true; } bool pl_queue_push_block(pl_queue p, uint64_t timeout, const struct pl_source_frame *frame) { pl_mutex_lock(&p->lock_weak); if (!timeout || !frame || p->eof) goto skip_blocking; while (!queue_has_room(p) && !p->eof) { if (pl_cond_timedwait(&p->wakeup, &p->lock_weak, timeout) == ETIMEDOUT) { pl_mutex_unlock(&p->lock_weak); return false; } } skip_blocking: queue_push(p, frame); pl_mutex_unlock(&p->lock_weak); return true; } static void report_estimates(pl_queue p) { if (p->fps.total >= MIN_SAMPLES && p->vps.total >= MIN_SAMPLES) { if (p->reported_fps && p->reported_vps) { // Only re-report the estimates if they've changed considerably // from the previously reported values static const float report_delta = 0.3f; float delta_fps = delta(p->reported_fps, p->fps.estimate); float delta_vps = delta(p->reported_vps, p->vps.estimate); if (delta_fps < report_delta && delta_vps < report_delta) return; } PL_INFO(p, "Estimated source FPS: %.3f, display FPS: %.3f", 1.0 / p->fps.estimate, 1.0 / p->vps.estimate); p->reported_fps = p->fps.estimate; p->reported_vps = p->vps.estimate; } } // note: may add more than one frame, since it releases the lock static enum pl_queue_status get_frame(pl_queue p, const struct pl_queue_params *params) { if (p->eof) return PL_QUEUE_EOF; if (!params->get_frame) { if (!params->timeout) return PL_QUEUE_MORE; p->want_frame = true; pl_cond_signal(&p->wakeup); while (p->want_frame) { if (pl_cond_timedwait(&p->wakeup, &p->lock_weak, params->timeout) == ETIMEDOUT) return PL_QUEUE_MORE; } return p->eof ? PL_QUEUE_EOF : PL_QUEUE_OK; } // Don't hold the weak mutex while calling into `get_frame`, to allow // `pl_queue_push` to run concurrently while we're waiting for frames pl_mutex_unlock(&p->lock_weak); struct pl_source_frame src; enum pl_queue_status ret; switch ((ret = params->get_frame(&src, params))) { case PL_QUEUE_OK: pl_queue_push(p, &src); break; case PL_QUEUE_EOF: pl_queue_push(p, NULL); break; case PL_QUEUE_MORE: case PL_QUEUE_ERR: break; } pl_mutex_lock(&p->lock_weak); return ret; } static inline bool map_frame(pl_queue p, struct entry *entry) { if (!entry->mapped) { PL_TRACE(p, "Mapping frame id %"PRIu64" with PTS %f", entry->signature, entry->pts); entry->mapped = true; entry->ok = entry->src.map(p->gpu, entry->cache.tex, &entry->src, &entry->frame); if (!entry->ok) PL_ERR(p, "Failed mapping frame id %"PRIu64" with PTS %f", entry->signature, entry->pts); } return entry->ok; } static bool map_entry(pl_queue p, struct entry *entry) { bool ok = map_frame(p, entry->primary ? entry->primary : entry); if (entry->prev) ok &= map_frame(p, entry->prev); if (entry->next) ok &= map_frame(p, entry->next); if (!ok) return false; if (entry->primary) entry->frame = entry->primary->frame; if (entry->field) { entry->frame.field = entry->field; entry->frame.first_field = PL_DEF(entry->primary, entry)->src.first_field; entry->frame.prev = entry->prev ? &entry->prev->frame : NULL; entry->frame.next = entry->next ? &entry->next->frame : NULL; entry->dirty = true; } return true; } static bool entry_complete(struct entry *entry) { return entry->field ? !!entry->next : true; } // Advance the queue as needed to make sure idx 0 is the last frame before // `pts`, and idx 1 is the first frame after `pts` (unless this is the last). // // Returns PL_QUEUE_OK only if idx 0 is still legal under ZOH semantics. static enum pl_queue_status advance(pl_queue p, double pts, const struct pl_queue_params *params) { // Cull all frames except the last frame before `pts` int culled = 0; for (int i = 1; i < p->queue.num; i++) { if (p->queue.elem[i]->pts <= pts) { entry_cull(p, p->queue.elem[i - 1], true); culled++; } } PL_ARRAY_REMOVE_RANGE(p->queue, 0, culled); // Keep adding new frames until we find one in the future, or EOF enum pl_queue_status ret = PL_QUEUE_OK; while (p->queue.num < 2) { switch ((ret = get_frame(p, params))) { case PL_QUEUE_ERR: return ret; case PL_QUEUE_EOF: if (!p->queue.num) return ret; goto done; case PL_QUEUE_MORE: case PL_QUEUE_OK: while (p->queue.num > 1 && p->queue.elem[1]->pts <= pts) { entry_cull(p, p->queue.elem[0], true); PL_ARRAY_REMOVE_AT(p->queue, 0); } if (ret == PL_QUEUE_MORE) return ret; continue; } } if (!entry_complete(p->queue.elem[1])) { switch (get_frame(p, params)) { case PL_QUEUE_ERR: return PL_QUEUE_ERR; case PL_QUEUE_MORE: ret = PL_QUEUE_MORE; // fall through case PL_QUEUE_EOF: case PL_QUEUE_OK: goto done; } } done: if (p->eof && p->queue.num == 1) { if (p->queue.elem[0]->pts == 0.0 || !p->fps.estimate) { // If the last frame has PTS 0.0, or we have no FPS estimate, then // this is probably a single-frame file, in which case we want to // extend the ZOH to infinity, rather than returning. Not a perfect // heuristic, but w/e return PL_QUEUE_OK; } // Last frame is held for an extra `p->fps.estimate` duration, // afterwards this function just returns EOF. if (pts < p->queue.elem[0]->pts + p->fps.estimate) { ret = PL_QUEUE_OK; } else { entry_cull(p, p->queue.elem[0], true); p->queue.num = 0; return PL_QUEUE_EOF; } } pl_assert(p->queue.num); return ret; } static inline enum pl_queue_status point(pl_queue p, struct pl_frame_mix *mix, const struct pl_queue_params *params) { if (!p->queue.num) { *mix = (struct pl_frame_mix) {0}; return PL_QUEUE_MORE; } // Find closest frame (nearest neighbour semantics) struct entry *entry = p->queue.elem[0]; if (entry->pts > params->pts) { // first frame not visible yet *mix = (struct pl_frame_mix) {0}; return PL_QUEUE_OK; } double best = fabs(entry->pts - params->pts); for (int i = 1; i < p->queue.num; i++) { double dist = fabs(p->queue.elem[i]->pts - params->pts); if (dist < best) { entry = p->queue.elem[i]; best = dist; continue; } else { break; } } if (!map_entry(p, entry)) return PL_QUEUE_ERR; // Return a mix containing only this single frame p->tmp_sig.num = p->tmp_ts.num = p->tmp_frame.num = 0; PL_ARRAY_APPEND(p, p->tmp_sig, entry->signature); PL_ARRAY_APPEND(p, p->tmp_frame, &entry->frame); PL_ARRAY_APPEND(p, p->tmp_ts, 0.0); *mix = (struct pl_frame_mix) { .num_frames = 1, .frames = p->tmp_frame.elem, .signatures = p->tmp_sig.elem, .timestamps = p->tmp_ts.elem, .vsync_duration = 1.0, }; PL_TRACE(p, "Showing single frame id %"PRIu64" with PTS %f for target PTS %f", entry->signature, entry->pts, params->pts); report_estimates(p); return PL_QUEUE_OK; } // Present a single frame as appropriate for `pts` static enum pl_queue_status nearest(pl_queue p, struct pl_frame_mix *mix, const struct pl_queue_params *params) { enum pl_queue_status ret; switch ((ret = advance(p, params->pts, params))) { case PL_QUEUE_ERR: case PL_QUEUE_EOF: return ret; case PL_QUEUE_OK: case PL_QUEUE_MORE: if (mix && point(p, mix, params) == PL_QUEUE_ERR) return PL_QUEUE_ERR; return ret; } pl_unreachable(); } // Special case of `interpolate` for radius = 0, in which case we need exactly // the previous frame and the following frame static enum pl_queue_status oversample(pl_queue p, struct pl_frame_mix *mix, const struct pl_queue_params *params) { enum pl_queue_status ret; switch ((ret = advance(p, params->pts, params))) { case PL_QUEUE_ERR: case PL_QUEUE_EOF: return ret; case PL_QUEUE_OK: break; case PL_QUEUE_MORE: if (!p->queue.num) { if (mix) *mix = (struct pl_frame_mix) {0}; return ret; } break; } if (!mix) return PL_QUEUE_OK; // Can't oversample with only a single frame, fall back to point sampling if (p->queue.num < 2 || p->queue.elem[0]->pts > params->pts) { if (point(p, mix, params) != PL_QUEUE_OK) return PL_QUEUE_ERR; return ret; } struct entry *entries[2] = { p->queue.elem[0], p->queue.elem[1] }; pl_assert(entries[0]->pts <= params->pts); pl_assert(entries[1]->pts >= params->pts); // Returning a mix containing both of these two frames p->tmp_sig.num = p->tmp_ts.num = p->tmp_frame.num = 0; for (int i = 0; i < 2; i++) { if (!map_entry(p, entries[i])) return PL_QUEUE_ERR; float ts = (entries[i]->pts - params->pts) / p->fps.estimate; PL_ARRAY_APPEND(p, p->tmp_sig, entries[i]->signature); PL_ARRAY_APPEND(p, p->tmp_frame, &entries[i]->frame); PL_ARRAY_APPEND(p, p->tmp_ts, ts); } *mix = (struct pl_frame_mix) { .num_frames = 2, .frames = p->tmp_frame.elem, .signatures = p->tmp_sig.elem, .timestamps = p->tmp_ts.elem, .vsync_duration = p->vps.estimate / p->fps.estimate, }; PL_TRACE(p, "Oversampling 2 frames for target PTS %f:", params->pts); for (int i = 0; i < mix->num_frames; i++) PL_TRACE(p, " id %"PRIu64" ts %f", mix->signatures[i], mix->timestamps[i]); report_estimates(p); return ret; } // Present a mixture of frames, relative to the vsync ratio static enum pl_queue_status interpolate(pl_queue p, struct pl_frame_mix *mix, const struct pl_queue_params *params) { // No FPS estimate available, possibly source contains only a single frame, // or this is the first frame to be rendered. Fall back to point sampling. if (!p->fps.estimate) return nearest(p, mix, params); // Silently disable interpolation if the ratio dips lower than the // configured threshold float ratio = fabs(p->fps.estimate / p->vps.estimate - 1.0); if (ratio < params->interpolation_threshold) { if (!p->threshold_frames) { PL_INFO(p, "Detected fps ratio %.4f below threshold %.4f, " "disabling interpolation", ratio, params->interpolation_threshold); } p->threshold_frames = THRESHOLD_FRAMES + 1; return nearest(p, mix, params); } else if (ratio < THRESHOLD_MAX_RATIO && p->threshold_frames > 1) { p->threshold_frames--; return nearest(p, mix, params); } else { if (p->threshold_frames) { PL_INFO(p, "Detected fps ratio %.4f exceeds threshold %.4f, " "re-enabling interpolation", ratio, params->interpolation_threshold); } p->threshold_frames = 0; } // No radius information, special case in which we only need the previous // and next frames. if (!params->radius) return oversample(p, mix, params); pl_assert(p->fps.estimate && p->vps.estimate); float radius = params->radius * fmaxf(1.0f, p->vps.estimate / p->fps.estimate); double min_pts = params->pts - radius * p->fps.estimate, max_pts = params->pts + radius * p->fps.estimate; enum pl_queue_status ret; switch ((ret = advance(p, min_pts, params))) { case PL_QUEUE_ERR: case PL_QUEUE_EOF: return ret; case PL_QUEUE_MORE: goto done; case PL_QUEUE_OK: break; } // Keep adding new frames until we've covered the range we care about pl_assert(p->queue.num); while (p->queue.elem[p->queue.num - 1]->pts < max_pts) { switch ((ret = get_frame(p, params))) { case PL_QUEUE_ERR: return ret; case PL_QUEUE_MORE: goto done; case PL_QUEUE_EOF:; // Don't forward EOF until we've held the last frame for the // desired ZOH hold duration double last_pts = p->queue.elem[p->queue.num - 1]->pts; if (last_pts && params->pts >= last_pts + p->fps.estimate) return ret; ret = PL_QUEUE_OK; goto done; case PL_QUEUE_OK: continue; } } if (!entry_complete(p->queue.elem[p->queue.num - 1])) { switch ((ret = get_frame(p, params))) { case PL_QUEUE_MORE: case PL_QUEUE_OK: break; case PL_QUEUE_ERR: case PL_QUEUE_EOF: return ret; } } done: ; if (!mix) return PL_QUEUE_OK; // Construct a mix object representing the current queue state, starting at // the last frame before `min_pts` to make sure there's a fallback frame // available for ZOH semantics. p->tmp_sig.num = p->tmp_ts.num = p->tmp_frame.num = 0; for (int i = 0; i < p->queue.num; i++) { struct entry *entry = p->queue.elem[i]; if (entry->pts > max_pts) break; if (!map_entry(p, entry)) return PL_QUEUE_ERR; float ts = (entry->pts - params->pts) / p->fps.estimate; PL_ARRAY_APPEND(p, p->tmp_sig, entry->signature); PL_ARRAY_APPEND(p, p->tmp_frame, &entry->frame); PL_ARRAY_APPEND(p, p->tmp_ts, ts); } *mix = (struct pl_frame_mix) { .num_frames = p->tmp_frame.num, .frames = p->tmp_frame.elem, .signatures = p->tmp_sig.elem, .timestamps = p->tmp_ts.elem, .vsync_duration = p->vps.estimate / p->fps.estimate, }; PL_TRACE(p, "Showing mix of %d frames for target PTS %f:", mix->num_frames, params->pts); for (int i = 0; i < mix->num_frames; i++) PL_TRACE(p, " id %"PRIu64" ts %f", mix->signatures[i], mix->timestamps[i]); report_estimates(p); return ret; } static bool prefill(pl_queue p, const struct pl_queue_params *params) { int min_frames = 2 * ceilf(params->radius); if (p->fps.estimate && p->vps.estimate && p->vps.estimate <= 1.0f / MIN_FPS) min_frames *= ceilf(p->vps.estimate / p->fps.estimate); min_frames = PL_MAX(min_frames, PREFETCH_FRAMES); while (p->queue.num < min_frames) { switch (get_frame(p, params)) { case PL_QUEUE_ERR: return false; case PL_QUEUE_EOF: case PL_QUEUE_MORE: return true; case PL_QUEUE_OK: continue; } } // In the most likely case, the first few frames will all be required. So // force-map them all to initialize GPU state on initial rendering. This is // better than the alternative of missing the cache later, when timing is // more relevant. for (int i = 0; i < min_frames; i++) { if (!map_entry(p, p->queue.elem[i])) return false; } return true; } enum pl_queue_status pl_queue_update(pl_queue p, struct pl_frame_mix *out_mix, const struct pl_queue_params *params) { pl_mutex_lock(&p->lock_strong); pl_mutex_lock(&p->lock_weak); default_estimate(&p->vps, params->vsync_duration); float delta = params->pts - p->prev_pts; if (delta < 0.0f) { // This is a backwards PTS jump. This is something we can handle // semi-gracefully, but only if we haven't culled past the current // frame yet. if (p->queue.num && p->queue.elem[0]->pts > params->pts) { PL_ERR(p, "Requested PTS %f is lower than the oldest frame " "PTS %f. This is not supported, PTS must be monotonically " "increasing! Please use `pl_queue_reset` to reset the frame " "queue on discontinuous PTS jumps.", params->pts, p->queue.elem[0]->pts); pl_mutex_unlock(&p->lock_weak); pl_mutex_unlock(&p->lock_strong); return PL_QUEUE_ERR; } } else if (delta > 1.0f) { // A jump of more than a second is probably the result of a // discontinuous jump after a suspend. To prevent this from exploding // the FPS estimate, treat this as a new frame. PL_TRACE(p, "Discontinuous target PTS jump %f -> %f, ignoring...", p->prev_pts, params->pts); } else if (delta > 0) { update_estimate(&p->vps, params->pts - p->prev_pts); } p->prev_pts = params->pts; // As a special case, prefill the queue if this is the first frame if (!params->pts && !p->queue.num) { if (!prefill(p, params)) { pl_mutex_unlock(&p->lock_weak); pl_mutex_unlock(&p->lock_strong); return PL_QUEUE_ERR; } } // Ignore unrealistically high or low FPS, common near start of playback static const float max_vsync = 1.0 / MIN_FPS; static const float min_vsync = 1.0 / MAX_FPS; bool estimation_ok = p->vps.estimate > min_vsync && p->vps.estimate < max_vsync; enum pl_queue_status ret; if (estimation_ok || params->vsync_duration > 0) { // We know the vsync duration, so construct an interpolation mix ret = interpolate(p, out_mix, params); } else { // We don't know the vsync duration (yet), so just point-sample ret = nearest(p, out_mix, params); } pl_cond_signal(&p->wakeup); pl_mutex_unlock(&p->lock_weak); pl_mutex_unlock(&p->lock_strong); return ret; } float pl_queue_estimate_fps(pl_queue p) { pl_mutex_lock(&p->lock_weak); float estimate = p->fps.estimate; pl_mutex_unlock(&p->lock_weak); return estimate ? 1.0f / estimate : 0.0f; } float pl_queue_estimate_vps(pl_queue p) { pl_mutex_lock(&p->lock_weak); float estimate = p->vps.estimate; pl_mutex_unlock(&p->lock_weak); return estimate ? 1.0f / estimate : 0.0f; } int pl_queue_num_frames(pl_queue p) { pl_mutex_lock(&p->lock_weak); int count = p->queue.num; pl_mutex_unlock(&p->lock_weak); return count; } bool pl_queue_peek(pl_queue p, int idx, struct pl_source_frame *out) { pl_mutex_lock(&p->lock_weak); bool ok = idx >= 0 && idx < p->queue.num; if (ok) *out = p->queue.elem[idx]->src; pl_mutex_unlock(&p->lock_weak); return ok; }