1
0
mirror of https://github.com/mpv-player/mpv synced 2025-03-06 22:28:01 +00:00

options: add a thread-safe way to notify option updates

So far, we had a thread-safe way to read options, but no option update
notification mechanism. Everything was funneled though the main thread's
central mp_option_change_callback() function. For example, if the
panscan options were changed, the function called vo_control() with
VOCTRL_SET_PANSCAN to manually notify the VO thread of updates. This
worked, but's pretty inconvenient. Most of these problems come from the
fact that MPlayer was written as a single-threaded program.

This commit works towards a more flexible mechanism. It adds an update
callback to m_config_cache (the thing that is already used for
thread-safe access of global options).

This alone would still be rather inconvenient, at least in context of
VOs. Add another mechanism on top of it that uses mp_dispatch_queue, and
takes care of some annoying synchronization issues. We extend
mp_dispatch_queue itself to make this easier and slightly more
efficient.

As a first application, use this to reimplement certain VO scaling and
renderer options. The update_opts() function translates these to the
"old" VOCTRLs, though.

An annoyingly subtle issue is that m_config_cache's destructor now
releases pending notifications, and must be released before the
associated dispatch queue. Otherwise, it could happen that option
updates during e.g. VO destruction queue or run stale entries, which is
not expected.

Rather untested. The singly-linked list code in dispatch.c is probably
buggy, and I bet some aspects about synchronization are not entirely
sane.
This commit is contained in:
wm4 2017-08-22 15:50:33 +02:00
parent 5361c0b5d8
commit d2bdb72b69
10 changed files with 223 additions and 23 deletions

View File

@ -59,6 +59,7 @@ struct mp_dispatch_item {
mp_dispatch_fn fn;
void *fn_data;
bool asynchronous;
bool mergeable;
bool completed;
struct mp_dispatch_item *next;
};
@ -113,12 +114,25 @@ static void mp_dispatch_append(struct mp_dispatch_queue *queue,
struct mp_dispatch_item *item)
{
pthread_mutex_lock(&queue->lock);
if (item->mergeable) {
for (struct mp_dispatch_item *cur = queue->head; cur; cur = cur->next) {
if (cur->mergeable && cur->fn == item->fn &&
cur->fn_data == item->fn_data)
{
talloc_free(item);
pthread_mutex_unlock(&queue->lock);
return;
}
}
}
if (queue->tail) {
queue->tail->next = item;
} else {
queue->head = item;
}
queue->tail = item;
// Wake up the main thread; note that other threads might wait on this
// condition for reasons, so broadcast the condition.
pthread_cond_broadcast(&queue->cond);
@ -127,6 +141,7 @@ static void mp_dispatch_append(struct mp_dispatch_queue *queue,
if (!queue->wakeup_fn)
queue->interrupted = true;
pthread_mutex_unlock(&queue->lock);
if (queue->wakeup_fn)
queue->wakeup_fn(queue->wakeup_ctx);
}
@ -165,6 +180,47 @@ void mp_dispatch_enqueue_autofree(struct mp_dispatch_queue *queue,
mp_dispatch_append(queue, item);
}
// Like mp_dispatch_enqueue(), but
void mp_dispatch_enqueue_notify(struct mp_dispatch_queue *queue,
mp_dispatch_fn fn, void *fn_data)
{
struct mp_dispatch_item *item = talloc_ptrtype(NULL, item);
*item = (struct mp_dispatch_item){
.fn = fn,
.fn_data = fn_data,
.mergeable = true,
.asynchronous = true,
};
mp_dispatch_append(queue, item);
}
// Remove already queued item. Only items enqueued with the following functions
// can be canceled:
// - mp_dispatch_enqueue()
// - mp_dispatch_enqueue_notify()
// Items which were enqueued, and which are currently executing, can not be
// canceled anymore. This function is mostly for being called from the same
// context as mp_dispatch_queue_process(), where the "currently executing" case
// can be excluded.
void mp_dispatch_cancel_fn(struct mp_dispatch_queue *queue,
mp_dispatch_fn fn, void *fn_data)
{
pthread_mutex_lock(&queue->lock);
struct mp_dispatch_item **pcur = &queue->head;
queue->tail = NULL;
while (*pcur) {
struct mp_dispatch_item *cur = *pcur;
if (cur->fn == fn && cur->fn_data == fn_data) {
*pcur = cur->next;
talloc_free(cur);
} else {
queue->tail = cur;
pcur = &cur->next;
}
}
pthread_mutex_unlock(&queue->lock);
}
// Run fn(fn_data) on the target thread synchronously. This function enqueues
// the callback and waits until the target thread is done doing this.
// This is redundant to calling the function inside mp_dispatch_[un]lock(),

View File

@ -12,6 +12,10 @@ void mp_dispatch_enqueue(struct mp_dispatch_queue *queue,
mp_dispatch_fn fn, void *fn_data);
void mp_dispatch_enqueue_autofree(struct mp_dispatch_queue *queue,
mp_dispatch_fn fn, void *fn_data);
void mp_dispatch_enqueue_notify(struct mp_dispatch_queue *queue,
mp_dispatch_fn fn, void *fn_data);
void mp_dispatch_cancel_fn(struct mp_dispatch_queue *queue,
mp_dispatch_fn fn, void *fn_data);
void mp_dispatch_run(struct mp_dispatch_queue *queue,
mp_dispatch_fn fn, void *fn_data);
void mp_dispatch_queue_process(struct mp_dispatch_queue *queue, double timeout);

View File

@ -39,6 +39,7 @@
#include "common/global.h"
#include "common/msg.h"
#include "common/msg_control.h"
#include "misc/dispatch.h"
#include "misc/node.h"
#include "osdep/atomic.h"
@ -57,6 +58,8 @@ struct m_config_shadow {
pthread_mutex_t lock;
struct m_config *root;
char *data;
struct m_config_cache **listeners;
int num_listeners;
};
// Represents a sub-struct (OPT_SUBSTRUCT()).
@ -156,8 +159,11 @@ static void config_destroy(void *p)
m_option_free(co->opt, config->shadow->data + co->shadow_offset);
}
if (config->shadow)
if (config->shadow) {
// must all have been unregistered
assert(config->shadow->num_listeners == 0);
pthread_mutex_destroy(&config->shadow->lock);
}
}
struct m_config *m_config_new(void *talloc_ctx, struct mp_log *log,
@ -1190,6 +1196,16 @@ static bool is_group_included(struct m_config *config, int group, int parent)
return false;
}
static void cache_destroy(void *p)
{
struct m_config_cache *cache = p;
// (technically speaking, being able to call them both without anything
// breaking is a feature provided by these functions)
m_config_cache_set_wakeup_cb(cache, NULL, NULL);
m_config_cache_set_dispatch_change_cb(cache, NULL, NULL, NULL);
}
struct m_config_cache *m_config_cache_alloc(void *ta_parent,
struct mpv_global *global,
const struct m_sub_options *group)
@ -1198,6 +1214,7 @@ struct m_config_cache *m_config_cache_alloc(void *ta_parent,
struct m_config *root = shadow->root;
struct m_config_cache *cache = talloc_zero(ta_parent, struct m_config_cache);
talloc_set_destructor(cache, cache_destroy);
cache->shadow = shadow;
cache->shadow_config = m_config_new(cache, mp_null_log, root->size,
root->defaults, root->options);
@ -1292,12 +1309,84 @@ void m_config_notify_change_co(struct m_config *config,
group = g->parent_group;
}
if (shadow) {
pthread_mutex_lock(&shadow->lock);
for (int n = 0; n < shadow->num_listeners; n++) {
struct m_config_cache *cache = shadow->listeners[n];
if (cache->wakeup_cb)
cache->wakeup_cb(cache->wakeup_cb_ctx);
}
pthread_mutex_unlock(&shadow->lock);
}
if (config->option_change_callback) {
config->option_change_callback(config->option_change_callback_ctx, co,
changed);
}
}
void m_config_cache_set_wakeup_cb(struct m_config_cache *cache,
void (*cb)(void *ctx), void *cb_ctx)
{
struct m_config_shadow *shadow = cache->shadow;
pthread_mutex_lock(&shadow->lock);
if (cache->in_list) {
for (int n = 0; n < shadow->num_listeners; n++) {
if (shadow->listeners[n] == cache)
MP_TARRAY_REMOVE_AT(shadow->listeners, shadow->num_listeners, n);
}
if (!shadow->num_listeners) {
talloc_free(shadow->listeners);
shadow->listeners = NULL;
}
}
if (cb) {
MP_TARRAY_APPEND(NULL, shadow->listeners, shadow->num_listeners, cache);
cache->in_list = true;
cache->wakeup_cb = cb;
cache->wakeup_cb_ctx = cb_ctx;
}
pthread_mutex_unlock(&shadow->lock);
}
static void dispatch_notify(void *p)
{
struct m_config_cache *cache = p;
assert(cache->wakeup_dispatch_queue);
mp_dispatch_enqueue_notify(cache->wakeup_dispatch_queue,
cache->wakeup_dispatch_cb,
cache->wakeup_dispatch_cb_ctx);
}
void m_config_cache_set_dispatch_change_cb(struct m_config_cache *cache,
struct mp_dispatch_queue *dispatch,
void (*cb)(void *ctx), void *cb_ctx)
{
// Remove the old one is tricky. Firts make sure no new notifications will
// come.
m_config_cache_set_wakeup_cb(cache, NULL, NULL);
// Remove any pending notifications (assume we're on the same thread as
// any potential mp_dispatch_queue_process() callers).
if (cache->wakeup_dispatch_queue) {
mp_dispatch_cancel_fn(cache->wakeup_dispatch_queue,
cache->wakeup_dispatch_cb,
cache->wakeup_dispatch_cb_ctx);
}
cache->wakeup_dispatch_queue = NULL;
cache->wakeup_dispatch_cb = NULL;
cache->wakeup_dispatch_cb_ctx = NULL;
if (cb) {
cache->wakeup_dispatch_queue = dispatch;
cache->wakeup_dispatch_cb = cb;
cache->wakeup_dispatch_cb_ctx = cb_ctx;
m_config_cache_set_wakeup_cb(cache, dispatch_notify, cache);
}
}
bool m_config_is_in_group(struct m_config *config,
const struct m_sub_options *group,
struct m_config_option *co)

View File

@ -275,9 +275,20 @@ struct m_config_cache {
struct m_config *shadow_config;
long long ts;
int group;
bool in_list;
// --- Implicitly synchronized by setting/unsetting wakeup_cb.
struct mp_dispatch_queue *wakeup_dispatch_queue;
void (*wakeup_dispatch_cb)(void *ctx);
void *wakeup_dispatch_cb_ctx;
// --- Protected by shadow->lock
void (*wakeup_cb)(void *ctx);
void *wakeup_cb_ctx;
};
// Create a mirror copy from the global options.
// Keep in mind that a m_config_cache object is not thread-safe; it merely
// provides thread-safe access to the global options. All API functions for
// the same m_config_cache object must synchronized, unless otherwise noted.
// ta_parent: parent for the returned allocation
// global: option data source
// group: the option group to return. This can be NULL for the global option
@ -287,6 +298,22 @@ struct m_config_cache *m_config_cache_alloc(void *ta_parent,
struct mpv_global *global,
const struct m_sub_options *group);
// If any of the options in the group possibly changes, call this callback. The
// callback must not actually access the cache or anything option related.
// Instead, it must wake up the thread that normally accesses the cache.
void m_config_cache_set_wakeup_cb(struct m_config_cache *cache,
void (*cb)(void *ctx), void *cb_ctx);
// If any of the options in the group change, call this callback on the given
// dispatch queue. This is higher level than m_config_cache_set_wakeup_cb(),
// and you can do anything you want in the callback (assuming the dispatch
// queue is processed in the same thread that accesses m_config_cache API).
// To ensure clean shutdown, you must destroy the m_config_cache (or unset the
// callback) before the dispatch queue is destroyed.
void m_config_cache_set_dispatch_change_cb(struct m_config_cache *cache,
struct mp_dispatch_queue *dispatch,
void (*cb)(void *ctx), void *cb_ctx);
// Update the options in cache->opts to current global values. Return whether
// there was an update notification at all (which may or may not indicate that
// some options have changed).
@ -295,13 +322,13 @@ struct m_config_cache *m_config_cache_alloc(void *ta_parent,
bool m_config_cache_update(struct m_config_cache *cache);
// Like m_config_cache_alloc(), but return the struct (m_config_cache->opts)
// directly, with no way to update the config.
// directly, with no way to update the config. Basically this returns a copy
// with a snapshot of the current option values.
// Warning: does currently not set the child as its own talloc root, which
// means the only way to free the struct is by freeing ta_parent.
void *mp_get_config_group(void *ta_parent, struct mpv_global *global,
const struct m_sub_options *group);
// Read a single global option in a thread-safe way. For multiple options,
// use m_config_cache. The option must exist and match the provided type (the
// type is used as a sanity check only). Performs semi-expensive lookup.

View File

@ -398,8 +398,6 @@ struct m_option {
// certain groups of options.
#define UPDATE_OPT_FIRST (1 << 7)
#define UPDATE_TERM (1 << 7) // terminal options
#define UPDATE_RENDERER (1 << 8) // mainly vo_opengl options
#define UPDATE_VIDEOPOS (1 << 9) // video position (panscan etc.)
#define UPDATE_OSD (1 << 10) // related to OSD rendering
#define UPDATE_BUILTIN_SCRIPTS (1 << 11) // osc/ytdl
#define UPDATE_IMGPAR (1 << 12) // video image params overrides

View File

@ -146,20 +146,20 @@ static const m_option_t mp_vo_opt_list[] = {
OPT_FLAG("fullscreen", fullscreen, 0),
OPT_ALIAS("fs", "fullscreen"),
OPT_FLAG("native-keyrepeat", native_keyrepeat, 0),
OPT_FLOATRANGE("panscan", panscan, UPDATE_VIDEOPOS, 0.0, 1.0),
OPT_FLOATRANGE("video-zoom", zoom, UPDATE_VIDEOPOS, -20.0, 20.0),
OPT_FLOATRANGE("video-pan-x", pan_x, UPDATE_VIDEOPOS, -3.0, 3.0),
OPT_FLOATRANGE("video-pan-y", pan_y, UPDATE_VIDEOPOS, -3.0, 3.0),
OPT_FLOATRANGE("video-align-x", align_x, UPDATE_VIDEOPOS, -1.0, 1.0),
OPT_FLOATRANGE("video-align-y", align_y, UPDATE_VIDEOPOS, -1.0, 1.0),
OPT_CHOICE("video-unscaled", unscaled, UPDATE_VIDEOPOS,
OPT_FLOATRANGE("panscan", panscan, 0, 0.0, 1.0),
OPT_FLOATRANGE("video-zoom", zoom, 0, -20.0, 20.0),
OPT_FLOATRANGE("video-pan-x", pan_x, 0, -3.0, 3.0),
OPT_FLOATRANGE("video-pan-y", pan_y, 0, -3.0, 3.0),
OPT_FLOATRANGE("video-align-x", align_x, 0, -1.0, 1.0),
OPT_FLOATRANGE("video-align-y", align_y, 0, -1.0, 1.0),
OPT_CHOICE("video-unscaled", unscaled, 0,
({"no", 0}, {"yes", 1}, {"downscale-big", 2})),
OPT_INT64("wid", WinID, 0),
OPT_CHOICE_OR_INT("screen", screen_id, 0, 0, 32,
({"default", -1})),
OPT_CHOICE_OR_INT("fs-screen", fsscreen_id, 0, 0, 32,
({"all", -2}, {"current", -1})),
OPT_FLAG("keepaspect", keepaspect, UPDATE_VIDEOPOS),
OPT_FLAG("keepaspect", keepaspect, 0),
OPT_FLAG("keepaspect-window", keepaspect_window, 0),
OPT_FLAG("hidpi-window-scale", hidpi_window_scale, 0),
OPT_FLAG("native-fs", native_fs, 0),

View File

@ -5854,14 +5854,6 @@ void mp_option_change_callback(void *ctx, struct m_config_option *co, int flags)
if (flags & UPDATE_TERM)
mp_update_logging(mpctx, false);
if (mpctx->video_out) {
if (flags & UPDATE_VIDEOPOS)
vo_control(mpctx->video_out, VOCTRL_SET_PANSCAN, NULL);
if (flags & UPDATE_RENDERER)
vo_control(mpctx->video_out, VOCTRL_UPDATE_RENDER_OPTS, NULL);
}
if (flags & UPDATE_OSD) {
osd_changed(mpctx->osd);
for (int n = 0; n < NUM_PTRACKS; n++) {

View File

@ -414,7 +414,6 @@ const struct m_sub_options gl_video_conf = {
},
.size = sizeof(struct gl_video_opts),
.defaults = &gl_video_opts_def,
.change_flags = UPDATE_RENDERER,
};
static void uninit_rendering(struct gl_video *p);

View File

@ -167,6 +167,8 @@ struct vo_internal {
int opt_framedrop;
};
extern const struct m_sub_options gl_video_conf;
static void forget_frames(struct vo *vo);
static void *vo_thread(void *ptr);
@ -210,10 +212,33 @@ static void dispatch_wakeup_cb(void *ptr)
vo_wakeup(vo);
}
static void update_opts(void *p)
{
struct vo *vo = p;
if (m_config_cache_update(vo->opts_cache)) {
// "Legacy" update of video position related options.
if (vo->driver->control)
vo->driver->control(vo, VOCTRL_SET_PANSCAN, NULL);
}
if (vo->gl_opts_cache && m_config_cache_update(vo->gl_opts_cache))
{
// "Legacy" update of video GL renderer related options.
if (vo->driver->control)
vo->driver->control(vo, VOCTRL_UPDATE_RENDER_OPTS, NULL);
}
}
// Does not include thread- and VO uninit.
static void dealloc_vo(struct vo *vo)
{
forget_frames(vo); // implicitly synchronized
// These must be free'd before vo->in->dispatch.
talloc_free(vo->opts_cache);
talloc_free(vo->gl_opts_cache);
pthread_mutex_destroy(&vo->in->lock);
pthread_cond_destroy(&vo->in->wakeup);
talloc_free(vo);
@ -254,9 +279,18 @@ static struct vo *vo_create(bool probing, struct mpv_global *global,
pthread_mutex_init(&vo->in->lock, NULL);
pthread_cond_init(&vo->in->wakeup, NULL);
vo->opts_cache = m_config_cache_alloc(vo, global, &vo_sub_opts);
vo->opts_cache = m_config_cache_alloc(NULL, global, &vo_sub_opts);
vo->opts = vo->opts_cache->opts;
m_config_cache_set_dispatch_change_cb(vo->opts_cache, vo->in->dispatch,
update_opts, vo);
#if HAVE_GL
vo->gl_opts_cache = m_config_cache_alloc(NULL, global, &gl_video_conf);
m_config_cache_set_dispatch_change_cb(vo->gl_opts_cache, vo->in->dispatch,
update_opts, vo);
#endif
mp_input_set_mouse_transform(vo->input_ctx, NULL, NULL);
if (vo->driver->encode != !!vo->encode_lavc_ctx)
goto error;

View File

@ -400,6 +400,7 @@ struct vo {
struct m_config_cache *opts_cache; // cache for ->opts
struct mp_vo_opts *opts;
struct m_config_cache *gl_opts_cache;
bool want_redraw; // redraw as soon as possible