From 36a2127682f796c60e397488e106903839758043 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Fri, 21 Feb 2025 13:46:21 +0100 Subject: [PATCH 01/41] video player: use square BO When uploading memory with size X into a dmabuf, create a buffer that is square (e.g. sqrt(x) by sqrt(x)) instead of one that is X pixels wide and one pixel high. Drivers have limitations on buffer dimensions, and e.g. the intel (iris) driver ran into this limitation. --- src/plugins/gstreamer_video_player/frame.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/plugins/gstreamer_video_player/frame.c b/src/plugins/gstreamer_video_player/frame.c index 66498ce6..98370fea 100644 --- a/src/plugins/gstreamer_video_player/frame.c +++ b/src/plugins/gstreamer_video_player/frame.c @@ -359,14 +359,18 @@ UNUSED int dup_gst_buffer_range_as_dmabuf(struct gbm_device *gbm_device, GstBuff return -1; } - bo = gbm_bo_create(gbm_device, map_info.size, 1, GBM_FORMAT_R8, GBM_BO_USE_LINEAR); +// Create a square texture large enough to fit our bytes instead of one with only one huge row, + // because some drivers have limitations on the row length. (Intel) + uint32_t dim = (uint32_t) ceil(sqrt(map_info.size)); + + bo = gbm_bo_create(gbm_device, dim, dim, GBM_FORMAT_R8, GBM_BO_USE_LINEAR); if (bo == NULL) { LOG_ERROR("Couldn't create GBM BO to copy video frame into.\n"); goto fail_unmap_buffer; } map_data = NULL; - map = gbm_bo_map(bo, 0, 0, map_info.size, 1, GBM_BO_TRANSFER_WRITE, &stride, &map_data); + map = gbm_bo_map(bo, 0, 0, dim, dim, GBM_BO_TRANSFER_WRITE, &stride, &map_data); if (map == NULL) { LOG_ERROR("Couldn't mmap GBM BO to copy video frame into it.\n"); goto fail_destroy_bo; @@ -415,14 +419,18 @@ UNUSED int dup_gst_memory_as_dmabuf(struct gbm_device *gbm_device, GstMemory *me return -1; } - bo = gbm_bo_create(gbm_device, map_info.size, 1, GBM_FORMAT_R8, GBM_BO_USE_LINEAR); +// Create a square texture large enough to fit our bytes instead of one with only one huge row, + // because some drivers have limitations on the row length. (Intel) + uint32_t dim = (uint32_t) ceil(sqrt(map_info.size)); + + bo = gbm_bo_create(gbm_device, dim, dim, GBM_FORMAT_R8, GBM_BO_USE_LINEAR); if (bo == NULL) { LOG_ERROR("Couldn't create GBM BO to copy video frame into.\n"); goto fail_unmap_buffer; } map_data = NULL; - map = gbm_bo_map(bo, 0, 0, map_info.size, 1, GBM_BO_TRANSFER_WRITE, &stride, &map_data); + map = gbm_bo_map(bo, 0, 0, dim, dim, GBM_BO_TRANSFER_WRITE, &stride, &map_data); if (map == NULL) { LOG_ERROR("Couldn't mmap GBM BO to copy video frame into it.\n"); goto fail_destroy_bo; From 0b3f3a4ec752ac7e5b240b86a29fdd3b34a6459b Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Fri, 21 Feb 2025 13:55:56 +0100 Subject: [PATCH 02/41] video player: use playbin3 instead of uridecodebin Use playbin3 for playback instead of uridecodebin. This also enables audio support. Furthermore, separate out the appsink (which uploads buffers into a flutter texture) into a separate "pseudo" gstreamer-element, which can be created using `flutter_gl_texture_sink_new`. Synchronization is reworked as well, it is now assumed the player is only accessed from a single thread, the platform thread. The callbacks called by gstreamer (which are called on internal threads) will rethread, if necessary, by posting messages to the GstBus. The buffering query is also now properly fixed. Previously it queried the playbin instead of the element that does the actual queueing (a `multiqueue`). Also, a one-shot debug logging message is added to the frame uploader, to inform the user when slower, manual dmabuf uploads are used instead of zero-copy. --- CMakeLists.txt | 1 + src/plugins/gstreamer_video_player.h | 54 +- .../flutter_texture_sink.c | 288 +++++ src/plugins/gstreamer_video_player/frame.c | 83 +- src/plugins/gstreamer_video_player/player.c | 1106 +++++++---------- src/plugins/gstreamer_video_player/plugin.c | 136 +- 6 files changed, 852 insertions(+), 816 deletions(-) create mode 100644 src/plugins/gstreamer_video_player/flutter_texture_sink.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 734aba51..76392b57 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -335,6 +335,7 @@ if (BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN) src/plugins/gstreamer_video_player/plugin.c src/plugins/gstreamer_video_player/player.c src/plugins/gstreamer_video_player/frame.c + src/plugins/gstreamer_video_player/flutter_texture_sink.c ) target_link_libraries(flutterpi_module PUBLIC PkgConfig::LIBGSTREAMER diff --git a/src/plugins/gstreamer_video_player.h b/src/plugins/gstreamer_video_player.h index 02e6ed25..4ad9110f 100644 --- a/src/plugins/gstreamer_video_player.h +++ b/src/plugins/gstreamer_video_player.h @@ -14,6 +14,9 @@ #include "gles.h" #endif +#define GSTREAMER_VER(major, minor, patch) ((((major) &0xFF) << 16) | (((minor) &0xFF) << 8) | ((patch) &0xFF)) +#define THIS_GSTREAMER_VER GSTREAMER_VER(LIBGSTREAMER_VERSION_MAJOR, LIBGSTREAMER_VERSION_MINOR, LIBGSTREAMER_VERSION_PATCH) + enum format_hint { FORMAT_HINT_NONE, FORMAT_HINT_MPEG_DASH, FORMAT_HINT_HLS, FORMAT_HINT_SS, FORMAT_HINT_OTHER }; enum buffering_mode { BUFFERING_MODE_STREAM, BUFFERING_MODE_DOWNLOAD, BUFFERING_MODE_TIMESHIFT, BUFFERING_MODE_LIVE }; @@ -72,7 +75,7 @@ struct gstplayer *gstplayer_new_from_asset(struct flutterpi *flutterpi, const ch /// @arg uri The URI to the video. (for example, http://, https://, rtmp://, rtsp://) /// @arg format_hint A hint to the format of the video. FORMAT_HINT_NONE means there's no hint. /// @arg userdata The userdata associated with this player. -struct gstplayer *gstplayer_new_from_network(struct flutterpi *flutterpi, const char *uri, enum format_hint format_hint, void *userdata); +struct gstplayer *gstplayer_new_from_network(struct flutterpi *flutterpi, const char *uri, enum format_hint format_hint, void *userdata, GstStructure *headers); /// Create a gstreamer video player that loads the video from a file URI. /// @arg uri The file:// URI to the video. @@ -92,26 +95,20 @@ struct gstplayer *gstplayer_new_from_pipeline(struct flutterpi *flutterpi, const /// might be a race condition. void gstplayer_destroy(struct gstplayer *player); -DECLARE_LOCK_OPS(gstplayer) - /// Set the generic userdata associated with this gstreamer player instance. /// Overwrites the userdata set in the constructor and any userdata previously -/// set using @ref gstplayer_set_userdata_locked. +/// set using @ref gstplayer_set_userdata. /// @arg userdata The new userdata that should be associated with this player. -void gstplayer_set_userdata_locked(struct gstplayer *player, void *userdata); +void gstplayer_set_userdata(struct gstplayer *player, void *userdata); /// Get the userdata that was given to the constructor or was previously set using -/// @ref gstplayer_set_userdata_locked. +/// @ref gstplayer_set_userdata. /// @returns userdata associated with this player. -void *gstplayer_get_userdata_locked(struct gstplayer *player); +void *gstplayer_get_userdata(struct gstplayer *player); /// Get the id of the flutter external texture that this player is rendering into. int64_t gstplayer_get_texture_id(struct gstplayer *player); -//void gstplayer_set_info_callback(struct gstplayer *player, gstplayer_info_callback_t cb, void *userdata); - -//void gstplayer_set_buffering_callback(struct gstplayer *player, gstplayer_buffering_callback_t callback, void *userdata); - /// Add a http header (consisting of a string key and value) to the list of http headers that /// gstreamer will use when playing back from a HTTP/S URI. /// This has no effect after @ref gstplayer_initialize was called. @@ -122,12 +119,6 @@ void gstplayer_put_http_header(struct gstplayer *player, const char *key, const /// @returns 0 if initialization was successfull, errno-style error code if an error ocurred. int gstplayer_initialize(struct gstplayer *player); -/// Get the video info. If the video info (format, size, etc) is already known, @arg callback will be called -/// synchronously, inside this call. If the video info is not known, @arg callback will be called on the flutter-pi -/// platform thread as soon as the info is known. -/// @returns The handle for the deferred callback. -//struct sd_event_source_generic *gstplayer_probe_video_info(struct gstplayer *player, gstplayer_info_callback_t callback, void *userdata); - /// Set the current playback state to "playing" if that's not the case already. /// @returns 0 if initialization was successfull, errno-style error code if an error ocurred. int gstplayer_play(struct gstplayer *player); @@ -164,6 +155,17 @@ int gstplayer_step_forward(struct gstplayer *player); int gstplayer_step_backward(struct gstplayer *player); +struct video_info { + int width, height; + + double fps; + + int64_t duration_ms; + + bool can_seek; + int64_t seek_begin_ms, seek_end_ms; +}; + /// @brief Get the value notifier for the video info. /// /// Gets notified with a value of type `struct video_info*` when the video info changes. @@ -212,19 +214,6 @@ DECLARE_REF_OPS(frame_interface) typedef struct _GstVideoInfo GstVideoInfo; typedef struct _GstVideoMeta GstVideoMeta; -struct video_info { - int width, height; - double fps; - int64_t duration_ms; - bool can_seek; - int64_t seek_begin_ms, seek_end_ms; -}; - -struct frame_info { - const GstVideoInfo *gst_info; - uint32_t drm_format; - EGLint egl_color_space; -}; struct _GstSample; @@ -238,4 +227,9 @@ struct gl_texture_frame; const struct gl_texture_frame *frame_get_gl_frame(struct video_frame *frame); +struct texture; +struct gl_renderer; +typedef struct _GstElement GstElement; +GstElement *flutter_gl_texture_sink_new(struct texture *texture, struct gl_renderer *renderer); + #endif diff --git a/src/plugins/gstreamer_video_player/flutter_texture_sink.c b/src/plugins/gstreamer_video_player/flutter_texture_sink.c new file mode 100644 index 00000000..76a14f43 --- /dev/null +++ b/src/plugins/gstreamer_video_player/flutter_texture_sink.c @@ -0,0 +1,288 @@ +#include +#include +#include +#include +#include + +#include "../gstreamer_video_player.h" +#include "texture_registry.h" +#include "util/logging.h" + +struct texture_sink { + struct texture *fl_texture; + struct frame_interface *interface; +}; + +static void on_destroy_texture_frame(const struct texture_frame *texture_frame, void *userdata) { + struct video_frame *frame; + + (void) texture_frame; + + ASSERT_NOT_NULL(texture_frame); + ASSERT_NOT_NULL(userdata); + + frame = userdata; + + frame_destroy(frame); +} + +static void on_appsink_eos(GstAppSink *appsink, void *userdata) { + gboolean ok; + + ASSERT_NOT_NULL(appsink); + ASSERT_NOT_NULL(userdata); + + (void) userdata; + + LOG_DEBUG("on_appsink_eos()\n"); + + // this method is called from the streaming thread. + // we shouldn't access the player directly here, it could change while we use it. + // post a message to the gstreamer bus instead, will be handled by + // @ref on_bus_message. + ok = gst_element_post_message( + GST_ELEMENT(appsink), + gst_message_new_application(GST_OBJECT(appsink), gst_structure_new_empty("appsink-eos")) + ); + if (ok == FALSE) { + LOG_ERROR("Could not post appsink end-of-stream event to the message bus.\n"); + } +} + +static GstFlowReturn on_appsink_new_preroll(GstAppSink *appsink, void *userdata) { + struct video_frame *frame; + GstSample *sample; + + ASSERT_NOT_NULL(appsink); + ASSERT_NOT_NULL(userdata); + + struct texture_sink *meta = userdata; + + sample = gst_app_sink_try_pull_preroll(appsink, 0); + if (sample == NULL) { + LOG_ERROR("gstreamer returned a NULL sample.\n"); + return GST_FLOW_ERROR; + } + + // supply video info here + frame = frame_new(meta->interface, sample, NULL); + + // the frame has a reference on the sample internally. + gst_sample_unref(sample); + + if (frame != NULL) { + texture_push_frame( + meta->fl_texture, + &(struct texture_frame){ + .gl = *frame_get_gl_frame(frame), + .destroy = on_destroy_texture_frame, + .userdata = frame, + } + ); + } + + return GST_FLOW_OK; +} + +static GstFlowReturn on_appsink_new_sample(GstAppSink *appsink, void *userdata) { + struct video_frame *frame; + GstSample *sample; + + ASSERT_NOT_NULL(appsink); + ASSERT_NOT_NULL(userdata); + + struct texture_sink *meta = userdata; + + sample = gst_app_sink_try_pull_sample(appsink, 0); + if (sample == NULL) { + LOG_ERROR("gstreamer returned a NULL sample.\n"); + return GST_FLOW_ERROR; + } + + // supply video info here + frame = frame_new(meta->interface, sample, NULL); + + // the frame has a reference on the sample internally. + gst_sample_unref(sample); + + if (frame != NULL) { + texture_push_frame( + meta->fl_texture, + &(struct texture_frame){ + .gl = *frame_get_gl_frame(frame), + .destroy = on_destroy_texture_frame, + .userdata = frame, + } + ); + } + + return GST_FLOW_OK; +} + +static void on_appsink_cbs_destroy(void *userdata) { + struct gstplayer *player; + + LOG_DEBUG("on_appsink_cbs_destroy()\n"); + ASSERT_NOT_NULL(userdata); + + player = userdata; + + (void) player; +} + +static GstCaps *caps_for_frame_interface(struct frame_interface *interface) { + GstCaps *caps = gst_caps_new_empty(); + if (caps == NULL) { + return NULL; + } + + /// TODO: Add dmabuf caps here + for_each_format_in_frame_interface(i, format, interface) { + GstVideoFormat gst_format = gst_video_format_from_drm_format(format->format); + if (gst_format == GST_VIDEO_FORMAT_UNKNOWN) { + continue; + } + + gst_caps_append(caps, gst_caps_new_simple("video/x-raw", "format", G_TYPE_STRING, gst_video_format_to_string(gst_format), NULL)); + } + + return caps; +} + +static gboolean on_appsink_new_event(GstAppSink *appsink, gpointer userdata) { + (void) userdata; + + GstMiniObject *obj; + + do { + obj = gst_app_sink_try_pull_object(appsink, 0); + if (obj == NULL) { + return FALSE; + } + + if (!GST_IS_EVENT(obj)) { + LOG_DEBUG("Got non-event from gst_app_sink_try_pull_object.\n"); + } + } while (obj && !GST_IS_EVENT(obj)); + + // GstEvent *event = GST_EVENT_CAST(obj); + + // char *str = gst_structure_to_string(gst_event_get_structure(event)); + // LOG_DEBUG("Got event: %s\n", str); + // g_free(str); + + gst_mini_object_unref(obj); + + return FALSE; +} + +#if THIS_GSTREAMER_VER >= GSTREAMER_VER(1, 24, 0) +static gboolean on_appsink_propose_allocation(GstAppSink *appsink, GstQuery *query, gpointer userdata) { + (void) appsink; + (void) userdata; + + gst_query_add_allocation_meta(query, GST_VIDEO_META_API_TYPE, NULL); + + return FALSE; +} +#else +static GstPadProbeReturn on_query_appsink_pad(GstPad *pad, GstPadProbeInfo *info, void *userdata) { + GstQuery *query; + + (void) pad; + (void) userdata; + + query = gst_pad_probe_info_get_query(info); + if (query == NULL) { + LOG_DEBUG("Couldn't get query from pad probe info.\n"); + return GST_PAD_PROBE_OK; + } + + if (GST_QUERY_TYPE(query) != GST_QUERY_ALLOCATION) { + return GST_PAD_PROBE_OK; + } + + gst_query_add_allocation_meta(query, GST_VIDEO_META_API_TYPE, NULL); + + return GST_PAD_PROBE_HANDLED; +} +#endif + +GstElement *flutter_gl_texture_sink_new(struct texture *texture, struct gl_renderer *renderer) { + ASSERT_NOT_NULL(texture); + ASSERT_NOT_NULL(renderer); + + struct texture_sink *meta = calloc(1, sizeof(struct texture_sink)); + if (meta == NULL) { + return NULL; + } + + meta->fl_texture = texture; + + GstElement *element = gst_element_factory_make("appsink", "appsink"); + if (element == NULL) { + free(meta); + return NULL; + } + + meta->interface = frame_interface_new(renderer); + if (meta->interface == NULL) { + gst_object_unref(element); + free(meta); + return NULL; + } + + GstCaps *caps = caps_for_frame_interface(meta->interface); + if (caps == NULL) { + frame_interface_unref(meta->interface); + gst_object_unref(element); + free(meta); + return NULL; + } + + GstBaseSink *basesink = GST_BASE_SINK_CAST(element); + GstAppSink *appsink = GST_APP_SINK_CAST(element); + + gst_base_sink_set_max_lateness(basesink, 20 * GST_MSECOND); + gst_base_sink_set_qos_enabled(basesink, TRUE); + gst_base_sink_set_sync(basesink, TRUE); + gst_app_sink_set_max_buffers(appsink, 2); + gst_app_sink_set_emit_signals(appsink, TRUE); + gst_app_sink_set_drop(appsink, FALSE); + gst_app_sink_set_caps(appsink, caps); + gst_caps_unref(caps); + + GstAppSinkCallbacks cbs; + memset(&cbs, 0, sizeof(cbs)); + + cbs.new_preroll = on_appsink_new_preroll; + cbs.new_sample = on_appsink_new_sample; + cbs.eos = on_appsink_eos; +#if THIS_GSTREAMER_VER >= GSTREAMER_VER(1, 20, 0) + cbs.new_event = on_appsink_new_event; +#endif + +#if THIS_GSTREAMER_VER >= GSTREAMER_VER(1, 24, 0) + cbs.propose_allocation = on_appsink_propose_allocation; +#else + GstPad *pad = gst_element_get_static_pad(sink, "sink"); + if (pad == NULL) { + LOG_ERROR("Couldn't get static pad `sink` from appsink.\n"); + frame_interface_unref(meta->interface); + gst_object_unref(element); + free(meta); + return NULL; + } + + gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_QUERY_DOWNSTREAM, on_query_appsink_pad, NULL); +#endif + + gst_app_sink_set_callbacks( + GST_APP_SINK(appsink), + &cbs, + meta, + on_appsink_cbs_destroy + ); + + return element; +} diff --git a/src/plugins/gstreamer_video_player/frame.c b/src/plugins/gstreamer_video_player/frame.c index 98370fea..24182a84 100644 --- a/src/plugins/gstreamer_video_player/frame.c +++ b/src/plugins/gstreamer_video_player/frame.c @@ -21,9 +21,6 @@ #define MAX_N_PLANES 4 -#define GSTREAMER_VER(major, minor, patch) ((((major) &0xFF) << 16) | (((minor) &0xFF) << 8) | ((patch) &0xFF)) -#define THIS_GSTREAMER_VER GSTREAMER_VER(LIBGSTREAMER_VERSION_MAJOR, LIBGSTREAMER_VERSION_MINOR, LIBGSTREAMER_VERSION_PATCH) - #define DRM_FOURCC_FORMAT "c%c%c%c" #define DRM_FOURCC_ARGS(format) (format) & 0xFF, ((format) >> 8) & 0xFF, ((format) >> 16) & 0xFF, ((format) >> 24) & 0xFF @@ -31,14 +28,13 @@ struct video_frame { GstSample *sample; struct frame_interface *interface; - + uint32_t drm_format; int n_dmabuf_fds; int dmabuf_fds[MAX_N_PLANES]; EGLImageKHR image; - size_t width, height; struct gl_texture_frame gl_frame; }; @@ -359,7 +355,7 @@ UNUSED int dup_gst_buffer_range_as_dmabuf(struct gbm_device *gbm_device, GstBuff return -1; } -// Create a square texture large enough to fit our bytes instead of one with only one huge row, + // Create a square texture large enough to fit our bytes instead of one with only one huge row, // because some drivers have limitations on the row length. (Intel) uint32_t dim = (uint32_t) ceil(sqrt(map_info.size)); @@ -419,7 +415,7 @@ UNUSED int dup_gst_memory_as_dmabuf(struct gbm_device *gbm_device, GstMemory *me return -1; } -// Create a square texture large enough to fit our bytes instead of one with only one huge row, + // Create a square texture large enough to fit our bytes instead of one with only one huge row, // because some drivers have limitations on the row length. (Intel) uint32_t dim = (uint32_t) ceil(sqrt(map_info.size)); @@ -627,14 +623,22 @@ get_plane_infos(GstBuffer *buffer, const GstVideoInfo *info, struct gbm_device * goto fail_close_fds; } + static bool logged_dmabuf_feedback = false; + if (n_memories != 1) { + if (!logged_dmabuf_feedback) { + LOG_DEBUG("INFO: Flutter-Pi is using manual dmabuf uploads to show video frames. This can result in poor performance.\n"); + logged_dmabuf_feedback = true; + } + ok = dup_gst_buffer_range_as_dmabuf(gbm_device, buffer, memory_index, n_memories); if (ok < 0) { - LOG_ERROR("Could not duplicate gstreamer buffer range as dmabuf.\n"); + LOG_ERROR("Could not upload gstreamer buffer range into dmabufs.\n"); ok = EIO; goto fail_close_fds; } + plane_infos[i].fd = ok; } else { memory = gst_buffer_peek_memory(buffer, memory_index); @@ -655,11 +659,16 @@ get_plane_infos(GstBuffer *buffer, const GstVideoInfo *info, struct gbm_device * plane_infos[i].fd = ok; } else { + if (!logged_dmabuf_feedback) { + LOG_DEBUG("INFO: Flutter-Pi is using manual dmabuf uploads to show video frames. This can result in poor performance.\n"); + logged_dmabuf_feedback = true; + } + /// TODO: When duping, duplicate all non-dmabuf memories into one /// gbm buffer instead. ok = dup_gst_memory_as_dmabuf(gbm_device, memory); if (ok < 0) { - LOG_ERROR("Could not duplicate gstreamer memory as dmabuf.\n"); + LOG_ERROR("Could not upload gstreamer memory into dmabuf.\n"); ok = EIO; goto fail_close_fds; } @@ -810,7 +819,7 @@ static EGLint egl_vertical_chroma_siting_from_gst_info(const GstVideoInfo *info) } } -struct video_frame *frame_new(struct frame_interface *interface, GstSample *sample, const GstVideoInfo *info) { +static struct video_frame *frame_new_egl_imported(struct frame_interface *interface, GstSample *sample, const GstVideoInfo *info) { #define PUT_ATTR(_key, _value) \ do { \ assert(attr_index + 2 <= ARRAY_SIZE(attributes)); \ @@ -819,12 +828,14 @@ struct video_frame *frame_new(struct frame_interface *interface, GstSample *samp } while (false) struct video_frame *frame; struct plane_info planes[MAX_N_PLANES]; - GstVideoInfo _info; + GstVideoInfoDmaDrm drm_video_info; + GstVideoInfo video_info; EGLBoolean egl_ok; GstBuffer *buffer; EGLImageKHR egl_image; gboolean gst_ok; uint32_t drm_format; + uint64_t drm_modifier; GstCaps *caps; GLuint texture; GLenum gl_error; @@ -839,6 +850,8 @@ struct video_frame *frame_new(struct frame_interface *interface, GstSample *samp return NULL; } + bool is_drm_video_info = false; + // If we don't have an explicit info given, we determine it from the sample caps. if (info == NULL) { caps = gst_sample_get_caps(sample); @@ -846,11 +859,11 @@ struct video_frame *frame_new(struct frame_interface *interface, GstSample *samp return NULL; } - info = &_info; + is_drm_video_info = gst_video_info_dma_drm_from_caps(&drm_video_info, caps); - gst_ok = gst_video_info_from_caps(&_info, caps); + gst_ok = gst_video_info_from_caps(&drm_video_info, caps); if (gst_ok == FALSE) { - LOG_ERROR("Could not get video info from video sample caps.\n"); + LOG_ERROR("Could not get video info from caps.\n"); return NULL; } } else { @@ -862,16 +875,21 @@ struct video_frame *frame_new(struct frame_interface *interface, GstSample *samp height = GST_VIDEO_INFO_HEIGHT(info); n_planes = GST_VIDEO_INFO_N_PLANES(info); - // query the drm format for this sample - drm_format = drm_format_from_gst_info(info); - if (drm_format == DRM_FORMAT_INVALID) { - LOG_ERROR("Video format has no EGL equivalent.\n"); - return NULL; + if (is_drm_video_info) { + drm_format = drm_video_info.drm_fourcc; + drm_modifier = drm_video_info.drm_modifier; + } else { + drm_modifier = DRM_FORMAT_MOD_LINEAR; + drm_format = drm_format = drm_format_from_gst_info(info); + if (drm_format == DRM_FORMAT_INVALID) { + LOG_ERROR("Video format has no EGL equivalent.\n"); + return NULL; + } } bool external_only; for_each_format_in_frame_interface(i, format, interface) { - if (format->format == drm_format && format->modifier == DRM_FORMAT_MOD_LINEAR) { + if (format->format == drm_format && format->modifier == drm_modifier) { external_only = format->external_only; goto format_supported; } @@ -880,7 +898,7 @@ struct video_frame *frame_new(struct frame_interface *interface, GstSample *samp LOG_ERROR( "Video format is not supported by EGL: %" DRM_FOURCC_FORMAT " (modifier: %" PRIu64 ").\n", DRM_FOURCC_ARGS(drm_format), - (uint64_t) DRM_FORMAT_MOD_LINEAR + (uint64_t) drm_modifier ); return NULL; @@ -1137,6 +1155,29 @@ struct video_frame *frame_new(struct frame_interface *interface, GstSample *samp return NULL; } +static struct video_frame *frame_new_egl_duped(struct frame_interface *interface, GstSample *sample, const GstVideoInfo *info) { + (void) interface; + (void) sample; + (void) info; + return NULL; +} + +struct video_frame *frame_new(struct frame_interface *interface, GstSample *sample, const GstVideoInfo *info) { + struct video_frame *frame; + + frame = frame_new_egl_imported(interface, sample, info); + if (frame != NULL) { + return frame; + } + + frame = frame_new_egl_duped(interface, sample, info); + if (frame != NULL) { + return frame; + } + + return NULL; +} + void frame_destroy(struct video_frame *frame) { EGLBoolean egl_ok; int ok; diff --git a/src/plugins/gstreamer_video_player/player.c b/src/plugins/gstreamer_video_player/player.c index a6555eec..baa2d242 100644 --- a/src/plugins/gstreamer_video_player/player.c +++ b/src/plugins/gstreamer_video_player/player.c @@ -13,7 +13,6 @@ #include #include #include -#include #include "flutter-pi.h" #include "notifier_listener.h" @@ -21,25 +20,8 @@ #include "pluginregistry.h" #include "plugins/gstreamer_video_player.h" #include "texture_registry.h" -#include "util/collection.h" #include "util/logging.h" -#ifdef DEBUG - #define DEBUG_TRACE_BEGIN(player, name) trace_begin(player, name) - #define DEBUG_TRACE_END(player, name) trace_end(player, name) - #define DEBUG_TRACE_INSTANT(player, name) trace_instant(player, name) -#else - #define DEBUG_TRACE_BEGIN(player, name) \ - do { \ - } while (0) - #define DEBUG_TRACE_END(player, name) \ - do { \ - } while (0) - #define DEBUG_TRACE_INSTANT(player, name) \ - do { \ - } while (0) -#endif - #define LOG_GST_SET_STATE_ERROR(_element) \ LOG_ERROR( \ "setting gstreamer playback state failed. gst_element_set_state(element name: %s): GST_STATE_CHANGE_FAILURE\n", \ @@ -51,6 +33,13 @@ GST_ELEMENT_NAME(_element) \ ) +#define LOG_PLAYER_DEBUG(player, fmtstring, ...) LOG_DEBUG("gstplayer-%"PRIi64": " fmtstring, player->debug_id, ##__VA_ARGS__) +#ifdef DEBUG + #define LOG_PLAYER_ERROR(player, fmtstring, ...) LOG_ERROR("gstplayer-%"PRIi64": " fmtstring, player->debug_id, ##__VA_ARGS__) +#else + #define LOG_PLAYER_ERROR(player, fmtstring, ...) LOG_ERROR(fmtstring, ##__VA_ARGS__) +#endif + struct incomplete_video_info { bool has_resolution; bool has_fps; @@ -69,16 +58,23 @@ enum playback_direction { kForward, kBackward }; (playpause_state) == kStepping ? "stepping" : \ "?") + +#ifdef DEBUG +static int64_t allocate_id() { + static atomic_int_fast64_t next_id = 1; + + return atomic_fetch_add_explicit(&next_id, 1, memory_order_relaxed); +} +#endif struct gstplayer { - pthread_mutex_t lock; +#ifdef DEBUG + int64_t debug_id; +#endif struct flutterpi *flutterpi; - void *userdata; + struct tracer *tracer; - char *video_uri; - char *pipeline_description; - - GstStructure *headers; + void *userdata; /** * @brief The desired playback rate that should be used when @ref playpause_state is kPlayingForward. (should be > 0) @@ -143,48 +139,23 @@ struct gstplayer { struct notifier video_info_notifier, buffering_state_notifier, error_notifier; - bool is_initialized; bool has_sent_info; struct incomplete_video_info info; - bool has_gst_info; - GstVideoInfo gst_info; + // bool has_gst_info; + // GstVideoInfo gst_info; struct texture *texture; - int64_t texture_id; - struct frame_interface *frame_interface; - - GstElement *pipeline, *sink; - GstBus *bus; + // GstElement *pipeline, *sink; + // GstBus *bus; sd_event_source *busfd_events; + GstElement *playbin; + bool is_live; }; -#define MAX_N_PLANES 4 -#define MAX_N_EGL_DMABUF_IMAGE_ATTRIBUTES 6 + 6 * MAX_N_PLANES + 1 - -UNUSED static inline void lock(struct gstplayer *player) { - pthread_mutex_lock(&player->lock); -} - -UNUSED static inline void unlock(struct gstplayer *player) { - pthread_mutex_unlock(&player->lock); -} - -UNUSED static inline void trace_instant(struct gstplayer *player, const char *name) { - return flutterpi_trace_event_instant(player->flutterpi, name); -} - -UNUSED static inline void trace_begin(struct gstplayer *player, const char *name) { - return flutterpi_trace_event_begin(player->flutterpi, name); -} - -UNUSED static inline void trace_end(struct gstplayer *player, const char *name) { - return flutterpi_trace_event_end(player->flutterpi, name); -} - static int maybe_send_info(struct gstplayer *player) { struct video_info *duped; @@ -205,14 +176,14 @@ static void fetch_duration(struct gstplayer *player) { gboolean ok; int64_t duration; - ok = gst_element_query_duration(player->pipeline, GST_FORMAT_TIME, &duration); + ok = gst_element_query_duration(player->playbin, GST_FORMAT_TIME, &duration); if (ok == FALSE) { if (player->is_live) { player->info.info.duration_ms = INT64_MAX; player->info.has_duration = true; return; } else { - LOG_ERROR("Could not fetch duration. (gst_element_query_duration)\n"); + LOG_PLAYER_ERROR(player, "Could not fetch duration. (gst_element_query_duration)\n"); return; } } @@ -227,7 +198,7 @@ static void fetch_seeking(struct gstplayer *player) { int64_t seek_begin, seek_end; seeking_query = gst_query_new_seeking(GST_FORMAT_TIME); - ok = gst_element_query(player->pipeline, seeking_query); + ok = gst_element_query(player->playbin, seeking_query); if (ok == FALSE) { if (player->is_live) { player->info.info.can_seek = false; @@ -236,7 +207,7 @@ static void fetch_seeking(struct gstplayer *player) { player->info.has_seeking_info = true; return; } else { - LOG_DEBUG("Could not query seeking info. (gst_element_query)\n"); + LOG_PLAYER_DEBUG(player, "Could not query seeking info. (gst_element_query)\n"); return; } } @@ -251,7 +222,7 @@ static void fetch_seeking(struct gstplayer *player) { player->info.has_seeking_info = true; } -static void update_buffering_state(struct gstplayer *player) { +static void update_buffering_state(struct gstplayer *player, GstObject *element) { struct buffering_state *state; GstBufferingMode mode; GstQuery *query; @@ -260,9 +231,9 @@ static void update_buffering_state(struct gstplayer *player) { int n_ranges, percent, avg_in, avg_out; query = gst_query_new_buffering(GST_FORMAT_TIME); - ok = gst_element_query(player->pipeline, query); + ok = gst_element_query(GST_ELEMENT(element), query); if (ok == FALSE) { - LOG_ERROR("Could not query buffering state. (gst_element_query)\n"); + LOG_PLAYER_DEBUG(player, "Could not query precise buffering state.\n"); goto fail_unref_query; } @@ -311,10 +282,6 @@ static void update_buffering_state(struct gstplayer *player) { gst_query_unref(query); } -static int init(struct gstplayer *player, bool force_sw_decoders); - -static void maybe_deinit(struct gstplayer *player); - static int apply_playback_state(struct gstplayer *player) { GstStateChangeReturn ok; GstState desired_state, current_state, pending_state; @@ -334,22 +301,23 @@ static int apply_playback_state(struct gstplayer *player) { if (player->has_desired_position) { position = player->desired_position_ms * GST_MSECOND; } else { - ok = gst_element_query_position(GST_ELEMENT(player->pipeline), GST_FORMAT_TIME, &position); + ok = gst_element_query_position(GST_ELEMENT(player->playbin), GST_FORMAT_TIME, &position); if (ok == FALSE) { - LOG_ERROR("Could not get the current playback position to apply the playback speed.\n"); + LOG_PLAYER_ERROR(player, "Could not get the current playback position to apply the playback speed.\n"); return EIO; } } if (player->direction == kForward) { - LOG_DEBUG( + LOG_PLAYER_DEBUG( + player, "gst_element_seek(..., rate: %f, start: %" GST_TIME_FORMAT ", end: %" GST_TIME_FORMAT ", ...)\n", desired_rate, GST_TIME_ARGS(position), GST_TIME_ARGS(GST_CLOCK_TIME_NONE) ); ok = gst_element_seek( - GST_ELEMENT(player->pipeline), + GST_ELEMENT(player->playbin), desired_rate, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | @@ -360,7 +328,8 @@ static int apply_playback_state(struct gstplayer *player) { GST_CLOCK_TIME_NONE ); if (ok == FALSE) { - LOG_ERROR( + LOG_PLAYER_ERROR( + player, "Could not set the new playback speed / playback position (speed: %f, pos: %" GST_TIME_FORMAT ").\n", desired_rate, GST_TIME_ARGS(position) @@ -368,14 +337,15 @@ static int apply_playback_state(struct gstplayer *player) { return EIO; } } else { - LOG_DEBUG( + LOG_PLAYER_DEBUG( + player, "gst_element_seek(..., rate: %f, start: %" GST_TIME_FORMAT ", end: %" GST_TIME_FORMAT ", ...)\n", desired_rate, GST_TIME_ARGS(0), GST_TIME_ARGS(position) ); ok = gst_element_seek( - GST_ELEMENT(player->pipeline), + GST_ELEMENT(player->playbin), desired_rate, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | @@ -387,7 +357,8 @@ static int apply_playback_state(struct gstplayer *player) { ); if (ok == FALSE) { - LOG_ERROR( + LOG_PLAYER_ERROR( + player, "Could not set the new playback speed / playback position (speed: %f, pos: %" GST_TIME_FORMAT ").\n", desired_rate, GST_TIME_ARGS(position) @@ -401,16 +372,13 @@ static int apply_playback_state(struct gstplayer *player) { player->has_desired_position = false; } - DEBUG_TRACE_BEGIN(player, "gst_element_get_state"); - ok = gst_element_get_state(player->pipeline, ¤t_state, &pending_state, 0); - DEBUG_TRACE_END(player, "gst_element_get_state"); - + ok = gst_element_get_state(player->playbin, ¤t_state, &pending_state, 0); if (ok == GST_STATE_CHANGE_FAILURE) { - LOG_ERROR( + LOG_PLAYER_DEBUG( + player, "last gstreamer pipeline state change failed. gst_element_get_state(element name: %s): GST_STATE_CHANGE_FAILURE\n", - GST_ELEMENT_NAME(player->pipeline) + GST_ELEMENT_NAME(player->playbin) ); - DEBUG_TRACE_END(player, "apply_playback_state"); return EIO; } @@ -418,713 +386,506 @@ static int apply_playback_state(struct gstplayer *player) { if (current_state == desired_state) { // we're already in the desired state, and we're also not changing it // no need to do anything. - LOG_DEBUG( + LOG_PLAYER_DEBUG( + player, "apply_playback_state(playing: %s): already in desired state and none pending\n", PLAYPAUSE_STATE_AS_STRING(player->playpause_state) ); - DEBUG_TRACE_END(player, "apply_playback_state"); return 0; } - LOG_DEBUG( + LOG_PLAYER_DEBUG( + player, "apply_playback_state(playing: %s): setting state to %s\n", PLAYPAUSE_STATE_AS_STRING(player->playpause_state), gst_element_state_get_name(desired_state) ); - DEBUG_TRACE_BEGIN(player, "gst_element_set_state"); - ok = gst_element_set_state(player->pipeline, desired_state); - DEBUG_TRACE_END(player, "gst_element_set_state"); + ok = gst_element_set_state(player->playbin, desired_state); if (ok == GST_STATE_CHANGE_FAILURE) { - LOG_GST_SET_STATE_ERROR(player->pipeline); - DEBUG_TRACE_END(player, "apply_playback_state"); + LOG_GST_SET_STATE_ERROR(player->playbin); return EIO; } } else if (pending_state != desired_state) { // queue to be executed when pending async state change completes /// TODO: Implement properly - LOG_DEBUG( + LOG_PLAYER_DEBUG( + player, "apply_playback_state(playing: %s): async state change in progress, setting state to %s\n", PLAYPAUSE_STATE_AS_STRING(player->playpause_state), gst_element_state_get_name(desired_state) ); - DEBUG_TRACE_BEGIN(player, "gst_element_set_state"); - ok = gst_element_set_state(player->pipeline, desired_state); - DEBUG_TRACE_END(player, "gst_element_set_state"); - + ok = gst_element_set_state(player->playbin, desired_state); if (ok == GST_STATE_CHANGE_FAILURE) { - LOG_GST_SET_STATE_ERROR(player->pipeline); - DEBUG_TRACE_END(player, "apply_playback_state"); + LOG_GST_SET_STATE_ERROR(player->playbin); return EIO; } } - - DEBUG_TRACE_END(player, "apply_playback_state"); return 0; } -static void on_bus_message(struct gstplayer *player, GstMessage *msg) { - GstState old, current, pending, requested; +static void on_gstreamer_error_message(struct gstplayer *player, GstMessage *msg) { + (void) player; + + GError *error; + gchar *debug_info; + + gst_message_parse_error(msg, &error, &debug_info); + + LOG_PLAYER_ERROR( + player, + "gstreamer error: code: %d, domain: %s, msg: %s (debug info: %s)\n", + error->code, + g_quark_to_string(error->domain), + error->message, + debug_info + ); + g_clear_error(&error); + g_free(debug_info); +} + +static void on_gstreamer_warning_message(struct gstplayer *player, GstMessage *msg) { + (void) player; + + GError *error; + gchar *debug_info; + + gst_message_parse_warning(msg, &error, &debug_info); + + LOG_PLAYER_ERROR( + player, + "gstreamer warning: code: %d, domain: %s, msg: %s (debug info: %s)\n", + error->code, + g_quark_to_string(error->domain), + error->message, + debug_info + ); + g_clear_error(&error); + g_free(debug_info); +} + +static void on_gstreamer_info_message(struct gstplayer *player, GstMessage *msg) { GError *error; gchar *debug_info; - DEBUG_TRACE_BEGIN(player, "on_bus_message"); + gst_message_parse_info(msg, &error, &debug_info); + + LOG_PLAYER_DEBUG(player, "gstreamer info: %s (debug info: %s)\n", error->message, debug_info); + g_clear_error(&error); + g_free(debug_info); +} + +static void on_buffering_message(struct gstplayer *player, GstMessage *msg) { + GstBufferingMode mode; + int64_t buffering_left; + int percent, avg_in, avg_out; + + gst_message_parse_buffering(msg, &percent); + gst_message_parse_buffering_stats(msg, &mode, &avg_in, &avg_out, &buffering_left); + + LOG_PLAYER_DEBUG( + player, + "buffering, src: %s, percent: %d, mode: %s, avg in: %d B/s, avg out: %d B/s, %" GST_TIME_FORMAT "\n", + GST_MESSAGE_SRC_NAME(msg), + percent, + mode == GST_BUFFERING_STREAM ? "stream" : + mode == GST_BUFFERING_DOWNLOAD ? "download" : + mode == GST_BUFFERING_TIMESHIFT ? "timeshift" : + mode == GST_BUFFERING_LIVE ? "live" : + "?", + avg_in, + avg_out, + GST_TIME_ARGS(buffering_left * GST_MSECOND) + ); + + /// TODO: GST_MESSAGE_BUFFERING is only emitted when we actually need to wait on some buffering till we can resume the playback. + /// However, the info we send to the callback also contains information on the buffered video ranges. + /// That information is constantly changing, but we only notify the player about it when we actively wait for the buffer to be filled. + update_buffering_state(player, GST_MESSAGE_SRC(msg)); +} + +static void on_state_change_message(struct gstplayer *player, GstMessage *msg) { + GstState old, current, pending; + + gst_message_parse_state_changed(msg, &old, ¤t, &pending); + + if (GST_MESSAGE_SRC(msg) == GST_OBJECT(player->playbin)) { + LOG_PLAYER_DEBUG( + player, + "playbin state changed: src: %s, old: %s, current: %s, pending: %s\n", + GST_MESSAGE_SRC_NAME(msg), + gst_element_state_get_name(old), + gst_element_state_get_name(current), + gst_element_state_get_name(pending) + ); + + if (!player->info.has_duration && (current == GST_STATE_PAUSED || current == GST_STATE_PLAYING)) { + // it's our pipeline that changed to either playing / paused, and we don't have info about our video duration yet. + // get that info now. + // technically we can already fetch the duration when the decodebin changed to PAUSED state. + fetch_duration(player); + fetch_seeking(player); + maybe_send_info(player); + } + } +} + +static void on_application_message(struct gstplayer *player, GstMessage *msg) { + if (gst_message_has_name(msg, "appsink-eos")) { + if (player->looping) { + // we have an appsink end of stream event + // and we should be looping, so seek back to start + LOG_PLAYER_DEBUG(player, "appsink eos, seeking back to segment start (flushing)\n"); + gst_element_seek( + GST_ELEMENT(player->playbin), + player->current_playback_rate, + GST_FORMAT_TIME, + GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE, + GST_SEEK_TYPE_SET, + 0, + GST_SEEK_TYPE_SET, + GST_CLOCK_TIME_NONE + ); + + apply_playback_state(player); + } + } else if (gst_message_has_name(msg, "video-info")) { + const GstStructure *structure = gst_message_get_structure(msg); + + const GValue *value = gst_structure_get_value(structure, "info"); + assert(G_VALUE_HOLDS_POINTER(value)); + + GstVideoInfo *info = g_value_get_pointer(value); + + player->info.info.width = GST_VIDEO_INFO_WIDTH(info); + player->info.info.height = GST_VIDEO_INFO_HEIGHT(info); + player->info.info.fps = (double) GST_VIDEO_INFO_FPS_N(info) / GST_VIDEO_INFO_FPS_D(info); + player->info.has_resolution = true; + player->info.has_fps = true; + + gst_video_info_free(info); + + LOG_PLAYER_DEBUG(player, "Determined resolution: %d x %d and framerate: %f\n", player->info.info.width, player->info.info.height, player->info.info.fps); + } else if (gst_message_has_name(msg, "about-to-finish")) { + LOG_PLAYER_DEBUG(player, "Got about-to-finish signal\n"); + } +} + +static void on_bus_message(struct gstplayer *player, GstMessage *msg) { switch (GST_MESSAGE_TYPE(msg)) { case GST_MESSAGE_ERROR: - gst_message_parse_error(msg, &error, &debug_info); - - LOG_ERROR( - "gstreamer error: code: %d, domain: %s, msg: %s (debug info: %s)\n", - error->code, - g_quark_to_string(error->domain), - error->message, - debug_info - ); - g_clear_error(&error); - g_free(debug_info); + on_gstreamer_error_message(player, msg); break; case GST_MESSAGE_WARNING: - gst_message_parse_warning(msg, &error, &debug_info); - LOG_ERROR("gstreamer warning: %s (debug info: %s)\n", error->message, debug_info); - g_clear_error(&error); - g_free(debug_info); + on_gstreamer_warning_message(player, msg); break; case GST_MESSAGE_INFO: - gst_message_parse_info(msg, &error, &debug_info); - LOG_DEBUG("gstreamer info: %s (debug info: %s)\n", error->message, debug_info); - g_clear_error(&error); - g_free(debug_info); + on_gstreamer_info_message(player, msg); break; - case GST_MESSAGE_BUFFERING: { - GstBufferingMode mode; - int64_t buffering_left; - int percent, avg_in, avg_out; - - gst_message_parse_buffering(msg, &percent); - gst_message_parse_buffering_stats(msg, &mode, &avg_in, &avg_out, &buffering_left); - - LOG_DEBUG( - "buffering, src: %s, percent: %d, mode: %s, avg in: %d B/s, avg out: %d B/s, %" GST_TIME_FORMAT "\n", - GST_MESSAGE_SRC_NAME(msg), - percent, - mode == GST_BUFFERING_STREAM ? "stream" : - mode == GST_BUFFERING_DOWNLOAD ? "download" : - mode == GST_BUFFERING_TIMESHIFT ? "timeshift" : - mode == GST_BUFFERING_LIVE ? "live" : - "?", - avg_in, - avg_out, - GST_TIME_ARGS(buffering_left * GST_MSECOND) - ); - - /// TODO: GST_MESSAGE_BUFFERING is only emitted when we actually need to wait on some buffering till we can resume the playback. - /// However, the info we send to the callback also contains information on the buffered video ranges. - /// That information is constantly changing, but we only notify the player about it when we actively wait for the buffer to be filled. - DEBUG_TRACE_BEGIN(player, "update_buffering_state"); - update_buffering_state(player); - DEBUG_TRACE_END(player, "update_buffering_state"); - + case GST_MESSAGE_BUFFERING: + on_buffering_message(player, msg); break; - }; case GST_MESSAGE_STATE_CHANGED: - gst_message_parse_state_changed(msg, &old, ¤t, &pending); - LOG_DEBUG( - "state-changed: src: %s, old: %s, current: %s, pending: %s\n", - GST_MESSAGE_SRC_NAME(msg), - gst_element_state_get_name(old), - gst_element_state_get_name(current), - gst_element_state_get_name(pending) - ); - - if (GST_MESSAGE_SRC(msg) == GST_OBJECT(player->pipeline)) { - if (!player->info.has_duration && (current == GST_STATE_PAUSED || current == GST_STATE_PLAYING)) { - // it's our pipeline that changed to either playing / paused, and we don't have info about our video duration yet. - // get that info now. - // technically we can already fetch the duration when the decodebin changed to PAUSED state. - DEBUG_TRACE_BEGIN(player, "fetch video info"); - fetch_duration(player); - fetch_seeking(player); - maybe_send_info(player); - DEBUG_TRACE_END(player, "fetch video info"); - } - } + on_state_change_message(player, msg); break; case GST_MESSAGE_ASYNC_DONE: break; case GST_MESSAGE_LATENCY: - LOG_DEBUG("gstreamer: redistributing latency\n"); - DEBUG_TRACE_BEGIN(player, "gst_bin_recalculate_latency"); - gst_bin_recalculate_latency(GST_BIN(player->pipeline)); - DEBUG_TRACE_END(player, "gst_bin_recalculate_latency"); + LOG_PLAYER_DEBUG(player, "gstreamer: redistributing latency\n"); + gst_bin_recalculate_latency(GST_BIN(player->playbin)); break; - case GST_MESSAGE_EOS: LOG_DEBUG("end of stream, src: %s\n", GST_MESSAGE_SRC_NAME(msg)); break; + case GST_MESSAGE_EOS: + LOG_PLAYER_DEBUG(player, "end of stream, src: %s\n", GST_MESSAGE_SRC_NAME(msg)); + break; + + case GST_MESSAGE_REQUEST_STATE: { + GstState requested; - case GST_MESSAGE_REQUEST_STATE: gst_message_parse_request_state(msg, &requested); - LOG_DEBUG( + LOG_PLAYER_DEBUG( + player, "gstreamer state change to %s was requested by %s\n", gst_element_state_get_name(requested), GST_MESSAGE_SRC_NAME(msg) ); - DEBUG_TRACE_BEGIN(player, "gst_element_set_state"); - gst_element_set_state(GST_ELEMENT(player->pipeline), requested); - DEBUG_TRACE_END(player, "gst_element_set_state"); + gst_element_set_state(GST_ELEMENT(player->playbin), requested); break; + } case GST_MESSAGE_APPLICATION: - if (player->looping && gst_message_has_name(msg, "appsink-eos")) { - // we have an appsink end of stream event - // and we should be looping, so seek back to start - LOG_DEBUG("appsink eos, seeking back to segment start (flushing)\n"); - DEBUG_TRACE_BEGIN(player, "gst_element_seek"); - gst_element_seek( - GST_ELEMENT(player->pipeline), - player->current_playback_rate, - GST_FORMAT_TIME, - GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE, - GST_SEEK_TYPE_SET, - 0, - GST_SEEK_TYPE_SET, - GST_CLOCK_TIME_NONE - ); - DEBUG_TRACE_END(player, "gst_element_seek"); - - apply_playback_state(player); - } + on_application_message(player, msg); break; - default: LOG_DEBUG("gstreamer message: %s, src: %s\n", GST_MESSAGE_TYPE_NAME(msg), GST_MESSAGE_SRC_NAME(msg)); break; + default: + LOG_PLAYER_DEBUG(player, "gstreamer message: %s, src: %s\n", GST_MESSAGE_TYPE_NAME(msg), GST_MESSAGE_SRC_NAME(msg)); + break; } - DEBUG_TRACE_END(player, "on_bus_message"); return; } static int on_bus_fd_ready(sd_event_source *s, int fd, uint32_t revents, void *userdata) { - struct gstplayer *player; - GstMessage *msg; - (void) s; (void) fd; (void) revents; - player = userdata; - - DEBUG_TRACE_BEGIN(player, "on_bus_fd_ready"); + struct gstplayer *player = userdata; - msg = gst_bus_pop(player->bus); + GstMessage *msg = gst_bus_pop(gst_element_get_bus(player->playbin)); if (msg != NULL) { on_bus_message(player, msg); gst_message_unref(msg); } - DEBUG_TRACE_END(player, "on_bus_fd_ready"); - return 0; } -static GstPadProbeReturn on_query_appsink(GstPad *pad, GstPadProbeInfo *info, void *userdata) { - GstQuery *query; +void on_source_setup(GstElement *playbin, GstElement *source, gpointer userdata) { + (void) playbin; - (void) pad; - (void) userdata; - - query = gst_pad_probe_info_get_query(info); - if (query == NULL) { - LOG_DEBUG("Couldn't get query from pad probe info.\n"); - return GST_PAD_PROBE_OK; - } + ASSERT_NOT_NULL(userdata); - if (GST_QUERY_TYPE(query) != GST_QUERY_ALLOCATION) { - return GST_PAD_PROBE_OK; + if (g_object_class_find_property(G_OBJECT_GET_CLASS(source), "extra-headers") != NULL) { + g_object_set(source, "extra-headers", (GstStructure *) userdata, NULL); + } else { + LOG_ERROR("Failed to set custom HTTP headers because gstreamer source element has no 'extra-headers' property.\n"); } - - gst_query_add_allocation_meta(query, GST_VIDEO_META_API_TYPE, NULL); - - return GST_PAD_PROBE_HANDLED; } -static void on_element_added(GstBin *bin, GstElement *element, void *userdata) { - GstElementFactory *factory; - const char *factory_name; +typedef enum { + GST_PLAY_FLAG_VIDEO = (1 << 0), + GST_PLAY_FLAG_AUDIO = (1 << 1), + GST_PLAY_FLAG_TEXT = (1 << 2) +} GstPlayFlags; +static void on_element_setup(GstElement *playbin, GstElement *element, gpointer userdata) { + (void) playbin; (void) userdata; - (void) bin; - factory = gst_element_get_factory(element); - factory_name = gst_plugin_feature_get_name(factory); + GstElementFactory *factory = gst_element_get_factory(element); + if (factory == NULL) { + return; + } + + const char *factory_name = gst_plugin_feature_get_name(factory); if (g_str_has_prefix(factory_name, "v4l2video") && g_str_has_suffix(factory_name, "dec")) { gst_util_set_object_arg(G_OBJECT(element), "capture-io-mode", "dmabuf"); - fprintf(stderr, "[gstreamer video player] found gstreamer V4L2 video decoder element with name \"%s\"\n", GST_OBJECT_NAME(element)); + LOG_DEBUG("Applied capture-io-mode = dmabuf\n"); } } -static GstPadProbeReturn on_probe_pad(GstPad *pad, GstPadProbeInfo *info, void *userdata) { - struct gstplayer *player; - GstEvent *event; - GstCaps *caps; - gboolean ok; - - (void) pad; - - player = userdata; - event = GST_PAD_PROBE_INFO_EVENT(info); +static void on_about_to_finish(GstElement *playbin, gpointer userdata) { + (void) userdata; - if (GST_EVENT_TYPE(event) != GST_EVENT_CAPS) { - return GST_PAD_PROBE_OK; + GstBus *bus = gst_element_get_bus(playbin); + if (bus == NULL) { + LOG_ERROR("Could not acquire bus to post about-to-finish message.\n"); + return; } - gst_event_parse_caps(event, &caps); - if (caps == NULL) { - LOG_ERROR("gstreamer: caps event without caps\n"); - return GST_PAD_PROBE_OK; + GstStructure *s = gst_structure_new_empty("about-to-finish"); + if (s == NULL) { + LOG_ERROR("Could not create about-to-finish gst structure.\n"); + gst_object_unref(bus); + return; } - ok = gst_video_info_from_caps(&player->gst_info, caps); - if (!ok) { - LOG_ERROR("gstreamer: caps event with invalid video caps\n"); - return GST_PAD_PROBE_OK; + GstMessage *msg = gst_message_new_application(GST_OBJECT(playbin), s); + if (msg == NULL) { + LOG_ERROR("Could not create about-to-finish gst message.\n"); + gst_structure_free(s); + gst_object_unref(bus); + return; } - player->has_gst_info = true; - - LOG_DEBUG( - "on_probe_pad, fps: %f, res: % 4d x % 4d, format: %s\n", - (double) GST_VIDEO_INFO_FPS_N(&player->gst_info) / GST_VIDEO_INFO_FPS_D(&player->gst_info), - GST_VIDEO_INFO_WIDTH(&player->gst_info), - GST_VIDEO_INFO_HEIGHT(&player->gst_info), - gst_video_format_to_string(player->gst_info.finfo->format) - ); - - player->info.info.width = GST_VIDEO_INFO_WIDTH(&player->gst_info); - player->info.info.height = GST_VIDEO_INFO_HEIGHT(&player->gst_info); - player->info.info.fps = (double) GST_VIDEO_INFO_FPS_N(&player->gst_info) / GST_VIDEO_INFO_FPS_D(&player->gst_info); - player->info.has_resolution = true; - player->info.has_fps = true; - maybe_send_info(player); - - return GST_PAD_PROBE_OK; -} - -static void on_destroy_texture_frame(const struct texture_frame *texture_frame, void *userdata) { - struct video_frame *frame; - - (void) texture_frame; - - ASSERT_NOT_NULL(texture_frame); - ASSERT_NOT_NULL(userdata); - - frame = userdata; + gboolean ok = gst_bus_post(bus, msg); + if (ok != TRUE) { + LOG_ERROR("Could not notify player about about-to-finish signal.\n"); + } - frame_destroy(frame); + gst_object_unref(bus); } -static void on_appsink_eos(GstAppSink *appsink, void *userdata) { - gboolean ok; - - ASSERT_NOT_NULL(appsink); - ASSERT_NOT_NULL(userdata); - - (void) userdata; - - LOG_DEBUG("on_appsink_eos()\n"); +static GstPadProbeReturn on_video_sink_event(GstPad *pad, GstPadProbeInfo *info, gpointer userdata) { + GstBus *bus = userdata; + + (void) pad; - // this method is called from the streaming thread. - // we shouldn't access the player directly here, it could change while we use it. - // post a message to the gstreamer bus instead, will be handled by - // @ref on_bus_message. - ok = gst_element_post_message( - GST_ELEMENT(appsink), - gst_message_new_application(GST_OBJECT(appsink), gst_structure_new_empty("appsink-eos")) - ); - if (ok == FALSE) { - LOG_ERROR("Could not post appsink end-of-stream event to the message bus.\n"); + GstEvent *event = gst_pad_probe_info_get_event(info); + if (event == NULL) { + return GST_PAD_PROBE_OK; } -} -static GstFlowReturn on_appsink_new_preroll(GstAppSink *appsink, void *userdata) { - struct video_frame *frame; - struct gstplayer *player; - GstSample *sample; - - ASSERT_NOT_NULL(appsink); - ASSERT_NOT_NULL(userdata); - - player = userdata; - - sample = gst_app_sink_try_pull_preroll(appsink, 0); - if (sample == NULL) { - LOG_ERROR("gstreamer returned a NULL sample.\n"); - return GST_FLOW_ERROR; + if (GST_EVENT_TYPE(event) != GST_EVENT_CAPS) { + return GST_PAD_PROBE_OK; } - /// TODO: Attempt to upload using gst_gl_upload here - frame = frame_new(player->frame_interface, sample, player->has_gst_info ? &player->gst_info : NULL); + GstCaps *caps = NULL; + gst_event_parse_caps(event, &caps); - gst_sample_unref(sample); + if (!caps) { + LOG_ERROR("Could not parse caps event.\n"); + return GST_PAD_PROBE_OK; + } - if (frame != NULL) { - texture_push_frame( - player->texture, - &(struct texture_frame){ - .gl = *frame_get_gl_frame(frame), - .destroy = on_destroy_texture_frame, - .userdata = frame, - } - ); + GstVideoInfo *videoinfo = gst_video_info_new_from_caps(caps); + if (!videoinfo) { + LOG_ERROR("Could not determine video properties of caps event.\n"); + return GST_PAD_PROBE_OK; } - return GST_FLOW_OK; -} + GValue v = G_VALUE_INIT; + g_value_init(&v, G_TYPE_POINTER); + g_value_set_pointer(&v, videoinfo); -static GstFlowReturn on_appsink_new_sample(GstAppSink *appsink, void *userdata) { - struct video_frame *frame; - struct gstplayer *player; - GstSample *sample; + GstStructure *msg_structure = gst_structure_new_empty("video-info"); + gst_structure_set_value(msg_structure, "info", &v); - ASSERT_NOT_NULL(appsink); - ASSERT_NOT_NULL(userdata); + gst_bus_post(bus, gst_message_new_application(GST_OBJECT(pad), msg_structure)); - player = userdata; + // We're just interested in the caps event. + // Once we have that, we can unlisten. + return GST_PAD_PROBE_REMOVE; +} - /// TODO: Attempt to upload using gst_gl_upload here - sample = gst_app_sink_try_pull_sample(appsink, 0); - if (sample == NULL) { - LOG_ERROR("gstreamer returned a NULL sample.\n"); - return GST_FLOW_ERROR; +static struct gstplayer *gstplayer_new_v2(struct flutterpi *flutterpi, const char *uri, void *userdata, bool play_video, bool play_audio, bool subtitles, GstStructure *headers) { + struct gstplayer *p = calloc(1, sizeof(struct gstplayer)); + if (p == NULL) { + return NULL; } + +#ifdef DEBUG + p->debug_id = allocate_id(); +#endif + p->userdata = userdata; - frame = frame_new(player->frame_interface, sample, player->has_gst_info ? &player->gst_info : NULL); - - gst_sample_unref(sample); + value_notifier_init(&p->video_info_notifier, NULL, free); + value_notifier_init(&p->buffering_state_notifier, NULL, free); + change_notifier_init(&p->error_notifier); - if (frame != NULL) { - texture_push_frame( - player->texture, - &(struct texture_frame){ - .gl = *frame_get_gl_frame(frame), - .destroy = on_destroy_texture_frame, - .userdata = frame, - } - ); + /// TODO: Use playbin or playbin3? + p->playbin = gst_element_factory_make("playbin3", "playbin"); + if (p->playbin == NULL) { + LOG_PLAYER_ERROR(p, "Couldn't create playbin instance.\n"); + goto fail_free_p; } - return GST_FLOW_OK; -} + g_object_set(p->playbin, "uri", uri, NULL); -static void on_appsink_cbs_destroy(void *userdata) { - struct gstplayer *player; + gint flags = 0; - LOG_DEBUG("on_appsink_cbs_destroy()\n"); - ASSERT_NOT_NULL(userdata); + g_object_get(p->playbin, "flags", &flags, NULL); - player = userdata; - - (void) player; -} - -void on_source_setup(GstElement *bin, GstElement *source, gpointer userdata) { - (void) bin; - - if (g_object_class_find_property(G_OBJECT_GET_CLASS(source), "extra-headers") != NULL) { - g_object_set(source, "extra-headers", (GstStructure *) userdata, NULL); + if (play_video) { + flags |= GST_PLAY_FLAG_VIDEO; } else { - LOG_ERROR("Failed to set custom HTTP headers because gstreamer source element has no 'extra-headers' property.\n"); + flags &= ~GST_PLAY_FLAG_VIDEO; } -} - -static int init(struct gstplayer *player, bool force_sw_decoders) { - GstStateChangeReturn state_change_return; - sd_event_source *busfd_event_source; - GstElement *pipeline, *sink, *src; - GstBus *bus; - GstPad *pad; - GPollFD fd; - GError *error = NULL; - int ok; - - static const char *default_pipeline_descr = "uridecodebin name=\"src\" ! video/x-raw ! appsink sync=true name=\"sink\""; - const char *pipeline_descr; - if (player->pipeline_description != NULL) { - pipeline_descr = player->pipeline_description; + if (play_audio) { + flags |= GST_PLAY_FLAG_AUDIO; } else { - pipeline_descr = default_pipeline_descr; - } - - pipeline = gst_parse_launch(pipeline_descr, &error); - if (pipeline == NULL) { - LOG_ERROR("Could create GStreamer pipeline from description: %s (pipeline: `%s`)\n", error->message, pipeline_descr); - return error->code; - } - - sink = gst_bin_get_by_name(GST_BIN(pipeline), "sink"); - if (sink == NULL) { - LOG_ERROR("Couldn't find appsink in pipeline bin.\n"); - ok = EINVAL; - goto fail_unref_pipeline; + flags &= ~GST_PLAY_FLAG_AUDIO; } - pad = gst_element_get_static_pad(sink, "sink"); - if (pad == NULL) { - LOG_ERROR("Couldn't get static pad \"sink\" from video sink.\n"); - ok = EINVAL; - goto fail_unref_sink; + if (subtitles) { + flags |= GST_PLAY_FLAG_TEXT; + } else { + flags &= ~GST_PLAY_FLAG_TEXT; } - gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_QUERY_DOWNSTREAM, on_query_appsink, player, NULL); - - src = gst_bin_get_by_name(GST_BIN(pipeline), "src"); - - if (player->video_uri != NULL) { - if (src != NULL) { - g_object_set(G_OBJECT(src), "uri", player->video_uri, NULL); - } else { - LOG_ERROR("Couldn't find \"src\" element to configure Video URI.\n"); - } - } + g_object_set(p->playbin, "flags", &flags, NULL); - if (force_sw_decoders) { - if (src != NULL) { - g_object_set(G_OBJECT(src), "force-sw-decoders", force_sw_decoders, NULL); - } else { - LOG_ERROR("Couldn't find \"src\" element to force sw decoding.\n"); + if (play_video) { + p->texture = flutterpi_create_texture(flutterpi); + if (p->texture == NULL) { + goto fail_unref_playbin; } - } + + struct gl_renderer *gl_renderer = flutterpi_get_gl_renderer(flutterpi); - if (player->headers != NULL) { - if (src != NULL) { - g_signal_connect(G_OBJECT(src), "source-setup", G_CALLBACK(on_source_setup), player->headers); - } else { - LOG_ERROR("Couldn't find \"src\" element to configure additional HTTP headers.\n"); + GstElement *sink = flutter_gl_texture_sink_new(p->texture, gl_renderer); + if (sink == NULL) { + goto fail_destroy_texture; } - } - gst_base_sink_set_max_lateness(GST_BASE_SINK(sink), 20 * GST_MSECOND); - gst_base_sink_set_qos_enabled(GST_BASE_SINK(sink), TRUE); - gst_base_sink_set_sync(GST_BASE_SINK(sink), TRUE); - gst_app_sink_set_max_buffers(GST_APP_SINK(sink), 2); - gst_app_sink_set_emit_signals(GST_APP_SINK(sink), TRUE); - gst_app_sink_set_drop(GST_APP_SINK(sink), FALSE); - - // configure our caps - // we only accept video formats that we can actually upload to EGL - GstCaps *caps = gst_caps_new_empty(); - for_each_format_in_frame_interface(i, format, player->frame_interface) { - GstVideoFormat gst_format = gst_video_format_from_drm_format(format->format); - if (gst_format == GST_VIDEO_FORMAT_UNKNOWN) { - continue; + /// TODO: What's the ownership transfer here? + g_object_set(p->playbin, "video-sink", sink, NULL); + + // Apply capture-io-mode: dmabuf to any v4l2 decoders. + /// TODO: This might be unnecessary / deprecated nowadays. + g_signal_connect(p->playbin, "element-setup", G_CALLBACK(on_element_setup), NULL); + + GstPad *video_sink_pad = gst_element_get_static_pad(sink, "sink"); + if (video_sink_pad == NULL) { + LOG_PLAYER_ERROR(p, "Could not acquire sink pad of video sink to wait for video configuration.\n"); + goto fail_destroy_texture; } - gst_caps_append(caps, gst_caps_new_simple("video/x-raw", "format", G_TYPE_STRING, gst_video_format_to_string(gst_format), NULL)); + // This will send a `video-info` application message to the bus when it sees a caps event. + gst_pad_add_probe(video_sink_pad, GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM, on_video_sink_event, gst_pipeline_get_bus(GST_PIPELINE(p->playbin)), NULL); } - gst_app_sink_set_caps(GST_APP_SINK(sink), caps); - gst_caps_unref(caps); - - gst_app_sink_set_callbacks( - GST_APP_SINK(sink), - &(GstAppSinkCallbacks - ){ .eos = on_appsink_eos, .new_preroll = on_appsink_new_preroll, .new_sample = on_appsink_new_sample, ._gst_reserved = { 0 } }, - player, - on_appsink_cbs_destroy - ); - gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM, on_probe_pad, player, NULL); - - /// FIXME: Make this work for custom pipelines as well. - if (src != NULL) { - g_signal_connect(src, "element-added", G_CALLBACK(on_element_added), player); - } else { - LOG_DEBUG("Couldn't find \"src\" element to setup v4l2 'capture-io-mode' to 'dmabuf'.\n"); + // Only try to configure headers if we actually have some. + if (headers != NULL && gst_structure_n_fields(headers) > 0) { + g_signal_connect(p->playbin, "source-setup", G_CALLBACK(on_source_setup), headers); } - if (src != NULL) { - gst_object_unref(src); - src = NULL; - } + g_signal_connect(p->playbin, "about-to-finish", G_CALLBACK(on_about_to_finish), NULL); - bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline)); + // Listen to the bus + GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(p->playbin)); + ASSERT_NOT_NULL(bus); + GPollFD fd; gst_bus_get_pollfd(bus, &fd); - flutterpi_sd_event_add_io(&busfd_event_source, fd.fd, EPOLLIN, on_bus_fd_ready, player); - - LOG_DEBUG("Setting state to paused...\n"); - state_change_return = gst_element_set_state(GST_ELEMENT(pipeline), GST_STATE_PAUSED); - if (state_change_return == GST_STATE_CHANGE_NO_PREROLL) { - LOG_DEBUG("Is Live!\n"); - player->is_live = true; - } else { - LOG_DEBUG("Not live!\n"); - player->is_live = false; - } - - player->sink = sink; - /// FIXME: Not sure we need this here. pipeline is floating after gst_parse_launch, which - /// means we should take a reference, but the examples don't increase the refcount. - player->pipeline = pipeline; //gst_object_ref(pipeline); - player->bus = bus; - player->busfd_events = busfd_event_source; - - gst_object_unref(pad); - return 0; - -fail_unref_sink: - gst_object_unref(sink); - -fail_unref_pipeline: - gst_object_unref(pipeline); - - return ok; -} - -static void maybe_deinit(struct gstplayer *player) { - if (player->busfd_events != NULL) { - sd_event_source_unrefp(&player->busfd_events); - } - if (player->sink != NULL) { - gst_object_unref(GST_OBJECT(player->sink)); - player->sink = NULL; - } - if (player->bus != NULL) { - gst_object_unref(GST_OBJECT(player->bus)); - player->bus = NULL; - } - if (player->pipeline != NULL) { - gst_element_set_state(GST_ELEMENT(player->pipeline), GST_STATE_READY); - gst_element_set_state(GST_ELEMENT(player->pipeline), GST_STATE_NULL); - gst_object_unref(GST_OBJECT(player->pipeline)); - player->pipeline = NULL; - } -} - -DEFINE_LOCK_OPS(gstplayer, lock) - -static struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, const char *pipeline_descr, void *userdata) { - struct frame_interface *frame_interface; - struct gstplayer *player; - struct texture *texture; - GstStructure *gst_headers; - int64_t texture_id; - char *uri_owned, *pipeline_descr_owned; - int ok; - - ASSERT_NOT_NULL(flutterpi); - assert((uri != NULL) != (pipeline_descr != NULL)); - - player = malloc(sizeof *player); - if (player == NULL) - return NULL; - - texture = flutterpi_create_texture(flutterpi); - if (texture == NULL) - goto fail_free_player; - - frame_interface = frame_interface_new(flutterpi_get_gl_renderer(flutterpi)); - if (frame_interface == NULL) - goto fail_destroy_texture; + flutterpi_sd_event_add_io(&p->busfd_events, fd.fd, EPOLLIN, on_bus_fd_ready, p); - texture_id = texture_get_id(texture); + gst_object_unref(bus); - if (uri != NULL) { - uri_owned = strdup(uri); - if (uri_owned == NULL) - goto fail_destroy_frame_interface; + GstStateChangeReturn status = gst_element_set_state(p->playbin, GST_STATE_PAUSED); + if (status == GST_STATE_CHANGE_NO_PREROLL) { + LOG_PLAYER_DEBUG(p, "Is live!\n"); + p->is_live = true; + } else if (status == GST_STATE_CHANGE_FAILURE) { + LOG_PLAYER_ERROR(p, "Could not set pipeline to paused state.\n"); + goto fail_rm_event_source; } else { - uri_owned = NULL; + LOG_PLAYER_DEBUG(p, "Not live!\n"); + p->is_live = false; } - if (pipeline_descr != NULL) { - pipeline_descr_owned = strdup(pipeline_descr); - if (pipeline_descr_owned == NULL) - goto fail_destroy_frame_interface; - } else { - pipeline_descr_owned = NULL; - } - - gst_headers = gst_structure_new_empty("http-headers"); - - ok = pthread_mutex_init(&player->lock, NULL); - if (ok != 0) - goto fail_free_gst_headers; - - ok = value_notifier_init(&player->video_info_notifier, NULL, free /* free(NULL) is a no-op, I checked */); - if (ok != 0) - goto fail_destroy_mutex; - - ok = value_notifier_init(&player->buffering_state_notifier, NULL, free); - if (ok != 0) - goto fail_deinit_video_info_notifier; - - ok = change_notifier_init(&player->error_notifier); - if (ok != 0) - goto fail_deinit_buffering_state_notifier; - - player->flutterpi = flutterpi; - player->userdata = userdata; - player->video_uri = uri_owned; - player->pipeline_description = pipeline_descr_owned; - player->headers = gst_headers; - player->playback_rate_forward = 1.0; - player->playback_rate_backward = 1.0; - player->looping = false; - player->playpause_state = kPaused; - player->direction = kForward; - player->current_playback_rate = 1.0; - player->fallback_position_ms = 0; - player->has_desired_position = false; - player->desired_position_ms = 0; - player->has_sent_info = false; - player->info.has_resolution = false; - player->info.has_fps = false; - player->info.has_duration = false; - player->info.has_seeking_info = false; - player->has_gst_info = false; - memset(&player->gst_info, 0, sizeof(player->gst_info)); - player->texture = texture; - player->texture_id = texture_id; - player->frame_interface = frame_interface; - player->pipeline = NULL; - player->sink = NULL; - player->bus = NULL; - player->busfd_events = NULL; - player->is_live = false; - return player; - - //fail_deinit_error_notifier: - //notifier_deinit(&player->error_notifier); - -fail_deinit_buffering_state_notifier: - notifier_deinit(&player->buffering_state_notifier); - -fail_deinit_video_info_notifier: - notifier_deinit(&player->video_info_notifier); + return p; -fail_destroy_mutex: - pthread_mutex_destroy(&player->lock); - -fail_free_gst_headers: - gst_structure_free(gst_headers); - free(uri_owned); - -fail_destroy_frame_interface: - frame_interface_unref(frame_interface); +fail_rm_event_source: + sd_event_source_disable_unref(p->busfd_events); fail_destroy_texture: - texture_destroy(texture); + gst_object_unref(p->playbin); + + // The flutter upload sink uses the texture internally, + // so the playbin (which contains the upload sink) must be destroyed first, + // before the texture can be destroyed. + if (play_video) { + texture_destroy(p->texture); + } + return NULL; -fail_free_player: - free(player); +fail_unref_playbin: + gst_object_unref(p->playbin); +fail_free_p: + free(p); return NULL; } @@ -1140,98 +901,84 @@ struct gstplayer *gstplayer_new_from_asset(struct flutterpi *flutterpi, const ch return NULL; } - player = gstplayer_new(flutterpi, uri, NULL, userdata); + player = gstplayer_new_v2(flutterpi, uri, userdata, true, true, false, NULL); free(uri); return player; } -struct gstplayer *gstplayer_new_from_network(struct flutterpi *flutterpi, const char *uri, enum format_hint format_hint, void *userdata) { +struct gstplayer *gstplayer_new_from_network(struct flutterpi *flutterpi, const char *uri, enum format_hint format_hint, void *userdata, GstStructure *headers) { (void) format_hint; - return gstplayer_new(flutterpi, uri, NULL, userdata); + return gstplayer_new_v2(flutterpi, uri, userdata, true, true, false, headers); } struct gstplayer *gstplayer_new_from_file(struct flutterpi *flutterpi, const char *uri, void *userdata) { - return gstplayer_new(flutterpi, uri, NULL, userdata); + return gstplayer_new_v2(flutterpi, uri, userdata, true, true, false, NULL); } -struct gstplayer *gstplayer_new_from_content_uri(struct flutterpi *flutterpi, const char *uri, void *userdata) { - return gstplayer_new(flutterpi, uri, NULL, userdata); +struct gstplayer *gstplayer_new_from_content_uri(struct flutterpi *flutterpi, const char *uri, void *userdata, GstStructure *headers) { + return gstplayer_new_v2(flutterpi, uri, userdata, true, true, false, headers); } struct gstplayer *gstplayer_new_from_pipeline(struct flutterpi *flutterpi, const char *pipeline, void *userdata) { - return gstplayer_new(flutterpi, NULL, pipeline, userdata); + /// TODO: Implement + (void) flutterpi; + (void) pipeline; + (void) userdata; + return NULL; } void gstplayer_destroy(struct gstplayer *player) { - LOG_DEBUG("gstplayer_destroy(%p)\n", player); + LOG_PLAYER_DEBUG(player, "destroy()\n"); notifier_deinit(&player->video_info_notifier); notifier_deinit(&player->buffering_state_notifier); notifier_deinit(&player->error_notifier); - maybe_deinit(player); - pthread_mutex_destroy(&player->lock); - if (player->headers != NULL) { - gst_structure_free(player->headers); - } - if (player->video_uri != NULL) { - free(player->video_uri); + gst_element_set_state(GST_ELEMENT(player->playbin), GST_STATE_READY); + gst_element_set_state(GST_ELEMENT(player->playbin), GST_STATE_NULL); + gst_object_unref(player->playbin); + if (player->texture) { + texture_destroy(player->texture); } - if (player->pipeline_description != NULL) { - free(player->pipeline_description); - } - frame_interface_unref(player->frame_interface); - texture_destroy(player->texture); free(player); } int64_t gstplayer_get_texture_id(struct gstplayer *player) { - return player->texture_id; -} - -void gstplayer_put_http_header(struct gstplayer *player, const char *key, const char *value) { - GValue gvalue = G_VALUE_INIT; - g_value_set_string(&gvalue, value); - gst_structure_take_value(player->headers, key, &gvalue); + // If the player was started with play_video == false, player->texture is NULL. + return player->texture ? texture_get_id(player->texture) : -1; } -void gstplayer_set_userdata_locked(struct gstplayer *player, void *userdata) { +void gstplayer_set_userdata(struct gstplayer *player, void *userdata) { player->userdata = userdata; } -void *gstplayer_get_userdata_locked(struct gstplayer *player) { +void *gstplayer_get_userdata(struct gstplayer *player) { return player->userdata; } -int gstplayer_initialize(struct gstplayer *player) { - return init(player, false); -} - int gstplayer_play(struct gstplayer *player) { - LOG_DEBUG("gstplayer_play()\n"); + LOG_PLAYER_DEBUG(player, "play()\n"); player->playpause_state = kPlaying; player->direction = kForward; return apply_playback_state(player); } int gstplayer_pause(struct gstplayer *player) { - LOG_DEBUG("gstplayer_pause()\n"); + LOG_PLAYER_DEBUG(player, "pause()\n"); player->playpause_state = kPaused; player->direction = kForward; return apply_playback_state(player); } int gstplayer_set_looping(struct gstplayer *player, bool looping) { - LOG_DEBUG("gstplayer_set_looping(%s)\n", looping ? "true" : "false"); + LOG_PLAYER_DEBUG(player, "set_looping(%s)\n", looping ? "true" : "false"); player->looping = looping; return 0; } int gstplayer_set_volume(struct gstplayer *player, double volume) { - (void) player; - (void) volume; - LOG_DEBUG("gstplayer_set_volume(%f)\n", volume); - /// TODO: Implement + LOG_PLAYER_DEBUG(player, "set_volume(%f)\n", volume); + g_object_set(player->playbin, "volume", (gdouble) volume, NULL); return 0; } @@ -1240,9 +987,9 @@ int64_t gstplayer_get_position(struct gstplayer *player) { gboolean ok; int64_t position; - GstStateChangeReturn statechange = gst_element_get_state(GST_ELEMENT(player->pipeline), ¤t, &pending, 0); + GstStateChangeReturn statechange = gst_element_get_state(GST_ELEMENT(player->playbin), ¤t, &pending, 0); if (statechange == GST_STATE_CHANGE_FAILURE) { - LOG_GST_GET_STATE_ERROR(player->pipeline); + LOG_GST_GET_STATE_ERROR(player->playbin); return -1; } @@ -1252,22 +999,17 @@ int64_t gstplayer_get_position(struct gstplayer *player) { return player->fallback_position_ms; } - DEBUG_TRACE_BEGIN(player, "gstplayer_get_position"); - DEBUG_TRACE_BEGIN(player, "gst_element_query_position"); - ok = gst_element_query_position(player->pipeline, GST_FORMAT_TIME, &position); - DEBUG_TRACE_END(player, "gst_element_query_position"); - + ok = gst_element_query_position(player->playbin, GST_FORMAT_TIME, &position); if (ok == FALSE) { - LOG_ERROR("Could not query gstreamer position. (gst_element_query_position)\n"); + LOG_PLAYER_ERROR(player, "Could not query gstreamer position. (gst_element_query_position)\n"); return 0; } - DEBUG_TRACE_END(player, "gstplayer_get_position"); return GST_TIME_AS_MSECONDS(position); } int gstplayer_seek_to(struct gstplayer *player, int64_t position, bool nearest_keyframe) { - LOG_DEBUG("gstplayer_seek_to(%" PRId64 ")\n", position); + LOG_PLAYER_DEBUG(player, "seek_to(%" PRId64 ")\n", position); player->has_desired_position = true; player->desired_position_ms = position; player->do_fast_seeking = nearest_keyframe; @@ -1275,7 +1017,7 @@ int gstplayer_seek_to(struct gstplayer *player, int64_t position, bool nearest_k } int gstplayer_set_playback_speed(struct gstplayer *player, double playback_speed) { - LOG_DEBUG("gstplayer_set_playback_speed(%f)\n", playback_speed); + LOG_PLAYER_DEBUG(player, "set_playback_speed(%f)\n", playback_speed); ASSERT_MSG(playback_speed > 0, "playback speed must be > 0."); player->playback_rate_forward = playback_speed; return apply_playback_state(player); @@ -1294,9 +1036,9 @@ int gstplayer_step_forward(struct gstplayer *player) { return ok; } - gst_ok = gst_element_send_event(player->pipeline, gst_event_new_step(GST_FORMAT_BUFFERS, 1, 1, TRUE, FALSE)); + gst_ok = gst_element_send_event(player->playbin, gst_event_new_step(GST_FORMAT_BUFFERS, 1, 1, TRUE, FALSE)); if (gst_ok == FALSE) { - LOG_ERROR("Could not send frame-step event to pipeline. (gst_element_send_event)\n"); + LOG_PLAYER_ERROR(player, "Could not send frame-step event to pipeline. (gst_element_send_event)\n"); return EIO; } return 0; @@ -1315,9 +1057,9 @@ int gstplayer_step_backward(struct gstplayer *player) { return ok; } - gst_ok = gst_element_send_event(player->pipeline, gst_event_new_step(GST_FORMAT_BUFFERS, 1, 1, TRUE, FALSE)); + gst_ok = gst_element_send_event(player->playbin, gst_event_new_step(GST_FORMAT_BUFFERS, 1, 1, TRUE, FALSE)); if (gst_ok == FALSE) { - LOG_ERROR("Could not send frame-step event to pipeline. (gst_element_send_event)\n"); + LOG_PLAYER_ERROR(player, "Could not send frame-step event to pipeline. (gst_element_send_event)\n"); return EIO; } diff --git a/src/plugins/gstreamer_video_player/plugin.c b/src/plugins/gstreamer_video_player/plugin.c index d1de5b49..1d42008d 100644 --- a/src/plugins/gstreamer_video_player/plugin.c +++ b/src/plugins/gstreamer_video_player/plugin.c @@ -121,7 +121,7 @@ static void remove_player_locked(struct gstplayer_meta *meta) { } static struct gstplayer_meta *get_meta(struct gstplayer *player) { - return (struct gstplayer_meta *) gstplayer_get_userdata_locked(player); + return (struct gstplayer_meta *) gstplayer_get_userdata(player); } /// Get the player id from the given arg, which is a kStdMap. @@ -368,7 +368,7 @@ static int on_receive_evch(char *channel, struct platch_obj *object, FlutterPlat return platch_respond_not_implemented(responsehandle); } - meta = gstplayer_get_userdata_locked(player); + meta = gstplayer_get_userdata(player); if (streq("listen", method)) { platch_respond_success_std(responsehandle, NULL); @@ -417,44 +417,32 @@ static int on_initialize(char *channel, struct platch_obj *object, FlutterPlatfo return platch_respond_success_pigeon(responsehandle, NULL); } -static int check_headers(const struct std_value *headers, FlutterPlatformMessageResponseHandle *responsehandle) { - const struct std_value *key, *value; - - if (headers == NULL || STDVALUE_IS_NULL(*headers)) { - return 0; - } else if (!STDVALUE_IS_MAP(*headers)) { - platch_respond_illegal_arg_pigeon(responsehandle, "Expected `arg['httpHeaders']` to be a map of strings or null."); - return EINVAL; - } - - for (int i = 0; i < headers->size; i++) { - key = headers->keys + i; - value = headers->values + i; - - if (STDVALUE_IS_NULL(*key) || STDVALUE_IS_NULL(*value)) { - // ignore this value - continue; - } else if (STDVALUE_IS_STRING(*key) && STDVALUE_IS_STRING(*value)) { - // valid too - continue; - } else { - platch_respond_illegal_arg_pigeon(responsehandle, "Expected `arg['httpHeaders']` to be a map of strings or null."); - return EINVAL; - } - } +static void gst_structure_put_string(GstStructure *structure, const char *key, const char *value) { + GValue gvalue = G_VALUE_INIT; + g_value_set_string(&gvalue, value); + gst_structure_take_value(structure, key, &gvalue); +} - return 0; +static void gst_structure_take_string(GstStructure *structure, const char *key, char *value) { + GValue gvalue = G_VALUE_INIT; + g_value_take_string(&gvalue, value); + gst_structure_take_value(structure, key, &gvalue); } -static int add_headers_to_player(const struct std_value *headers, struct gstplayer *player) { +static bool get_headers(const struct std_value *headers, GstStructure **structure_out, FlutterPlatformMessageResponseHandle *responsehandle) { const struct std_value *key, *value; if (headers == NULL || STDVALUE_IS_NULL(*headers)) { - return 0; + *structure_out = NULL; + return true; } else if (!STDVALUE_IS_MAP(*headers)) { - assert(false); + *structure_out = NULL; + platch_respond_illegal_arg_pigeon(responsehandle, "Expected `arg['httpHeaders']` to be a map of strings or null."); + return false; } + *structure_out = gst_structure_new_empty("http-headers"); + for (int i = 0; i < headers->size; i++) { key = headers->keys + i; value = headers->values + i; @@ -463,13 +451,17 @@ static int add_headers_to_player(const struct std_value *headers, struct gstplay // ignore this value continue; } else if (STDVALUE_IS_STRING(*key) && STDVALUE_IS_STRING(*value)) { - gstplayer_put_http_header(player, STDVALUE_AS_STRING(*key), STDVALUE_AS_STRING(*value)); + gst_structure_put_string(*structure_out, STDVALUE_AS_STRING(*key), STDVALUE_AS_STRING(*value)); } else { - assert(false); + gst_structure_free(*structure_out); + *structure_out = NULL; + + platch_respond_illegal_arg_pigeon(responsehandle, "Expected `arg['httpHeaders']` to be a map of strings or null."); + return false; } } - return 0; + return true; } /// Allocates and initializes a gstplayer_meta struct, which we @@ -612,19 +604,20 @@ static int on_create(char *channel, struct platch_obj *object, FlutterPlatformMe ); } - temp = stdmap_get_str(arg, "httpHeaders"); - - // check our headers are valid, so we don't create our player for nothing - ok = check_headers(temp, responsehandle); - if (ok != 0) { - return 0; - } - // create our actual player (this doesn't initialize it) if (asset != NULL) { player = gstplayer_new_from_asset(flutterpi, asset, package_name, NULL); } else { - player = gstplayer_new_from_network(flutterpi, uri, format_hint, NULL); + temp = stdmap_get_str(arg, "httpHeaders"); + + // check our headers are valid, so we don't create our player for nothing + GstStructure *headers = NULL; + ok = get_headers(temp, &headers, responsehandle); + if (ok == false) { + return 0; + } + + player = gstplayer_new_from_network(flutterpi, uri, format_hint, NULL, headers); } if (player == NULL) { LOG_ERROR("Couldn't create gstreamer video player.\n"); @@ -640,10 +633,7 @@ static int on_create(char *channel, struct platch_obj *object, FlutterPlatformMe goto fail_destroy_player; } - gstplayer_set_userdata_locked(player, meta); - - // Add all our HTTP headers to gstplayer using gstplayer_put_http_header - add_headers_to_player(temp, player); + gstplayer_set_userdata(player, meta); // add it to our player collection add_player(meta); @@ -654,17 +644,8 @@ static int on_create(char *channel, struct platch_obj *object, FlutterPlatformMe goto fail_remove_player; } - // Finally, start initializing - ok = gstplayer_initialize(player); - if (ok != 0) { - goto fail_remove_receiver; - } - return platch_respond_success_pigeon(responsehandle, &STDMAP1(STDSTRING("textureId"), STDINT64(gstplayer_get_texture_id(player)))); -fail_remove_receiver: - plugin_registry_remove_receiver(meta->event_channel_name); - fail_remove_player: remove_player(meta); destroy_meta(meta); @@ -1050,7 +1031,6 @@ static int on_initialize_v2(const struct raw_std_value *arg, FlutterPlatformMess } static int on_create_v2(const struct raw_std_value *arg, FlutterPlatformMessageResponseHandle *responsehandle) { - const struct raw_std_value *headers; struct gstplayer_meta *meta; struct gstplayer *player; enum format_hint format_hint; @@ -1152,6 +1132,8 @@ static int on_create_v2(const struct raw_std_value *arg, FlutterPlatformMessageR format_hint = FORMAT_HINT_NONE; } + GstStructure *headers = NULL; + // arg[4]: HTTP Headers if (size >= 5) { arg = raw_std_value_after(arg); @@ -1160,13 +1142,23 @@ static int on_create_v2(const struct raw_std_value *arg, FlutterPlatformMessageR headers = NULL; } else if (raw_std_value_is_map(arg)) { for_each_entry_in_raw_std_map(key, value, arg) { - if (!raw_std_value_is_string(key) || !raw_std_value_is_string(value)) { + if (raw_std_value_is_string(key) && raw_std_value_is_string(value)) { + if (headers == NULL) { + headers = gst_structure_new_empty("http-headers"); + } + + char *key_str = raw_std_string_dup(key); + gst_structure_take_string(headers, key_str, raw_std_string_dup(value)); + free(key_str); + } else { goto invalid_headers; } } - headers = arg; } else { invalid_headers: + if (headers != NULL) { + gst_structure_free(headers); + } return platch_respond_illegal_arg_std(responsehandle, "Expected `arg[4]` to be a map of strings or null."); } } else { @@ -1201,7 +1193,7 @@ static int on_create_v2(const struct raw_std_value *arg, FlutterPlatformMessageR free(asset); asset = NULL; } else if (uri != NULL) { - player = gstplayer_new_from_network(flutterpi, uri, format_hint, NULL); + player = gstplayer_new_from_network(flutterpi, uri, format_hint, NULL, headers); // gstplayer_new_from_network will dup the uri internally. free(uri); @@ -1230,20 +1222,7 @@ static int on_create_v2(const struct raw_std_value *arg, FlutterPlatformMessageR goto fail_destroy_player; } - gstplayer_set_userdata_locked(player, meta); - - // Add all the HTTP headers to gstplayer using gstplayer_put_http_header - if (headers != NULL) { - for_each_entry_in_raw_std_map(header_name, header_value, headers) { - char *header_name_duped = raw_std_string_dup(header_name); - char *header_value_duped = raw_std_string_dup(header_value); - - gstplayer_put_http_header(player, header_name_duped, header_value_duped); - - free(header_value_duped); - free(header_name_duped); - } - } + gstplayer_set_userdata(player, meta); // Add it to our player collection add_player(meta); @@ -1254,17 +1233,8 @@ static int on_create_v2(const struct raw_std_value *arg, FlutterPlatformMessageR goto fail_remove_player; } - // Finally, start initializing - ok = gstplayer_initialize(player); - if (ok != 0) { - goto fail_remove_receiver; - } - return platch_respond_success_std(responsehandle, &STDINT64(gstplayer_get_texture_id(player))); -fail_remove_receiver: - plugin_registry_remove_receiver(meta->event_channel_name); - fail_remove_player: remove_player(meta); destroy_meta(meta); From 2298020d25875d07e398d7a9e937ca02e6aec6ac Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Fri, 21 Feb 2025 14:14:43 +0100 Subject: [PATCH 03/41] video player: don't handle dma_drm video info Only supported since gstreamer 1.24.0 and requires some more work to implement correctly. --- src/plugins/gstreamer_video_player/frame.c | 27 ++++++++-------------- 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/src/plugins/gstreamer_video_player/frame.c b/src/plugins/gstreamer_video_player/frame.c index 24182a84..24023b00 100644 --- a/src/plugins/gstreamer_video_player/frame.c +++ b/src/plugins/gstreamer_video_player/frame.c @@ -828,14 +828,11 @@ static struct video_frame *frame_new_egl_imported(struct frame_interface *interf } while (false) struct video_frame *frame; struct plane_info planes[MAX_N_PLANES]; - GstVideoInfoDmaDrm drm_video_info; GstVideoInfo video_info; EGLBoolean egl_ok; GstBuffer *buffer; EGLImageKHR egl_image; gboolean gst_ok; - uint32_t drm_format; - uint64_t drm_modifier; GstCaps *caps; GLuint texture; GLenum gl_error; @@ -850,8 +847,6 @@ static struct video_frame *frame_new_egl_imported(struct frame_interface *interf return NULL; } - bool is_drm_video_info = false; - // If we don't have an explicit info given, we determine it from the sample caps. if (info == NULL) { caps = gst_sample_get_caps(sample); @@ -859,13 +854,13 @@ static struct video_frame *frame_new_egl_imported(struct frame_interface *interf return NULL; } - is_drm_video_info = gst_video_info_dma_drm_from_caps(&drm_video_info, caps); - - gst_ok = gst_video_info_from_caps(&drm_video_info, caps); + gst_ok = gst_video_info_from_caps(&video_info, caps); if (gst_ok == FALSE) { LOG_ERROR("Could not get video info from caps.\n"); return NULL; } + + info = &video_info; } else { caps = NULL; } @@ -875,17 +870,13 @@ static struct video_frame *frame_new_egl_imported(struct frame_interface *interf height = GST_VIDEO_INFO_HEIGHT(info); n_planes = GST_VIDEO_INFO_N_PLANES(info); - if (is_drm_video_info) { - drm_format = drm_video_info.drm_fourcc; - drm_modifier = drm_video_info.drm_modifier; - } else { - drm_modifier = DRM_FORMAT_MOD_LINEAR; - drm_format = drm_format = drm_format_from_gst_info(info); - if (drm_format == DRM_FORMAT_INVALID) { - LOG_ERROR("Video format has no EGL equivalent.\n"); - return NULL; - } + uint64_t drm_modifier = DRM_FORMAT_MOD_LINEAR; + uint32_t drm_format = drm_format_from_gst_info(info); + if (drm_format == DRM_FORMAT_INVALID) { + LOG_ERROR("Video format has no EGL equivalent.\n"); + return NULL; } + bool external_only; for_each_format_in_frame_interface(i, format, interface) { From ca1eee4d26796d862076903fec8fbe1234ef8ce2 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Fri, 21 Feb 2025 14:22:04 +0100 Subject: [PATCH 04/41] video player: fix code for gstreamer < 1.24.0 Also don't use conditional compilation in places that don't absolutely depend on it. --- .../flutter_texture_sink.c | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/plugins/gstreamer_video_player/flutter_texture_sink.c b/src/plugins/gstreamer_video_player/flutter_texture_sink.c index 76a14f43..0d8a971a 100644 --- a/src/plugins/gstreamer_video_player/flutter_texture_sink.c +++ b/src/plugins/gstreamer_video_player/flutter_texture_sink.c @@ -176,8 +176,7 @@ static gboolean on_appsink_new_event(GstAppSink *appsink, gpointer userdata) { return FALSE; } -#if THIS_GSTREAMER_VER >= GSTREAMER_VER(1, 24, 0) -static gboolean on_appsink_propose_allocation(GstAppSink *appsink, GstQuery *query, gpointer userdata) { +UNUSED static gboolean on_appsink_propose_allocation(GstAppSink *appsink, GstQuery *query, gpointer userdata) { (void) appsink; (void) userdata; @@ -185,8 +184,8 @@ static gboolean on_appsink_propose_allocation(GstAppSink *appsink, GstQuery *que return FALSE; } -#else -static GstPadProbeReturn on_query_appsink_pad(GstPad *pad, GstPadProbeInfo *info, void *userdata) { + +UNUSED static GstPadProbeReturn on_query_appsink_pad(GstPad *pad, GstPadProbeInfo *info, void *userdata) { GstQuery *query; (void) pad; @@ -206,7 +205,6 @@ static GstPadProbeReturn on_query_appsink_pad(GstPad *pad, GstPadProbeInfo *info return GST_PAD_PROBE_HANDLED; } -#endif GstElement *flutter_gl_texture_sink_new(struct texture *texture, struct gl_renderer *renderer) { ASSERT_NOT_NULL(texture); @@ -264,19 +262,23 @@ GstElement *flutter_gl_texture_sink_new(struct texture *texture, struct gl_rende #if THIS_GSTREAMER_VER >= GSTREAMER_VER(1, 24, 0) cbs.propose_allocation = on_appsink_propose_allocation; -#else - GstPad *pad = gst_element_get_static_pad(sink, "sink"); - if (pad == NULL) { - LOG_ERROR("Couldn't get static pad `sink` from appsink.\n"); - frame_interface_unref(meta->interface); - gst_object_unref(element); - free(meta); - return NULL; - } - - gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_QUERY_DOWNSTREAM, on_query_appsink_pad, NULL); #endif + // If instead of conditional compilation so + // this is type-checked even for >= 1.24.0. + if (THIS_GSTREAMER_VER < GSTREAMER_VER(1, 24, 0)) { + GstPad *pad = gst_element_get_static_pad(element, "sink"); + if (pad == NULL) { + LOG_ERROR("Couldn't get static pad `sink` from appsink.\n"); + frame_interface_unref(meta->interface); + gst_object_unref(element); + free(meta); + return NULL; + } + + gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_QUERY_DOWNSTREAM, on_query_appsink_pad, NULL, NULL); + } + gst_app_sink_set_callbacks( GST_APP_SINK(appsink), &cbs, From 58663ba70409e6ea7807e42ff247d630a374cd54 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Fri, 21 Feb 2025 14:50:58 +0100 Subject: [PATCH 05/41] video player: fix parameter in g_object_set --- src/plugins/gstreamer_video_player/player.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/gstreamer_video_player/player.c b/src/plugins/gstreamer_video_player/player.c index baa2d242..1c8a84b5 100644 --- a/src/plugins/gstreamer_video_player/player.c +++ b/src/plugins/gstreamer_video_player/player.c @@ -803,7 +803,7 @@ static struct gstplayer *gstplayer_new_v2(struct flutterpi *flutterpi, const cha flags &= ~GST_PLAY_FLAG_TEXT; } - g_object_set(p->playbin, "flags", &flags, NULL); + g_object_set(p->playbin, "flags", flags, NULL); if (play_video) { p->texture = flutterpi_create_texture(flutterpi); From dc0ff2ae852e2eb1f4a4c08e37bd0fbd12d2e537 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Thu, 8 May 2025 19:51:28 +0200 Subject: [PATCH 06/41] video player: disable hw decoding for gstreamer < 1.22.8 There's an issue in the gstreamer video4linux2 plugin where seeking will lead to buffer management issues, causing streaming to stop, or even memory leaks. See: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/4465 --- src/plugins/gstreamer_video_player/player.c | 39 ++++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/plugins/gstreamer_video_player/player.c b/src/plugins/gstreamer_video_player/player.c index 1c8a84b5..05eb70b4 100644 --- a/src/plugins/gstreamer_video_player/player.c +++ b/src/plugins/gstreamer_video_player/player.c @@ -660,10 +660,23 @@ void on_source_setup(GstElement *playbin, GstElement *source, gpointer userdata) } } +/** + * See: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/blob/main/subprojects/gst-plugins-base/gst/playback/gstplay-enum.h + */ typedef enum { - GST_PLAY_FLAG_VIDEO = (1 << 0), - GST_PLAY_FLAG_AUDIO = (1 << 1), - GST_PLAY_FLAG_TEXT = (1 << 2) + GST_PLAY_FLAG_VIDEO = (1 << 0), + GST_PLAY_FLAG_AUDIO = (1 << 1), + GST_PLAY_FLAG_TEXT = (1 << 2), + GST_PLAY_FLAG_VIS = (1 << 3), + GST_PLAY_FLAG_SOFT_VOLUME = (1 << 4), + GST_PLAY_FLAG_NATIVE_AUDIO = (1 << 5), + GST_PLAY_FLAG_NATIVE_VIDEO = (1 << 6), + GST_PLAY_FLAG_DOWNLOAD = (1 << 7), + GST_PLAY_FLAG_BUFFERING = (1 << 8), + GST_PLAY_FLAG_DEINTERLACE = (1 << 9), + GST_PLAY_FLAG_SOFT_COLORBALANCE = (1 << 10), + GST_PLAY_FLAG_FORCE_FILTERS = (1 << 11), + GST_PLAY_FLAG_FORCE_SW_DECODERS = (1 << 12), } GstPlayFlags; static void on_element_setup(GstElement *playbin, GstElement *element, gpointer userdata) { @@ -772,8 +785,13 @@ static struct gstplayer *gstplayer_new_v2(struct flutterpi *flutterpi, const cha value_notifier_init(&p->buffering_state_notifier, NULL, free); change_notifier_init(&p->error_notifier); - /// TODO: Use playbin or playbin3? + // playbin3 doesn't let use disable hardware decoders, + // which we need to do for gstreamer < 1.22.8. +#if THIS_GSTREAMER_VER < GSTREAMER_VER(1, 22, 8) + p->playbin = gst_element_factory_make("playbin", "playbin"); +#else p->playbin = gst_element_factory_make("playbin3", "playbin"); +#endif if (p->playbin == NULL) { LOG_PLAYER_ERROR(p, "Couldn't create playbin instance.\n"); goto fail_free_p; @@ -803,6 +821,17 @@ static struct gstplayer *gstplayer_new_v2(struct flutterpi *flutterpi, const cha flags &= ~GST_PLAY_FLAG_TEXT; } + // Gstreamer older than 1.22.8 has a buffer management issue when seeking. + // https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/4465 + // + // This is a bit more coarse than necessary; we technically only + // need to disable the v4l2 decoders. +#if THIS_GSTREAMER_VER < GSTREAMER_VER(1, 22, 8) + flags |= GST_PLAY_FLAG_FORCE_SW_DECODERS; +#else + flags &= ~GST_PLAY_FLAG_FORCE_SW_DECODERS; +#endif + g_object_set(p->playbin, "flags", flags, NULL); if (play_video) { @@ -901,7 +930,7 @@ struct gstplayer *gstplayer_new_from_asset(struct flutterpi *flutterpi, const ch return NULL; } - player = gstplayer_new_v2(flutterpi, uri, userdata, true, true, false, NULL); + player = gstplayer_new_v2(flutterpi, uri, userdata, /* play_video */ true, /* play_audio */ false, /* play_text */ false, NULL); free(uri); From ef858c0137a11afeef2a77bb6b10b2feb48a33ec Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Thu, 8 May 2025 21:30:45 +0200 Subject: [PATCH 07/41] video player: make seek flags construction a bit more easier to follow --- src/plugins/gstreamer_video_player/player.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/plugins/gstreamer_video_player/player.c b/src/plugins/gstreamer_video_player/player.c index 05eb70b4..468e695d 100644 --- a/src/plugins/gstreamer_video_player/player.c +++ b/src/plugins/gstreamer_video_player/player.c @@ -308,6 +308,13 @@ static int apply_playback_state(struct gstplayer *player) { } } + GstSeekFlags seek_flags = GST_SEEK_FLAG_FLUSH; + if (player->do_fast_seeking) { + seek_flags |= GST_SEEK_FLAG_KEY_UNIT | GST_SEEK_FLAG_SNAP_NEAREST; + } else { + seek_flags |= GST_SEEK_FLAG_ACCURATE; + } + if (player->direction == kForward) { LOG_PLAYER_DEBUG( player, @@ -316,12 +323,13 @@ static int apply_playback_state(struct gstplayer *player) { GST_TIME_ARGS(position), GST_TIME_ARGS(GST_CLOCK_TIME_NONE) ); + + ok = gst_element_seek( GST_ELEMENT(player->playbin), desired_rate, GST_FORMAT_TIME, - GST_SEEK_FLAG_FLUSH | - (player->do_fast_seeking ? GST_SEEK_FLAG_KEY_UNIT | GST_SEEK_FLAG_SNAP_NEAREST : GST_SEEK_FLAG_ACCURATE), + seek_flags, GST_SEEK_TYPE_SET, position, GST_SEEK_TYPE_SET, @@ -348,8 +356,7 @@ static int apply_playback_state(struct gstplayer *player) { GST_ELEMENT(player->playbin), desired_rate, GST_FORMAT_TIME, - GST_SEEK_FLAG_FLUSH | - (player->do_fast_seeking ? GST_SEEK_FLAG_KEY_UNIT | GST_SEEK_FLAG_SNAP_NEAREST : GST_SEEK_FLAG_ACCURATE), + seek_flags, GST_SEEK_TYPE_SET, 0, GST_SEEK_TYPE_SET, From 878f39c43ad002c0da0822efcb7d417345a36029 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Thu, 8 May 2025 21:41:48 +0200 Subject: [PATCH 08/41] util: add khash hashmap implementation --- src/util/khash.h | 627 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 627 insertions(+) create mode 100644 src/util/khash.h diff --git a/src/util/khash.h b/src/util/khash.h new file mode 100644 index 00000000..f75f3474 --- /dev/null +++ b/src/util/khash.h @@ -0,0 +1,627 @@ +/* The MIT License + + Copyright (c) 2008, 2009, 2011 by Attractive Chaos + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +/* + An example: + +#include "khash.h" +KHASH_MAP_INIT_INT(32, char) +int main() { + int ret, is_missing; + khiter_t k; + khash_t(32) *h = kh_init(32); + k = kh_put(32, h, 5, &ret); + kh_value(h, k) = 10; + k = kh_get(32, h, 10); + is_missing = (k == kh_end(h)); + k = kh_get(32, h, 5); + kh_del(32, h, k); + for (k = kh_begin(h); k != kh_end(h); ++k) + if (kh_exist(h, k)) kh_value(h, k) = 1; + kh_destroy(32, h); + return 0; +} +*/ + +/* + 2013-05-02 (0.2.8): + + * Use quadratic probing. When the capacity is power of 2, stepping function + i*(i+1)/2 guarantees to traverse each bucket. It is better than double + hashing on cache performance and is more robust than linear probing. + + In theory, double hashing should be more robust than quadratic probing. + However, my implementation is probably not for large hash tables, because + the second hash function is closely tied to the first hash function, + which reduce the effectiveness of double hashing. + + Reference: http://research.cs.vt.edu/AVresearch/hashing/quadratic.php + + 2011-12-29 (0.2.7): + + * Minor code clean up; no actual effect. + + 2011-09-16 (0.2.6): + + * The capacity is a power of 2. This seems to dramatically improve the + speed for simple keys. Thank Zilong Tan for the suggestion. Reference: + + - http://code.google.com/p/ulib/ + - http://nothings.org/computer/judy/ + + * Allow to optionally use linear probing which usually has better + performance for random input. Double hashing is still the default as it + is more robust to certain non-random input. + + * Added Wang's integer hash function (not used by default). This hash + function is more robust to certain non-random input. + + 2011-02-14 (0.2.5): + + * Allow to declare global functions. + + 2009-09-26 (0.2.4): + + * Improve portability + + 2008-09-19 (0.2.3): + + * Corrected the example + * Improved interfaces + + 2008-09-11 (0.2.2): + + * Improved speed a little in kh_put() + + 2008-09-10 (0.2.1): + + * Added kh_clear() + * Fixed a compiling error + + 2008-09-02 (0.2.0): + + * Changed to token concatenation which increases flexibility. + + 2008-08-31 (0.1.2): + + * Fixed a bug in kh_get(), which has not been tested previously. + + 2008-08-31 (0.1.1): + + * Added destructor +*/ + + +#ifndef __AC_KHASH_H +#define __AC_KHASH_H + +/*! + @header + + Generic hash table library. + */ + +#define AC_VERSION_KHASH_H "0.2.8" + +#include +#include +#include + +/* compiler specific configuration */ + +#if UINT_MAX == 0xffffffffu +typedef unsigned int khint32_t; +#elif ULONG_MAX == 0xffffffffu +typedef unsigned long khint32_t; +#endif + +#if ULONG_MAX == ULLONG_MAX +typedef unsigned long khint64_t; +#else +typedef unsigned long long khint64_t; +#endif + +#ifndef kh_inline +#ifdef _MSC_VER +#define kh_inline __inline +#else +#define kh_inline inline +#endif +#endif /* kh_inline */ + +#ifndef klib_unused +#if (defined __clang__ && __clang_major__ >= 3) || (defined __GNUC__ && __GNUC__ >= 3) +#define klib_unused __attribute__ ((__unused__)) +#else +#define klib_unused +#endif +#endif /* klib_unused */ + +typedef khint32_t khint_t; +typedef khint_t khiter_t; + +#define __ac_isempty(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&2) +#define __ac_isdel(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&1) +#define __ac_iseither(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&3) +#define __ac_set_isdel_false(flag, i) (flag[i>>4]&=~(1ul<<((i&0xfU)<<1))) +#define __ac_set_isempty_false(flag, i) (flag[i>>4]&=~(2ul<<((i&0xfU)<<1))) +#define __ac_set_isboth_false(flag, i) (flag[i>>4]&=~(3ul<<((i&0xfU)<<1))) +#define __ac_set_isdel_true(flag, i) (flag[i>>4]|=1ul<<((i&0xfU)<<1)) + +#define __ac_fsize(m) ((m) < 16? 1 : (m)>>4) + +#ifndef kroundup32 +#define kroundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) +#endif + +#ifndef kcalloc +#define kcalloc(N,Z) calloc(N,Z) +#endif +#ifndef kmalloc +#define kmalloc(Z) malloc(Z) +#endif +#ifndef krealloc +#define krealloc(P,Z) realloc(P,Z) +#endif +#ifndef kfree +#define kfree(P) free(P) +#endif + +static const double __ac_HASH_UPPER = 0.77; + +#define __KHASH_TYPE(name, khkey_t, khval_t) \ + typedef struct kh_##name##_s { \ + khint_t n_buckets, size, n_occupied, upper_bound; \ + khint32_t *flags; \ + khkey_t *keys; \ + khval_t *vals; \ + } kh_##name##_t; + +#define __KHASH_PROTOTYPES(name, khkey_t, khval_t) \ + extern kh_##name##_t *kh_init_##name(void); \ + extern void kh_destroy_##name(kh_##name##_t *h); \ + extern void kh_clear_##name(kh_##name##_t *h); \ + extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key); \ + extern int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \ + extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \ + extern void kh_del_##name(kh_##name##_t *h, khint_t x); + +#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ + SCOPE kh_##name##_t *kh_init_##name(void) { \ + return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t)); \ + } \ + SCOPE void kh_destroy_##name(kh_##name##_t *h) \ + { \ + if (h) { \ + kfree((void *)h->keys); kfree(h->flags); \ + kfree((void *)h->vals); \ + kfree(h); \ + } \ + } \ + SCOPE void kh_clear_##name(kh_##name##_t *h) \ + { \ + if (h && h->flags) { \ + memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \ + h->size = h->n_occupied = 0; \ + } \ + } \ + SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \ + { \ + if (h->n_buckets) { \ + khint_t k, i, last, mask, step = 0; \ + mask = h->n_buckets - 1; \ + k = __hash_func(key); i = k & mask; \ + last = i; \ + while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ + i = (i + (++step)) & mask; \ + if (i == last) return h->n_buckets; \ + } \ + return __ac_iseither(h->flags, i)? h->n_buckets : i; \ + } else return 0; \ + } \ + SCOPE int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \ + { /* This function uses 0.25*n_buckets bytes of working space instead of [sizeof(key_t+val_t)+.25]*n_buckets. */ \ + khint32_t *new_flags = 0; \ + khint_t j = 1; \ + { \ + kroundup32(new_n_buckets); \ + if (new_n_buckets < 4) new_n_buckets = 4; \ + if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0; /* requested size is too small */ \ + else { /* hash table size to be changed (shrink or expand); rehash */ \ + new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ + if (!new_flags) return -1; \ + memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \ + if (h->n_buckets < new_n_buckets) { /* expand */ \ + khkey_t *new_keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ + if (!new_keys) { kfree(new_flags); return -1; } \ + h->keys = new_keys; \ + if (kh_is_map) { \ + khval_t *new_vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ + if (!new_vals) { kfree(new_flags); return -1; } \ + h->vals = new_vals; \ + } \ + } /* otherwise shrink */ \ + } \ + } \ + if (j) { /* rehashing is needed */ \ + for (j = 0; j != h->n_buckets; ++j) { \ + if (__ac_iseither(h->flags, j) == 0) { \ + khkey_t key = h->keys[j]; \ + khval_t val; \ + khint_t new_mask; \ + new_mask = new_n_buckets - 1; \ + if (kh_is_map) val = h->vals[j]; \ + __ac_set_isdel_true(h->flags, j); \ + while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \ + khint_t k, i, step = 0; \ + k = __hash_func(key); \ + i = k & new_mask; \ + while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \ + __ac_set_isempty_false(new_flags, i); \ + if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \ + { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \ + if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \ + __ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \ + } else { /* write the element and jump out of the loop */ \ + h->keys[i] = key; \ + if (kh_is_map) h->vals[i] = val; \ + break; \ + } \ + } \ + } \ + } \ + if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \ + h->keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \ + if (kh_is_map) h->vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \ + } \ + kfree(h->flags); /* free the working space */ \ + h->flags = new_flags; \ + h->n_buckets = new_n_buckets; \ + h->n_occupied = h->size; \ + h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \ + } \ + return 0; \ + } \ + SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \ + { \ + khint_t x; \ + if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \ + if (h->n_buckets > (h->size<<1)) { \ + if (kh_resize_##name(h, h->n_buckets - 1) < 0) { /* clear "deleted" elements */ \ + *ret = -1; return h->n_buckets; \ + } \ + } else if (kh_resize_##name(h, h->n_buckets + 1) < 0) { /* expand the hash table */ \ + *ret = -1; return h->n_buckets; \ + } \ + } /* TODO: to implement automatically shrinking; resize() already support shrinking */ \ + { \ + khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \ + x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \ + if (__ac_isempty(h->flags, i)) x = i; /* for speed up */ \ + else { \ + last = i; \ + while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \ + if (__ac_isdel(h->flags, i)) site = i; \ + i = (i + (++step)) & mask; \ + if (i == last) { x = site; break; } \ + } \ + if (x == h->n_buckets) { \ + if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \ + else x = i; \ + } \ + } \ + } \ + if (__ac_isempty(h->flags, x)) { /* not present at all */ \ + h->keys[x] = key; \ + __ac_set_isboth_false(h->flags, x); \ + ++h->size; ++h->n_occupied; \ + *ret = 1; \ + } else if (__ac_isdel(h->flags, x)) { /* deleted */ \ + h->keys[x] = key; \ + __ac_set_isboth_false(h->flags, x); \ + ++h->size; \ + *ret = 2; \ + } else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \ + return x; \ + } \ + SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \ + { \ + if (x != h->n_buckets && !__ac_iseither(h->flags, x)) { \ + __ac_set_isdel_true(h->flags, x); \ + --h->size; \ + } \ + } + +#define KHASH_DECLARE(name, khkey_t, khval_t) \ + __KHASH_TYPE(name, khkey_t, khval_t) \ + __KHASH_PROTOTYPES(name, khkey_t, khval_t) + +#define KHASH_INIT2(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ + __KHASH_TYPE(name, khkey_t, khval_t) \ + __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) + +#define KHASH_INIT(name, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \ + KHASH_INIT2(name, static kh_inline klib_unused, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) + +/* --- BEGIN OF HASH FUNCTIONS --- */ + +/*! @function + @abstract Integer hash function + @param key The integer [khint32_t] + @return The hash value [khint_t] + */ +#define kh_int_hash_func(key) (khint32_t)(key) +/*! @function + @abstract Integer comparison function + */ +#define kh_int_hash_equal(a, b) ((a) == (b)) +/*! @function + @abstract 64-bit integer hash function + @param key The integer [khint64_t] + @return The hash value [khint_t] + */ +#define kh_int64_hash_func(key) (khint32_t)((key)>>33^(key)^(key)<<11) +/*! @function + @abstract 64-bit integer comparison function + */ +#define kh_int64_hash_equal(a, b) ((a) == (b)) +/*! @function + @abstract const char* hash function + @param s Pointer to a null terminated string + @return The hash value + */ +static kh_inline khint_t __ac_X31_hash_string(const char *s) +{ + khint_t h = (khint_t)*s; + if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)*s; + return h; +} +/*! @function + @abstract Another interface to const char* hash function + @param key Pointer to a null terminated string [const char*] + @return The hash value [khint_t] + */ +#define kh_str_hash_func(key) __ac_X31_hash_string(key) +/*! @function + @abstract Const char* comparison function + */ +#define kh_str_hash_equal(a, b) (strcmp(a, b) == 0) + +static kh_inline khint_t __ac_Wang_hash(khint_t key) +{ + key += ~(key << 15); + key ^= (key >> 10); + key += (key << 3); + key ^= (key >> 6); + key += ~(key << 11); + key ^= (key >> 16); + return key; +} +#define kh_int_hash_func2(key) __ac_Wang_hash((khint_t)key) + +/* --- END OF HASH FUNCTIONS --- */ + +/* Other convenient macros... */ + +/*! + @abstract Type of the hash table. + @param name Name of the hash table [symbol] + */ +#define khash_t(name) kh_##name##_t + +/*! @function + @abstract Initiate a hash table. + @param name Name of the hash table [symbol] + @return Pointer to the hash table [khash_t(name)*] + */ +#define kh_init(name) kh_init_##name() + +/*! @function + @abstract Destroy a hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + */ +#define kh_destroy(name, h) kh_destroy_##name(h) + +/*! @function + @abstract Reset a hash table without deallocating memory. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + */ +#define kh_clear(name, h) kh_clear_##name(h) + +/*! @function + @abstract Resize a hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param s New size [khint_t] + */ +#define kh_resize(name, h, s) kh_resize_##name(h, s) + +/*! @function + @abstract Insert a key to the hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param k Key [type of keys] + @param r Extra return code: -1 if the operation failed; + 0 if the key is present in the hash table; + 1 if the bucket is empty (never used); 2 if the element in + the bucket has been deleted [int*] + @return Iterator to the inserted element [khint_t] + */ +#define kh_put(name, h, k, r) kh_put_##name(h, k, r) + +/*! @function + @abstract Retrieve a key from the hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param k Key [type of keys] + @return Iterator to the found element, or kh_end(h) if the element is absent [khint_t] + */ +#define kh_get(name, h, k) kh_get_##name(h, k) + +/*! @function + @abstract Remove a key from the hash table. + @param name Name of the hash table [symbol] + @param h Pointer to the hash table [khash_t(name)*] + @param k Iterator to the element to be deleted [khint_t] + */ +#define kh_del(name, h, k) kh_del_##name(h, k) + +/*! @function + @abstract Test whether a bucket contains data. + @param h Pointer to the hash table [khash_t(name)*] + @param x Iterator to the bucket [khint_t] + @return 1 if containing data; 0 otherwise [int] + */ +#define kh_exist(h, x) (!__ac_iseither((h)->flags, (x))) + +/*! @function + @abstract Get key given an iterator + @param h Pointer to the hash table [khash_t(name)*] + @param x Iterator to the bucket [khint_t] + @return Key [type of keys] + */ +#define kh_key(h, x) ((h)->keys[x]) + +/*! @function + @abstract Get value given an iterator + @param h Pointer to the hash table [khash_t(name)*] + @param x Iterator to the bucket [khint_t] + @return Value [type of values] + @discussion For hash sets, calling this results in segfault. + */ +#define kh_val(h, x) ((h)->vals[x]) + +/*! @function + @abstract Alias of kh_val() + */ +#define kh_value(h, x) ((h)->vals[x]) + +/*! @function + @abstract Get the start iterator + @param h Pointer to the hash table [khash_t(name)*] + @return The start iterator [khint_t] + */ +#define kh_begin(h) (khint_t)(0) + +/*! @function + @abstract Get the end iterator + @param h Pointer to the hash table [khash_t(name)*] + @return The end iterator [khint_t] + */ +#define kh_end(h) ((h)->n_buckets) + +/*! @function + @abstract Get the number of elements in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @return Number of elements in the hash table [khint_t] + */ +#define kh_size(h) ((h)->size) + +/*! @function + @abstract Get the number of buckets in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @return Number of buckets in the hash table [khint_t] + */ +#define kh_n_buckets(h) ((h)->n_buckets) + +/*! @function + @abstract Iterate over the entries in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @param kvar Variable to which key will be assigned + @param vvar Variable to which value will be assigned + @param code Block of code to execute + */ +#define kh_foreach(h, kvar, vvar, code) { khint_t __i; \ + for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ + if (!kh_exist(h,__i)) continue; \ + (kvar) = kh_key(h,__i); \ + (vvar) = kh_val(h,__i); \ + code; \ + } } + +/*! @function + @abstract Iterate over the values in the hash table + @param h Pointer to the hash table [khash_t(name)*] + @param vvar Variable to which value will be assigned + @param code Block of code to execute + */ +#define kh_foreach_value(h, vvar, code) { khint_t __i; \ + for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \ + if (!kh_exist(h,__i)) continue; \ + (vvar) = kh_val(h,__i); \ + code; \ + } } + +/* More convenient interfaces */ + +/*! @function + @abstract Instantiate a hash set containing integer keys + @param name Name of the hash table [symbol] + */ +#define KHASH_SET_INIT_INT(name) \ + KHASH_INIT(name, khint32_t, char, 0, kh_int_hash_func, kh_int_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing integer keys + @param name Name of the hash table [symbol] + @param khval_t Type of values [type] + */ +#define KHASH_MAP_INIT_INT(name, khval_t) \ + KHASH_INIT(name, khint32_t, khval_t, 1, kh_int_hash_func, kh_int_hash_equal) + +/*! @function + @abstract Instantiate a hash set containing 64-bit integer keys + @param name Name of the hash table [symbol] + */ +#define KHASH_SET_INIT_INT64(name) \ + KHASH_INIT(name, khint64_t, char, 0, kh_int64_hash_func, kh_int64_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing 64-bit integer keys + @param name Name of the hash table [symbol] + @param khval_t Type of values [type] + */ +#define KHASH_MAP_INIT_INT64(name, khval_t) \ + KHASH_INIT(name, khint64_t, khval_t, 1, kh_int64_hash_func, kh_int64_hash_equal) + +typedef const char *kh_cstr_t; +/*! @function + @abstract Instantiate a hash map containing const char* keys + @param name Name of the hash table [symbol] + */ +#define KHASH_SET_INIT_STR(name) \ + KHASH_INIT(name, kh_cstr_t, char, 0, kh_str_hash_func, kh_str_hash_equal) + +/*! @function + @abstract Instantiate a hash map containing const char* keys + @param name Name of the hash table [symbol] + @param khval_t Type of values [type] + */ +#define KHASH_MAP_INIT_STR(name, khval_t) \ + KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal) + +#endif /* __AC_KHASH_H */ From 0e6b1dab51deee18bedd02a322907cec9fe143cc Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Mon, 12 May 2025 21:37:18 +0200 Subject: [PATCH 09/41] platform channels: helpers for decoding method calls - add `raw_std_method_call_from_buffer` to create a "raw_std_value" from a byte buffer & size, and check whether its encoding is valid - add `platch_respond_malformed_message_std` to send a pre-determined response if the encoding was invalid --- src/platformchannel.c | 14 ++++++++++++++ src/platformchannel.h | 5 ++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/platformchannel.c b/src/platformchannel.c index 4bc13be1..5ed3cbca 100644 --- a/src/platformchannel.c +++ b/src/platformchannel.c @@ -1320,6 +1320,10 @@ int platch_respond_native_error_std(const FlutterPlatformMessageResponseHandle * return platch_respond_error_std(handle, "nativeerror", strerror(_errno), &STDINT32(_errno)); } +int platch_respond_malformed_message_std(const FlutterPlatformMessage *message) { + return platch_respond_error_std(message->response_handle, "malformed-message", "The platform message received was malformed.", &STDNULL); +} + /************************ * JSON METHOD CHANNELS * ************************/ @@ -2483,3 +2487,13 @@ MALLOCLIKE MUST_CHECK char *raw_std_method_call_get_method_dup(const struct raw_ ATTR_PURE const struct raw_std_value *raw_std_method_call_get_arg(const struct raw_std_value *value) { return raw_std_value_after(value); } + +ATTR_PURE const struct raw_std_value *raw_std_method_call_from_buffer(const void *buffer, size_t buffer_size) { + const struct raw_std_value *envelope = (const struct raw_std_value *) buffer; + + if (!raw_std_method_call_check(envelope, buffer_size)) { + return NULL; + } else { + return envelope; + } +} diff --git a/src/platformchannel.h b/src/platformchannel.h index 76c03315..14ea7287 100644 --- a/src/platformchannel.h +++ b/src/platformchannel.h @@ -3,7 +3,7 @@ * Platform Channels * * Encoding/Decoding of flutter platform messages, with different - * + * * Supported codecs: * - standard message & method codec, * - json message & method codec @@ -1491,6 +1491,8 @@ int platch_respond_illegal_arg_ext_std(const FlutterPlatformMessageResponseHandl int platch_respond_native_error_std(const FlutterPlatformMessageResponseHandle *handle, int _errno); +int platch_respond_malformed_message_std(const FlutterPlatformMessage *message); + int platch_respond_success_json(const FlutterPlatformMessageResponseHandle *handle, struct json_value *return_value); int platch_respond_error_json( @@ -1614,6 +1616,7 @@ ATTR_PURE bool raw_std_method_call_check(const struct raw_std_value *value, size ATTR_PURE bool raw_std_method_call_response_check(const struct raw_std_value *value, size_t buffer_size); ATTR_PURE bool raw_std_event_check(const struct raw_std_value *value, size_t buffer_size); +ATTR_PURE const struct raw_std_value *raw_std_method_call_from_buffer(const void *buffer, size_t buffer_size); ATTR_PURE const struct raw_std_value *raw_std_method_call_get_method(const struct raw_std_value *value); ATTR_PURE bool raw_std_method_call_is_method(const struct raw_std_value *value, const char *method_name); MALLOCLIKE MUST_CHECK char *raw_std_method_call_get_method_dup(const struct raw_std_value *value); From 5f65e738bdf06af07a0e852795f1735610b87183 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Mon, 12 May 2025 21:45:23 +0200 Subject: [PATCH 10/41] gitignore: ignore .cache Used by Zed to store clangd cache. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index c4a9d2c9..c84156cd 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,6 @@ Icon Network Trash Folder Temporary Items .apdisk + +# Used by zed to store clangd cache +.cache From 63de32bafa387a83d262ddc91a6352fdf4fdc83f Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Mon, 12 May 2025 21:47:32 +0200 Subject: [PATCH 11/41] gstplayer: feature parity with audioplayers player - add audio balance setter/getter - support releasing & prerolling a playback source - add separate seeking info, duration and end of stream notifiers - make gstreamer_video_player.h not depend on gstreamer headers (if possible) --- src/plugins/gstreamer_video_player.h | 39 ++++ src/plugins/gstreamer_video_player/player.c | 232 ++++++++++++++++---- 2 files changed, 230 insertions(+), 41 deletions(-) diff --git a/src/plugins/gstreamer_video_player.h b/src/plugins/gstreamer_video_player.h index 4ad9110f..d130c448 100644 --- a/src/plugins/gstreamer_video_player.h +++ b/src/plugins/gstreamer_video_player.h @@ -5,6 +5,8 @@ #include "util/lock_ops.h" #include "util/refcounting.h" +#include + #include "config.h" #if !defined(HAVE_EGL_GLES2) @@ -65,6 +67,19 @@ struct video_info; struct gstplayer; struct flutterpi; +typedef struct _GstStructure GstStructure; + +/// Create a gstreamer video player. +struct gstplayer *gstplayer_new( + struct flutterpi *flutterpi, + const char *uri, + void *userdata, + bool play_video, + bool play_audio, + bool subtitles, + GstStructure *headers +); + /// Create a gstreamer video player that loads the video from a flutter asset. /// @arg asset_path The path of the asset inside the asset bundle. /// @arg package_name The name of the package containing the asset @@ -131,6 +146,11 @@ int gstplayer_pause(struct gstplayer *player); /// @returns Current playback position, in milliseconds from the beginning of the video. int64_t gstplayer_get_position(struct gstplayer *player); +/// Get the duration of the currently playing medium. +/// @returns Duration of the current medium in milliseconds, -1 if the duration +/// is not yet known, or INT64_MAX for live sources. +int64_t gstplayer_get_duration(struct gstplayer *player); + /// Set whether the video should loop. /// @arg looping Whether the video should start playing from the beginning when the /// end is reached. @@ -155,6 +175,14 @@ int gstplayer_step_forward(struct gstplayer *player); int gstplayer_step_backward(struct gstplayer *player); +void gstplayer_set_audio_balance(struct gstplayer *player, float balance); + +float gstplayer_get_audio_balance(struct gstplayer *player); + +bool gstplayer_release(struct gstplayer *p); + +bool gstplayer_preroll(struct gstplayer *p, const char *uri); + struct video_info { int width, height; @@ -173,6 +201,17 @@ struct video_info { /// So you need to make sure you do the proper rethreading in the listener callback. struct notifier *gstplayer_get_video_info_notifier(struct gstplayer *player); +struct seeking_info { + bool can_seek; + int64_t seek_begin_ms, seek_end_ms; +}; + +struct notifier *gstplayer_get_seeking_info_notifier(struct gstplayer *player); + +struct notifier *gstplayer_get_duration_notifier(struct gstplayer *player); + +struct notifier *gstplayer_get_eos_notifier(struct gstplayer *player); + /// @brief Get the value notifier for the buffering state. /// /// Gets notified with a value of type `struct buffering_state*` when the buffering state changes. diff --git a/src/plugins/gstreamer_video_player/player.c b/src/plugins/gstreamer_video_player/player.c index 468e695d..82296fd5 100644 --- a/src/plugins/gstreamer_video_player/player.c +++ b/src/plugins/gstreamer_video_player/player.c @@ -9,10 +9,13 @@ #include #include #include -#include #include #include #include +#include +#include +#include +#include #include "flutter-pi.h" #include "notifier_listener.h" @@ -138,10 +141,18 @@ struct gstplayer { int64_t desired_position_ms; struct notifier video_info_notifier, buffering_state_notifier, error_notifier; + struct notifier duration_notifier, seeking_info_notifier; + struct notifier eos_notifier; bool has_sent_info; struct incomplete_video_info info; + bool has_duration; + int64_t duration; + + bool has_seeking_info; + struct seeking_info seeking_info; + // bool has_gst_info; // GstVideoInfo gst_info; @@ -152,11 +163,14 @@ struct gstplayer { sd_event_source *busfd_events; GstElement *playbin; + GstElement *audiopanorama; + + gulong on_element_added_signal_handler; bool is_live; }; -static int maybe_send_info(struct gstplayer *player) { +static int maybe_send_video_info(struct gstplayer *player) { struct video_info *duped; if (player->info.has_resolution && player->info.has_fps && player->info.has_duration && player->info.has_seeking_info) { @@ -169,6 +183,7 @@ static int maybe_send_info(struct gstplayer *player) { notifier_notify(&player->video_info_notifier, duped); } + return 0; } @@ -181,6 +196,9 @@ static void fetch_duration(struct gstplayer *player) { if (player->is_live) { player->info.info.duration_ms = INT64_MAX; player->info.has_duration = true; + + player->has_duration = true; + player->duration = INT64_MAX; return; } else { LOG_PLAYER_ERROR(player, "Could not fetch duration. (gst_element_query_duration)\n"); @@ -190,6 +208,9 @@ static void fetch_duration(struct gstplayer *player) { player->info.info.duration_ms = GST_TIME_AS_MSECONDS(duration); player->info.has_duration = true; + + player->duration = GST_TIME_AS_MSECONDS(duration); + player->has_duration = true; } static void fetch_seeking(struct gstplayer *player) { @@ -205,6 +226,11 @@ static void fetch_seeking(struct gstplayer *player) { player->info.info.seek_begin_ms = 0; player->info.info.seek_end_ms = 0; player->info.has_seeking_info = true; + + player->seeking_info.can_seek = false; + player->seeking_info.seek_begin_ms = 0; + player->seeking_info.seek_end_ms = 0; + player->has_seeking_info = true; return; } else { LOG_PLAYER_DEBUG(player, "Could not query seeking info. (gst_element_query)\n"); @@ -220,6 +246,11 @@ static void fetch_seeking(struct gstplayer *player) { player->info.info.seek_begin_ms = GST_TIME_AS_MSECONDS(seek_begin); player->info.info.seek_end_ms = GST_TIME_AS_MSECONDS(seek_end); player->info.has_seeking_info = true; + + player->seeking_info.can_seek = seekable; + player->seeking_info.seek_begin_ms = GST_TIME_AS_MSECONDS(seek_begin); + player->seeking_info.seek_end_ms = GST_TIME_AS_MSECONDS(seek_end); + player->has_seeking_info = true; } static void update_buffering_state(struct gstplayer *player, GstObject *element) { @@ -324,7 +355,6 @@ static int apply_playback_state(struct gstplayer *player) { GST_TIME_ARGS(GST_CLOCK_TIME_NONE) ); - ok = gst_element_seek( GST_ELEMENT(player->playbin), desired_rate, @@ -346,7 +376,7 @@ static int apply_playback_state(struct gstplayer *player) { } } else { LOG_PLAYER_DEBUG( - player, + player, "gst_element_seek(..., rate: %f, start: %" GST_TIME_FORMAT ", end: %" GST_TIME_FORMAT ", ...)\n", desired_rate, GST_TIME_ARGS(0), @@ -382,7 +412,7 @@ static int apply_playback_state(struct gstplayer *player) { ok = gst_element_get_state(player->playbin, ¤t_state, &pending_state, 0); if (ok == GST_STATE_CHANGE_FAILURE) { LOG_PLAYER_DEBUG( - player, + player, "last gstreamer pipeline state change failed. gst_element_get_state(element name: %s): GST_STATE_CHANGE_FAILURE\n", GST_ELEMENT_NAME(player->playbin) ); @@ -394,7 +424,7 @@ static int apply_playback_state(struct gstplayer *player) { // we're already in the desired state, and we're also not changing it // no need to do anything. LOG_PLAYER_DEBUG( - player, + player, "apply_playback_state(playing: %s): already in desired state and none pending\n", PLAYPAUSE_STATE_AS_STRING(player->playpause_state) ); @@ -402,7 +432,7 @@ static int apply_playback_state(struct gstplayer *player) { } LOG_PLAYER_DEBUG( - player, + player, "apply_playback_state(playing: %s): setting state to %s\n", PLAYPAUSE_STATE_AS_STRING(player->playpause_state), gst_element_state_get_name(desired_state) @@ -419,7 +449,7 @@ static int apply_playback_state(struct gstplayer *player) { /// TODO: Implement properly LOG_PLAYER_DEBUG( - player, + player, "apply_playback_state(playing: %s): async state change in progress, setting state to %s\n", PLAYPAUSE_STATE_AS_STRING(player->playpause_state), gst_element_state_get_name(desired_state) @@ -436,7 +466,7 @@ static int apply_playback_state(struct gstplayer *player) { static void on_gstreamer_error_message(struct gstplayer *player, GstMessage *msg) { (void) player; - + GError *error; gchar *debug_info; @@ -450,6 +480,9 @@ static void on_gstreamer_error_message(struct gstplayer *player, GstMessage *msg error->message, debug_info ); + + notifier_notify(&player->error_notifier, error); + g_clear_error(&error); g_free(debug_info); } @@ -518,7 +551,7 @@ static void on_state_change_message(struct gstplayer *player, GstMessage *msg) { GstState old, current, pending; gst_message_parse_state_changed(msg, &old, ¤t, &pending); - + if (GST_MESSAGE_SRC(msg) == GST_OBJECT(player->playbin)) { LOG_PLAYER_DEBUG( player, @@ -529,13 +562,32 @@ static void on_state_change_message(struct gstplayer *player, GstMessage *msg) { gst_element_state_get_name(pending) ); - if (!player->info.has_duration && (current == GST_STATE_PAUSED || current == GST_STATE_PLAYING)) { + if (current == GST_STATE_PAUSED || current == GST_STATE_PLAYING) { // it's our pipeline that changed to either playing / paused, and we don't have info about our video duration yet. // get that info now. // technically we can already fetch the duration when the decodebin changed to PAUSED state. - fetch_duration(player); - fetch_seeking(player); - maybe_send_info(player); + + if (!player->has_duration) { + fetch_duration(player); + + if (player->has_duration) { + int64_t *duped = memdup(&player->duration, sizeof(int64_t)); + + notifier_notify(&player->duration_notifier, duped); + } + } + + if (!player->has_seeking_info) { + fetch_seeking(player); + + if (player->has_seeking_info) { + struct seeking_info *duped = memdup(&player->seeking_info, sizeof(struct seeking_info)); + + notifier_notify(&player->seeking_info_notifier, duped); + } + } + + maybe_send_video_info(player); } } } @@ -561,7 +613,7 @@ static void on_application_message(struct gstplayer *player, GstMessage *msg) { } } else if (gst_message_has_name(msg, "video-info")) { const GstStructure *structure = gst_message_get_structure(msg); - + const GValue *value = gst_structure_get_value(structure, "info"); assert(G_VALUE_HOLDS_POINTER(value)); @@ -612,6 +664,10 @@ static void on_bus_message(struct gstplayer *player, GstMessage *msg) { case GST_MESSAGE_EOS: LOG_PLAYER_DEBUG(player, "end of stream, src: %s\n", GST_MESSAGE_SRC_NAME(msg)); + + if (GST_MESSAGE_SRC(msg) == GST_OBJECT(player->playbin)) { + notifier_notify(&player->eos_notifier, NULL); + } break; case GST_MESSAGE_REQUEST_STATE: { @@ -737,7 +793,7 @@ static void on_about_to_finish(GstElement *playbin, gpointer userdata) { static GstPadProbeReturn on_video_sink_event(GstPad *pad, GstPadProbeInfo *info, gpointer userdata) { GstBus *bus = userdata; - + (void) pad; GstEvent *event = gst_pad_probe_info_get_event(info); @@ -777,20 +833,23 @@ static GstPadProbeReturn on_video_sink_event(GstPad *pad, GstPadProbeInfo *info, return GST_PAD_PROBE_REMOVE; } -static struct gstplayer *gstplayer_new_v2(struct flutterpi *flutterpi, const char *uri, void *userdata, bool play_video, bool play_audio, bool subtitles, GstStructure *headers) { +struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, void *userdata, bool play_video, bool play_audio, bool subtitles, GstStructure *headers) { struct gstplayer *p = calloc(1, sizeof(struct gstplayer)); if (p == NULL) { return NULL; } - + #ifdef DEBUG p->debug_id = allocate_id(); #endif p->userdata = userdata; value_notifier_init(&p->video_info_notifier, NULL, free); + value_notifier_init(&p->duration_notifier, NULL, free); + value_notifier_init(&p->seeking_info_notifier, NULL, free); value_notifier_init(&p->buffering_state_notifier, NULL, free); change_notifier_init(&p->error_notifier); + change_notifier_init(&p->eos_notifier); // playbin3 doesn't let use disable hardware decoders, // which we need to do for gstreamer < 1.22.8. @@ -804,8 +863,6 @@ static struct gstplayer *gstplayer_new_v2(struct flutterpi *flutterpi, const cha goto fail_free_p; } - g_object_set(p->playbin, "uri", uri, NULL); - gint flags = 0; g_object_get(p->playbin, "flags", &flags, NULL); @@ -846,7 +903,7 @@ static struct gstplayer *gstplayer_new_v2(struct flutterpi *flutterpi, const cha if (p->texture == NULL) { goto fail_unref_playbin; } - + struct gl_renderer *gl_renderer = flutterpi_get_gl_renderer(flutterpi); GstElement *sink = flutter_gl_texture_sink_new(p->texture, gl_renderer); @@ -854,12 +911,9 @@ static struct gstplayer *gstplayer_new_v2(struct flutterpi *flutterpi, const cha goto fail_destroy_texture; } - /// TODO: What's the ownership transfer here? + // playbin (playsink) takes a (sinking) reference + // on the video sink g_object_set(p->playbin, "video-sink", sink, NULL); - - // Apply capture-io-mode: dmabuf to any v4l2 decoders. - /// TODO: This might be unnecessary / deprecated nowadays. - g_signal_connect(p->playbin, "element-setup", G_CALLBACK(on_element_setup), NULL); GstPad *video_sink_pad = gst_element_get_static_pad(sink, "sink"); if (video_sink_pad == NULL) { @@ -869,6 +923,22 @@ static struct gstplayer *gstplayer_new_v2(struct flutterpi *flutterpi, const cha // This will send a `video-info` application message to the bus when it sees a caps event. gst_pad_add_probe(video_sink_pad, GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM, on_video_sink_event, gst_pipeline_get_bus(GST_PIPELINE(p->playbin)), NULL); + + gst_object_unref(video_sink_pad); + video_sink_pad = NULL; + + // Apply capture-io-mode: dmabuf to any v4l2 decoders. + /// TODO: This might be unnecessary / deprecated nowadays. + g_signal_connect(p->playbin, "element-setup", G_CALLBACK(on_element_setup), NULL); + + gst_object_unref(sink); + } + + if (play_audio) { + p->audiopanorama = gst_element_factory_make("audiopanorama", NULL); + if (p->audiopanorama != NULL) { + g_object_set(p->playbin, "audio-filter", p->audiopanorama, NULL); + } } // Only try to configure headers if we actually have some. @@ -889,16 +959,21 @@ static struct gstplayer *gstplayer_new_v2(struct flutterpi *flutterpi, const cha gst_object_unref(bus); - GstStateChangeReturn status = gst_element_set_state(p->playbin, GST_STATE_PAUSED); - if (status == GST_STATE_CHANGE_NO_PREROLL) { - LOG_PLAYER_DEBUG(p, "Is live!\n"); - p->is_live = true; - } else if (status == GST_STATE_CHANGE_FAILURE) { - LOG_PLAYER_ERROR(p, "Could not set pipeline to paused state.\n"); - goto fail_rm_event_source; - } else { - LOG_PLAYER_DEBUG(p, "Not live!\n"); - p->is_live = false; + // If we have a URI, preroll it. + if (uri != NULL) { + g_object_set(p->playbin, "uri", uri, NULL); + + GstStateChangeReturn status = gst_element_set_state(p->playbin, GST_STATE_PAUSED); + if (status == GST_STATE_CHANGE_NO_PREROLL) { + LOG_PLAYER_DEBUG(p, "Is live!\n"); + p->is_live = true; + } else if (status == GST_STATE_CHANGE_FAILURE) { + LOG_PLAYER_ERROR(p, "Could not set pipeline to paused state.\n"); + goto fail_rm_event_source; + } else { + LOG_PLAYER_DEBUG(p, "Not live!\n"); + p->is_live = false; + } } return p; @@ -908,7 +983,7 @@ static struct gstplayer *gstplayer_new_v2(struct flutterpi *flutterpi, const cha fail_destroy_texture: gst_object_unref(p->playbin); - + // The flutter upload sink uses the texture internally, // so the playbin (which contains the upload sink) must be destroyed first, // before the texture can be destroyed. @@ -937,7 +1012,7 @@ struct gstplayer *gstplayer_new_from_asset(struct flutterpi *flutterpi, const ch return NULL; } - player = gstplayer_new_v2(flutterpi, uri, userdata, /* play_video */ true, /* play_audio */ false, /* play_text */ false, NULL); + player = gstplayer_new(flutterpi, uri, userdata, /* play_video */ true, /* play_audio */ false, /* play_text */ false, NULL); free(uri); @@ -946,15 +1021,15 @@ struct gstplayer *gstplayer_new_from_asset(struct flutterpi *flutterpi, const ch struct gstplayer *gstplayer_new_from_network(struct flutterpi *flutterpi, const char *uri, enum format_hint format_hint, void *userdata, GstStructure *headers) { (void) format_hint; - return gstplayer_new_v2(flutterpi, uri, userdata, true, true, false, headers); + return gstplayer_new(flutterpi, uri, userdata, true, true, false, headers); } struct gstplayer *gstplayer_new_from_file(struct flutterpi *flutterpi, const char *uri, void *userdata) { - return gstplayer_new_v2(flutterpi, uri, userdata, true, true, false, NULL); + return gstplayer_new(flutterpi, uri, userdata, true, true, false, NULL); } struct gstplayer *gstplayer_new_from_content_uri(struct flutterpi *flutterpi, const char *uri, void *userdata, GstStructure *headers) { - return gstplayer_new_v2(flutterpi, uri, userdata, true, true, false, headers); + return gstplayer_new(flutterpi, uri, userdata, true, true, false, headers); } struct gstplayer *gstplayer_new_from_pipeline(struct flutterpi *flutterpi, const char *pipeline, void *userdata) { @@ -968,8 +1043,11 @@ struct gstplayer *gstplayer_new_from_pipeline(struct flutterpi *flutterpi, const void gstplayer_destroy(struct gstplayer *player) { LOG_PLAYER_DEBUG(player, "destroy()\n"); notifier_deinit(&player->video_info_notifier); + notifier_deinit(&player->duration_notifier); + notifier_deinit(&player->seeking_info_notifier); notifier_deinit(&player->buffering_state_notifier); notifier_deinit(&player->error_notifier); + notifier_deinit(&player->eos_notifier); gst_element_set_state(GST_ELEMENT(player->playbin), GST_STATE_READY); gst_element_set_state(GST_ELEMENT(player->playbin), GST_STATE_NULL); gst_object_unref(player->playbin); @@ -1044,6 +1122,14 @@ int64_t gstplayer_get_position(struct gstplayer *player) { return GST_TIME_AS_MSECONDS(position); } +int64_t gstplayer_get_duration(struct gstplayer *player) { + if (!player->has_duration) { + return -1; + } else { + return player->duration; + } +} + int gstplayer_seek_to(struct gstplayer *player, int64_t position, bool nearest_keyframe) { LOG_PLAYER_DEBUG(player, "seek_to(%" PRId64 ")\n", position); player->has_desired_position = true; @@ -1102,10 +1188,70 @@ int gstplayer_step_backward(struct gstplayer *player) { return 0; } +void gstplayer_set_audio_balance(struct gstplayer *player, float balance) { + if (player->audiopanorama) { + g_object_set(player->audiopanorama, "panorama", (gfloat) balance, NULL); + } +} + +float gstplayer_get_audio_balance(struct gstplayer *player) { + if (player->audiopanorama) { + gfloat balance = 0.0; + g_object_get(player->audiopanorama, "panorama", &balance, NULL); + return balance; + } else { + return 0.0; + } +} + +bool gstplayer_release(struct gstplayer *p) { + GstStateChangeReturn status = gst_element_set_state(p->playbin, GST_STATE_NULL); + if (status == GST_STATE_CHANGE_FAILURE) { + LOG_ERROR("Could not set pipeline to NULL state.\n"); + return false; + } else { + return true; + } +} + +bool gstplayer_preroll(struct gstplayer *p, const char *uri) { + GstState current, pending; + + GstStateChangeReturn status = gst_element_get_state(p->playbin, ¤t, &pending, 0 * GST_SECOND); + if (status != GST_STATE_CHANGE_SUCCESS || current != GST_STATE_NULL) { + LOG_ERROR("Pipeline must be in NULL (released) state to preroll new source.\n"); + return false; + } + + g_object_set(p->playbin, "uri", uri, NULL); + + status = gst_element_set_state(p->playbin, GST_STATE_PAUSED); + if (status == GST_STATE_CHANGE_NO_PREROLL) { + p->is_live = true; + } else if (status == GST_STATE_CHANGE_FAILURE) { + LOG_ERROR("Could not set pipeline to paused state.\n"); + return false; + } else { + p->is_live = false; + } + + // TODO: Trigger events here (duration change, video info, etc) + + return true; +} + struct notifier *gstplayer_get_video_info_notifier(struct gstplayer *player) { return &player->video_info_notifier; } +struct notifier *gstplayer_get_duration_notifier(struct gstplayer *player) { + return &player->duration_notifier; +} + +struct notifier *gstplayer_get_seeking_info_notifier(struct gstplayer *player) { + return &player->seeking_info_notifier; +} + struct notifier *gstplayer_get_buffering_state_notifier(struct gstplayer *player) { return &player->buffering_state_notifier; } @@ -1113,3 +1259,7 @@ struct notifier *gstplayer_get_buffering_state_notifier(struct gstplayer *player struct notifier *gstplayer_get_error_notifier(struct gstplayer *player) { return &player->error_notifier; } + +struct notifier *gstplayer_get_eos_notifier(struct gstplayer *player) { + return &player->eos_notifier; +} From 2f377275bb589cb9246d48b1e72bc1c27c2607cb Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Fri, 9 May 2025 06:44:04 +0200 Subject: [PATCH 12/41] audioplayers: migrate to gstplayer --- src/plugins/audioplayers/plugin.c | 1118 ++++++++++++++++++++++------- 1 file changed, 874 insertions(+), 244 deletions(-) diff --git a/src/plugins/audioplayers/plugin.c b/src/plugins/audioplayers/plugin.c index 2f136f1a..7e291d77 100644 --- a/src/plugins/audioplayers/plugin.c +++ b/src/plugins/audioplayers/plugin.c @@ -1,333 +1,963 @@ #define _GNU_SOURCE +#include +#include +#include + +#include "flutter_embedder.h" +#include "util/asserts.h" +#include "util/macros.h" + #include "flutter-pi.h" #include "platformchannel.h" #include "pluginregistry.h" -#include "plugins/audioplayers.h" +#include "notifier_listener.h" + #include "util/collection.h" #include "util/list.h" #include "util/logging.h" +#include "util/khash.h" + +#include "plugins/gstreamer_video_player.h" #define AUDIOPLAYERS_LOCAL_CHANNEL "xyz.luan/audioplayers" #define AUDIOPLAYERS_GLOBAL_CHANNEL "xyz.luan/audioplayers.global" -static struct audio_player *audioplayers_linux_plugin_get_player(char *player_id, char *mode); -static void audioplayers_linux_plugin_dispose_player(struct audio_player *player); +#define STR_LINK_TROUBLESHOOTING \ + "https://github.com/bluefireteam/audioplayers/blob/main/troubleshooting.md" + +KHASH_MAP_INIT_STR(audioplayers, struct gstplayer *) + +struct audioplayer_meta { + char *id; + char *event_channel; + bool subscribed; + bool release_on_stop; -struct audio_player_entry { - struct list_head entry; - struct audio_player *player; + struct listener *duration_listener; + struct listener *eos_listener; }; -static struct plugin { +struct plugin { struct flutterpi *flutterpi; bool initialized; - struct list_head players; -} plugin; + khash_t(audioplayers) players; +}; -static int on_local_method_call(char *channel, struct platch_obj *object, FlutterPlatformMessageResponseHandle *responsehandle) { - struct audio_player *player; - struct std_value *args, *tmp; - const char *method; - char *player_id, *mode; - struct std_value result = STDNULL; - int ok; +static void on_receive_event_ch(void *userdata, const FlutterPlatformMessage *message); - (void) responsehandle; - (void) channel; - method = object->method; - args = &object->std_arg; +static void respond_plugin_error_ext(const FlutterPlatformMessageResponseHandle *response_handle, const char *message, struct std_value *details) { + platch_respond_error_std(response_handle, "LinuxAudioError", (char*) message, details); +} - LOG_DEBUG("call(method=%s)\n", method); +static void respond_plugin_error(const FlutterPlatformMessageResponseHandle *response_handle, const char *message) { + respond_plugin_error_ext(response_handle, message, NULL); +} - if (args == NULL || !STDVALUE_IS_MAP(*args)) { - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg` to be a map."); +static bool ensure_gstreamer_initialized(struct plugin *plugin, const FlutterPlatformMessageResponseHandle *responsehandle) { + if (plugin->initialized) { + return true; } - tmp = stdmap_get_str(&object->std_arg, "playerId"); - if (tmp == NULL || !STDVALUE_IS_STRING(*tmp)) { - LOG_ERROR("Call missing mandatory parameter player_id.\n"); - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['playerId'] to be a string."); + GError *error; + gboolean success = gst_init_check(NULL, NULL, &error); + if (success) { + plugin->initialized = true; + return true; } - player_id = STDVALUE_AS_STRING(*tmp); - tmp = stdmap_get_str(args, "mode"); - if (tmp == NULL) { - mode = ""; - } else if (STDVALUE_IS_STRING(*tmp)) { - mode = STDVALUE_AS_STRING(*tmp); + + char *details = NULL; + int status = asprintf(&details, "%s (Domain: %s, Code: %d)", error->message, g_quark_to_string(error->domain), error->code); + if (status == -1) { + // ENOMEM; + return false; + } + + // clang-format off + respond_plugin_error_ext( + responsehandle, + "Failed to initialize gstreamer.", + &STDSTRING(details) + ); + // clang-format on + + free(details); + + return false; +} + +static struct gstplayer *get_player_by_id(struct plugin *plugin, const char *id) { + khint_t index = kh_get_audioplayers(&plugin->players, id); + if (index == kh_end(&plugin->players)) { + return NULL; + } + + return kh_value(&plugin->players, index); +} + +static const struct raw_std_value *get_player_id_from_arg(const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + if (!raw_std_value_is_map(arg)) { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg` to be a map."); + return NULL; + } + + const struct raw_std_value *player_id = raw_std_map_find_str(arg, "playerId"); + if (player_id == NULL || !raw_std_value_is_string(player_id)) { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['playerId']` to be a string."); + return NULL; + } + + return player_id; +} + +static struct gstplayer *get_player_from_arg(struct plugin *plugin, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + const struct raw_std_value *id = get_player_id_from_arg(arg, responsehandle); + if (id == NULL) { + return NULL; + } + + char *id_duped = raw_std_string_dup(id); + if (id_duped == NULL) { + return NULL; + } + + struct gstplayer *player = get_player_by_id(plugin, id_duped); + + free(id_duped); + + if (player == NULL) { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['playerId']` to be a valid player id."); + return NULL; + } + + return player; +} + +UNUSED static void send_error_event(struct audioplayer_meta *meta, GError *error) { + if (!meta->subscribed) { + return; + } + + gchar* message; + if (error->domain == GST_STREAM_ERROR || + error->domain == GST_RESOURCE_ERROR) { + message = + "Failed to set source. For troubleshooting, " + "see: " STR_LINK_TROUBLESHOOTING; } else { - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['mode']` to be a string or null."); + message = "Unknown GstGError. See details."; + } + + char *details = NULL; + int status = asprintf(&details, "%s (Domain: %s, Code: %d)", error->message, g_quark_to_string(error->domain), error->code); + if (status == -1) { + // ENOMEM; + return; + } + + // clang-format off + platch_send_error_event_std( + meta->event_channel, + "LinuxAudioError", + message, + &STDSTRING(details) + ); + // clang-format on + + free(details); +} + +UNUSED static void send_prepared_event(struct audioplayer_meta *meta, bool prepared) { + if (!meta->subscribed) { + return; + } + + // clang-format off + platch_send_success_event_std( + meta->event_channel, + &STDMAP2( + STDSTRING("event"), STDSTRING("audio.onPrepared"), + STDSTRING("value"), STDBOOL(prepared) + ) + ); + // clang-format on +} + +static void send_duration_update(struct audioplayer_meta *meta, int64_t duration_ms) { + if (!meta->subscribed) { + return; + } + + // clang-format off + platch_send_success_event_std( + meta->event_channel, + &STDMAP2( + STDSTRING("event"), STDSTRING("audio.onDuration"), + STDSTRING("value"), STDINT64(duration_ms) + ) + ); + // clang-format on +} + +UNUSED static void send_seek_completed(struct audioplayer_meta *meta) { + if (!meta->subscribed) { + return; + } + + // clang-format off + platch_send_success_event_std( + meta->event_channel, + &STDMAP2( + STDSTRING("event"), STDSTRING("audio.onDuration"), + STDSTRING("value"), STDBOOL(true) + ) + ); + // clang-format on +} + +static void send_playback_complete(struct audioplayer_meta *meta) { + if (!meta->subscribed) { + return; + } + + // clang-format off + platch_send_success_event_std( + meta->event_channel, + &STDMAP2( + STDSTRING("event"), STDSTRING("audio.onComplete"), + STDSTRING("value"), STDBOOL(true) + ) + ); + // clang-format on +} + +UNUSED static void send_player_log(struct audioplayer_meta *meta, const char *message) { + if (!meta->subscribed) { + return; + } + + // clang-format off + platch_send_success_event_std( + meta->event_channel, + &STDMAP2( + STDSTRING("event"), STDSTRING("audio.onLog"), + STDSTRING("value"), STDSTRING((char*) message) + ) + ); + // clang-format on +} + +static void on_create(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + const struct raw_std_value *player_id = get_player_id_from_arg(arg, responsehandle); + if (!player_id) { + return; } - player = audioplayers_linux_plugin_get_player(player_id, mode); + if (!ensure_gstreamer_initialized(p, responsehandle)) { + return; + } + + struct audioplayer_meta *meta = calloc(1, sizeof(struct audioplayer_meta)); + if (meta == NULL) { + platch_respond_native_error_std(responsehandle, ENOMEM); + return; + } + + meta->id = raw_std_string_dup(player_id); + if (meta->id == NULL) { + platch_respond_native_error_std(responsehandle, ENOMEM); + return; + } + + int status = 0; + khint_t index = kh_put(audioplayers, &p->players, meta->id, &status); + if (status == -1) { + free(meta->id); + free(meta); + platch_respond_native_error_std(responsehandle, ENOMEM); + return; + } else if (status == 0) { + free(meta->id); + free(meta); + + platch_respond_illegal_arg_std(responsehandle, "Player with given id already exists."); + return; + } + + status = asprintf(&meta->event_channel, "xyz.luan/audioplayers/events/%s", meta->id); + if (status == -1) { + kh_del(audioplayers, &p->players, index); + free(meta->id); + free(meta); + + platch_respond_native_error_std(responsehandle, ENOMEM); + return; + } + + struct gstplayer *player = gstplayer_new( + p->flutterpi, + NULL, + meta, + /* play_video */ false, /* play_audio */ true, /* subtitles */ false, + NULL + ); if (player == NULL) { - return platch_respond_native_error_std(responsehandle, ENOMEM); - } - - if (streq(method, "create")) { - //audioplayers_linux_plugin_get_player() creates player if it doesn't exist - } else if (streq(method, "pause")) { - audio_player_pause(player); - } else if (streq(method, "resume")) { - audio_player_resume(player); - } else if (streq(method, "stop")) { - audio_player_pause(player); - audio_player_set_position(player, 0); - } else if (streq(method, "release")) { - audio_player_release(player); - } else if (streq(method, "seek")) { - tmp = stdmap_get_str(args, "position"); - if (tmp == NULL || !STDVALUE_IS_INT(*tmp)) { - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['position']` to be an int."); - } + free(meta->event_channel); + kh_del(audioplayers, &p->players, index); + free(meta->id); + free(meta); - int64_t position = STDVALUE_AS_INT(*tmp); - audio_player_set_position(player, position); - } else if (streq(method, "setSourceUrl")) { - tmp = stdmap_get_str(args, "url"); - if (tmp == NULL || !STDVALUE_IS_STRING(*tmp)) { - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['url']` to be a string."); - } - char *url = STDVALUE_AS_STRING(*tmp); + platch_respond_error_std(responsehandle, "not-initialized", "Could not initialize gstplayer.", NULL); + return; + } - tmp = stdmap_get_str(args, "isLocal"); - if (tmp == NULL || !STDVALUE_IS_BOOL(*tmp)) { - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['isLocal']` to be a bool."); - } + gstplayer_set_userdata(player, meta); - bool is_local = STDVALUE_AS_BOOL(*tmp); - if (is_local) { - char *local_url = NULL; - ok = asprintf(&local_url, "file://%s", url); - if (ok < 0) { - return platch_respond_native_error_std(responsehandle, ENOMEM); - } - url = local_url; - } + plugin_registry_set_receiver_v2( + flutterpi_get_plugin_registry(flutterpi), + meta->event_channel, + on_receive_event_ch, + player + ); - audio_player_set_source_url(player, url); - } else if (streq(method, "getDuration")) { - result = STDINT64(audio_player_get_duration(player)); - } else if (streq(method, "setVolume")) { - tmp = stdmap_get_str(args, "volume"); - if (tmp != NULL && STDVALUE_IS_FLOAT(*tmp)) { - audio_player_set_volume(player, STDVALUE_AS_FLOAT(*tmp)); - } else { - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['volume']` to be a float."); - } - } else if (streq(method, "getCurrentPosition")) { - result = STDINT64(audio_player_get_position(player)); - } else if (streq(method, "setPlaybackRate")) { - tmp = stdmap_get_str(args, "playbackRate"); - if (tmp != NULL && STDVALUE_IS_FLOAT(*tmp)) { - audio_player_set_playback_rate(player, STDVALUE_AS_FLOAT(*tmp)); - } else { - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['playbackRate']` to be a float."); - } - } else if (streq(method, "setReleaseMode")) { - tmp = stdmap_get_str(args, "releaseMode"); - if (tmp != NULL && STDVALUE_IS_STRING(*tmp)) { - char *release_mode = STDVALUE_AS_STRING(*tmp); - bool looping = strstr(release_mode, "loop") != NULL; - audio_player_set_looping(player, looping); - } else { - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['releaseMode']` to be a string."); - } - } else if (streq(method, "setPlayerMode")) { - // TODO check support for low latency mode: - // https://gstreamer.freedesktop.org/documentation/additional/design/latency.html?gi-language=c - } else if (strcmp(method, "setBalance") == 0) { - tmp = stdmap_get_str(args, "balance"); - if (tmp != NULL && STDVALUE_IS_FLOAT(*tmp)) { - audio_player_set_balance(player, STDVALUE_AS_FLOAT(*tmp)); - } else { - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['balance']` to be a float."); - } - } else if (strcmp(method, "emitLog") == 0) { - tmp = stdmap_get_str(args, "message"); - char *message; - - if (tmp == NULL) { - message = ""; - } else if (STDVALUE_IS_STRING(*tmp)) { - message = STDVALUE_AS_STRING(*tmp); - } else { - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['message']` to be a string."); - } + kh_value(&p->players, index) = player; +} - // Avoid unused variable compile message if debugging is disabled. - (void) message; - - LOG_DEBUG("%s\n", message); - //TODO: https://github.com/bluefireteam/audioplayers/blob/main/packages/audioplayers_linux/linux/audio_player.cc#L247 - } else if (strcmp(method, "emitError") == 0) { - tmp = stdmap_get_str(args, "code"); - char *code; - - if (tmp == NULL) { - code = ""; - } else if (STDVALUE_IS_STRING(*tmp)) { - code = STDVALUE_AS_STRING(*tmp); - } else { - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['code']` to be a string."); - } +static void on_pause(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + struct gstplayer *player = get_player_from_arg(p, arg, responsehandle); + if (player == NULL) { + return; + } - tmp = stdmap_get_str(args, "message"); - char *message; + int err = gstplayer_pause(player); + if (err != 0) { + platch_respond_native_error_std(responsehandle, err); + } else { + platch_respond_success_std(responsehandle, NULL); + } +} - if (tmp == NULL) { - message = ""; - } else if (STDVALUE_IS_STRING(*tmp)) { - message = STDVALUE_AS_STRING(*tmp); - } else { - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg['message']` to be a string."); - } +static void on_resume(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + struct gstplayer *player = get_player_from_arg(p, arg, responsehandle); + if (player == NULL) { + return; + } - LOG_ERROR("Error: %s; message=%s\n", code, message); - //TODO: https://github.com/bluefireteam/audioplayers/blob/main/packages/audioplayers_linux/linux/audio_player.cc#L144 - } else if (strcmp(method, "dispose") == 0) { - audioplayers_linux_plugin_dispose_player(player); - player = NULL; + /// TODO: Should resume behave different to play? + int err = gstplayer_play(player); + if (err != 0) { + platch_respond_native_error_std(responsehandle, err); } else { - return platch_respond_not_implemented(responsehandle); + platch_respond_success_std(responsehandle, NULL); + } +} + +static void on_stop(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + struct gstplayer *player = get_player_from_arg(p, arg, responsehandle); + if (player == NULL) { + return; + } + + /// TODO: Maybe provide gstplayer_stop + int err = gstplayer_pause(player); + if (err != 0) { + platch_respond_native_error_std(responsehandle, err); + return; + } + + err = gstplayer_seek_to(player, 0, /* nearest_keyframe */ false); + if (err != 0) { + platch_respond_native_error_std(responsehandle, err); + return; } - return platch_respond_success_std(responsehandle, &result); + platch_respond_success_std(responsehandle, NULL); } -static int on_global_method_call(char *channel, struct platch_obj *object, FlutterPlatformMessageResponseHandle *responsehandle) { - (void) responsehandle; - (void) channel; - (void) object; +static void on_release(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + struct gstplayer *player = get_player_from_arg(p, arg, responsehandle); + if (player == NULL) { + return; + } + + gstplayer_release(player); - return platch_respond_success_std(responsehandle, &STDBOOL(true)); + platch_respond_success_std(responsehandle, NULL); } -static int on_receive_event_ch(char *channel, struct platch_obj *object, FlutterPlatformMessageResponseHandle *responsehandle) { - if (strcmp(object->method, "listen") == 0) { - LOG_DEBUG("%s: listen()\n", channel); +static void on_seek(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + struct gstplayer *player = get_player_from_arg(p, arg, responsehandle); + if (player == NULL) { + return; + } - list_for_each_entry_safe(struct audio_player_entry, entry, &plugin.players, entry) { - if (audio_player_set_subscription_status(entry->player, channel, true)) { - return platch_respond_success_std(responsehandle, NULL); - } - } + const struct raw_std_value *position = raw_std_map_find_str(arg, "position"); + if (position == NULL || !raw_std_value_is_int(position)) { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['position'] to be an int."); + return; + } - LOG_ERROR("%s: player not found\n", channel); - return platch_respond_not_implemented(responsehandle); - } else if (strcmp(object->method, "cancel") == 0) { - LOG_DEBUG("%s: cancel()\n", channel); + int64_t position_int = raw_std_value_as_int(position); - list_for_each_entry_safe(struct audio_player_entry, entry, &plugin.players, entry) { - if (audio_player_set_subscription_status(entry->player, channel, false)) { - return platch_respond_success_std(responsehandle, NULL); - } - } + int err = gstplayer_seek_to(player, position_int, /* nearest_keyframe */ false); + if (err != 0) { + platch_respond_native_error_std(responsehandle, err); + return; + } - LOG_ERROR("%s: player not found\n", channel); - return platch_respond_not_implemented(responsehandle); - } else { - return platch_respond_not_implemented(responsehandle); + platch_respond_success_std(responsehandle, NULL); +} + +static void on_set_source_url(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + struct gstplayer *player = get_player_from_arg(p, arg, responsehandle); + if (player == NULL) { + return; + } + + const struct raw_std_value *src_url = raw_std_map_find_str(arg, "url"); + if (src_url == NULL || !raw_std_value_is_string(src_url)) { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['url']` to be a string."); + return; + } + + const struct raw_std_value *is_local = raw_std_map_find_str(arg, "isLocal"); + if (src_url != NULL && !raw_std_value_is_null(is_local) && !raw_std_value_is_bool(is_local)) { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['isLocal']` to be a bool or null."); + return; + } + + const struct raw_std_value *mime_type = raw_std_map_find_str(arg, "mimeType"); + if (mime_type != NULL && !raw_std_value_is_null(mime_type) && !raw_std_value_is_string(mime_type)) { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['mimeType']` to be a bool or null."); + return; + } + + char *src_url_duped = raw_std_string_dup(src_url); + + bool ok = gstplayer_preroll(player, src_url_duped); + + free(src_url_duped); + + if (!ok) { + respond_plugin_error(responsehandle, "Could not preroll pipeline."); + return; } - return 0; + platch_respond_success_std(responsehandle, NULL); } -enum plugin_init_result audioplayers_plugin_init(struct flutterpi *flutterpi, void **userdata_out) { - int ok; +static void on_get_duration(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + struct gstplayer *player = get_player_from_arg(p, arg, responsehandle); + if (player == NULL) { + return; + } - (void) userdata_out; + int64_t duration_ms = gstplayer_get_duration(player); + if (duration_ms == -1) { + platch_respond_success_std(responsehandle, NULL); + } - plugin.flutterpi = flutterpi; - plugin.initialized = false; - list_inithead(&plugin.players); + platch_respond_success_std(responsehandle, &STDINT64(duration_ms)); +} - ok = plugin_registry_set_receiver_locked(AUDIOPLAYERS_GLOBAL_CHANNEL, kStandardMethodCall, on_global_method_call); - if (ok != 0) { - return PLUGIN_INIT_RESULT_ERROR; +static void on_set_volume(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + struct gstplayer *player = get_player_from_arg(p, arg, responsehandle); + if (player == NULL) { + return; } - ok = plugin_registry_set_receiver_locked(AUDIOPLAYERS_LOCAL_CHANNEL, kStandardMethodCall, on_local_method_call); - if (ok != 0) { - goto fail_remove_global_receiver; + const struct raw_std_value *volume = raw_std_map_find_str(arg, "volume"); + if (volume == NULL || !raw_std_value_is_float64(volume)) { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['volume'] to be a double."); + return; } - return PLUGIN_INIT_RESULT_INITIALIZED; + double volume_float = raw_std_value_as_float64(volume); -fail_remove_global_receiver: - plugin_registry_remove_receiver_locked(AUDIOPLAYERS_GLOBAL_CHANNEL); + int err = gstplayer_set_volume(player, volume_float); + if (err != 0) { + platch_respond_native_error_std(responsehandle, err); + return; + } - return PLUGIN_INIT_RESULT_ERROR; + platch_respond_success_std(responsehandle, NULL); } -void audioplayers_plugin_deinit(struct flutterpi *flutterpi, void *userdata) { - (void) flutterpi; - (void) userdata; +static void on_get_position(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + struct gstplayer *player = get_player_from_arg(p, arg, responsehandle); + if (player == NULL) { + return; + } - plugin_registry_remove_receiver_locked(AUDIOPLAYERS_GLOBAL_CHANNEL); - plugin_registry_remove_receiver_locked(AUDIOPLAYERS_LOCAL_CHANNEL); + int64_t position = gstplayer_get_position(player); + if (position < 0) { + platch_respond_native_error_std(responsehandle, EIO); + return; + } - list_for_each_entry_safe(struct audio_player_entry, entry, &plugin.players, entry) { - audio_player_destroy(entry->player); - list_del(&entry->entry); - free(entry); + platch_respond_success_std(responsehandle, &STDINT64(position)); +} + +static void on_set_playback_rate(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + struct gstplayer *player = get_player_from_arg(p, arg, responsehandle); + if (player == NULL) { + return; } + + const struct raw_std_value *rate = raw_std_map_find_str(arg, "playbackRate"); + if (rate == NULL || !raw_std_value_is_float64(rate)) { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['playbackRate'] to be a double."); + return; + } + + double rate_float = raw_std_value_as_float64(rate); + + int err = gstplayer_set_playback_speed(player, rate_float); + if (err != 0) { + platch_respond_native_error_std(responsehandle, err); + return; + } + + platch_respond_success_std(responsehandle, NULL); } -static struct audio_player *audioplayers_linux_plugin_get_player(char *player_id, char *mode) { - struct audio_player_entry *entry; - struct audio_player *player; +static void on_set_release_mode(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + struct gstplayer *player = get_player_from_arg(p, arg, responsehandle); + if (player == NULL) { + return; + } + + const struct raw_std_value *mode = raw_std_map_find_str(arg, "releaseMode"); + if (mode == NULL || !raw_std_value_is_string(mode)) { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['releaseMode'] to be a string."); + return; + } - (void) mode; + bool is_release = false; + bool is_loop = false; + bool is_stop = false; - list_for_each_entry_safe(struct audio_player_entry, entry, &plugin.players, entry) { - if (audio_player_is_id(entry->player, player_id)) { - return entry->player; - } + if (raw_std_string_equals(mode, "ReleaseMode.release")) { + is_release = true; + } else if (raw_std_string_equals(mode, "ReleaseMode.loop")) { + is_loop = true; + } else if (raw_std_string_equals(mode, "ReleaseMode.stop")) { + is_stop = true; + } else { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['releaseMode']` to be a string-ification of a ReleaseMode enum value."); + return; } - entry = malloc(sizeof *entry); - ASSUME(entry != NULL); + // TODO: Handle ReleaseMode.release & ReleaseMode.stop + (void) is_release; + (void) is_stop; - LOG_DEBUG("Create player(id=%s)\n", player_id); - player = audio_player_new(player_id, AUDIOPLAYERS_LOCAL_CHANNEL); + int err = gstplayer_set_looping(player, is_loop); + if (err != 0) { + platch_respond_native_error_std(responsehandle, err); + return; + } + platch_respond_success_std(responsehandle, NULL); +} + +static void on_set_player_mode(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + struct gstplayer *player = get_player_from_arg(p, arg, responsehandle); if (player == NULL) { - LOG_ERROR("player(id=%s) cannot be created", player_id); - free(entry); - return NULL; + return; + } + + const struct raw_std_value *mode = raw_std_map_find_str(arg, "playerMode"); + if (mode == NULL || !raw_std_value_is_string(mode)) { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['playerMode'] to be a string."); + return; + } + + bool is_media_player = false; + bool is_low_latency = false; + + if (raw_std_string_equals(mode, "PlayerMode.mediaPlayer")) { + is_media_player = true; + } else if (raw_std_string_equals(mode, "PlayerMode.lowLatency")) { + is_low_latency = true; + } else { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['playerMode']` to be a string-ification of a PlayerMode enum value."); + return; + } + + // TODO: Handle player mode + // TODO check support for low latency mode: + // https://gstreamer.freedesktop.org/documentation/additional/design/latency.html?gi-language=c + (void) is_media_player; + (void) is_low_latency; + + platch_respond_success_std(responsehandle, NULL); +} + +static void on_set_balance(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + struct gstplayer *player = get_player_from_arg(p, arg, responsehandle); + if (player == NULL) { + return; + } + + const struct raw_std_value *balance = raw_std_map_find_str(arg, "balance"); + if (balance == NULL || !raw_std_value_is_float64(balance)) { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['balance'] to be a double."); + return; + } + + double balance_float = raw_std_value_as_float64(balance); + + if (balance_float < -1.0) { + balance_float = -1.0; + } else if (balance_float > 1.0) { + balance_float = 1.0; } - const char* event_channel = audio_player_subscribe_channel_name(player); - // set a receiver on the videoEvents event channel - int ok = plugin_registry_set_receiver( - event_channel, - kStandardMethodCall, - on_receive_event_ch + gstplayer_set_audio_balance(player, balance_float); + + platch_respond_success_std(responsehandle, NULL); +} + +static void on_player_emit_log(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + struct gstplayer *player = get_player_from_arg(p, arg, responsehandle); + if (player == NULL) { + return; + } + + const struct raw_std_value *message = raw_std_map_find_str(arg, "message"); + if (message == NULL || !raw_std_value_is_string(message)) { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['message'] to be a string."); + return; + } + + LOG_DEBUG("%*s", (int) raw_std_string_get_length(message), raw_std_string_get_nonzero_terminated(message)); + + platch_respond_success_std(responsehandle, NULL); +} + +static void on_player_emit_error(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + struct gstplayer *player = get_player_from_arg(p, arg, responsehandle); + if (player == NULL) { + return; + } + + const struct raw_std_value *code = raw_std_map_find_str(arg, "code"); + if (code == NULL || !raw_std_value_is_string(code)) { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['code'] to be a string."); + return; + } + + const struct raw_std_value *message = raw_std_map_find_str(arg, "message"); + if (message == NULL || !raw_std_value_is_string(message)) { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['message'] to be a string."); + return; + } + + LOG_ERROR( + "%*s, %*s", + (int) raw_std_string_get_length(code), raw_std_string_get_nonzero_terminated(code), + (int) raw_std_string_get_length(message), raw_std_string_get_nonzero_terminated(message) ); - if (ok != 0) { - LOG_ERROR("Cannot set player receiver for event channel: %s\n", event_channel); - audio_player_destroy(player); - free(entry); - return NULL; + + platch_respond_success_std(responsehandle, NULL); +} + +static void on_dispose(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + const struct raw_std_value *id = get_player_id_from_arg(arg, responsehandle); + if (id == NULL) { + return; + } + + struct gstplayer *player = get_player_from_arg(p, arg, responsehandle); + if (player == NULL) { + return; } - entry->entry = (struct list_head){ NULL, NULL }; - entry->player = player; + char *id_duped = raw_std_string_dup(id); - list_add(&entry->entry, &plugin.players); - return player; + khint_t index = kh_get(audioplayers, &p->players, id_duped); + + // Should be valid since we already know the player exists from above + assert(index <= kh_end(&p->players)); + + free(id_duped); + + // Remove the entry from the hashmap + kh_del(audioplayers, &p->players, index); + + struct audioplayer_meta *meta = gstplayer_get_userdata(player); + + plugin_registry_remove_receiver_v2(flutterpi_get_plugin_registry(p->flutterpi), meta->event_channel); + free(meta->event_channel); + free(meta->id); + free(meta); + + // Destroy the player + gstplayer_destroy(player); + + platch_respond_success_std(responsehandle, NULL); +} + +static void on_player_method_call(void *userdata, const FlutterPlatformMessage *message) { + struct plugin *plugin = userdata; + + const struct raw_std_value *envelope = raw_std_method_call_from_buffer(message->message, message->message_size); + if (!envelope) { + platch_respond_malformed_message_std(message); + return; + } + + const struct raw_std_value *arg = raw_std_method_call_get_arg(envelope); + ASSERT_NOT_NULL(arg); + + if (raw_std_method_call_is_method(envelope, "create")) { + on_create(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "pause")) { + on_pause(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "resume")) { + on_resume(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "stop")) { + on_stop(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "release")) { + on_release(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "seek")) { + on_seek(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "setSourceUrl")) { + on_set_source_url(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "getDuration")) { + on_get_duration(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "setVolume")) { + on_set_volume(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "getCurrentPosition")) { + on_get_position(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "setPlaybackRate")) { + on_set_playback_rate(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "setReleaseMode")) { + on_set_release_mode(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "setPlayerMode")) { + on_set_player_mode(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "setBalance") == 0) { + on_set_balance(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "emitLog") == 0) { + on_player_emit_log(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "emitError") == 0) { + on_player_emit_error(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "dispose") == 0) { + on_dispose(plugin, arg, message->response_handle); + } else { + platch_respond_not_implemented(message->response_handle); + } +} + +static void on_init(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + (void) p; + (void) arg; + platch_respond_success_std(responsehandle, NULL); } -static void audioplayers_linux_plugin_dispose_player(struct audio_player *player) { - list_for_each_entry_safe(struct audio_player_entry, entry, &plugin.players, entry) { - if (entry->player == player) { - list_del(&entry->entry); - plugin_registry_remove_receiver(audio_player_subscribe_channel_name(player)); - audio_player_destroy(player); +static void on_set_audio_context(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { + (void) p; + (void) arg; + platch_respond_success_std(responsehandle, NULL); +} + +static void on_emit_log( + struct plugin *p, + const struct raw_std_value *arg, + const FlutterPlatformMessageResponseHandle *responsehandle +) { + (void) p; + + const struct raw_std_value *message = raw_std_map_find_str(arg, "message"); + if (message == NULL || !raw_std_value_is_string(message)) { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['message'] to be a string."); + return; + } + + LOG_DEBUG("%*s", (int) raw_std_string_get_length(message), raw_std_string_get_nonzero_terminated(message)); + + platch_respond_success_std(responsehandle, NULL); +} + +static void on_emit_error( + struct plugin *p, + const struct raw_std_value *arg, + const FlutterPlatformMessageResponseHandle *responsehandle +) { + (void) p; + + const struct raw_std_value *code = raw_std_map_find_str(arg, "code"); + if (code == NULL || !raw_std_value_is_string(code)) { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['code'] to be a string."); + return; + } + + const struct raw_std_value *message = raw_std_map_find_str(arg, "message"); + if (message == NULL || !raw_std_value_is_string(message)) { + platch_respond_illegal_arg_std(responsehandle, "Expected `arg['message'] to be a string."); + return; + } + + LOG_ERROR( + "%*s, %*s", + (int) raw_std_string_get_length(code), raw_std_string_get_nonzero_terminated(code), + (int) raw_std_string_get_length(message), raw_std_string_get_nonzero_terminated(message) + ); + + platch_respond_success_std(responsehandle, NULL); +} + +static void on_global_method_call(void *userdata, const FlutterPlatformMessage *message) { + struct plugin *plugin = userdata; + + const struct raw_std_value *envelope = raw_std_method_call_from_buffer(message->message, message->message_size); + if (!envelope) { + platch_respond_malformed_message_std(message); + return; + } + + const struct raw_std_value *arg = raw_std_method_call_get_arg(envelope); + ASSERT_NOT_NULL(arg); + + if (raw_std_method_call_is_method(envelope, "init")) { + on_init(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "setAudioContext")) { + on_set_audio_context(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "emitLog")) { + on_emit_log(plugin, arg, message->response_handle); + } else if (raw_std_method_call_is_method(envelope, "emitError")) { + on_emit_error(plugin, arg, message->response_handle); + } else { + platch_respond_not_implemented(message->response_handle); + } +} + +static enum listener_return on_duration_notify(void *arg, void *userdata) { + ASSERT_NOT_NULL(userdata); + struct gstplayer *player = userdata; + + struct audioplayer_meta *meta = gstplayer_get_userdata(player); + ASSERT_NOT_NULL(meta); + + ASSERT_NOT_NULL(arg); + int64_t *duration_ms = arg; + + send_duration_update(meta, *duration_ms); + return kNoAction; +} + +static enum listener_return on_eos_notify(void *arg, void *userdata) { + (void) arg; + + ASSERT_NOT_NULL(userdata); + struct gstplayer *player = userdata; + + struct audioplayer_meta *meta = gstplayer_get_userdata(player); + ASSERT_NOT_NULL(meta); + + send_playback_complete(meta); + + return kNoAction; +} + +static void on_receive_event_ch(void *userdata, const FlutterPlatformMessage *message) { + ASSERT_NOT_NULL(userdata); + + struct gstplayer *player = userdata; + + struct audioplayer_meta *meta = gstplayer_get_userdata(player); + ASSERT_NOT_NULL(meta); + + const struct raw_std_value *envelope = raw_std_method_call_from_buffer(message->message, message->message_size); + if (envelope == NULL) { + platch_respond_malformed_message_std(message); + return; + } + + /// TODO: Implement + if (raw_std_method_call_is_method(envelope, "listen") == 0) { + platch_respond_success_std(message->response_handle, NULL); + + if (!meta->subscribed) { + meta->subscribed = true; + + meta->duration_listener = notifier_listen(gstplayer_get_duration_notifier(player), on_duration_notify, NULL, player); + meta->eos_listener = notifier_listen(gstplayer_get_eos_notifier(player), on_eos_notify, NULL, player); + } + } else if (raw_std_method_call_is_method(envelope, "cancel") == 0) { + platch_respond_success_std(message->response_handle, NULL); + + if (meta->subscribed) { + meta->subscribed = false; + + notifier_unlisten(gstplayer_get_duration_notifier(player), meta->duration_listener); + notifier_unlisten(gstplayer_get_eos_notifier(player), meta->eos_listener); } + } else { + platch_respond_not_implemented(message->response_handle); + } +} + +enum plugin_init_result audioplayers_plugin_init(struct flutterpi *flutterpi, void **userdata_out) { + int ok; + + (void) userdata_out; + + struct plugin *plugin = calloc(1, sizeof(struct plugin)); + if (plugin == NULL) { + return PLUGIN_INIT_RESULT_ERROR; + } + + plugin->initialized = false; + + ok = plugin_registry_set_receiver_v2_locked( + flutterpi_get_plugin_registry(flutterpi), + AUDIOPLAYERS_GLOBAL_CHANNEL, + on_global_method_call, + plugin + ); + if (ok != 0) { + return PLUGIN_INIT_RESULT_ERROR; + } + + ok = plugin_registry_set_receiver_v2_locked( + flutterpi_get_plugin_registry(flutterpi), + AUDIOPLAYERS_LOCAL_CHANNEL, + on_player_method_call, + plugin + ); + if (ok != 0) { + goto fail_remove_global_receiver; } + + return PLUGIN_INIT_RESULT_INITIALIZED; + +fail_remove_global_receiver: + plugin_registry_remove_receiver_v2_locked( + flutterpi_get_plugin_registry(flutterpi), + AUDIOPLAYERS_GLOBAL_CHANNEL + ); + + return PLUGIN_INIT_RESULT_ERROR; +} + +void audioplayers_plugin_deinit(struct flutterpi *flutterpi, void *userdata) { + (void) flutterpi; + + ASSERT_NOT_NULL(userdata); + struct plugin *plugin = userdata; + + plugin_registry_remove_receiver_v2_locked(flutterpi_get_plugin_registry(flutterpi), AUDIOPLAYERS_GLOBAL_CHANNEL); + plugin_registry_remove_receiver_v2_locked(flutterpi_get_plugin_registry(flutterpi), AUDIOPLAYERS_LOCAL_CHANNEL); + + const char *id; + struct gstplayer *player; + kh_foreach(&plugin->players, id, player, { + gstplayer_destroy(player); + free((char*) id); + }) } FLUTTERPI_PLUGIN("audioplayers", audioplayers, audioplayers_plugin_init, audioplayers_plugin_deinit) From 07402534e3b6fdbd644bde998988751a39eb67e2 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Mon, 12 May 2025 23:24:04 +0200 Subject: [PATCH 13/41] gstplayer: move gstplayer into separate file Finishing unifying playback element used by audioplayers and video player plugins. --- CMakeLists.txt | 104 ++- config.h.in | 1 + src/plugins/audioplayers/player.c | 605 ------------------ src/plugins/audioplayers/plugin.c | 6 +- .../player.c => gstplayer.c} | 23 +- src/plugins/gstplayer.h | 217 +++++++ src/plugins/gstreamer_video_player.h | 213 +----- .../flutter_texture_sink.c | 12 +- src/plugins/gstreamer_video_player/frame.c | 5 +- src/plugins/gstreamer_video_player/plugin.c | 13 +- 10 files changed, 314 insertions(+), 885 deletions(-) delete mode 100644 src/plugins/audioplayers/player.c rename src/plugins/{gstreamer_video_player/player.c => gstplayer.c} (98%) create mode 100644 src/plugins/gstplayer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 76392b57..cb42b54e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -305,74 +305,66 @@ endif() if (BUILD_TEST_PLUGIN) target_sources(flutterpi_module PRIVATE src/plugins/testplugin.c) endif() -if (BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN) - if (NOT HAVE_EGL_GLES2) - message(NOTICE "EGL and OpenGL ES2 are required for gstreamer video player. Gstreamer video player plugin won't be build.") - else() + +set(HAVE_GSTREAMER_VIDEO_PLAYER OFF) +if (BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN OR BUILD_GSTREAMER_AUDIO_PLAYER_PLUGIN)# + pkg_check_modules(LIBGSTREAMER IMPORTED_TARGET + gstreamer-1.0 + gstreamer-plugins-base-1.0 + gstreamer-app-1.0 + gstreamer-allocators-1.0 + gstreamer-video-1.0 + gstreamer-audio-1.0 + ) + + if (LIBGSTREAMER_FOUND) + string(REPLACE "." ";" LIBGSTREAMER_VERSION_AS_LIST ${LIBGSTREAMER_gstreamer-1.0_VERSION}) + list(GET LIBGSTREAMER_VERSION_AS_LIST 0 LIBGSTREAMER_VERSION_MAJOR) + list(GET LIBGSTREAMER_VERSION_AS_LIST 1 LIBGSTREAMER_VERSION_MINOR) + list(GET LIBGSTREAMER_VERSION_AS_LIST 2 LIBGSTREAMER_VERSION_PATCH) + + target_sources(flutterpi_module PRIVATE src/plugins/gstplayer.c) + target_link_libraries(flutterpi_module PUBLIC PkgConfig::LIBGSTREAMER) + endif() + + if (BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN AND NOT LIBGSTREAMER_FOUND) if (TRY_BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN) - pkg_check_modules(LIBGSTREAMER IMPORTED_TARGET gstreamer-1.0) - pkg_check_modules(LIBGSTREAMER_PLUGINS_BASE IMPORTED_TARGET gstreamer-plugins-base-1.0) - pkg_check_modules(LIBGSTREAMER_APP IMPORTED_TARGET gstreamer-app-1.0) - pkg_check_modules(LIBGSTREAMER_ALLOCATORS IMPORTED_TARGET gstreamer-allocators-1.0) - pkg_check_modules(LIBGSTREAMER_VIDEO IMPORTED_TARGET gstreamer-video-1.0) + message(NOTICE "Some required gstreamer dependencies were not found. Gstreamer video player plugin won't be built.") else() - pkg_check_modules(LIBGSTREAMER REQUIRED IMPORTED_TARGET gstreamer-1.0) - pkg_check_modules(LIBGSTREAMER_PLUGINS_BASE REQUIRED IMPORTED_TARGET gstreamer-plugins-base-1.0) - pkg_check_modules(LIBGSTREAMER_APP REQUIRED IMPORTED_TARGET gstreamer-app-1.0) - pkg_check_modules(LIBGSTREAMER_ALLOCATORS REQUIRED IMPORTED_TARGET gstreamer-allocators-1.0) - pkg_check_modules(LIBGSTREAMER_VIDEO REQUIRED IMPORTED_TARGET gstreamer-video-1.0) + message(ERROR "Some required gstreamer dependencies were not found. Can't build gstreamer video player plugin.") endif() + endif() - if (LIBGSTREAMER_FOUND AND LIBGSTREAMER_PLUGINS_BASE_FOUND AND LIBGSTREAMER_APP_FOUND AND LIBGSTREAMER_ALLOCATORS_FOUND AND LIBGSTREAMER_VIDEO_FOUND) - # There's no other way to query the libinput version (in code) somehow. - # So we need to roll our own libinput version macro - string(REPLACE "." ";" LIBGSTREAMER_VERSION_AS_LIST ${LIBGSTREAMER_VERSION}) - list(GET LIBGSTREAMER_VERSION_AS_LIST 0 LIBGSTREAMER_VERSION_MAJOR) - list(GET LIBGSTREAMER_VERSION_AS_LIST 1 LIBGSTREAMER_VERSION_MINOR) - list(GET LIBGSTREAMER_VERSION_AS_LIST 2 LIBGSTREAMER_VERSION_PATCH) - - target_sources(flutterpi_module PRIVATE - src/plugins/gstreamer_video_player/plugin.c - src/plugins/gstreamer_video_player/player.c - src/plugins/gstreamer_video_player/frame.c - src/plugins/gstreamer_video_player/flutter_texture_sink.c - ) - target_link_libraries(flutterpi_module PUBLIC - PkgConfig::LIBGSTREAMER - PkgConfig::LIBGSTREAMER_PLUGINS_BASE - PkgConfig::LIBGSTREAMER_APP - PkgConfig::LIBGSTREAMER_ALLOCATORS - PkgConfig::LIBGSTREAMER_VIDEO - ) + if (BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN AND NOT HAVE_EGL_GLES2) + if (TRY_BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN) + message(NOTICE "EGL and OpenGL ES2 are required for gstreamer video player. Gstreamer video player plugin won't be built.") else() - message(NOTICE "Couldn't find gstreamer libraries. Gstreamer video player plugin won't be build.") + message(ERROR "EGL and OpenGL ES2 are required for gstreamer video player. Can't build gstreamer video player plugin.") endif() endif() -endif() -if (BUILD_GSTREAMER_AUDIO_PLAYER_PLUGIN) - if (TRY_BUILD_GSTREAMER_AUDIO_PLAYER_PLUGIN) - pkg_check_modules(LIBGSTREAMER IMPORTED_TARGET gstreamer-1.0) - pkg_check_modules(LIBGSTREAMER_APP IMPORTED_TARGET gstreamer-app-1.0) - pkg_check_modules(LIBGSTREAMER_AUDIO IMPORTED_TARGET gstreamer-audio-1.0) - else() - pkg_check_modules(LIBGSTREAMER REQUIRED IMPORTED_TARGET gstreamer-1.0) - pkg_check_modules(LIBGSTREAMER_APP REQUIRED IMPORTED_TARGET gstreamer-app-1.0) - pkg_check_modules(LIBGSTREAMER_AUDIO REQUIRED IMPORTED_TARGET gstreamer-audio-1.0) + if (BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN AND LIBGSTREAMER_FOUND AND HAVE_EGL_GLES2) + set(HAVE_GSTREAMER_VIDEO_PLAYER ON) + target_sources(flutterpi_module PRIVATE + src/plugins/gstreamer_video_player/frame.c + src/plugins/gstreamer_video_player/flutter_texture_sink.c + src/plugins/gstreamer_video_player/plugin.c + ) endif() - if (LIBGSTREAMER_FOUND AND LIBGSTREAMER_APP_FOUND AND LIBGSTREAMER_AUDIO_FOUND) + if (BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN AND NOT LIBGSTREAMER_FOUND) + if (TRY_BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN) + message(NOTICE "Some required gstreamer dependencies were not found. Gstreamer audio player plugin won't be built.") + else() + message(ERROR "Some required gstreamer dependencies were not found. Can't build gstreamer audio player plugin.") + endif() + endif() + + if (BUILD_GSTREAMER_AUDIO_PLAYER_PLUGIN AND LIBGSTREAMER_FOUND) target_sources(flutterpi_module PRIVATE src/plugins/audioplayers/plugin.c src/plugins/audioplayers/player.c ) - target_link_libraries(flutterpi_module PUBLIC - PkgConfig::LIBGSTREAMER - PkgConfig::LIBGSTREAMER_APP - PkgConfig::LIBGSTREAMER_AUDIO - ) - else() - message(NOTICE "Couldn't find gstreamer libraries. Gstreamer audio player plugin won't be build.") endif() endif() @@ -391,10 +383,10 @@ if (BUILD_SENTRY_PLUGIN) if (SENTRY_BACKEND STREQUAL "crashpad" AND SENTRY_PLUGIN_BUNDLE_CRASHPAD_HANDLER) set(HAVE_BUNDLED_CRASHPAD_HANDLER ON) - + target_sources(flutter-pi PRIVATE src/crashpad_handler_trampoline.cc) # link against the same libraries the crashpad_handler uses - + get_target_property(handler_deps crashpad_handler INTERFACE_LINK_LIBRARIES) target_link_libraries(flutter-pi PUBLIC ${handler_deps}) endif() diff --git a/config.h.in b/config.h.in index 8a8d8a88..fe28bd89 100644 --- a/config.h.in +++ b/config.h.in @@ -26,5 +26,6 @@ #cmakedefine ENABLE_MTRACE #cmakedefine ENABLE_ASAN #cmakedefine HAVE_BUNDLED_CRASHPAD_HANDLER +#cmakedefine HAVE_GSTREAMER_VIDEO_PLAYER #endif diff --git a/src/plugins/audioplayers/player.c b/src/plugins/audioplayers/player.c deleted file mode 100644 index 1e948e62..00000000 --- a/src/plugins/audioplayers/player.c +++ /dev/null @@ -1,605 +0,0 @@ -#define _GNU_SOURCE - -#include -#include - -#include -#include -#include -#include - -#include "flutter-pi.h" -#include "platformchannel.h" -#include "plugins/audioplayers.h" -#include "util/asserts.h" -#include "util/logging.h" - -struct audio_player { - GstElement *source; - GstElement *playbin; - GstBus *bus; - - GstElement *panorama; - GstElement *audiobin; - GstElement *audiosink; - GstPad *panoramaSinkPad; - - bool is_initialized; - bool is_playing; - bool is_looping; - bool is_seek_completed; - double playback_rate; - - char *url; - char *player_id; - char *event_channel_name; - - _Atomic bool event_subscribed; -}; - -// Private Class functions -static gboolean audio_player_on_bus_message(GstBus *bus, GstMessage *message, struct audio_player *data); -static gboolean audio_player_on_refresh(struct audio_player *data); -static void audio_player_set_playback(struct audio_player *self, int64_t seekTo, double rate); -static void audio_player_on_media_error(struct audio_player *self, GError *error, gchar *debug); -static void audio_player_on_media_state_change(struct audio_player *self, GstObject *src, GstState *old_state, GstState *new_state); -static void audio_player_on_prepared(struct audio_player *self, bool value); -static void audio_player_on_position_update(struct audio_player *self); -static void audio_player_on_duration_update(struct audio_player *self); -static void audio_player_on_seek_completed(struct audio_player *self); -static void audio_player_on_playback_ended(struct audio_player *self); - -static int on_bus_fd_ready(sd_event_source *src, int fd, uint32_t revents, void *userdata) { - struct audio_player *player = userdata; - GstMessage *msg; - - (void) src; - (void) fd; - (void) revents; - - /* DEBUG_TRACE_BEGIN(player, "on_bus_fd_ready"); */ - - msg = gst_bus_pop(player->bus); - if (msg != NULL) { - audio_player_on_bus_message(player->bus, msg, player); - gst_message_unref(msg); - } - - /* DEBUG_TRACE_END(player, "on_bus_fd_ready"); */ - - return 0; -} - -static void audio_player_source_setup(GstElement *playbin, GstElement *source, GstElement **p_src) { - (void)(playbin); - (void)(p_src); - - if (g_object_class_find_property(G_OBJECT_GET_CLASS(source), "ssl-strict") != 0) { - g_object_set(G_OBJECT(source), "ssl-strict", FALSE, NULL); - } -} - -struct audio_player *audio_player_new(char *player_id, char *channel) { - GPollFD fd; - sd_event_source *busfd_event_source; - int ok; - - struct audio_player *self = malloc(sizeof(struct audio_player)); - if (self == NULL) { - return NULL; - } - - self->url = NULL; - self->source = NULL; - self->is_initialized = false; - self->is_playing = false; - self->is_looping = false; - self->is_seek_completed = false; - self->playback_rate = 1.0; - self->event_subscribed = false; - - gst_init(NULL, NULL); - self->playbin = gst_element_factory_make("playbin", NULL); - if (!self->playbin) { - LOG_ERROR("Could not create gstreamer playbin.\n"); - goto deinit_self; - } - - // Setup stereo balance controller - self->panorama = gst_element_factory_make("audiopanorama", NULL); - if (self->panorama) { - self->audiobin = gst_bin_new(NULL); - self->audiosink = gst_element_factory_make("autoaudiosink", NULL); - - gst_bin_add_many(GST_BIN(self->audiobin), self->panorama, self->audiosink, NULL); - gst_element_link(self->panorama, self->audiosink); - - GstPad *sinkpad = gst_element_get_static_pad(self->panorama, "sink"); - self->panoramaSinkPad = gst_ghost_pad_new("sink", sinkpad); - gst_element_add_pad(self->audiobin, self->panoramaSinkPad); - gst_object_unref(GST_OBJECT(sinkpad)); - - g_object_set(G_OBJECT(self->playbin), "audio-sink", self->audiobin, NULL); - g_object_set(G_OBJECT(self->panorama), "method", 1, NULL); - } else { - self->audiobin = NULL; - self->audiosink = NULL; - self->panoramaSinkPad = NULL; - } - - g_signal_connect(self->playbin, "source-setup", G_CALLBACK(audio_player_source_setup), &self->source); - - self->bus = gst_element_get_bus(self->playbin); - - gst_bus_get_pollfd(self->bus, &fd); - - flutterpi_sd_event_add_io(&busfd_event_source, fd.fd, EPOLLIN, on_bus_fd_ready, self); - - // Refresh continuously to emit recurring events - g_timeout_add(1000, (GSourceFunc) audio_player_on_refresh, self); - - self->player_id = strdup(player_id); - if (self->player_id == NULL) { - goto deinit_player; - } - - // audioplayers player event channel clang: - // /events/ - ok = asprintf(&self->event_channel_name, "%s/events/%s", channel, player_id); - ASSERT_MSG(ok, "event channel name OEM"); - - if (ok < 0) { - goto deinit_player_id; - } - - return self; - - //Deinit doesn't require to NULL, as we just delete player. -deinit_player_id: - free(self->player_id); - -deinit_player: - gst_object_unref(self->bus); - - if (self->panorama != NULL) { - gst_element_set_state(self->audiobin, GST_STATE_NULL); - - gst_element_remove_pad(self->audiobin, self->panoramaSinkPad); - gst_bin_remove(GST_BIN(self->audiobin), self->audiosink); - gst_bin_remove(GST_BIN(self->audiobin), self->panorama); - - self->panorama = NULL; - self->audiosink = NULL; - self->panoramaSinkPad = NULL; - self->audiobin = NULL; - } - - gst_element_set_state(self->playbin, GST_STATE_NULL); - gst_object_unref(self->playbin); - -deinit_self: - free(self); - return NULL; -} - -gboolean audio_player_on_bus_message(GstBus *bus, GstMessage *message, struct audio_player *data) { - (void) bus; - switch (GST_MESSAGE_TYPE(message)) { - case GST_MESSAGE_ERROR: { - GError *err; - gchar *debug; - - gst_message_parse_error(message, &err, &debug); - audio_player_on_media_error(data, err, debug); - g_error_free(err); - g_free(debug); - break; - } - case GST_MESSAGE_STATE_CHANGED: { - GstState old_state, new_state; - - gst_message_parse_state_changed(message, &old_state, &new_state, NULL); - audio_player_on_media_state_change(data, message->src, &old_state, &new_state); - break; - } - case GST_MESSAGE_EOS: - audio_player_on_playback_ended(data); - break; - case GST_MESSAGE_DURATION_CHANGED: - audio_player_on_duration_update(data); - break; - case GST_MESSAGE_ASYNC_DONE: - if (!data->is_seek_completed) { - audio_player_on_seek_completed(data); - data->is_seek_completed = true; - } - break; - default: - // For more GstMessage types see: - // https://gstreamer.freedesktop.org/documentation/gstreamer/gstmessage.html?gi-language=c#enumerations - break; - } - - // Continue watching for messages - return TRUE; -} - -gboolean audio_player_on_refresh(struct audio_player *self) { - if (self == NULL) { - return FALSE; - } - - GstState playbinState; - gst_element_get_state(self->playbin, &playbinState, NULL, GST_CLOCK_TIME_NONE); - if (playbinState == GST_STATE_PLAYING) { - audio_player_on_position_update(self); - } - return TRUE; -} - -void audio_player_set_playback(struct audio_player *self, int64_t seekTo, double rate) { - const GstSeekFlags seek_flags = GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE; - - if (!self->is_initialized) { - return; - } - // See: - // https://gstreamer.freedesktop.org/documentation/tutorials/basic/playback-speed.html?gi-language=c - if (!self->is_seek_completed) { - return; - } - if (rate == 0) { - // Do not set rate if it's 0, rather pause. - audio_player_pause(self); - return; - } - self->playback_rate = rate; - self->is_seek_completed = false; - - GstEvent *seek_event; - if (rate > 0) { - seek_event = gst_event_new_seek(rate, GST_FORMAT_TIME, seek_flags, GST_SEEK_TYPE_SET, seekTo * GST_MSECOND, GST_SEEK_TYPE_NONE, -1); - } else { - seek_event = gst_event_new_seek(rate, GST_FORMAT_TIME, seek_flags, GST_SEEK_TYPE_SET, 0, GST_SEEK_TYPE_SET, seekTo * GST_MSECOND); - } - - if (!gst_element_send_event(self->playbin, seek_event)) { - // Not clear how to treat this error? - const int64_t seekMs = seekTo * GST_MSECOND; - LOG_ERROR("Could not set playback to position " GST_STIME_FORMAT " and rate %f.\n", GST_TIME_ARGS(seekMs), rate); - self->is_seek_completed = true; - } -} - -void audio_player_on_media_error(struct audio_player *self, GError *error, gchar *debug) { - if (!self->event_subscribed) { - return; - } - - char error_code[16] = {0}; - snprintf(error_code, sizeof(error_code), "%d", error->code); - // clang-format off - platch_send_error_event_std( - self->event_channel_name, - error_code, - error->message, - debug ? &STDSTRING(debug) : NULL - ); - // clang-format on -} - -void audio_player_on_media_state_change(struct audio_player *self, GstObject *src, GstState *old_state, GstState *new_state) { - (void) old_state; - if (src == GST_OBJECT(self->playbin)) { - LOG_DEBUG("%s: on_media_state_change(old_state=%d, new_state=%d)\n", self->player_id, *old_state, *new_state); - if (*new_state == GST_STATE_READY) { - // Need to set to pause state, in order to make player functional - GstStateChangeReturn ret = gst_element_set_state(self->playbin, GST_STATE_PAUSED); - if (ret == GST_STATE_CHANGE_FAILURE) { - LOG_ERROR("Unable to set the pipeline to the paused state.\n"); - } - - self->is_initialized = false; - } else if (*old_state == GST_STATE_PAUSED && *new_state == GST_STATE_PLAYING) { - audio_player_on_position_update(self); - audio_player_on_duration_update(self); - } else if (*new_state >= GST_STATE_PAUSED) { - if (!self->is_initialized) { - self->is_initialized = true; - audio_player_on_prepared(self, true); - if (self->is_playing) { - audio_player_resume(self); - } - } - } else if (self->is_initialized) { - self->is_initialized = false; - } - } -} - -void audio_player_on_prepared(struct audio_player *self, bool value) { - if (!self->event_subscribed) { - return; - } - - // clang-format off - platch_send_success_event_std( - self->event_channel_name, - &STDMAP2( - STDSTRING("event"), STDSTRING("audio.onPrepared"), - STDSTRING("value"), STDBOOL(value) - ) - ); - // clang-format on -} - -void audio_player_on_position_update(struct audio_player *self) { - if (!self->event_subscribed) { - return; - } - - // clang-format off - platch_send_success_event_std( - self->event_channel_name, - &STDMAP2( - STDSTRING("event"), STDSTRING("audio.onCurrentPosition"), - STDSTRING("value"), STDINT64(audio_player_get_position(self)) - ) - ); - // clang-format on -} - -void audio_player_on_duration_update(struct audio_player *self) { - if (!self->event_subscribed) { - return; - } - // clang-format off - platch_send_success_event_std( - self->event_channel_name, - &STDMAP2( - STDSTRING("event"), STDSTRING("audio.onDuration"), - STDSTRING("value"), STDINT64(audio_player_get_duration(self)) - ) - ); - // clang-format on -} -void audio_player_on_seek_completed(struct audio_player *self) { - audio_player_on_position_update(self); - - if (self->event_subscribed) { - // clang-format off - platch_send_success_event_std( - self->event_channel_name, - &STDMAP2( - STDSTRING("event"), STDSTRING("audio.onSeekComplete"), - STDSTRING("value"), STDBOOL(true) - ) - ); - // clang-format on - } - self->is_seek_completed = true; -} -void audio_player_on_playback_ended(struct audio_player *self) { - if (self->event_subscribed) { - // clang-format off - platch_send_success_event_std( - self->event_channel_name, - &STDMAP2( - STDSTRING("event"), STDSTRING("audio.onComplete"), - STDSTRING("value"), STDBOOL(true) - ) - ); - // clang-format on - } - - if (audio_player_get_looping(self)) { - audio_player_play(self); - } else { - audio_player_pause(self); - audio_player_set_position(self, 0); - } -} - -void audio_player_set_looping(struct audio_player *self, bool is_looping) { - self->is_looping = is_looping; -} - -bool audio_player_get_looping(struct audio_player *self) { - return self->is_looping; -} - -void audio_player_play(struct audio_player *self) { - audio_player_set_position(self, 0); - audio_player_resume(self); -} - -void audio_player_pause(struct audio_player *self) { - self->is_playing = false; - - if (!self->is_initialized) { - return; - } - - GstStateChangeReturn ret = gst_element_set_state(self->playbin, GST_STATE_PAUSED); - if (ret == GST_STATE_CHANGE_FAILURE) { - LOG_ERROR("Unable to set the pipeline to the paused state.\n"); - return; - } - audio_player_on_position_update(self); // Update to exact position when pausing -} - -void audio_player_resume(struct audio_player *self) { - self->is_playing = true; - if (!self->is_initialized) { - return; - } - - GstStateChangeReturn ret = gst_element_set_state(self->playbin, GST_STATE_PLAYING); - if (ret == GST_STATE_CHANGE_FAILURE) { - LOG_ERROR("Unable to set the pipeline to the playing state.\n"); - return; - } - audio_player_on_position_update(self); - audio_player_on_duration_update(self); -} - -void audio_player_destroy(struct audio_player *self) { - if (self->is_initialized) { - audio_player_pause(self); - } - - if (self->source) { - gst_object_unref(GST_OBJECT(self->source)); - self->source = NULL; - } - - gst_object_unref(self->bus); - self->bus = NULL; - - if (self->panorama != NULL) { - gst_element_set_state(self->audiobin, GST_STATE_NULL); - - gst_element_remove_pad(self->audiobin, self->panoramaSinkPad); - gst_bin_remove(GST_BIN(self->audiobin), self->audiosink); - gst_bin_remove(GST_BIN(self->audiobin), self->panorama); - - self->panorama = NULL; - self->audiosink = NULL; - self->panoramaSinkPad = NULL; - self->audiobin = NULL; - } - - gst_element_set_state(self->playbin, GST_STATE_NULL); - gst_object_unref(self->playbin); - self->playbin = NULL; - - self->is_initialized = false; - - if (self->url != NULL) { - free(self->url); - self->url = NULL; - } - - if (self->player_id != NULL) { - free(self->player_id); - self->player_id = NULL; - } - - if (self->event_channel_name != NULL) { - free(self->event_channel_name); - self->event_channel_name = NULL;; - } - - free(self); -} - -int64_t audio_player_get_position(struct audio_player *self) { - gint64 current = 0; - if (!gst_element_query_position(self->playbin, GST_FORMAT_TIME, ¤t)) { - LOG_ERROR("Could not query current position.\n"); - return 0; - } - return current / 1000000; -} - -int64_t audio_player_get_duration(struct audio_player *self) { - gint64 duration = 0; - if (!gst_element_query_duration(self->playbin, GST_FORMAT_TIME, &duration)) { - LOG_ERROR("Could not query current duration.\n"); - return 0; - } - return duration / 1000000; -} - -void audio_player_set_volume(struct audio_player *self, double volume) { - if (volume > 1) { - volume = 1; - } else if (volume < 0) { - volume = 0; - } - g_object_set(G_OBJECT(self->playbin), "volume", volume, NULL); -} - -void audio_player_set_balance(struct audio_player *self, double balance) { - if (!self->panorama) { - return; - } - - if (balance > 1.0l) { - balance = 1.0l; - } else if (balance < -1.0l) { - balance = -1.0l; - } - g_object_set(G_OBJECT(self->panorama), "panorama", balance, NULL); -} - -void audio_player_set_playback_rate(struct audio_player *self, double rate) { - audio_player_set_playback(self, audio_player_get_position(self), rate); -} - -void audio_player_set_position(struct audio_player *self, int64_t position) { - if (!self->is_initialized) { - return; - } - audio_player_set_playback(self, position, self->playback_rate); -} - -void audio_player_set_source_url(struct audio_player *self, char *url) { - ASSERT_NOT_NULL(url); - if (self->url == NULL || !streq(self->url, url)) { - LOG_DEBUG("%s: set source=%s\n", self->player_id, url); - if (self->url != NULL) { - free(self->url); - self->url = NULL; - } - self->url = strdup(url); - gst_element_set_state(self->playbin, GST_STATE_NULL); - self->is_initialized = false; - self->is_playing = false; - - if (strlen(self->url) != 0) { - g_object_set(self->playbin, "uri", self->url, NULL); - if (self->playbin->current_state != GST_STATE_READY) { - if (gst_element_set_state(self->playbin, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE) { - //This should not happen generally - LOG_ERROR("Could not set player into ready state.\n"); - } - } - } - } else { - audio_player_on_prepared(self, true); - } -} - -bool audio_player_is_id(struct audio_player *self, char *player_id) { - return streq(self->player_id, player_id); -} - -const char* audio_player_subscribe_channel_name(const struct audio_player *self) { - return self->event_channel_name; -} - -bool audio_player_set_subscription_status(struct audio_player *self, const char *channel, bool value) { - if (strcmp(self->event_channel_name, channel) == 0) { - self->event_subscribed = value; - return true; - } else { - return false; - } -} - -void audio_player_release(struct audio_player *self) { - self->is_initialized = false; - self->is_playing = false; - if (self->url != NULL) { - free(self->url); - self->url = NULL; - } - - GstState playbinState; - gst_element_get_state(self->playbin, &playbinState, NULL, GST_CLOCK_TIME_NONE); - - if (playbinState > GST_STATE_NULL) { - gst_element_set_state(self->playbin, GST_STATE_NULL); - } -} diff --git a/src/plugins/audioplayers/plugin.c b/src/plugins/audioplayers/plugin.c index 7e291d77..c68b28d5 100644 --- a/src/plugins/audioplayers/plugin.c +++ b/src/plugins/audioplayers/plugin.c @@ -4,6 +4,9 @@ #include #include +#include +#include + #include "flutter_embedder.h" #include "util/asserts.h" #include "util/macros.h" @@ -17,8 +20,7 @@ #include "util/list.h" #include "util/logging.h" #include "util/khash.h" - -#include "plugins/gstreamer_video_player.h" +#include "plugins/gstplayer.h" #define AUDIOPLAYERS_LOCAL_CHANNEL "xyz.luan/audioplayers" #define AUDIOPLAYERS_GLOBAL_CHANNEL "xyz.luan/audioplayers.global" diff --git a/src/plugins/gstreamer_video_player/player.c b/src/plugins/gstplayer.c similarity index 98% rename from src/plugins/gstreamer_video_player/player.c rename to src/plugins/gstplayer.c index 82296fd5..3981a672 100644 --- a/src/plugins/gstreamer_video_player/player.c +++ b/src/plugins/gstplayer.c @@ -1,5 +1,6 @@ #define _GNU_SOURCE +#include #include #include #include @@ -17,14 +18,21 @@ #include #include + #include "flutter-pi.h" #include "notifier_listener.h" #include "platformchannel.h" #include "pluginregistry.h" -#include "plugins/gstreamer_video_player.h" +#include "plugins/gstplayer.h" #include "texture_registry.h" #include "util/logging.h" +#include "config.h" + +#ifdef HAVE_GSTREAMER_VIDEO_PLAYER + #include "gstreamer_video_player.h" +#endif + #define LOG_GST_SET_STATE_ERROR(_element) \ LOG_ERROR( \ "setting gstreamer playback state failed. gst_element_set_state(element name: %s): GST_STATE_CHANGE_FAILURE\n", \ @@ -742,7 +750,7 @@ typedef enum { GST_PLAY_FLAG_FORCE_SW_DECODERS = (1 << 12), } GstPlayFlags; -static void on_element_setup(GstElement *playbin, GstElement *element, gpointer userdata) { +UNUSED static void on_element_setup(GstElement *playbin, GstElement *element, gpointer userdata) { (void) playbin; (void) userdata; @@ -791,7 +799,7 @@ static void on_about_to_finish(GstElement *playbin, gpointer userdata) { gst_object_unref(bus); } -static GstPadProbeReturn on_video_sink_event(GstPad *pad, GstPadProbeInfo *info, gpointer userdata) { +UNUSED static GstPadProbeReturn on_video_sink_event(GstPad *pad, GstPadProbeInfo *info, gpointer userdata) { GstBus *bus = userdata; (void) pad; @@ -899,6 +907,7 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo g_object_set(p->playbin, "flags", flags, NULL); if (play_video) { +#ifdef HAVE_GSTREAMER_VIDEO_PLAYER p->texture = flutterpi_create_texture(flutterpi); if (p->texture == NULL) { goto fail_unref_playbin; @@ -932,6 +941,12 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo g_signal_connect(p->playbin, "element-setup", G_CALLBACK(on_element_setup), NULL); gst_object_unref(sink); +#else + (void) flutterpi; + + ASSERT_MSG(0, "Video playback with gstplayer is only supported when building with the gstreamer video player plugin."); + goto fail_unref_playbin; +#endif } if (play_audio) { @@ -981,7 +996,7 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo fail_rm_event_source: sd_event_source_disable_unref(p->busfd_events); -fail_destroy_texture: +fail_destroy_texture: UNUSED gst_object_unref(p->playbin); // The flutter upload sink uses the texture internally, diff --git a/src/plugins/gstplayer.h b/src/plugins/gstplayer.h new file mode 100644 index 00000000..c97c3714 --- /dev/null +++ b/src/plugins/gstplayer.h @@ -0,0 +1,217 @@ +#ifndef _FLUTTERPI_INCLUDE_PLUGINS_GSTPLAYER_H +#define _FLUTTERPI_INCLUDE_PLUGINS_GSTPLAYER_H + +#include +#include + +#include "config.h" + +#define GSTREAMER_VER(major, minor, patch) ((((major) &0xFF) << 16) | (((minor) &0xFF) << 8) | ((patch) &0xFF)) +#define THIS_GSTREAMER_VER GSTREAMER_VER(LIBGSTREAMER_VERSION_MAJOR, LIBGSTREAMER_VERSION_MINOR, LIBGSTREAMER_VERSION_PATCH) + +enum format_hint { FORMAT_HINT_NONE, FORMAT_HINT_MPEG_DASH, FORMAT_HINT_HLS, FORMAT_HINT_SS, FORMAT_HINT_OTHER }; + +enum buffering_mode { BUFFERING_MODE_STREAM, BUFFERING_MODE_DOWNLOAD, BUFFERING_MODE_TIMESHIFT, BUFFERING_MODE_LIVE }; + +struct buffering_range { + int64_t start_ms; + int64_t stop_ms; +}; + +struct buffering_state { + // The percentage that the buffer is filled. + // If this is 100 playback will resume. + int percent; + + // The buffering mode currently used by the pipeline. + enum buffering_mode mode; + + // The average input / consumption speed in bytes per second. + int avg_in, avg_out; + + // Time left till buffering finishes, in ms. + // 0 means not buffering right now. + int64_t time_left_ms; + + // The ranges of already buffered video. + // For the BUFFERING_MODE_DOWNLOAD and BUFFERING_MODE_TIMESHIFT buffering modes, this specifies the ranges + // where efficient seeking is possible. + // For the BUFFERING_MODE_STREAM and BUFFERING_MODE_LIVE buffering modes, this describes the oldest and + // newest item in the buffer. + int n_ranges; + + // Flexible array member. + // For example, if n_ranges is 2, just allocate using + // `state = malloc(sizeof(struct buffering_state) + 2*sizeof(struct buffering_range))` + // and we can use state->ranges[0] and so on. + // This is cool because we don't need to allocate two blocks of memory and we can just call + // `free` once to free the whole thing. + // More precisely, we don't need to define a new function we can give to value_notifier_init + // as the value destructor, we can just use `free`. + struct buffering_range ranges[]; +}; + +#define BUFFERING_STATE_SIZE(n_ranges) (sizeof(struct buffering_state) + (n_ranges) * sizeof(struct buffering_range)) + +struct video_info; +struct gstplayer; +struct flutterpi; +struct notifier; + +typedef struct _GstStructure GstStructure; + +/// Create a gstreamer video player. +struct gstplayer *gstplayer_new( + struct flutterpi *flutterpi, + const char *uri, + void *userdata, + bool play_video, + bool play_audio, + bool subtitles, + GstStructure *headers +); + +/// Create a gstreamer video player that loads the video from a flutter asset. +/// @arg asset_path The path of the asset inside the asset bundle. +/// @arg package_name The name of the package containing the asset +/// @arg userdata The userdata associated with this player +struct gstplayer *gstplayer_new_from_asset(struct flutterpi *flutterpi, const char *asset_path, const char *package_name, void *userdata); + +/// Create a gstreamer video player that loads the video from a network URI. +/// @arg uri The URI to the video. (for example, http://, https://, rtmp://, rtsp://) +/// @arg format_hint A hint to the format of the video. FORMAT_HINT_NONE means there's no hint. +/// @arg userdata The userdata associated with this player. +struct gstplayer *gstplayer_new_from_network(struct flutterpi *flutterpi, const char *uri, enum format_hint format_hint, void *userdata, GstStructure *headers); + +/// Create a gstreamer video player that loads the video from a file URI. +/// @arg uri The file:// URI to the video. +/// @arg userdata The userdata associated with this player. +struct gstplayer *gstplayer_new_from_file(struct flutterpi *flutterpi, const char *uri, void *userdata); + +/// Create a gstreamer video player with a custom gstreamer pipeline. +/// @arg pipeline The description of the custom pipeline that should be used. Should contain an appsink called "sink". +/// @arg userdata The userdata associated with this player. +struct gstplayer *gstplayer_new_from_pipeline(struct flutterpi *flutterpi, const char *pipeline, void *userdata); + +/// Destroy this gstreamer player instance and the resources +/// associated with it. (texture, gstreamer pipeline, etc) +/// +/// Should be called on the flutterpi main/platform thread, +/// because otherwise destroying the gstreamer event bus listener +/// might be a race condition. +void gstplayer_destroy(struct gstplayer *player); + +/// Set the generic userdata associated with this gstreamer player instance. +/// Overwrites the userdata set in the constructor and any userdata previously +/// set using @ref gstplayer_set_userdata. +/// @arg userdata The new userdata that should be associated with this player. +void gstplayer_set_userdata(struct gstplayer *player, void *userdata); + +/// Get the userdata that was given to the constructor or was previously set using +/// @ref gstplayer_set_userdata. +/// @returns userdata associated with this player. +void *gstplayer_get_userdata(struct gstplayer *player); + +/// Get the id of the flutter external texture that this player is rendering into. +int64_t gstplayer_get_texture_id(struct gstplayer *player); + +/// Add a http header (consisting of a string key and value) to the list of http headers that +/// gstreamer will use when playing back from a HTTP/S URI. +/// This has no effect after @ref gstplayer_initialize was called. +void gstplayer_put_http_header(struct gstplayer *player, const char *key, const char *value); + +/// Initializes the video playback, i.e. boots up the gstreamer pipeline, starts +/// buffering the video. +/// @returns 0 if initialization was successfull, errno-style error code if an error ocurred. +int gstplayer_initialize(struct gstplayer *player); + +/// Set the current playback state to "playing" if that's not the case already. +/// @returns 0 if initialization was successfull, errno-style error code if an error ocurred. +int gstplayer_play(struct gstplayer *player); + +/// Sets the current playback state to "paused" if that's not the case already. +/// @returns 0 if initialization was successfull, errno-style error code if an error ocurred. +int gstplayer_pause(struct gstplayer *player); + +/// Get the current playback position. +/// @returns Current playback position, in milliseconds from the beginning of the video. +int64_t gstplayer_get_position(struct gstplayer *player); + +/// Get the duration of the currently playing medium. +/// @returns Duration of the current medium in milliseconds, -1 if the duration +/// is not yet known, or INT64_MAX for live sources. +int64_t gstplayer_get_duration(struct gstplayer *player); + +/// Set whether the video should loop. +/// @arg looping Whether the video should start playing from the beginning when the +/// end is reached. +int gstplayer_set_looping(struct gstplayer *player, bool looping); + +/// Set the playback volume. +/// @arg volume Desired volume as a value between 0 and 1. +int gstplayer_set_volume(struct gstplayer *player, double volume); + +/// Seek to a specific position in the video. +/// @arg position Position to seek to in milliseconds from the beginning of the video. +/// @arg nearest_keyframe If true, seek to the nearest keyframe instead. Might be faster but less accurate. +int gstplayer_seek_to(struct gstplayer *player, int64_t position, bool nearest_keyframe); + +/// Set the playback speed of the player. +/// 1.0: normal playback speed +/// 0.5: half playback speed +/// 2.0: double playback speed +int gstplayer_set_playback_speed(struct gstplayer *player, double playback_speed); + +int gstplayer_step_forward(struct gstplayer *player); + +int gstplayer_step_backward(struct gstplayer *player); + +void gstplayer_set_audio_balance(struct gstplayer *player, float balance); + +float gstplayer_get_audio_balance(struct gstplayer *player); + +bool gstplayer_release(struct gstplayer *p); + +bool gstplayer_preroll(struct gstplayer *p, const char *uri); + +struct video_info { + int width, height; + + double fps; + + int64_t duration_ms; + + bool can_seek; + int64_t seek_begin_ms, seek_end_ms; +}; + +/// @brief Get the value notifier for the video info. +/// +/// Gets notified with a value of type `struct video_info*` when the video info changes. +/// The listeners will be called on an internal gstreamer thread. +/// So you need to make sure you do the proper rethreading in the listener callback. +struct notifier *gstplayer_get_video_info_notifier(struct gstplayer *player); + +struct seeking_info { + bool can_seek; + int64_t seek_begin_ms, seek_end_ms; +}; + +struct notifier *gstplayer_get_seeking_info_notifier(struct gstplayer *player); + +struct notifier *gstplayer_get_duration_notifier(struct gstplayer *player); + +struct notifier *gstplayer_get_eos_notifier(struct gstplayer *player); + +/// @brief Get the value notifier for the buffering state. +/// +/// Gets notified with a value of type `struct buffering_state*` when the buffering state changes. +/// The listeners will be called on the main flutterpi platform thread. +struct notifier *gstplayer_get_buffering_state_notifier(struct gstplayer *player); + +/// @brief Get the change notifier for errors. +/// +/// Gets notified when an error happens. (Not yet implemented) +struct notifier *gstplayer_get_error_notifier(struct gstplayer *player); + +#endif diff --git a/src/plugins/gstreamer_video_player.h b/src/plugins/gstreamer_video_player.h index d130c448..6b248e6a 100644 --- a/src/plugins/gstreamer_video_player.h +++ b/src/plugins/gstreamer_video_player.h @@ -1,5 +1,5 @@ -#ifndef _FLUTTERPI_INCLUDE_PLUGINS_OMXPLAYER_VIDEO_PLUGIN_H -#define _FLUTTERPI_INCLUDE_PLUGINS_OMXPLAYER_VIDEO_PLUGIN_H +#ifndef _FLUTTERPI_INCLUDE_PLUGINS_GSTREAMER_VIDEO_PLAYER_H +#define _FLUTTERPI_INCLUDE_PLUGINS_GSTREAMER_VIDEO_PLAYER_H #include "util/collection.h" #include "util/lock_ops.h" @@ -16,212 +16,9 @@ #include "gles.h" #endif -#define GSTREAMER_VER(major, minor, patch) ((((major) &0xFF) << 16) | (((minor) &0xFF) << 8) | ((patch) &0xFF)) -#define THIS_GSTREAMER_VER GSTREAMER_VER(LIBGSTREAMER_VERSION_MAJOR, LIBGSTREAMER_VERSION_MINOR, LIBGSTREAMER_VERSION_PATCH) - -enum format_hint { FORMAT_HINT_NONE, FORMAT_HINT_MPEG_DASH, FORMAT_HINT_HLS, FORMAT_HINT_SS, FORMAT_HINT_OTHER }; - -enum buffering_mode { BUFFERING_MODE_STREAM, BUFFERING_MODE_DOWNLOAD, BUFFERING_MODE_TIMESHIFT, BUFFERING_MODE_LIVE }; - -struct buffering_range { - int64_t start_ms; - int64_t stop_ms; -}; - -struct buffering_state { - // The percentage that the buffer is filled. - // If this is 100 playback will resume. - int percent; - - // The buffering mode currently used by the pipeline. - enum buffering_mode mode; - - // The average input / consumption speed in bytes per second. - int avg_in, avg_out; - - // Time left till buffering finishes, in ms. - // 0 means not buffering right now. - int64_t time_left_ms; - - // The ranges of already buffered video. - // For the BUFFERING_MODE_DOWNLOAD and BUFFERING_MODE_TIMESHIFT buffering modes, this specifies the ranges - // where efficient seeking is possible. - // For the BUFFERING_MODE_STREAM and BUFFERING_MODE_LIVE buffering modes, this describes the oldest and - // newest item in the buffer. - int n_ranges; - - // Flexible array member. - // For example, if n_ranges is 2, just allocate using - // `state = malloc(sizeof(struct buffering_state) + 2*sizeof(struct buffering_range))` - // and we can use state->ranges[0] and so on. - // This is cool because we don't need to allocate two blocks of memory and we can just call - // `free` once to free the whole thing. - // More precisely, we don't need to define a new function we can give to value_notifier_init - // as the value destructor, we can just use `free`. - struct buffering_range ranges[]; -}; - -#define BUFFERING_STATE_SIZE(n_ranges) (sizeof(struct buffering_state) + (n_ranges) * sizeof(struct buffering_range)) - -struct video_info; -struct gstplayer; -struct flutterpi; - -typedef struct _GstStructure GstStructure; - -/// Create a gstreamer video player. -struct gstplayer *gstplayer_new( - struct flutterpi *flutterpi, - const char *uri, - void *userdata, - bool play_video, - bool play_audio, - bool subtitles, - GstStructure *headers -); - -/// Create a gstreamer video player that loads the video from a flutter asset. -/// @arg asset_path The path of the asset inside the asset bundle. -/// @arg package_name The name of the package containing the asset -/// @arg userdata The userdata associated with this player -struct gstplayer *gstplayer_new_from_asset(struct flutterpi *flutterpi, const char *asset_path, const char *package_name, void *userdata); - -/// Create a gstreamer video player that loads the video from a network URI. -/// @arg uri The URI to the video. (for example, http://, https://, rtmp://, rtsp://) -/// @arg format_hint A hint to the format of the video. FORMAT_HINT_NONE means there's no hint. -/// @arg userdata The userdata associated with this player. -struct gstplayer *gstplayer_new_from_network(struct flutterpi *flutterpi, const char *uri, enum format_hint format_hint, void *userdata, GstStructure *headers); - -/// Create a gstreamer video player that loads the video from a file URI. -/// @arg uri The file:// URI to the video. -/// @arg userdata The userdata associated with this player. -struct gstplayer *gstplayer_new_from_file(struct flutterpi *flutterpi, const char *uri, void *userdata); - -/// Create a gstreamer video player with a custom gstreamer pipeline. -/// @arg pipeline The description of the custom pipeline that should be used. Should contain an appsink called "sink". -/// @arg userdata The userdata associated with this player. -struct gstplayer *gstplayer_new_from_pipeline(struct flutterpi *flutterpi, const char *pipeline, void *userdata); - -/// Destroy this gstreamer player instance and the resources -/// associated with it. (texture, gstreamer pipeline, etc) -/// -/// Should be called on the flutterpi main/platform thread, -/// because otherwise destroying the gstreamer event bus listener -/// might be a race condition. -void gstplayer_destroy(struct gstplayer *player); - -/// Set the generic userdata associated with this gstreamer player instance. -/// Overwrites the userdata set in the constructor and any userdata previously -/// set using @ref gstplayer_set_userdata. -/// @arg userdata The new userdata that should be associated with this player. -void gstplayer_set_userdata(struct gstplayer *player, void *userdata); - -/// Get the userdata that was given to the constructor or was previously set using -/// @ref gstplayer_set_userdata. -/// @returns userdata associated with this player. -void *gstplayer_get_userdata(struct gstplayer *player); - -/// Get the id of the flutter external texture that this player is rendering into. -int64_t gstplayer_get_texture_id(struct gstplayer *player); - -/// Add a http header (consisting of a string key and value) to the list of http headers that -/// gstreamer will use when playing back from a HTTP/S URI. -/// This has no effect after @ref gstplayer_initialize was called. -void gstplayer_put_http_header(struct gstplayer *player, const char *key, const char *value); - -/// Initializes the video playback, i.e. boots up the gstreamer pipeline, starts -/// buffering the video. -/// @returns 0 if initialization was successfull, errno-style error code if an error ocurred. -int gstplayer_initialize(struct gstplayer *player); - -/// Set the current playback state to "playing" if that's not the case already. -/// @returns 0 if initialization was successfull, errno-style error code if an error ocurred. -int gstplayer_play(struct gstplayer *player); - -/// Sets the current playback state to "paused" if that's not the case already. -/// @returns 0 if initialization was successfull, errno-style error code if an error ocurred. -int gstplayer_pause(struct gstplayer *player); - -/// Get the current playback position. -/// @returns Current playback position, in milliseconds from the beginning of the video. -int64_t gstplayer_get_position(struct gstplayer *player); - -/// Get the duration of the currently playing medium. -/// @returns Duration of the current medium in milliseconds, -1 if the duration -/// is not yet known, or INT64_MAX for live sources. -int64_t gstplayer_get_duration(struct gstplayer *player); - -/// Set whether the video should loop. -/// @arg looping Whether the video should start playing from the beginning when the -/// end is reached. -int gstplayer_set_looping(struct gstplayer *player, bool looping); - -/// Set the playback volume. -/// @arg volume Desired volume as a value between 0 and 1. -int gstplayer_set_volume(struct gstplayer *player, double volume); - -/// Seek to a specific position in the video. -/// @arg position Position to seek to in milliseconds from the beginning of the video. -/// @arg nearest_keyframe If true, seek to the nearest keyframe instead. Might be faster but less accurate. -int gstplayer_seek_to(struct gstplayer *player, int64_t position, bool nearest_keyframe); - -/// Set the playback speed of the player. -/// 1.0: normal playback speed -/// 0.5: half playback speed -/// 2.0: double playback speed -int gstplayer_set_playback_speed(struct gstplayer *player, double playback_speed); - -int gstplayer_step_forward(struct gstplayer *player); - -int gstplayer_step_backward(struct gstplayer *player); - -void gstplayer_set_audio_balance(struct gstplayer *player, float balance); - -float gstplayer_get_audio_balance(struct gstplayer *player); - -bool gstplayer_release(struct gstplayer *p); - -bool gstplayer_preroll(struct gstplayer *p, const char *uri); - -struct video_info { - int width, height; - - double fps; - - int64_t duration_ms; - - bool can_seek; - int64_t seek_begin_ms, seek_end_ms; -}; - -/// @brief Get the value notifier for the video info. -/// -/// Gets notified with a value of type `struct video_info*` when the video info changes. -/// The listeners will be called on an internal gstreamer thread. -/// So you need to make sure you do the proper rethreading in the listener callback. -struct notifier *gstplayer_get_video_info_notifier(struct gstplayer *player); - -struct seeking_info { - bool can_seek; - int64_t seek_begin_ms, seek_end_ms; -}; - -struct notifier *gstplayer_get_seeking_info_notifier(struct gstplayer *player); - -struct notifier *gstplayer_get_duration_notifier(struct gstplayer *player); - -struct notifier *gstplayer_get_eos_notifier(struct gstplayer *player); - -/// @brief Get the value notifier for the buffering state. -/// -/// Gets notified with a value of type `struct buffering_state*` when the buffering state changes. -/// The listeners will be called on the main flutterpi platform thread. -struct notifier *gstplayer_get_buffering_state_notifier(struct gstplayer *player); - -/// @brief Get the change notifier for errors. -/// -/// Gets notified when an error happens. (Not yet implemented) -struct notifier *gstplayer_get_error_notifier(struct gstplayer *player); +#if !defined(HAVE_GSTREAMER_VIDEO_PLAYER) + #error "gstreamer_video_player.h can't be used when building without gstreamer video player." +#endif struct video_frame; struct gl_renderer; diff --git a/src/plugins/gstreamer_video_player/flutter_texture_sink.c b/src/plugins/gstreamer_video_player/flutter_texture_sink.c index 0d8a971a..c4cd6e5e 100644 --- a/src/plugins/gstreamer_video_player/flutter_texture_sink.c +++ b/src/plugins/gstreamer_video_player/flutter_texture_sink.c @@ -4,10 +4,18 @@ #include #include -#include "../gstreamer_video_player.h" +#include "plugins/gstreamer_video_player.h" +#include "plugins/gstplayer.h" #include "texture_registry.h" #include "util/logging.h" +#if !defined(HAVE_EGL_GLES2) + #error "gstreamer video player requires EGL and OpenGL ES2 support." +#else + #include "egl.h" + #include "gles.h" +#endif + struct texture_sink { struct texture *fl_texture; struct frame_interface *interface; @@ -153,7 +161,7 @@ static gboolean on_appsink_new_event(GstAppSink *appsink, gpointer userdata) { (void) userdata; GstMiniObject *obj; - + do { obj = gst_app_sink_try_pull_object(appsink, 0); if (obj == NULL) { diff --git a/src/plugins/gstreamer_video_player/frame.c b/src/plugins/gstreamer_video_player/frame.c index 24023b00..59062e0b 100644 --- a/src/plugins/gstreamer_video_player/frame.c +++ b/src/plugins/gstreamer_video_player/frame.c @@ -16,6 +16,7 @@ // This will error if we don't have EGL / OpenGL ES support. #include "gl_renderer.h" #include "plugins/gstreamer_video_player.h" +#include "plugins/gstplayer.h" #include "util/logging.h" #include "util/refcounting.h" @@ -28,7 +29,7 @@ struct video_frame { GstSample *sample; struct frame_interface *interface; - + uint32_t drm_format; int n_dmabuf_fds; @@ -876,7 +877,7 @@ static struct video_frame *frame_new_egl_imported(struct frame_interface *interf LOG_ERROR("Video format has no EGL equivalent.\n"); return NULL; } - + bool external_only; for_each_format_in_frame_interface(i, format, interface) { diff --git a/src/plugins/gstreamer_video_player/plugin.c b/src/plugins/gstreamer_video_player/plugin.c index 1d42008d..d2a85a54 100644 --- a/src/plugins/gstreamer_video_player/plugin.c +++ b/src/plugins/gstreamer_video_player/plugin.c @@ -15,6 +15,7 @@ #include "platformchannel.h" #include "pluginregistry.h" #include "plugins/gstreamer_video_player.h" +#include "plugins/gstplayer.h" #include "texture_registry.h" #include "util/collection.h" #include "util/list.h" @@ -97,9 +98,9 @@ static struct gstplayer *get_player_by_evch(const char *const event_channel_name /** * @brief Remove a player instance from the player list. - * + * * Assumes the plugin struct is not locked. - * + * */ static void remove_player(struct gstplayer_meta *meta) { plugin_lock(&plugin); @@ -111,9 +112,9 @@ static void remove_player(struct gstplayer_meta *meta) { /** * @brief Remove a player instance from the player list. - * + * * Assumes the plugin struct is locked. - * + * */ static void remove_player_locked(struct gstplayer_meta *meta) { ASSERT_MUTEX_LOCKED(plugin.lock); @@ -315,7 +316,7 @@ static enum listener_return on_video_info_notify(void *arg, void *userdata) { /// on_video_info_notify is called on an internal thread, /// but send_initialized_event is (should be) mt-safe send_initialized_event(meta, !info->can_seek, info->width, info->height, info->duration_ms); - + /// FIXME: Threading /// Set this to NULL here so we don't unlisten to it twice. meta->video_info_listener = NULL; @@ -1146,7 +1147,7 @@ static int on_create_v2(const struct raw_std_value *arg, FlutterPlatformMessageR if (headers == NULL) { headers = gst_structure_new_empty("http-headers"); } - + char *key_str = raw_std_string_dup(key); gst_structure_take_string(headers, key_str, raw_std_string_dup(value)); free(key_str); From efab797562d68d4dfda14f6c640b1bbea821eb77 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Mon, 12 May 2025 23:26:25 +0200 Subject: [PATCH 14/41] modesetting: remove logs if a separate cursor plane couldn't be found Did cause a message to be logged on every atomic commit. --- src/modesetting.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/modesetting.c b/src/modesetting.c index 722a71ea..892c2973 100644 --- a/src/modesetting.c +++ b/src/modesetting.c @@ -2448,9 +2448,6 @@ int kms_req_builder_push_fb_layer( /* id_range */ false, 0 // clang-format on ); - if (plane == NULL) { - LOG_DEBUG("Couldn't find a fitting cursor plane.\n"); - } } /// TODO: Not sure we can use crtc_x, crtc_y, etc with primary planes From 436f790f877a546461da6973efb70d16404118d5 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Mon, 12 May 2025 23:28:56 +0200 Subject: [PATCH 15/41] cmake: remove deleted `audioplayers/player.c` file --- CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cb42b54e..8fa47818 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -363,7 +363,6 @@ if (BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN OR BUILD_GSTREAMER_AUDIO_PLAYER_PLUGIN)# if (BUILD_GSTREAMER_AUDIO_PLAYER_PLUGIN AND LIBGSTREAMER_FOUND) target_sources(flutterpi_module PRIVATE src/plugins/audioplayers/plugin.c - src/plugins/audioplayers/player.c ) endif() endif() From c7a74932a76ac2ac718beca378c0341a6e2aaf74 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Tue, 13 May 2025 14:01:25 +0200 Subject: [PATCH 16/41] Revert "video player: disable hw decoding for gstreamer < 1.22.8" On Raspberry Pi, the fix is now backported to 1.22: https://github.com/RPi-Distro/repo/issues/391 This reverts commit dc0ff2ae852e2eb1f4a4c08e37bd0fbd12d2e537. --- src/plugins/gstplayer.c | 34 ++++------------------------------ 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/src/plugins/gstplayer.c b/src/plugins/gstplayer.c index 3981a672..6a0894d5 100644 --- a/src/plugins/gstplayer.c +++ b/src/plugins/gstplayer.c @@ -735,19 +735,9 @@ void on_source_setup(GstElement *playbin, GstElement *source, gpointer userdata) * See: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/blob/main/subprojects/gst-plugins-base/gst/playback/gstplay-enum.h */ typedef enum { - GST_PLAY_FLAG_VIDEO = (1 << 0), - GST_PLAY_FLAG_AUDIO = (1 << 1), - GST_PLAY_FLAG_TEXT = (1 << 2), - GST_PLAY_FLAG_VIS = (1 << 3), - GST_PLAY_FLAG_SOFT_VOLUME = (1 << 4), - GST_PLAY_FLAG_NATIVE_AUDIO = (1 << 5), - GST_PLAY_FLAG_NATIVE_VIDEO = (1 << 6), - GST_PLAY_FLAG_DOWNLOAD = (1 << 7), - GST_PLAY_FLAG_BUFFERING = (1 << 8), - GST_PLAY_FLAG_DEINTERLACE = (1 << 9), - GST_PLAY_FLAG_SOFT_COLORBALANCE = (1 << 10), - GST_PLAY_FLAG_FORCE_FILTERS = (1 << 11), - GST_PLAY_FLAG_FORCE_SW_DECODERS = (1 << 12), + GST_PLAY_FLAG_VIDEO = (1 << 0), + GST_PLAY_FLAG_AUDIO = (1 << 1), + GST_PLAY_FLAG_TEXT = (1 << 2) } GstPlayFlags; UNUSED static void on_element_setup(GstElement *playbin, GstElement *element, gpointer userdata) { @@ -859,13 +849,8 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo change_notifier_init(&p->error_notifier); change_notifier_init(&p->eos_notifier); - // playbin3 doesn't let use disable hardware decoders, - // which we need to do for gstreamer < 1.22.8. -#if THIS_GSTREAMER_VER < GSTREAMER_VER(1, 22, 8) - p->playbin = gst_element_factory_make("playbin", "playbin"); -#else + /// TODO: Use playbin or playbin3? p->playbin = gst_element_factory_make("playbin3", "playbin"); -#endif if (p->playbin == NULL) { LOG_PLAYER_ERROR(p, "Couldn't create playbin instance.\n"); goto fail_free_p; @@ -893,17 +878,6 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo flags &= ~GST_PLAY_FLAG_TEXT; } - // Gstreamer older than 1.22.8 has a buffer management issue when seeking. - // https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/4465 - // - // This is a bit more coarse than necessary; we technically only - // need to disable the v4l2 decoders. -#if THIS_GSTREAMER_VER < GSTREAMER_VER(1, 22, 8) - flags |= GST_PLAY_FLAG_FORCE_SW_DECODERS; -#else - flags &= ~GST_PLAY_FLAG_FORCE_SW_DECODERS; -#endif - g_object_set(p->playbin, "flags", flags, NULL); if (play_video) { From 815ac825f9d9c0ed107849f6f774d2e0906cd819 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Tue, 13 May 2025 16:45:18 +0200 Subject: [PATCH 17/41] gstplayer: don't use gst_video_info_new_from_caps Was added in gstreamer 1.20, debian bullseye is still on 1.18. --- src/plugins/gstplayer.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/plugins/gstplayer.c b/src/plugins/gstplayer.c index 6a0894d5..caa63da1 100644 --- a/src/plugins/gstplayer.c +++ b/src/plugins/gstplayer.c @@ -26,6 +26,7 @@ #include "plugins/gstplayer.h" #include "texture_registry.h" #include "util/logging.h" +#include "util/macros.h" #include "config.h" @@ -811,8 +812,10 @@ UNUSED static GstPadProbeReturn on_video_sink_event(GstPad *pad, GstPadProbeInfo return GST_PAD_PROBE_OK; } - GstVideoInfo *videoinfo = gst_video_info_new_from_caps(caps); - if (!videoinfo) { + GstVideoInfo *videoinfo = gst_video_info_new(); + ASSUME(videoinfo != NULL); + + if (!gst_video_info_from_caps(videoinfo, caps)) { LOG_ERROR("Could not determine video properties of caps event.\n"); return GST_PAD_PROBE_OK; } From facc7fc25b87a426c171b2cb07bd608fb4083685 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Tue, 13 May 2025 16:53:36 +0200 Subject: [PATCH 18/41] gstplayer: only define appsink event handler for gstreamer >= 1.20.0 Uses gstreamer 1.20.0-only API and is only used for 1.20.0 anyway. --- src/plugins/gstreamer_video_player/flutter_texture_sink.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/gstreamer_video_player/flutter_texture_sink.c b/src/plugins/gstreamer_video_player/flutter_texture_sink.c index c4cd6e5e..8730e671 100644 --- a/src/plugins/gstreamer_video_player/flutter_texture_sink.c +++ b/src/plugins/gstreamer_video_player/flutter_texture_sink.c @@ -157,6 +157,7 @@ static GstCaps *caps_for_frame_interface(struct frame_interface *interface) { return caps; } +#if THIS_GSTREAMER_VER >= GSTREAMER_VER(1, 20, 0) static gboolean on_appsink_new_event(GstAppSink *appsink, gpointer userdata) { (void) userdata; @@ -183,6 +184,7 @@ static gboolean on_appsink_new_event(GstAppSink *appsink, gpointer userdata) { return FALSE; } +#endif UNUSED static gboolean on_appsink_propose_allocation(GstAppSink *appsink, GstQuery *query, gpointer userdata) { (void) appsink; From 5e723463a691a62f1773ebc46562a61cbec5e75b Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Tue, 13 May 2025 17:01:48 +0200 Subject: [PATCH 19/41] gstplayer: don't use sd_event_source_disable_unref --- src/plugins/gstplayer.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/gstplayer.c b/src/plugins/gstplayer.c index caa63da1..6abeb2dc 100644 --- a/src/plugins/gstplayer.c +++ b/src/plugins/gstplayer.c @@ -18,7 +18,6 @@ #include #include - #include "flutter-pi.h" #include "notifier_listener.h" #include "platformchannel.h" @@ -971,7 +970,8 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo return p; fail_rm_event_source: - sd_event_source_disable_unref(p->busfd_events); + sd_event_source_set_enabled(p->busfd_events, false); + sd_event_source_unref(p->busfd_events); fail_destroy_texture: UNUSED gst_object_unref(p->playbin); From cd2c8ef16165ecba90fb5d5aaa3356e45f25e600 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Fri, 16 May 2025 12:47:06 +0200 Subject: [PATCH 20/41] notifier/listener: fix change notifiers notifying on listen Change notifiers should only trigger when notifier_notify is called, not on listen. --- src/notifier_listener.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/notifier_listener.c b/src/notifier_listener.c index d8156538..278b9749 100644 --- a/src/notifier_listener.c +++ b/src/notifier_listener.c @@ -112,10 +112,12 @@ struct listener *notifier_listen(struct notifier *notifier, listener_cb_t notify return NULL; } - r = listener_notify(l, notifier->state); - if (r == kUnlisten) { - listener_destroy(l); - return NULL; + if (notifier->is_value_notifier) { + r = listener_notify(l, notifier->state); + if (r == kUnlisten) { + listener_destroy(l); + return NULL; + } } notifier_lock(notifier); From 1584c596b783a3c80a0e2a7b44a97f164723c5b0 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Fri, 16 May 2025 12:58:50 +0200 Subject: [PATCH 21/41] gstplayer/audio player/video player: various compatibility fixes gstplayer: - allow registering callbacks for async seeks/state changes - implement gapless looping using playbin about-to-finish or segments - listen to GST_MESSAGE_EOS instead of appsink eos signal, since appsink is only used as a video sink for video playback. - query duration in GST_MESSAGE_DURATION_CHANGED message audioplayers: - actually send prepared, completed, seek completed and error events - handle incorrect file source uris by audioplayers - fix incorrect non-null terminated string printing gstreamer_video_player: - enable gapless looping by default - allow overriding it in set_looping call --- src/plugins/audioplayers/plugin.c | 205 +++++-- src/plugins/gstplayer.c | 638 ++++++++++++++++---- src/plugins/gstplayer.h | 21 +- src/plugins/gstreamer_video_player/plugin.c | 19 +- 4 files changed, 695 insertions(+), 188 deletions(-) diff --git a/src/plugins/audioplayers/plugin.c b/src/plugins/audioplayers/plugin.c index c68b28d5..16c3d7fe 100644 --- a/src/plugins/audioplayers/plugin.c +++ b/src/plugins/audioplayers/plugin.c @@ -38,6 +38,7 @@ struct audioplayer_meta { struct listener *duration_listener; struct listener *eos_listener; + struct listener *error_listener; }; struct plugin { @@ -47,6 +48,15 @@ struct plugin { khash_t(audioplayers) players; }; +static const char *player_get_id(struct gstplayer *player) { + struct audioplayer_meta *meta = gstplayer_get_userdata(player); + + return meta->id; +} + +#define LOG_AUDIOPLAYER_DEBUG(player, fmtstring, ...) LOG_DEBUG("audio player \"%s\": " fmtstring, player_get_id(player), ##__VA_ARGS__) +#define LOG_AUDIOPLAYER_ERROR(player, fmtstring, ...) LOG_ERROR("audio player \"%s\": " fmtstring, player_get_id(player), ##__VA_ARGS__) + static void on_receive_event_ch(void *userdata, const FlutterPlatformMessage *message); static void respond_plugin_error_ext(const FlutterPlatformMessageResponseHandle *response_handle, const char *message, struct std_value *details) { @@ -136,7 +146,7 @@ static struct gstplayer *get_player_from_arg(struct plugin *plugin, const struct return player; } -UNUSED static void send_error_event(struct audioplayer_meta *meta, GError *error) { +static void send_error_event(struct audioplayer_meta *meta, GError *error) { if (!meta->subscribed) { return; } @@ -170,7 +180,7 @@ UNUSED static void send_error_event(struct audioplayer_meta *meta, GError *error free(details); } -UNUSED static void send_prepared_event(struct audioplayer_meta *meta, bool prepared) { +static void send_prepared_event(struct audioplayer_meta *meta, bool prepared) { if (!meta->subscribed) { return; } @@ -186,11 +196,16 @@ UNUSED static void send_prepared_event(struct audioplayer_meta *meta, bool prepa // clang-format on } -static void send_duration_update(struct audioplayer_meta *meta, int64_t duration_ms) { +static void send_duration_update(struct audioplayer_meta *meta, bool has_duration, int64_t duration_ms) { if (!meta->subscribed) { return; } + if (!has_duration) { + // TODO: Check the behaviour in upstream audioplayers + return; + } + // clang-format off platch_send_success_event_std( meta->event_channel, @@ -202,7 +217,7 @@ static void send_duration_update(struct audioplayer_meta *meta, int64_t duration // clang-format on } -UNUSED static void send_seek_completed(struct audioplayer_meta *meta) { +static void send_seek_completed(struct audioplayer_meta *meta) { if (!meta->subscribed) { return; } @@ -210,9 +225,8 @@ UNUSED static void send_seek_completed(struct audioplayer_meta *meta) { // clang-format off platch_send_success_event_std( meta->event_channel, - &STDMAP2( - STDSTRING("event"), STDSTRING("audio.onDuration"), - STDSTRING("value"), STDBOOL(true) + &STDMAP1( + STDSTRING("event"), STDSTRING("audio.onSeekComplete") ) ); // clang-format on @@ -226,9 +240,8 @@ static void send_playback_complete(struct audioplayer_meta *meta) { // clang-format off platch_send_success_event_std( meta->event_channel, - &STDMAP2( - STDSTRING("event"), STDSTRING("audio.onComplete"), - STDSTRING("value"), STDBOOL(true) + &STDMAP1( + STDSTRING("event"), STDSTRING("audio.onComplete") ) ); // clang-format on @@ -272,6 +285,8 @@ static void on_create(struct plugin *p, const struct raw_std_value *arg, const F return; } + LOG_DEBUG("create(id: \"%s\")\n", meta->id); + int status = 0; khint_t index = kh_put(audioplayers, &p->players, meta->id, &status); if (status == -1) { @@ -301,7 +316,7 @@ static void on_create(struct plugin *p, const struct raw_std_value *arg, const F p->flutterpi, NULL, meta, - /* play_video */ false, /* play_audio */ true, /* subtitles */ false, + /* play_video */ false, /* play_audio */ true, NULL ); if (player == NULL) { @@ -324,6 +339,8 @@ static void on_create(struct plugin *p, const struct raw_std_value *arg, const F ); kh_value(&p->players, index) = player; + + platch_respond_success_std(responsehandle, NULL); } static void on_pause(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { @@ -332,12 +349,11 @@ static void on_pause(struct plugin *p, const struct raw_std_value *arg, const Fl return; } - int err = gstplayer_pause(player); - if (err != 0) { - platch_respond_native_error_std(responsehandle, err); - } else { - platch_respond_success_std(responsehandle, NULL); - } + LOG_AUDIOPLAYER_DEBUG(player, "pause()\n"); + + gstplayer_pause(player); + + platch_respond_success_std(responsehandle, NULL); } static void on_resume(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { @@ -346,13 +362,12 @@ static void on_resume(struct plugin *p, const struct raw_std_value *arg, const F return; } + LOG_AUDIOPLAYER_DEBUG(player, "resume()\n"); + /// TODO: Should resume behave different to play? - int err = gstplayer_play(player); - if (err != 0) { - platch_respond_native_error_std(responsehandle, err); - } else { - platch_respond_success_std(responsehandle, NULL); - } + gstplayer_play(player); + + platch_respond_success_std(responsehandle, NULL); } static void on_stop(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { @@ -361,16 +376,18 @@ static void on_stop(struct plugin *p, const struct raw_std_value *arg, const Flu return; } + LOG_AUDIOPLAYER_DEBUG(player, "stop()\n"); + /// TODO: Maybe provide gstplayer_stop int err = gstplayer_pause(player); if (err != 0) { - platch_respond_native_error_std(responsehandle, err); + platch_respond_success_std(responsehandle, NULL); return; } err = gstplayer_seek_to(player, 0, /* nearest_keyframe */ false); if (err != 0) { - platch_respond_native_error_std(responsehandle, err); + platch_respond_success_std(responsehandle, NULL); return; } @@ -383,7 +400,9 @@ static void on_release(struct plugin *p, const struct raw_std_value *arg, const return; } - gstplayer_release(player); + LOG_AUDIOPLAYER_DEBUG(player, "release()\n"); + + gstplayer_set_source(player, NULL); platch_respond_success_std(responsehandle, NULL); } @@ -402,15 +421,28 @@ static void on_seek(struct plugin *p, const struct raw_std_value *arg, const Flu int64_t position_int = raw_std_value_as_int(position); - int err = gstplayer_seek_to(player, position_int, /* nearest_keyframe */ false); - if (err != 0) { - platch_respond_native_error_std(responsehandle, err); - return; - } + LOG_AUDIOPLAYER_DEBUG(player, "seek(position_ms: %"PRIi64")\n", position_int); + + gstplayer_seek_with_completer( + player, + position_int, + /* nearest_keyframe */ false, + (struct async_completer) { + .on_done = (void_callback_t) send_seek_completed, + .on_error = NULL, + .userdata = gstplayer_get_userdata(player) + } + ); platch_respond_success_std(responsehandle, NULL); } +static void on_set_source_url_complete(void *userdata) { + struct audioplayer_meta *meta = userdata; + + send_prepared_event(meta, true); +} + static void on_set_source_url(struct plugin *p, const struct raw_std_value *arg, const FlutterPlatformMessageResponseHandle *responsehandle) { struct gstplayer *player = get_player_from_arg(p, arg, responsehandle); if (player == NULL) { @@ -436,8 +468,34 @@ static void on_set_source_url(struct plugin *p, const struct raw_std_value *arg, } char *src_url_duped = raw_std_string_dup(src_url); + if (!src_url_duped) return; + + LOG_AUDIOPLAYER_DEBUG(player, "set_source_url(url: \"%s\")\n", src_url_duped); + + // audioplayers attempts to use file paths (e.g. /tmp/abcd) as source URIs. + // detect that and constrcut a proper url from it. + if (src_url_duped[0] == '/') { + free(src_url_duped); + + int result = asprintf( + &src_url_duped, + "file://%.*s", + (int) raw_std_string_get_length(src_url), + raw_std_string_get_nonzero_terminated(src_url) + ); + if (result < 0) { + return; + } + } - bool ok = gstplayer_preroll(player, src_url_duped); + bool ok = gstplayer_set_source_with_completer( + player, + src_url_duped, + (struct async_completer) { + .on_done = on_set_source_url_complete, + .userdata = gstplayer_get_userdata(player) + } + ); free(src_url_duped); @@ -455,9 +513,12 @@ static void on_get_duration(struct plugin *p, const struct raw_std_value *arg, c return; } + LOG_AUDIOPLAYER_DEBUG(player, "get_duration()\n"); + int64_t duration_ms = gstplayer_get_duration(player); if (duration_ms == -1) { platch_respond_success_std(responsehandle, NULL); + return; } platch_respond_success_std(responsehandle, &STDINT64(duration_ms)); @@ -477,11 +538,9 @@ static void on_set_volume(struct plugin *p, const struct raw_std_value *arg, con double volume_float = raw_std_value_as_float64(volume); - int err = gstplayer_set_volume(player, volume_float); - if (err != 0) { - platch_respond_native_error_std(responsehandle, err); - return; - } + LOG_AUDIOPLAYER_DEBUG(player, "set_volume(volume: %f)\n", volume_float); + + gstplayer_set_volume(player, volume_float); platch_respond_success_std(responsehandle, NULL); } @@ -494,7 +553,7 @@ static void on_get_position(struct plugin *p, const struct raw_std_value *arg, c int64_t position = gstplayer_get_position(player); if (position < 0) { - platch_respond_native_error_std(responsehandle, EIO); + platch_respond_success_std(responsehandle, &STDNULL); return; } @@ -515,10 +574,15 @@ static void on_set_playback_rate(struct plugin *p, const struct raw_std_value *a double rate_float = raw_std_value_as_float64(rate); - int err = gstplayer_set_playback_speed(player, rate_float); - if (err != 0) { - platch_respond_native_error_std(responsehandle, err); + LOG_AUDIOPLAYER_DEBUG(player, "set_playback_rate(rate: %f)\n", rate_float); + + if (rate_float < 0.0) { + respond_plugin_error(responsehandle, "Backward playback is not supported.\n"); return; + } else if (rate_float == 0.0) { + gstplayer_pause(player); + } else { + gstplayer_set_playback_speed(player, rate_float); } platch_respond_success_std(responsehandle, NULL); @@ -536,6 +600,11 @@ static void on_set_release_mode(struct plugin *p, const struct raw_std_value *ar return; } + LOG_AUDIOPLAYER_DEBUG(player, "set_release_mode(mode: %.*s)\n", + (int) raw_std_string_get_length(mode), + raw_std_string_get_nonzero_terminated(mode) + ); + bool is_release = false; bool is_loop = false; bool is_stop = false; @@ -555,9 +624,9 @@ static void on_set_release_mode(struct plugin *p, const struct raw_std_value *ar (void) is_release; (void) is_stop; - int err = gstplayer_set_looping(player, is_loop); + int err = gstplayer_set_looping(player, is_loop, false); if (err != 0) { - platch_respond_native_error_std(responsehandle, err); + platch_respond_success_std(responsehandle, NULL); return; } @@ -576,6 +645,11 @@ static void on_set_player_mode(struct plugin *p, const struct raw_std_value *arg return; } + LOG_AUDIOPLAYER_DEBUG(player, "set_player_mode(mode: %.*s)\n", + (int) raw_std_string_get_length(mode), + raw_std_string_get_nonzero_terminated(mode) + ); + bool is_media_player = false; bool is_low_latency = false; @@ -611,6 +685,8 @@ static void on_set_balance(struct plugin *p, const struct raw_std_value *arg, co double balance_float = raw_std_value_as_float64(balance); + LOG_AUDIOPLAYER_DEBUG(player, "set_balance(balance: %f)\n", balance_float); + if (balance_float < -1.0) { balance_float = -1.0; } else if (balance_float > 1.0) { @@ -634,7 +710,7 @@ static void on_player_emit_log(struct plugin *p, const struct raw_std_value *arg return; } - LOG_DEBUG("%*s", (int) raw_std_string_get_length(message), raw_std_string_get_nonzero_terminated(message)); + LOG_DEBUG("%.*s", (int) raw_std_string_get_length(message), raw_std_string_get_nonzero_terminated(message)); platch_respond_success_std(responsehandle, NULL); } @@ -658,7 +734,7 @@ static void on_player_emit_error(struct plugin *p, const struct raw_std_value *a } LOG_ERROR( - "%*s, %*s", + "%.*s, %.*s", (int) raw_std_string_get_length(code), raw_std_string_get_nonzero_terminated(code), (int) raw_std_string_get_length(message), raw_std_string_get_nonzero_terminated(message) ); @@ -677,6 +753,8 @@ static void on_dispose(struct plugin *p, const struct raw_std_value *arg, const return; } + LOG_AUDIOPLAYER_DEBUG(player, "dispose()\n"); + char *id_duped = raw_std_string_dup(id); khint_t index = kh_get(audioplayers, &p->players, id_duped); @@ -778,7 +856,7 @@ static void on_emit_log( return; } - LOG_DEBUG("%*s", (int) raw_std_string_get_length(message), raw_std_string_get_nonzero_terminated(message)); + LOG_DEBUG("%.*s", (int) raw_std_string_get_length(message), raw_std_string_get_nonzero_terminated(message)); platch_respond_success_std(responsehandle, NULL); } @@ -803,7 +881,7 @@ static void on_emit_error( } LOG_ERROR( - "%*s, %*s", + "%.*s, %.*s", (int) raw_std_string_get_length(code), raw_std_string_get_nonzero_terminated(code), (int) raw_std_string_get_length(message), raw_std_string_get_nonzero_terminated(message) ); @@ -843,10 +921,13 @@ static enum listener_return on_duration_notify(void *arg, void *userdata) { struct audioplayer_meta *meta = gstplayer_get_userdata(player); ASSERT_NOT_NULL(meta); - ASSERT_NOT_NULL(arg); - int64_t *duration_ms = arg; + if (arg != NULL) { + int64_t *duration_ms = arg; + send_duration_update(meta, true, *duration_ms); + } else { + send_duration_update(meta, false, -1); + } - send_duration_update(meta, *duration_ms); return kNoAction; } @@ -864,6 +945,21 @@ static enum listener_return on_eos_notify(void *arg, void *userdata) { return kNoAction; } +static enum listener_return on_error_notify(void *arg, void *userdata) { + ASSERT_NOT_NULL(arg); + GError *error = arg; + + ASSERT_NOT_NULL(userdata); + struct gstplayer *player = userdata; + + struct audioplayer_meta *meta = gstplayer_get_userdata(player); + ASSERT_NOT_NULL(meta); + + send_error_event(meta, error); + + return kNoAction; +} + static void on_receive_event_ch(void *userdata, const FlutterPlatformMessage *message) { ASSERT_NOT_NULL(userdata); @@ -879,7 +975,7 @@ static void on_receive_event_ch(void *userdata, const FlutterPlatformMessage *me } /// TODO: Implement - if (raw_std_method_call_is_method(envelope, "listen") == 0) { + if (raw_std_method_call_is_method(envelope, "listen")) { platch_respond_success_std(message->response_handle, NULL); if (!meta->subscribed) { @@ -887,15 +983,17 @@ static void on_receive_event_ch(void *userdata, const FlutterPlatformMessage *me meta->duration_listener = notifier_listen(gstplayer_get_duration_notifier(player), on_duration_notify, NULL, player); meta->eos_listener = notifier_listen(gstplayer_get_eos_notifier(player), on_eos_notify, NULL, player); + meta->error_listener = notifier_listen(gstplayer_get_error_notifier(player), on_error_notify, NULL, player); } - } else if (raw_std_method_call_is_method(envelope, "cancel") == 0) { + } else if (raw_std_method_call_is_method(envelope, "cancel")) { platch_respond_success_std(message->response_handle, NULL); if (meta->subscribed) { meta->subscribed = false; - notifier_unlisten(gstplayer_get_duration_notifier(player), meta->duration_listener); + notifier_unlisten(gstplayer_get_eos_notifier(player), meta->error_listener); notifier_unlisten(gstplayer_get_eos_notifier(player), meta->eos_listener); + notifier_unlisten(gstplayer_get_duration_notifier(player), meta->duration_listener); } } else { platch_respond_not_implemented(message->response_handle); @@ -912,6 +1010,7 @@ enum plugin_init_result audioplayers_plugin_init(struct flutterpi *flutterpi, vo return PLUGIN_INIT_RESULT_ERROR; } + plugin->flutterpi = flutterpi; plugin->initialized = false; ok = plugin_registry_set_receiver_v2_locked( diff --git a/src/plugins/gstplayer.c b/src/plugins/gstplayer.c index 6abeb2dc..ae792922 100644 --- a/src/plugins/gstplayer.c +++ b/src/plugins/gstplayer.c @@ -17,6 +17,9 @@ #include #include #include +#include +#include +#include #include "flutter-pi.h" #include "notifier_listener.h" @@ -24,8 +27,11 @@ #include "pluginregistry.h" #include "plugins/gstplayer.h" #include "texture_registry.h" +#include "tracer.h" #include "util/logging.h" #include "util/macros.h" +#include "util/collection.h" +#include "util/asserts.h" #include "config.h" @@ -33,17 +39,6 @@ #include "gstreamer_video_player.h" #endif -#define LOG_GST_SET_STATE_ERROR(_element) \ - LOG_ERROR( \ - "setting gstreamer playback state failed. gst_element_set_state(element name: %s): GST_STATE_CHANGE_FAILURE\n", \ - GST_ELEMENT_NAME(_element) \ - ) -#define LOG_GST_GET_STATE_ERROR(_element) \ - LOG_ERROR( \ - "last gstreamer state change failed. gst_element_get_state(element name: %s): GST_STATE_CHANGE_FAILURE\n", \ - GST_ELEMENT_NAME(_element) \ - ) - #define LOG_PLAYER_DEBUG(player, fmtstring, ...) LOG_DEBUG("gstplayer-%"PRIi64": " fmtstring, player->debug_id, ##__VA_ARGS__) #ifdef DEBUG #define LOG_PLAYER_ERROR(player, fmtstring, ...) LOG_ERROR("gstplayer-%"PRIi64": " fmtstring, player->debug_id, ##__VA_ARGS__) @@ -51,6 +46,20 @@ #define LOG_PLAYER_ERROR(player, fmtstring, ...) LOG_ERROR(fmtstring, ##__VA_ARGS__) #endif +#define LOG_GST_SET_STATE_ERROR(player, _element) \ + LOG_PLAYER_ERROR( \ + player, \ + "setting gstreamer playback state failed. gst_element_set_state(element name: %s): GST_STATE_CHANGE_FAILURE\n", \ + GST_ELEMENT_NAME(_element) \ + ) + +#define LOG_GST_GET_STATE_ERROR(player, _element) \ + LOG_PLAYER_ERROR( \ + player, \ + "last gstreamer state change failed. gst_element_get_state(element name: %s): GST_STATE_CHANGE_FAILURE\n", \ + GST_ELEMENT_NAME(_element) \ + ) + struct incomplete_video_info { bool has_resolution; bool has_fps; @@ -83,7 +92,6 @@ struct gstplayer { #endif struct flutterpi *flutterpi; - struct tracer *tracer; void *userdata; @@ -103,7 +111,15 @@ struct gstplayer { * @brief True if the video should seemlessly start from the beginning once the end is reached. * */ - atomic_bool looping; + bool looping; + + /** + * @brief True if the looping should use gapless looping using either the about-to-finish callback + * from playbin or segments. + * + * Configured in gstplayer_set_looping + */ + bool gapless_looping; /** * @brief The desired playback state. Either paused, playing, or single-frame stepping. @@ -161,23 +177,114 @@ struct gstplayer { bool has_seeking_info; struct seeking_info seeking_info; - // bool has_gst_info; - // GstVideoInfo gst_info; - + /** + * The flutter texture that this video player is pushing frames to. + */ struct texture *texture; - // GstElement *pipeline, *sink; - // GstBus *bus; sd_event_source *busfd_events; + /** + * The gstreamer playbin, which gstplayer is just + * a wrapper around, basically. + */ GstElement *playbin; - GstElement *audiopanorama; - gulong on_element_added_signal_handler; + /** + * The gstreamer audiopanorama element, used as the "audio-filter" + * if audio playback is enabled, and used to change the audio + * left/right balance. + */ + GstElement *audiopanorama; + /** + * True if we're playing back a live source, + * e.g. a live stream + */ bool is_live; + + /** + * Callbacks to be called on ASYNC_DONE gstreamer messages. + * + * ASYNC_DONE messages indicate completion of an async state + * change or a flushing seek. + */ + size_t n_async_completers; + struct async_completer completers[8]; + + /** + * @brief Use the playbin "uri" property and "about-to-finish" signal + * to achieve gapless looping, if looping is desired. + * + * It's a bit unclear whether this is worse or equally as good as + * using segments; so segment looping is preferred for now. + * + * However, segments are not always super reliable (e.g. playbin3 + * segment looping is broken in gstreamer < 1.22.9), so the playbin + * method is kept intact still as a backup. + */ + bool playbin_gapless; + + /** + * @brief Use segments to do gapless looping, if looping is desired. + * + * (Instead of e.g. seeking back to start on EOS, or setting the + * playbin uri property in about-to-finish) + */ + bool segment_gapless; + + /** + * The source uri this gstplayer should play back. + * + * Mostly used to as the argument to `g_object_set(p->playbin, "uri", ...)` + * in on_about_to_finish, as querying the current source uri from the playbin + * is not always reliable. + */ + char *uri; + + /** + * True if we did already issue a flushing seek + * with GST_SEEK_FLAG_SEGMENT. + * + * A flushing seek with GST_SEEK_FLAG_SEGMENT has to be + * issued to start gapless looping. + */ + bool did_configure_segment; }; +static struct async_completer pop_completer(struct gstplayer *player) { + ASSERT(player->n_async_completers > 0); + + struct async_completer completer = player->completers[0]; + + player->n_async_completers--; + if (player->n_async_completers > 0) { + memmove(player->completers + 0, player->completers + 1, player->n_async_completers * sizeof(struct async_completer)); + } + + return completer; +} + +static void on_async_done_message(struct gstplayer *player) { + if (player->n_async_completers > 0) { + struct async_completer completer = pop_completer(player); + + if (completer.on_done) { + completer.on_done(completer.userdata); + } + } +} + +static void on_async_error(struct gstplayer *player, GError *error) { + if (player->n_async_completers > 0) { + struct async_completer completer = pop_completer(player); + + if (completer.on_error) { + completer.on_error(completer.userdata, error); + } + } +} + static int maybe_send_video_info(struct gstplayer *player) { struct video_info *duped; @@ -327,6 +434,22 @@ static int apply_playback_state(struct gstplayer *player) { double desired_rate; int64_t position; + ok = gst_element_get_state(player->playbin, ¤t_state, &pending_state, 0); + if (ok == GST_STATE_CHANGE_FAILURE) { + LOG_PLAYER_DEBUG( + player, + "last gstreamer pipeline state change failed. gst_element_get_state(element name: %s): GST_STATE_CHANGE_FAILURE\n", + GST_ELEMENT_NAME(player->playbin) + ); + return EIO; + } + + if (current_state == GST_STATE_NULL) { + // We don't have a playback source right now. + // Don't do anything. + return 0; + } + desired_state = player->playpause_state == kPlaying ? GST_STATE_PLAYING : GST_STATE_PAUSED; /* use GST_STATE_PAUSED if we're stepping */ /// Use 1.0 if we're stepping, otherwise use the stored playback rate for the current direction. @@ -336,7 +459,8 @@ static int apply_playback_state(struct gstplayer *player) { desired_rate = player->direction == kForward ? player->playback_rate_forward : player->playback_rate_backward; } - if (player->current_playback_rate != desired_rate || player->has_desired_position) { + bool is_segment_looping = player->looping && player->gapless_looping && player->segment_gapless; + if (player->current_playback_rate != desired_rate || player->has_desired_position || (player->did_configure_segment != is_segment_looping)) { if (player->has_desired_position) { position = player->desired_position_ms * GST_MSECOND; } else { @@ -348,6 +472,14 @@ static int apply_playback_state(struct gstplayer *player) { } GstSeekFlags seek_flags = GST_SEEK_FLAG_FLUSH; + + // Only configure segment looping if we actually + // are segment looping, because it will + // swallow the end-of-stream events apparently. + if (is_segment_looping) { + seek_flags |= GST_SEEK_FLAG_SEGMENT; + } + if (player->do_fast_seeking) { seek_flags |= GST_SEEK_FLAG_KEY_UNIT | GST_SEEK_FLAG_SNAP_NEAREST; } else { @@ -368,10 +500,8 @@ static int apply_playback_state(struct gstplayer *player) { desired_rate, GST_FORMAT_TIME, seek_flags, - GST_SEEK_TYPE_SET, - position, - GST_SEEK_TYPE_SET, - GST_CLOCK_TIME_NONE + GST_SEEK_TYPE_SET, position, + GST_SEEK_TYPE_SET, GST_CLOCK_TIME_NONE ); if (ok == FALSE) { LOG_PLAYER_ERROR( @@ -395,10 +525,8 @@ static int apply_playback_state(struct gstplayer *player) { desired_rate, GST_FORMAT_TIME, seek_flags, - GST_SEEK_TYPE_SET, - 0, - GST_SEEK_TYPE_SET, - position + GST_SEEK_TYPE_SET, 0, + GST_SEEK_TYPE_SET, position ); if (ok == FALSE) { @@ -415,16 +543,7 @@ static int apply_playback_state(struct gstplayer *player) { player->current_playback_rate = desired_rate; player->fallback_position_ms = GST_TIME_AS_MSECONDS(position); player->has_desired_position = false; - } - - ok = gst_element_get_state(player->playbin, ¤t_state, &pending_state, 0); - if (ok == GST_STATE_CHANGE_FAILURE) { - LOG_PLAYER_DEBUG( - player, - "last gstreamer pipeline state change failed. gst_element_get_state(element name: %s): GST_STATE_CHANGE_FAILURE\n", - GST_ELEMENT_NAME(player->playbin) - ); - return EIO; + player->did_configure_segment = is_segment_looping; } if (pending_state == GST_STATE_VOID_PENDING) { @@ -449,7 +568,7 @@ static int apply_playback_state(struct gstplayer *player) { ok = gst_element_set_state(player->playbin, desired_state); if (ok == GST_STATE_CHANGE_FAILURE) { - LOG_GST_SET_STATE_ERROR(player->playbin); + LOG_GST_SET_STATE_ERROR(player, player->playbin); return EIO; } } else if (pending_state != desired_state) { @@ -465,13 +584,29 @@ static int apply_playback_state(struct gstplayer *player) { ok = gst_element_set_state(player->playbin, desired_state); if (ok == GST_STATE_CHANGE_FAILURE) { - LOG_GST_SET_STATE_ERROR(player->playbin); + LOG_GST_SET_STATE_ERROR(player, player->playbin); return EIO; } } return 0; } +static void on_eos_message(struct gstplayer *player, GstMessage *msg) { + if (GST_MESSAGE_SRC(msg) == GST_OBJECT(player->playbin)) { + if (player->looping) { + LOG_PLAYER_DEBUG(player, "playbin end of stream, seeking back to start (flushing)\n"); + player->desired_position_ms = 0; + player->has_desired_position = true; + apply_playback_state(player); + } else { + LOG_PLAYER_DEBUG(player, "playbin end of stream\n"); + notifier_notify(&player->eos_notifier, NULL); + } + } else { + LOG_PLAYER_DEBUG(player, "end of stream for element: %s\n", GST_MESSAGE_SRC_NAME(msg)); + } +} + static void on_gstreamer_error_message(struct gstplayer *player, GstMessage *msg) { (void) player; @@ -489,6 +624,8 @@ static void on_gstreamer_error_message(struct gstplayer *player, GstMessage *msg debug_info ); + on_async_error(player, error); + notifier_notify(&player->error_notifier, error); g_clear_error(&error); @@ -534,20 +671,22 @@ static void on_buffering_message(struct gstplayer *player, GstMessage *msg) { gst_message_parse_buffering(msg, &percent); gst_message_parse_buffering_stats(msg, &mode, &avg_in, &avg_out, &buffering_left); - LOG_PLAYER_DEBUG( - player, - "buffering, src: %s, percent: %d, mode: %s, avg in: %d B/s, avg out: %d B/s, %" GST_TIME_FORMAT "\n", - GST_MESSAGE_SRC_NAME(msg), - percent, - mode == GST_BUFFERING_STREAM ? "stream" : - mode == GST_BUFFERING_DOWNLOAD ? "download" : - mode == GST_BUFFERING_TIMESHIFT ? "timeshift" : - mode == GST_BUFFERING_LIVE ? "live" : - "?", - avg_in, - avg_out, - GST_TIME_ARGS(buffering_left * GST_MSECOND) - ); + if (percent == 0 || percent == 100) { + LOG_PLAYER_DEBUG( + player, + "buffering, src: %s, percent: %d, mode: %s, avg in: %d B/s, avg out: %d B/s, %" GST_TIME_FORMAT "\n", + GST_MESSAGE_SRC_NAME(msg), + percent, + mode == GST_BUFFERING_STREAM ? "stream" : + mode == GST_BUFFERING_DOWNLOAD ? "download" : + mode == GST_BUFFERING_TIMESHIFT ? "timeshift" : + mode == GST_BUFFERING_LIVE ? "live" : + "?", + avg_in, + avg_out, + GST_TIME_ARGS(buffering_left * GST_MSECOND) + ); + } /// TODO: GST_MESSAGE_BUFFERING is only emitted when we actually need to wait on some buffering till we can resume the playback. /// However, the info we send to the callback also contains information on the buffered video ranges. @@ -555,7 +694,7 @@ static void on_buffering_message(struct gstplayer *player, GstMessage *msg) { update_buffering_state(player, GST_MESSAGE_SRC(msg)); } -static void on_state_change_message(struct gstplayer *player, GstMessage *msg) { +static void on_state_changed_message(struct gstplayer *player, GstMessage *msg) { GstState old, current, pending; gst_message_parse_state_changed(msg, &old, ¤t, &pending); @@ -563,14 +702,25 @@ static void on_state_change_message(struct gstplayer *player, GstMessage *msg) { if (GST_MESSAGE_SRC(msg) == GST_OBJECT(player->playbin)) { LOG_PLAYER_DEBUG( player, - "playbin state changed: src: %s, old: %s, current: %s, pending: %s\n", - GST_MESSAGE_SRC_NAME(msg), + "playbin state changed: old: %s, current: %s, pending: %s\n", gst_element_state_get_name(old), gst_element_state_get_name(current), gst_element_state_get_name(pending) ); - if (current == GST_STATE_PAUSED || current == GST_STATE_PLAYING) { + if (current == GST_STATE_READY || current == GST_STATE_NULL) { + if (player->has_duration) { + player->has_duration = false; + notifier_notify(&player->duration_notifier, NULL); + } + + player->info.has_duration = false; + + player->has_seeking_info = false; + player->info.has_seeking_info = false; + + player->did_configure_segment = false; + } else if ((current == GST_STATE_PAUSED || current == GST_STATE_PLAYING) && (old == GST_STATE_READY || old == GST_STATE_NULL)) { // it's our pipeline that changed to either playing / paused, and we don't have info about our video duration yet. // get that info now. // technically we can already fetch the duration when the decodebin changed to PAUSED state. @@ -600,25 +750,86 @@ static void on_state_change_message(struct gstplayer *player, GstMessage *msg) { } } -static void on_application_message(struct gstplayer *player, GstMessage *msg) { - if (gst_message_has_name(msg, "appsink-eos")) { - if (player->looping) { - // we have an appsink end of stream event - // and we should be looping, so seek back to start - LOG_PLAYER_DEBUG(player, "appsink eos, seeking back to segment start (flushing)\n"); - gst_element_seek( - GST_ELEMENT(player->playbin), - player->current_playback_rate, - GST_FORMAT_TIME, - GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE, - GST_SEEK_TYPE_SET, - 0, - GST_SEEK_TYPE_SET, - GST_CLOCK_TIME_NONE - ); +static void on_segment_start_message(struct gstplayer *player, GstMessage *msg) { + GstFormat format; + gint64 position; + gst_message_parse_segment_start(msg, &format, &position); - apply_playback_state(player); + if (format == GST_FORMAT_TIME) { + LOG_PLAYER_DEBUG( + player, + "segment start. src: %s, position: %" GST_TIME_FORMAT "\n", + GST_MESSAGE_SRC_NAME(msg), + GST_TIME_ARGS(position) + ); + } else { + LOG_PLAYER_DEBUG( + player, + "segment start. src: %s, position: %" PRId64 " (%s)\n", + GST_MESSAGE_SRC_NAME(msg), + position, + gst_format_get_name(format) + ); + } +} + +static void on_segment_done_message(struct gstplayer *player, GstMessage *msg) { + (void) msg; + + if (player->looping && player->gapless_looping && player->segment_gapless) { + LOG_PLAYER_DEBUG(player, "Segment done. Seeking back to segment start (segment, non-flushing)\n"); + gboolean ok = gst_element_seek( + player->playbin, + player->current_playback_rate, + GST_FORMAT_TIME, + GST_SEEK_FLAG_SEGMENT, + GST_SEEK_TYPE_SET, 0, + GST_SEEK_TYPE_SET, GST_CLOCK_TIME_NONE + ); + if (!ok) { + LOG_PLAYER_DEBUG(player, "Could not seek back to segment start.\n"); } + } +} + +static void on_duration_changed_message(struct gstplayer *player, GstMessage *msg) { + (void) msg; + + if (!player->has_duration) { + fetch_duration(player); + + if (player->has_duration) { + int64_t *duped = memdup(&player->duration, sizeof(int64_t)); + + notifier_notify(&player->duration_notifier, duped); + } + } + + if (!player->has_seeking_info) { + fetch_seeking(player); + + if (player->has_seeking_info) { + struct seeking_info *duped = memdup(&player->seeking_info, sizeof(struct seeking_info)); + + notifier_notify(&player->seeking_info_notifier, duped); + } + } + + maybe_send_video_info(player); +} + +static void on_about_to_finish_message(struct gstplayer *player) { + if (player->looping && player->uri && player->playbin_gapless) { + LOG_PLAYER_DEBUG(player, "Got about-to-finish signal, configuring next playback item\n"); + g_object_set(player->playbin, "uri", player->uri, NULL); + } else { + LOG_PLAYER_DEBUG(player, "Got about-to-finish signal\n"); + } +} + +static void on_application_message(struct gstplayer *player, GstMessage *msg) { + if (gst_message_has_name(msg, "appsink-eos")) { + // unhandled } else if (gst_message_has_name(msg, "video-info")) { const GstStructure *structure = gst_message_get_structure(msg); @@ -637,12 +848,22 @@ static void on_application_message(struct gstplayer *player, GstMessage *msg) { LOG_PLAYER_DEBUG(player, "Determined resolution: %d x %d and framerate: %f\n", player->info.info.width, player->info.info.height, player->info.info.fps); } else if (gst_message_has_name(msg, "about-to-finish")) { - LOG_PLAYER_DEBUG(player, "Got about-to-finish signal\n"); + on_about_to_finish_message(player); } } +static void start_async(struct gstplayer *player, struct async_completer completer) { + ASSERT(player->n_async_completers < ARRAY_SIZE(player->completers)); + + player->completers[player->n_async_completers++] = completer; +} + static void on_bus_message(struct gstplayer *player, GstMessage *msg) { switch (GST_MESSAGE_TYPE(msg)) { + case GST_MESSAGE_EOS: + on_eos_message(player, msg); + break; + case GST_MESSAGE_ERROR: on_gstreamer_error_message(player, msg); break; @@ -655,49 +876,115 @@ static void on_bus_message(struct gstplayer *player, GstMessage *msg) { on_gstreamer_info_message(player, msg); break; + case GST_MESSAGE_TAG: { + if (0) { + GstTagList *tags; + gst_message_parse_tag(msg, &tags); + + char *str = gst_tag_list_to_string(tags); + + LOG_PLAYER_DEBUG(player, "%s found tags: %s\n", GST_MESSAGE_SRC_NAME(msg), str); + + free(str); + } + break; + } + case GST_MESSAGE_BUFFERING: on_buffering_message(player, msg); break; case GST_MESSAGE_STATE_CHANGED: - on_state_change_message(player, msg); + on_state_changed_message(player, msg); break; - case GST_MESSAGE_ASYNC_DONE: break; + case GST_MESSAGE_APPLICATION: + on_application_message(player, msg); + break; + + case GST_MESSAGE_SEGMENT_START: + on_segment_start_message(player, msg); + break; + + case GST_MESSAGE_SEGMENT_DONE: + on_segment_done_message(player, msg); + break; + + case GST_MESSAGE_DURATION_CHANGED: + on_duration_changed_message(player, msg); + break; case GST_MESSAGE_LATENCY: - LOG_PLAYER_DEBUG(player, "gstreamer: redistributing latency\n"); + LOG_PLAYER_DEBUG(player, "redistributing latency\n"); gst_bin_recalculate_latency(GST_BIN(player->playbin)); break; - case GST_MESSAGE_EOS: - LOG_PLAYER_DEBUG(player, "end of stream, src: %s\n", GST_MESSAGE_SRC_NAME(msg)); - - if (GST_MESSAGE_SRC(msg) == GST_OBJECT(player->playbin)) { - notifier_notify(&player->eos_notifier, NULL); - } + case GST_MESSAGE_ASYNC_DONE: + on_async_done_message(player); break; case GST_MESSAGE_REQUEST_STATE: { GstState requested; gst_message_parse_request_state(msg, &requested); - LOG_PLAYER_DEBUG( - player, - "gstreamer state change to %s was requested by %s\n", - gst_element_state_get_name(requested), - GST_MESSAGE_SRC_NAME(msg) - ); gst_element_set_state(GST_ELEMENT(player->playbin), requested); break; } - case GST_MESSAGE_APPLICATION: - on_application_message(player, msg); + case GST_MESSAGE_QOS: { + if (0) { + gboolean live = false; + uint64_t running_time = 0; + uint64_t stream_time = 0; + uint64_t timestamp = 0; + uint64_t duration = 0; + + GstFormat format = GST_FORMAT_DEFAULT; + uint64_t processed = 0; + uint64_t dropped = 0; + + int64_t jitter = 0; + double proportion = 1.0; + int quality = 0; + + gst_message_parse_qos(msg, &live, &running_time, &stream_time, ×tamp, &duration); + gst_message_parse_qos_stats(msg, &format, &processed, &dropped); + gst_message_parse_qos_values(msg, &jitter, &proportion, &quality); + + LOG_PLAYER_DEBUG( + player, + "Quality of Service: %s\n" + " live: %s\n" + " running time: %" GST_TIME_FORMAT "\n" + " stream time: %" GST_TIME_FORMAT "\n" + " timestamp: %" GST_TIME_FORMAT "\n" + " duration: %" GST_TIME_FORMAT "\n" + " processed: %" PRIu64 " (%s)\n" + " dropped: %" PRIu64 " (%s)\n" + " jitter: %" PRId64 "\n" + " proportion: %f\n" + " quality: %d\n", + GST_MESSAGE_SRC_NAME(msg), + live ? "yes" : "no", + GST_TIME_ARGS(running_time), + GST_TIME_ARGS(stream_time), + GST_TIME_ARGS(timestamp), + GST_TIME_ARGS(duration), + processed, gst_format_get_name(format), + dropped, gst_format_get_name(format), + jitter, + proportion, + quality + ); + } break; + } default: - LOG_PLAYER_DEBUG(player, "gstreamer message: %s, src: %s\n", GST_MESSAGE_TYPE_NAME(msg), GST_MESSAGE_SRC_NAME(msg)); + if (0) { + LOG_PLAYER_DEBUG(player, "gstreamer message: %s, src: %s\n", GST_MESSAGE_TYPE_NAME(msg), GST_MESSAGE_SRC_NAME(msg)); + } + break; } return; @@ -833,7 +1120,9 @@ UNUSED static GstPadProbeReturn on_video_sink_event(GstPad *pad, GstPadProbeInfo return GST_PAD_PROBE_REMOVE; } -struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, void *userdata, bool play_video, bool play_audio, bool subtitles, GstStructure *headers) { +struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, void *userdata, bool play_video, bool play_audio, GstStructure *headers) { + ASSERT_NOT_NULL(flutterpi); + struct gstplayer *p = calloc(1, sizeof(struct gstplayer)); if (p == NULL) { return NULL; @@ -843,6 +1132,38 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo p->debug_id = allocate_id(); #endif p->userdata = userdata; + p->current_playback_rate = 1.0; + p->playback_rate_forward = 1.0; + p->playback_rate_backward = 1.0; + + // Gapless looping is configured in the gstplayer_set_looping call. + // + // Without gapless looping, we'll just seek back to start on EOS, + // which always works. + p->gapless_looping = false; + + // Gapless looping using playbin "about-to-finish" is unreliable + // in audio playback. + // + // E.g., using the audioplayers example and looping the first ("coin") + // sound, switching to the second sound will first play the second sound, + // then play part of the first sound at higher pitch, and then loop the + // second sound. + // + // Also, it seems like the playbin recreates all the elements & decoders, + // so it's not super resource-saving either. + p->playbin_gapless = false; + + // Segment gapless looping works mostly fine, but is also + // not completely reliable. + // + // E.g., looping the second ("laser") sound of the audioplayers + // example will play back 1-2 seconds of noise after + // the laser sound, then play the laser sound, then noise, etc. + // + // Segment looping does not work with playbin3 in gstreamer + // < 1.22.9 because of a bug in multiqueue. + p->segment_gapless = true; value_notifier_init(&p->video_info_notifier, NULL, free); value_notifier_init(&p->duration_notifier, NULL, free); @@ -851,8 +1172,8 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo change_notifier_init(&p->error_notifier); change_notifier_init(&p->eos_notifier); - /// TODO: Use playbin or playbin3? - p->playbin = gst_element_factory_make("playbin3", "playbin"); + // playbin is more reliable for now than playbin3 (see above) + p->playbin = gst_element_factory_make("playbin", "playbin"); if (p->playbin == NULL) { LOG_PLAYER_ERROR(p, "Couldn't create playbin instance.\n"); goto fail_free_p; @@ -874,11 +1195,7 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo flags &= ~GST_PLAY_FLAG_AUDIO; } - if (subtitles) { - flags |= GST_PLAY_FLAG_TEXT; - } else { - flags &= ~GST_PLAY_FLAG_TEXT; - } + flags &= ~GST_PLAY_FLAG_TEXT; g_object_set(p->playbin, "flags", flags, NULL); @@ -925,6 +1242,7 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo #endif } + if (play_audio) { p->audiopanorama = gst_element_factory_make("audiopanorama", NULL); if (p->audiopanorama != NULL) { @@ -965,6 +1283,8 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo LOG_PLAYER_DEBUG(p, "Not live!\n"); p->is_live = false; } + + p->uri = strdup(uri); } return p; @@ -1004,7 +1324,7 @@ struct gstplayer *gstplayer_new_from_asset(struct flutterpi *flutterpi, const ch return NULL; } - player = gstplayer_new(flutterpi, uri, userdata, /* play_video */ true, /* play_audio */ false, /* play_text */ false, NULL); + player = gstplayer_new(flutterpi, uri, userdata, /* play_video */ true, /* play_audio */ true, NULL); free(uri); @@ -1013,15 +1333,15 @@ struct gstplayer *gstplayer_new_from_asset(struct flutterpi *flutterpi, const ch struct gstplayer *gstplayer_new_from_network(struct flutterpi *flutterpi, const char *uri, enum format_hint format_hint, void *userdata, GstStructure *headers) { (void) format_hint; - return gstplayer_new(flutterpi, uri, userdata, true, true, false, headers); + return gstplayer_new(flutterpi, uri, userdata, /* play_video */ true, /* play_audio */ true, headers); } struct gstplayer *gstplayer_new_from_file(struct flutterpi *flutterpi, const char *uri, void *userdata) { - return gstplayer_new(flutterpi, uri, userdata, true, true, false, NULL); + return gstplayer_new(flutterpi, uri, userdata, /* play_video */ true, /* play_audio */ true, NULL); } struct gstplayer *gstplayer_new_from_content_uri(struct flutterpi *flutterpi, const char *uri, void *userdata, GstStructure *headers) { - return gstplayer_new(flutterpi, uri, userdata, true, true, false, headers); + return gstplayer_new(flutterpi, uri, userdata, /* play_video */ true, /* play_audio */ true, headers); } struct gstplayer *gstplayer_new_from_pipeline(struct flutterpi *flutterpi, const char *pipeline, void *userdata) { @@ -1076,9 +1396,24 @@ int gstplayer_pause(struct gstplayer *player) { return apply_playback_state(player); } -int gstplayer_set_looping(struct gstplayer *player, bool looping) { - LOG_PLAYER_DEBUG(player, "set_looping(%s)\n", looping ? "true" : "false"); +int gstplayer_set_looping(struct gstplayer *player, bool looping, bool gapless) { + LOG_PLAYER_DEBUG(player, "set_looping(%s, gapless: %s)\n", looping ? "true" : "false", gapless ? "true" : "false"); + + if (player->playbin_gapless && gapless) { + // If we're enabling (gapless) looping, + // already configure the next playback URI, + // since we don't know if the about-to-finish callback + // has already arrived or not. + if (!player->looping && looping && player->uri) { + g_object_set(player->playbin, "uri", player->uri, NULL); + } + } + player->looping = looping; + player->gapless_looping = gapless; + + apply_playback_state(player); + return 0; } @@ -1095,7 +1430,7 @@ int64_t gstplayer_get_position(struct gstplayer *player) { GstStateChangeReturn statechange = gst_element_get_state(GST_ELEMENT(player->playbin), ¤t, &pending, 0); if (statechange == GST_STATE_CHANGE_FAILURE) { - LOG_GST_GET_STATE_ERROR(player->playbin); + LOG_GST_GET_STATE_ERROR(player, player->playbin); return -1; } @@ -1130,6 +1465,19 @@ int gstplayer_seek_to(struct gstplayer *player, int64_t position, bool nearest_k return apply_playback_state(player); } +int gstplayer_seek_with_completer(struct gstplayer *player, int64_t position, bool nearest_keyframe, struct async_completer completer) { + LOG_PLAYER_DEBUG(player, "seek_to(%" PRId64 ")\n", position); + player->has_desired_position = true; + player->desired_position_ms = position; + player->do_fast_seeking = nearest_keyframe; + + if (completer.on_done || completer.on_error) { + start_async(player, completer); + } + + return apply_playback_state(player); +} + int gstplayer_set_playback_speed(struct gstplayer *player, double playback_speed) { LOG_PLAYER_DEBUG(player, "set_playback_speed(%f)\n", playback_speed); ASSERT_MSG(playback_speed > 0, "playback speed must be > 0."); @@ -1196,42 +1544,74 @@ float gstplayer_get_audio_balance(struct gstplayer *player) { } } -bool gstplayer_release(struct gstplayer *p) { - GstStateChangeReturn status = gst_element_set_state(p->playbin, GST_STATE_NULL); - if (status == GST_STATE_CHANGE_FAILURE) { - LOG_ERROR("Could not set pipeline to NULL state.\n"); - return false; - } else { +bool gstplayer_set_source_with_completer(struct gstplayer *p, const char *uri, struct async_completer completer) { + GstStateChangeReturn result; + const char *current_uri = NULL; + + g_object_get(p->playbin, "current-uri", ¤t_uri, NULL); + + // If we're already playing back the desired uri, don't change it. + if ((current_uri == uri) || (uri && current_uri && streq(current_uri, uri))) { + if (completer.on_done) { + completer.on_done(completer.userdata); + } + return true; } -} -bool gstplayer_preroll(struct gstplayer *p, const char *uri) { - GstState current, pending; + p->uri = strdup(uri); - GstStateChangeReturn status = gst_element_get_state(p->playbin, ¤t, &pending, 0 * GST_SECOND); - if (status != GST_STATE_CHANGE_SUCCESS || current != GST_STATE_NULL) { - LOG_ERROR("Pipeline must be in NULL (released) state to preroll new source.\n"); + // If the playbin supports instant-uri, use it. + // if (g_object_class_find_property(G_OBJECT_GET_CLASS(p->playbin), "instant-uri")) { + // g_object_set(p->playbin, "instant-uri", TRUE, "uri", uri, NULL); + // } else { + + result = gst_element_set_state(p->playbin, GST_STATE_NULL); + if (result != GST_STATE_CHANGE_SUCCESS) { + LOG_PLAYER_ERROR(p, "Could not set pipeline to NULL state to change uri.\n"); return false; } g_object_set(p->playbin, "uri", uri, NULL); - status = gst_element_set_state(p->playbin, GST_STATE_PAUSED); - if (status == GST_STATE_CHANGE_NO_PREROLL) { - p->is_live = true; - } else if (status == GST_STATE_CHANGE_FAILURE) { - LOG_ERROR("Could not set pipeline to paused state.\n"); + result = gst_element_set_state(p->playbin, GST_STATE_PAUSED); + if (result == GST_STATE_CHANGE_FAILURE) { + LOG_PLAYER_ERROR(p, "Could not set pipeline to PAUSED state to play new uri.\n"); return false; - } else { + } else if (result == GST_STATE_CHANGE_NO_PREROLL) { + p->is_live = true; + + if (completer.on_done != NULL) { + completer.on_done(completer.userdata); + } + } else if (result == GST_STATE_CHANGE_SUCCESS) { p->is_live = false; + + if (completer.on_done) { + completer.on_done(completer.userdata); + } + } else if (result == GST_STATE_CHANGE_ASYNC) { + /// TODO: What is is_live here? + p->is_live = false; + + if (completer.on_done || completer.on_error) { + start_async(p, completer); + } } - // TODO: Trigger events here (duration change, video info, etc) + gstplayer_seek_to(p, 0, false); return true; } +bool gstplayer_set_source(struct gstplayer *p, const char *uri) { + return gstplayer_set_source_with_completer(p, uri, (struct async_completer) { + .on_done = NULL, + .on_error = NULL, + .userdata = NULL + }); +} + struct notifier *gstplayer_get_video_info_notifier(struct gstplayer *player) { return &player->video_info_notifier; } diff --git a/src/plugins/gstplayer.h b/src/plugins/gstplayer.h index c97c3714..a5f1922f 100644 --- a/src/plugins/gstplayer.h +++ b/src/plugins/gstplayer.h @@ -4,6 +4,10 @@ #include #include +#include + +#include "util/collection.h" + #include "config.h" #define GSTREAMER_VER(major, minor, patch) ((((major) &0xFF) << 16) | (((minor) &0xFF) << 8) | ((patch) &0xFF)) @@ -60,6 +64,12 @@ struct notifier; typedef struct _GstStructure GstStructure; +struct async_completer { + void_callback_t on_done; + void (*on_error)(void *userdata, GError *error); + void *userdata; +}; + /// Create a gstreamer video player. struct gstplayer *gstplayer_new( struct flutterpi *flutterpi, @@ -67,7 +77,6 @@ struct gstplayer *gstplayer_new( void *userdata, bool play_video, bool play_audio, - bool subtitles, GstStructure *headers ); @@ -145,7 +154,7 @@ int64_t gstplayer_get_duration(struct gstplayer *player); /// Set whether the video should loop. /// @arg looping Whether the video should start playing from the beginning when the /// end is reached. -int gstplayer_set_looping(struct gstplayer *player, bool looping); +int gstplayer_set_looping(struct gstplayer *player, bool looping, bool gapless); /// Set the playback volume. /// @arg volume Desired volume as a value between 0 and 1. @@ -156,6 +165,10 @@ int gstplayer_set_volume(struct gstplayer *player, double volume); /// @arg nearest_keyframe If true, seek to the nearest keyframe instead. Might be faster but less accurate. int gstplayer_seek_to(struct gstplayer *player, int64_t position, bool nearest_keyframe); +/// Seek to a specific position in the video and call +/// @arg on_seek_done with @arg userdata when done. +int gstplayer_seek_with_completer(struct gstplayer *player, int64_t position, bool nearest_keyframe, struct async_completer completer); + /// Set the playback speed of the player. /// 1.0: normal playback speed /// 0.5: half playback speed @@ -170,9 +183,9 @@ void gstplayer_set_audio_balance(struct gstplayer *player, float balance); float gstplayer_get_audio_balance(struct gstplayer *player); -bool gstplayer_release(struct gstplayer *p); +bool gstplayer_set_source(struct gstplayer *p, const char *uri); -bool gstplayer_preroll(struct gstplayer *p, const char *uri); +bool gstplayer_set_source_with_completer(struct gstplayer *p, const char *uri, struct async_completer completer); struct video_info { int width, height; diff --git a/src/plugins/gstreamer_video_player/plugin.c b/src/plugins/gstreamer_video_player/plugin.c index d2a85a54..6690b5b7 100644 --- a/src/plugins/gstreamer_video_player/plugin.c +++ b/src/plugins/gstreamer_video_player/plugin.c @@ -698,7 +698,8 @@ static int on_set_looping(char *channel, struct platch_obj *object, FlutterPlatf return platch_respond_illegal_arg_ext_pigeon(responsehandle, "Expected `arg['isLooping']` to be a boolean, but was:", temp); } - gstplayer_set_looping(player, loop); + gstplayer_set_looping(player, loop, true); + return platch_respond_success_pigeon(responsehandle, NULL); } @@ -1283,7 +1284,21 @@ static int on_set_looping_v2(const struct raw_std_value *arg, FlutterPlatformMes return platch_respond_illegal_arg_std(responsehandle, "Expected `arg[1]` to be a bool."); } - ok = gstplayer_set_looping(player, looping); + // For video playback, gapless looping usually works fine + // it seems. + bool gapless = true; + if (raw_std_list_get_size(arg) >= 3) { + const struct raw_std_value *third = raw_std_list_get_nth_element(arg, 2); + if (raw_std_value_is_null(third)) { + // unchanged + } else if (raw_std_value_is_bool(third)) { + gapless = raw_std_value_as_bool(third); + } else { + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg[2]` to be a bool or null."); + } + } + + ok = gstplayer_set_looping(player, looping, gapless); if (ok != 0) { return platch_respond_native_error_std(responsehandle, ok); } From c2ed35874faa31096191772bf478144502f1f595 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Fri, 16 May 2025 12:59:12 +0200 Subject: [PATCH 22/41] flutter-pi: allow getting tracer of flutterpi instance --- src/flutter-pi.c | 5 +++++ src/flutter-pi.h | 3 +++ 2 files changed, 8 insertions(+) diff --git a/src/flutter-pi.c b/src/flutter-pi.c index 10219914..eacd80e9 100644 --- a/src/flutter-pi.c +++ b/src/flutter-pi.c @@ -1039,6 +1039,11 @@ struct gl_renderer *flutterpi_get_gl_renderer(struct flutterpi *flutterpi) { return flutterpi->gl_renderer; } +struct tracer *flutterpi_get_tracer(struct flutterpi *flutterpi) { + ASSERT_NOT_NULL(flutterpi); + return flutterpi->tracer; +} + void flutterpi_set_pointer_kind(struct flutterpi *flutterpi, enum pointer_kind kind) { return compositor_set_cursor(flutterpi->compositor, false, false, true, kind, false, VEC2F(0, 0)); } diff --git a/src/flutter-pi.h b/src/flutter-pi.h index d2e831d9..00f9eb65 100644 --- a/src/flutter-pi.h +++ b/src/flutter-pi.h @@ -92,6 +92,7 @@ struct drmdev; struct locales; struct vk_renderer; struct flutterpi; +struct tracer; /// TODO: Remove this extern struct flutterpi *flutterpi; @@ -188,6 +189,8 @@ bool flutterpi_has_gl_renderer(struct flutterpi *flutterpi); struct gl_renderer *flutterpi_get_gl_renderer(struct flutterpi *flutterpi); +struct tracer *flutterpi_get_tracer(struct flutterpi *flutterpi); + void flutterpi_set_pointer_kind(struct flutterpi *flutterpi, enum pointer_kind kind); void flutterpi_trace_event_instant(struct flutterpi *flutterpi, const char *name); From d008074c03b30e5a1a1718b73bf3770481931698 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Mon, 19 May 2025 12:18:07 +0200 Subject: [PATCH 23/41] video player: Better feedback when trying to create pipeline video player Respond with a detailed error message, that pipeline playback is not yet implemented instead of just "video player creation failed". --- src/plugins/gstreamer_video_player/plugin.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/plugins/gstreamer_video_player/plugin.c b/src/plugins/gstreamer_video_player/plugin.c index 6690b5b7..6cff6708 100644 --- a/src/plugins/gstreamer_video_player/plugin.c +++ b/src/plugins/gstreamer_video_player/plugin.c @@ -1201,11 +1201,16 @@ static int on_create_v2(const struct raw_std_value *arg, FlutterPlatformMessageR free(uri); uri = NULL; } else if (pipeline != NULL) { - player = gstplayer_new_from_pipeline(flutterpi, pipeline, NULL); - - // gstplayer_new_from_network will dup the pipeline internally. free(pipeline); - pipeline = NULL; + + /// TODO: Implement + + return platch_respond_error_std( + responsehandle, + "unimplemented", + "Creating players from pipeline is not supported yet for the playbin-based video player.", + NULL + ); } else { UNREACHABLE(); } From 3a9556739805065e274068d3b42c6e79b1b97821 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Mon, 2 Jun 2025 15:59:14 +0200 Subject: [PATCH 24/41] gstplayer: re-implement creating video players from pipeline description Disable audio for video players by default, for compatibility. Only enable when calling `createWithAudio` platform channel method. --- src/plugins/gstplayer.c | 251 ++++++++++++++---- src/plugins/gstplayer.h | 72 +++-- src/plugins/gstreamer_video_player.h | 3 + .../flutter_texture_sink.c | 49 ++-- src/plugins/gstreamer_video_player/plugin.c | 212 ++++++++++++++- 5 files changed, 478 insertions(+), 109 deletions(-) diff --git a/src/plugins/gstplayer.c b/src/plugins/gstplayer.c index ae792922..762fd8c4 100644 --- a/src/plugins/gstplayer.c +++ b/src/plugins/gstplayer.c @@ -185,11 +185,20 @@ struct gstplayer { sd_event_source *busfd_events; /** - * The gstreamer playbin, which gstplayer is just - * a wrapper around, basically. + * The gstreamer playbin. + * + * In most cases this is the same as the pipeline (since a playbin is a pipeline). + * The only exception is when the gstplayer was initialized using a pipeline description, + * in which case we don't have a playbin. In that case, playbin will be NULL and + * pipeline will be valid. */ GstElement *playbin; + /** + * The gstreamer pipeline. + */ + GstElement *pipeline; + /** * The gstreamer audiopanorama element, used as the "audio-filter" * if audio playback is enabled, and used to change the audio @@ -250,6 +259,8 @@ struct gstplayer { * issued to start gapless looping. */ bool did_configure_segment; + + struct tracer *tracer; }; static struct async_completer pop_completer(struct gstplayer *player) { @@ -306,7 +317,7 @@ static void fetch_duration(struct gstplayer *player) { gboolean ok; int64_t duration; - ok = gst_element_query_duration(player->playbin, GST_FORMAT_TIME, &duration); + ok = gst_element_query_duration(player->pipeline, GST_FORMAT_TIME, &duration); if (ok == FALSE) { if (player->is_live) { player->info.info.duration_ms = INT64_MAX; @@ -334,7 +345,7 @@ static void fetch_seeking(struct gstplayer *player) { int64_t seek_begin, seek_end; seeking_query = gst_query_new_seeking(GST_FORMAT_TIME); - ok = gst_element_query(player->playbin, seeking_query); + ok = gst_element_query(player->pipeline, seeking_query); if (ok == FALSE) { if (player->is_live) { player->info.info.can_seek = false; @@ -434,19 +445,22 @@ static int apply_playback_state(struct gstplayer *player) { double desired_rate; int64_t position; - ok = gst_element_get_state(player->playbin, ¤t_state, &pending_state, 0); + TRACER_BEGIN(player->tracer, "apply_playback_state()"); + + ok = gst_element_get_state(player->pipeline, ¤t_state, &pending_state, 0); if (ok == GST_STATE_CHANGE_FAILURE) { LOG_PLAYER_DEBUG( player, "last gstreamer pipeline state change failed. gst_element_get_state(element name: %s): GST_STATE_CHANGE_FAILURE\n", - GST_ELEMENT_NAME(player->playbin) + GST_ELEMENT_NAME(player->pipeline) ); - return EIO; + goto fail_stop_trace; } if (current_state == GST_STATE_NULL) { // We don't have a playback source right now. // Don't do anything. + TRACER_END(player->tracer, "apply_playback_state()"); return 0; } @@ -464,10 +478,10 @@ static int apply_playback_state(struct gstplayer *player) { if (player->has_desired_position) { position = player->desired_position_ms * GST_MSECOND; } else { - ok = gst_element_query_position(GST_ELEMENT(player->playbin), GST_FORMAT_TIME, &position); + ok = gst_element_query_position(GST_ELEMENT(player->pipeline), GST_FORMAT_TIME, &position); if (ok == FALSE) { LOG_PLAYER_ERROR(player, "Could not get the current playback position to apply the playback speed.\n"); - return EIO; + goto fail_stop_trace; } } @@ -496,7 +510,7 @@ static int apply_playback_state(struct gstplayer *player) { ); ok = gst_element_seek( - GST_ELEMENT(player->playbin), + GST_ELEMENT(player->pipeline), desired_rate, GST_FORMAT_TIME, seek_flags, @@ -510,7 +524,7 @@ static int apply_playback_state(struct gstplayer *player) { desired_rate, GST_TIME_ARGS(position) ); - return EIO; + goto fail_stop_trace; } } else { LOG_PLAYER_DEBUG( @@ -521,7 +535,7 @@ static int apply_playback_state(struct gstplayer *player) { GST_TIME_ARGS(position) ); ok = gst_element_seek( - GST_ELEMENT(player->playbin), + GST_ELEMENT(player->pipeline), desired_rate, GST_FORMAT_TIME, seek_flags, @@ -536,7 +550,7 @@ static int apply_playback_state(struct gstplayer *player) { desired_rate, GST_TIME_ARGS(position) ); - return EIO; + goto fail_stop_trace; } } @@ -555,6 +569,7 @@ static int apply_playback_state(struct gstplayer *player) { "apply_playback_state(playing: %s): already in desired state and none pending\n", PLAYPAUSE_STATE_AS_STRING(player->playpause_state) ); + TRACER_END(player->tracer, "apply_playback_state()"); return 0; } @@ -565,11 +580,11 @@ static int apply_playback_state(struct gstplayer *player) { gst_element_state_get_name(desired_state) ); - ok = gst_element_set_state(player->playbin, desired_state); + ok = gst_element_set_state(player->pipeline, desired_state); if (ok == GST_STATE_CHANGE_FAILURE) { - LOG_GST_SET_STATE_ERROR(player, player->playbin); - return EIO; + LOG_GST_SET_STATE_ERROR(player, player->pipeline); + goto fail_stop_trace; } } else if (pending_state != desired_state) { // queue to be executed when pending async state change completes @@ -582,24 +597,30 @@ static int apply_playback_state(struct gstplayer *player) { gst_element_state_get_name(desired_state) ); - ok = gst_element_set_state(player->playbin, desired_state); + ok = gst_element_set_state(player->pipeline, desired_state); if (ok == GST_STATE_CHANGE_FAILURE) { - LOG_GST_SET_STATE_ERROR(player, player->playbin); - return EIO; + LOG_GST_SET_STATE_ERROR(player, player->pipeline); + goto fail_stop_trace; } } + + TRACER_END(player->tracer, "apply_playback_state()"); return 0; + +fail_stop_trace: + TRACER_END(player->tracer, "apply_playback_state()"); + return EIO; } static void on_eos_message(struct gstplayer *player, GstMessage *msg) { - if (GST_MESSAGE_SRC(msg) == GST_OBJECT(player->playbin)) { + if (GST_MESSAGE_SRC(msg) == GST_OBJECT(player->pipeline)) { if (player->looping) { - LOG_PLAYER_DEBUG(player, "playbin end of stream, seeking back to start (flushing)\n"); + LOG_PLAYER_DEBUG(player, "pipeline end of stream, seeking back to start (flushing)\n"); player->desired_position_ms = 0; player->has_desired_position = true; apply_playback_state(player); } else { - LOG_PLAYER_DEBUG(player, "playbin end of stream\n"); + LOG_PLAYER_DEBUG(player, "pipeline end of stream\n"); notifier_notify(&player->eos_notifier, NULL); } } else { @@ -699,10 +720,10 @@ static void on_state_changed_message(struct gstplayer *player, GstMessage *msg) gst_message_parse_state_changed(msg, &old, ¤t, &pending); - if (GST_MESSAGE_SRC(msg) == GST_OBJECT(player->playbin)) { + if (GST_MESSAGE_SRC(msg) == GST_OBJECT(player->pipeline)) { LOG_PLAYER_DEBUG( player, - "playbin state changed: old: %s, current: %s, pending: %s\n", + "pipeline state changed: old: %s, current: %s, pending: %s\n", gst_element_state_get_name(old), gst_element_state_get_name(current), gst_element_state_get_name(pending) @@ -779,7 +800,7 @@ static void on_segment_done_message(struct gstplayer *player, GstMessage *msg) { if (player->looping && player->gapless_looping && player->segment_gapless) { LOG_PLAYER_DEBUG(player, "Segment done. Seeking back to segment start (segment, non-flushing)\n"); gboolean ok = gst_element_seek( - player->playbin, + player->pipeline, player->current_playback_rate, GST_FORMAT_TIME, GST_SEEK_FLAG_SEGMENT, @@ -819,6 +840,8 @@ static void on_duration_changed_message(struct gstplayer *player, GstMessage *ms } static void on_about_to_finish_message(struct gstplayer *player) { + ASSERT_NOT_NULL(player->playbin); + if (player->looping && player->uri && player->playbin_gapless) { LOG_PLAYER_DEBUG(player, "Got about-to-finish signal, configuring next playback item\n"); g_object_set(player->playbin, "uri", player->uri, NULL); @@ -916,7 +939,7 @@ static void on_bus_message(struct gstplayer *player, GstMessage *msg) { case GST_MESSAGE_LATENCY: LOG_PLAYER_DEBUG(player, "redistributing latency\n"); - gst_bin_recalculate_latency(GST_BIN(player->playbin)); + gst_bin_recalculate_latency(GST_BIN(player->pipeline)); break; case GST_MESSAGE_ASYNC_DONE: @@ -927,7 +950,7 @@ static void on_bus_message(struct gstplayer *player, GstMessage *msg) { GstState requested; gst_message_parse_request_state(msg, &requested); - gst_element_set_state(GST_ELEMENT(player->playbin), requested); + gst_element_set_state(GST_ELEMENT(player->pipeline), requested); break; } @@ -997,7 +1020,7 @@ static int on_bus_fd_ready(sd_event_source *s, int fd, uint32_t revents, void *u struct gstplayer *player = userdata; - GstMessage *msg = gst_bus_pop(gst_element_get_bus(player->playbin)); + GstMessage *msg = gst_bus_pop(gst_element_get_bus(player->pipeline)); if (msg != NULL) { on_bus_message(player, msg); gst_message_unref(msg); @@ -1165,6 +1188,8 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo // < 1.22.9 because of a bug in multiqueue. p->segment_gapless = true; + p->tracer = flutterpi_get_tracer(flutterpi); + value_notifier_init(&p->video_info_notifier, NULL, free); value_notifier_init(&p->duration_notifier, NULL, free); value_notifier_init(&p->seeking_info_notifier, NULL, free); @@ -1172,6 +1197,8 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo change_notifier_init(&p->error_notifier); change_notifier_init(&p->eos_notifier); + TRACER_BEGIN(p->tracer, "gstplayer_new()"); + // playbin is more reliable for now than playbin3 (see above) p->playbin = gst_element_factory_make("playbin", "playbin"); if (p->playbin == NULL) { @@ -1179,6 +1206,8 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo goto fail_free_p; } + p->pipeline = p->playbin; + gint flags = 0; g_object_get(p->playbin, "flags", &flags, NULL); @@ -1213,10 +1242,6 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo goto fail_destroy_texture; } - // playbin (playsink) takes a (sinking) reference - // on the video sink - g_object_set(p->playbin, "video-sink", sink, NULL); - GstPad *video_sink_pad = gst_element_get_static_pad(sink, "sink"); if (video_sink_pad == NULL) { LOG_PLAYER_ERROR(p, "Could not acquire sink pad of video sink to wait for video configuration.\n"); @@ -1229,11 +1254,13 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo gst_object_unref(video_sink_pad); video_sink_pad = NULL; + // playbin (playsink) takes a (sinking) reference + // on the video sink + g_object_set(p->playbin, "video-sink", sink, NULL); + // Apply capture-io-mode: dmabuf to any v4l2 decoders. /// TODO: This might be unnecessary / deprecated nowadays. g_signal_connect(p->playbin, "element-setup", G_CALLBACK(on_element_setup), NULL); - - gst_object_unref(sink); #else (void) flutterpi; @@ -1287,6 +1314,8 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo p->uri = strdup(uri); } + TRACER_END(p->tracer, "gstplayer_new()"); + return p; fail_rm_event_source: @@ -1308,11 +1337,12 @@ fail_destroy_texture: UNUSED gst_object_unref(p->playbin); fail_free_p: + TRACER_END(p->tracer, "gstplayer_new()"); free(p); return NULL; } -struct gstplayer *gstplayer_new_from_asset(struct flutterpi *flutterpi, const char *asset_path, const char *package_name, void *userdata) { +struct gstplayer *gstplayer_new_from_asset(struct flutterpi *flutterpi, const char *asset_path, const char *package_name, bool play_video, bool play_audio, void *userdata) { struct gstplayer *player; char *uri; int ok; @@ -1324,31 +1354,121 @@ struct gstplayer *gstplayer_new_from_asset(struct flutterpi *flutterpi, const ch return NULL; } - player = gstplayer_new(flutterpi, uri, userdata, /* play_video */ true, /* play_audio */ true, NULL); + player = gstplayer_new(flutterpi, uri, userdata, /* play_video */ play_video, /* play_audio */ play_audio, NULL); free(uri); return player; } -struct gstplayer *gstplayer_new_from_network(struct flutterpi *flutterpi, const char *uri, enum format_hint format_hint, void *userdata, GstStructure *headers) { +struct gstplayer *gstplayer_new_from_network(struct flutterpi *flutterpi, const char *uri, enum format_hint format_hint, bool play_video, bool play_audio, void *userdata, GstStructure *headers) { (void) format_hint; - return gstplayer_new(flutterpi, uri, userdata, /* play_video */ true, /* play_audio */ true, headers); + return gstplayer_new(flutterpi, uri, userdata, /* play_video */ play_video, /* play_audio */ play_audio, headers); } -struct gstplayer *gstplayer_new_from_file(struct flutterpi *flutterpi, const char *uri, void *userdata) { - return gstplayer_new(flutterpi, uri, userdata, /* play_video */ true, /* play_audio */ true, NULL); +struct gstplayer *gstplayer_new_from_file(struct flutterpi *flutterpi, const char *uri, bool play_video, bool play_audio, void *userdata) { + return gstplayer_new(flutterpi, uri, userdata, /* play_video */ play_video, /* play_audio */ play_audio, NULL); } -struct gstplayer *gstplayer_new_from_content_uri(struct flutterpi *flutterpi, const char *uri, void *userdata, GstStructure *headers) { - return gstplayer_new(flutterpi, uri, userdata, /* play_video */ true, /* play_audio */ true, headers); +struct gstplayer *gstplayer_new_from_content_uri(struct flutterpi *flutterpi, const char *uri, bool play_video, bool play_audio, void *userdata, GstStructure *headers) { + return gstplayer_new(flutterpi, uri, userdata, /* play_video */ play_video, /* play_audio */ play_audio, headers); } -struct gstplayer *gstplayer_new_from_pipeline(struct flutterpi *flutterpi, const char *pipeline, void *userdata) { - /// TODO: Implement - (void) flutterpi; - (void) pipeline; - (void) userdata; +struct gstplayer *gstplayer_new_from_pipeline(struct flutterpi *flutterpi, const char *pipeline_descr, void *userdata) { + ASSERT_NOT_NULL(flutterpi); + + struct gstplayer *p = calloc(1, sizeof(struct gstplayer)); + if (p == NULL) { + return NULL; + } + +#ifdef DEBUG + p->debug_id = allocate_id(); +#endif + p->userdata = userdata; + p->current_playback_rate = 1.0; + p->playback_rate_forward = 1.0; + p->playback_rate_backward = 1.0; + + p->gapless_looping = false; + p->playbin_gapless = false; + p->segment_gapless = false; + + p->tracer = flutterpi_get_tracer(flutterpi); + + value_notifier_init(&p->video_info_notifier, NULL, free); + value_notifier_init(&p->duration_notifier, NULL, free); + value_notifier_init(&p->seeking_info_notifier, NULL, free); + value_notifier_init(&p->buffering_state_notifier, NULL, free); + change_notifier_init(&p->error_notifier); + change_notifier_init(&p->eos_notifier); + + GError *error = NULL; + p->pipeline = gst_parse_launch(pipeline_descr, &error); + if (p->pipeline == NULL) { + LOG_ERROR("Could create GStreamer pipeline from description: %s (pipeline: `%s`)\n", error->message, pipeline_descr); + return NULL; + } + + // Remove the sink from the parsed pipeline description, and add our own sink. + GstElement *sink = gst_bin_get_by_name(GST_BIN(p->pipeline), "sink"); + if (sink == NULL) { + LOG_ERROR("Couldn't find appsink in pipeline bin.\n"); + goto fail_unref_pipeline; + } + + p->texture = flutterpi_create_texture(flutterpi); + if (p->texture == NULL) { + goto fail_unref_pipeline; + } + + struct gl_renderer *gl_renderer = flutterpi_get_gl_renderer(flutterpi); + + if (!flutter_gl_texture_sink_patch(sink, p->texture, gl_renderer)) { + LOG_ERROR("Could not setup appsink.\n"); + goto fail_unref_pipeline; + } + + // Listen to the bus + GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(p->pipeline)); + ASSERT_NOT_NULL(bus); + + GPollFD fd; + gst_bus_get_pollfd(bus, &fd); + + flutterpi_sd_event_add_io(&p->busfd_events, fd.fd, EPOLLIN, on_bus_fd_ready, p); + + gst_object_unref(bus); + + GstStateChangeReturn status = gst_element_set_state(p->pipeline, GST_STATE_PAUSED); + if (status == GST_STATE_CHANGE_NO_PREROLL) { + LOG_PLAYER_DEBUG(p, "Is live!\n"); + p->is_live = true; + } else if (status == GST_STATE_CHANGE_FAILURE) { + LOG_PLAYER_ERROR(p, "Could not set pipeline to paused state.\n"); + goto fail_rm_event_source; + } else { + LOG_PLAYER_DEBUG(p, "Not live!\n"); + p->is_live = false; + } + + return p; + +fail_rm_event_source: + sd_event_source_set_enabled(p->busfd_events, false); + sd_event_source_unref(p->busfd_events); + +fail_destroy_texture: UNUSED + gst_object_unref(p->pipeline); + + // The flutter upload sink uses the texture internally, + // so the appsink (which contains the upload sink) must be destroyed first, + // before the texture can be destroyed. + texture_destroy(p->texture); + return NULL; + +fail_unref_pipeline: + gst_object_unref(p->pipeline); return NULL; } @@ -1360,12 +1480,17 @@ void gstplayer_destroy(struct gstplayer *player) { notifier_deinit(&player->buffering_state_notifier); notifier_deinit(&player->error_notifier); notifier_deinit(&player->eos_notifier); - gst_element_set_state(GST_ELEMENT(player->playbin), GST_STATE_READY); - gst_element_set_state(GST_ELEMENT(player->playbin), GST_STATE_NULL); - gst_object_unref(player->playbin); + + gst_element_set_state(GST_ELEMENT(player->pipeline), GST_STATE_READY); + gst_element_set_state(GST_ELEMENT(player->pipeline), GST_STATE_NULL); + + player->playbin = NULL; + gst_object_unref(player->pipeline); + if (player->texture) { texture_destroy(player->texture); } + free(player); } @@ -1400,6 +1525,8 @@ int gstplayer_set_looping(struct gstplayer *player, bool looping, bool gapless) LOG_PLAYER_DEBUG(player, "set_looping(%s, gapless: %s)\n", looping ? "true" : "false", gapless ? "true" : "false"); if (player->playbin_gapless && gapless) { + ASSERT_NOT_NULL(player->playbin); + // If we're enabling (gapless) looping, // already configure the next playback URI, // since we don't know if the about-to-finish callback @@ -1418,8 +1545,13 @@ int gstplayer_set_looping(struct gstplayer *player, bool looping, bool gapless) } int gstplayer_set_volume(struct gstplayer *player, double volume) { - LOG_PLAYER_DEBUG(player, "set_volume(%f)\n", volume); - g_object_set(player->playbin, "volume", (gdouble) volume, NULL); + if (player->playbin) { + LOG_PLAYER_DEBUG(player, "set_volume(%f)\n", volume); + g_object_set(player->playbin, "volume", (gdouble) volume, NULL); + } else { + LOG_PLAYER_DEBUG(player, "set_volume(%f): can't set volume on pipeline video player\n", volume); + } + return 0; } @@ -1428,9 +1560,9 @@ int64_t gstplayer_get_position(struct gstplayer *player) { gboolean ok; int64_t position; - GstStateChangeReturn statechange = gst_element_get_state(GST_ELEMENT(player->playbin), ¤t, &pending, 0); + GstStateChangeReturn statechange = gst_element_get_state(GST_ELEMENT(player->pipeline), ¤t, &pending, 0); if (statechange == GST_STATE_CHANGE_FAILURE) { - LOG_GST_GET_STATE_ERROR(player, player->playbin); + LOG_GST_GET_STATE_ERROR(player, player->pipeline); return -1; } @@ -1440,7 +1572,7 @@ int64_t gstplayer_get_position(struct gstplayer *player) { return player->fallback_position_ms; } - ok = gst_element_query_position(player->playbin, GST_FORMAT_TIME, &position); + ok = gst_element_query_position(player->pipeline, GST_FORMAT_TIME, &position); if (ok == FALSE) { LOG_PLAYER_ERROR(player, "Could not query gstreamer position. (gst_element_query_position)\n"); return 0; @@ -1498,7 +1630,7 @@ int gstplayer_step_forward(struct gstplayer *player) { return ok; } - gst_ok = gst_element_send_event(player->playbin, gst_event_new_step(GST_FORMAT_BUFFERS, 1, 1, TRUE, FALSE)); + gst_ok = gst_element_send_event(player->pipeline, gst_event_new_step(GST_FORMAT_BUFFERS, 1, 1, TRUE, FALSE)); if (gst_ok == FALSE) { LOG_PLAYER_ERROR(player, "Could not send frame-step event to pipeline. (gst_element_send_event)\n"); return EIO; @@ -1519,7 +1651,7 @@ int gstplayer_step_backward(struct gstplayer *player) { return ok; } - gst_ok = gst_element_send_event(player->playbin, gst_event_new_step(GST_FORMAT_BUFFERS, 1, 1, TRUE, FALSE)); + gst_ok = gst_element_send_event(player->pipeline, gst_event_new_step(GST_FORMAT_BUFFERS, 1, 1, TRUE, FALSE)); if (gst_ok == FALSE) { LOG_PLAYER_ERROR(player, "Could not send frame-step event to pipeline. (gst_element_send_event)\n"); return EIO; @@ -1548,6 +1680,11 @@ bool gstplayer_set_source_with_completer(struct gstplayer *p, const char *uri, s GstStateChangeReturn result; const char *current_uri = NULL; + if (!p->playbin) { + LOG_PLAYER_ERROR(p, "Can't set source for a pipeline video player.\n"); + return false; + } + g_object_get(p->playbin, "current-uri", ¤t_uri, NULL); // If we're already playing back the desired uri, don't change it. diff --git a/src/plugins/gstplayer.h b/src/plugins/gstplayer.h index a5f1922f..6cbd8aca 100644 --- a/src/plugins/gstplayer.h +++ b/src/plugins/gstplayer.h @@ -70,7 +70,16 @@ struct async_completer { void *userdata; }; -/// Create a gstreamer video player. +/** + * @brief Create a gstreamer player that loads the media from a generic (file, network) URI. + * + * @arg uri The URI to the media. + * @arg format_hint A hint to the format of the video. FORMAT_HINT_NONE means there's no hint. + * @arg play_video Whether the player should play video. + * @arg play_audio Whether the player should play audio. + * @arg userdata The userdata associated with this player. + * @arg headers HTTP headers to use for the network requests. + */ struct gstplayer *gstplayer_new( struct flutterpi *flutterpi, const char *uri, @@ -80,27 +89,46 @@ struct gstplayer *gstplayer_new( GstStructure *headers ); -/// Create a gstreamer video player that loads the video from a flutter asset. -/// @arg asset_path The path of the asset inside the asset bundle. -/// @arg package_name The name of the package containing the asset -/// @arg userdata The userdata associated with this player -struct gstplayer *gstplayer_new_from_asset(struct flutterpi *flutterpi, const char *asset_path, const char *package_name, void *userdata); - -/// Create a gstreamer video player that loads the video from a network URI. -/// @arg uri The URI to the video. (for example, http://, https://, rtmp://, rtsp://) -/// @arg format_hint A hint to the format of the video. FORMAT_HINT_NONE means there's no hint. -/// @arg userdata The userdata associated with this player. -struct gstplayer *gstplayer_new_from_network(struct flutterpi *flutterpi, const char *uri, enum format_hint format_hint, void *userdata, GstStructure *headers); - -/// Create a gstreamer video player that loads the video from a file URI. -/// @arg uri The file:// URI to the video. -/// @arg userdata The userdata associated with this player. -struct gstplayer *gstplayer_new_from_file(struct flutterpi *flutterpi, const char *uri, void *userdata); - -/// Create a gstreamer video player with a custom gstreamer pipeline. -/// @arg pipeline The description of the custom pipeline that should be used. Should contain an appsink called "sink". -/// @arg userdata The userdata associated with this player. -struct gstplayer *gstplayer_new_from_pipeline(struct flutterpi *flutterpi, const char *pipeline, void *userdata); +/** + * @brief Create a gstreamer player that loads the media from a flutter asset. + * + * @arg asset_path The path of the asset inside the asset bundle. + * @arg package_name The name of the package containing the asset + * @arg play_video Whether the player should play video. + * @arg play_audio Whether the player should play audio. + * @arg userdata The userdata associated with this player + */ +struct gstplayer *gstplayer_new_from_asset(struct flutterpi *flutterpi, const char *asset_path, const char *package_name, bool play_video, bool play_audio, void *userdata); + +/** + * @brief Create a gstreamer player that loads the media from a network URI. + * + * @arg uri The URI to the media. (for example, http://, https://, rtmp://, rtsp://) + * @arg format_hint A hint to the format of the media. FORMAT_HINT_NONE means there's no hint. + * @arg play_video Whether the player should play video. + * @arg play_audio Whether the player should play audio. + * @arg userdata The userdata associated with this player. + * @arg headers HTTP headers to use for the network requests. + */ +struct gstplayer *gstplayer_new_from_network(struct flutterpi *flutterpi, const char *uri, enum format_hint format_hint, bool play_video, bool play_audio, void *userdata, GstStructure *headers); + +/** + * @brief Create a gstreamer player that loads the media from a file URI. + * + * @arg uri The file:// URI to the audio or video file. + * @arg play_video Whether the player should play video. + * @arg play_audio Whether the player should play audio. + * @arg userdata The userdata associated with this player. + */ +struct gstplayer *gstplayer_new_from_file(struct flutterpi *flutterpi, const char *uri, bool play_video, bool play_audio, void *userdata); + +/** + * @brief Create a gstreamer player with a custom gstreamer pipeline. + * + * @arg pipeline_descr The description of the custom pipeline that should be used. Should contain an appsink called "sink". + * @arg userdata The userdata associated with this player. + */ +struct gstplayer *gstplayer_new_from_pipeline(struct flutterpi *flutterpi, const char *pipeline_descr, void *userdata); /// Destroy this gstreamer player instance and the resources /// associated with it. (texture, gstreamer pipeline, etc) diff --git a/src/plugins/gstreamer_video_player.h b/src/plugins/gstreamer_video_player.h index 6b248e6a..c30d5e82 100644 --- a/src/plugins/gstreamer_video_player.h +++ b/src/plugins/gstreamer_video_player.h @@ -66,6 +66,9 @@ const struct gl_texture_frame *frame_get_gl_frame(struct video_frame *frame); struct texture; struct gl_renderer; typedef struct _GstElement GstElement; + GstElement *flutter_gl_texture_sink_new(struct texture *texture, struct gl_renderer *renderer); +bool flutter_gl_texture_sink_patch(GstElement *element, struct texture *texture, struct gl_renderer *renderer); + #endif diff --git a/src/plugins/gstreamer_video_player/flutter_texture_sink.c b/src/plugins/gstreamer_video_player/flutter_texture_sink.c index 8730e671..bc7f822e 100644 --- a/src/plugins/gstreamer_video_player/flutter_texture_sink.c +++ b/src/plugins/gstreamer_video_player/flutter_texture_sink.c @@ -128,14 +128,16 @@ static GstFlowReturn on_appsink_new_sample(GstAppSink *appsink, void *userdata) } static void on_appsink_cbs_destroy(void *userdata) { - struct gstplayer *player; + struct texture_sink *meta; LOG_DEBUG("on_appsink_cbs_destroy()\n"); ASSERT_NOT_NULL(userdata); - player = userdata; + meta = userdata; - (void) player; + // meta->texture is not owned by us. freed by the player + frame_interface_unref(meta->interface); + free(meta); } static GstCaps *caps_for_frame_interface(struct frame_interface *interface) { @@ -216,40 +218,33 @@ UNUSED static GstPadProbeReturn on_query_appsink_pad(GstPad *pad, GstPadProbeInf return GST_PAD_PROBE_HANDLED; } -GstElement *flutter_gl_texture_sink_new(struct texture *texture, struct gl_renderer *renderer) { +bool flutter_gl_texture_sink_patch(GstElement *element, struct texture *texture, struct gl_renderer *renderer) { + ASSERT_NOT_NULL(element); ASSERT_NOT_NULL(texture); ASSERT_NOT_NULL(renderer); struct texture_sink *meta = calloc(1, sizeof(struct texture_sink)); if (meta == NULL) { - return NULL; + return false; } meta->fl_texture = texture; - GstElement *element = gst_element_factory_make("appsink", "appsink"); - if (element == NULL) { - free(meta); - return NULL; - } - meta->interface = frame_interface_new(renderer); if (meta->interface == NULL) { - gst_object_unref(element); free(meta); - return NULL; + return false; } GstCaps *caps = caps_for_frame_interface(meta->interface); if (caps == NULL) { frame_interface_unref(meta->interface); - gst_object_unref(element); free(meta); - return NULL; + return false; } - GstBaseSink *basesink = GST_BASE_SINK_CAST(element); - GstAppSink *appsink = GST_APP_SINK_CAST(element); + GstBaseSink *basesink = GST_BASE_SINK(element); + GstAppSink *appsink = GST_APP_SINK(element); gst_base_sink_set_max_lateness(basesink, 20 * GST_MSECOND); gst_base_sink_set_qos_enabled(basesink, TRUE); @@ -281,9 +276,8 @@ GstElement *flutter_gl_texture_sink_new(struct texture *texture, struct gl_rende if (pad == NULL) { LOG_ERROR("Couldn't get static pad `sink` from appsink.\n"); frame_interface_unref(meta->interface); - gst_object_unref(element); free(meta); - return NULL; + return false; } gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_QUERY_DOWNSTREAM, on_query_appsink_pad, NULL, NULL); @@ -298,3 +292,20 @@ GstElement *flutter_gl_texture_sink_new(struct texture *texture, struct gl_rende return element; } + +GstElement *flutter_gl_texture_sink_new(struct texture *texture, struct gl_renderer *renderer) { + ASSERT_NOT_NULL(texture); + ASSERT_NOT_NULL(renderer); + + GstElement *element = gst_element_factory_make("appsink", "appsink"); + if (element == NULL) { + return NULL; + } + + if (!flutter_gl_texture_sink_patch(element, texture, renderer)) { + gst_object_unref(element); + return NULL; + } + + return element; +} diff --git a/src/plugins/gstreamer_video_player/plugin.c b/src/plugins/gstreamer_video_player/plugin.c index 6cff6708..b1dba592 100644 --- a/src/plugins/gstreamer_video_player/plugin.c +++ b/src/plugins/gstreamer_video_player/plugin.c @@ -607,7 +607,7 @@ static int on_create(char *channel, struct platch_obj *object, FlutterPlatformMe // create our actual player (this doesn't initialize it) if (asset != NULL) { - player = gstplayer_new_from_asset(flutterpi, asset, package_name, NULL); + player = gstplayer_new_from_asset(flutterpi, asset, package_name, true, false, NULL); } else { temp = stdmap_get_str(arg, "httpHeaders"); @@ -618,7 +618,7 @@ static int on_create(char *channel, struct platch_obj *object, FlutterPlatformMe return 0; } - player = gstplayer_new_from_network(flutterpi, uri, format_hint, NULL, headers); + player = gstplayer_new_from_network(flutterpi, uri, format_hint, true, false, NULL, headers); } if (player == NULL) { LOG_ERROR("Couldn't create gstreamer video player.\n"); @@ -1188,29 +1188,217 @@ static int on_create_v2(const struct raw_std_value *arg, FlutterPlatformMessageR // Create our actual player (this doesn't initialize it) if (asset != NULL) { - player = gstplayer_new_from_asset(flutterpi, asset, package_name, NULL); + player = gstplayer_new_from_asset(flutterpi, asset, package_name, true, false, NULL); // gstplayer_new_from_network will construct a file:// URI out of the // asset path internally. free(asset); asset = NULL; } else if (uri != NULL) { - player = gstplayer_new_from_network(flutterpi, uri, format_hint, NULL, headers); + player = gstplayer_new_from_network(flutterpi, uri, format_hint, true, false, NULL, headers); // gstplayer_new_from_network will dup the uri internally. free(uri); uri = NULL; } else if (pipeline != NULL) { + player = gstplayer_new_from_pipeline(flutterpi, pipeline, NULL); + free(pipeline); + } else { + UNREACHABLE(); + } - /// TODO: Implement + if (player == NULL) { + LOG_ERROR("Couldn't create gstreamer video player.\n"); + ok = EIO; + goto fail_respond_error; + } - return platch_respond_error_std( - responsehandle, - "unimplemented", - "Creating players from pipeline is not supported yet for the playbin-based video player.", - NULL - ); + // create a meta object so we can store the event channel name + // of a player with it + meta = create_meta(gstplayer_get_texture_id(player), player); + if (meta == NULL) { + ok = ENOMEM; + goto fail_destroy_player; + } + + gstplayer_set_userdata(player, meta); + + // Add it to our player collection + add_player(meta); + + // Set a receiver on the videoEvents event channel + ok = plugin_registry_set_receiver(meta->event_channel_name, kStandardMethodCall, on_receive_evch); + if (ok != 0) { + goto fail_remove_player; + } + + return platch_respond_success_std(responsehandle, &STDINT64(gstplayer_get_texture_id(player))); + +fail_remove_player: + remove_player(meta); + destroy_meta(meta); + +fail_destroy_player: + gstplayer_destroy(player); + +fail_respond_error: + return platch_respond_native_error_std(responsehandle, ok); +} + +static int on_create_with_audio(const struct raw_std_value *arg, FlutterPlatformMessageResponseHandle *responsehandle) { + struct gstplayer_meta *meta; + struct gstplayer *player; + enum format_hint format_hint; + char *asset, *uri, *package_name; + size_t size; + int ok; + + ok = ensure_initialized(); + if (ok != 0) { + return respond_init_failed_v2(responsehandle); + } + + if (!raw_std_value_is_list(arg)) { + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg` to be a List."); + } + + size = raw_std_list_get_size(arg); + + // arg[0]: Asset Path + if (size >= 1) { + arg = raw_std_list_get_first_element(arg); + + if (raw_std_value_is_null(arg)) { + asset = NULL; + } else if (raw_std_value_is_string(arg)) { + asset = raw_std_string_dup(arg); + if (asset == NULL) { + ok = ENOMEM; + goto fail_respond_error; + } + } else { + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg[0]` to be a String or null."); + } + } else { + asset = NULL; + } + + // arg[1]: Package Name + if (size >= 2) { + arg = raw_std_value_after(arg); + + if (raw_std_value_is_null(arg)) { + package_name = NULL; + } else if (raw_std_value_is_string(arg)) { + package_name = raw_std_string_dup(arg); + if (package_name == NULL) { + ok = ENOMEM; + goto fail_respond_error; + } + } else { + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg[1]` to be a String or null."); + } + } else { + package_name = NULL; + } + + // arg[1]: URI + if (size >= 3) { + arg = raw_std_value_after(arg); + + if (raw_std_value_is_null(arg)) { + uri = NULL; + } else if (raw_std_value_is_string(arg)) { + uri = raw_std_string_dup(arg); + if (uri == NULL) { + ok = ENOMEM; + goto fail_respond_error; + } + } else { + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg[2]` to be a String or null."); + } + } else { + uri = NULL; + } + + // arg[3]: Format Hint + if (size >= 4) { + arg = raw_std_value_after(arg); + + if (raw_std_value_is_null(arg)) { + format_hint = FORMAT_HINT_NONE; + } else if (raw_std_value_is_string(arg)) { + if (raw_std_string_equals(arg, "ss")) { + format_hint = FORMAT_HINT_SS; + } else if (raw_std_string_equals(arg, "hls")) { + format_hint = FORMAT_HINT_HLS; + } else if (raw_std_string_equals(arg, "dash")) { + format_hint = FORMAT_HINT_MPEG_DASH; + } else if (raw_std_string_equals(arg, "other")) { + format_hint = FORMAT_HINT_OTHER; + } else { + goto invalid_format_hint; + } + } else { +invalid_format_hint: + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg[3]` to be one of 'ss', 'hls', 'dash', 'other' or null."); + } + } else { + format_hint = FORMAT_HINT_NONE; + } + + GstStructure *headers = NULL; + + // arg[4]: HTTP Headers + if (size >= 5) { + arg = raw_std_value_after(arg); + + if (raw_std_value_is_null(arg)) { + headers = NULL; + } else if (raw_std_value_is_map(arg)) { + for_each_entry_in_raw_std_map(key, value, arg) { + if (raw_std_value_is_string(key) && raw_std_value_is_string(value)) { + if (headers == NULL) { + headers = gst_structure_new_empty("http-headers"); + } + + char *key_str = raw_std_string_dup(key); + gst_structure_take_string(headers, key_str, raw_std_string_dup(value)); + free(key_str); + } else { + goto invalid_headers; + } + } + } else { +invalid_headers: + if (headers != NULL) { + gst_structure_free(headers); + } + return platch_respond_illegal_arg_std(responsehandle, "Expected `arg[4]` to be a map of strings or null."); + } + } else { + headers = NULL; + } + + if ((asset ? 1 : 0) + (uri ? 1 : 0) != 1) { + return platch_respond_illegal_arg_std(responsehandle, "Expected exactly one of `arg[0]` or `arg[2]` to be non-null."); + } + + // Create our actual player (this doesn't initialize it) + if (asset != NULL) { + player = gstplayer_new_from_asset(flutterpi, asset, package_name, true, true, NULL); + + // gstplayer_new_from_network will construct a file:// URI out of the + // asset path internally. + free(asset); + asset = NULL; + } else if (uri != NULL) { + player = gstplayer_new_from_network(flutterpi, uri, format_hint, true, true, NULL, headers); + + // gstplayer_new_from_network will dup the uri internally. + free(uri); + uri = NULL; } else { UNREACHABLE(); } @@ -1550,6 +1738,8 @@ static int on_receive_method_channel_v2(char *channel, struct platch_obj *object return on_initialize_v2(arg, responsehandle); } else if (raw_std_string_equals(method, "create")) { return on_create_v2(arg, responsehandle); + } else if (raw_std_string_equals(method, "createWithAudio")) { + return on_create_with_audio(arg, responsehandle); } else if (raw_std_string_equals(method, "dispose")) { return on_dispose_v2(arg, responsehandle); } else if (raw_std_string_equals(method, "setLooping")) { From 4d290381beefd72acca56fbf70bfe81d02bde94b Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Mon, 2 Jun 2025 18:22:46 +0200 Subject: [PATCH 25/41] style: make bool flag arguments more readable --- src/plugins/gstplayer.c | 6 ++++-- src/plugins/gstreamer_video_player/plugin.c | 12 ++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/plugins/gstplayer.c b/src/plugins/gstplayer.c index 762fd8c4..6ca23966 100644 --- a/src/plugins/gstplayer.c +++ b/src/plugins/gstplayer.c @@ -1190,6 +1190,8 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo p->tracer = flutterpi_get_tracer(flutterpi); + TRACER_BEGIN(p->tracer, "gstplayer_new()"); + value_notifier_init(&p->video_info_notifier, NULL, free); value_notifier_init(&p->duration_notifier, NULL, free); value_notifier_init(&p->seeking_info_notifier, NULL, free); @@ -1197,8 +1199,6 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo change_notifier_init(&p->error_notifier); change_notifier_init(&p->eos_notifier); - TRACER_BEGIN(p->tracer, "gstplayer_new()"); - // playbin is more reliable for now than playbin3 (see above) p->playbin = gst_element_factory_make("playbin", "playbin"); if (p->playbin == NULL) { @@ -1316,6 +1316,8 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo TRACER_END(p->tracer, "gstplayer_new()"); + LOG_PLAYER_DEBUG(p, "gstplayer_new(\"%s\", %s): %s\n", uri ?: "", play_audio ? "with audio" : "without audio", p->is_live ? "live" : "not live"); + return p; fail_rm_event_source: diff --git a/src/plugins/gstreamer_video_player/plugin.c b/src/plugins/gstreamer_video_player/plugin.c index b1dba592..6b8be89c 100644 --- a/src/plugins/gstreamer_video_player/plugin.c +++ b/src/plugins/gstreamer_video_player/plugin.c @@ -607,7 +607,7 @@ static int on_create(char *channel, struct platch_obj *object, FlutterPlatformMe // create our actual player (this doesn't initialize it) if (asset != NULL) { - player = gstplayer_new_from_asset(flutterpi, asset, package_name, true, false, NULL); + player = gstplayer_new_from_asset(flutterpi, asset, package_name, /* play_video */ true, /* play_audio */ false, NULL); } else { temp = stdmap_get_str(arg, "httpHeaders"); @@ -618,7 +618,7 @@ static int on_create(char *channel, struct platch_obj *object, FlutterPlatformMe return 0; } - player = gstplayer_new_from_network(flutterpi, uri, format_hint, true, false, NULL, headers); + player = gstplayer_new_from_network(flutterpi, uri, format_hint, /* play_video */ true, /* play_audio */ false, NULL, headers); } if (player == NULL) { LOG_ERROR("Couldn't create gstreamer video player.\n"); @@ -1188,14 +1188,14 @@ static int on_create_v2(const struct raw_std_value *arg, FlutterPlatformMessageR // Create our actual player (this doesn't initialize it) if (asset != NULL) { - player = gstplayer_new_from_asset(flutterpi, asset, package_name, true, false, NULL); + player = gstplayer_new_from_asset(flutterpi, asset, package_name, /* play_video */ true, /* play_audio */ false, NULL); // gstplayer_new_from_network will construct a file:// URI out of the // asset path internally. free(asset); asset = NULL; } else if (uri != NULL) { - player = gstplayer_new_from_network(flutterpi, uri, format_hint, true, false, NULL, headers); + player = gstplayer_new_from_network(flutterpi, uri, format_hint, /* play_video */ true, /* play_audio */ false, NULL, headers); // gstplayer_new_from_network will dup the uri internally. free(uri); @@ -1387,14 +1387,14 @@ static int on_create_with_audio(const struct raw_std_value *arg, FlutterPlatform // Create our actual player (this doesn't initialize it) if (asset != NULL) { - player = gstplayer_new_from_asset(flutterpi, asset, package_name, true, true, NULL); + player = gstplayer_new_from_asset(flutterpi, asset, package_name, /* play_video */ true, /* play_audio */ true, NULL); // gstplayer_new_from_network will construct a file:// URI out of the // asset path internally. free(asset); asset = NULL; } else if (uri != NULL) { - player = gstplayer_new_from_network(flutterpi, uri, format_hint, true, true, NULL, headers); + player = gstplayer_new_from_network(flutterpi, uri, format_hint, /* play_video */ true, /* play_audio */ true, NULL, headers); // gstplayer_new_from_network will dup the uri internally. free(uri); From 98215b19a350525114bd227351b55ffe7938c3bf Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Jun 2025 18:56:48 +0200 Subject: [PATCH 26/41] gstplayer, video player: add tracing --- src/plugins/gstplayer.c | 24 ++++++++++- src/plugins/gstreamer_video_player.h | 5 ++- .../flutter_texture_sink.c | 43 ++++++++++++++++--- 3 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/plugins/gstplayer.c b/src/plugins/gstplayer.c index 6ca23966..24962145 100644 --- a/src/plugins/gstplayer.c +++ b/src/plugins/gstplayer.c @@ -447,7 +447,10 @@ static int apply_playback_state(struct gstplayer *player) { TRACER_BEGIN(player->tracer, "apply_playback_state()"); + TRACER_BEGIN(player->tracer, "gst_element_get_state()"); ok = gst_element_get_state(player->pipeline, ¤t_state, &pending_state, 0); + TRACER_END(player->tracer, "gst_element_get_state()"); + if (ok == GST_STATE_CHANGE_FAILURE) { LOG_PLAYER_DEBUG( player, @@ -478,7 +481,10 @@ static int apply_playback_state(struct gstplayer *player) { if (player->has_desired_position) { position = player->desired_position_ms * GST_MSECOND; } else { + TRACER_BEGIN(player->tracer, "gst_element_query_position()"); ok = gst_element_query_position(GST_ELEMENT(player->pipeline), GST_FORMAT_TIME, &position); + TRACER_END(player->tracer, "gst_element_query_position()"); + if (ok == FALSE) { LOG_PLAYER_ERROR(player, "Could not get the current playback position to apply the playback speed.\n"); goto fail_stop_trace; @@ -509,6 +515,7 @@ static int apply_playback_state(struct gstplayer *player) { GST_TIME_ARGS(GST_CLOCK_TIME_NONE) ); + TRACER_BEGIN(player->tracer, "gst_element_seek()"); ok = gst_element_seek( GST_ELEMENT(player->pipeline), desired_rate, @@ -517,6 +524,8 @@ static int apply_playback_state(struct gstplayer *player) { GST_SEEK_TYPE_SET, position, GST_SEEK_TYPE_SET, GST_CLOCK_TIME_NONE ); + TRACER_END(player->tracer, "gst_element_seek()"); + if (ok == FALSE) { LOG_PLAYER_ERROR( player, @@ -534,6 +543,8 @@ static int apply_playback_state(struct gstplayer *player) { GST_TIME_ARGS(0), GST_TIME_ARGS(position) ); + + TRACER_BEGIN(player->tracer, "gst_element_seek()"); ok = gst_element_seek( GST_ELEMENT(player->pipeline), desired_rate, @@ -542,6 +553,7 @@ static int apply_playback_state(struct gstplayer *player) { GST_SEEK_TYPE_SET, 0, GST_SEEK_TYPE_SET, position ); + TRACER_END(player->tracer, "gst_element_seek()"); if (ok == FALSE) { LOG_PLAYER_ERROR( @@ -580,7 +592,9 @@ static int apply_playback_state(struct gstplayer *player) { gst_element_state_get_name(desired_state) ); + TRACER_BEGIN(player->tracer, "gst_element_set_state()"); ok = gst_element_set_state(player->pipeline, desired_state); + TRACER_END(player->tracer, "gst_element_set_state()"); if (ok == GST_STATE_CHANGE_FAILURE) { LOG_GST_SET_STATE_ERROR(player, player->pipeline); @@ -597,7 +611,10 @@ static int apply_playback_state(struct gstplayer *player) { gst_element_state_get_name(desired_state) ); + TRACER_BEGIN(player->tracer, "gst_element_set_state()"); ok = gst_element_set_state(player->pipeline, desired_state); + TRACER_END(player->tracer, "gst_element_set_state()"); + if (ok == GST_STATE_CHANGE_FAILURE) { LOG_GST_SET_STATE_ERROR(player, player->pipeline); goto fail_stop_trace; @@ -1022,7 +1039,10 @@ static int on_bus_fd_ready(sd_event_source *s, int fd, uint32_t revents, void *u GstMessage *msg = gst_bus_pop(gst_element_get_bus(player->pipeline)); if (msg != NULL) { + TRACER_BEGIN(player->tracer, "on_bus_message()"); on_bus_message(player, msg); + TRACER_END(player->tracer, "on_bus_message()"); + gst_message_unref(msg); } @@ -1237,7 +1257,7 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo struct gl_renderer *gl_renderer = flutterpi_get_gl_renderer(flutterpi); - GstElement *sink = flutter_gl_texture_sink_new(p->texture, gl_renderer); + GstElement *sink = flutter_gl_texture_sink_new(p->texture, gl_renderer, p->tracer); if (sink == NULL) { goto fail_destroy_texture; } @@ -1426,7 +1446,7 @@ struct gstplayer *gstplayer_new_from_pipeline(struct flutterpi *flutterpi, const struct gl_renderer *gl_renderer = flutterpi_get_gl_renderer(flutterpi); - if (!flutter_gl_texture_sink_patch(sink, p->texture, gl_renderer)) { + if (!flutter_gl_texture_sink_patch(sink, p->texture, gl_renderer, p->tracer)) { LOG_ERROR("Could not setup appsink.\n"); goto fail_unref_pipeline; } diff --git a/src/plugins/gstreamer_video_player.h b/src/plugins/gstreamer_video_player.h index c30d5e82..4d087b1b 100644 --- a/src/plugins/gstreamer_video_player.h +++ b/src/plugins/gstreamer_video_player.h @@ -66,9 +66,10 @@ const struct gl_texture_frame *frame_get_gl_frame(struct video_frame *frame); struct texture; struct gl_renderer; typedef struct _GstElement GstElement; +struct tracer; -GstElement *flutter_gl_texture_sink_new(struct texture *texture, struct gl_renderer *renderer); +bool flutter_gl_texture_sink_patch(GstElement *element, struct texture *texture, struct gl_renderer *renderer, struct tracer *tracer); -bool flutter_gl_texture_sink_patch(GstElement *element, struct texture *texture, struct gl_renderer *renderer); +GstElement *flutter_gl_texture_sink_new(struct texture *texture, struct gl_renderer *renderer, struct tracer *tracer); #endif diff --git a/src/plugins/gstreamer_video_player/flutter_texture_sink.c b/src/plugins/gstreamer_video_player/flutter_texture_sink.c index bc7f822e..201d9f77 100644 --- a/src/plugins/gstreamer_video_player/flutter_texture_sink.c +++ b/src/plugins/gstreamer_video_player/flutter_texture_sink.c @@ -8,6 +8,7 @@ #include "plugins/gstplayer.h" #include "texture_registry.h" #include "util/logging.h" +#include "tracer.h" #if !defined(HAVE_EGL_GLES2) #error "gstreamer video player requires EGL and OpenGL ES2 support." @@ -19,6 +20,7 @@ struct texture_sink { struct texture *fl_texture; struct frame_interface *interface; + struct tracer *tracer; }; static void on_destroy_texture_frame(const struct texture_frame *texture_frame, void *userdata) { @@ -66,19 +68,27 @@ static GstFlowReturn on_appsink_new_preroll(GstAppSink *appsink, void *userdata) struct texture_sink *meta = userdata; + TRACER_BEGIN(meta->tracer, "on_appsink_new_preroll()"); + + TRACER_BEGIN(meta->tracer, "gst_app_sink_try_pull_preroll()"); sample = gst_app_sink_try_pull_preroll(appsink, 0); + TRACER_END(meta->tracer, "gst_app_sink_try_pull_preroll()"); + if (sample == NULL) { LOG_ERROR("gstreamer returned a NULL sample.\n"); - return GST_FLOW_ERROR; + goto fail_stop_tracing; } + TRACER_BEGIN(meta->tracer, "frame_new()"); // supply video info here frame = frame_new(meta->interface, sample, NULL); + TRACER_END(meta->tracer, "frame_new()"); // the frame has a reference on the sample internally. gst_sample_unref(sample); if (frame != NULL) { + TRACER_BEGIN(meta->tracer, "texture_push_frame()"); texture_push_frame( meta->fl_texture, &(struct texture_frame){ @@ -87,9 +97,15 @@ static GstFlowReturn on_appsink_new_preroll(GstAppSink *appsink, void *userdata) .userdata = frame, } ); + TRACER_END(meta->tracer, "texture_push_frame()"); } + TRACER_END(meta->tracer, "on_appsink_new_preroll()"); return GST_FLOW_OK; + +fail_stop_tracing: + TRACER_END(meta->tracer, "on_appsink_new_preroll()"); + return GST_FLOW_ERROR; } static GstFlowReturn on_appsink_new_sample(GstAppSink *appsink, void *userdata) { @@ -101,19 +117,27 @@ static GstFlowReturn on_appsink_new_sample(GstAppSink *appsink, void *userdata) struct texture_sink *meta = userdata; + TRACER_BEGIN(meta->tracer, "on_appsink_new_sample()"); + + TRACER_BEGIN(meta->tracer, "gst_app_sink_try_pull_sample()"); sample = gst_app_sink_try_pull_sample(appsink, 0); + TRACER_END(meta->tracer, "gst_app_sink_try_pull_sample()"); + if (sample == NULL) { LOG_ERROR("gstreamer returned a NULL sample.\n"); - return GST_FLOW_ERROR; + goto fail_stop_tracing; } + TRACER_BEGIN(meta->tracer, "frame_new()"); // supply video info here frame = frame_new(meta->interface, sample, NULL); + TRACER_END(meta->tracer, "frame_new()"); // the frame has a reference on the sample internally. gst_sample_unref(sample); if (frame != NULL) { + TRACER_BEGIN(meta->tracer, "texture_push_frame()"); texture_push_frame( meta->fl_texture, &(struct texture_frame){ @@ -122,9 +146,15 @@ static GstFlowReturn on_appsink_new_sample(GstAppSink *appsink, void *userdata) .userdata = frame, } ); + TRACER_END(meta->tracer, "texture_push_frame()"); } + TRACER_END(meta->tracer, "on_appsink_new_preroll()"); return GST_FLOW_OK; + +fail_stop_tracing: + TRACER_END(meta->tracer, "on_appsink_new_preroll()"); + return GST_FLOW_ERROR; } static void on_appsink_cbs_destroy(void *userdata) { @@ -136,6 +166,7 @@ static void on_appsink_cbs_destroy(void *userdata) { meta = userdata; // meta->texture is not owned by us. freed by the player + tracer_unref(meta->tracer); frame_interface_unref(meta->interface); free(meta); } @@ -218,7 +249,7 @@ UNUSED static GstPadProbeReturn on_query_appsink_pad(GstPad *pad, GstPadProbeInf return GST_PAD_PROBE_HANDLED; } -bool flutter_gl_texture_sink_patch(GstElement *element, struct texture *texture, struct gl_renderer *renderer) { +bool flutter_gl_texture_sink_patch(GstElement *element, struct texture *texture, struct gl_renderer *renderer, struct tracer *tracer) { ASSERT_NOT_NULL(element); ASSERT_NOT_NULL(texture); ASSERT_NOT_NULL(renderer); @@ -283,6 +314,8 @@ bool flutter_gl_texture_sink_patch(GstElement *element, struct texture *texture, gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_QUERY_DOWNSTREAM, on_query_appsink_pad, NULL, NULL); } + meta->tracer = tracer_ref(tracer); + gst_app_sink_set_callbacks( GST_APP_SINK(appsink), &cbs, @@ -293,7 +326,7 @@ bool flutter_gl_texture_sink_patch(GstElement *element, struct texture *texture, return element; } -GstElement *flutter_gl_texture_sink_new(struct texture *texture, struct gl_renderer *renderer) { +GstElement *flutter_gl_texture_sink_new(struct texture *texture, struct gl_renderer *renderer, struct tracer *tracer) { ASSERT_NOT_NULL(texture); ASSERT_NOT_NULL(renderer); @@ -302,7 +335,7 @@ GstElement *flutter_gl_texture_sink_new(struct texture *texture, struct gl_rende return NULL; } - if (!flutter_gl_texture_sink_patch(element, texture, renderer)) { + if (!flutter_gl_texture_sink_patch(element, texture, renderer, tracer)) { gst_object_unref(element); return NULL; } From 561eb37908de77c0977765408c7a8d9bbedec2df Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 21 Jun 2025 13:13:26 +0200 Subject: [PATCH 27/41] ci: add CodeChecker static analysis workflow Only diagnose flutter-pi sources by default. The diagnosis results are uploaded as an HTML workflow artifact for debugging. Also, remove the old (unused) CodeQL workflow, which had a lot of hard to disable false positives. --- .codechecker.json | 22 ++++ .codechecker.skipfile | 2 + .github/workflows/codeql-buildscript.sh | 25 +++- .github/workflows/codeql.yml | 122 ------------------ .../{fail_on_error.py => fail_on_warning.py} | 15 ++- .github/workflows/static-analysis.yml | 58 +++++++++ .gitignore | 1 + 7 files changed, 114 insertions(+), 131 deletions(-) create mode 100644 .codechecker.json create mode 100644 .codechecker.skipfile mode change 100644 => 100755 .github/workflows/codeql-buildscript.sh delete mode 100644 .github/workflows/codeql.yml rename .github/workflows/{fail_on_error.py => fail_on_warning.py} (68%) create mode 100644 .github/workflows/static-analysis.yml diff --git a/.codechecker.json b/.codechecker.json new file mode 100644 index 00000000..790b678d --- /dev/null +++ b/.codechecker.json @@ -0,0 +1,22 @@ +{ + "analyze": [ + "-d", + "clang-diagnostic-reserved-macro-identifier", + "-d", + "clang-diagnostic-reserved-identifier", + "-d", + "cert-err33-c", + "-d", + "clang-diagnostic-sign-compare", + "-d", + "clang-diagnostic-implicit-int-float-conversion", + "-d", + "clang-diagnostic-switch-enum", + "--analyzers", + "clangsa", + "clang-tidy", + "gcc", + "-i", + ".codechecker.skipfile" + ] +} \ No newline at end of file diff --git a/.codechecker.skipfile b/.codechecker.skipfile new file mode 100644 index 00000000..10051fa6 --- /dev/null +++ b/.codechecker.skipfile @@ -0,0 +1,2 @@ ++*/flutter-pi/src +-* diff --git a/.github/workflows/codeql-buildscript.sh b/.github/workflows/codeql-buildscript.sh old mode 100644 new mode 100755 index 2eff8eef..546c0380 --- a/.github/workflows/codeql-buildscript.sh +++ b/.github/workflows/codeql-buildscript.sh @@ -1,6 +1,23 @@ #!/usr/bin/env bash -sudo apt install -y cmake libgl1-mesa-dev libgles2-mesa-dev libegl1-mesa-dev libdrm-dev libgbm-dev ttf-mscorefonts-installer fontconfig libsystemd-dev libinput-dev libudev-dev libxkbcommon-dev -mkdir build && cd build -cmake .. -make -j`nproc` +# gstreamer and libc++ want different versions of libunwind-dev. +# We explicitly install the version that gstreamer wants so +# we don't get install errors. + +sudo apt-get install -y --no-install-recommends \ + git cmake pkg-config ninja-build clang clang-tools \ + libgl-dev libgles-dev libegl-dev libvulkan-dev libdrm-dev libgbm-dev libsystemd-dev libinput-dev libudev-dev libxkbcommon-dev \ + libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ + libunwind-dev + +$WRAPPER cmake \ + -S . -B build \ + -GNinja \ + -DCMAKE_BUILD_TYPE=Debug \ + -DBUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN=ON \ + -DBUILD_GSTREAMER_AUDIO_PLAYER_PLUGIN=ON \ + -DENABLE_VULKAN=ON \ + -DENABLE_SESSION_SWITCHING=ON \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + +$WRAPPER cmake --build build diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index f40e3ab8..00000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,122 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ "main", "master" ] - schedule: - - cron: '0 0 * * *' - pull_request: - branches: '*' - -jobs: - analyze: - name: Analyze - # Runner size impacts CodeQL analysis time. To learn more, please see: - # - https://gh.io/recommended-hardware-resources-for-running-codeql - # - https://gh.io/supported-runners-and-hardware-resources - # - https://gh.io/using-larger-runners - # Consider using larger runners for possible analysis time improvements. - runs-on: 'ubuntu-24.04' - timeout-minutes: 360 - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'cpp' ] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - queries: security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). - # If this step fails, then you should remove it and run the build manually (see below) - #- name: Autobuild - # uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - - run: | - ./.github/workflows/codeql-buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" - upload: false - id: step1 - - # Filter out rules with low severity or high false positve rate - # Also filter out warnings in third-party code - - name: Filter out unwanted errors and warnings - uses: advanced-security/filter-sarif@v1 - with: - patterns: | - -**:cpp/path-injection - -**:cpp/world-writable-file-creation - -**:cpp/poorly-documented-function - -**:cpp/potentially-dangerous-function - -**:cpp/use-of-goto - -**:cpp/integer-multiplication-cast-to-long - -**:cpp/comparison-with-wider-type - -**:cpp/leap-year/* - -**:cpp/ambiguously-signed-bit-field - -**:cpp/suspicious-pointer-scaling - -**:cpp/suspicious-pointer-scaling-void - -**:cpp/unsigned-comparison-zero - -**/cmake*/Modules/** - input: ${{ steps.step1.outputs.sarif-output }}/cpp.sarif - output: ${{ steps.step1.outputs.sarif-output }}/cpp.sarif - - - name: Upload CodeQL results to code scanning - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: ${{ steps.step1.outputs.sarif-output }} - category: "/language:${{matrix.language}}" - - - name: Upload CodeQL results as an artifact - if: success() || failure() - uses: actions/upload-artifact@v4 - with: - name: codeql-results - path: ${{ steps.step1.outputs.sarif-output }} - retention-days: 5 - - - name: Fail if an error is found - run: | - ./.github/workflows/fail_on_error.py \ - ${{ steps.step1.outputs.sarif-output }}/cpp.sarif diff --git a/.github/workflows/fail_on_error.py b/.github/workflows/fail_on_warning.py similarity index 68% rename from .github/workflows/fail_on_error.py rename to .github/workflows/fail_on_warning.py index 29791742..b6ce953e 100755 --- a/.github/workflows/fail_on_error.py +++ b/.github/workflows/fail_on_warning.py @@ -20,13 +20,18 @@ def codeql_sarif_contain_error(filename): rule_index = res['rule']['index'] else: continue + try: rule_level = rules_metadata[rule_index]['defaultConfiguration']['level'] - except IndexError as e: - print(e, rule_index, len(rules_metadata)) - else: - if rule_level == 'error': - return True + except LookupError: + # According to the SARIF schema (https://www.schemastore.org/schemas/json/sarif-2.1.0-rtm.6.json), + # the defalt level is "warning" if not specified. + rule_level = 'warning' + + if rule_level == 'error': + return True + elif rule_level == 'warning': + return True return False if __name__ == "__main__": diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 00000000..70c90d9d --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,58 @@ +name: "Static Analysis" + +on: + push: + branches: [ "main", "master" ] + schedule: + - cron: '0 0 * * *' + pull_request: + branches: '*' + +jobs: + codechecker: + name: CodeChecker + + # Use latest Ubuntu 24.04 for latest GCC. + # CodeChecker requires gcc >= 13.0.0. + # ubuntu-latest is ubuntu 22.04 (atm) + runs-on: ubuntu-24.04 + + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Deps, Configure and Build + run: | + ./.github/workflows/codeql-buildscript.sh + + - name: Run CodeChecker + uses: ardera/CodeChecker-Action@master + id: codechecker + with: + ctu: true + logfile: ${{ github.workspace }}/build/compile_commands.json + config: ${{ github.workspace }}/.codechecker.json + + - uses: actions/upload-artifact@v4 + id: upload + with: + name: "CodeChecker Bug Reports" + path: ${{ steps.codechecker.outputs.result-html-dir }} + + - name: Fail on Warnings + if: ${{ steps.codechecker.outputs.warnings == 'true' }} + run: | + cat <>$GITHUB_STEP_SUMMARY + ## ⚠️ CodeChecker found warnings + Please see the 'CodeChecker Bug Reports' artifact for more details: + - ${{ steps.upload.outputs.artifact-url }} + EOF + + exit 1 diff --git a/.gitignore b/.gitignore index c84156cd..cac3106c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /.vscode /build /out +/.codechecker # CMake docs says it should not be checked in. CMakeUserPresets.json From 9617e92e9a9358a349c5bd5d7acd8fce839fdd27 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 21 Jun 2025 13:16:58 +0200 Subject: [PATCH 28/41] fix: lots of CodeChecker warnings Mostly double-float casts, functions without arguments without an explicit `void` in the parameter list, but in platformchannel.c there were actually some memory leaks on error paths, which are now fixed. Also enable -pedantic for even more diagnostics. --- CMakeLists.txt | 6 +- CMakePresets.json | 45 +- src/compositor_ng.c | 25 +- src/cursor.c | 16 +- src/cursor.h | 2 +- src/dmabuf_surface.c | 10 +- src/egl.h | 3 +- src/egl_gbm_render_surface.c | 74 +- src/filesystem_layout.c | 9 +- src/flutter-pi.c | 98 ++- src/flutter_embedder.h | 11 + src/gl_renderer.c | 41 +- src/gl_renderer.h | 6 +- src/locales.c | 16 +- src/modesetting.c | 60 +- src/modesetting.h | 4 +- src/notifier_listener.c | 2 +- src/notifier_listener.h | 2 +- src/pixel_format.c | 2 +- src/pixel_format.h | 2 +- src/platformchannel.c | 728 ++++++++++++------ src/platformchannel.h | 5 +- src/pluginregistry.c | 22 +- src/pluginregistry.h | 44 +- src/plugins/gstplayer.c | 7 +- src/plugins/gstreamer_video_player/frame.c | 32 +- src/plugins/gstreamer_video_player/plugin.c | 198 +++-- src/plugins/raw_keyboard.c | 7 +- src/plugins/sentry/sentry.c | 15 +- src/plugins/text_input.c | 23 +- src/texture_registry.c | 8 +- src/tracer.c | 2 +- src/tracer.h | 4 +- src/user_input.c | 27 +- src/util/asserts.h | 13 +- src/util/collection.c | 4 +- src/util/collection.h | 4 +- src/util/lock_ops.h | 14 + src/util/macros.h | 35 +- src/util/uuid.h | 4 +- src/vk_renderer.c | 4 +- src/vk_renderer.h | 2 +- src/vulkan.c | 81 ++ src/vulkan.h | 72 +- src/window.c | 31 +- src/window.h | 2 +- test/flutterpi_test.c | 26 +- test/platformchannel_test.c | 118 +-- .../flutter_embedder.h | 0 .../mesa3d/include/mesa3d}/dynarray.h | 2 - 50 files changed, 1282 insertions(+), 686 deletions(-) create mode 100644 src/flutter_embedder.h create mode 100644 src/vulkan.c rename third_party/flutter_embedder_header/include/{ => flutter_embedder_header}/flutter_embedder.h (100%) rename {src/util => third_party/mesa3d/include/mesa3d}/dynarray.h (99%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8fa47818..b4eba442 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -162,13 +162,14 @@ target_link_libraries(flutterpi_module PUBLIC ) target_include_directories(flutterpi_module PUBLIC - ${CMAKE_SOURCE_DIR}/third_party/flutter_embedder_header/include ${CMAKE_SOURCE_DIR}/src ${CMAKE_BINARY_DIR} + ${CMAKE_SOURCE_DIR}/third_party/mesa3d/include + ${CMAKE_SOURCE_DIR}/third_party/flutter_embedder_header/include ) target_compile_options(flutterpi_module PUBLIC - $<$:-O0 -Wall -Wextra -Wno-sign-compare -Werror -ggdb -U_FORTIFY_SOURCE -DDEBUG> + $<$:-O0 -Wall -Wextra -Wno-sign-compare -Wswitch-enum -Wformat -Wdouble-promotion -Wno-overlength-strings -Wno-gnu-zero-variadic-macro-arguments -pedantic -Werror -ggdb -U_FORTIFY_SOURCE -DDEBUG> $<$:-O3 -Wall -Wextra -Wno-sign-compare -ggdb -DNDEBUG> $<$:-O3 -Wall -Wextra -Wno-sign-compare -DNDEBUG> ) @@ -237,6 +238,7 @@ if (ENABLE_VULKAN) target_sources(flutterpi_module PRIVATE src/vk_gbm_render_surface.c src/vk_renderer.c + src/vulkan.c ) target_link_libraries(flutterpi_module PUBLIC PkgConfig::VULKAN diff --git a/CMakePresets.json b/CMakePresets.json index 866cc51f..31106e29 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -7,15 +7,58 @@ "description": "Sets Ninja generator, build and install directory", "generator": "Ninja", "binaryDir": "${sourceDir}/out/build/${presetName}", + "hidden": true, "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}", + "TRY_ENABLE_OPENGL": false, "ENABLE_OPENGL": true, + "TRY_ENABLE_VULKAN": false, + "ENABLE_VULKAN": true, + "BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN": true, + "TRY_BUILD_GSTREAMER_VIDEO_PLAYER_PLUGIN": false, "BUILD_GSTREAMER_AUDIO_PLAYER_PLUGIN": true, + "TRY_BUILD_GSTREAMER_AUDIO_PLAYER_PLUGIN": false, "BUILD_SENTRY_PLUGIN": true, "ENABLE_TESTS": true } }, + { + "name": "default-clang", + "displayName": "Default OpenGL host build (clang)", + "description": "Sets Ninja generator, build and install directory", + "generator": "Ninja", + "binaryDir": "${sourceDir}/out/build/${presetName}", + "inherits": "default", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "default-clang-20", + "displayName": "Default OpenGL host build (clang-20)", + "description": "Sets Ninja generator, build and install directory", + "generator": "Ninja", + "binaryDir": "${sourceDir}/out/build/${presetName}", + "inherits": "default", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang-20", + "CMAKE_CXX_COMPILER": "clang++-20" + } + }, + { + "name": "default-gcc", + "displayName": "Default OpenGL host build (gcc)", + "description": "Sets Ninja generator, build and install directory", + "generator": "Ninja", + "binaryDir": "${sourceDir}/out/build/${presetName}", + "inherits": "default", + "cacheVariables": { + "CMAKE_C_COMPILER": "gcc", + "CMAKE_CXX_COMPILER": "g++" + } + }, { "name": "cross-aarch64-default", "displayName": "OpenGL AArch64 cross-build", @@ -41,4 +84,4 @@ } } ] -} +} \ No newline at end of file diff --git a/src/compositor_ng.c b/src/compositor_ng.c index d6f6718b..dcbf6b9c 100644 --- a/src/compositor_ng.c +++ b/src/compositor_ng.c @@ -20,6 +20,7 @@ #include #include +#include #include #include "cursor.h" @@ -33,7 +34,6 @@ #include "surface.h" #include "tracer.h" #include "util/collection.h" -#include "util/dynarray.h" #include "util/logging.h" #include "util/refcounting.h" #include "window.h" @@ -231,7 +231,7 @@ static void fill_platform_view_layer_props( size_t n_mutations, const struct mat3f *display_to_view_transform, const struct mat3f *view_to_display_transform, - double device_pixel_ratio + float device_pixel_ratio ) { (void) view_to_display_transform; @@ -262,8 +262,8 @@ static void fill_platform_view_layer_props( * ``` */ - rect.size.x /= device_pixel_ratio; - rect.size.y /= device_pixel_ratio; + rect.size.x /= (double) device_pixel_ratio; + rect.size.y /= (double) device_pixel_ratio; // okay, now we have the params.finalBoundingRect().x() in aa_back_transformed.x and // params.finalBoundingRect().y() in aa_back_transformed.y. @@ -348,8 +348,9 @@ static int compositor_push_fl_layers(struct compositor *compositor, size_t n_fl_ /// TODO: Implement layer->surface = compositor_get_view_by_id_locked(compositor, fl_layer->platform_view->identifier); if (layer->surface == NULL) { - layer->surface = - CAST_SURFACE(dummy_render_surface_new(compositor->tracer, VEC2I(fl_layer->size.width, fl_layer->size.height))); + layer->surface = CAST_SURFACE( + dummy_render_surface_new(compositor->tracer, VEC2I((int) fl_layer->size.width, (int) fl_layer->size.height)) + ); } #else // in release mode, we just assume the id is valid. @@ -384,10 +385,6 @@ static int compositor_push_fl_layers(struct compositor *compositor, size_t n_fl_ fl_layer_composition_unref(composition); - return 0; - - //fail_free_composition: - //fl_layer_composition_unref(composition); return ok; } @@ -556,14 +553,14 @@ void compositor_set_cursor( struct view_geometry viewgeo = window_get_view_geometry(compositor->main_window); - if (compositor->cursor_pos.x < 0.0f) { - compositor->cursor_pos.x = 0.0f; + if (compositor->cursor_pos.x < 0.0) { + compositor->cursor_pos.x = 0.0; } else if (compositor->cursor_pos.x > viewgeo.view_size.x) { compositor->cursor_pos.x = viewgeo.view_size.x; } - if (compositor->cursor_pos.y < 0.0f) { - compositor->cursor_pos.y = 0.0f; + if (compositor->cursor_pos.y < 0.0) { + compositor->cursor_pos.y = 0.0; } else if (compositor->cursor_pos.y > viewgeo.view_size.y) { compositor->cursor_pos.y = viewgeo.view_size.y; } diff --git a/src/cursor.c b/src/cursor.c index 823d5346..94cc56f5 100644 --- a/src/cursor.c +++ b/src/cursor.c @@ -7,12 +7,12 @@ #include "util/collection.h" #include "util/geometry.h" -#define PIXEL_RATIO_LDPI 1.25 -#define PIXEL_RATIO_MDPI 1.6666 -#define PIXEL_RATIO_HDPI 2.5 -#define PIXEL_RATIO_XHDPI 3.3333 -#define PIXEL_RATIO_XXHDPI 5 -#define PIXEL_RATIO_XXXHDPI 6.6666 +#define PIXEL_RATIO_LDPI 1.25f +#define PIXEL_RATIO_MDPI 1.6666f +#define PIXEL_RATIO_HDPI 2.5f +#define PIXEL_RATIO_XHDPI 3.3333f +#define PIXEL_RATIO_XXHDPI 5.0f +#define PIXEL_RATIO_XXXHDPI 6.6666f struct pointer_icon { enum pointer_kind kind; @@ -1450,7 +1450,7 @@ static void run_length_decode(void *image_buf, const void *rle_data, size_t size } } -const struct pointer_icon *pointer_icon_for_details(enum pointer_kind kind, double pixel_ratio) { +const struct pointer_icon *pointer_icon_for_details(enum pointer_kind kind, float pixel_ratio) { const struct pointer_icon *best; best = NULL; @@ -1461,7 +1461,7 @@ const struct pointer_icon *pointer_icon_for_details(enum pointer_kind kind, doub if (best == NULL) { best = icon; continue; - } else if (fabs(pixel_ratio - icon->pixel_ratio) < fabs(pixel_ratio - best->pixel_ratio)) { + } else if (fabsf(pixel_ratio - icon->pixel_ratio) < fabsf(pixel_ratio - best->pixel_ratio)) { best = icon; continue; } diff --git a/src/cursor.h b/src/cursor.h index 270404e7..049b9fb4 100644 --- a/src/cursor.h +++ b/src/cursor.h @@ -56,7 +56,7 @@ enum pointer_kind { struct pointer_icon; -const struct pointer_icon *pointer_icon_for_details(enum pointer_kind kind, double pixel_ratio); +const struct pointer_icon *pointer_icon_for_details(enum pointer_kind kind, float pixel_ratio); enum pointer_kind pointer_icon_get_kind(const struct pointer_icon *icon); diff --git a/src/dmabuf_surface.c b/src/dmabuf_surface.c index e8e3f310..fa4990a8 100644 --- a/src/dmabuf_surface.c +++ b/src/dmabuf_surface.c @@ -68,7 +68,7 @@ void refcounted_dmabuf_destroy(struct refcounted_dmabuf *dmabuf) { free(dmabuf); } -DEFINE_STATIC_REF_OPS(refcounted_dmabuf, n_refs); +DEFINE_STATIC_REF_OPS(refcounted_dmabuf, n_refs) struct dmabuf_surface { struct surface surface; @@ -298,10 +298,10 @@ static int dmabuf_surface_present_kms(struct surface *_s, const struct fl_layer_ .src_w = DOUBLE_TO_FP1616_ROUNDED(s->next_buf->buf.width), .src_h = DOUBLE_TO_FP1616_ROUNDED(s->next_buf->buf.height), - .dst_x = props->aa_rect.offset.x, - .dst_y = props->aa_rect.offset.y, - .dst_w = props->aa_rect.size.x, - .dst_h = props->aa_rect.size.y, + .dst_x = (int) round(props->aa_rect.offset.x), + .dst_y = (int) round(props->aa_rect.offset.y), + .dst_w = (int) round(props->aa_rect.size.x), + .dst_h = (int) round(props->aa_rect.size.y), .has_rotation = false, .rotation = PLANE_TRANSFORM_ROTATE_0, diff --git a/src/egl.h b/src/egl.h index 9a9a936c..ca7b9efe 100644 --- a/src/egl.h +++ b/src/egl.h @@ -475,7 +475,8 @@ static inline const char *egl_strerror(EGLenum result) { } } - #define LOG_EGL_ERROR(result, fmt, ...) LOG_ERROR(fmt ": %s\n", __VA_ARGS__ egl_strerror(result)) + #define LOG_EGL_ERROR_FMT(result, fmt, ...) LOG_ERROR(fmt ": %s\n", __VA_ARGS__ egl_strerror(result)) + #define LOG_EGL_ERROR(result, fmt) LOG_ERROR(fmt ": %s\n", egl_strerror(result)) #endif #endif // _FLUTTERPI_SRC_EGL_H diff --git a/src/egl_gbm_render_surface.c b/src/egl_gbm_render_surface.c index 30484b45..74b0ae01 100644 --- a/src/egl_gbm_render_surface.c +++ b/src/egl_gbm_render_surface.c @@ -146,6 +146,7 @@ static int egl_gbm_render_surface_init( } #endif + int with_modifiers_errno = 0; gbm_surface = NULL; if (allowed_modifiers != NULL) { gbm_surface = gbm_surface_create_with_modifiers( @@ -157,11 +158,10 @@ static int egl_gbm_render_surface_init( n_allowed_modifiers ); if (gbm_surface == NULL) { - ok = errno; - LOG_ERROR("Couldn't create GBM surface for rendering. gbm_surface_create_with_modifiers: %s\n", strerror(ok)); - LOG_ERROR("Will retry without modifiers\n"); + with_modifiers_errno = errno; } } + if (gbm_surface == NULL) { gbm_surface = gbm_surface_create( gbm_device, @@ -172,8 +172,20 @@ static int egl_gbm_render_surface_init( ); if (gbm_surface == NULL) { ok = errno; - LOG_ERROR("Couldn't create GBM surface for rendering. gbm_surface_create: %s\n", strerror(ok)); - return ok; + + if (allowed_modifiers != NULL) { + LOG_ERROR( + "Couldn't create GBM surface for rendering. gbm_surface_create_with_modifiers: %s, gbm_surface_create: %s\n", + strerror(with_modifiers_errno), + strerror(ok) + ); + } else { + LOG_ERROR("Couldn't create GBM surface for rendering. gbm_surface_create: %s\n", strerror(ok)); + } + + // Return an error != 0 in any case, so the caller doesn't think + // that the surface was created successfully. + return ok ? ok : EIO; } } @@ -383,10 +395,8 @@ static void on_release_layer(void *userdata) { static int egl_gbm_render_surface_present_kms(struct surface *s, const struct fl_layer_props *props, struct kms_req_builder *builder) { struct egl_gbm_render_surface *egl_surface; struct gbm_bo_meta *meta; - struct drmdev *drmdev; struct gbm_bo *bo; enum pixfmt pixel_format; - uint32_t fb_id, opaque_fb_id; int ok; egl_surface = CAST_THIS(s); @@ -410,16 +420,18 @@ static int egl_gbm_render_surface_present_kms(struct surface *s, const struct fl goto fail_unlock; } - drmdev = kms_req_builder_get_drmdev(builder); - ASSERT_NOT_NULL(drmdev); + meta->drmdev = kms_req_builder_get_drmdev(builder); + ASSERT_NOT_NULL(meta->drmdev); + + drmdev_ref(meta->drmdev); struct drm_crtc *crtc = kms_req_builder_get_crtc(builder); ASSERT_NOT_NULL(crtc); - if (drm_crtc_any_plane_supports_format(drmdev, crtc, egl_surface->pixel_format)) { + if (drm_crtc_any_plane_supports_format(meta->drmdev, crtc, egl_surface->pixel_format)) { TRACER_BEGIN(egl_surface->surface.tracer, "drmdev_add_fb (non-opaque)"); - fb_id = drmdev_add_fb_from_gbm_bo( - drmdev, + uint32_t fb_id = drmdev_add_fb_from_gbm_bo( + meta->drmdev, bo, /* cast_opaque */ false ); @@ -428,7 +440,7 @@ static int egl_gbm_render_surface_present_kms(struct surface *s, const struct fl if (fb_id == 0) { ok = EIO; LOG_ERROR("Couldn't add GBM buffer as DRM framebuffer.\n"); - goto fail_free_meta; + goto fail_unref_drmdev; } meta->has_nonopaque_fb_id = true; @@ -441,16 +453,16 @@ static int egl_gbm_render_surface_present_kms(struct surface *s, const struct fl // if this EGL surface is non-opaque and has an opaque equivalent if (!get_pixfmt_info(egl_surface->pixel_format)->is_opaque && pixfmt_opaque(egl_surface->pixel_format) != egl_surface->pixel_format && - drm_crtc_any_plane_supports_format(drmdev, crtc, pixfmt_opaque(egl_surface->pixel_format))) { - opaque_fb_id = drmdev_add_fb_from_gbm_bo( - drmdev, + drm_crtc_any_plane_supports_format(meta->drmdev, crtc, pixfmt_opaque(egl_surface->pixel_format))) { + uint32_t opaque_fb_id = drmdev_add_fb_from_gbm_bo( + meta->drmdev, bo, /* cast_opaque */ true ); if (opaque_fb_id == 0) { ok = EIO; LOG_ERROR("Couldn't add GBM buffer as opaque DRM framebuffer.\n"); - goto fail_remove_fb; + goto fail_rm_nonopaque_fb; } meta->has_opaque_fb_id = true; @@ -463,11 +475,9 @@ static int egl_gbm_render_surface_present_kms(struct surface *s, const struct fl if (!meta->has_nonopaque_fb_id && !meta->has_opaque_fb_id) { ok = EIO; LOG_ERROR("Couldn't add GBM buffer as DRM framebuffer.\n"); - goto fail_free_meta; + goto fail_remove_opaque_fb; } - meta->drmdev = drmdev_ref(drmdev); - meta->nonopaque_fb_id = fb_id; gbm_bo_set_user_data(bo, meta, on_destroy_gbm_bo_meta); } else { // We can only add this GBM BO to a single KMS device as an fb right now. @@ -493,6 +503,8 @@ static int egl_gbm_render_surface_present_kms(struct surface *s, const struct fl ); */ + uint32_t fb_id; + // So we just cast our fb to an XRGB8888 framebuffer and scanout that instead. if (meta->has_nonopaque_fb_id && !meta->has_opaque_fb_id) { fb_id = meta->nonopaque_fb_id; @@ -555,10 +567,18 @@ static int egl_gbm_render_surface_present_kms(struct surface *s, const struct fl locked_fb_unref(egl_surface->locked_front_fb); goto fail_unlock; -fail_remove_fb: - drmdev_rm_fb(drmdev, fb_id); +fail_remove_opaque_fb: + if (meta->has_opaque_fb_id) { + drmdev_rm_fb(meta->drmdev, meta->opaque_fb_id); + } + +fail_rm_nonopaque_fb: + if (meta->has_nonopaque_fb_id) { + drmdev_rm_fb(meta->drmdev, meta->nonopaque_fb_id); + } -fail_free_meta: +fail_unref_drmdev: + drmdev_unref(meta->drmdev); free(meta); fail_unlock: @@ -647,10 +667,10 @@ static int egl_gbm_render_surface_queue_present(struct render_surface *s, const LOG_DEBUG( "using fourcc %c%c%c%c (%s) with modifier 0x%" PRIx64 "\n", - fourcc & 0xFF, - (fourcc >> 8) & 0xFF, - (fourcc >> 16) & 0xFF, - (fourcc >> 24) & 0xFF, + (char) (fourcc & 0xFF), + (char) ((fourcc >> 8) & 0xFF), + (char) ((fourcc >> 16) & 0xFF), + (char) ((fourcc >> 24) & 0xFF), has_format ? get_pixfmt_info(format)->name : "?", modifier ); diff --git a/src/filesystem_layout.c b/src/filesystem_layout.c index 39924c30..4f73cb89 100644 --- a/src/filesystem_layout.c +++ b/src/filesystem_layout.c @@ -135,16 +135,14 @@ static struct flutter_paths *resolve( // We still haven't found it. Fail because we need it to run flutter. if (path_exists(icudtl_path) == false) { LOG_DEBUG("icudtl file not found at %s.\n", icudtl_path); - free(icudtl_path); - LOG_ERROR("icudtl file not found!\n"); - goto fail_free_asset_bundle_path; + goto fail_free_icudtl_path; } // Find the kernel_blob.bin file. Only necessary for JIT (debug) mode. ok = asprintf(&kernel_blob_path, "%s/%s", app_bundle_path_real, kernel_blob_subpath); if (ok == -1) { - goto fail_free_asset_bundle_path; + goto fail_free_icudtl_path; } if (FLUTTER_RUNTIME_MODE_IS_JIT(runtime_mode) && !path_exists(kernel_blob_path)) { @@ -222,6 +220,9 @@ static struct flutter_paths *resolve( fail_free_kernel_blob_path: free(kernel_blob_path); +fail_free_icudtl_path: + free(icudtl_path); + fail_free_asset_bundle_path: free(asset_bundle_path); diff --git a/src/flutter-pi.c b/src/flutter-pi.c index eacd80e9..afbafb6e 100644 --- a/src/flutter-pi.c +++ b/src/flutter-pi.c @@ -394,7 +394,8 @@ static void *proc_resolver(void *userdata, const char *name) { flutterpi = userdata; ASSERT_NOT_NULL(flutterpi->gl_renderer); - return gl_renderer_get_proc_address(flutterpi->gl_renderer, name); + fn_ptr_t fn = gl_renderer_get_proc_address(flutterpi->gl_renderer, name); + return *((void **) &fn); } #endif @@ -408,7 +409,8 @@ UNUSED static void *on_get_vulkan_proc_address(void *userdata, FlutterVulkanInst name = "vkGetInstanceProcAddr"; } - return (void *) vkGetInstanceProcAddr((VkInstance) instance, name); + PFN_vkVoidFunction fn = vkGetInstanceProcAddr((VkInstance) instance, name); + return *(void **) (&fn); #else (void) userdata; (void) instance; @@ -546,7 +548,7 @@ UNUSED static void on_frame_request(void *userdata, intptr_t baton) { req->flutterpi = flutterpi; req->baton = baton; req->vblank_ns = get_monotonic_time(); - req->next_vblank_ns = req->vblank_ns + (1000000000.0 / compositor_get_refresh_rate(flutterpi->compositor)); + req->next_vblank_ns = req->vblank_ns + (uint64_t) (1000000000.0 / compositor_get_refresh_rate(flutterpi->compositor)); if (flutterpi_runs_platform_tasks_on_current_thread(req->flutterpi)) { TRACER_INSTANT(req->flutterpi->tracer, "FlutterEngineOnVsync"); @@ -768,7 +770,7 @@ static int on_execute_flutter_task(void *userdata) { result = flutterpi->flutter.procs.RunTask(flutterpi->flutter.engine, task); if (result != kSuccess) { - LOG_ERROR("Error running platform task. FlutterEngineRunTask: %d\n", result); + LOG_ERROR("Error running platform task. FlutterEngineRunTask: %u\n", result); free(task); return EINVAL; } @@ -1045,7 +1047,7 @@ struct tracer *flutterpi_get_tracer(struct flutterpi *flutterpi) { } void flutterpi_set_pointer_kind(struct flutterpi *flutterpi, enum pointer_kind kind) { - return compositor_set_cursor(flutterpi->compositor, false, false, true, kind, false, VEC2F(0, 0)); + compositor_set_cursor(flutterpi->compositor, false, false, true, kind, false, VEC2F(0, 0)); } void flutterpi_trace_event_instant(struct flutterpi *flutterpi, const char *name) { @@ -1171,19 +1173,20 @@ static void unload_flutter_engine_lib(void *handle) { dlclose(handle); } -static int get_flutter_engine_procs(void *engine_handle, FlutterEngineProcTable *procs_out) { - // clang-format off - FlutterEngineResult (*get_proc_addresses)(FlutterEngineProcTable *table); - // clang-format on +typedef FlutterEngineResult (*flutter_engine_get_proc_addresses_t)(FlutterEngineProcTable *table); +static int get_flutter_engine_procs(void *engine_handle, FlutterEngineProcTable *procs_out) { FlutterEngineResult engine_result; - get_proc_addresses = dlsym(engine_handle, "FlutterEngineGetProcAddresses"); - if (get_proc_addresses == NULL) { + void *fn = dlsym(engine_handle, "FlutterEngineGetProcAddresses"); + if (fn == NULL) { LOG_ERROR("Could not resolve flutter engine function FlutterEngineGetProcAddresses.\n"); return EINVAL; } + flutter_engine_get_proc_addresses_t get_proc_addresses; + *((void **) &get_proc_addresses) = fn; + procs_out->struct_size = sizeof(FlutterEngineProcTable); engine_result = get_proc_addresses(procs_out); if (engine_result != kSuccess) { @@ -1456,9 +1459,9 @@ static int flutterpi_run(struct flutterpi *flutterpi) { memset(&window_metrics_event, 0, sizeof(window_metrics_event)); window_metrics_event.struct_size = sizeof(FlutterWindowMetricsEvent); - window_metrics_event.width = geometry.view_size.x; - window_metrics_event.height = geometry.view_size.y; - window_metrics_event.pixel_ratio = geometry.device_pixel_ratio; + window_metrics_event.width = (size_t) geometry.view_size.x; + window_metrics_event.height = (size_t) geometry.view_size.y; + window_metrics_event.pixel_ratio = (double) geometry.device_pixel_ratio; window_metrics_event.left = 0; window_metrics_event.top = 0; window_metrics_event.physical_view_inset_top = 0; @@ -1925,7 +1928,7 @@ bool flutterpi_parse_cmdline_args(int argc, char **argv, struct flutterpi_cmdlin "%s", usage ); - return false; + goto fail; } break; @@ -1939,7 +1942,7 @@ bool flutterpi_parse_cmdline_args(int argc, char **argv, struct flutterpi_cmdlin "%s", usage ); - return false; + goto fail; } result_out->rotation = rotation; @@ -1950,13 +1953,13 @@ bool flutterpi_parse_cmdline_args(int argc, char **argv, struct flutterpi_cmdlin ok = parse_vec2i(optarg, &result_out->physical_dimensions); if (!ok) { LOG_ERROR("ERROR: Invalid argument for --dimensions passed.\n"); - return false; + goto fail; } if (result_out->physical_dimensions.x < 0 || result_out->physical_dimensions.y < 0) { LOG_ERROR("ERROR: Invalid argument for --dimensions passed.\n"); result_out->physical_dimensions = VEC2I(0, 0); - return false; + goto fail; } result_out->has_physical_dimensions = true; @@ -1979,7 +1982,7 @@ bool flutterpi_parse_cmdline_args(int argc, char **argv, struct flutterpi_cmdlin "%s", usage ); - return false; + goto fail; valid_format: break; @@ -1987,7 +1990,11 @@ bool flutterpi_parse_cmdline_args(int argc, char **argv, struct flutterpi_cmdlin case 'v':; char *vmode_dup = strdup(optarg); if (vmode_dup == NULL) { - return false; + goto fail; + } + + if (result_out->desired_videomode != NULL) { + free(result_out->desired_videomode); } result_out->desired_videomode = vmode_dup; @@ -1997,15 +2004,15 @@ bool flutterpi_parse_cmdline_args(int argc, char **argv, struct flutterpi_cmdlin ok = parse_vec2i(optarg, &result_out->dummy_display_size); if (!ok) { LOG_ERROR("ERROR: Invalid argument for --dummy-display-size passed.\n"); - return false; + goto fail; } break; - case 'h': printf("%s", usage); return false; + case 'h': printf("%s", usage); goto fail; case '?': - case ':': LOG_ERROR("Invalid option specified.\n%s", usage); return false; + case ':': LOG_ERROR("Invalid option specified.\n%s", usage); goto fail; case -1: finished_parsing_options = true; break; @@ -2016,7 +2023,7 @@ bool flutterpi_parse_cmdline_args(int argc, char **argv, struct flutterpi_cmdlin if (optind >= argc) { LOG_ERROR("ERROR: Expected asset bundle path after options.\n"); printf("%s", usage); - return false; + goto fail; } result_out->bundle_path = strdup(argv[optind]); @@ -2032,6 +2039,17 @@ bool flutterpi_parse_cmdline_args(int argc, char **argv, struct flutterpi_cmdlin result_out->dummy_display = !!dummy_display_int; return true; + +fail: + if (result_out->bundle_path != NULL) { + free(result_out->bundle_path); + } + + if (result_out->desired_videomode != NULL) { + free(result_out->desired_videomode); + } + + return false; } static int on_drmdev_open(const char *path, int flags, void **fd_metadata_out, void *userdata) { @@ -2114,7 +2132,7 @@ static struct drmdev *find_drmdev(struct libseat *libseat) { ASSERT_EQUALS(libseat, NULL); #endif - ok = drmGetDevices2(0, devices, sizeof(devices) / sizeof(*devices)); + ok = drmGetDevices2(0, devices, ARRAY_SIZE(devices)); if (ok < 0) { LOG_ERROR("Could not query DRM device list: %s\n", strerror(-ok)); return NULL; @@ -2171,12 +2189,12 @@ static struct drmdev *find_drmdev(struct libseat *libseat) { return NULL; } -static struct gbm_device *open_rendernode_as_gbm_device() { +static struct gbm_device *open_rendernode_as_gbm_device(void) { struct gbm_device *gbm; drmDevicePtr devices[64]; int ok, n_devices; - ok = drmGetDevices2(0, devices, sizeof(devices) / sizeof(*devices)); + ok = drmGetDevices2(0, devices, ARRAY_SIZE(devices)); if (ok < 0) { LOG_ERROR("Could not query DRM device list: %s\n", strerror(-ok)); return NULL; @@ -2324,7 +2342,7 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { struct tracer *tracer; struct window *window; void *engine_handle; - char *bundle_path, **engine_argv, *desired_videomode; + char **engine_argv, *desired_videomode; int ok, engine_argc, wakeup_fd; fpi = malloc(sizeof *fpi); @@ -2344,15 +2362,14 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { if (cmd_args.use_vulkan == true) { LOG_ERROR("ERROR: --vulkan was specified, but flutter-pi was built without vulkan support.\n"); printf("%s", usage); - return NULL; + goto fail_free_cmd_args; } #endif runtime_mode = cmd_args.has_runtime_mode ? cmd_args.runtime_mode : FLUTTER_RUNTIME_MODE_DEBUG; - bundle_path = cmd_args.bundle_path; + engine_argc = cmd_args.engine_argc; engine_argv = cmd_args.engine_argv; - #if defined(HAVE_EGL_GLES2) && defined(HAVE_VULKAN) renderer_type = cmd_args.use_vulkan ? kVulkan_RendererType : kOpenGL_RendererType; #elif defined(HAVE_EGL_GLES2) && !defined(HAVE_VULKAN) @@ -2366,16 +2383,13 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { desired_videomode = cmd_args.desired_videomode; - if (bundle_path == NULL) { - LOG_ERROR("ERROR: Bundle path does not exist.\n"); - goto fail_free_cmd_args; - } - - paths = setup_paths(runtime_mode, bundle_path); + paths = setup_paths(runtime_mode, cmd_args.bundle_path); if (paths == NULL) { goto fail_free_cmd_args; } + fpi->flutter.bundle_path = realpath(cmd_args.bundle_path, NULL); + wakeup_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); if (wakeup_fd < 0) { LOG_ERROR("Could not create fd for waking up the main loop. eventfd: %s\n", strerror(errno)); @@ -2483,7 +2497,6 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { vk_renderer = vk_renderer_new(); if (vk_renderer == NULL) { LOG_ERROR("Couldn't create vulkan renderer.\n"); - ok = EIO; goto fail_unref_scheduler; } #else @@ -2495,7 +2508,6 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { gl_renderer = gl_renderer_new_from_gbm_device(tracer, gbm_device, cmd_args.has_pixel_format, cmd_args.pixel_format); if (gl_renderer == NULL) { LOG_ERROR("Couldn't create EGL/OpenGL renderer.\n"); - ok = EIO; goto fail_unref_scheduler; } @@ -2598,8 +2610,8 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { fpi, &geometry.display_to_view_transform, &geometry.view_to_display_transform, - geometry.display_size.x, - geometry.display_size.y + (unsigned int) geometry.display_size.x, + (unsigned int) geometry.display_size.y ); if (input == NULL) { LOG_ERROR("Couldn't initialize user input. flutter-pi will run without user input.\n"); @@ -2701,6 +2713,8 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { frame_scheduler_unref(scheduler); window_unref(window); + free(cmd_args.bundle_path); + pthread_mutex_init(&fpi->event_loop_mutex, get_default_mutex_attrs()); fpi->event_loop_thread = pthread_self(); fpi->wakeup_event_loop_fd = wakeup_fd; @@ -2712,7 +2726,6 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { fpi->vk_renderer = vk_renderer; fpi->user_input = input; fpi->flutter.runtime_mode = runtime_mode; - fpi->flutter.bundle_path = realpath(bundle_path, NULL); fpi->flutter.engine_argc = engine_argc; fpi->flutter.engine_argv = engine_argv; fpi->flutter.paths = paths; @@ -2790,6 +2803,7 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { fail_free_cmd_args: free(cmd_args.bundle_path); + free(cmd_args.desired_videomode); fail_free_fpi: free(fpi); diff --git a/src/flutter_embedder.h b/src/flutter_embedder.h new file mode 100644 index 00000000..b5fbe6a7 --- /dev/null +++ b/src/flutter_embedder.h @@ -0,0 +1,11 @@ +#ifndef _FLUTTERPI_SRC_FLUTTER_EMBEDDER_H +#define _FLUTTERPI_SRC_FLUTTER_EMBEDDER_H + +#include "util/macros.h" + +PRAGMA_DIAGNOSTIC_PUSH +PRAGMA_DIAGNOSTIC_IGNORED("-Wstrict-prototypes") +#include "flutter_embedder_header/flutter_embedder.h" +PRAGMA_DIAGNOSTIC_POP + +#endif // _FLUTTERPI_SRC_FLUTTER_EMBEDDER_H diff --git a/src/gl_renderer.c b/src/gl_renderer.c index 58b25ea2..a151310e 100644 --- a/src/gl_renderer.c +++ b/src/gl_renderer.c @@ -62,24 +62,23 @@ struct gl_renderer { #endif }; -static void *try_get_proc_address(const char *name) { - void *address; - - address = eglGetProcAddress(name); - if (address) { - return address; +static fn_ptr_t try_get_proc_address(const char *name) { + fn_ptr_t fn = eglGetProcAddress(name); + if (fn) { + return fn; } - address = dlsym(RTLD_DEFAULT, name); - if (address) { - return address; + void *void_fn = dlsym(RTLD_DEFAULT, name); + if (void_fn) { + *((void **) &fn) = void_fn; + return fn; } - return NULL; + return (fn_ptr_t) NULL; } -static void *get_proc_address(const char *name) { - void *address; +static fn_ptr_t get_proc_address(const char *name) { + fn_ptr_t address; address = try_get_proc_address(name); if (address == NULL) { @@ -177,13 +176,13 @@ struct gl_renderer *gl_renderer_new_from_gbm_device( // PFNEGLGETPLATFORMDISPLAYEXTPROC, PFNEGLCREATEPLATFORMWINDOWSURFACEEXTPROC // are defined by EGL_EXT_platform_base. #ifdef EGL_EXT_platform_base - PFNEGLGETPLATFORMDISPLAYEXTPROC egl_get_platform_display_ext; - PFNEGLCREATEPLATFORMWINDOWSURFACEEXTPROC egl_create_platform_window_surface_ext; + PFNEGLGETPLATFORMDISPLAYEXTPROC egl_get_platform_display_ext = NULL; + PFNEGLCREATEPLATFORMWINDOWSURFACEEXTPROC egl_create_platform_window_surface_ext = NULL; #endif if (supports_egl_ext_platform_base) { #ifdef EGL_EXT_platform_base - egl_get_platform_display_ext = try_get_proc_address("eglGetPlatformDisplayEXT"); + egl_get_platform_display_ext = (PFNEGLGETPLATFORMDISPLAYEXTPROC) try_get_proc_address("eglGetPlatformDisplayEXT"); if (egl_get_platform_display_ext == NULL) { LOG_ERROR("Couldn't resolve \"eglGetPlatformDisplayEXT\" even though \"EGL_EXT_platform_base\" was listed as supported.\n"); supports_egl_ext_platform_base = false; @@ -195,7 +194,8 @@ struct gl_renderer *gl_renderer_new_from_gbm_device( if (supports_egl_ext_platform_base) { #ifdef EGL_EXT_platform_base - egl_create_platform_window_surface_ext = try_get_proc_address("eglCreatePlatformWindowSurfaceEXT"); + egl_create_platform_window_surface_ext = (PFNEGLCREATEPLATFORMWINDOWSURFACEEXTPROC + ) try_get_proc_address("eglCreatePlatformWindowSurfaceEXT"); if (egl_create_platform_window_surface_ext == NULL) { LOG_ERROR( "Couldn't resolve \"eglCreatePlatformWindowSurfaceEXT\" even though \"EGL_EXT_platform_base\" was listed as supported.\n" @@ -217,8 +217,9 @@ struct gl_renderer *gl_renderer_new_from_gbm_device( bool failed_before = false; #ifdef EGL_VERSION_1_5 - PFNEGLGETPLATFORMDISPLAYPROC egl_get_platform_display = try_get_proc_address("eglGetPlatformDisplay"); - PFNEGLCREATEPLATFORMWINDOWSURFACEPROC egl_create_platform_window_surface = try_get_proc_address("eglCreatePlatformWindowSurface"); + PFNEGLGETPLATFORMDISPLAYPROC egl_get_platform_display = (PFNEGLGETPLATFORMDISPLAYPROC) try_get_proc_address("eglGetPlatformDisplay"); + PFNEGLCREATEPLATFORMWINDOWSURFACEPROC egl_create_platform_window_surface = (PFNEGLCREATEPLATFORMWINDOWSURFACEPROC + ) try_get_proc_address("eglCreatePlatformWindowSurface"); if (egl_display == EGL_NO_DISPLAY && egl_get_platform_display != NULL) { egl_display = egl_get_platform_display(EGL_PLATFORM_GBM_KHR, gbm_device, NULL); @@ -550,7 +551,7 @@ int gl_renderer_clear_current(struct gl_renderer *renderer) { return 0; } -void *gl_renderer_get_proc_address(ASSERTED struct gl_renderer *renderer, const char *name) { +fn_ptr_t gl_renderer_get_proc_address(ASSERTED struct gl_renderer *renderer, const char *name) { ASSERT_NOT_NULL(renderer); ASSERT_NOT_NULL(name); return get_proc_address(name); @@ -628,7 +629,7 @@ int gl_renderer_make_this_a_render_thread(struct gl_renderer *renderer) { return 0; } -void gl_renderer_cleanup_this_render_thread() { +void gl_renderer_cleanup_this_render_thread(void) { EGLDisplay display; EGLContext context; EGLBoolean egl_ok; diff --git a/src/gl_renderer.h b/src/gl_renderer.h index d6c8160a..6afcd577 100644 --- a/src/gl_renderer.h +++ b/src/gl_renderer.h @@ -26,6 +26,8 @@ #include "egl.h" +typedef void (*fn_ptr_t)(void); + struct tracer; struct gl_renderer *gl_renderer_new_from_gbm_device( @@ -59,7 +61,7 @@ int gl_renderer_clear_current(struct gl_renderer *renderer); EGLContext gl_renderer_create_context(struct gl_renderer *renderer); -void *gl_renderer_get_proc_address(struct gl_renderer *renderer, const char *name); +fn_ptr_t gl_renderer_get_proc_address(struct gl_renderer *renderer, const char *name); EGLDisplay gl_renderer_get_egl_display(struct gl_renderer *renderer); @@ -71,7 +73,7 @@ bool gl_renderer_is_llvmpipe(struct gl_renderer *renderer); int gl_renderer_make_this_a_render_thread(struct gl_renderer *renderer); -void gl_renderer_cleanup_this_render_thread(); +void gl_renderer_cleanup_this_render_thread(void); ATTR_PURE EGLConfig gl_renderer_choose_config(struct gl_renderer *renderer, bool has_desired_pixel_format, enum pixfmt desired_pixel_format); diff --git a/src/locales.c b/src/locales.c index ff66a098..7f13b1a6 100644 --- a/src/locales.c +++ b/src/locales.c @@ -220,7 +220,7 @@ static int add_locale_variants(struct list_head *locales, const char *locale_des } // then append all possible combinations - for (int i = 0b111; i >= 0; i--) { + for (int i = 7; i >= 0; i--) { char *territory_2 = NULL, *codeset_2 = NULL, *modifier_2 = NULL; if ((i & 1) != 0) { @@ -311,7 +311,7 @@ struct locales *locales_new(void) { // Use those to create our flutter locales. n_locales = list_length(&locales->locales); - fl_locales = calloc(n_locales, sizeof *fl_locales); + fl_locales = calloc(n_locales == 0 ? 1 : n_locales, sizeof(const FlutterLocale *)); if (fl_locales == NULL) { goto fail_free_allocated_locales; } @@ -322,6 +322,18 @@ struct locales *locales_new(void) { i++; } + // If we have no locales, add a default "C" locale. + if (i == 0) { + fl_locales[0] = &(const FlutterLocale){ + .struct_size = sizeof(FlutterLocale), + .language_code = "C", + .country_code = NULL, + .script_code = NULL, + .variant_code = NULL, + }; + i++; + } + if (streq(fl_locales[0]->language_code, "C")) { LOG_LOCALES_ERROR("Warning: The system has no configured locale. The default \"C\" locale may or may not be supported by the app.\n" ); diff --git a/src/modesetting.c b/src/modesetting.c index 892c2973..80508a1b 100644 --- a/src/modesetting.c +++ b/src/modesetting.c @@ -417,8 +417,10 @@ static int fetch_crtc(int drm_fd, int crtc_index, uint32_t crtc_id, struct drm_c prop_info = NULL; } + ASSUME(0 <= crtc_index && crtc_index < 32); + crtc_out->id = crtc->crtc_id; - crtc_out->index = crtc_index; + crtc_out->index = (uint8_t) crtc_index; crtc_out->bitmask = 1u << crtc_index; crtc_out->ids = ids; crtc_out->committed_state.has_mode = crtc->mode_valid; @@ -610,15 +612,16 @@ extern void drmModeFreeFB2(struct _drmModeFB2 *ptr) __attribute__((weak)); static int fetch_plane(int drm_fd, uint32_t plane_id, struct drm_plane *plane_out) { struct drm_plane_prop_ids ids; drmModeObjectProperties *props; - drm_plane_transform_t hardcoded_rotation, supported_rotations, committed_rotation; - enum drm_blend_mode committed_blend_mode; - enum drm_plane_type type; + drm_plane_transform_t hardcoded_rotation = PLANE_TRANSFORM_NONE, supported_rotations = PLANE_TRANSFORM_NONE, + committed_rotation = PLANE_TRANSFORM_NONE; + enum drm_blend_mode committed_blend_mode = kNone_DrmBlendMode; + enum drm_plane_type type = kPrimary_DrmPlaneType; drmModePropertyRes *info; drmModePlane *plane; uint32_t comitted_crtc_x, comitted_crtc_y, comitted_crtc_w, comitted_crtc_h; uint32_t comitted_src_x, comitted_src_y, comitted_src_w, comitted_src_h; - uint16_t committed_alpha; - int64_t min_zpos, max_zpos, hardcoded_zpos, committed_zpos; + uint16_t committed_alpha = 0; + int64_t min_zpos = 0, max_zpos = 0, hardcoded_zpos = 0, committed_zpos = 0; bool supported_blend_modes[kCount_DrmBlendMode] = { 0 }; bool supported_formats[PIXFMT_COUNT] = { 0 }; bool has_type, has_rotation, has_zpos, has_hardcoded_zpos, has_hardcoded_rotation, has_alpha, has_blend_mode; @@ -848,12 +851,12 @@ static int fetch_plane(int drm_fd, uint32_t plane_id, struct drm_plane *plane_ou plane_out->id = plane->plane_id; plane_out->possible_crtcs = plane->possible_crtcs; plane_out->ids = ids; - plane_out->type = type; + plane_out->type = has_type ? type : kPrimary_DrmPlaneType; plane_out->has_zpos = has_zpos; - plane_out->min_zpos = min_zpos; - plane_out->max_zpos = max_zpos; + plane_out->min_zpos = has_zpos ? min_zpos : 0; + plane_out->max_zpos = has_zpos ? max_zpos : 0; plane_out->has_hardcoded_zpos = has_hardcoded_zpos; - plane_out->hardcoded_zpos = hardcoded_zpos; + plane_out->hardcoded_zpos = has_hardcoded_zpos ? hardcoded_zpos : 0; plane_out->has_rotation = has_rotation; plane_out->supported_rotations = supported_rotations; plane_out->has_hardcoded_rotation = has_hardcoded_rotation; @@ -914,7 +917,7 @@ static int fetch_planes(struct drmdev *drmdev, struct drm_plane **planes_out, si ok = fetch_plane(drmdev->fd, drmdev->plane_res->planes[i], planes + i); if (ok != 0) { for (int j = 0; j < i; j++) { - free_plane(planes + i); + free_plane(planes + j); } free(planes); return ENOMEM; @@ -936,7 +939,7 @@ static void free_planes(struct drm_plane *planes, size_t n_planes) { free(planes); } -static void assert_rotations_work() { +static void assert_rotations_work(void) { assert(PLANE_TRANSFORM_ROTATE_0.rotate_0 == true); assert(PLANE_TRANSFORM_ROTATE_0.rotate_90 == false); assert(PLANE_TRANSFORM_ROTATE_0.rotate_180 == false); @@ -2270,7 +2273,7 @@ struct kms_req_builder *drmdev_create_request_builder(struct drmdev *drmdev, uin } if (crtc == NULL) { - LOG_ERROR("Invalid CRTC id: %" PRId32 "\n", crtc_id); + LOG_ERROR("Invalid CRTC id: %" PRIu32 "\n", crtc_id); goto fail_unlock; } @@ -2630,15 +2633,15 @@ UNUSED struct kms_req *kms_req_ref(struct kms_req *req) { } UNUSED void kms_req_unref(struct kms_req *req) { - return kms_req_builder_unref((struct kms_req_builder *) req); + kms_req_builder_unref((struct kms_req_builder *) req); } UNUSED void kms_req_unrefp(struct kms_req **req) { - return kms_req_builder_unrefp((struct kms_req_builder **) req); + kms_req_builder_unrefp((struct kms_req_builder **) req); } UNUSED void kms_req_swap_ptrs(struct kms_req **oldp, struct kms_req *new) { - return kms_req_builder_swap_ptrs((struct kms_req_builder **) oldp, (struct kms_req_builder *) new); + kms_req_builder_swap_ptrs((struct kms_req_builder **) oldp, (struct kms_req_builder *) new); } static bool drm_plane_is_active(struct drm_plane *plane) { @@ -2728,13 +2731,13 @@ kms_req_commit_common(struct kms_req *req, bool blocking, kms_scanout_cb_t scano struct drm_plane *plane = layer->plane; ASSERT_NOT_NULL(plane); +#ifndef DEBUG if (plane->committed_state.has_format && plane->committed_state.format == layer->layer.format) { needs_set_crtc = false; } else { needs_set_crtc = true; } - -#ifdef DEBUG +#else drmModeFBPtr committed_fb = drmModeGetFB(builder->drmdev->master_fd, plane->committed_state.fb_id); if (committed_fb == NULL) { needs_set_crtc = true; @@ -2915,6 +2918,8 @@ kms_req_commit_common(struct kms_req *req, bool blocking, kms_scanout_cb_t scano goto fail_unref_builder; } + struct drmdev *drmdev = builder->drmdev; + drmdev_on_page_flip_locked( builder->drmdev->fd, (unsigned int) sequence, @@ -2923,16 +2928,22 @@ kms_req_commit_common(struct kms_req *req, bool blocking, kms_scanout_cb_t scano builder->crtc->id, kms_req_ref(req) ); + + drmdev_unlock(drmdev); } else if (blocking) { + struct drmdev *drmdev = builder->drmdev; + // handle the page-flip event here, rather than via the eventfd ok = drmdev_on_modesetting_fd_ready_locked(builder->drmdev); if (ok != 0) { LOG_ERROR("Couldn't synchronously handle pageflip event.\n"); goto fail_unset_scanout_callback; } - } - drmdev_unlock(builder->drmdev); + drmdev_unlock(drmdev); + } else { + drmdev_unlock(builder->drmdev); + } return 0; @@ -2942,8 +2953,15 @@ kms_req_commit_common(struct kms_req *req, bool blocking, kms_scanout_cb_t scano builder->drmdev->per_crtc_state[builder->crtc->index].userdata = NULL; goto fail_unlock; -fail_unref_builder: +fail_unref_builder: { + struct drmdev *drmdev = builder->drmdev; kms_req_builder_unref(builder); + if (mode_blob != NULL) { + drm_mode_blob_destroy(mode_blob); + } + drmdev_unlock(drmdev); + return ok; +} fail_maybe_destroy_mode_blob: if (mode_blob != NULL) diff --git a/src/modesetting.h b/src/modesetting.h index 401e1150..bbbea305 100644 --- a/src/modesetting.h +++ b/src/modesetting.h @@ -751,7 +751,7 @@ struct kms_req_builder; struct kms_req_builder *drmdev_create_request_builder(struct drmdev *drmdev, uint32_t crtc_id); -DECLARE_REF_OPS(kms_req_builder); +DECLARE_REF_OPS(kms_req_builder) /** * @brief Gets the @ref drmdev associated with this KMS request builder. @@ -900,7 +900,7 @@ int kms_req_builder_push_zpos_placeholder_layer(struct kms_req_builder *builder, */ struct kms_req; -DECLARE_REF_OPS(kms_req); +DECLARE_REF_OPS(kms_req) /** * @brief Build the KMS request builder into an actual, immutable KMS request diff --git a/src/notifier_listener.c b/src/notifier_listener.c index 278b9749..3cb8adea 100644 --- a/src/notifier_listener.c +++ b/src/notifier_listener.c @@ -48,7 +48,7 @@ int value_notifier_init(struct notifier *notifier, void *initial_value, void_cal return 0; } -struct notifier *change_notifier_new() { +struct notifier *change_notifier_new(void) { struct notifier *n; int ok; diff --git a/src/notifier_listener.h b/src/notifier_listener.h index 3861182e..cbc042b9 100644 --- a/src/notifier_listener.h +++ b/src/notifier_listener.h @@ -59,7 +59,7 @@ int value_notifier_init(struct notifier *notifier, void *initial_value, void_cal * For the behaviour of change notifiers, see @ref change_notifier_init. * */ -struct notifier *change_notifier_new(); +struct notifier *change_notifier_new(void); /** * @brief Create a new heap allocated value notifier. diff --git a/src/pixel_format.c b/src/pixel_format.c index 6b936619..a1d09b2f 100644 --- a/src/pixel_format.c +++ b/src/pixel_format.c @@ -77,7 +77,7 @@ const size_t n_pixfmt_infos = n_pixfmt_infos_constexpr; COMPILE_ASSERT(n_pixfmt_infos_constexpr == PIXFMT_MAX + 1); #ifdef DEBUG -void assert_pixfmt_list_valid() { +void assert_pixfmt_list_valid(void) { for (enum pixfmt format = 0; format < PIXFMT_COUNT; format++) { assert(pixfmt_infos[format].format == format); } diff --git a/src/pixel_format.h b/src/pixel_format.h index 3ad991ee..14119039 100644 --- a/src/pixel_format.h +++ b/src/pixel_format.h @@ -359,7 +359,7 @@ extern const struct pixfmt_info pixfmt_infos[]; extern const size_t n_pixfmt_infos; #ifdef DEBUG -void assert_pixfmt_list_valid(); +void assert_pixfmt_list_valid(void); #endif /** diff --git a/src/platformchannel.c b/src/platformchannel.c index 5ed3cbca..05a5c03c 100644 --- a/src/platformchannel.c +++ b/src/platformchannel.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -165,45 +166,48 @@ static int _readSize(const uint8_t **pbuffer, uint32_t *psize, size_t *remaining return 0; } -int platch_free_value_std(struct std_value *value) { - int ok; - +void platch_free_value_std(struct std_value *value) { switch (value->type) { + case kStdNull: + case kStdTrue: + case kStdFalse: + case kStdInt32: + case kStdInt64: + case kStdLargeInt: + case kStdFloat64: break; case kStdString: free(value->string_value); break; + case kStdUInt8Array: + case kStdInt32Array: + case kStdInt64Array: + case kStdFloat64Array: break; case kStdList: for (int i = 0; i < value->size; i++) { - ok = platch_free_value_std(&(value->list[i])); - if (ok != 0) - return ok; + platch_free_value_std(value->list + i); } free(value->list); break; case kStdMap: for (int i = 0; i < value->size; i++) { - ok = platch_free_value_std(&(value->keys[i])); - if (ok != 0) - return ok; - ok = platch_free_value_std(&(value->values[i])); - if (ok != 0) - return ok; + platch_free_value_std(value->keys + i); + platch_free_value_std(value->values + i); } free(value->keys); break; + case kStdFloat32Array: break; default: break; } - - return 0; } -int platch_free_json_value(struct json_value *value, bool shallow) { - int ok; - +void platch_free_json_value(struct json_value *value, bool shallow) { switch (value->type) { + case kJsonNull: + case kJsonTrue: + case kJsonFalse: + case kJsonNumber: + case kJsonString: break; case kJsonArray: if (!shallow) { for (int i = 0; i < value->size; i++) { - ok = platch_free_json_value(&(value->array[i]), false); - if (ok != 0) - return ok; + platch_free_json_value(&(value->array[i]), false); } } @@ -212,9 +216,7 @@ int platch_free_json_value(struct json_value *value, bool shallow) { case kJsonObject: if (!shallow) { for (int i = 0; i < value->size; i++) { - ok = platch_free_json_value(&(value->values[i]), false); - if (ok != 0) - return ok; + platch_free_json_value(&(value->values[i]), false); } } @@ -223,11 +225,11 @@ int platch_free_json_value(struct json_value *value, bool shallow) { break; default: break; } - - return 0; } -int platch_free_obj(struct platch_obj *object) { + +void platch_free_obj(struct platch_obj *object) { switch (object->codec) { + case kNotImplemented: break; case kStringCodec: free(object->string_value); break; case kBinaryCodec: break; case kJSONMessageCodec: platch_free_json_value(&(object->json_value), false); break; @@ -235,12 +237,34 @@ int platch_free_obj(struct platch_obj *object) { case kStandardMethodCall: free(object->method); platch_free_value_std(&(object->std_arg)); + break; + case kStandardMethodCallResponse: + if (object->success) { + platch_free_value_std(&(object->std_result)); + } else { + free(object->error_code); + if (object->error_msg) { + free(object->error_msg); + } + platch_free_value_std(&(object->std_error_details)); + } + break; case kJSONMethodCall: platch_free_json_value(&(object->json_arg), false); break; - default: break; - } + case kJSONMethodCallResponse: + if (object->success) { + platch_free_json_value(&(object->json_result), false); + } else { + free(object->error_code); + if (object->error_msg) { + free(object->error_msg); + } + platch_free_json_value(&(object->json_error_details), false); + } - return 0; + break; + default: UNREACHABLE(); + } } int platch_calc_value_size_std(struct std_value *value, size_t *size_out) { @@ -330,6 +354,15 @@ int platch_calc_value_size_std(struct std_value *value, size_t *size_out) { } break; + case kStdFloat32Array: + element_size = value->size; + + _advance_size_bytes(&size, element_size, NULL); + _align(&size, 4, NULL); + _advance(&size, element_size * 4, NULL); + + break; + default: return EINVAL; } @@ -342,7 +375,7 @@ int platch_write_value_to_buffer_std(struct std_value *value, uint8_t **pbuffer) size_t size; int ok; - _write_u8(pbuffer, value->type, NULL); + _write_u8(pbuffer, (uint8_t) value->type, NULL); switch (value->type) { case kStdNull: @@ -425,6 +458,16 @@ int platch_write_value_to_buffer_std(struct std_value *value, uint8_t **pbuffer) return ok; } break; + case kStdFloat32Array: + size = value->size; + + _writeSize(pbuffer, size, NULL); + _align((uintptr_t *) pbuffer, 4, NULL); + + for (int i = 0; i < size; i++) { + _write_float(pbuffer, value->float32array[i], NULL); + } + break; default: return EINVAL; } @@ -678,7 +721,7 @@ int platch_decode_value_std(const uint8_t **pbuffer, size_t *premaining, struct return ok; break; - case kStdList: + case kStdList: { ok = _readSize(pbuffer, &size, premaining); if (ok != 0) return ok; @@ -687,36 +730,80 @@ int platch_decode_value_std(const uint8_t **pbuffer, size_t *premaining, struct value_out->list = calloc(size, sizeof(struct std_value)); for (int i = 0; i < size; i++) { - ok = platch_decode_value_std(pbuffer, premaining, &value_out->list[i]); - if (ok != 0) + ok = platch_decode_value_std(pbuffer, premaining, value_out->list + i); + if (ok != 0) { + for (int j = 0; j < i; j++) { + platch_free_value_std(value_out->list + j); + } + free(value_out->list); return ok; + } } break; - case kStdMap: + } + case kStdMap: { ok = _readSize(pbuffer, &size, premaining); - if (ok != 0) + if (ok != 0) { return ok; + } value_out->size = size; value_out->keys = calloc(size * 2, sizeof(struct std_value)); - if (!value_out->keys) + if (!value_out->keys) { return ENOMEM; + } value_out->values = &value_out->keys[size]; for (int i = 0; i < size; i++) { ok = platch_decode_value_std(pbuffer, premaining, &(value_out->keys[i])); - if (ok != 0) + if (ok != 0) { + for (int j = 0; j < i; j++) { + platch_free_value_std(&(value_out->values[j])); + platch_free_value_std(&(value_out->keys[j])); + } + free(value_out->keys); return ok; + } ok = platch_decode_value_std(pbuffer, premaining, &(value_out->values[i])); - if (ok != 0) + if (ok != 0) { + platch_free_value_std(&(value_out->keys[i])); + for (int j = 0; j < i; j++) { + platch_free_value_std(&(value_out->values[j])); + platch_free_value_std(&(value_out->keys[j])); + } + free(value_out->keys); return ok; + } } break; + } + + case kStdFloat32Array: { + ok = _readSize(pbuffer, &size, premaining); + if (ok != 0) + return ok; + + ok = _align((uintptr_t *) pbuffer, 4, premaining); + if (ok != 0) + return ok; + + if (*premaining < size * 4) + return EBADMSG; + + value_out->size = size; + value_out->float32array = (float *) *pbuffer; + + ok = _advance((uintptr_t *) pbuffer, size * 4, premaining); + if (ok != 0) + return ok; + + break; + } default: return EBADMSG; } @@ -792,8 +879,10 @@ int platch_decode_value_json(char *message, size_t size, jsmntok_t **pptoken, si for (int i = 0; i < ptoken->size; i++) { ok = platch_decode_value_json(message, size, pptoken, ptokensremaining, &array[i]); - if (ok != 0) + if (ok != 0) { + free(array); return ok; + } } value_out->type = kJsonArray; @@ -801,25 +890,52 @@ int platch_decode_value_json(char *message, size_t size, jsmntok_t **pptoken, si value_out->array = array; break; - case JSMN_OBJECT:; - struct json_value key; + case JSMN_OBJECT: { char **keys = calloc(ptoken->size, sizeof(char *)); + if (!keys) { + return ENOMEM; + } + struct json_value *values = calloc(ptoken->size, sizeof(struct json_value)); - if ((!keys) || (!values)) + if (!values) { + free(keys); return ENOMEM; + } for (int i = 0; i < ptoken->size; i++) { + struct json_value key; + ok = platch_decode_value_json(message, size, pptoken, ptokensremaining, &key); - if (ok != 0) + if (ok != 0) { + for (int j = 0; j < i; j++) { + free(keys[j]); + } + free(keys); + free(values); return ok; + } - if (key.type != kJsonString) + if (key.type != kJsonString) { + platch_free_json_value(&key, true); + for (int j = 0; j < i; j++) { + free(keys[j]); + } + free(keys); + free(values); return EBADMSG; + } + keys[i] = key.string_value; ok = platch_decode_value_json(message, size, pptoken, ptokensremaining, &values[i]); - if (ok != 0) + if (ok != 0) { + for (int j = 0; j < i; j++) { + free(keys[j]); + } + free(keys); + free(values); return ok; + } } value_out->type = kJsonObject; @@ -828,6 +944,7 @@ int platch_decode_value_json(char *message, size_t size, jsmntok_t **pptoken, si value_out->values = values; break; + } default: return EBADMSG; } } @@ -840,135 +957,191 @@ int platch_decode_json(char *string, struct json_value *out) { } int platch_decode(const uint8_t *buffer, size_t size, enum platch_codec codec, struct platch_obj *object_out) { - struct json_value root_jsvalue; - const uint8_t *buffer_cursor = buffer; - size_t remaining = size; int ok; - if ((size == 0) && (buffer == NULL)) { - object_out->codec = kNotImplemented; - return 0; + if (codec != kNotImplemented && ((size == 0) || (buffer == NULL))) { + return EINVAL; } + const uint8_t *buffer_cursor = buffer; + size_t remaining = size; + object_out->codec = codec; switch (codec) { - case kStringCodec:; - /// buffer is a non-null-terminated, UTF8-encoded string. - /// it's really sad we have to allocate a new memory block for this, but we have to since string codec buffers are not null-terminated. + case kNotImplemented: { + if (size != 0) { + return EINVAL; + } + if (buffer != NULL) { + return EINVAL; + } + + break; + } - char *string; - if (!(string = malloc(size + 1))) + case kStringCodec: { + char *string = malloc(size + 1); + if (string == NULL) { return ENOMEM; - memcpy(string, buffer, size); + } + + strncpy(string, (char *) buffer, size); string[size] = '\0'; object_out->string_value = string; - break; - case kBinaryCodec: + } + case kBinaryCodec: { + if (size == 0) { + return EINVAL; + } + if (buffer == NULL) { + return EINVAL; + } + object_out->binarydata = buffer; object_out->binarydata_size = size; - break; - case kJSONMessageCodec: - ok = platch_decode_value_json((char *) buffer, size, NULL, NULL, &(object_out->json_value)); - if (ok != 0) + } + case kJSONMessageCodec: { + ok = platch_decode_value_json((char *) buffer, size, NULL, NULL, &object_out->json_value); + if (ok != 0) { return ok; + } break; - case kJSONMethodCall:; - ok = platch_decode_value_json((char *) buffer, size, NULL, NULL, &root_jsvalue); - if (ok != 0) - return ok; + } + case kJSONMethodCall: { + struct json_value root; - if (root_jsvalue.type != kJsonObject) - return EBADMSG; + ok = platch_decode_value_json((char *) buffer, size, NULL, NULL, &root); + if (ok != 0) { + return ok; + } - for (int i = 0; i < root_jsvalue.size; i++) { - if ((streq(root_jsvalue.keys[i], "method")) && (root_jsvalue.values[i].type == kJsonString)) { - object_out->method = root_jsvalue.values[i].string_value; - } else if (streq(root_jsvalue.keys[i], "args")) { - object_out->json_arg = root_jsvalue.values[i]; - } else - return EBADMSG; + if (root.type != kJsonObject) { + platch_free_json_value(&root, true); + return EINVAL; } - platch_free_json_value(&root_jsvalue, true); + for (int i = 0; i < root.size; i++) { + if ((streq(root.keys[i], "method")) && (root.values[i].type == kJsonString)) { + object_out->method = root.values[i].string_value; + } else if (streq(root.keys[i], "args")) { + object_out->json_arg = root.values[i]; + } else { + return EINVAL; + } + } + platch_free_json_value(&root, true); break; - case kJSONMethodCallResponse:; - ok = platch_decode_value_json((char *) buffer, size, NULL, NULL, &root_jsvalue); - if (ok != 0) + } + case kJSONMethodCallResponse: { + struct json_value root; + + ok = platch_decode_value_json((char *) buffer, size, NULL, NULL, &root); + if (ok != 0) { return ok; - if (root_jsvalue.type != kJsonArray) - return EBADMSG; + } + + if (root.type != kJsonArray) { + platch_free_json_value(&root, true); + return EINVAL; + } - if (root_jsvalue.size == 1) { + if (root.size == 1) { object_out->success = true; - object_out->json_result = root_jsvalue.array[0]; - return platch_free_json_value(&root_jsvalue, true); - } else if ((root_jsvalue.size == 3) && - (root_jsvalue.array[0].type == kJsonString) && - ((root_jsvalue.array[1].type == kJsonString) || (root_jsvalue.array[1].type == kJsonNull))) { + object_out->json_result = root.array[0]; + } else if ((root.size == 3) && (root.array[0].type == kJsonString) && + ((root.array[1].type == kJsonString) || (root.array[1].type == kJsonNull))) { object_out->success = false; - object_out->error_code = root_jsvalue.array[0].string_value; - object_out->error_msg = root_jsvalue.array[1].string_value; - object_out->json_error_details = root_jsvalue.array[2]; - return platch_free_json_value(&root_jsvalue, true); - } else - return EBADMSG; + object_out->error_code = root.array[0].string_value; + object_out->error_msg = root.array[1].string_value; + object_out->json_error_details = root.array[2]; + } else { + platch_free_json_value(&root, true); + return EINVAL; + } + platch_free_json_value(&root, true); break; - case kStandardMessageCodec: + } + case kStandardMessageCodec: { ok = platch_decode_value_std(&buffer_cursor, &remaining, &object_out->std_value); - if (ok != 0) + if (ok != 0) { return ok; + } + break; - case kStandardMethodCall:; + } + case kStandardMethodCall: { struct std_value methodname; ok = platch_decode_value_std(&buffer_cursor, &remaining, &methodname); - if (ok != 0) + if (ok != 0) { return ok; + } + if (methodname.type != kStdString) { platch_free_value_std(&methodname); - return EBADMSG; + return EINVAL; } + object_out->method = methodname.string_value; ok = platch_decode_value_std(&buffer_cursor, &remaining, &object_out->std_arg); - if (ok != 0) + if (ok != 0) { return ok; + } break; - case kStandardMethodCallResponse:; + } + case kStandardMethodCallResponse: { ok = _read_u8(&buffer_cursor, (uint8_t *) &object_out->success, &remaining); + if (ok != 0) { + return ok; + } if (object_out->success) { ok = platch_decode_value_std(&buffer_cursor, &remaining, &(object_out->std_result)); - if (ok != 0) + if (ok != 0) { return ok; + } } else { struct std_value error_code, error_msg; ok = platch_decode_value_std(&buffer_cursor, &remaining, &error_code); - if (ok != 0) + if (ok != 0) { return ok; + } + ok = platch_decode_value_std(&buffer_cursor, &remaining, &error_msg); - if (ok != 0) + if (ok != 0) { + platch_free_value_std(&error_code); return ok; + } + ok = platch_decode_value_std(&buffer_cursor, &remaining, &(object_out->std_error_details)); - if (ok != 0) + if (ok != 0) { + platch_free_value_std(&error_msg); + platch_free_value_std(&error_code); return ok; + } if ((error_code.type == kStdString) && ((error_msg.type == kStdString) || (error_msg.type == kStdNull))) { object_out->error_code = error_code.string_value; object_out->error_msg = (error_msg.type == kStdString) ? error_msg.string_value : NULL; } else { - return EBADMSG; + platch_free_value_std(&object_out->std_error_details); + platch_free_value_std(&error_code); + platch_free_value_std(&error_msg); + return EINVAL; } } + break; + } default: return EINVAL; } @@ -976,153 +1149,224 @@ int platch_decode(const uint8_t *buffer, size_t size, enum platch_codec codec, s } int platch_encode(struct platch_obj *object, uint8_t **buffer_out, size_t *size_out) { - struct std_value stdmethod, stderrcode, stderrmessage; - uint8_t *buffer, *buffer_cursor; - size_t size = 0; int ok = 0; - *size_out = 0; - *buffer_out = NULL; - switch (object->codec) { - case kNotImplemented: + case kNotImplemented: { *size_out = 0; *buffer_out = NULL; return 0; - case kStringCodec: size = strlen(object->string_value); break; - case kBinaryCodec: + } + case kStringCodec: { + *buffer_out = (uint8_t *) strdup(object->string_value); + if (buffer_out == NULL) { + return ENOMEM; + } + + *size_out = strlen(object->string_value); + return 0; + } + case kBinaryCodec: { /// FIXME: Copy buffer instead *buffer_out = (uint8_t *) object->binarydata; *size_out = object->binarydata_size; return 0; - case kJSONMessageCodec: - size = platch_calc_value_size_json(&(object->json_value)); + } + case kJSONMessageCodec: { + size_t size = platch_calc_value_size_json(&(object->json_value)); size += 1; // JSONMsgCodec uses sprintf, which null-terminates strings, // so lets allocate one more byte for the last null-terminator. // this is decremented again in the second switch-case, so flutter // doesn't complain about a malformed message. + + uint8_t *buffer = malloc(size); + if (buffer == NULL) { + return ENOMEM; + } + + uint8_t *buffer_cursor = buffer; + + ok = platch_write_value_to_buffer_json(&(object->json_value), &buffer_cursor); + if (ok != 0) { + free(buffer); + return ok; + } + + *buffer_out = buffer; + *size_out = size; break; - case kStandardMessageCodec: - ok = platch_calc_value_size_std(&(object->std_value), &size); - if (ok != 0) + } + case kStandardMessageCodec: { + size_t size; + + ok = platch_calc_value_size_std(&object->std_value, &size); + if (ok != 0) { return ok; + } + + uint8_t *buffer = malloc(size); + if (buffer == NULL) { + return ENOMEM; + } + + uint8_t *buffer_cursor = buffer; + + ok = platch_write_value_to_buffer_std(&object->std_value, &buffer_cursor); + if (ok != 0) { + free(buffer); + return ok; + } + + *buffer_out = buffer; + *size_out = size; break; - case kStandardMethodCall: + } + case kStandardMethodCall: { + struct std_value stdmethod; + size_t size; + stdmethod.type = kStdString; stdmethod.string_value = object->method; ok = platch_calc_value_size_std(&stdmethod, &size); - if (ok != 0) + if (ok != 0) { return ok; + } ok = platch_calc_value_size_std(&(object->std_arg), &size); - if (ok != 0) + if (ok != 0) { + return ok; + } + + uint8_t *buffer = malloc(size); + if (buffer == NULL) { + return ENOMEM; + } + + uint8_t *buffer_cursor = buffer; + ok = platch_write_value_to_buffer_std(&stdmethod, &buffer_cursor); + if (ok != 0) { + free(buffer); return ok; + } + ok = platch_write_value_to_buffer_std(&(object->std_arg), &buffer_cursor); + if (ok != 0) { + free(buffer); + return ok; + } + + *buffer_out = buffer; + *size_out = size; break; - case kStandardMethodCallResponse: - size += 1; + } + case kStandardMethodCallResponse: { + size_t size = 1; if (object->success) { ok = platch_calc_value_size_std(&(object->std_result), &size); - if (ok != 0) + if (ok != 0) { return ok; + } } else { - stderrcode = (struct std_value){ .type = kStdString, .string_value = object->error_code }; - stderrmessage = (struct std_value){ .type = kStdString, .string_value = object->error_msg }; - - ok = platch_calc_value_size_std(&stderrcode, &size); - if (ok != 0) + ok = platch_calc_value_size_std(&STDSTRING(object->error_code), &size); + if (ok != 0) { return ok; - ok = platch_calc_value_size_std(&stderrmessage, &size); - if (ok != 0) + } + + ok = platch_calc_value_size_std(&STDSTRING(object->error_msg), &size); + if (ok != 0) { return ok; + } + ok = platch_calc_value_size_std(&(object->std_error_details), &size); - if (ok != 0) + if (ok != 0) { return ok; + } } - break; - case kJSONMethodCall: - size = platch_calc_value_size_json(&JSONOBJECT2("method", JSONSTRING(object->method), "args", object->json_arg)); - size += 1; - break; - case kJSONMethodCallResponse: - if (object->success) { - size = 1 + platch_calc_value_size_json(&JSONARRAY1(object->json_result)); - } else { - size = 1 + platch_calc_value_size_json(&JSONARRAY3( - JSONSTRING(object->error_code), - (object->error_msg != NULL) ? JSONSTRING(object->error_msg) : JSONNULL, - object->json_error_details - )); - } - break; - default: return EINVAL; - } - - buffer = malloc(size); - if (buffer == NULL) { - return ENOMEM; - } - - buffer_cursor = buffer; - - switch (object->codec) { - case kStringCodec: memcpy(buffer, object->string_value, size); break; - case kStandardMessageCodec: - ok = platch_write_value_to_buffer_std(&(object->std_value), &buffer_cursor); - if (ok != 0) - goto free_buffer_and_return_ok; - break; - case kStandardMethodCall: - ok = platch_write_value_to_buffer_std(&stdmethod, &buffer_cursor); - if (ok != 0) - goto free_buffer_and_return_ok; - ok = platch_write_value_to_buffer_std(&(object->std_arg), &buffer_cursor); - if (ok != 0) - goto free_buffer_and_return_ok; + uint8_t *buffer = malloc(size); + if (buffer == NULL) { + return ENOMEM; + } - break; - case kStandardMethodCallResponse: + uint8_t *buffer_cursor = buffer; if (object->success) { _write_u8(&buffer_cursor, 0x00, NULL); - ok = platch_write_value_to_buffer_std(&(object->std_result), &buffer_cursor); - if (ok != 0) - goto free_buffer_and_return_ok; + ok = platch_write_value_to_buffer_std(&object->std_result, &buffer_cursor); + if (ok != 0) { + free(buffer); + return ok; + } } else { _write_u8(&buffer_cursor, 0x01, NULL); - ok = platch_write_value_to_buffer_std(&stderrcode, &buffer_cursor); - if (ok != 0) - goto free_buffer_and_return_ok; - ok = platch_write_value_to_buffer_std(&stderrmessage, &buffer_cursor); - if (ok != 0) - goto free_buffer_and_return_ok; + ok = platch_write_value_to_buffer_std(&STDSTRING(object->error_code), &buffer_cursor); + if (ok != 0) { + free(buffer); + return ok; + } + + ok = platch_write_value_to_buffer_std(&STDSTRING(object->error_msg), &buffer_cursor); + if (ok != 0) { + free(buffer); + return ok; + } + ok = platch_write_value_to_buffer_std(&(object->std_error_details), &buffer_cursor); - if (ok != 0) - goto free_buffer_and_return_ok; + if (ok != 0) { + free(buffer); + return ok; + } } + *buffer_out = buffer; + *size_out = size; break; - case kJSONMessageCodec: - size -= 1; - ok = platch_write_value_to_buffer_json(&(object->json_value), &buffer_cursor); - if (ok != 0) - goto free_buffer_and_return_ok; - break; - case kJSONMethodCall: - size -= 1; + } + case kJSONMethodCall: { + size_t size = platch_calc_value_size_json(&JSONOBJECT2("method", JSONSTRING(object->method), "args", object->json_arg)); + + uint8_t *buffer = malloc(size + 1); + if (buffer == NULL) { + return ENOMEM; + } + + uint8_t *buffer_cursor = buffer; + ok = platch_write_value_to_buffer_json( &JSONOBJECT2("method", JSONSTRING(object->method), "args", object->json_arg), &buffer_cursor ); if (ok != 0) { - goto free_buffer_and_return_ok; + free(buffer); + return ok; } + + *buffer_out = buffer; + *size_out = size; break; - case kJSONMethodCallResponse: + } + case kJSONMethodCallResponse: { + size_t size; + + if (object->success) { + size = platch_calc_value_size_json(&JSONARRAY1(object->json_result)); + } else { + size = platch_calc_value_size_json(&JSONARRAY3( + JSONSTRING(object->error_code), + (object->error_msg != NULL) ? JSONSTRING(object->error_msg) : JSONNULL, + object->json_error_details + )); + } + + uint8_t *buffer = malloc(size + 1); + if (buffer == NULL) { + return ENOMEM; + } + + uint8_t *buffer_cursor = buffer; if (object->success) { ok = platch_write_value_to_buffer_json(&JSONARRAY1(object->json_result), &buffer_cursor); } else { @@ -1135,21 +1379,21 @@ int platch_encode(struct platch_obj *object, uint8_t **buffer_out, size_t *size_ &buffer_cursor ); } - size -= 1; + if (ok != 0) { - goto free_buffer_and_return_ok; + free(buffer); + return ok; } + + *buffer_out = buffer; + *size_out = size; + break; + } default: return EINVAL; } - *buffer_out = buffer; - *size_out = size; return 0; - -free_buffer_and_return_ok: - free(buffer); - return ok; } void platch_on_response_internal(const uint8_t *buffer, size_t size, void *userdata) { @@ -1168,9 +1412,7 @@ void platch_on_response_internal(const uint8_t *buffer, size_t size, void *userd free(handlerdata); - ok = platch_free_obj(&object); - if (ok != 0) - return; + platch_free_obj(&object); } int platch_send( @@ -1193,7 +1435,8 @@ int platch_send( if (on_response) { handlerdata = malloc(sizeof(struct platch_msg_resp_handler_data)); if (!handlerdata) { - return ENOMEM; + ok = ENOMEM; + goto fail_free_object; } handlerdata->codec = response_codec; @@ -1232,6 +1475,11 @@ int platch_send( free(handlerdata); } +fail_free_object: + if (object->codec != kBinaryCodec) { + free(buffer); + } + return ok; } @@ -1266,7 +1514,7 @@ int platch_respond(const FlutterPlatformMessageResponseHandle *handle, struct pl free(buffer); } - return 0; + return ok; } int platch_respond_not_implemented(const FlutterPlatformMessageResponseHandle *handle) { @@ -1578,7 +1826,11 @@ bool stdvalue_equals(struct std_value *a, struct std_value *b) { ASSERT_NOT_NULL(a->string_value); ASSERT_NOT_NULL(b->string_value); return streq(a->string_value, b->string_value); - case kStdFloat64: return a->float64_value == b->float64_value; + case kStdFloat64: + PRAGMA_DIAGNOSTIC_PUSH + PRAGMA_DIAGNOSTIC_IGNORED("-Wfloat-equal") + return a->float64_value == b->float64_value; + PRAGMA_DIAGNOSTIC_POP case kStdUInt8Array: if (a->size != b->size) return false; @@ -1616,16 +1868,24 @@ bool stdvalue_equals(struct std_value *a, struct std_value *b) { return false; return true; case kStdFloat64Array: - if (a->size != b->size) - return false; if (a->float64array == b->float64array) return true; + if (a->size != b->size) + return false; + ASSERT_NOT_NULL(a->float64array); ASSERT_NOT_NULL(b->float64array); - for (int i = 0; i < a->size; i++) - if (a->float64array[i] != b->float64array[i]) + + PRAGMA_DIAGNOSTIC_PUSH + PRAGMA_DIAGNOSTIC_IGNORED("-Wfloat-equal") + for (int i = 0; i < a->size; i++) { + if (a->float64array[i] != b->float64array[i]) { return false; + } + } + PRAGMA_DIAGNOSTIC_POP + return true; case kStdList: // the order of list elements is important @@ -1684,6 +1944,25 @@ bool stdvalue_equals(struct std_value *a, struct std_value *b) { return true; } + case kStdFloat32Array: + if (a->float32array == b->float32array) + return true; + + if (a->size != b->size) + return false; + + ASSERT_NOT_NULL(a->float32array); + ASSERT_NOT_NULL(b->float32array); + + PRAGMA_DIAGNOSTIC_PUSH + PRAGMA_DIAGNOSTIC_IGNORED("-Wfloat-equal") + for (int i = 0; i < a->size; i++) { + if (a->float32array[i] != b->float32array[i]) { + return false; + } + } + PRAGMA_DIAGNOSTIC_POP + return true; default: return false; } @@ -1948,7 +2227,12 @@ ATTR_PURE bool raw_std_value_equals(const struct raw_std_value *a, const struct case kStdFalse: return true; case kStdInt32: return raw_std_value_as_int32(a) == raw_std_value_as_int32(b); case kStdInt64: return raw_std_value_as_int64(a) == raw_std_value_as_int64(b); - case kStdFloat64: return raw_std_value_as_float64(a) == raw_std_value_as_float64(b); + case kStdFloat64: + PRAGMA_DIAGNOSTIC_PUSH + PRAGMA_DIAGNOSTIC_IGNORED("-Wfloat-equal") + return raw_std_value_as_float64(a) == raw_std_value_as_float64(b); + PRAGMA_DIAGNOSTIC_POP + case kStdLargeInt: case kStdString: alignment = 0; element_size = 1; @@ -1982,11 +2266,15 @@ ATTR_PURE bool raw_std_value_equals(const struct raw_std_value *a, const struct length = raw_std_value_get_size(a); const double *a_doubles = raw_std_value_as_float64array(a); const double *b_doubles = raw_std_value_as_float64array(b); + + PRAGMA_DIAGNOSTIC_PUSH + PRAGMA_DIAGNOSTIC_IGNORED("-Wfloat-equal") for (int i = 0; i < length; i++) { if (a_doubles[i] != b_doubles[i]) { return false; } } + PRAGMA_DIAGNOSTIC_POP return true; case kStdList: @@ -2066,11 +2354,15 @@ ATTR_PURE bool raw_std_value_equals(const struct raw_std_value *a, const struct length = raw_std_value_get_size(a); const float *a_floats = raw_std_value_as_float32array(a); const float *b_floats = raw_std_value_as_float32array(b); + + PRAGMA_DIAGNOSTIC_PUSH + PRAGMA_DIAGNOSTIC_IGNORED("-Wfloat-equal") for (int i = 0; i < length; i++) { if (a_floats[i] != b_floats[i]) { return false; } } + PRAGMA_DIAGNOSTIC_POP return true; default: assert(false); return false; @@ -2281,6 +2573,7 @@ ATTR_PURE bool raw_std_value_check(const struct raw_std_value *value, size_t buf case kStdInt32: return buffer_size >= 5; case kStdInt64: return buffer_size >= 9; case kStdFloat64: return buffer_size >= 9; + case kStdLargeInt: case kStdString: alignment = 0; element_size = 1; @@ -2338,9 +2631,6 @@ ATTR_PURE bool raw_std_value_check(const struct raw_std_value *value, size_t buf return false; } - // get the value size. - size = raw_std_value_get_size(value); - for_each_element_in_raw_std_list(element, value) { int diff = (intptr_t) element - (intptr_t) value; if (buffer_size < diff) { diff --git a/src/platformchannel.h b/src/platformchannel.h index 14ea7287..8520d2e7 100644 --- a/src/platformchannel.h +++ b/src/platformchannel.h @@ -89,6 +89,7 @@ struct std_value { const uint8_t *uint8array; int32_t *int32array; int64_t *int64array; + float *float32array; double *float64array; struct std_value *list; struct { @@ -1540,9 +1541,7 @@ int platch_send_error_event_json(char *channel, char *error_code, char *error_ms /// frees a ChannelObject that was decoded using PlatformChannel_decode. /// not freeing ChannelObjects may result in a memory leak. -int platch_free_obj(struct platch_obj *object); - -int platch_free_json_value(struct json_value *value, bool shallow); +void platch_free_obj(struct platch_obj *object); /// returns true if values a and b are equal. /// for JS arrays, the order of the values is relevant diff --git a/src/pluginregistry.c b/src/pluginregistry.c index 1c6ae730..f78b917b 100644 --- a/src/pluginregistry.c +++ b/src/pluginregistry.c @@ -89,6 +89,7 @@ static struct plugin_instance *get_plugin_by_name(struct plugin_registry *regist return instance; } +// clang-format off static struct platch_obj_cb_data *get_cb_data_by_channel_locked(struct plugin_registry *registry, const char *channel) { list_for_each_entry(struct platch_obj_cb_data, data, ®istry->callbacks, entry) { if (streq(data->channel, channel)) { @@ -98,6 +99,7 @@ static struct platch_obj_cb_data *get_cb_data_by_channel_locked(struct plugin_re return NULL; } +// clang-format on struct plugin_registry *plugin_registry_new(struct flutterpi *flutterpi) { struct plugin_registry *reg; @@ -211,7 +213,7 @@ void plugin_registry_add_plugin(struct plugin_registry *registry, const struct f plugin_registry_unlock(registry); } -static void static_plugin_registry_ensure_initialized(); +static void static_plugin_registry_ensure_initialized(void); int plugin_registry_add_plugins_from_static_registry(struct plugin_registry *registry) { ASSERTED int ok; @@ -301,7 +303,7 @@ static int set_receiver_locked( char *channel_dup; ASSERT_MSG((!!callback) != (!!callback_v2), "Exactly one of callback or callback_v2 must be non-NULL."); - ASSERT_MUTEX_LOCKED(registry->lock); + assert_mutex_locked(®istry->lock); data_ptr = get_cb_data_by_channel_locked(registry, channel); if (data_ptr == NULL) { @@ -398,6 +400,16 @@ int plugin_registry_remove_receiver_v2_locked(struct plugin_registry *registry, } list_del(&data->entry); + + // Analyzer thinks get_cb_data_by_channel might still return our data + // after list_del and emits a "use-after-free" warning. + // assert()s can change the assumptions of the analyzer, so we use them here. +#ifdef DEBUG + list_for_each_entry(struct platch_obj_cb_data, data_iter, ®istry->callbacks, entry) { + ASSUME(data_iter != data); + } +#endif + free(data->channel); free(data); @@ -456,7 +468,7 @@ void *plugin_registry_get_plugin_userdata_locked(struct plugin_registry *registr return instance != NULL ? instance->userdata : NULL; } -static void static_plugin_registry_initialize() { +static void static_plugin_registry_initialize(void) { ASSERTED int ok; list_inithead(&static_plugins); @@ -465,7 +477,7 @@ static void static_plugin_registry_initialize() { ASSERT_ZERO(ok); } -static void static_plugin_registry_ensure_initialized() { +static void static_plugin_registry_ensure_initialized(void) { pthread_once(&static_plugins_init_flag, static_plugin_registry_initialize); } @@ -480,7 +492,7 @@ void static_plugin_registry_add_plugin(const struct flutterpi_plugin_v2 *plugin) entry = malloc(sizeof *entry); ASSERT_NOT_NULL(entry); - + entry->plugin = plugin; list_addtail(&entry->entry, &static_plugins); diff --git a/src/pluginregistry.h b/src/pluginregistry.h index 4efe6bf8..b629e720 100644 --- a/src/pluginregistry.h +++ b/src/pluginregistry.h @@ -20,16 +20,6 @@ struct flutterpi; struct plugin_registry; -typedef enum plugin_init_result (*plugin_init_t)(struct flutterpi *flutterpi, void **userdata_out); - -typedef void (*plugin_deinit_t)(struct flutterpi *flutterpi, void *userdata); - -struct flutterpi_plugin_v2 { - const char *name; - plugin_init_t init; - plugin_deinit_t deinit; -}; - /// The return value of a plugin initializer function. enum plugin_init_result { PLUGIN_INIT_RESULT_INITIALIZED, ///< The plugin was successfully initialized. @@ -40,6 +30,16 @@ enum plugin_init_result { /// Flutter-pi may decide to abort the startup phase of the whole flutter-pi instance at that point. }; +typedef enum plugin_init_result (*plugin_init_t)(struct flutterpi *flutterpi, void **userdata_out); + +typedef void (*plugin_deinit_t)(struct flutterpi *flutterpi, void *userdata); + +struct flutterpi_plugin_v2 { + const char *name; + plugin_init_t init; + plugin_deinit_t deinit; +}; + struct _FlutterPlatformMessageResponseHandle; typedef struct _FlutterPlatformMessageResponseHandle FlutterPlatformMessageResponseHandle; @@ -162,16 +162,18 @@ void static_plugin_registry_add_plugin(const struct flutterpi_plugin_v2 *plugin) void static_plugin_registry_remove_plugin(const char *plugin_name); -#define FLUTTERPI_PLUGIN(_name, _identifier_name, _init, _deinit) \ - __attribute__((constructor)) static void __reg_plugin_##_identifier_name() { \ - static struct flutterpi_plugin_v2 plugin = { \ - .name = (_name), \ - .init = (_init), \ - .deinit = (_deinit), \ - }; \ - static_plugin_registry_add_plugin(&plugin); \ - } \ - \ - __attribute__((destructor)) static void __unreg_plugin_##_identifier_name() { static_plugin_registry_remove_plugin(_name); } +#define FLUTTERPI_PLUGIN(_name, _identifier_name, _init, _deinit) \ + __attribute__((constructor)) static void __reg_plugin_##_identifier_name(void) { \ + static struct flutterpi_plugin_v2 plugin = { \ + .name = (_name), \ + .init = (_init), \ + .deinit = (_deinit), \ + }; \ + static_plugin_registry_add_plugin(&plugin); \ + } \ + \ + __attribute__((destructor)) static void __unreg_plugin_##_identifier_name(void) { \ + static_plugin_registry_remove_plugin(_name); \ + } #endif // _FLUTTERPI_SRC_PLUGINREGISTRY_H diff --git a/src/plugins/gstplayer.c b/src/plugins/gstplayer.c index 24962145..d1e1ac97 100644 --- a/src/plugins/gstplayer.c +++ b/src/plugins/gstplayer.c @@ -899,6 +899,8 @@ static void start_async(struct gstplayer *player, struct async_completer complet } static void on_bus_message(struct gstplayer *player, GstMessage *msg) { + PRAGMA_DIAGNOSTIC_PUSH + PRAGMA_DIAGNOSTIC_IGNORED("-Wswitch-enum") switch (GST_MESSAGE_TYPE(msg)) { case GST_MESSAGE_EOS: on_eos_message(player, msg); @@ -1027,6 +1029,7 @@ static void on_bus_message(struct gstplayer *player, GstMessage *msg) { break; } + PRAGMA_DIAGNOSTIC_POP return; } @@ -1336,7 +1339,7 @@ struct gstplayer *gstplayer_new(struct flutterpi *flutterpi, const char *uri, vo TRACER_END(p->tracer, "gstplayer_new()"); - LOG_PLAYER_DEBUG(p, "gstplayer_new(\"%s\", %s): %s\n", uri ?: "", play_audio ? "with audio" : "without audio", p->is_live ? "live" : "not live"); + LOG_PLAYER_DEBUG(p, "gstplayer_new(\"%s\", %s): %s\n", uri ? uri : "", play_audio ? "with audio" : "without audio", p->is_live ? "live" : "not live"); return p; @@ -1684,7 +1687,7 @@ int gstplayer_step_backward(struct gstplayer *player) { void gstplayer_set_audio_balance(struct gstplayer *player, float balance) { if (player->audiopanorama) { - g_object_set(player->audiopanorama, "panorama", (gfloat) balance, NULL); + g_object_set(player->audiopanorama, "panorama", (double) balance, NULL); } } diff --git a/src/plugins/gstreamer_video_player/frame.c b/src/plugins/gstreamer_video_player/frame.c index 59062e0b..a6225f90 100644 --- a/src/plugins/gstreamer_video_player/frame.c +++ b/src/plugins/gstreamer_video_player/frame.c @@ -23,7 +23,8 @@ #define MAX_N_PLANES 4 #define DRM_FOURCC_FORMAT "c%c%c%c" -#define DRM_FOURCC_ARGS(format) (format) & 0xFF, ((format) >> 8) & 0xFF, ((format) >> 16) & 0xFF, ((format) >> 24) & 0xFF +#define DRM_FOURCC_ARGS(format) \ + (char) ((format) & 0xFF), (char) (((format) >> 8) & 0xFF), (char) (((format) >> 16) & 0xFF), (char) (((format) >> 24) & 0xFF) struct video_frame { GstSample *sample; @@ -119,6 +120,10 @@ static bool query_formats( } } + if (n_modified_formats == 0 || max_n_modifiers == 0) { + goto fail_free_formats; + } + modified_formats = malloc(n_modified_formats * sizeof *modified_formats); if (modified_formats == NULL) { goto fail_free_formats; @@ -139,7 +144,7 @@ static bool query_formats( egl_ok = egl_query_dmabuf_modifiers(display, formats[i], max_n_modifiers, modifiers, external_only, &n_modifiers); if (egl_ok != EGL_TRUE) { LOG_ERROR("Could not query dmabuf formats supported by EGL.\n"); - goto fail_free_formats; + goto fail_free_external_only; } LOG_DEBUG_UNPREFIXED("%" DRM_FOURCC_FORMAT ", ", DRM_FOURCC_ARGS(formats[i])); @@ -161,6 +166,9 @@ static bool query_formats( *formats_out = modified_formats; return true; +fail_free_external_only: + free(external_only); + fail_free_modifiers: free(modifiers); @@ -563,6 +571,11 @@ get_plane_infos(GstBuffer *buffer, const GstVideoInfo *info, struct gbm_device * n_planes = GST_VIDEO_INFO_N_PLANES(info); + if (n_planes <= 0 || n_planes > MAX_N_PLANES) { + LOG_ERROR("Unsupported number of planes in video frame.\n"); + return EINVAL; + } + // There's so many ways to get the plane sizes. // 1. Preferably we should use the video meta. // 2. If that doesn't work, we'll use gst_video_info_align_full() with the video info. @@ -602,6 +615,8 @@ get_plane_infos(GstBuffer *buffer, const GstVideoInfo *info, struct gbm_device * has_plane_sizes = true; } + ASSERT_MSG(has_plane_sizes, "Couldn't determine video frame plane sizes.\n"); + for (int i = 0; i < n_planes; i++) { size_t offset_in_memory = 0; size_t offset_in_buffer = 0; @@ -653,7 +668,7 @@ get_plane_infos(GstBuffer *buffer, const GstVideoInfo *info, struct gbm_device * ok = dup(ok); if (ok < 0) { - ok = errno; + ok = errno ? errno : EIO; LOG_ERROR("Could not dup fd. dup: %s\n", strerror(ok)); goto fail_close_fds; } @@ -692,7 +707,7 @@ get_plane_infos(GstBuffer *buffer, const GstVideoInfo *info, struct gbm_device * fail_close_fds: for (int j = i - 1; j > 0; j--) { - close(plane_infos[i].fd); + close(plane_infos[j].fd); } return ok; } @@ -701,6 +716,8 @@ get_plane_infos(GstBuffer *buffer, const GstVideoInfo *info, struct gbm_device * } static uint32_t drm_format_from_gst_info(const GstVideoInfo *info) { + PRAGMA_DIAGNOSTIC_PUSH + PRAGMA_DIAGNOSTIC_IGNORED("-Wswitch-enum") switch (GST_VIDEO_INFO_FORMAT(info)) { case GST_VIDEO_FORMAT_YUY2: return DRM_FORMAT_YUYV; case GST_VIDEO_FORMAT_YVYU: return DRM_FORMAT_YVYU; @@ -734,6 +751,7 @@ static uint32_t drm_format_from_gst_info(const GstVideoInfo *info) { case GST_VIDEO_FORMAT_xBGR: return DRM_FORMAT_RGBX8888; default: return DRM_FORMAT_INVALID; } + PRAGMA_DIAGNOSTIC_POP } ATTR_CONST GstVideoFormat gst_video_format_from_drm_format(uint32_t drm_format) { @@ -1114,9 +1132,9 @@ static struct video_frame *frame_new_egl_imported(struct frame_interface *interf frame->drm_format = drm_format; frame->n_dmabuf_fds = n_planes; frame->dmabuf_fds[0] = planes[0].fd; - frame->dmabuf_fds[1] = planes[1].fd; - frame->dmabuf_fds[2] = planes[2].fd; - frame->dmabuf_fds[3] = planes[3].fd; + frame->dmabuf_fds[1] = n_planes >= 2 ? planes[1].fd : -1; + frame->dmabuf_fds[2] = n_planes >= 3 ? planes[2].fd : -1; + frame->dmabuf_fds[3] = n_planes >= 4 ? planes[3].fd : -1; frame->image = egl_image; frame->gl_frame.target = target; frame->gl_frame.name = texture; diff --git a/src/plugins/gstreamer_video_player/plugin.c b/src/plugins/gstreamer_video_player/plugin.c index 6b8be89c..c8d23a9d 100644 --- a/src/plugins/gstreamer_video_player/plugin.c +++ b/src/plugins/gstreamer_video_player/plugin.c @@ -55,7 +55,7 @@ static struct plugin { struct list_head players; } plugin; -DEFINE_LOCK_OPS(plugin, lock); +DEFINE_LOCK_OPS(plugin, lock) /// Add a player instance to the player collection. static void add_player(struct gstplayer_meta *meta) { @@ -117,7 +117,7 @@ static void remove_player(struct gstplayer_meta *meta) { * */ static void remove_player_locked(struct gstplayer_meta *meta) { - ASSERT_MUTEX_LOCKED(plugin.lock); + assert_mutex_locked(&plugin.lock); list_del(&meta->entry); } @@ -205,7 +205,7 @@ get_player_from_map_arg(struct std_value *arg, struct gstplayer **player_out, Fl return 0; } -static int ensure_initialized() { +static int ensure_initialized(void) { GError *gst_error; gboolean success; @@ -936,11 +936,18 @@ get_player_from_texture_id_with_custom_errmsg(int64_t texture_id, FlutterPlatfor plugin_lock(&plugin); int n_texture_ids = list_length(&plugin.players); - int64_t *texture_ids = alloca(sizeof(int64_t) * n_texture_ids); - int64_t *texture_ids_cursor = texture_ids; - list_for_each_entry(struct gstplayer_meta, meta, &plugin.players, entry) { - *texture_ids_cursor++ = gstplayer_get_texture_id(meta->player); + int64_t *texture_ids; + + if (n_texture_ids == 0) { + texture_ids = NULL; + } else { + texture_ids = alloca(sizeof(int64_t) * n_texture_ids); + int64_t *texture_ids_cursor = texture_ids; + + list_for_each_entry(struct gstplayer_meta, meta, &plugin.players, entry) { + *texture_ids_cursor++ = gstplayer_get_texture_id(meta->player); + } } plugin_unlock(&plugin); @@ -1060,8 +1067,7 @@ static int on_create_v2(const struct raw_std_value *arg, FlutterPlatformMessageR } else if (raw_std_value_is_string(arg)) { asset = raw_std_string_dup(arg); if (asset == NULL) { - ok = ENOMEM; - goto fail_respond_error; + return platch_respond_native_error_std(responsehandle, ENOMEM); } } else { return platch_respond_illegal_arg_std(responsehandle, "Expected `arg[0]` to be a String or null."); @@ -1079,11 +1085,12 @@ static int on_create_v2(const struct raw_std_value *arg, FlutterPlatformMessageR } else if (raw_std_value_is_string(arg)) { package_name = raw_std_string_dup(arg); if (package_name == NULL) { - ok = ENOMEM; - goto fail_respond_error; + ok = platch_respond_native_error_std(responsehandle, ENOMEM); + goto fail_free_asset; } } else { - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg[1]` to be a String or null."); + ok = platch_respond_illegal_arg_std(responsehandle, "Expected `arg[1]` to be a String or null."); + goto fail_free_asset; } } else { package_name = NULL; @@ -1098,11 +1105,12 @@ static int on_create_v2(const struct raw_std_value *arg, FlutterPlatformMessageR } else if (raw_std_value_is_string(arg)) { uri = raw_std_string_dup(arg); if (uri == NULL) { - ok = ENOMEM; - goto fail_respond_error; + ok = platch_respond_native_error_std(responsehandle, ENOMEM); + goto fail_free_package_name; } } else { - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg[2]` to be a String or null."); + ok = platch_respond_illegal_arg_std(responsehandle, "Expected `arg[2]` to be a String or null."); + goto fail_free_package_name; } } else { uri = NULL; @@ -1128,7 +1136,8 @@ static int on_create_v2(const struct raw_std_value *arg, FlutterPlatformMessageR } } else { invalid_format_hint: - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg[3]` to be one of 'ss', 'hls', 'dash', 'other' or null."); + ok = platch_respond_illegal_arg_std(responsehandle, "Expected `arg[3]` to be one of 'ss', 'hls', 'dash', 'other' or null."); + goto fail_free_uri; } } else { format_hint = FORMAT_HINT_NONE; @@ -1161,7 +1170,8 @@ static int on_create_v2(const struct raw_std_value *arg, FlutterPlatformMessageR if (headers != NULL) { gst_structure_free(headers); } - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg[4]` to be a map of strings or null."); + ok = platch_respond_illegal_arg_std(responsehandle, "Expected `arg[4]` to be a map of strings or null."); + goto fail_free_uri; } } else { headers = NULL; @@ -1176,49 +1186,65 @@ static int on_create_v2(const struct raw_std_value *arg, FlutterPlatformMessageR } else if (raw_std_value_is_string(arg)) { pipeline = raw_std_string_dup(arg); } else { - return platch_respond_illegal_arg_std(responsehandle, "Expected `arg[5]` to be a string or null."); + ok = platch_respond_illegal_arg_std(responsehandle, "Expected `arg[5]` to be a string or null."); + goto fail_free_headers; } } else { pipeline = NULL; } if ((asset ? 1 : 0) + (uri ? 1 : 0) + (pipeline ? 1 : 0) != 1) { - return platch_respond_illegal_arg_std(responsehandle, "Expected exactly one of `arg[0]`, `arg[2]` or `arg[5]` to be non-null."); + ok = platch_respond_illegal_arg_std(responsehandle, "Expected exactly one of `arg[0]`, `arg[2]` or `arg[5]` to be non-null."); + goto fail_free_pipeline; } // Create our actual player (this doesn't initialize it) if (asset != NULL) { player = gstplayer_new_from_asset(flutterpi, asset, package_name, /* play_video */ true, /* play_audio */ false, NULL); + } else if (uri != NULL) { + player = gstplayer_new_from_network(flutterpi, uri, format_hint, /* play_video */ true, /* play_audio */ false, NULL, headers); + + // player owns the headers now, except creation failed + if (player) { + headers = NULL; + } + } else if (pipeline != NULL) { + player = gstplayer_new_from_pipeline(flutterpi, uri, NULL); + } else { + UNREACHABLE(); + } - // gstplayer_new_from_network will construct a file:// URI out of the - // asset path internally. + if (asset != NULL) { free(asset); asset = NULL; - } else if (uri != NULL) { - player = gstplayer_new_from_network(flutterpi, uri, format_hint, /* play_video */ true, /* play_audio */ false, NULL, headers); + } + + if (package_name != NULL) { + free(package_name); + package_name = NULL; + } - // gstplayer_new_from_network will dup the uri internally. + if (uri != NULL) { free(uri); uri = NULL; - } else if (pipeline != NULL) { - player = gstplayer_new_from_pipeline(flutterpi, pipeline, NULL); + } + if (pipeline != NULL) { free(pipeline); - } else { - UNREACHABLE(); + pipeline = NULL; } if (player == NULL) { LOG_ERROR("Couldn't create gstreamer video player.\n"); - ok = EIO; - goto fail_respond_error; + ok = platch_respond_native_error_std(responsehandle, EIO); + goto fail_destroy_player; } - + // create a meta object so we can store the event channel name // of a player with it meta = create_meta(gstplayer_get_texture_id(player), player); if (meta == NULL) { - ok = ENOMEM; + ok = platch_respond_native_error_std(responsehandle, ENOMEM); goto fail_destroy_player; } @@ -1242,8 +1268,37 @@ static int on_create_v2(const struct raw_std_value *arg, FlutterPlatformMessageR fail_destroy_player: gstplayer_destroy(player); -fail_respond_error: - return platch_respond_native_error_std(responsehandle, ok); +fail_free_pipeline: + if (pipeline) { + free(pipeline); + pipeline = NULL; + } + +fail_free_headers: + if (headers != NULL) { + gst_structure_free(headers); + headers = NULL; + } + +fail_free_uri: + if (uri) { + free(uri); + uri = NULL; + } + +fail_free_package_name: + if (package_name) { + free(package_name); + package_name = NULL; + } + +fail_free_asset: + if (asset) { + free(asset); + asset = NULL; + } + + return ok; } static int on_create_with_audio(const struct raw_std_value *arg, FlutterPlatformMessageResponseHandle *responsehandle) { @@ -1273,10 +1328,7 @@ static int on_create_with_audio(const struct raw_std_value *arg, FlutterPlatform asset = NULL; } else if (raw_std_value_is_string(arg)) { asset = raw_std_string_dup(arg); - if (asset == NULL) { - ok = ENOMEM; - goto fail_respond_error; - } + ASSERT_NOT_NULL(asset); } else { return platch_respond_illegal_arg_std(responsehandle, "Expected `arg[0]` to be a String or null."); } @@ -1292,10 +1344,7 @@ static int on_create_with_audio(const struct raw_std_value *arg, FlutterPlatform package_name = NULL; } else if (raw_std_value_is_string(arg)) { package_name = raw_std_string_dup(arg); - if (package_name == NULL) { - ok = ENOMEM; - goto fail_respond_error; - } + ASSERT_NOT_NULL(package_name); } else { return platch_respond_illegal_arg_std(responsehandle, "Expected `arg[1]` to be a String or null."); } @@ -1312,8 +1361,7 @@ static int on_create_with_audio(const struct raw_std_value *arg, FlutterPlatform } else if (raw_std_value_is_string(arg)) { uri = raw_std_string_dup(arg); if (uri == NULL) { - ok = ENOMEM; - goto fail_respond_error; + ASSERT_NOT_NULL(uri); } } else { return platch_respond_illegal_arg_std(responsehandle, "Expected `arg[2]` to be a String or null."); @@ -1364,6 +1412,8 @@ static int on_create_with_audio(const struct raw_std_value *arg, FlutterPlatform } char *key_str = raw_std_string_dup(key); + ASSERT_NOT_NULL(key_str); + gst_structure_take_string(headers, key_str, raw_std_string_dup(value)); free(key_str); } else { @@ -1382,40 +1432,48 @@ static int on_create_with_audio(const struct raw_std_value *arg, FlutterPlatform } if ((asset ? 1 : 0) + (uri ? 1 : 0) != 1) { - return platch_respond_illegal_arg_std(responsehandle, "Expected exactly one of `arg[0]` or `arg[2]` to be non-null."); + platch_respond_illegal_arg_std(responsehandle, "Expected exactly one of `arg[0]` or `arg[2]` to be non-null."); + goto fail_free_headers; } // Create our actual player (this doesn't initialize it) if (asset != NULL) { player = gstplayer_new_from_asset(flutterpi, asset, package_name, /* play_video */ true, /* play_audio */ true, NULL); + } else if (uri != NULL) { + player = gstplayer_new_from_network(flutterpi, uri, format_hint, /* play_video */ true, /* play_audio */ true, NULL, headers); - // gstplayer_new_from_network will construct a file:// URI out of the - // asset path internally. + if (player) { + headers = NULL; + } + } else { + UNREACHABLE(); + } + + if (asset != NULL) { free(asset); asset = NULL; - } else if (uri != NULL) { - player = gstplayer_new_from_network(flutterpi, uri, format_hint, /* play_video */ true, /* play_audio */ true, NULL, headers); + } - // gstplayer_new_from_network will dup the uri internally. + if (package_name != NULL) { + free(package_name); + package_name = NULL; + } + + if (uri != NULL) { free(uri); uri = NULL; - } else { - UNREACHABLE(); } if (player == NULL) { LOG_ERROR("Couldn't create gstreamer video player.\n"); - ok = EIO; - goto fail_respond_error; + ok = platch_respond_native_error_std(responsehandle, EIO); + goto fail_free_headers; } // create a meta object so we can store the event channel name // of a player with it meta = create_meta(gstplayer_get_texture_id(player), player); - if (meta == NULL) { - ok = ENOMEM; - goto fail_destroy_player; - } + ASSERT_NOT_NULL(meta); gstplayer_set_userdata(player, meta); @@ -1425,6 +1483,7 @@ static int on_create_with_audio(const struct raw_std_value *arg, FlutterPlatform // Set a receiver on the videoEvents event channel ok = plugin_registry_set_receiver(meta->event_channel_name, kStandardMethodCall, on_receive_evch); if (ok != 0) { + platch_respond_native_error_std(responsehandle, ok); goto fail_remove_player; } @@ -1433,12 +1492,27 @@ static int on_create_with_audio(const struct raw_std_value *arg, FlutterPlatform fail_remove_player: remove_player(meta); destroy_meta(meta); - -fail_destroy_player: gstplayer_destroy(player); -fail_respond_error: - return platch_respond_native_error_std(responsehandle, ok); +fail_free_headers: + if (headers != NULL) { + gst_structure_free(headers); + headers = NULL; + } + + if (uri != NULL) { + free(uri); + } + + if (package_name != NULL) { + free(package_name); + } + + if (asset != NULL) { + free(asset); + } + + return ok; } static int on_dispose_v2(const struct raw_std_value *arg, FlutterPlatformMessageResponseHandle *responsehandle) { diff --git a/src/plugins/raw_keyboard.c b/src/plugins/raw_keyboard.c index a814088a..8ace05bb 100644 --- a/src/plugins/raw_keyboard.c +++ b/src/plugins/raw_keyboard.c @@ -424,7 +424,7 @@ ATTR_CONST static uint64_t physical_key_for_evdev_keycode(uint16_t evdev_keycode ATTR_CONST static uint64_t physical_key_for_xkb_keycode(xkb_keycode_t xkb_keycode) { assert(xkb_keycode >= 8); - return physical_key_for_evdev_keycode(xkb_keycode - 8); + return physical_key_for_evdev_keycode((uint16_t) (xkb_keycode - 8)); } ATTR_CONST static char eascii_to_lower(unsigned char n) { @@ -622,7 +622,7 @@ ATTR_CONST static uint32_t logical_key_for_xkb_keysym(xkb_keysym_t keysym) { if (keysym == XKB_KEY_yen) { return apply_flutter_key_plane(0x00022); } else if (keysym < 256) { - return apply_unicode_key_plane(eascii_to_lower(keysym)); + return apply_unicode_key_plane(eascii_to_lower((int8_t) keysym)); } else if (keysym >= 0xfd06 && keysym - 0xfd06 < ARRAY_SIZE(logical_keys_1)) { logical = logical_keys_1[keysym]; } else if (keysym >= 0x1008ff02 && keysym - 0x1008ff02 < ARRAY_SIZE(logical_keys_2)) { @@ -818,6 +818,7 @@ int rawkb_on_key_event( return ok; } + // NOLINTNEXTLINE(readability-suspicious-call-argument) ok = rawkb_send_gtk_keyevent(plain_codepoint, xkb_keysym, xkb_keycode, modifiers.u32, is_down); if (ok != 0) { return ok; @@ -826,7 +827,7 @@ int rawkb_on_key_event( return 0; } -static void assert_key_modifiers_work() { +static void assert_key_modifiers_work(void) { key_modifiers_t mods; memset(&mods, 0, sizeof(mods)); diff --git a/src/plugins/sentry/sentry.c b/src/plugins/sentry/sentry.c index d18031c2..0c28a0c5 100644 --- a/src/plugins/sentry/sentry.c +++ b/src/plugins/sentry/sentry.c @@ -301,7 +301,15 @@ static sentry_value_t raw_std_value_as_sentry_value(const struct raw_std_value * case kStdInt32: return sentry_value_new_int32(raw_std_value_as_int32(arg)); case kStdInt64: return sentry_value_new_int32((int32_t) raw_std_value_as_int64(arg)); case kStdFloat64: return sentry_value_new_double(raw_std_value_as_float64(arg)); + + case kStdUInt8Array: + case kStdInt32Array: + case kStdInt64Array: + case kStdFloat64Array: return sentry_value_new_null(); + + case kStdLargeInt: case kStdString: return sentry_value_new_string_n(raw_std_string_get_nonzero_terminated(arg), raw_std_string_get_length(arg)); + case kStdMap: { sentry_value_t map = sentry_value_new_object(); for_each_entry_in_raw_std_map(key, value, arg) { @@ -328,6 +336,7 @@ static sentry_value_t raw_std_value_as_sentry_value(const struct raw_std_value * return list; } + case kStdFloat32Array: return sentry_value_new_null(); default: return sentry_value_new_null(); } } @@ -755,7 +764,7 @@ static void on_method_call(void *userdata, const FlutterPlatformMessage *message } } -enum plugin_init_result sentry_plugin_deinit(struct flutterpi *flutterpi, void **userdata_out) { +enum plugin_init_result sentry_plugin_init(struct flutterpi *flutterpi, void **userdata_out) { struct sentry_plugin *plugin; int ok; @@ -780,7 +789,7 @@ enum plugin_init_result sentry_plugin_deinit(struct flutterpi *flutterpi, void * return PLUGIN_INIT_RESULT_INITIALIZED; } -void sentry_plugin_init(struct flutterpi *flutterpi, void *userdata) { +void sentry_plugin_fini(struct flutterpi *flutterpi, void *userdata) { struct sentry_plugin *plugin; ASSERT_NOT_NULL(userdata); @@ -794,4 +803,4 @@ void sentry_plugin_init(struct flutterpi *flutterpi, void *userdata) { free(plugin); } -FLUTTERPI_PLUGIN("sentry", sentry_plugin_init, sentry_plugin_deinit, NULL); +FLUTTERPI_PLUGIN("sentry", sentry, sentry_plugin_init, sentry_plugin_fini) diff --git a/src/plugins/text_input.c b/src/plugins/text_input.c index c57938cf..15674cd4 100644 --- a/src/plugins/text_input.c +++ b/src/plugins/text_input.c @@ -11,6 +11,7 @@ #include "flutter-pi.h" #include "pluginregistry.h" #include "util/asserts.h" +#include "util/logging.h" struct text_input { int64_t connection_id; @@ -33,16 +34,16 @@ struct text_input { * UTF8 utility functions */ static inline uint8_t utf8_symbol_length(uint8_t c) { - if ((c & 0b11110000) == 0b11110000) { + if ((c & 240 /* 0b11110000 */) == 240 /* 0b11110000 */) { return 4; } - if ((c & 0b11100000) == 0b11100000) { + if ((c & 224 /* 0b11100000 */) == 224 /* 0b11100000 */) { return 3; } - if ((c & 0b11000000) == 0b11000000) { + if ((c & 192 /* 0b11000000 */) == 192 /* 0b11000000 */) { return 2; } - if ((c & 0b10000000) == 0b10000000) { + if ((c & 128 /* 0b10000000 */) == 128 /* 0b10000000 */) { // XXX should we return 1 and don't care here? ASSERT_MSG(false, "Invalid UTF-8 character"); return 0; @@ -181,6 +182,7 @@ static int on_set_client(struct platch_obj *object, FlutterPlatformMessageRespon temp2 = jsobject_get(temp, "signed"); if (temp2 == NULL || temp2->type == kJsonNull) { has_allow_signs = false; + allow_signs = true; } else if (temp2->type == kJsonTrue || temp2->type == kJsonFalse) { has_allow_signs = true; allow_signs = temp2->type == kJsonTrue; @@ -191,6 +193,7 @@ static int on_set_client(struct platch_obj *object, FlutterPlatformMessageRespon temp2 = jsobject_get(temp, "decimal"); if (temp2 == NULL || temp2->type == kJsonNull) { has_allow_decimal = false; + allow_decimal = true; } else if (temp2->type == kJsonTrue || temp2->type == kJsonFalse) { has_allow_decimal = true; allow_decimal = temp2->type == kJsonTrue; @@ -239,14 +242,18 @@ static int on_set_client(struct platch_obj *object, FlutterPlatformMessageRespon int32_t new_id = (int32_t) object->json_arg.array[0].number_value; // everything okay, apply the new text editing config + text_input.has_allow_signs = has_allow_signs; + text_input.allow_signs = allow_signs; + text_input.has_allow_decimal = has_allow_decimal; + text_input.allow_decimal = allow_decimal; text_input.connection_id = new_id; text_input.autocorrect = autocorrect; text_input.input_action = input_action; text_input.input_type = input_type; if (autocorrect && !text_input.warned_about_autocorrect) { - printf( - "[text_input] warning: flutter requested native autocorrect, which" + LOG_ERROR( + "info: flutter requested native autocorrect, which" "is not supported by flutter-pi.\n" ); text_input.warned_about_autocorrect = true; @@ -527,7 +534,9 @@ int client_perform_action(double connection_id, enum text_input_action action) { } int client_perform_private_command(double connection_id, char *action, struct json_value *data) { - if (data != NULL && data->type != kJsonNull && data->type != kJsonObject) { + if (data == NULL) { + return EINVAL; + } else if (data->type != kJsonNull && data->type != kJsonObject) { return EINVAL; } diff --git a/src/texture_registry.c b/src/texture_registry.c index a2ab95b0..2b2fd961 100644 --- a/src/texture_registry.c +++ b/src/texture_registry.c @@ -202,6 +202,7 @@ struct texture *texture_new(struct texture_registry *reg) { if (ok != 0) { pthread_mutex_destroy(&texture->lock); free(texture); + return NULL; } return texture; @@ -301,7 +302,7 @@ texture_gl_external_texture_frame_callback(struct texture *texture, size_t width if (texture->next_frame != NULL) { /// TODO: If acquiring the texture frame fails, flutter will destroy the texture frame two times. /// So we'll probably have a segfault if that happens. - frame = counted_texture_frame_ref(texture->next_frame); + frame = texture->next_frame; } else { frame = NULL; } @@ -315,14 +316,17 @@ texture_gl_external_texture_frame_callback(struct texture *texture, size_t width ok = frame->unresolved_frame.resolve(width, height, frame->unresolved_frame.userdata, &frame->frame); if (ok != 0) { LOG_ERROR("Couldn't resolve texture frame.\n"); - counted_texture_frame_unrefp(&frame); counted_texture_frame_unrefp(&texture->next_frame); + texture_unlock(texture); + return false; } frame->unresolved_frame.destroy(frame->unresolved_frame.userdata); frame->is_resolved = true; } + frame = counted_texture_frame_ref(frame); + texture_unlock(texture); // only actually fill out the frame info when we have a frame. diff --git a/src/tracer.c b/src/tracer.c index 9177fede..99623cc7 100644 --- a/src/tracer.c +++ b/src/tracer.c @@ -50,7 +50,7 @@ struct tracer *tracer_new_with_cbs( return NULL; } -struct tracer *tracer_new_with_stubs() { +struct tracer *tracer_new_with_stubs(void) { struct tracer *tracer; tracer = malloc(sizeof *tracer); diff --git a/src/tracer.h b/src/tracer.h index 896ee686..11ab967d 100644 --- a/src/tracer.h +++ b/src/tracer.h @@ -20,9 +20,9 @@ struct tracer *tracer_new_with_cbs( FlutterEngineTraceEventInstantFnPtr trace_instant ); -struct tracer *tracer_new_with_stubs(); +struct tracer *tracer_new_with_stubs(void); -DECLARE_REF_OPS(tracer); +DECLARE_REF_OPS(tracer) void __tracer_begin(struct tracer *tracer, const char *name); diff --git a/src/user_input.c b/src/user_input.c index 652d7df5..17531743 100644 --- a/src/user_input.c +++ b/src/user_input.c @@ -270,7 +270,7 @@ static void on_close(int fd, void *userdata) { ASSERT_NOT_NULL(userdata); input = userdata; - return input->interface.close(fd, input->userdata); + input->interface.close(fd, input->userdata); } static const struct libinput_interface libinput_interface = { .open_restricted = on_open, .close_restricted = on_close }; @@ -376,6 +376,8 @@ void user_input_destroy(struct user_input *input) { event = libinput_get_event(input->libinput); event_type = libinput_event_get_type(event); + PRAGMA_DIAGNOSTIC_PUSH + PRAGMA_DIAGNOSTIC_IGNORED("-Wswitch-enum") switch (event_type) { case LIBINPUT_EVENT_DEVICE_REMOVED: ok = on_device_removed(input, event, 0, false); @@ -383,6 +385,7 @@ void user_input_destroy(struct user_input *input) { break; default: break; } + PRAGMA_DIAGNOSTIC_POP libinput_event_destroy(event); } @@ -746,21 +749,21 @@ static int on_key_event(struct user_input *input, struct libinput_event *event) // we emit UTF8 unconditionally here, // maybe we should check if codepoint is a control character? if (isprint(codepoint)) { - utf8_character[0] = codepoint; + utf8_character[0] = (uint8_t) codepoint; } } else if (codepoint < 0x800) { - utf8_character[0] = 0xc0 | (codepoint >> 6); + utf8_character[0] = 0xc0 | (uint8_t) (codepoint >> 6); utf8_character[1] = 0x80 | (codepoint & 0x3f); } else if (codepoint < 0x10000) { // the console keyboard driver of the linux kernel checks // at this point whether `codepoint` is a UTF16 high surrogate (U+D800 to U+DFFF) // or U+FFFF and returns without emitting UTF8 in that case. // don't know whether we should do this here too - utf8_character[0] = 0xe0 | (codepoint >> 12); + utf8_character[0] = 0xe0 | (uint8_t) (codepoint >> 12); utf8_character[1] = 0x80 | ((codepoint >> 6) & 0x3f); utf8_character[2] = 0x80 | (codepoint & 0x3f); } else if (codepoint < 0x110000) { - utf8_character[0] = 0xf0 | (codepoint >> 18); + utf8_character[0] = 0xf0 | (uint8_t) (codepoint >> 18); utf8_character[1] = 0x80 | ((codepoint >> 12) & 0x3f); utf8_character[2] = 0x80 | ((codepoint >> 6) & 0x3f); utf8_character[3] = 0x80 | (codepoint & 0x3f); @@ -1356,6 +1359,11 @@ static int process_libinput_events(struct user_input *input, uint64_t timestamp) event = libinput_get_event(input->libinput); event_type = libinput_event_get_type(event); + // We explicitly don't want to handle every event type here. + // Otherwise we'd need to add a new `case` every libinput introduces + // a new event. + PRAGMA_DIAGNOSTIC_PUSH + PRAGMA_DIAGNOSTIC_IGNORED("-Wswitch-enum") switch (event_type) { case LIBINPUT_EVENT_DEVICE_ADDED: ok = on_device_added(input, event, timestamp); @@ -1481,6 +1489,7 @@ static int process_libinput_events(struct user_input *input, uint64_t timestamp) #endif default: break; } + PRAGMA_DIAGNOSTIC_POP libinput_event_destroy(event); } @@ -1514,8 +1523,8 @@ int user_input_on_fd_ready(struct user_input *input) { // record cursor state before handling events cursor_enabled_before = input->n_cursor_devices > 0; - cursor_x_before = round(input->cursor_x); - cursor_y_before = round(input->cursor_y); + cursor_x_before = (int) round(input->cursor_x); + cursor_y_before = (int) round(input->cursor_y); // handle all available libinput events ok = process_libinput_events(input, timestamp); @@ -1526,8 +1535,8 @@ int user_input_on_fd_ready(struct user_input *input) { // record cursor state after handling events cursor_enabled = input->n_cursor_devices > 0; - cursor_x = round(input->cursor_x); - cursor_y = round(input->cursor_y); + cursor_x = (int) round(input->cursor_x); + cursor_y = (int) round(input->cursor_y); // make sure we've dispatched all the flutter pointer events flush_pointer_events(input); diff --git a/src/util/asserts.h b/src/util/asserts.h index b2926833..7ca0d312 100644 --- a/src/util/asserts.h +++ b/src/util/asserts.h @@ -18,18 +18,7 @@ #define ASSERT_EQUALS_MSG(__a, __b, __msg) ASSERT_MSG((__a) == (__b), __msg) #define ASSERT_EGL_TRUE(__var) assert((__var) == EGL_TRUE) #define ASSERT_EGL_TRUE_MSG(__var, __msg) ASSERT_MSG((__var) == EGL_TRUE, __msg) -#define ASSERT_MUTEX_LOCKED(__mutex) \ - assert(({ \ - bool result; \ - int r = pthread_mutex_trylock(&(__mutex)); \ - if (r == 0) { \ - pthread_mutex_unlock(&(__mutex)); \ - result = false; \ - } else { \ - result = true; \ - } \ - result; \ - })) + #define ASSERT_ZERO(__var) assert((__var) == 0) #define ASSERT_ZERO_MSG(__var, __msg) ASSERT_MSG((__var) == 0, __msg) diff --git a/src/util/collection.c b/src/util/collection.c index 323f4fb2..dcc50394 100644 --- a/src/util/collection.c +++ b/src/util/collection.c @@ -2,14 +2,14 @@ static pthread_mutexattr_t default_mutex_attrs; -static void init_default_mutex_attrs() { +static void init_default_mutex_attrs(void) { pthread_mutexattr_init(&default_mutex_attrs); #ifdef DEBUG pthread_mutexattr_settype(&default_mutex_attrs, PTHREAD_MUTEX_ERRORCHECK); #endif } -const pthread_mutexattr_t *get_default_mutex_attrs() { +const pthread_mutexattr_t *get_default_mutex_attrs(void) { static pthread_once_t init_once_ctl = PTHREAD_ONCE_INIT; pthread_once(&init_once_ctl, init_default_mutex_attrs); diff --git a/src/util/collection.h b/src/util/collection.h index bbc48d23..d466ae35 100644 --- a/src/util/collection.h +++ b/src/util/collection.h @@ -108,7 +108,7 @@ static inline void *uint32_to_ptr(const uint32_t v) { #define MAX_ALIGNMENT (__alignof__(max_align_t)) #define IS_MAX_ALIGNED(num) ((num) % MAX_ALIGNMENT == 0) -#define DOUBLE_TO_FP1616(v) ((uint32_t) ((v) *65536)) +#define DOUBLE_TO_FP1616(v) ((uint32_t) ((v) * 65536)) #define DOUBLE_TO_FP1616_ROUNDED(v) (((uint32_t) (v)) << 16) typedef void (*void_callback_t)(void *userdata); @@ -117,6 +117,6 @@ ATTR_PURE static inline bool streq(const char *a, const char *b) { return strcmp(a, b) == 0; } -const pthread_mutexattr_t *get_default_mutex_attrs(); +const pthread_mutexattr_t *get_default_mutex_attrs(void); #endif // _FLUTTERPI_SRC_UTIL_COLLECTION_H diff --git a/src/util/lock_ops.h b/src/util/lock_ops.h index dc0a31a0..648c9791 100644 --- a/src/util/lock_ops.h +++ b/src/util/lock_ops.h @@ -59,4 +59,18 @@ (void) ok; \ } +#ifdef DEBUG +static inline void assert_mutex_locked(pthread_mutex_t *mutex) { + int result = pthread_mutex_trylock(mutex); + if (result == 0) { + pthread_mutex_unlock(mutex); + ASSERT_MSG(false, "Mutex is not locked."); + } +} +#else +static inline void assert_mutex_locked(pthread_mutex_t *mutex) { + (void) mutex; +} +#endif + #endif // _FLUTTERPI_SRC_UTIL_LOCK_OPS_H diff --git a/src/util/macros.h b/src/util/macros.h index e0f7933a..8be7fc43 100644 --- a/src/util/macros.h +++ b/src/util/macros.h @@ -122,6 +122,9 @@ #if __has_attribute(noreturn) #define HAVE_FUNC_ATTRIBUTE_NORETURN #endif +#if __has_attribute(suppress) + #define HAVE_STMT_ATTRIBUTE_SUPPRESS +#endif /** * __builtin_expect macros @@ -405,6 +408,12 @@ #define ATTR_NOINLINE #endif +#ifdef HAVE_STMT_ATTRIBUTE_SUPPRESS + #define ANALYZER_SUPPRESS(stmt) __attribute__((suppress)) stmt +#else + #define ANALYZER_SUPPRESS(stmt) stmt +#endif + /** * Check that STRUCT::FIELD can hold MAXVAL. We use a lot of bitfields * in Mesa/gallium. We have to be sure they're of sufficient size to @@ -421,7 +430,7 @@ } while (0) /** Compute ceiling of integer quotient of A divided by B. */ -#define DIV_ROUND_UP(A, B) (((A) + (B) -1) / (B)) +#define DIV_ROUND_UP(A, B) (((A) + (B) - 1) / (B)) /** * Clamp X to [MIN,MAX]. Turn NaN into MIN, arbitrarily. @@ -450,10 +459,10 @@ #define MAX4(A, B, C, D) ((A) > (B) ? MAX3(A, C, D) : MAX3(B, C, D)) /** Align a value to a power of two */ -#define ALIGN_POT(x, pot_align) (((x) + (pot_align) -1) & ~((pot_align) -1)) +#define ALIGN_POT(x, pot_align) (((x) + (pot_align) - 1) & ~((pot_align) - 1)) /** Checks is a value is a power of two. Does not handle zero. */ -#define IS_POT(v) (((v) & ((v) -1)) == 0) +#define IS_POT(v) (((v) & ((v) - 1)) == 0) /** Set a single bit */ #define BITFIELD_BIT(b) (1u << (b)) @@ -547,21 +556,27 @@ typedef int lock_cap_t; #if defined(__clang__) #define PRAGMA_DIAGNOSTIC_PUSH _Pragma("clang diagnostic push") #define PRAGMA_DIAGNOSTIC_POP _Pragma("clang diagnostic pop") - #define PRAGMA_DIAGNOSTIC_ERROR(X) DO_PRAGMA(clang diagnostic error #X) - #define PRAGMA_DIAGNOSTIC_WARNING(X) DO_PRAGMA(clang diagnostic warning #X) - #define PRAGMA_DIAGNOSTIC_IGNORED(X) DO_PRAGMA(clang diagnostic ignored #X) + #define PRAGMA_DIAGNOSTIC_ERROR(X) DO_PRAGMA(clang diagnostic error X) + #define PRAGMA_DIAGNOSTIC_WARNING(X) DO_PRAGMA(clang diagnostic warning X) + #define PRAGMA_DIAGNOSTIC_IGNORED(X) DO_PRAGMA(clang diagnostic ignored X) + #define PRAGMA_GCC_DIAGNOSTIC_IGNORED(X) + #define PRAGMA_CLANG_DIAGNOSTIC_IGNORED(X) DO_PRAGMA(clang diagnostic ignored X) #elif defined(__GNUC__) #define PRAGMA_DIAGNOSTIC_PUSH _Pragma("GCC diagnostic push") #define PRAGMA_DIAGNOSTIC_POP _Pragma("GCC diagnostic pop") - #define PRAGMA_DIAGNOSTIC_ERROR(X) DO_PRAGMA(GCC diagnostic error #X) - #define PRAGMA_DIAGNOSTIC_WARNING(X) DO_PRAGMA(GCC diagnostic warning #X) - #define PRAGMA_DIAGNOSTIC_IGNORED(X) DO_PRAGMA(GCC diagnostic ignored #X) + #define PRAGMA_DIAGNOSTIC_ERROR(X) DO_PRAGMA(GCC diagnostic error X) + #define PRAGMA_DIAGNOSTIC_WARNING(X) DO_PRAGMA(GCC diagnostic warning X) + #define PRAGMA_DIAGNOSTIC_IGNORED(X) DO_PRAGMA(GCC diagnostic ignored X) + #define PRAGMA_GCC_DIAGNOSTIC_IGNORED(X) DO_PRAGMA(GCC diagnostic ignored X) + #define PRAGMA_CLANG_DIAGNOSTIC_IGNORED(X) #else #define PRAGMA_DIAGNOSTIC_PUSH #define PRAGMA_DIAGNOSTIC_POP #define PRAGMA_DIAGNOSTIC_ERROR(X) #define PRAGMA_DIAGNOSTIC_WARNING(X) #define PRAGMA_DIAGNOSTIC_IGNORED(X) + #define PRAGMA_GCC_DIAGNOSTIC_IGNORED(X) + #define PRAGMA_CLANG_DIAGNOSTIC_IGNORED(X) #endif #define PASTE2(a, b) a##b @@ -588,7 +603,7 @@ typedef int lock_cap_t; #define UNIMPLEMENTED() \ do { \ - fprintf(stderr, "%s%s:%u: Unimplemented\n", __FILE__, __func__, __LINE__); \ + fprintf(stderr, "%s%s:%d: Unimplemented\n", __FILE__, __func__, __LINE__); \ TRAP(); \ } while (0) diff --git a/src/util/uuid.h b/src/util/uuid.h index 160d16d4..db2a679e 100644 --- a/src/util/uuid.h +++ b/src/util/uuid.h @@ -21,9 +21,9 @@ typedef struct { }) #define CONST_UUID(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15) \ - ((const uuid_t){ \ + { \ .bytes = { _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15 }, \ - }) + } static inline bool uuid_equals(const uuid_t a, const uuid_t b) { return memcmp(&a, &b, sizeof(uuid_t)) == 0; diff --git a/src/vk_renderer.c b/src/vk_renderer.c index 4a96b810..3c2abe5a 100644 --- a/src/vk_renderer.c +++ b/src/vk_renderer.c @@ -52,7 +52,7 @@ static VkBool32 on_debug_utils_message( UNUSED void *userdata ) { LOG_DEBUG( - "[%s] (%d, %s) %s (queues: %d, cmdbufs: %d, objects: %d)\n", + "[%s] (%d, %s) %s (queues: %u, cmdbufs: %u, objects: %u)\n", severity == VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT ? "VERBOSE" : severity == VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT ? "INFO" : severity == VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT ? "WARNING" : @@ -160,7 +160,7 @@ struct vk_renderer { const char **enabled_device_extensions; }; -MUST_CHECK struct vk_renderer *vk_renderer_new() { +MUST_CHECK struct vk_renderer *vk_renderer_new(void) { PFN_vkDestroyDebugUtilsMessengerEXT destroy_debug_utils_messenger; PFN_vkCreateDebugUtilsMessengerEXT create_debug_utils_messenger; VkDebugUtilsMessengerEXT debug_utils_messenger; diff --git a/src/vk_renderer.h b/src/vk_renderer.h index 6e25b145..6ce8a61a 100644 --- a/src/vk_renderer.h +++ b/src/vk_renderer.h @@ -55,7 +55,7 @@ struct vk_renderer; * * @return New vulkan renderer instance. */ -struct vk_renderer *vk_renderer_new(); +struct vk_renderer *vk_renderer_new(void); void vk_renderer_destroy(struct vk_renderer *renderer); diff --git a/src/vulkan.c b/src/vulkan.c new file mode 100644 index 00000000..0b67b97e --- /dev/null +++ b/src/vulkan.c @@ -0,0 +1,81 @@ +#include "vulkan.h" + +#include "util/macros.h" + +const char *vk_strerror(VkResult result) { + PRAGMA_DIAGNOSTIC_PUSH + + // We'd really like to use PRAGMA_DIAGNOSTIC_WARNING for "-Wswitch-enum" here, + // but CodeChecker makes it hard to distinguish between warnings and errors + // and will always treat this an error. + // So ignore it for now. + PRAGMA_DIAGNOSTIC_IGNORED("-Wswitch-enum") + switch (result) { + case VK_SUCCESS: return "VK_SUCCESS"; + case VK_NOT_READY: return "VK_NOT_READY"; + case VK_TIMEOUT: return "VK_TIMEOUT"; + case VK_EVENT_SET: return "VK_EVENT_SET"; + case VK_EVENT_RESET: return "VK_EVENT_RESET"; + case VK_INCOMPLETE: return "VK_INCOMPLETE"; + case VK_ERROR_OUT_OF_HOST_MEMORY: return "VK_ERROR_OUT_OF_HOST_MEMORY"; + case VK_ERROR_OUT_OF_DEVICE_MEMORY: return "VK_ERROR_OUT_OF_DEVICE_MEMORY"; + case VK_ERROR_INITIALIZATION_FAILED: return "VK_ERROR_INITIALIZATION_FAILED"; + case VK_ERROR_DEVICE_LOST: return "VK_ERROR_DEVICE_LOST"; + case VK_ERROR_MEMORY_MAP_FAILED: return "VK_ERROR_MEMORY_MAP_FAILED"; + case VK_ERROR_LAYER_NOT_PRESENT: return "VK_ERROR_LAYER_NOT_PRESENT"; + case VK_ERROR_EXTENSION_NOT_PRESENT: return "VK_ERROR_EXTENSION_NOT_PRESENT"; + case VK_ERROR_FEATURE_NOT_PRESENT: return "VK_ERROR_FEATURE_NOT_PRESENT"; + case VK_ERROR_INCOMPATIBLE_DRIVER: return "VK_ERROR_INCOMPATIBLE_DRIVER"; + case VK_ERROR_TOO_MANY_OBJECTS: return "VK_ERROR_TOO_MANY_OBJECTS"; + case VK_ERROR_FORMAT_NOT_SUPPORTED: return "VK_ERROR_FORMAT_NOT_SUPPORTED"; + case VK_ERROR_FRAGMENTED_POOL: return "VK_ERROR_FRAGMENTED_POOL"; +#if VK_HEADER_VERSION >= 131 + case VK_ERROR_UNKNOWN: return "VK_ERROR_UNKNOWN"; +#endif + case VK_ERROR_OUT_OF_POOL_MEMORY: return "VK_ERROR_OUT_OF_POOL_MEMORY"; + case VK_ERROR_INVALID_EXTERNAL_HANDLE: return "VK_ERROR_INVALID_EXTERNAL_HANDLE"; +#if VK_HEADER_VERSION >= 131 + case VK_ERROR_FRAGMENTATION: return "VK_ERROR_FRAGMENTATION"; + case VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS: return "VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS"; +#endif +#if VK_HEADER_VERSION >= 204 + case VK_PIPELINE_COMPILE_REQUIRED: return "VK_PIPELINE_COMPILE_REQUIRED_EXT"; +#endif + case VK_ERROR_SURFACE_LOST_KHR: return "VK_ERROR_SURFACE_LOST_KHR"; + case VK_ERROR_NATIVE_WINDOW_IN_USE_KHR: return "VK_ERROR_NATIVE_WINDOW_IN_USE_KHR"; + case VK_SUBOPTIMAL_KHR: return "VK_SUBOPTIMAL_KHR"; + case VK_ERROR_OUT_OF_DATE_KHR: return "VK_ERROR_OUT_OF_DATE_KHR"; + case VK_ERROR_INCOMPATIBLE_DISPLAY_KHR: return "VK_ERROR_INCOMPATIBLE_DISPLAY_KHR"; + case VK_ERROR_VALIDATION_FAILED_EXT: return "VK_ERROR_VALIDATION_FAILED_EXT"; + case VK_ERROR_INVALID_SHADER_NV: return "VK_ERROR_INVALID_SHADER_NV"; +#if VK_HEADER_VERSION >= 218 && VK_ENABLE_BETA_EXTENSIONS + case VK_ERROR_IMAGE_USAGE_NOT_SUPPORTED_KHR: return "VK_ERROR_IMAGE_USAGE_NOT_SUPPORTED_KHR"; + case VK_ERROR_VIDEO_PICTURE_LAYOUT_NOT_SUPPORTED_KHR: return "VK_ERROR_VIDEO_PICTURE_LAYOUT_NOT_SUPPORTED_KHR"; + case VK_ERROR_VIDEO_PROFILE_OPERATION_NOT_SUPPORTED_KHR: return "VK_ERROR_VIDEO_PROFILE_OPERATION_NOT_SUPPORTED_KHR"; + case VK_ERROR_VIDEO_PROFILE_FORMAT_NOT_SUPPORTED_KHR: return "VK_ERROR_VIDEO_PROFILE_FORMAT_NOT_SUPPORTED_KHR"; + case VK_ERROR_VIDEO_PROFILE_CODEC_NOT_SUPPORTED_KHR: return "VK_ERROR_VIDEO_PROFILE_CODEC_NOT_SUPPORTED_KHR"; + case VK_ERROR_VIDEO_STD_VERSION_NOT_SUPPORTED_KHR: return "VK_ERROR_VIDEO_STD_VERSION_NOT_SUPPORTED_KHR"; +#endif +#if VK_HEADER_VERSION >= 89 + case VK_ERROR_INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT_EXT: return "VK_ERROR_INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT_EXT"; +#endif +#if VK_HEADER_VERSION >= 204 + case VK_ERROR_NOT_PERMITTED_KHR: return "VK_ERROR_NOT_PERMITTED_KHR"; +#endif +#if VK_HEADER_VERSION >= 105 + case VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT: return "VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT"; +#endif +#if VK_HEADER_VERSION >= 135 + case VK_THREAD_IDLE_KHR: return "VK_THREAD_IDLE_KHR"; + case VK_THREAD_DONE_KHR: return "VK_THREAD_DONE_KHR"; + case VK_OPERATION_DEFERRED_KHR: return "VK_OPERATION_DEFERRED_KHR"; + case VK_OPERATION_NOT_DEFERRED_KHR: return "VK_OPERATION_NOT_DEFERRED_KHR"; +#endif +#if VK_HEADER_VERSION >= 213 + case VK_ERROR_COMPRESSION_EXHAUSTED_EXT: return "VK_ERROR_COMPRESSION_EXHAUSTED_EXT"; +#endif + case VK_RESULT_MAX_ENUM: + default: return ""; + } + PRAGMA_DIAGNOSTIC_POP +} diff --git a/src/vulkan.h b/src/vulkan.h index 3821805a..e665af4c 100644 --- a/src/vulkan.h +++ b/src/vulkan.h @@ -16,75 +16,9 @@ #include -static inline const char *vk_strerror(VkResult result) { - switch (result) { - case VK_SUCCESS: return "VK_SUCCESS"; - case VK_NOT_READY: return "VK_NOT_READY"; - case VK_TIMEOUT: return "VK_TIMEOUT"; - case VK_EVENT_SET: return "VK_EVENT_SET"; - case VK_EVENT_RESET: return "VK_EVENT_RESET"; - case VK_INCOMPLETE: return "VK_INCOMPLETE"; - case VK_ERROR_OUT_OF_HOST_MEMORY: return "VK_ERROR_OUT_OF_HOST_MEMORY"; - case VK_ERROR_OUT_OF_DEVICE_MEMORY: return "VK_ERROR_OUT_OF_DEVICE_MEMORY"; - case VK_ERROR_INITIALIZATION_FAILED: return "VK_ERROR_INITIALIZATION_FAILED"; - case VK_ERROR_DEVICE_LOST: return "VK_ERROR_DEVICE_LOST"; - case VK_ERROR_MEMORY_MAP_FAILED: return "VK_ERROR_MEMORY_MAP_FAILED"; - case VK_ERROR_LAYER_NOT_PRESENT: return "VK_ERROR_LAYER_NOT_PRESENT"; - case VK_ERROR_EXTENSION_NOT_PRESENT: return "VK_ERROR_EXTENSION_NOT_PRESENT"; - case VK_ERROR_FEATURE_NOT_PRESENT: return "VK_ERROR_FEATURE_NOT_PRESENT"; - case VK_ERROR_INCOMPATIBLE_DRIVER: return "VK_ERROR_INCOMPATIBLE_DRIVER"; - case VK_ERROR_TOO_MANY_OBJECTS: return "VK_ERROR_TOO_MANY_OBJECTS"; - case VK_ERROR_FORMAT_NOT_SUPPORTED: return "VK_ERROR_FORMAT_NOT_SUPPORTED"; - case VK_ERROR_FRAGMENTED_POOL: return "VK_ERROR_FRAGMENTED_POOL"; -#if VK_HEADER_VERSION >= 131 - case VK_ERROR_UNKNOWN: return "VK_ERROR_UNKNOWN"; -#endif - case VK_ERROR_OUT_OF_POOL_MEMORY: return "VK_ERROR_OUT_OF_POOL_MEMORY"; - case VK_ERROR_INVALID_EXTERNAL_HANDLE: return "VK_ERROR_INVALID_EXTERNAL_HANDLE"; -#if VK_HEADER_VERSION >= 131 - case VK_ERROR_FRAGMENTATION: return "VK_ERROR_FRAGMENTATION"; - case VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS: return "VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS"; -#endif -#if VK_HEADER_VERSION >= 204 - case VK_PIPELINE_COMPILE_REQUIRED: return "VK_PIPELINE_COMPILE_REQUIRED_EXT"; -#endif - case VK_ERROR_SURFACE_LOST_KHR: return "VK_ERROR_SURFACE_LOST_KHR"; - case VK_ERROR_NATIVE_WINDOW_IN_USE_KHR: return "VK_ERROR_NATIVE_WINDOW_IN_USE_KHR"; - case VK_SUBOPTIMAL_KHR: return "VK_SUBOPTIMAL_KHR"; - case VK_ERROR_OUT_OF_DATE_KHR: return "VK_ERROR_OUT_OF_DATE_KHR"; - case VK_ERROR_INCOMPATIBLE_DISPLAY_KHR: return "VK_ERROR_INCOMPATIBLE_DISPLAY_KHR"; - case VK_ERROR_VALIDATION_FAILED_EXT: return "VK_ERROR_VALIDATION_FAILED_EXT"; - case VK_ERROR_INVALID_SHADER_NV: return "VK_ERROR_INVALID_SHADER_NV"; -#if VK_HEADER_VERSION >= 218 && VK_ENABLE_BETA_EXTENSIONS - case VK_ERROR_IMAGE_USAGE_NOT_SUPPORTED_KHR: return "VK_ERROR_IMAGE_USAGE_NOT_SUPPORTED_KHR"; - case VK_ERROR_VIDEO_PICTURE_LAYOUT_NOT_SUPPORTED_KHR: return "VK_ERROR_VIDEO_PICTURE_LAYOUT_NOT_SUPPORTED_KHR"; - case VK_ERROR_VIDEO_PROFILE_OPERATION_NOT_SUPPORTED_KHR: return "VK_ERROR_VIDEO_PROFILE_OPERATION_NOT_SUPPORTED_KHR"; - case VK_ERROR_VIDEO_PROFILE_FORMAT_NOT_SUPPORTED_KHR: return "VK_ERROR_VIDEO_PROFILE_FORMAT_NOT_SUPPORTED_KHR"; - case VK_ERROR_VIDEO_PROFILE_CODEC_NOT_SUPPORTED_KHR: return "VK_ERROR_VIDEO_PROFILE_CODEC_NOT_SUPPORTED_KHR"; - case VK_ERROR_VIDEO_STD_VERSION_NOT_SUPPORTED_KHR: return "VK_ERROR_VIDEO_STD_VERSION_NOT_SUPPORTED_KHR"; -#endif -#if VK_HEADER_VERSION >= 89 - case VK_ERROR_INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT_EXT: return "VK_ERROR_INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT_EXT"; -#endif -#if VK_HEADER_VERSION >= 204 - case VK_ERROR_NOT_PERMITTED_KHR: return "VK_ERROR_NOT_PERMITTED_KHR"; -#endif -#if VK_HEADER_VERSION >= 105 - case VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT: return "VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT"; -#endif -#if VK_HEADER_VERSION >= 135 - case VK_THREAD_IDLE_KHR: return "VK_THREAD_IDLE_KHR"; - case VK_THREAD_DONE_KHR: return "VK_THREAD_DONE_KHR"; - case VK_OPERATION_DEFERRED_KHR: return "VK_OPERATION_DEFERRED_KHR"; - case VK_OPERATION_NOT_DEFERRED_KHR: return "VK_OPERATION_NOT_DEFERRED_KHR"; -#endif -#if VK_HEADER_VERSION >= 213 - case VK_ERROR_COMPRESSION_EXHAUSTED_EXT: return "VK_ERROR_COMPRESSION_EXHAUSTED_EXT"; -#endif - default: return ""; - } -} +const char *vk_strerror(VkResult result); -#define LOG_VK_ERROR(result, fmt, ...) LOG_ERROR(fmt ": %s\n", __VA_ARGS__ vk_strerror(result)) +#define LOG_VK_ERROR_FMT(result, fmt, ...) LOG_ERROR(fmt ": %s\n", __VA_ARGS__ vk_strerror(result)) +#define LOG_VK_ERROR(result, str) LOG_ERROR(str ": %s\n", vk_strerror(result)) #endif // _FLUTTERPI_SRC_VULKAN_H diff --git a/src/window.c b/src/window.c index 687a747f..920878ee 100644 --- a/src/window.c +++ b/src/window.c @@ -73,7 +73,7 @@ struct window { * To calculate this, the physical dimensions of the display are required. If there are no physical dimensions, * this will default to 1.0. */ - double pixel_ratio; + float pixel_ratio; /** * @brief Whether we have physical screen dimensions and @ref width_mm and @ref height_mm contain usable values. @@ -300,7 +300,7 @@ static int window_init( // clang-format on ) { enum device_orientation original_orientation; - double pixel_ratio; + float pixel_ratio; ASSERT_NOT_NULL(window); ASSERT_NOT_NULL(tracer); @@ -317,7 +317,7 @@ static int window_init( ); pixel_ratio = 1.0; } else { - pixel_ratio = (10.0 * width) / (width_mm * 38.0); + pixel_ratio = (10.0f * width) / (width_mm * 38.0f); int horizontal_dpi = (int) (width / (width_mm / 25.4)); int vertical_dpi = (int) (height / (height_mm / 25.4)); @@ -943,9 +943,8 @@ MUST_CHECK struct window *kms_window_new( has_dimensions = true; width_mm = selected_connector->variable_state.width_mm; height_mm = selected_connector->variable_state.height_mm; - } else if (selected_connector->type == DRM_MODE_CONNECTOR_DSI - && selected_connector->variable_state.width_mm == 0 - && selected_connector->variable_state.height_mm == 0) { + } else if (selected_connector->type == DRM_MODE_CONNECTOR_DSI && selected_connector->variable_state.width_mm == 0 && + selected_connector->variable_state.height_mm == 0) { // assume this is the official Raspberry Pi DSI display. has_dimensions = true; width_mm = 155; @@ -985,7 +984,7 @@ MUST_CHECK struct window *kms_window_new( mode_get_vrefresh(selected_mode), width_mm, height_mm, - window->pixel_ratio, + (double) (window->pixel_ratio), has_forced_pixel_format ? get_pixfmt_info(forced_pixel_format)->name : "(any)" ); @@ -1219,6 +1218,7 @@ static int kms_window_push_composition_locked(struct window *window, struct fl_l req = kms_req_builder_build(builder); if (req == NULL) { + ok = EIO; goto fail_unref_builder; } @@ -1227,6 +1227,7 @@ static int kms_window_push_composition_locked(struct window *window, struct fl_l frame = malloc(sizeof *frame); if (frame == NULL) { + ok = ENOMEM; goto fail_unref_req; } @@ -1428,11 +1429,15 @@ static struct render_surface *kms_window_get_render_surface_internal(struct wind drm_plane_for_each_modified_format(plane, count_modifiers_for_pixel_format, &context); n_allowed_modifiers = context.n_modifiers; - allowed_modifiers = calloc(n_allowed_modifiers, sizeof(*context.modifiers)); - context.modifiers = allowed_modifiers; + if (n_allowed_modifiers) { + allowed_modifiers = calloc(n_allowed_modifiers, sizeof(*context.modifiers)); + context.modifiers = allowed_modifiers; - // Next, fill context.modifiers with the allowed modifiers. - drm_plane_for_each_modified_format(plane, extract_modifiers_for_pixel_format, &context); + // Next, fill context.modifiers with the allowed modifiers. + drm_plane_for_each_modified_format(plane, extract_modifiers_for_pixel_format, &context); + } else { + allowed_modifiers = NULL; + } break; } } @@ -1750,6 +1755,10 @@ static EGLSurface dummy_window_get_egl_surface(struct window *window) { if (window->renderer_type == kOpenGL_RendererType) { struct render_surface *render_surface = dummy_window_get_render_surface_internal(window, false, VEC2I(0, 0)); + if (render_surface == NULL) { + return EGL_NO_SURFACE; + } + return egl_gbm_render_surface_get_egl_surface(CAST_EGL_GBM_RENDER_SURFACE(render_surface)); } else { return EGL_NO_SURFACE; diff --git a/src/window.h b/src/window.h index 2efd0196..7fe89d9b 100644 --- a/src/window.h +++ b/src/window.h @@ -27,7 +27,7 @@ struct view_geometry { struct vec2f view_size, display_size; struct mat3f display_to_view_transform; struct mat3f view_to_display_transform; - double device_pixel_ratio; + float device_pixel_ratio; }; enum renderer_type { kOpenGL_RendererType, kVulkan_RendererType }; diff --git a/test/flutterpi_test.c b/test/flutterpi_test.c index 96236b78..a23cbd90 100644 --- a/test/flutterpi_test.c +++ b/test/flutterpi_test.c @@ -1,10 +1,10 @@ #include #include -void setUp() { +void setUp(void) { } -void tearDown() { +void tearDown(void) { } #define TEST_ASSERT_EQUAL_BOOL(expected, actual) \ @@ -50,7 +50,7 @@ void expect_parsed_cmdline_args_matches(int argc, char **argv, bool expected_res TEST_ASSERT_EQUAL_INT(expected.dummy_display_size.y, actual.dummy_display_size.y); } -static struct flutterpi_cmdline_args get_default_args() { +static struct flutterpi_cmdline_args get_default_args(void) { static char *engine_argv[1] = { "flutter-pi" }; return (struct flutterpi_cmdline_args){ @@ -74,7 +74,7 @@ static struct flutterpi_cmdline_args get_default_args() { }; } -void test_parse_orientation_arg() { +void test_parse_orientation_arg(void) { struct flutterpi_cmdline_args expected = get_default_args(); // test --orientation @@ -132,7 +132,7 @@ void test_parse_orientation_arg() { ); } -void test_parse_rotation_arg() { +void test_parse_rotation_arg(void) { struct flutterpi_cmdline_args expected = get_default_args(); expected.has_rotation = true; @@ -149,7 +149,7 @@ void test_parse_rotation_arg() { expect_parsed_cmdline_args_matches(4, (char *[]){ "flutter-pi", "--rotation", "270", BUNDLE_PATH }, true, expected); } -void test_parse_physical_dimensions_arg() { +void test_parse_physical_dimensions_arg(void) { struct flutterpi_cmdline_args expected = get_default_args(); expected.bundle_path = NULL; @@ -164,7 +164,7 @@ void test_parse_physical_dimensions_arg() { expect_parsed_cmdline_args_matches(4, (char *[]){ "flutter-pi", "--dimensions", "10,10", BUNDLE_PATH }, true, expected); } -void test_parse_pixel_format_arg() { +void test_parse_pixel_format_arg(void) { struct flutterpi_cmdline_args expected = get_default_args(); expected.has_pixel_format = true; @@ -176,7 +176,7 @@ void test_parse_pixel_format_arg() { expect_parsed_cmdline_args_matches(4, (char *[]){ "flutter-pi", "--pixelformat", "RGBA8888", BUNDLE_PATH }, true, expected); } -void test_parse_runtime_mode_arg() { +void test_parse_runtime_mode_arg(void) { struct flutterpi_cmdline_args expected = get_default_args(); // test --debug, --profile, --release @@ -194,14 +194,14 @@ void test_parse_runtime_mode_arg() { expect_parsed_cmdline_args_matches(3, (char *[]){ "flutter-pi", "--release", BUNDLE_PATH }, true, expected); } -void test_parse_bundle_path_arg() { +void test_parse_bundle_path_arg(void) { struct flutterpi_cmdline_args expected = get_default_args(); expected.bundle_path = "/path/to/bundle/test"; expect_parsed_cmdline_args_matches(2, (char *[]){ "flutter-pi", "/path/to/bundle/test" }, true, expected); } -void test_parse_engine_arg() { +void test_parse_engine_arg(void) { struct flutterpi_cmdline_args expected = get_default_args(); expected.engine_argc = 2; @@ -210,14 +210,14 @@ void test_parse_engine_arg() { expect_parsed_cmdline_args_matches(3, (char *[]){ "flutter-pi", BUNDLE_PATH, "engine-arg" }, true, expected); } -void test_parse_vulkan_arg() { +void test_parse_vulkan_arg(void) { struct flutterpi_cmdline_args expected = get_default_args(); expected.use_vulkan = true; expect_parsed_cmdline_args_matches(3, (char *[]){ "flutter-pi", "--vulkan", BUNDLE_PATH }, true, expected); } -void test_parse_desired_videomode_arg() { +void test_parse_desired_videomode_arg(void) { struct flutterpi_cmdline_args expected = get_default_args(); expected.desired_videomode = "1920x1080"; @@ -227,7 +227,7 @@ void test_parse_desired_videomode_arg() { expect_parsed_cmdline_args_matches(4, (char *[]){ "flutter-pi", "--videomode", "1920x1080@60", BUNDLE_PATH }, true, expected); } -int main() { +int main(void) { UNITY_BEGIN(); RUN_TEST(test_parse_runtime_mode_arg); diff --git a/test/platformchannel_test.c b/test/platformchannel_test.c index 5047a082..6adb3e30 100644 --- a/test/platformchannel_test.c +++ b/test/platformchannel_test.c @@ -1,6 +1,7 @@ #define _GNU_SOURCE #include "platformchannel.h" +#include #include #include #include @@ -10,34 +11,37 @@ #define RAW_STD_BUF(...) (const struct raw_std_value *) ((const uint8_t[]){ __VA_ARGS__ }) #define AS_RAW_STD_VALUE(_value) ((const struct raw_std_value *) (_value)) +#define DBL_INFINITY ((double) INFINITY) +#define DBL_NAN ((double) NAN) + // required by Unity. -void setUp() { +void setUp(void) { } -void tearDown() { +void tearDown(void) { } -void test_raw_std_value_is_null() { +void test_raw_std_value_is_null(void) { TEST_ASSERT_TRUE(raw_std_value_is_null(RAW_STD_BUF(kStdNull))); TEST_ASSERT_FALSE(raw_std_value_is_null(RAW_STD_BUF(kStdTrue))); } -void test_raw_std_value_is_true() { +void test_raw_std_value_is_true(void) { TEST_ASSERT_TRUE(raw_std_value_is_true(RAW_STD_BUF(kStdTrue))); TEST_ASSERT_FALSE(raw_std_value_is_true(RAW_STD_BUF(kStdFalse))); } -void test_raw_std_value_is_false() { +void test_raw_std_value_is_false(void) { TEST_ASSERT_TRUE(raw_std_value_is_false(RAW_STD_BUF(kStdFalse))); TEST_ASSERT_FALSE(raw_std_value_is_false(RAW_STD_BUF(kStdTrue))); } -void test_raw_std_value_is_int32() { +void test_raw_std_value_is_int32(void) { TEST_ASSERT_TRUE(raw_std_value_is_int32(RAW_STD_BUF(kStdInt32))); TEST_ASSERT_FALSE(raw_std_value_is_int32(RAW_STD_BUF(kStdNull))); } -void test_raw_std_value_as_int32() { +void test_raw_std_value_as_int32(void) { // clang-format off alignas(16) uint8_t buffer[5] = { kStdInt32, @@ -53,12 +57,12 @@ void test_raw_std_value_as_int32() { TEST_ASSERT_EQUAL_INT32(-2003205, raw_std_value_as_int32(AS_RAW_STD_VALUE(buffer))); } -void test_raw_std_value_is_int64() { +void test_raw_std_value_is_int64(void) { TEST_ASSERT_TRUE(raw_std_value_is_int64(RAW_STD_BUF(kStdInt64))); TEST_ASSERT_FALSE(raw_std_value_is_int64(RAW_STD_BUF(kStdNull))); } -void test_raw_std_value_as_int64() { +void test_raw_std_value_as_int64(void) { // clang-format off alignas(16) uint8_t buffer[9] = { kStdInt64, @@ -74,12 +78,12 @@ void test_raw_std_value_as_int64() { TEST_ASSERT_EQUAL_INT64(-7998090352538419200, raw_std_value_as_int64(AS_RAW_STD_VALUE(buffer))); } -void test_raw_std_value_is_float64() { +void test_raw_std_value_is_float64(void) { TEST_ASSERT_TRUE(raw_std_value_is_float64(RAW_STD_BUF(kStdFloat64))); TEST_ASSERT_FALSE(raw_std_value_is_float64(RAW_STD_BUF(kStdNull))); } -void test_raw_std_value_as_float64() { +void test_raw_std_value_as_float64(void) { // clang-format off alignas(16) uint8_t buffer[] = { kStdFloat64, @@ -93,18 +97,18 @@ void test_raw_std_value_as_float64() { TEST_ASSERT_EQUAL_DOUBLE(M_PI, raw_std_value_as_float64(AS_RAW_STD_VALUE(buffer))); - value = INFINITY; + value = DBL_INFINITY; memcpy(buffer + 8, &value, sizeof(value)); - TEST_ASSERT_EQUAL_DOUBLE(INFINITY, raw_std_value_as_float64(AS_RAW_STD_VALUE(buffer))); + TEST_ASSERT_EQUAL_DOUBLE(DBL_INFINITY, raw_std_value_as_float64(AS_RAW_STD_VALUE(buffer))); } -void test_raw_std_value_is_string() { +void test_raw_std_value_is_string(void) { TEST_ASSERT_TRUE(raw_std_value_is_string(RAW_STD_BUF(kStdString))); TEST_ASSERT_FALSE(raw_std_value_is_string(RAW_STD_BUF(kStdNull))); } -void test_raw_std_string_dup() { +void test_raw_std_string_dup(void) { const char *str = "The quick brown fox jumps over the lazy dog."; // clang-format off @@ -129,7 +133,7 @@ void test_raw_std_string_dup() { free(str_duped); } -void test_raw_std_string_equals() { +void test_raw_std_string_equals(void) { const char *str = "The quick brown fox jumps over the lazy dog."; alignas(16) uint8_t buffer[1 + 1 + strlen(str)]; @@ -151,12 +155,12 @@ void test_raw_std_string_equals() { TEST_ASSERT_FALSE(raw_std_string_equals(AS_RAW_STD_VALUE(buffer), "anything")); } -void test_raw_std_value_is_uint8array() { +void test_raw_std_value_is_uint8array(void) { TEST_ASSERT_TRUE(raw_std_value_is_uint8array(RAW_STD_BUF(kStdUInt8Array))); TEST_ASSERT_FALSE(raw_std_value_is_uint8array(RAW_STD_BUF(kStdNull))); } -void test_raw_std_value_as_uint8array() { +void test_raw_std_value_as_uint8array(void) { // clang-format off alignas(16) uint8_t buffer[] = { kStdUInt8Array, @@ -179,12 +183,12 @@ void test_raw_std_value_as_uint8array() { TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, raw_std_value_as_uint8array(AS_RAW_STD_VALUE(buffer)), 4); } -void test_raw_std_value_is_int32array() { +void test_raw_std_value_is_int32array(void) { TEST_ASSERT_TRUE(raw_std_value_is_int32array(RAW_STD_BUF(kStdInt32Array))); TEST_ASSERT_FALSE(raw_std_value_is_int32array(RAW_STD_BUF(kStdNull))); } -void test_raw_std_value_as_int32array() { +void test_raw_std_value_as_int32array(void) { // clang-format off alignas(16) uint8_t buffer[] = { // type @@ -216,12 +220,12 @@ void test_raw_std_value_as_int32array() { TEST_ASSERT_EQUAL_INT32_ARRAY(expected, raw_std_value_as_int32array(AS_RAW_STD_VALUE(buffer)), 2); } -void test_raw_std_value_is_int64array() { +void test_raw_std_value_is_int64array(void) { TEST_ASSERT_TRUE(raw_std_value_is_int64array(RAW_STD_BUF(kStdInt64Array))); TEST_ASSERT_FALSE(raw_std_value_is_int64array(RAW_STD_BUF(kStdNull))); } -void test_raw_std_value_as_int64array() { +void test_raw_std_value_as_int64array(void) { // clang-format off alignas(16) uint8_t buffer[] = { // type @@ -251,12 +255,12 @@ void test_raw_std_value_as_int64array() { TEST_ASSERT_EQUAL_INT64_ARRAY(expected, raw_std_value_as_int64array(AS_RAW_STD_VALUE(buffer)), 2); } -void test_raw_std_value_is_float64array() { +void test_raw_std_value_is_float64array(void) { TEST_ASSERT_TRUE(raw_std_value_is_float64array(RAW_STD_BUF(kStdFloat64Array))); TEST_ASSERT_FALSE(raw_std_value_is_float64array(RAW_STD_BUF(kStdNull))); } -void test_raw_std_value_as_float64array() { +void test_raw_std_value_as_float64array(void) { // clang-format off alignas(16) uint8_t buffer[] = { // type @@ -274,7 +278,7 @@ void test_raw_std_value_as_float64array() { // clang-format off double expected[] = { M_PI, - INFINITY, + DBL_INFINITY, }; // clang-format on @@ -288,12 +292,12 @@ void test_raw_std_value_as_float64array() { TEST_ASSERT_EQUAL_DOUBLE_ARRAY(expected, raw_std_value_as_float64array(AS_RAW_STD_VALUE(buffer)), 2); } -void test_raw_std_value_is_list() { +void test_raw_std_value_is_list(void) { TEST_ASSERT_TRUE(raw_std_value_is_list(RAW_STD_BUF(kStdList))); TEST_ASSERT_FALSE(raw_std_value_is_list(RAW_STD_BUF(kStdNull))); } -void test_raw_std_list_get_size() { +void test_raw_std_list_get_size(void) { // clang-format off alignas(16) uint8_t buffer[] = { // type @@ -325,12 +329,12 @@ void test_raw_std_list_get_size() { TEST_ASSERT_EQUAL_size_t(0xDEADBEEF, raw_std_list_get_size(AS_RAW_STD_VALUE(buffer))); } -void test_raw_std_value_is_map() { +void test_raw_std_value_is_map(void) { TEST_ASSERT_TRUE(raw_std_value_is_map(RAW_STD_BUF(kStdMap))); TEST_ASSERT_FALSE(raw_std_value_is_map(RAW_STD_BUF(kStdNull))); } -void test_raw_std_map_get_size() { +void test_raw_std_map_get_size(void) { // clang-format off alignas(16) uint8_t buffer[] = { // type @@ -362,12 +366,12 @@ void test_raw_std_map_get_size() { TEST_ASSERT_EQUAL_size_t(0xDEADBEEF, raw_std_map_get_size(AS_RAW_STD_VALUE(buffer))); } -void test_raw_std_value_is_float32array() { +void test_raw_std_value_is_float32array(void) { TEST_ASSERT_TRUE(raw_std_value_is_float32array(RAW_STD_BUF(kStdFloat32Array))); TEST_ASSERT_FALSE(raw_std_value_is_float32array(RAW_STD_BUF(kStdNull))); } -void test_raw_std_value_as_float32array() { +void test_raw_std_value_as_float32array(void) { // clang-format off alignas(16) uint8_t buffer[] = { // type @@ -385,7 +389,7 @@ void test_raw_std_value_as_float32array() { // clang-format off float expected[] = { M_PI, - INFINITY, + DBL_INFINITY, }; // clang-format on @@ -399,7 +403,7 @@ void test_raw_std_value_as_float32array() { TEST_ASSERT_EQUAL_FLOAT_ARRAY(expected, raw_std_value_as_float32array(AS_RAW_STD_VALUE(buffer)), 2); } -void test_raw_std_value_equals() { +void test_raw_std_value_equals(void) { TEST_ASSERT_TRUE(raw_std_value_equals(RAW_STD_BUF(kStdNull), RAW_STD_BUF(kStdNull))); TEST_ASSERT_FALSE(raw_std_value_equals(RAW_STD_BUF(kStdNull), RAW_STD_BUF(kStdTrue))); TEST_ASSERT_FALSE(raw_std_value_equals(RAW_STD_BUF(kStdTrue), RAW_STD_BUF(kStdFalse))); @@ -479,7 +483,7 @@ void test_raw_std_value_equals() { TEST_ASSERT_TRUE(raw_std_value_equals(AS_RAW_STD_VALUE(lhs), AS_RAW_STD_VALUE(rhs))); - f = NAN; + f = DBL_NAN; memcpy(rhs + 8, &f, sizeof(f)); TEST_ASSERT_FALSE(raw_std_value_equals(AS_RAW_STD_VALUE(lhs), AS_RAW_STD_VALUE(rhs))); @@ -689,7 +693,7 @@ void test_raw_std_value_equals() { double array[] = { M_PI, - INFINITY, + DBL_INFINITY, }; // clang-format on @@ -705,7 +709,7 @@ void test_raw_std_value_equals() { rhs[1] = 2; double array2[] = { 0.0, - INFINITY, + DBL_INFINITY, }; memcpy(rhs + 8, array2, sizeof(array2)); @@ -783,7 +787,7 @@ void test_raw_std_value_equals() { int64_t int64 = (int64_t) INT64_MIN; float floats[] = { M_PI, - INFINITY, + DBL_INFINITY, }; // clang-format on @@ -835,7 +839,7 @@ void test_raw_std_value_equals() { float array[] = { M_PI, - INFINITY, + DBL_INFINITY, }; // clang-format on @@ -852,7 +856,7 @@ void test_raw_std_value_equals() { // clang-format off float array2[] = { 0.0, - INFINITY, + DBL_INFINITY, }; // clang-format on memcpy(rhs + 4, array2, sizeof(array2)); @@ -861,18 +865,18 @@ void test_raw_std_value_equals() { } } -void test_raw_std_value_is_bool() { +void test_raw_std_value_is_bool(void) { TEST_ASSERT_FALSE(raw_std_value_is_bool(RAW_STD_BUF(kStdNull))); TEST_ASSERT_TRUE(raw_std_value_is_bool(RAW_STD_BUF(kStdTrue))); TEST_ASSERT_TRUE(raw_std_value_is_bool(RAW_STD_BUF(kStdFalse))); } -void test_raw_std_value_as_bool() { +void test_raw_std_value_as_bool(void) { TEST_ASSERT_TRUE(raw_std_value_as_bool(RAW_STD_BUF(kStdTrue))); TEST_ASSERT_FALSE(raw_std_value_as_bool(RAW_STD_BUF(kStdFalse))); } -void test_raw_std_value_is_int() { +void test_raw_std_value_is_int(void) { TEST_ASSERT_FALSE(raw_std_value_is_int(RAW_STD_BUF(kStdNull))); TEST_ASSERT_FALSE(raw_std_value_is_int(RAW_STD_BUF(kStdTrue))); TEST_ASSERT_FALSE(raw_std_value_is_int(RAW_STD_BUF(kStdFalse))); @@ -881,7 +885,7 @@ void test_raw_std_value_is_int() { TEST_ASSERT_FALSE(raw_std_value_is_int(RAW_STD_BUF(kStdFloat64))); } -void test_raw_std_value_as_int() { +void test_raw_std_value_as_int(void) { // clang-format off alignas(16) uint8_t buffer[9] = { kStdInt32, @@ -905,7 +909,7 @@ void test_raw_std_value_as_int() { TEST_ASSERT_EQUAL_INT64(INT32_MIN, raw_std_value_as_int(AS_RAW_STD_VALUE(buffer))); } -void test_raw_std_value_get_size() { +void test_raw_std_value_get_size(void) { // clang-format off alignas(16) uint8_t buffer[] = { // type @@ -938,7 +942,7 @@ void test_raw_std_value_get_size() { TEST_ASSERT_EQUAL_size_t(0xDEADBEEF, raw_std_value_get_size(AS_RAW_STD_VALUE(buffer))); } -void test_raw_std_value_after() { +void test_raw_std_value_after(void) { // null { // clang-format off @@ -1263,7 +1267,7 @@ void test_raw_std_value_after() { } } -void test_raw_std_list_get_first_element() { +void test_raw_std_list_get_first_element(void) { // list const char *str = "The quick brown fox jumps over the lazy dog."; @@ -1286,7 +1290,7 @@ void test_raw_std_list_get_first_element() { ); } -void test_raw_std_list_get_nth_element() { +void test_raw_std_list_get_nth_element(void) { // list const char *str = "The quick brown fox jumps over the lazy dog."; @@ -1309,7 +1313,7 @@ void test_raw_std_list_get_nth_element() { ); } -void test_raw_std_map_get_first_key() { +void test_raw_std_map_get_first_key(void) { // map // clang-format off alignas(16) uint8_t buffer[] = { @@ -1340,31 +1344,31 @@ void test_raw_std_map_get_first_key() { TEST_ASSERT_EQUAL_PTR(buffer + 1 + 1 + 4, raw_std_map_get_first_key(AS_RAW_STD_VALUE(buffer))); } -void test_raw_std_map_find() { +void test_raw_std_map_find(void) { } -void test_raw_std_map_find_str() { +void test_raw_std_map_find_str(void) { } -void test_raw_std_value_check() { +void test_raw_std_value_check(void) { } -void test_raw_std_method_call_check() { +void test_raw_std_method_call_check(void) { } -void test_raw_std_method_call_response_check() { +void test_raw_std_method_call_response_check(void) { } -void test_raw_std_event_check() { +void test_raw_std_event_check(void) { } -void test_raw_std_method_call_get_method() { +void test_raw_std_method_call_get_method(void) { } -void test_raw_std_method_call_get_method_dup() { +void test_raw_std_method_call_get_method_dup(void) { } -void test_raw_std_method_call_get_arg() { +void test_raw_std_method_call_get_arg(void) { } int main(void) { diff --git a/third_party/flutter_embedder_header/include/flutter_embedder.h b/third_party/flutter_embedder_header/include/flutter_embedder_header/flutter_embedder.h similarity index 100% rename from third_party/flutter_embedder_header/include/flutter_embedder.h rename to third_party/flutter_embedder_header/include/flutter_embedder_header/flutter_embedder.h diff --git a/src/util/dynarray.h b/third_party/mesa3d/include/mesa3d/dynarray.h similarity index 99% rename from src/util/dynarray.h rename to third_party/mesa3d/include/mesa3d/dynarray.h index 2d9f2101..220ea57f 100644 --- a/src/util/dynarray.h +++ b/third_party/mesa3d/include/mesa3d/dynarray.h @@ -32,8 +32,6 @@ #include #include -#include "macros.h" - #ifdef __cplusplus extern "C" { #endif From a4fcecebd7a3e5b7657d70ecc36d1dcf312b3d6b Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 10 Aug 2024 16:22:16 +0200 Subject: [PATCH 29/41] add script for rolling `flutter_embedder.h` --- tools/roll_embedder_header.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100755 tools/roll_embedder_header.sh diff --git a/tools/roll_embedder_header.sh b/tools/roll_embedder_header.sh new file mode 100755 index 00000000..518f0188 --- /dev/null +++ b/tools/roll_embedder_header.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +ENGINE_VERSION="$(curl -s https://raw.githubusercontent.com/flutter/flutter/stable/bin/internal/engine.version)" + +if [ ! -f "third_party/flutter_embedder_header/include/flutter_embedder.h" ]; then + echo "Incorrect working directory. Please launch this script with the flutter-pi repo root as the working directory, using 'tools/roll_embedder_header.sh'.". + exit 1 +fi + +curl -o third_party/flutter_embedder_header/include/flutter_embedder.h "https://raw.githubusercontent.com/flutter/engine/$ENGINE_VERSION/shell/platform/embedder/embedder.h" +echo "$ENGINE_VERSION" > third_party/flutter_embedder_header/engine.version From 13bf4782791d211fa902c8b87858f3183d53aa1d Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 10 Aug 2024 16:22:34 +0200 Subject: [PATCH 30/41] Roll `flutter_embedder.h` (flutter stable 3.24.0) --- src/compositor_ng.c | 2 +- .../flutter_embedder_header/engine.version | 2 +- .../flutter_embedder.h | 435 +++++++++++++++++- 3 files changed, 422 insertions(+), 17 deletions(-) diff --git a/src/compositor_ng.c b/src/compositor_ng.c index dcbf6b9c..48df8015 100644 --- a/src/compositor_ng.c +++ b/src/compositor_ng.c @@ -163,7 +163,7 @@ MUST_CHECK struct compositor *compositor_new(struct tracer *tracer, struct windo compositor->main_window = window_ref(main_window); // just so we get an error if the FlutterCompositor struct was updated - COMPILE_ASSERT(sizeof(FlutterCompositor) == 24 || sizeof(FlutterCompositor) == 48); + COMPILE_ASSERT(sizeof(FlutterCompositor) == 28 || sizeof(FlutterCompositor) == 56); memset(&compositor->flutter_compositor, 0, sizeof(FlutterCompositor)); compositor->flutter_compositor.struct_size = sizeof(FlutterCompositor); diff --git a/third_party/flutter_embedder_header/engine.version b/third_party/flutter_embedder_header/engine.version index a7aa73a8..0def69d1 100644 --- a/third_party/flutter_embedder_header/engine.version +++ b/third_party/flutter_embedder_header/engine.version @@ -1 +1 @@ -d44b5a94c976fbb65815374f61ab5392a220b084 \ No newline at end of file +b8800d88be4866db1b15f8b954ab2573bba9960f diff --git a/third_party/flutter_embedder_header/include/flutter_embedder_header/flutter_embedder.h b/third_party/flutter_embedder_header/include/flutter_embedder_header/flutter_embedder.h index 5cdba06e..069c813a 100644 --- a/third_party/flutter_embedder_header/include/flutter_embedder_header/flutter_embedder.h +++ b/third_party/flutter_embedder_header/include/flutter_embedder_header/flutter_embedder.h @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef FLUTTER_EMBEDDER_H_ -#define FLUTTER_EMBEDDER_H_ +#ifndef FLUTTER_SHELL_PLATFORM_EMBEDDER_EMBEDDER_H_ +#define FLUTTER_SHELL_PLATFORM_EMBEDDER_EMBEDDER_H_ #include #include @@ -25,9 +25,9 @@ // - Function signatures (names, argument counts, argument order, and argument // type) cannot change. // - The core behavior of existing functions cannot change. -// - Instead of nesting structures by value within another structure, prefer -// nesting by pointer. This ensures that adding members to the nested struct -// does not break the ABI of the parent struct. +// - Instead of nesting structures by value within another structure/union, +// prefer nesting by pointer. This ensures that adding members to the nested +// struct does not break the ABI of the parent struct/union. // - Instead of array of structures, prefer array of pointers to structures. // This ensures that array indexing does not break if members are added // to the structure. @@ -162,6 +162,8 @@ typedef enum { kFlutterSemanticsActionMoveCursorBackwardByWord = 1 << 20, /// Replace the current text in the text field. kFlutterSemanticsActionSetText = 1 << 21, + /// Request that the respective focusable widget gain input focus. + kFlutterSemanticsActionFocus = 1 << 22, } FlutterSemanticsAction; /// The set of properties that may be associated with a semantics node. @@ -236,6 +238,11 @@ typedef enum { kFlutterSemanticsFlagIsKeyboardKey = 1 << 24, /// Whether the semantics node represents a tristate checkbox in mixed state. kFlutterSemanticsFlagIsCheckStateMixed = 1 << 25, + /// The semantics node has the quality of either being "expanded" or + /// "collapsed". + kFlutterSemanticsFlagHasExpandedState = 1 << 26, + /// Whether a semantic node that hasExpandedState is currently expanded. + kFlutterSemanticsFlagIsExpanded = 1 << 27, } FlutterSemanticsFlag; typedef enum { @@ -261,6 +268,12 @@ typedef enum { typedef struct _FlutterEngine* FLUTTER_API_SYMBOL(FlutterEngine); +/// Unique identifier for views. +/// +/// View IDs are generated by the embedder and are +/// opaque to the engine; the engine does not interpret view IDs in any way. +typedef int64_t FlutterViewId; + typedef struct { /// horizontal scale factor double scaleX; @@ -676,9 +689,13 @@ typedef struct { FlutterMetalCommandQueueHandle present_command_queue; /// The callback that gets invoked when the engine requests the embedder for a /// texture to render to. + /// + /// Not used if a FlutterCompositor is supplied in FlutterProjectArgs. FlutterMetalTextureCallback get_next_drawable_callback; /// The callback presented to the embedder to present a fully populated metal /// texture to the user. + /// + /// Not used if a FlutterCompositor is supplied in FlutterProjectArgs. FlutterMetalPresentCallback present_drawable_callback; /// When the embedder specifies that a texture has a frame available, the /// engine will call this method (on an internal engine managed thread) so @@ -748,6 +765,11 @@ typedef struct { /// The queue family index of the VkQueue supplied in the next field. uint32_t queue_family_index; /// VkQueue handle. + /// The queue should not be used without protection from a mutex to make sure + /// it is not used simultaneously with other threads. That mutex should match + /// the one injected via the |get_instance_proc_address_callback|. + /// There is a proposal to remove the need for the mutex at + /// https://github.com/flutter/flutter/issues/134573. FlutterVulkanQueueHandle queue; /// The number of instance extensions available for enumerating in the next /// field. @@ -771,6 +793,12 @@ typedef struct { /// For example: VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME const char** enabled_device_extensions; /// The callback invoked when resolving Vulkan function pointers. + /// At a bare minimum this should be used to swap out any calls that operate + /// on vkQueue's for threadsafe variants that obtain locks for their duration. + /// The functions to swap out are "vkQueueSubmit" and "vkQueueWaitIdle". An + /// example of how to do that can be found in the test + /// "EmbedderTest.CanSwapOutVulkanCalls" unit-test in + /// //shell/platform/embedder/tests/embedder_vk_unittests.cc. FlutterVulkanInstanceProcAddressCallback get_instance_proc_address_callback; /// The callback invoked when the engine requests a VkImage from the embedder /// for rendering the next frame. @@ -805,6 +833,11 @@ typedef struct { }; } FlutterRendererConfig; +/// Display refers to a graphics hardware system consisting of a framebuffer, +/// typically a monitor or a screen. This ID is unique per display and is +/// stable until the Flutter application restarts. +typedef uint64_t FlutterEngineDisplayId; + typedef struct { /// The size of this struct. Must be sizeof(FlutterWindowMetricsEvent). size_t struct_size; @@ -826,8 +859,108 @@ typedef struct { double physical_view_inset_bottom; /// Left inset of window. double physical_view_inset_left; + /// The identifier of the display the view is rendering on. + FlutterEngineDisplayId display_id; + /// The view that this event is describing. + int64_t view_id; } FlutterWindowMetricsEvent; +typedef struct { + /// The size of this struct. + /// Must be sizeof(FlutterAddViewResult). + size_t struct_size; + + /// True if the add view operation succeeded. + bool added; + + /// The |FlutterAddViewInfo.user_data|. + void* user_data; +} FlutterAddViewResult; + +/// The callback invoked by the engine when the engine has attempted to add a +/// view. +/// +/// The |FlutterAddViewResult| is only guaranteed to be valid during this +/// callback. +typedef void (*FlutterAddViewCallback)(const FlutterAddViewResult* result); + +typedef struct { + /// The size of this struct. + /// Must be sizeof(FlutterAddViewInfo). + size_t struct_size; + + /// The identifier for the view to add. This must be unique. + FlutterViewId view_id; + + /// The view's properties. + /// + /// The metric's |view_id| must match this struct's |view_id|. + const FlutterWindowMetricsEvent* view_metrics; + + /// A baton that is not interpreted by the engine in any way. It will be given + /// back to the embedder in |add_view_callback|. Embedder resources may be + /// associated with this baton. + void* user_data; + + /// Called once the engine has attempted to add the view. This callback is + /// required. + /// + /// The embedder/app must not use the view until the callback is invoked with + /// an `added` value of `true`. + /// + /// This callback is invoked on an internal engine managed thread. Embedders + /// must re-thread if necessary. + FlutterAddViewCallback add_view_callback; +} FlutterAddViewInfo; + +typedef struct { + /// The size of this struct. + /// Must be sizeof(FlutterRemoveViewResult). + size_t struct_size; + + /// True if the remove view operation succeeded. + bool removed; + + /// The |FlutterRemoveViewInfo.user_data|. + void* user_data; +} FlutterRemoveViewResult; + +/// The callback invoked by the engine when the engine has attempted to remove +/// a view. +/// +/// The |FlutterRemoveViewResult| is only guaranteed to be valid during this +/// callback. +typedef void (*FlutterRemoveViewCallback)( + const FlutterRemoveViewResult* /* result */); + +typedef struct { + /// The size of this struct. + /// Must be sizeof(FlutterRemoveViewInfo). + size_t struct_size; + + /// The identifier for the view to remove. + /// + /// The implicit view cannot be removed if it is enabled. + FlutterViewId view_id; + + /// A baton that is not interpreted by the engine in any way. + /// It will be given back to the embedder in |remove_view_callback|. + /// Embedder resources may be associated with this baton. + void* user_data; + + /// Called once the engine has attempted to remove the view. + /// This callback is required. + /// + /// The embedder must not destroy the underlying surface until the callback is + /// invoked with a `removed` value of `true`. + /// + /// This callback is invoked on an internal engine managed thread. + /// Embedders must re-thread if necessary. + /// + /// The |result| argument will be deallocated when the callback returns. + FlutterRemoveViewCallback remove_view_callback; +} FlutterRemoveViewInfo; + /// The phase of the pointer event. typedef enum { kCancel, @@ -896,7 +1029,6 @@ typedef enum { kFlutterPointerSignalKindScroll, kFlutterPointerSignalKindScrollInertiaCancel, kFlutterPointerSignalKindScale, - kFlutterPointerSignalKindStylusAuxiliaryAction, } FlutterPointerSignalKind; typedef struct { @@ -935,6 +1067,8 @@ typedef struct { double scale; /// The rotation of the pan/zoom in radians, where 0.0 is the initial angle. double rotation; + /// The identifier of the view that received the pointer event. + FlutterViewId view_id; } FlutterPointerEvent; typedef enum { @@ -943,6 +1077,14 @@ typedef enum { kFlutterKeyEventTypeRepeat, } FlutterKeyEventType; +typedef enum { + kFlutterKeyEventDeviceTypeKeyboard = 1, + kFlutterKeyEventDeviceTypeDirectionalPad, + kFlutterKeyEventDeviceTypeGamepad, + kFlutterKeyEventDeviceTypeJoystick, + kFlutterKeyEventDeviceTypeHdmi, +} FlutterKeyEventDeviceType; + /// A structure to represent a key event. /// /// Sending `FlutterKeyEvent` via `FlutterEngineSendKeyEvent` results in a @@ -1006,6 +1148,8 @@ typedef struct { /// An event being synthesized means that the `timestamp` might greatly /// deviate from the actual time when the event occurs physically. bool synthesized; + /// The source device for the key event. + FlutterKeyEventDeviceType device_type; } FlutterKeyEvent; typedef void (*FlutterKeyEventCallback)(bool /* handled */, @@ -1049,6 +1193,57 @@ typedef int64_t FlutterPlatformViewIdentifier; FLUTTER_EXPORT extern const int32_t kFlutterSemanticsNodeIdBatchEnd; +// The enumeration of possible string attributes that affect how assistive +// technologies announce a string. +// +// See dart:ui's implementers of the StringAttribute abstract class. +typedef enum { + // Indicates the string should be announced character by character. + kSpellOut, + // Indicates the string should be announced using the specified locale. + kLocale, +} FlutterStringAttributeType; + +// Indicates the assistive technology should announce out the string character +// by character. +// +// See dart:ui's SpellOutStringAttribute. +typedef struct { + /// The size of this struct. Must be sizeof(FlutterSpellOutStringAttribute). + size_t struct_size; +} FlutterSpellOutStringAttribute; + +// Indicates the assistive technology should announce the string using the +// specified locale. +// +// See dart:ui's LocaleStringAttribute. +typedef struct { + /// The size of this struct. Must be sizeof(FlutterLocaleStringAttribute). + size_t struct_size; + // The locale of this attribute. + const char* locale; +} FlutterLocaleStringAttribute; + +// Indicates how the assistive technology should treat the string. +// +// See dart:ui's StringAttribute. +typedef struct { + /// The size of this struct. Must be sizeof(FlutterStringAttribute). + size_t struct_size; + // The position this attribute starts. + size_t start; + // The next position after the attribute ends. + size_t end; + /// The type of the attribute described by the subsequent union. + FlutterStringAttributeType type; + union { + // Indicates the string should be announced character by character. + const FlutterSpellOutStringAttribute* spell_out; + // Indicates the string should be announced using the specified locale. + const FlutterLocaleStringAttribute* locale; + }; +} FlutterStringAttribute; + /// A node that represents some semantic data. /// /// The semantics tree is maintained during the semantics phase of the pipeline @@ -1200,6 +1395,31 @@ typedef struct { FlutterPlatformViewIdentifier platform_view_id; /// A textual tooltip attached to the node. const char* tooltip; + // The number of string attributes associated with the `label`. + size_t label_attribute_count; + // Array of string attributes associated with the `label`. + // Has length `label_attribute_count`. + const FlutterStringAttribute** label_attributes; + // The number of string attributes associated with the `hint`. + size_t hint_attribute_count; + // Array of string attributes associated with the `hint`. + // Has length `hint_attribute_count`. + const FlutterStringAttribute** hint_attributes; + // The number of string attributes associated with the `value`. + size_t value_attribute_count; + // Array of string attributes associated with the `value`. + // Has length `value_attribute_count`. + const FlutterStringAttribute** value_attributes; + // The number of string attributes associated with the `increased_value`. + size_t increased_value_attribute_count; + // Array of string attributes associated with the `increased_value`. + // Has length `increased_value_attribute_count`. + const FlutterStringAttribute** increased_value_attributes; + // The number of string attributes associated with the `decreased_value`. + size_t decreased_value_attribute_count; + // Array of string attributes associated with the `decreased_value`. + // Has length `decreased_value_attribute_count`. + const FlutterStringAttribute** decreased_value_attributes; } FlutterSemanticsNode2; /// `FlutterSemanticsCustomAction` ID used as a sentinel to signal the end of a @@ -1311,6 +1531,20 @@ typedef void (*FlutterUpdateSemanticsCallback2)( const FlutterSemanticsUpdate2* /* semantics update */, void* /* user data*/); +/// An update to whether a message channel has a listener set or not. +typedef struct { + // The size of the struct. Must be sizeof(FlutterChannelUpdate). + size_t struct_size; + /// The name of the channel. + const char* channel; + /// True if a listener has been set, false if one has been cleared. + bool listening; +} FlutterChannelUpdate; + +typedef void (*FlutterChannelUpdateCallback)( + const FlutterChannelUpdate* /* channel update */, + void* /* user data */); + typedef struct _FlutterTaskRunner* FlutterTaskRunner; typedef struct { @@ -1548,6 +1782,9 @@ typedef struct { size_t struct_size; /// The size of the render target the engine expects to render into. FlutterSize size; + /// The identifier for the view that the engine will use this backing store to + /// render into. + FlutterViewId view_id; } FlutterBackingStoreConfig; typedef enum { @@ -1558,6 +1795,27 @@ typedef enum { kFlutterLayerContentTypePlatformView, } FlutterLayerContentType; +/// A region represented by a collection of non-overlapping rectangles. +typedef struct { + /// The size of this struct. Must be sizeof(FlutterRegion). + size_t struct_size; + /// Number of rectangles in the region. + size_t rects_count; + /// The rectangles that make up the region. + FlutterRect* rects; +} FlutterRegion; + +/// Contains additional information about the backing store provided +/// during presentation to the embedder. +typedef struct { + size_t struct_size; + + /// The area of the backing store that contains Flutter contents. Pixels + /// outside of this area are transparent and the embedder may choose not + /// to render them. Coordinates are in physical pixels. + FlutterRegion* paint_region; +} FlutterBackingStorePresentInfo; + typedef struct { /// This size of this struct. Must be sizeof(FlutterLayer). size_t struct_size; @@ -1577,8 +1835,34 @@ typedef struct { FlutterPoint offset; /// The size of the layer (in physical pixels). FlutterSize size; + + /// Extra information for the backing store that the embedder may + /// use during presentation. + FlutterBackingStorePresentInfo* backing_store_present_info; + + // Time in nanoseconds at which this frame is scheduled to be presented. 0 if + // not known. See FlutterEngineGetCurrentTime(). + uint64_t presentation_time; } FlutterLayer; +typedef struct { + /// The size of this struct. + /// Must be sizeof(FlutterPresentViewInfo). + size_t struct_size; + + /// The identifier of the target view. + FlutterViewId view_id; + + /// The layers that should be composited onto the view. + const FlutterLayer** layers; + + /// The count of layers. + size_t layers_count; + + /// The |FlutterCompositor.user_data|. + void* user_data; +} FlutterPresentViewInfo; + typedef bool (*FlutterBackingStoreCreateCallback)( const FlutterBackingStoreConfig* config, FlutterBackingStore* backing_store_out, @@ -1592,13 +1876,20 @@ typedef bool (*FlutterLayersPresentCallback)(const FlutterLayer** layers, size_t layers_count, void* user_data); +/// The callback invoked when the embedder should present to a view. +/// +/// The |FlutterPresentViewInfo| will be deallocated once the callback returns. +typedef bool (*FlutterPresentViewCallback)( + const FlutterPresentViewInfo* /* present info */); + typedef struct { /// This size of this struct. Must be sizeof(FlutterCompositor). size_t struct_size; /// A baton that in not interpreted by the engine in any way. If it passed /// back to the embedder in `FlutterCompositor.create_backing_store_callback`, - /// `FlutterCompositor.collect_backing_store_callback` and - /// `FlutterCompositor.present_layers_callback` + /// `FlutterCompositor.collect_backing_store_callback`, + /// `FlutterCompositor.present_layers_callback`, and + /// `FlutterCompositor.present_view_callback`. void* user_data; /// A callback invoked by the engine to obtain a backing store for a specific /// `FlutterLayer`. @@ -1607,15 +1898,38 @@ typedef struct { /// `FlutterBackingStore::struct_size` when specifying a new backing store to /// the engine. This only matters if the embedder expects to be used with /// engines older than the version whose headers it used during compilation. + /// + /// The callback should return true if the operation was successful. FlutterBackingStoreCreateCallback create_backing_store_callback; /// A callback invoked by the engine to release the backing store. The /// embedder may collect any resources associated with the backing store. + /// + /// The callback should return true if the operation was successful. FlutterBackingStoreCollectCallback collect_backing_store_callback; /// Callback invoked by the engine to composite the contents of each layer - /// onto the screen. + /// onto the implicit view. + /// + /// DEPRECATED: Use `present_view_callback` to support multiple views. + /// If this callback is provided, `FlutterEngineAddView` and + /// `FlutterEngineRemoveView` should not be used. + /// + /// Only one of `present_layers_callback` and `present_view_callback` may be + /// provided. Providing both is an error and engine initialization will + /// terminate. + /// + /// The callback should return true if the operation was successful. FlutterLayersPresentCallback present_layers_callback; /// Avoid caching backing stores provided by this compositor. bool avoid_backing_store_cache; + /// Callback invoked by the engine to composite the contents of each layer + /// onto the specified view. + /// + /// Only one of `present_layers_callback` and `present_view_callback` may be + /// provided. Providing both is an error and engine initialization will + /// terminate. + /// + /// The callback should return true if the operation was successful. + FlutterPresentViewCallback present_view_callback; } FlutterCompositor; typedef struct { @@ -1654,11 +1968,6 @@ typedef const FlutterLocale* (*FlutterComputePlatformResolvedLocaleCallback)( const FlutterLocale** /* supported_locales*/, size_t /* Number of locales*/); -/// Display refers to a graphics hardware system consisting of a framebuffer, -/// typically a monitor or a screen. This ID is unique per display and is -/// stable until the Flutter application restarts. -typedef uint64_t FlutterEngineDisplayId; - typedef struct { /// This size of this struct. Must be sizeof(FlutterDisplay). size_t struct_size; @@ -1674,6 +1983,16 @@ typedef struct { /// This represents the refresh period in frames per second. This value may be /// zero if the device is not running or unavailable or unknown. double refresh_rate; + + /// The width of the display, in physical pixels. + size_t width; + + /// The height of the display, in physical pixels. + size_t height; + + /// The pixel ratio of the display, which is used to convert physical pixels + /// to logical pixels. + double device_pixel_ratio; } FlutterEngineDisplay; /// The update type parameter that is passed to @@ -1920,6 +2239,10 @@ typedef struct { /// `update_semantics_callback`, and /// `update_semantics_callback2` may be provided; the others /// should be set to null. + /// + /// This callback is incompatible with multiple views. If this + /// callback is provided, `FlutterEngineAddView` and + /// `FlutterEngineRemoveView` should not be used. FlutterUpdateSemanticsNodeCallback update_semantics_node_callback; /// The legacy callback invoked by the engine in order to give the embedder /// the chance to respond to updates to semantics custom actions from the Dart @@ -1936,6 +2259,10 @@ typedef struct { /// `update_semantics_callback`, and /// `update_semantics_callback2` may be provided; the others /// should be set to null. + /// + /// This callback is incompatible with multiple views. If this + /// callback is provided, `FlutterEngineAddView` and + /// `FlutterEngineRemoveView` should not be used. FlutterUpdateSemanticsCustomActionCallback update_semantics_custom_action_callback; /// Path to a directory used to store data that is cached across runs of a @@ -2085,6 +2412,10 @@ typedef struct { /// `update_semantics_callback`, and /// `update_semantics_callback2` may be provided; the others /// must be set to null. + /// + /// This callback is incompatible with multiple views. If this + /// callback is provided, `FlutterEngineAddView` and + /// `FlutterEngineRemoveView` should not be used. FlutterUpdateSemanticsCallback update_semantics_callback; /// The callback invoked by the engine in order to give the embedder the @@ -2098,10 +2429,17 @@ typedef struct { /// and `update_semantics_callback2` may be provided; the others must be set /// to null. FlutterUpdateSemanticsCallback2 update_semantics_callback2; + + /// The callback invoked by the engine in response to a channel listener + /// being registered on the framework side. The callback is invoked from + /// a task posted to the platform thread. + FlutterChannelUpdateCallback channel_update_callback; } FlutterProjectArgs; #ifndef FLUTTER_ENGINE_NO_PROTOTYPES +// NOLINTBEGIN(google-objc-function-naming) + //------------------------------------------------------------------------------ /// @brief Creates the necessary data structures to launch a Flutter Dart /// application in AOT mode. The data may only be collected after @@ -2243,6 +2581,63 @@ FLUTTER_EXPORT FlutterEngineResult FlutterEngineRunInitialized( FLUTTER_API_SYMBOL(FlutterEngine) engine); +//------------------------------------------------------------------------------ +/// @brief Adds a view. +/// +/// This is an asynchronous operation. The view should not be used +/// until the |info.add_view_callback| is invoked with an |added| +/// value of true. The embedder should prepare resources in advance +/// but be ready to clean up on failure. +/// +/// A frame is scheduled if the operation succeeds. +/// +/// The callback is invoked on a thread managed by the engine. The +/// embedder should re-thread if needed. +/// +/// Attempting to add the implicit view will fail and will return +/// kInvalidArguments. Attempting to add a view with an already +/// existing view ID will fail, and |info.add_view_callback| will be +/// invoked with an |added| value of false. +/// +/// @param[in] engine A running engine instance. +/// @param[in] info The add view arguments. This can be deallocated +/// once |FlutterEngineAddView| returns, before +/// |add_view_callback| is invoked. +/// +/// @return The result of *starting* the asynchronous operation. If +/// `kSuccess`, the |add_view_callback| will be invoked. +FLUTTER_EXPORT +FlutterEngineResult FlutterEngineAddView(FLUTTER_API_SYMBOL(FlutterEngine) + engine, + const FlutterAddViewInfo* info); + +//------------------------------------------------------------------------------ +/// @brief Removes a view. +/// +/// This is an asynchronous operation. The view's resources must not +/// be cleaned up until |info.remove_view_callback| is invoked with +/// a |removed| value of true. +/// +/// The callback is invoked on a thread managed by the engine. The +/// embedder should re-thread if needed. +/// +/// Attempting to remove the implicit view will fail and will return +/// kInvalidArguments. Attempting to remove a view with a +/// non-existent view ID will fail, and |info.remove_view_callback| +/// will be invoked with a |removed| value of false. +/// +/// @param[in] engine A running engine instance. +/// @param[in] info The remove view arguments. This can be deallocated +/// once |FlutterEngineRemoveView| returns, before +/// |remove_view_callback| is invoked. +/// +/// @return The result of *starting* the asynchronous operation. If +/// `kSuccess`, the |remove_view_callback| will be invoked. +FLUTTER_EXPORT +FlutterEngineResult FlutterEngineRemoveView(FLUTTER_API_SYMBOL(FlutterEngine) + engine, + const FlutterRemoveViewInfo* info); + FLUTTER_EXPORT FlutterEngineResult FlutterEngineSendWindowMetricsEvent( FLUTTER_API_SYMBOL(FlutterEngine) engine, @@ -2913,6 +3308,12 @@ typedef FlutterEngineResult (*FlutterEngineSetNextFrameCallbackFnPtr)( FLUTTER_API_SYMBOL(FlutterEngine) engine, VoidCallback callback, void* user_data); +typedef FlutterEngineResult (*FlutterEngineAddViewFnPtr)( + FLUTTER_API_SYMBOL(FlutterEngine) engine, + const FlutterAddViewInfo* info); +typedef FlutterEngineResult (*FlutterEngineRemoveViewFnPtr)( + FLUTTER_API_SYMBOL(FlutterEngine) engine, + const FlutterRemoveViewInfo* info); /// Function-pointer-based versions of the APIs above. typedef struct { @@ -2959,6 +3360,8 @@ typedef struct { FlutterEngineNotifyDisplayUpdateFnPtr NotifyDisplayUpdate; FlutterEngineScheduleFrameFnPtr ScheduleFrame; FlutterEngineSetNextFrameCallbackFnPtr SetNextFrameCallback; + FlutterEngineAddViewFnPtr AddView; + FlutterEngineRemoveViewFnPtr RemoveView; } FlutterEngineProcTable; //------------------------------------------------------------------------------ @@ -2973,8 +3376,10 @@ FLUTTER_EXPORT FlutterEngineResult FlutterEngineGetProcAddresses( FlutterEngineProcTable* table); +// NOLINTEND(google-objc-function-naming) + #if defined(__cplusplus) } // extern "C" #endif -#endif // FLUTTER_EMBEDDER_H_ +#endif // FLUTTER_SHELL_PLATFORM_EMBEDDER_EMBEDDER_H_ From 67c66e48434c565747da60dd382390df4234302b Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Fri, 20 Jun 2025 21:13:04 +0200 Subject: [PATCH 31/41] kms: split drmdev into drm_resources and drmdev drmdev: (mostly) non-stateful part. Basically a better wrapper around a drm fd. Fully mt-safe. Also properly separate kms_req from drmdev. kms_req now mostly accesses public drmdev API, instead of having access to hidden drmdev state. drm_resources: the DRM state / resources. Is stateful, but does not update itself. To keep it in sync with kernel state, one needs to listen to kernel events with drm_monitor and call drm_resources_update. drm_resources is not mt-safe and only supposed to be used on a single thread. Also add a bunch of QoL stuff to drm_resources. --- CMakeLists.txt | 6 +- src/compositor_ng.c | 4 +- src/compositor_ng.h | 4 +- src/egl_gbm_render_surface.c | 7 +- src/flutter-pi.c | 92 +- src/kms/drmdev.c | 944 ++++++++ src/kms/drmdev.h | 165 ++ src/kms/req_builder.c | 959 ++++++++ src/kms/req_builder.h | 206 ++ src/kms/resources.c | 1495 ++++++++++++ src/{modesetting.h => kms/resources.h} | 541 ++--- src/modesetting.c | 3007 ------------------------ src/window.c | 144 +- src/window.h | 3 +- 14 files changed, 4043 insertions(+), 3534 deletions(-) create mode 100644 src/kms/drmdev.c create mode 100644 src/kms/drmdev.h create mode 100644 src/kms/req_builder.c create mode 100644 src/kms/req_builder.h create mode 100644 src/kms/resources.c rename src/{modesetting.h => kms/resources.h} (61%) delete mode 100644 src/modesetting.c diff --git a/CMakeLists.txt b/CMakeLists.txt index b4eba442..605029b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -127,10 +127,12 @@ add_library( src/platformchannel.c src/pluginregistry.c src/texture_registry.c - src/modesetting.c - src/util/collection.c + src/kms/drmdev.c + src/kms/req_builder.c + src/kms/resources.c src/util/bitscan.c src/util/vector.c + src/util/collection.c src/cursor.c src/keyboard.c src/user_input.c diff --git a/src/compositor_ng.c b/src/compositor_ng.c index 48df8015..2f2ada87 100644 --- a/src/compositor_ng.c +++ b/src/compositor_ng.c @@ -27,7 +27,9 @@ #include "dummy_render_surface.h" #include "flutter-pi.h" #include "frame_scheduler.h" -#include "modesetting.h" +#include "kms/drmdev.h" +#include "kms/req_builder.h" +#include "kms/resources.h" #include "notifier_listener.h" #include "pixel_format.h" #include "render_surface.h" diff --git a/src/compositor_ng.h b/src/compositor_ng.h index d7360964..d1895f69 100644 --- a/src/compositor_ng.h +++ b/src/compositor_ng.h @@ -15,7 +15,9 @@ #include "cursor.h" #include "flutter-pi.h" #include "frame_scheduler.h" -#include "modesetting.h" +#include "kms/drmdev.h" +#include "kms/resources.h" +#include "kms/req_builder.h" #include "pixel_format.h" #include "util/collection.h" #include "util/refcounting.h" diff --git a/src/egl_gbm_render_surface.c b/src/egl_gbm_render_surface.c index 74b0ae01..cffaf3ed 100644 --- a/src/egl_gbm_render_surface.c +++ b/src/egl_gbm_render_surface.c @@ -16,7 +16,8 @@ #include "egl.h" #include "gl_renderer.h" #include "gles.h" -#include "modesetting.h" +#include "kms/req_builder.h" +#include "kms/resources.h" #include "pixel_format.h" #include "render_surface.h" #include "render_surface_private.h" @@ -428,7 +429,7 @@ static int egl_gbm_render_surface_present_kms(struct surface *s, const struct fl struct drm_crtc *crtc = kms_req_builder_get_crtc(builder); ASSERT_NOT_NULL(crtc); - if (drm_crtc_any_plane_supports_format(meta->drmdev, crtc, egl_surface->pixel_format)) { + if (drm_resources_any_crtc_plane_supports_format(kms_req_builder_peek_resources(builder), crtc->id, egl_surface->pixel_format)) { TRACER_BEGIN(egl_surface->surface.tracer, "drmdev_add_fb (non-opaque)"); uint32_t fb_id = drmdev_add_fb_from_gbm_bo( meta->drmdev, @@ -453,7 +454,7 @@ static int egl_gbm_render_surface_present_kms(struct surface *s, const struct fl // if this EGL surface is non-opaque and has an opaque equivalent if (!get_pixfmt_info(egl_surface->pixel_format)->is_opaque && pixfmt_opaque(egl_surface->pixel_format) != egl_surface->pixel_format && - drm_crtc_any_plane_supports_format(meta->drmdev, crtc, pixfmt_opaque(egl_surface->pixel_format))) { + drm_resources_any_crtc_plane_supports_format(kms_req_builder_peek_resources(builder), crtc->id, pixfmt_opaque(egl_surface->pixel_format))) { uint32_t opaque_fb_id = drmdev_add_fb_from_gbm_bo( meta->drmdev, bo, diff --git a/src/flutter-pi.c b/src/flutter-pi.c index afbafb6e..0195c4db 100644 --- a/src/flutter-pi.c +++ b/src/flutter-pi.c @@ -45,7 +45,7 @@ #include "frame_scheduler.h" #include "keyboard.h" #include "locales.h" -#include "modesetting.h" +#include "kms/drmdev.h" #include "pixel_format.h" #include "platformchannel.h" #include "pluginregistry.h" @@ -1097,7 +1097,8 @@ static int on_drmdev_ready(sd_event_source *s, int fd, uint32_t revents, void *u ASSERT_NOT_NULL(userdata); drmdev = userdata; - return drmdev_on_event_fd_ready(drmdev); + drmdev_dispatch_modesetting(drmdev); + return 0; } static const FlutterLocale *on_compute_platform_resolved_locales(const FlutterLocale **locales, size_t n_locales) { @@ -2120,74 +2121,7 @@ static void on_drmdev_close(int fd, void *fd_metadata, void *userdata) { } } -static const struct drmdev_interface drmdev_interface = { .open = on_drmdev_open, .close = on_drmdev_close }; - -static struct drmdev *find_drmdev(struct libseat *libseat) { - struct drm_connector *connector; - struct drmdev *drmdev; - drmDevicePtr devices[64]; - int ok, n_devices; - -#ifndef HAVE_LIBSEAT - ASSERT_EQUALS(libseat, NULL); -#endif - - ok = drmGetDevices2(0, devices, ARRAY_SIZE(devices)); - if (ok < 0) { - LOG_ERROR("Could not query DRM device list: %s\n", strerror(-ok)); - return NULL; - } - - n_devices = ok; - - // find a GPU that has a primary node - drmdev = NULL; - for (int i = 0; i < n_devices; i++) { - drmDevicePtr device; - - device = devices[i]; - - if (!(device->available_nodes & (1 << DRM_NODE_PRIMARY))) { - // We need a primary node. - continue; - } - - drmdev = drmdev_new_from_path(device->nodes[DRM_NODE_PRIMARY], &drmdev_interface, libseat); - if (drmdev == NULL) { - LOG_ERROR("Could not create drmdev from device at \"%s\". Continuing.\n", device->nodes[DRM_NODE_PRIMARY]); - continue; - } - - for_each_connector_in_drmdev(drmdev, connector) { - if (connector->variable_state.connection_state == kConnected_DrmConnectionState) { - goto found_connected_connector; - } - } - LOG_ERROR("Device \"%s\" doesn't have a display connected. Skipping.\n", device->nodes[DRM_NODE_PRIMARY]); - drmdev_unref(drmdev); - continue; - -found_connected_connector: - break; - } - - drmFreeDevices(devices, n_devices); - - if (drmdev == NULL) { - LOG_ERROR( - "flutter-pi couldn't find a usable DRM device.\n" - "Please make sure you've enabled the Fake-KMS driver in raspi-config.\n" - "If you're not using a Raspberry Pi, please make sure there's KMS support for your graphics chip.\n" - ); - goto fail_free_devices; - } - - return drmdev; - -fail_free_devices: - drmFreeDevices(devices, n_devices); - return NULL; -} +static const struct drmdev_file_interface drmdev_interface = { .open = on_drmdev_open, .close = on_drmdev_close }; static struct gbm_device *open_rendernode_as_gbm_device(void) { struct gbm_device *gbm; @@ -2338,7 +2272,6 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { struct flutterpi_cmdline_args cmd_args; struct libseat *libseat; struct locales *locales; - struct drmdev *drmdev; struct tracer *tracer; struct window *window; void *engine_handle; @@ -2455,6 +2388,8 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { locales_print(locales); + struct drmdev *drmdev = NULL; + struct drm_resources *resources = NULL; if (cmd_args.dummy_display) { drmdev = NULL; @@ -2467,11 +2402,16 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { goto fail_destroy_locales; } } else { - drmdev = find_drmdev(libseat); + // TODO: Share this udev instance with the one the user input uses. + struct udev *udev = udev_new(); + + drmdev = drmdev_new_from_udev_primary(udev, "seat0", &drmdev_interface, NULL); if (drmdev == NULL) { goto fail_destroy_locales; } + udev_unref(udev); + gbm_device = drmdev_get_gbm_device(drmdev); if (gbm_device == NULL) { LOG_ERROR("Couldn't create GBM device.\n"); @@ -2563,6 +2503,7 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { cmd_args.has_physical_dimensions, cmd_args.physical_dimensions.x, cmd_args.physical_dimensions.y, cmd_args.has_pixel_format, cmd_args.pixel_format, drmdev, + resources, desired_videomode // clang-format on ); @@ -2572,6 +2513,8 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { } } + drm_resources_unrefp(&resources); + compositor = compositor_new(tracer, window); if (compositor == NULL) { LOG_ERROR("Couldn't create compositor.\n"); @@ -2580,7 +2523,7 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { /// TODO: Do we really need the window after this? if (drmdev != NULL) { - ok = sd_event_add_io(event_loop, NULL, drmdev_get_event_fd(drmdev), EPOLLIN | EPOLLHUP | EPOLLPRI, on_drmdev_ready, drmdev); + ok = sd_event_add_io(event_loop, NULL, drmdev_get_modesetting_fd(drmdev), EPOLLIN | EPOLLHUP | EPOLLPRI, on_drmdev_ready, drmdev); if (ok < 0) { LOG_ERROR("Could not add DRM pageflip event listener. sd_event_add_io: %s\n", strerror(-ok)); goto fail_unref_compositor; @@ -2778,6 +2721,9 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { tracer_unref(tracer); fail_destroy_drmdev: + if (resources != NULL) { + drm_resources_unref(resources); + } drmdev_unref(drmdev); fail_destroy_locales: diff --git a/src/kms/drmdev.c b/src/kms/drmdev.c new file mode 100644 index 00000000..f20078b4 --- /dev/null +++ b/src/kms/drmdev.c @@ -0,0 +1,944 @@ +#include "drmdev.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "pixel_format.h" +#include "resources.h" +#include "util/bitset.h" +#include "util/list.h" +#include "util/lock_ops.h" +#include "util/logging.h" +#include "util/macros.h" +#include "util/refcounting.h" +#include "util/khash.h" + +struct pageflip_callbacks { + uint32_t crtc_id; + + int index; + struct { + drmdev_scanout_cb_t scanout_callback; + void *scanout_callback_userdata; + + void_callback_t void_callback; + void *void_callback_userdata; + } callbacks[2]; +}; + +KHASH_MAP_INIT_INT(pageflip_callbacks, struct pageflip_callbacks) + +struct drm_fb { + struct list_head entry; + + uint32_t id; + + uint32_t width, height; + + enum pixfmt format; + + bool has_modifier; + uint64_t modifier; + + uint32_t flags; + + uint32_t handles[4]; + uint32_t pitches[4]; + uint32_t offsets[4]; +}; + +struct drmdev { + int fd; + void *fd_metadata; + + refcount_t n_refs; + pthread_mutex_t mutex; + + bool supports_atomic_modesetting; + bool supports_dumb_buffers; + + struct { + drmdev_scanout_cb_t scanout_callback; + void *userdata; + void_callback_t destroy_callback; + + struct kms_req *last_flipped; + } per_crtc_state[32]; + + struct gbm_device *gbm_device; + + struct drmdev_file_interface interface; + void *interface_userdata; + + struct list_head fbs; + khash_t(pageflip_callbacks) *pageflip_callbacks; + + struct udev *udev; + struct udev_device *kms_udev; + const char *sysnum; +}; + +/** + * @brief Check if the given file descriptor is a DRM master. + */ +static bool is_drm_master(int fd) { + return drmAuthMagic(fd, 0) != -EACCES; +} + +/** + * @brief Check if the given path is a path to a KMS device. + */ +static bool is_kms_device(const char *path, const struct drmdev_file_interface *interface, void *userdata) { + void *fd_metadata; + + int fd = interface->open(path, O_RDWR, &fd_metadata, userdata); + if (fd < 0) { + return false; + } + + if (!drmIsKMS(fd)) { + interface->close(fd, fd_metadata, userdata); + return false; + } + + interface->close(fd, fd_metadata, userdata); + return true; +} + +static void assert_rotations_work() { + assert(PLANE_TRANSFORM_ROTATE_0.rotate_0 == true); + assert(PLANE_TRANSFORM_ROTATE_0.rotate_90 == false); + assert(PLANE_TRANSFORM_ROTATE_0.rotate_180 == false); + assert(PLANE_TRANSFORM_ROTATE_0.rotate_270 == false); + assert(PLANE_TRANSFORM_ROTATE_0.reflect_x == false); + assert(PLANE_TRANSFORM_ROTATE_0.reflect_y == false); + + assert(PLANE_TRANSFORM_ROTATE_90.rotate_0 == false); + assert(PLANE_TRANSFORM_ROTATE_90.rotate_90 == true); + assert(PLANE_TRANSFORM_ROTATE_90.rotate_180 == false); + assert(PLANE_TRANSFORM_ROTATE_90.rotate_270 == false); + assert(PLANE_TRANSFORM_ROTATE_90.reflect_x == false); + assert(PLANE_TRANSFORM_ROTATE_90.reflect_y == false); + + assert(PLANE_TRANSFORM_ROTATE_180.rotate_0 == false); + assert(PLANE_TRANSFORM_ROTATE_180.rotate_90 == false); + assert(PLANE_TRANSFORM_ROTATE_180.rotate_180 == true); + assert(PLANE_TRANSFORM_ROTATE_180.rotate_270 == false); + assert(PLANE_TRANSFORM_ROTATE_180.reflect_x == false); + assert(PLANE_TRANSFORM_ROTATE_180.reflect_y == false); + + assert(PLANE_TRANSFORM_ROTATE_270.rotate_0 == false); + assert(PLANE_TRANSFORM_ROTATE_270.rotate_90 == false); + assert(PLANE_TRANSFORM_ROTATE_270.rotate_180 == false); + assert(PLANE_TRANSFORM_ROTATE_270.rotate_270 == true); + assert(PLANE_TRANSFORM_ROTATE_270.reflect_x == false); + assert(PLANE_TRANSFORM_ROTATE_270.reflect_y == false); + + assert(PLANE_TRANSFORM_REFLECT_X.rotate_0 == false); + assert(PLANE_TRANSFORM_REFLECT_X.rotate_90 == false); + assert(PLANE_TRANSFORM_REFLECT_X.rotate_180 == false); + assert(PLANE_TRANSFORM_REFLECT_X.rotate_270 == false); + assert(PLANE_TRANSFORM_REFLECT_X.reflect_x == true); + assert(PLANE_TRANSFORM_REFLECT_X.reflect_y == false); + + assert(PLANE_TRANSFORM_REFLECT_Y.rotate_0 == false); + assert(PLANE_TRANSFORM_REFLECT_Y.rotate_90 == false); + assert(PLANE_TRANSFORM_REFLECT_Y.rotate_180 == false); + assert(PLANE_TRANSFORM_REFLECT_Y.rotate_270 == false); + assert(PLANE_TRANSFORM_REFLECT_Y.reflect_x == false); + assert(PLANE_TRANSFORM_REFLECT_Y.reflect_y == true); + + drm_plane_transform_t r = PLANE_TRANSFORM_NONE; + + r.rotate_0 = true; + r.reflect_x = true; + assert(r.u32 == (DRM_MODE_ROTATE_0 | DRM_MODE_REFLECT_X)); + + r.u32 = DRM_MODE_ROTATE_90 | DRM_MODE_REFLECT_Y; + assert(r.rotate_0 == false); + assert(r.rotate_90 == true); + assert(r.rotate_180 == false); + assert(r.rotate_270 == false); + assert(r.reflect_x == false); + assert(r.reflect_y == true); + (void) r; +} + +static int set_drm_client_caps(int fd, bool *supports_atomic_modesetting) { + int ok; + + ok = drmSetClientCap(fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1); + if (ok < 0) { + ok = errno; + LOG_ERROR("Could not set DRM client universal planes capable. drmSetClientCap: %s\n", strerror(ok)); + return ok; + } + +#ifdef USE_LEGACY_KMS + *supports_atomic_modesetting = false; +#else + ok = drmSetClientCap(fd, DRM_CLIENT_CAP_ATOMIC, 1); + if ((ok < 0) && (errno == EOPNOTSUPP)) { + if (supports_atomic_modesetting != NULL) { + *supports_atomic_modesetting = false; + } + } else if (ok < 0) { + ok = errno; + LOG_ERROR("Could not set DRM client atomic capable. drmSetClientCap: %s\n", strerror(ok)); + return ok; + } else { + if (supports_atomic_modesetting != NULL) { + *supports_atomic_modesetting = true; + } + } +#endif + + return 0; +} + + +static struct udev_device *find_udev_kms_device(struct udev *udev, const char *seat, const struct drmdev_file_interface *interface, void *interface_userdata) { + struct udev_enumerate *enumerator; + + enumerator = udev_enumerate_new(udev); + udev_enumerate_add_match_subsystem(enumerator, "drm"); + udev_enumerate_add_match_sysname(enumerator, "card[0-9]*"); + + udev_enumerate_scan_devices(enumerator); + + struct udev_list_entry *entry; + udev_list_entry_foreach(entry, udev_enumerate_get_list_entry(enumerator)) { + const char *syspath = udev_list_entry_get_name(entry); + + struct udev_device *udev_device = udev_device_new_from_syspath(udev, syspath); + if (udev_device == NULL) { + LOG_ERROR("Could not create udev device from syspath. udev_device_new_from_syspath: %s\n", strerror(errno)); + continue; + } + + // Find out if the drm card is connected to our seat. + // This could also be part of the enumerator filter, e.g.: + // + // udev_enumerate_add_match_property(enumerator, "ID_SEAT", seat), + // + // if we didn't have to handle a NULL value for ID_SEAT. + const char *device_seat = udev_device_get_property_value(udev_device, "ID_SEAT"); + if (device_seat == NULL) { + device_seat = "seat0"; + } + if (!streq(device_seat, seat)) { + udev_device_unref(udev_device); + continue; + } + + // devnode is the path to the /dev/dri/cardX device. + const char *devnode = udev_device_get_devnode(udev_device); + if (devnode == NULL) { + // likely a connector, not a card. + udev_device_unref(udev_device); + continue; + } + + if (access(devnode, R_OK | W_OK) != 0) { + LOG_ERROR("Insufficient permissions to open KMS device \"%s\" for display output. access: %s\n", devnode, strerror(errno)); + udev_device_unref(udev_device); + continue; + } + + if (!is_kms_device(devnode, interface, interface_userdata)) { + udev_device_unref(udev_device); + continue; + } + + udev_enumerate_unref(enumerator); + return udev_device; + } + + udev_enumerate_unref(enumerator); + return NULL; +} + +static void drmdev_on_page_flip( + struct drmdev *drmdev, + uint32_t crtc_id, + uint64_t vblank_ns +) { + ASSERT_NOT_NULL(drmdev); + struct pageflip_callbacks cbs_copy; + + { + ASSERTED int ok; + ok = pthread_mutex_lock(&drmdev->mutex); + ASSERT_ZERO(ok); + + khint_t cbs_bucket = kh_get(pageflip_callbacks, drmdev->pageflip_callbacks, crtc_id); + if (cbs_bucket == kh_end(drmdev->pageflip_callbacks)) { + // No callbacks for this CRTC. + ok = pthread_mutex_unlock(&drmdev->mutex); + ASSERT_ZERO(ok); + return; + } + + struct pageflip_callbacks *cbs = &kh_value(drmdev->pageflip_callbacks, cbs_bucket); + memcpy(&cbs_copy, cbs, sizeof *cbs); + + cbs->callbacks[cbs->index].scanout_callback = NULL; + cbs->callbacks[cbs->index].void_callback = NULL; + cbs->callbacks[cbs->index].scanout_callback_userdata = NULL; + cbs->callbacks[cbs->index].void_callback_userdata = NULL; + cbs->index = cbs->index ^ 1; + + ok = pthread_mutex_unlock(&drmdev->mutex); + ASSERT_ZERO(ok); + } + + if (cbs_copy.callbacks[cbs_copy.index].scanout_callback != NULL) { + cbs_copy.callbacks[cbs_copy.index].scanout_callback(vblank_ns, cbs_copy.callbacks[cbs_copy.index].scanout_callback_userdata); + } + + if (cbs_copy.callbacks[cbs_copy.index].void_callback != NULL) { + cbs_copy.callbacks[cbs_copy.index].void_callback(cbs_copy.callbacks[cbs_copy.index].void_callback_userdata); + } +} + +static void on_page_flip( + int fd, + unsigned int sequence, + unsigned int tv_sec, unsigned int tv_usec, + unsigned int crtc_id, + void *userdata +) { + struct drmdev *drmdev; + + ASSERT_NOT_NULL(userdata); + drmdev = userdata; + + (void) fd; + (void) sequence; + + uint64_t vblank_ns = tv_sec * 1000000000ull + tv_usec * 1000ull; + drmdev_on_page_flip(drmdev, crtc_id, vblank_ns); + + drmdev_unref(drmdev); +} + +/** + * @brief Should be called when the drmdev modesetting fd is ready. + */ +void drmdev_dispatch_modesetting(struct drmdev *drmdev) { + int ok; + + static drmEventContext ctx = { + .version = DRM_EVENT_CONTEXT_VERSION, + .vblank_handler = NULL, + .page_flip_handler = NULL, + .page_flip_handler2 = on_page_flip, + .sequence_handler = NULL, + }; + + ok = drmHandleEvent(drmdev->fd, &ctx); + if (ok != 0) { + LOG_ERROR("Could not handle DRM event. drmHandleEvent: %s\n", strerror(errno)); + } +} + +/** + * @brief Create a new drmdev from the primary drm device for the given udev & seat. + */ +struct drmdev *drmdev_new_from_udev_primary(struct udev *udev, const char *seat, const struct drmdev_file_interface *interface, void *interface_userdata) { + struct drmdev *d; + uint64_t cap; + int ok; + + assert_rotations_work(); + + d = malloc(sizeof *d); + if (d == NULL) { + return NULL; + } + + d->n_refs = REFCOUNT_INIT_1; + pthread_mutex_init(&d->mutex, get_default_mutex_attrs()); + d->interface = *interface; + d->interface_userdata = interface_userdata; + + // find a KMS device for the given seat. + d->kms_udev = find_udev_kms_device(udev, seat, interface, interface_userdata); + if (d->kms_udev == NULL) { + LOG_ERROR("Could not find a KMS device for seat %s.\n", seat); + goto fail_free_dev; + } + + d->sysnum = udev_device_get_sysnum(d->kms_udev); + + d->fd = interface->open(udev_device_get_devnode(d->kms_udev), O_RDWR, &d->fd_metadata, interface_userdata); + if (d->fd < 0) { + LOG_ERROR("Could not open KMS device. interface->open: %s\n", strerror(errno)); + goto fail_unref_kms_udev; + } + + set_drm_client_caps(d->fd, &d->supports_atomic_modesetting); + + cap = 0; + ok = drmGetCap(d->fd, DRM_CAP_DUMB_BUFFER, &cap); + if (ok < 0) { + d->supports_dumb_buffers = false; + } else { + d->supports_dumb_buffers = !!cap; + } + + d->gbm_device = gbm_create_device(d->fd); + if (d->gbm_device == NULL) { + LOG_ERROR("Could not create GBM device.\n"); + goto fail_close_fd; + } + + list_inithead(&d->fbs); + d->pageflip_callbacks = kh_init(pageflip_callbacks); + + return d; + + +fail_close_fd: + interface->close(d->fd, d->fd_metadata, interface_userdata); + +fail_unref_kms_udev: + udev_device_unref(d->kms_udev); + +fail_free_dev: + free(d); + return NULL; +} + +static void drmdev_destroy(struct drmdev *drmdev) { + assert(refcount_is_zero(&drmdev->n_refs)); + + gbm_device_destroy(drmdev->gbm_device); + drmdev->interface.close(drmdev->fd, drmdev->fd_metadata, drmdev->interface_userdata); + free(drmdev); +} + +DEFINE_REF_OPS(drmdev, n_refs) + +struct drm_resources *drmdev_query_resources(struct drmdev *drmdev) { + ASSERT_NOT_NULL(drmdev); + return drm_resources_new(drmdev->fd); +} + +int drmdev_get_modesetting_fd(struct drmdev *drmdev) { + ASSERT_NOT_NULL(drmdev); + return drmdev->fd; +} + +bool drmdev_supports_dumb_buffers(struct drmdev *drmdev) { + return drmdev->supports_dumb_buffers; +} + +int drmdev_create_dumb_buffer( + struct drmdev *drmdev, + int width, + int height, + int bpp, + uint32_t *gem_handle_out, + uint32_t *pitch_out, + size_t *size_out +) { + struct drm_mode_create_dumb create_req; + int ok; + + ASSERT_NOT_NULL(drmdev); + ASSERT_NOT_NULL(gem_handle_out); + ASSERT_NOT_NULL(pitch_out); + ASSERT_NOT_NULL(size_out); + + memset(&create_req, 0, sizeof create_req); + create_req.width = width; + create_req.height = height; + create_req.bpp = bpp; + create_req.flags = 0; + + ok = ioctl(drmdev->fd, DRM_IOCTL_MODE_CREATE_DUMB, &create_req); + if (ok < 0) { + ok = errno; + LOG_ERROR("Could not create dumb buffer. ioctl: %s\n", strerror(errno)); + goto fail_return_ok; + } + + *gem_handle_out = create_req.handle; + *pitch_out = create_req.pitch; + *size_out = create_req.size; + return 0; + +fail_return_ok: + return ok; +} + +void drmdev_destroy_dumb_buffer(struct drmdev *drmdev, uint32_t gem_handle) { + struct drm_mode_destroy_dumb destroy_req; + int ok; + + ASSERT_NOT_NULL(drmdev); + + memset(&destroy_req, 0, sizeof destroy_req); + destroy_req.handle = gem_handle; + + ok = ioctl(drmdev->fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy_req); + if (ok < 0) { + LOG_ERROR("Could not destroy dumb buffer. ioctl: %s\n", strerror(errno)); + } +} + +void *drmdev_map_dumb_buffer(struct drmdev *drmdev, uint32_t gem_handle, size_t size) { + struct drm_mode_map_dumb map_req; + void *map; + int ok; + + ASSERT_NOT_NULL(drmdev); + + memset(&map_req, 0, sizeof map_req); + map_req.handle = gem_handle; + + ok = ioctl(drmdev->fd, DRM_IOCTL_MODE_MAP_DUMB, &map_req); + if (ok < 0) { + LOG_ERROR("Could not prepare dumb buffer mmap. ioctl: %s\n", strerror(errno)); + return NULL; + } + + map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, drmdev->fd, map_req.offset); + if (map == MAP_FAILED) { + LOG_ERROR("Could not mmap dumb buffer. mmap: %s\n", strerror(errno)); + return NULL; + } + + return map; +} + +void drmdev_unmap_dumb_buffer(struct drmdev *drmdev, void *map, size_t size) { + int ok; + + ASSERT_NOT_NULL(drmdev); + ASSERT_NOT_NULL(map); + (void) drmdev; + + ok = munmap(map, size); + if (ok < 0) { + LOG_ERROR("Couldn't unmap dumb buffer. munmap: %s\n", strerror(errno)); + } +} + +struct gbm_device *drmdev_get_gbm_device(struct drmdev *drmdev) { + ASSERT_NOT_NULL(drmdev); + return drmdev->gbm_device; +} + +int drmdev_get_last_vblank(struct drmdev *drmdev, uint32_t crtc_id, uint64_t *last_vblank_ns_out) { + int ok; + + ASSERT_NOT_NULL(drmdev); + ASSERT_NOT_NULL(last_vblank_ns_out); + + ok = drmCrtcGetSequence(drmdev->fd, crtc_id, NULL, last_vblank_ns_out); + if (ok < 0) { + ok = errno; + LOG_ERROR("Could not get next vblank timestamp. drmCrtcGetSequence: %s\n", strerror(ok)); + return ok; + } + + return 0; +} + +uint32_t drmdev_add_fb_multiplanar( + struct drmdev *drmdev, + uint32_t width, + uint32_t height, + enum pixfmt pixel_format, + const uint32_t bo_handles[4], + const uint32_t pitches[4], + const uint32_t offsets[4], + bool has_modifiers, + const uint64_t modifiers[4] +) { + struct drm_fb *fb; + uint32_t fb_id; + int ok; + + /// TODO: Code in https://elixir.bootlin.com/linux/latest/source/drivers/gpu/drm/drm_framebuffer.c#L257 + /// assumes handles, pitches, offsets and modifiers for unused planes are zero. Make sure that's the + /// case here. + ASSERT_NOT_NULL(drmdev); + assert(width > 0 && height > 0); + assert(bo_handles[0] != 0); + assert(pitches[0] != 0); + + fb = malloc(sizeof *fb); + if (fb == NULL) { + return 0; + } + + list_inithead(&fb->entry); + fb->id = 0; + fb->width = width; + fb->height = height; + fb->format = pixel_format; + fb->has_modifier = has_modifiers; + fb->modifier = modifiers[0]; + fb->flags = 0; + memcpy(fb->handles, bo_handles, sizeof(fb->handles)); + memcpy(fb->pitches, pitches, sizeof(fb->pitches)); + memcpy(fb->offsets, offsets, sizeof(fb->offsets)); + + fb_id = 0; + if (has_modifiers) { + ok = drmModeAddFB2WithModifiers( + drmdev->fd, + width, + height, + get_pixfmt_info(pixel_format)->drm_format, + bo_handles, + pitches, + offsets, + modifiers, + &fb_id, + DRM_MODE_FB_MODIFIERS + ); + if (ok < 0) { + LOG_ERROR("Couldn't add buffer as DRM fb. drmModeAddFB2WithModifiers: %s\n", strerror(-ok)); + goto fail_free_fb; + } + } else { + ok = drmModeAddFB2(drmdev->fd, width, height, get_pixfmt_info(pixel_format)->drm_format, bo_handles, pitches, offsets, &fb_id, 0); + if (ok < 0) { + LOG_ERROR("Couldn't add buffer as DRM fb. drmModeAddFB2: %s\n", strerror(-ok)); + goto fail_free_fb; + } + } + + fb->id = fb_id; + + pthread_mutex_lock(&drmdev->mutex); + + list_add(&fb->entry, &drmdev->fbs); + + pthread_mutex_unlock(&drmdev->mutex); + + assert(fb_id != 0); + return fb_id; + +fail_free_fb: + free(fb); + return 0; +} + +uint32_t drmdev_add_fb( + struct drmdev *drmdev, + uint32_t width, + uint32_t height, + enum pixfmt pixel_format, + uint32_t bo_handle, + uint32_t pitch, + uint32_t offset, + bool has_modifier, + uint64_t modifier +) { + return drmdev_add_fb_multiplanar( + drmdev, + width, + height, + pixel_format, + (uint32_t[4]){ bo_handle, 0 }, + (uint32_t[4]){ pitch, 0 }, + (uint32_t[4]){ offset, 0 }, + has_modifier, + (const uint64_t[4]){ modifier, 0 } + ); +} + +uint32_t drmdev_add_fb_from_dmabuf( + struct drmdev *drmdev, + uint32_t width, + uint32_t height, + enum pixfmt pixel_format, + int prime_fd, + uint32_t pitch, + uint32_t offset, + bool has_modifier, + uint64_t modifier +) { + uint32_t bo_handle; + int ok; + + ok = drmPrimeFDToHandle(drmdev->fd, prime_fd, &bo_handle); + if (ok < 0) { + LOG_ERROR("Couldn't import DMA-buffer as GEM buffer. drmPrimeFDToHandle: %s\n", strerror(errno)); + return 0; + } + + return drmdev_add_fb(drmdev, width, height, pixel_format, prime_fd, pitch, offset, has_modifier, modifier); +} + +uint32_t drmdev_add_fb_from_dmabuf_multiplanar( + struct drmdev *drmdev, + uint32_t width, + uint32_t height, + enum pixfmt pixel_format, + const int prime_fds[4], + const uint32_t pitches[4], + const uint32_t offsets[4], + bool has_modifiers, + const uint64_t modifiers[4] +) { + uint32_t bo_handles[4] = { 0 }; + int ok; + + for (int i = 0; (i < 4) && (prime_fds[i] != 0); i++) { + ok = drmPrimeFDToHandle(drmdev->fd, prime_fds[i], bo_handles + i); + if (ok < 0) { + LOG_ERROR("Couldn't import DMA-buffer as GEM buffer. drmPrimeFDToHandle: %s\n", strerror(errno)); + return 0; + } + } + + return drmdev_add_fb_multiplanar(drmdev, width, height, pixel_format, bo_handles, pitches, offsets, has_modifiers, modifiers); +} + +uint32_t drmdev_add_fb_from_gbm_bo(struct drmdev *drmdev, struct gbm_bo *bo, bool cast_opaque) { + enum pixfmt format; + uint32_t fourcc; + int n_planes; + + n_planes = gbm_bo_get_plane_count(bo); + ASSERT(0 <= n_planes && n_planes <= 4); + + fourcc = gbm_bo_get_format(bo); + + if (!has_pixfmt_for_gbm_format(fourcc)) { + LOG_ERROR("GBM pixel format is not supported.\n"); + return 0; + } + + format = get_pixfmt_for_gbm_format(fourcc); + + if (cast_opaque) { + format = pixfmt_opaque(format); + } + + uint32_t handles[4]; + uint32_t pitches[4]; + + // Returns DRM_FORMAT_MOD_INVALID on failure, or DRM_FORMAT_MOD_LINEAR + // for dumb buffers. + uint64_t modifier = gbm_bo_get_modifier(bo); + bool has_modifiers = modifier != DRM_FORMAT_MOD_INVALID; + + for (int i = 0; i < n_planes; i++) { + // gbm_bo_get_handle_for_plane will return -1 (in gbm_bo_handle.s32) and + // set errno on failure. + errno = 0; + union gbm_bo_handle handle = gbm_bo_get_handle_for_plane(bo, i); + if (handle.s32 == -1) { + LOG_ERROR("Could not get GEM handle for plane %d: %s\n", i, strerror(errno)); + return 0; + } + + handles[i] = handle.u32; + + // gbm_bo_get_stride_for_plane will return 0 and set errno on failure. + errno = 0; + uint32_t pitch = gbm_bo_get_stride_for_plane(bo, i); + if (pitch == 0 && errno != 0) { + LOG_ERROR("Could not get framebuffer stride for plane %d: %s\n", i, strerror(errno)); + return 0; + } + + pitches[i] = pitch; + } + + for (int i = n_planes; i < 4; i++) { + handles[i] = 0; + pitches[i] = 0; + } + + return drmdev_add_fb_multiplanar( + drmdev, + gbm_bo_get_width(bo), + gbm_bo_get_height(bo), + format, + handles, + pitches, + (uint32_t[4]){ + n_planes >= 1 ? gbm_bo_get_offset(bo, 0) : 0, + n_planes >= 2 ? gbm_bo_get_offset(bo, 1) : 0, + n_planes >= 3 ? gbm_bo_get_offset(bo, 2) : 0, + n_planes >= 4 ? gbm_bo_get_offset(bo, 3) : 0, + }, + has_modifiers, + (uint64_t[4]){ + n_planes >= 1 ? modifier : 0, + n_planes >= 2 ? modifier : 0, + n_planes >= 3 ? modifier : 0, + n_planes >= 4 ? modifier : 0, + } + ); +} + +int drmdev_rm_fb(struct drmdev *drmdev, uint32_t fb_id) { + int ok; + + pthread_mutex_lock(&drmdev->mutex); + + list_for_each_entry(struct drm_fb, fb, &drmdev->fbs, entry) { + if (fb->id == fb_id) { + list_del(&fb->entry); + free(fb); + break; + } + } + + pthread_mutex_unlock(&drmdev->mutex); + + ok = drmModeRmFB(drmdev->fd, fb_id); + if (ok < 0) { + ok = -ok; + LOG_ERROR("Could not remove DRM framebuffer. drmModeRmFB: %s\n", strerror(ok)); + return ok; + } + + return 0; +} + +int drmdev_move_cursor(struct drmdev *drmdev, uint32_t crtc_id, struct vec2i pos) { + int ok = drmModeMoveCursor(drmdev->fd, crtc_id, pos.x, pos.y); + if (ok < 0) { + LOG_ERROR("Couldn't move mouse cursor. drmModeMoveCursor: %s\n", strerror(-ok)); + return -ok; + } + + return 0; +} + +bool drmdev_can_commit(struct drmdev *drmdev) { + return is_drm_master(drmdev->fd); +} + +static int commit_atomic_common( + struct drmdev *drmdev, + drmModeAtomicReq *req, + bool sync, + bool allow_modeset, + uint32_t crtc_id, + drmdev_scanout_cb_t on_scanout, + void *scanout_cb_userdata, + void_callback_t on_release, + void *release_cb_userdata +) { + int bucket_status, ok; + + // If we don't get an event, we need to call the page flip callbacks manually. + uint64_t flags = 0; + if (allow_modeset) { + flags |= DRM_MODE_ATOMIC_ALLOW_MODESET; + } + if (!sync) { + flags |= DRM_MODE_PAGE_FLIP_EVENT; + flags |= DRM_MODE_ATOMIC_NONBLOCK; + } + + bool pageflip_event = !sync; + + if (on_scanout != NULL || on_release != NULL) { + ok = pthread_mutex_lock(&drmdev->mutex); + ASSERT_ZERO(ok); + + khint_t cbs_it = kh_put(pageflip_callbacks, drmdev->pageflip_callbacks, crtc_id, &bucket_status); + if (bucket_status == -1) { + ok = pthread_mutex_unlock(&drmdev->mutex); + ASSERT_ZERO(ok); + return ENOMEM; + } + + ok = drmModeAtomicCommit(drmdev->fd, req, flags, pageflip_event ? drmdev_ref(drmdev) : NULL); + if (ok != 0) { + ok = -errno; + + ASSERTED int mutex_ok = pthread_mutex_unlock(&drmdev->mutex); + ASSERT_ZERO(mutex_ok); + + LOG_ERROR("Could not commit atomic request. drmModeAtomicCommit: %s\n", strerror(-ok)); + return ok; + } + + struct pageflip_callbacks *cbs = &kh_value(drmdev->pageflip_callbacks, cbs_it); + + // If the entry didn't exist, we clear the memory. + if (bucket_status != 0) { + memset(cbs, 0, sizeof *cbs); + } + + cbs->callbacks[cbs->index].scanout_callback = on_scanout; + cbs->callbacks[cbs->index].scanout_callback_userdata = scanout_cb_userdata; + cbs->callbacks[cbs->index ^ 1].void_callback = on_release; + cbs->callbacks[cbs->index ^ 1].void_callback_userdata = release_cb_userdata; + + ok = pthread_mutex_unlock(&drmdev->mutex); + ASSERT_ZERO(ok); + } else { + ok = drmModeAtomicCommit(drmdev->fd, req, flags, pageflip_event ? drmdev_ref(drmdev) : NULL); + if (ok != 0) { + ok = -errno; + LOG_ERROR("Could not commit atomic request. drmModeAtomicCommit: %s\n", strerror(-ok)); + return ok; + } + } + + /// TODO: Use a more accurate timestamp, e.g. call drmCrtcGetSequence, + /// or queue a pageflip event even for synchronous (blocking) commits + /// and handle here. + if (!pageflip_event) { + drmdev_on_page_flip(drmdev, crtc_id, get_monotonic_time()); + } + + return 0; +} + +void set_vblank_timestamp(uint64_t vblank_ns, void *userdata) { + uint64_t *vblank_ns_out = userdata; + *vblank_ns_out = vblank_ns; +} + +int drmdev_commit_atomic_sync( + struct drmdev *drmdev, + drmModeAtomicReq *req, + bool allow_modeset, + uint32_t crtc_id, + void_callback_t on_release, + void *userdata, + uint64_t *vblank_ns_out +) { + return commit_atomic_common(drmdev, req, true, allow_modeset, crtc_id, vblank_ns_out ? set_vblank_timestamp : NULL, vblank_ns_out, on_release, userdata); +} + +int drmdev_commit_atomic_async( + struct drmdev *drmdev, + drmModeAtomicReq *req, + bool allow_modeset, + uint32_t crtc_id, + drmdev_scanout_cb_t on_scanout, + void_callback_t on_release, + void *userdata +) { + return commit_atomic_common(drmdev, req, false, allow_modeset, crtc_id, on_scanout, userdata, on_release, userdata); +} diff --git a/src/kms/drmdev.h b/src/kms/drmdev.h new file mode 100644 index 00000000..29453034 --- /dev/null +++ b/src/kms/drmdev.h @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT +/* + * KMS Modesetting + * + * - implements the interface to linux kernel modesetting + * - allows querying connected screens, crtcs, planes, etc + * - allows setting video modes, showing things on screen + * + * Copyright (c) 2022, Hannes Winkler + */ + +#ifndef _FLUTTERPI_SRC_MODESETTING_H +#define _FLUTTERPI_SRC_MODESETTING_H + +#include + +#include + +#include +#include + +#include "pixel_format.h" +#include "util/collection.h" +#include "util/geometry.h" +#include "util/refcounting.h" + +/** + * @brief Interface that will be used to open and close files. + */ +struct drmdev_file_interface { + int (*open)(const char *path, int flags, void **fd_metadata_out, void *userdata); + void (*close)(int fd, void *fd_metadata, void *userdata); +}; + +struct drmdev; + +typedef void (*drmdev_scanout_cb_t)(uint64_t vblank_ns, void *userdata); + +struct drmdev; +struct udev; + +struct drmdev *drmdev_new_from_udev_primary(struct udev *udev, const char *seat, const struct drmdev_file_interface *interface, void *interface_userdata); + +DECLARE_REF_OPS(drmdev) + +/** + * @brief Get the drm_resources for this drmdev, taking a reference on it. + * + * @param drmdev The drmdev. + * @returns The drm_resources for this drmdev. + */ +struct drm_resources *drmdev_query_resources(struct drmdev *drmdev); + +/** + * @brief Get the file descriptor for the modesetting-capable /dev/dri/cardX device. + * + * @param drmdev The drmdev. + * @returns The file descriptor for the device. + */ +int drmdev_get_modesetting_fd(struct drmdev *drmdev); + +/** + * @brief Notify the drmdev that the modesetting fd has available data. + */ +void drmdev_dispatch_modesetting(struct drmdev *drmdev); + +bool drmdev_supports_dumb_buffers(struct drmdev *drmdev); +int drmdev_create_dumb_buffer( + struct drmdev *drmdev, + int width, + int height, + int bpp, + uint32_t *gem_handle_out, + uint32_t *pitch_out, + size_t *size_out +); +void drmdev_destroy_dumb_buffer(struct drmdev *drmdev, uint32_t gem_handle); +void *drmdev_map_dumb_buffer(struct drmdev *drmdev, uint32_t gem_handle, size_t size); +void drmdev_unmap_dumb_buffer(struct drmdev *drmdev, void *map, size_t size); + +struct gbm_device *drmdev_get_gbm_device(struct drmdev *drmdev); + +uint32_t drmdev_add_fb( + struct drmdev *drmdev, + uint32_t width, + uint32_t height, + enum pixfmt pixel_format, + uint32_t bo_handle, + uint32_t pitch, + uint32_t offset, + bool has_modifier, + uint64_t modifier +); + +uint32_t drmdev_add_fb_multiplanar( + struct drmdev *drmdev, + uint32_t width, + uint32_t height, + enum pixfmt pixel_format, + const uint32_t bo_handles[4], + const uint32_t pitches[4], + const uint32_t offsets[4], + bool has_modifiers, + const uint64_t modifiers[4] +); + +uint32_t drmdev_add_fb_from_dmabuf( + struct drmdev *drmdev, + uint32_t width, + uint32_t height, + enum pixfmt pixel_format, + int prime_fd, + uint32_t pitch, + uint32_t offset, + bool has_modifier, + uint64_t modifier +); + +uint32_t drmdev_add_fb_from_dmabuf_multiplanar( + struct drmdev *drmdev, + uint32_t width, + uint32_t height, + enum pixfmt pixel_format, + const int prime_fds[4], + const uint32_t pitches[4], + const uint32_t offsets[4], + bool has_modifiers, + const uint64_t modifiers[4] +); + +uint32_t drmdev_add_fb_from_gbm_bo(struct drmdev *drmdev, struct gbm_bo *bo, bool cast_opaque); + +int drmdev_rm_fb(struct drmdev *drmdev, uint32_t fb_id); + +int drmdev_get_last_vblank(struct drmdev *drmdev, uint32_t crtc_id, uint64_t *last_vblank_ns_out); + +bool drmdev_can_commit(struct drmdev *drmdev); + +void drmdev_suspend(struct drmdev *drmdev); + +int drmdev_resume(struct drmdev *drmdev); + +int drmdev_move_cursor(struct drmdev *drmdev, uint32_t crtc_id, struct vec2i pos); + +int drmdev_commit_atomic_sync( + struct drmdev *drmdev, + drmModeAtomicReq *req, + bool allow_modeset, + uint32_t crtc_id, + void_callback_t on_release, + void *userdata, + uint64_t *vblank_ns_out +); + +int drmdev_commit_atomic_async( + struct drmdev *drmdev, + drmModeAtomicReq *req, + bool allow_modeset, + uint32_t crtc_id, + drmdev_scanout_cb_t on_scanout, + void_callback_t on_release, + void *userdata +); + +#endif // _FLUTTERPI_SRC_MODESETTING_H diff --git a/src/kms/req_builder.c b/src/kms/req_builder.c new file mode 100644 index 00000000..173d583b --- /dev/null +++ b/src/kms/req_builder.c @@ -0,0 +1,959 @@ +#include "req_builder.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "drmdev.h" +#include "resources.h" +#include "pixel_format.h" +#include "util/bitset.h" +#include "util/list.h" +#include "util/lock_ops.h" +#include "util/logging.h" +#include "util/macros.h" +#include "util/refcounting.h" + +#ifdef DEBUG_DRM_PLANE_ALLOCATIONS + #define LOG_DRM_PLANE_ALLOCATION_DEBUG LOG_DEBUG +#else + #define LOG_DRM_PLANE_ALLOCATION_DEBUG(...) +#endif + +struct kms_req_layer { + struct kms_fb_layer layer; + + uint32_t plane_id; + struct drm_plane *plane; + + bool set_zpos; + int64_t zpos; + + bool set_rotation; + drm_plane_transform_t rotation; + + void_callback_t release_callback; + kmsreq_syncfile_cb_t deferred_release_callback; + void *release_callback_userdata; +}; + +struct kms_req_builder { + refcount_t n_refs; + + struct drmdev *drmdev; + struct drm_resources *res; + struct drm_connector *connector; + struct drm_crtc *crtc; + uint32_t available_planes; + + bool use_atomic; + drmModeAtomicReq *req; + + int64_t next_zpos; + bool unset_mode; + bool has_mode; + drmModeModeInfo mode; + + int n_layers; + struct kms_req_layer layers[32]; + + kmsreq_scanout_cb_t scanout_cb; + void *scanout_cb_userdata; + + void_callback_t release_cb; + void *release_cb_userdata; +}; + +COMPILE_ASSERT(BITSET_SIZE(((struct kms_req_builder *) 0)->available_planes) == 32); + +static bool plane_qualifies( + // clang-format off + struct drm_plane *plane, + bool allow_primary, + bool allow_overlay, + bool allow_cursor, + enum pixfmt format, + bool has_modifier, uint64_t modifier, + bool has_zpos, int64_t zpos_lower_limit, int64_t zpos_upper_limit, + bool has_rotation, drm_plane_transform_t rotation, + bool has_id_range, uint32_t id_lower_limit + // clang-format on +) { + LOG_DRM_PLANE_ALLOCATION_DEBUG(" checking if plane with id %" PRIu32 " qualifies...\n", plane->id); + + if (plane->type == DRM_PRIMARY_PLANE) { + if (!allow_primary) { + LOG_DRM_PLANE_ALLOCATION_DEBUG(" does not qualify: plane type is primary but allow_primary is false\n"); + return false; + } + } else if (plane->type == DRM_OVERLAY_PLANE) { + if (!allow_overlay) { + LOG_DRM_PLANE_ALLOCATION_DEBUG(" does not qualify: plane type is overlay but allow_overlay is false\n"); + return false; + } + } else if (plane->type == DRM_CURSOR_PLANE) { + if (!allow_cursor) { + LOG_DRM_PLANE_ALLOCATION_DEBUG(" does not qualify: plane type is cursor but allow_cursor is false\n"); + return false; + } + } else { + ASSERT(false); + } + + if (has_modifier) { + if (drm_plane_supports_modified_formats(plane)) { + // return false if we want a modified format but the plane doesn't support modified formats + LOG_DRM_PLANE_ALLOCATION_DEBUG( + " does not qualify: framebuffer has modifier %" PRIu64 " but plane does not support modified formats\n", + modifier + ); + return false; + } + + if (!drm_plane_supports_modified_format(plane, format, modifier)) { + LOG_DRM_PLANE_ALLOCATION_DEBUG( + " does not qualify: plane does not support the modified format %s, %" PRIu64 ".\n", + get_pixfmt_info(format)->name, + modifier + ); + + // not found in the supported modified format list + return false; + } + } else { + // we don't want a modified format, return false if the format is not in the list + // of supported (unmodified) formats + if (!plane->supported_formats[format]) { + LOG_DRM_PLANE_ALLOCATION_DEBUG( + " does not qualify: plane does not support the (unmodified) format %s.\n", + get_pixfmt_info(format)->name + ); + return false; + } + } + + if (has_zpos) { + if (!plane->has_zpos) { + // return false if we want a zpos but the plane doesn't support one + LOG_DRM_PLANE_ALLOCATION_DEBUG(" does not qualify: zpos constraints specified but plane doesn't have a zpos property.\n"); + return false; + } else if (zpos_lower_limit > plane->max_zpos || zpos_upper_limit < plane->min_zpos) { + // return false if the zpos we want is outside the supported range of the plane + LOG_DRM_PLANE_ALLOCATION_DEBUG(" does not qualify: plane limits cannot satisfy the specified zpos constraints.\n"); + LOG_DRM_PLANE_ALLOCATION_DEBUG( + " plane zpos range: %" PRIi64 " <= zpos <= %" PRIi64 ", given zpos constraints: %" PRIi64 " <= zpos <= %" PRIi64 ".\n", + plane->min_zpos, + plane->max_zpos, + zpos_lower_limit, + zpos_upper_limit + ); + return false; + } + } + if (has_id_range && plane->id < id_lower_limit) { + LOG_DRM_PLANE_ALLOCATION_DEBUG(" does not qualify: plane id does not satisfy the given plane id constrains.\n"); + LOG_DRM_PLANE_ALLOCATION_DEBUG(" plane id: %" PRIu32 ", plane id lower limit: %" PRIu32 "\n", plane->id, id_lower_limit); + return false; + } + if (has_rotation) { + if (!plane->has_rotation) { + // return false if the plane doesn't support rotation + LOG_DRM_PLANE_ALLOCATION_DEBUG(" does not qualify: explicit rotation requested but plane has no rotation property.\n"); + return false; + } else if (plane->has_hardcoded_rotation && plane->hardcoded_rotation.u32 != rotation.u32) { + // return false if the plane has a hardcoded rotation and the rotation we want + // is not the hardcoded one + LOG_DRM_PLANE_ALLOCATION_DEBUG(" does not qualify: plane has hardcoded rotation that doesn't match the requested rotation.\n" + ); + return false; + } else if (rotation.u32 & ~plane->supported_rotations.u32) { + // return false if we can't construct the rotation using the rotation + // bits that are supported by the plane + LOG_DRM_PLANE_ALLOCATION_DEBUG(" does not qualify: requested rotation is not supported by the plane.\n"); + return false; + } + } + + LOG_DRM_PLANE_ALLOCATION_DEBUG(" does qualify.\n"); + return true; +} + +UNUSED static struct drm_plane *allocate_plane( + // clang-format off + struct kms_req_builder *builder, + bool allow_primary, + bool allow_overlay, + bool allow_cursor, + enum pixfmt format, + bool has_modifier, uint64_t modifier, + bool has_zpos, int64_t zpos_lower_limit, int64_t zpos_upper_limit, + bool has_rotation, drm_plane_transform_t rotation, + bool has_id_range, uint32_t id_lower_limit + // clang-format on +) { + for (unsigned int i = 0; i < builder->res->n_planes; i++) { + struct drm_plane *plane = builder->res->planes + i; + + if (builder->available_planes & (1 << i)) { + // find out if the plane matches our criteria + bool qualifies = plane_qualifies( + plane, + allow_primary, + allow_overlay, + allow_cursor, + format, + has_modifier, + modifier, + has_zpos, + zpos_lower_limit, + zpos_upper_limit, + has_rotation, + rotation, + has_id_range, + id_lower_limit + ); + + // if it doesn't, look for the next one + if (!qualifies) { + continue; + } + + // we found one, mark it as used and return it + builder->available_planes &= ~(1 << i); + return plane; + } + } + + // we didn't find an available plane matching our criteria + return NULL; +} + +UNUSED static void release_plane(struct kms_req_builder *builder, uint32_t plane_id) { + unsigned int index = drm_resources_get_plane_index(builder->res, plane_id); + if (index == UINT_MAX) { + LOG_ERROR("Could not find plane with id %" PRIu32 ".\n", plane_id); + return; + } + + assert(!(builder->available_planes & (1 << index))); + builder->available_planes |= (1 << index); +} + +struct kms_req_builder *kms_req_builder_new_atomic(struct drmdev *drmdev, struct drm_resources *resources, uint32_t crtc_id) { + struct kms_req_builder *builder; + + ASSERT_NOT_NULL(resources); + assert(crtc_id != 0 && crtc_id != 0xFFFFFFFF); + + builder = calloc(1, sizeof *builder); + if (builder == NULL) { + return NULL; + } + + builder->n_refs = REFCOUNT_INIT_1; + builder->res = drm_resources_ref(resources); + builder->use_atomic = true; + builder->drmdev = drmdev_ref(drmdev); + builder->connector = NULL; + builder->n_layers = 0; + builder->has_mode = false; + builder->unset_mode = false; + + builder->crtc = drm_resources_get_crtc(resources, crtc_id); + if (builder->crtc == NULL) { + LOG_ERROR("Invalid CRTC: %" PRId32 "\n", crtc_id); + goto fail_unref_drmdev; + } + + builder->req = drmModeAtomicAlloc(); + if (builder->req == NULL) { + goto fail_unref_drmdev; + } + + // set the CRTC to active + drmModeAtomicAddProperty(builder->req, crtc_id, builder->crtc->ids.active, 1); + + builder->next_zpos = drm_resources_get_min_zpos_for_crtc(resources, crtc_id); + builder->available_planes = drm_resources_get_possible_planes_for_crtc(resources, crtc_id); + return builder; + +fail_unref_drmdev: + drmdev_unref(builder->drmdev); + drm_resources_unref(builder->res); + free(builder); + return NULL; +} + +struct kms_req_builder *kms_req_builder_new_legacy(struct drmdev *drmdev, struct drm_resources *resources, uint32_t crtc_id) { + struct kms_req_builder *builder; + + ASSERT_NOT_NULL(resources); + assert(crtc_id != 0 && crtc_id != 0xFFFFFFFF); + + builder = calloc(1, sizeof *builder); + if (builder == NULL) { + return NULL; + } + + builder->n_refs = REFCOUNT_INIT_1; + builder->res = drm_resources_ref(resources); + builder->use_atomic = false; + builder->drmdev = drmdev_ref(drmdev); + builder->connector = NULL; + builder->n_layers = 0; + builder->has_mode = false; + builder->unset_mode = false; + + builder->crtc = drm_resources_get_crtc(resources, crtc_id); + if (builder->crtc == NULL) { + LOG_ERROR("Invalid CRTC: %" PRId32 "\n", crtc_id); + goto fail_unref_drmdev; + } + + builder->req = NULL; + builder->next_zpos = drm_resources_get_min_zpos_for_crtc(resources, crtc_id); + builder->available_planes = drm_resources_get_possible_planes_for_crtc(resources, crtc_id); + return builder; + +fail_unref_drmdev: + drmdev_unref(builder->drmdev); + drm_resources_unref(builder->res); + free(builder); + return NULL; +} + +static void kms_req_builder_destroy(struct kms_req_builder *builder) { + /// TODO: Is this complete? + for (int i = 0; i < builder->n_layers; i++) { + if (builder->layers[i].release_callback != NULL) { + builder->layers[i].release_callback(builder->layers[i].release_callback_userdata); + } + } + if (builder->req != NULL) { + drmModeAtomicFree(builder->req); + } + drm_resources_unref(builder->res); + drmdev_unref(builder->drmdev); + free(builder); +} + +DEFINE_REF_OPS(kms_req_builder, n_refs) + +struct drmdev *kms_req_builder_get_drmdev(struct kms_req_builder *builder) { + ASSERT_NOT_NULL(builder); + return drmdev_ref(builder->drmdev); +} + +struct drmdev *kms_req_builder_peek_drmdev(struct kms_req_builder *builder) { + ASSERT_NOT_NULL(builder); + return builder->drmdev; +} + +struct drm_resources *kms_req_builder_get_resources(struct kms_req_builder *builder) { + ASSERT_NOT_NULL(builder); + return drm_resources_ref(builder->res); +} + +struct drm_resources *kms_req_builder_peek_resources(struct kms_req_builder *builder) { + ASSERT_NOT_NULL(builder); + return builder->res; +} + +struct drm_crtc *kms_req_builder_get_crtc(struct kms_req_builder *builder) { + return builder->crtc; +} + +bool kms_req_builder_prefer_next_layer_opaque(struct kms_req_builder *builder) { + ASSERT_NOT_NULL(builder); + return builder->n_layers == 0; +} + +int kms_req_builder_set_mode(struct kms_req_builder *builder, const drmModeModeInfo *mode) { + ASSERT_NOT_NULL(builder); + ASSERT_NOT_NULL(mode); + builder->has_mode = true; + builder->mode = *mode; + return 0; +} + +int kms_req_builder_unset_mode(struct kms_req_builder *builder) { + ASSERT_NOT_NULL(builder); + assert(!builder->has_mode); + builder->unset_mode = true; + return 0; +} + +int kms_req_builder_set_connector(struct kms_req_builder *builder, uint32_t connector_id) { + struct drm_connector *conn; + + ASSERT_NOT_NULL(builder); + assert(DRM_ID_IS_VALID(connector_id)); + + conn = drm_resources_get_connector(builder->res, connector_id); + if (conn == NULL) { + LOG_ERROR("Could not find connector with id %" PRIu32 "\n", connector_id); + return EINVAL; + } + + builder->connector = conn; + return 0; +} + +int kms_req_builder_push_fb_layer( + struct kms_req_builder *builder, + const struct kms_fb_layer *layer, + void_callback_t release_callback, + kmsreq_syncfile_cb_t deferred_release_callback, + void *userdata +) { + struct drm_plane *plane; + int64_t zpos; + bool has_zpos; + bool close_in_fence_fd_after; + int ok, index; + + ASSERT_NOT_NULL(builder); + ASSERT_NOT_NULL(layer); + ASSERT_NOT_NULL(release_callback); + ASSERT_EQUALS_MSG(deferred_release_callback, NULL, "deferred release callbacks are not supported right now."); + + if (!builder->use_atomic && builder->n_layers > 1) { + // Multi-plane commits are un-vsynced without atomic modesetting. + // And when atomic modesetting is supported but we're still using legacy, + // every individual plane commit is vsynced. + LOG_DEBUG("Can't do multi-plane commits when using legacy modesetting.\n"); + return EINVAL; + } + + close_in_fence_fd_after = false; + if (!builder->use_atomic && layer->has_in_fence_fd) { + LOG_DEBUG("Explicit fencing is not supported for legacy modesetting. Implicit fencing will be used instead.\n"); + close_in_fence_fd_after = true; + } + + // Index of our layer. + index = builder->n_layers; + + // If we should prefer a cursor plane, try to find one first. + plane = NULL; + if (layer->prefer_cursor) { + plane = allocate_plane( + // clang-format off + builder, + /* allow_primary */ false, + /* allow_overlay */ false, + /* allow_cursor */ true, + /* format */ layer->format, + /* modifier */ layer->has_modifier, layer->modifier, + /* zpos */ false, 0, 0, + /* rotation */ layer->has_rotation, layer->rotation, + /* id_range */ false, 0 + // clang-format on + ); + if (plane == NULL) { + LOG_DEBUG("Couldn't find a fitting cursor plane.\n"); + } + } + + /// TODO: Not sure we can use crtc_x, crtc_y, etc with primary planes + if (plane == NULL && index == 0) { + // if this is the first layer, try using a + // primary plane for it. + + /// TODO: Use cursor_plane->max_zpos - 1 as the upper zpos limit, instead of INT64_MAX + plane = allocate_plane( + // clang-format off + builder, + /* allow_primary */ true, + /* allow_overlay */ false, + /* allow_cursor */ false, + /* format */ layer->format, + /* modifier */ layer->has_modifier, layer->modifier, + /* zpos */ false, 0, 0, + /* rotation */ layer->has_rotation, layer->rotation, + /* id_range */ false, 0 + // clang-format on + ); + + if (plane == NULL && !get_pixfmt_info(layer->format)->is_opaque) { + // maybe we can find a plane if we use the opaque version of this pixel format? + plane = allocate_plane( + // clang-format off + builder, + /* allow_primary */ true, + /* allow_overlay */ false, + /* allow_cursor */ false, + /* format */ pixfmt_opaque(layer->format), + /* modifier */ layer->has_modifier, layer->modifier, + /* zpos */ false, 0, 0, + /* rotation */ layer->has_rotation, layer->rotation, + /* id_range */ false, 0 + // clang-format on + ); + } + } else if (plane == NULL) { + // First try to find an overlay plane with a higher zpos. + plane = allocate_plane( + // clang-format off + builder, + /* allow_primary */ false, + /* allow_overlay */ true, + /* allow_cursor */ false, + /* format */ layer->format, + /* modifier */ layer->has_modifier, layer->modifier, + /* zpos */ true, builder->next_zpos, INT64_MAX, + /* rotation */ layer->has_rotation, layer->rotation, + /* id_range */ false, 0 + // clang-format on + ); + + // If we can't find one, find an overlay plane with the next highest plane_id. + // (According to some comments in the kernel, that's the fallback KMS uses for the + // occlusion order if no zpos property is supported, i.e. planes with plane id occlude + // planes with lower id) + if (plane == NULL) { + plane = allocate_plane( + // clang-format off + builder, + /* allow_primary */ false, + /* allow_overlay */ true, + /* allow_cursor */ false, + /* format */ layer->format, + /* modifier */ layer->has_modifier, layer->modifier, + /* zpos */ false, 0, 0, + /* rotation */ layer->has_rotation, layer->rotation, + /* id_range */ true, builder->layers[index - 1].plane_id + 1 + // clang-format on + ); + } + } + + if (plane == NULL) { + LOG_ERROR("Could not find a suitable unused DRM plane for pushing the framebuffer.\n"); + return EIO; + } + + // Now that we have a plane, use the minimum zpos + // that's both higher than the last layers zpos and + // also supported by the plane. + // This will also work for planes with hardcoded zpos. + has_zpos = plane->has_zpos; + if (has_zpos) { + zpos = builder->next_zpos; + if (plane->min_zpos > zpos) { + zpos = plane->min_zpos; + } + } else { + // just to silence an uninitialized use warning below. + zpos = 0; + } + + if (!builder->use_atomic) { + } else { + uint32_t plane_id = plane->id; + + /// TODO: Error checking + /// TODO: Maybe add these in the kms_req_builder_commit instead? + drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.crtc_id, builder->crtc->id); + drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.fb_id, layer->drm_fb_id); + drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.crtc_x, layer->dst_x); + drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.crtc_y, layer->dst_y); + drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.crtc_w, layer->dst_w); + drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.crtc_h, layer->dst_h); + drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.src_x, layer->src_x); + drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.src_y, layer->src_y); + drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.src_w, layer->src_w); + drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.src_h, layer->src_h); + + if (plane->has_zpos && !plane->has_hardcoded_zpos) { + drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.zpos, zpos); + } + + if (layer->has_rotation && plane->has_rotation && !plane->has_hardcoded_rotation) { + drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.rotation, layer->rotation.u64); + } + + if (index == 0) { + if (plane->has_alpha) { + drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.alpha, plane->max_alpha); + } + + if (plane->has_blend_mode && plane->supported_blend_modes[DRM_BLEND_MODE_NONE]) { + drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.pixel_blend_mode, DRM_BLEND_MODE_NONE); + } + } + } + + // This should be done when we're sure we're not failing. + // Because on failure it would be the callers job to close the fd. + if (close_in_fence_fd_after) { + ok = close(layer->in_fence_fd); + if (ok < 0) { + ok = errno; + LOG_ERROR("Could not close layer in_fence_fd. close: %s\n", strerror(ok)); + goto fail_release_plane; + } + } + + /// TODO: Right now we're adding zpos, rotation to the atomic request unconditionally + /// when specified in the fb layer. Ideally we would check for updates + /// on commit and only add to the atomic request when zpos / rotation changed. + builder->n_layers++; + if (has_zpos) { + builder->next_zpos = zpos + 1; + } + builder->layers[index].layer = *layer; + builder->layers[index].plane_id = plane->id; + builder->layers[index].plane = plane; + builder->layers[index].set_zpos = has_zpos; + builder->layers[index].zpos = zpos; + builder->layers[index].set_rotation = layer->has_rotation; + builder->layers[index].rotation = layer->rotation; + builder->layers[index].release_callback = release_callback; + builder->layers[index].deferred_release_callback = deferred_release_callback; + builder->layers[index].release_callback_userdata = userdata; + return 0; + +fail_release_plane: + release_plane(builder, plane->id); + return ok; +} + +int kms_req_builder_push_zpos_placeholder_layer(struct kms_req_builder *builder, int64_t *zpos_out) { + ASSERT_NOT_NULL(builder); + ASSERT_NOT_NULL(zpos_out); + *zpos_out = builder->next_zpos++; + return 0; +} + +struct kms_req *kms_req_builder_build(struct kms_req_builder *builder) { + return (struct kms_req *) kms_req_builder_ref(builder); +} + +UNUSED struct kms_req *kms_req_ref(struct kms_req *req) { + return (struct kms_req *) kms_req_builder_ref((struct kms_req_builder *) req); +} + +UNUSED void kms_req_unref(struct kms_req *req) { + kms_req_builder_unref((struct kms_req_builder *) req); +} + +UNUSED void kms_req_unrefp(struct kms_req **req) { + kms_req_builder_unrefp((struct kms_req_builder **) req); +} + +UNUSED void kms_req_swap_ptrs(struct kms_req **oldp, struct kms_req *new) { + kms_req_builder_swap_ptrs((struct kms_req_builder **) oldp, (struct kms_req_builder *) new); +} + +UNUSED static bool drm_plane_is_active(struct drm_plane *plane) { + return plane->committed_state.fb_id != 0 && plane->committed_state.crtc_id != 0; +} + +static void on_kms_req_scanout(uint64_t vblank_ns, void *userdata) { + struct kms_req_builder *b; + + ASSERT_NOT_NULL(userdata); + b = (struct kms_req_builder *) userdata; + + if (b->scanout_cb != NULL) { + b->scanout_cb(vblank_ns, b->scanout_cb_userdata); + } +} + +static void on_kms_req_release(void *userdata) { + struct kms_req_builder *b; + + ASSERT_NOT_NULL(userdata); + b = (struct kms_req_builder *) userdata; + + if (b->release_cb != NULL) { + b->release_cb(b->release_cb_userdata); + } + + kms_req_builder_unref(b); +} + + +static int kms_req_commit_common( + struct kms_req *req, + struct drmdev *drmdev, + bool blocking, + kmsreq_scanout_cb_t scanout_cb, + void *scanout_cb_userdata, + uint64_t *vblank_ns_out, + void_callback_t release_cb, + void *release_cb_userdata +) { + struct kms_req_builder *builder; + struct drm_blob *mode_blob; + bool update_mode; + int ok; + + update_mode = false; + mode_blob = NULL; + + ASSERT_NOT_NULL(req); + builder = (struct kms_req_builder *) req; + + if (!drmdev_can_commit(drmdev)) { + LOG_ERROR("Commit requested, but drmdev is paused right now.\n"); + return EBUSY; + } + + // only change the mode if the new mode differs from the old one + + /// TOOD: If this is not a standard mode reported by connector/CRTC, + /// is there a way to verify if it is valid? (maybe use DRM_MODE_ATOMIC_TEST) + + // this could be a single expression but this way you see a bit better what's going on. + // We need to upload the new mode blob if: + // - we have a new mode + // - and: we don't have an old mode + // - or: the old mode differs from the new mode + bool upload_mode = false; + if (builder->has_mode) { + if (!builder->crtc->committed_state.has_mode) { + upload_mode = true; + } else if (memcmp(&builder->crtc->committed_state.mode, &builder->mode, sizeof(drmModeModeInfo)) != 0) { + upload_mode = true; + } + } + + if (upload_mode) { + update_mode = true; + mode_blob = drm_blob_new_mode(drmdev_get_modesetting_fd(drmdev), &builder->mode, true); + if (mode_blob == NULL) { + return EIO; + } + } else if (builder->unset_mode) { + update_mode = true; + mode_blob = NULL; + } + + if (builder->use_atomic) { + /// TODO: If we can do explicit fencing, don't use the page flip event. + /// TODO: Can we set OUT_FENCE_PTR even though we didn't set any IN_FENCE_FDs? + + // For every plane that was previously active with our CRTC, but is not used by us anymore, + // disable it. + for (unsigned int i = 0; i < builder->res->n_planes; i++) { + if (!(builder->available_planes & (1 << i))) { + continue; + } + + struct drm_plane *plane = builder->res->planes + i; + if (drm_plane_is_active(plane) && plane->committed_state.crtc_id == builder->crtc->id) { + drmModeAtomicAddProperty(builder->req, plane->id, plane->ids.crtc_id, 0); + drmModeAtomicAddProperty(builder->req, plane->id, plane->ids.fb_id, 0); + } + } + + if (builder->connector != NULL) { + // add the CRTC_ID property if that was explicitly set + drmModeAtomicAddProperty(builder->req, builder->connector->id, builder->connector->ids.crtc_id, builder->crtc->id); + } + + if (update_mode) { + if (mode_blob != NULL) { + drmModeAtomicAddProperty(builder->req, builder->crtc->id, builder->crtc->ids.mode_id, drm_blob_get_id(mode_blob)); + } else { + drmModeAtomicAddProperty(builder->req, builder->crtc->id, builder->crtc->ids.mode_id, 0); + } + } + + builder->scanout_cb = scanout_cb; + builder->scanout_cb_userdata = scanout_cb_userdata; + builder->release_cb = release_cb; + builder->release_cb_userdata = release_cb_userdata; + + if (blocking) { + ok = drmdev_commit_atomic_sync(drmdev, builder->req, update_mode, builder->crtc->id, on_kms_req_release, kms_req_ref(req), vblank_ns_out); + } else { + ok = drmdev_commit_atomic_async(drmdev, builder->req, update_mode, builder->crtc->id, on_kms_req_scanout, on_kms_req_release, kms_req_ref(req)); + } + + if (ok != 0) { + ok = errno; + goto fail_unref_builder; + } + } else { + ASSERT_EQUALS(builder->layers[0].layer.dst_x, 0); + ASSERT_EQUALS(builder->layers[0].layer.dst_y, 0); + ASSERT_EQUALS(builder->layers[0].layer.dst_w, builder->mode.hdisplay); + ASSERT_EQUALS(builder->layers[0].layer.dst_h, builder->mode.vdisplay); + + /// TODO: Do we really need to assert this? + ASSERT_NOT_NULL(builder->connector); + + bool needs_set_crtc = update_mode; + + // check if the plane pixel format changed. + // that needs a drmModeSetCrtc for legacy KMS as well. + // get the current, committed fb for the plane, check if we have info + // for it (we can't use drmModeGetFB2 since that's not present on debian buster) + // and if we're not absolutely sure the formats match, set needs_set_crtc + // too. + if (!needs_set_crtc) { + struct kms_req_layer *layer = builder->layers + 0; + struct drm_plane *plane = layer->plane; + ASSERT_NOT_NULL(plane); + + if (plane->committed_state.has_format && plane->committed_state.format == layer->layer.format) { + needs_set_crtc = false; + } else { + needs_set_crtc = true; + } + } + + /// TODO: Handle {src,dst}_{x,y,w,h} here + /// TODO: Handle setting other properties as well + + /// TODO: Implement + UNIMPLEMENTED(); + + // if (needs_set_crtc) { + // /// TODO: Fetch new connector or current connector here since we seem to need it for drmModeSetCrtc + // ok = drmModeSetCrtc( + // , + // builder->crtc->id, + // builder->layers[0].layer.drm_fb_id, + // 0, + // 0, + // (uint32_t[1]){ builder->connector->id }, + // 1, + // builder->unset_mode ? NULL : &builder->mode + // ); + // if (ok != 0) { + // ok = errno; + // LOG_ERROR("Could not commit display update. drmModeSetCrtc: %s\n", strerror(ok)); + // goto fail_maybe_destroy_mode_blob; + // } + + // internally_blocking = true; + // } else { + // ok = drmModePageFlip( + // builder->drmdev->master_fd, + // builder->crtc->id, + // builder->layers[0].layer.drm_fb_id, + // DRM_MODE_PAGE_FLIP_EVENT, + // kms_req_builder_ref(builder) + // ); + // if (ok != 0) { + // ok = errno; + // LOG_ERROR("Could not commit display update. drmModePageFlip: %s\n", strerror(ok)); + // goto fail_unref_builder; + // } + // } + + // This should also be ensured by kms_req_builder_push_fb_layer + ASSERT_MSG( + builder->use_atomic || builder->n_layers <= 1, + "There can be at most one framebuffer layer when using legacy modesetting." + ); + + /// TODO: Call drmModeSetPlane for all other layers + /// TODO: Assert here + } + + // update struct drm_plane.committed_state for all planes + for (int i = 0; i < builder->n_layers; i++) { + struct drm_plane *plane = builder->layers[i].plane; + struct kms_req_layer *layer = builder->layers + i; + + plane->committed_state.crtc_id = builder->crtc->id; + plane->committed_state.fb_id = layer->layer.drm_fb_id; + plane->committed_state.src_x = layer->layer.src_x; + plane->committed_state.src_y = layer->layer.src_y; + plane->committed_state.src_w = layer->layer.src_w; + plane->committed_state.src_h = layer->layer.src_h; + plane->committed_state.crtc_x = layer->layer.dst_x; + plane->committed_state.crtc_y = layer->layer.dst_y; + plane->committed_state.crtc_w = layer->layer.dst_w; + plane->committed_state.crtc_h = layer->layer.dst_h; + + if (builder->layers[i].set_zpos) { + plane->committed_state.zpos = layer->zpos; + } + if (builder->layers[i].set_rotation) { + plane->committed_state.rotation = layer->rotation; + } + + plane->committed_state.has_format = true; + plane->committed_state.format = layer->layer.format; + + // builder->layers[i].plane->committed_state.alpha = layer->alpha; + // builder->layers[i].plane->committed_state.blend_mode = builder->layers[i].layer.blend_mode; + } + + // update struct drm_crtc.committed_state + if (update_mode) { + // destroy the old mode blob + if (builder->crtc->committed_state.mode_blob != NULL) { + drm_blob_destroy(builder->crtc->committed_state.mode_blob); + } + + // store the new mode + if (mode_blob != NULL) { + builder->crtc->committed_state.has_mode = true; + builder->crtc->committed_state.mode = builder->mode; + builder->crtc->committed_state.mode_blob = mode_blob; + } else { + builder->crtc->committed_state.has_mode = false; + builder->crtc->committed_state.mode_blob = NULL; + } + } + + // update struct drm_connector.committed_state + builder->connector->committed_state.crtc_id = builder->crtc->id; + // builder->connector->committed_state.encoder_id = 0; + + return 0; + +fail_unref_builder: + kms_req_builder_unref(builder); + +// fail_maybe_destroy_mode_blob: +// if (mode_blob != NULL) +// drm_blob_destroy(mode_blob); + + return ok; +} + +void set_vblank_ns(uint64_t vblank_ns, void *userdata) { + uint64_t *vblank_ns_out; + + ASSERT_NOT_NULL(userdata); + vblank_ns_out = userdata; + + *vblank_ns_out = vblank_ns; +} + +int kms_req_commit_blocking(struct kms_req *req, struct drmdev *drmdev, uint64_t *vblank_ns_out) { + int ok; + + ok = kms_req_commit_common(req, drmdev, true, NULL, NULL, vblank_ns_out, NULL, NULL); + if (ok != 0) { + return ok; + } + + return 0; +} + +int kms_req_commit_nonblocking(struct kms_req *req, struct drmdev *drmdev, kmsreq_scanout_cb_t scanout_cb, void *userdata, void_callback_t release_cb) { + return kms_req_commit_common(req, drmdev, false, scanout_cb, userdata, NULL, release_cb, userdata); +} diff --git a/src/kms/req_builder.h b/src/kms/req_builder.h new file mode 100644 index 00000000..7f2b7120 --- /dev/null +++ b/src/kms/req_builder.h @@ -0,0 +1,206 @@ +#ifndef _FLUTTERPI_SRC_MODESETTING_REQ_BUILDER_H_ +#define _FLUTTERPI_SRC_MODESETTING_REQ_BUILDER_H_ + +#include +#include + +#include "resources.h" + +struct kms_fb_layer { + uint32_t drm_fb_id; + enum pixfmt format; + bool has_modifier; + uint64_t modifier; + + int32_t src_x, src_y, src_w, src_h; + int32_t dst_x, dst_y, dst_w, dst_h; + + bool has_rotation; + drm_plane_transform_t rotation; + + bool has_in_fence_fd; + int in_fence_fd; + + bool prefer_cursor; +}; + +typedef void (*kmsreq_scanout_cb_t)(uint64_t vblank_ns, void *userdata); + +typedef void (*kmsreq_syncfile_cb_t)(void *userdata, int syncfile_fd); + + +struct drmdev; +struct kms_req_builder; + +struct kms_req_builder *kms_req_builder_new_atomic(struct drmdev *drmdev, struct drm_resources *resources, uint32_t crtc_id); + +struct kms_req_builder *kms_req_builder_new_legacy(struct drmdev *drmdev, struct drm_resources *resources, uint32_t crtc_id); + +DECLARE_REF_OPS(kms_req_builder) + +struct drmdev *kms_req_builder_get_drmdev(struct kms_req_builder *builder); + +struct drmdev *kms_req_builder_peek_drmdev(struct kms_req_builder *builder); + +struct drm_resources *kms_req_builder_get_resources(struct kms_req_builder *builder); + +struct drm_resources *kms_req_builder_peek_resources(struct kms_req_builder *builder); + +/** + * @brief Gets the CRTC associated with this KMS request builder. + * + * @param builder The KMS request builder. + * @returns The CRTC associated with this KMS request builder. + */ +struct drm_crtc *kms_req_builder_get_crtc(struct kms_req_builder *builder); + +/** + * @brief Adds a property to the KMS request that will set the given video mode + * on this CRTC on commit, regardless of whether the currently committed output + * mode is the same. + * + * @param builder The KMS request builder. + * @param mode The output mode to set (on @ref kms_req_commit) + * @returns Zero if successful, positive errno-style error on failure. + */ +int kms_req_builder_set_mode(struct kms_req_builder *builder, const drmModeModeInfo *mode); + +/** + * @brief Adds a property to the KMS request that will unset the configured + * output mode for this CRTC on commit, regardless of whether the currently + * committed output mdoe is already unset. + * + * @param builder The KMS request builder. + * @returns Zero if successful, positive errno-style error on failure. + */ +int kms_req_builder_unset_mode(struct kms_req_builder *builder); + +/** + * @brief Adds a property to the KMS request that will change the connector + * that this CRTC is displaying content on to @param connector_id. + * + * @param builder The KMS request builder. + * @param connector_id The connector that this CRTC should display contents on. + * @returns Zero if successful, EINVAL if the @param connector_id is invalid. + */ +int kms_req_builder_set_connector(struct kms_req_builder *builder, uint32_t connector_id); + +/** + * @brief True if the next layer pushed using @ref kms_req_builder_push_fb_layer + * should be opaque, i.e. use a framebuffer which has a pixel format that has no + * alpha channel. + * + * This is true for the bottom-most layer. There are some display controllers + * that don't support non-opaque pixel formats for the bottom-most (primary) + * plane. So ignoring this might lead to an EINVAL on commit. + * + * @param builder The KMS request builder. + * @returns True if the next layer should preferably be opaque, false if there's + * no preference. + */ +bool kms_req_builder_prefer_next_layer_opaque(struct kms_req_builder *builder); + +/** + * @brief Adds a new framebuffer (display) layer on top of the last layer. + * + * If this is the first layer, the framebuffer should cover the entire screen + * (CRTC). + * + * To allow the use of explicit fencing, specify an in_fence_fd in @param layer + * and a @param deferred_release_callback. + * + * If explicit fencing is supported: + * - the in_fence_fd should be a DRM syncobj fd that signals + * when the GPU has finished rendering to the framebuffer and is ready + * to be scanned out. + * - @param deferred_release_callback will be called + * with a DRM syncobj fd that is signaled once the framebuffer is no longer + * being displayed on screen (and can be rendered into again) + * + * If explicit fencing is not supported: + * - the in_fence_fd in @param layer will be closed by this procedure. + * - @param deferred_release_callback will NOT be called and + * @param release_callback will be called instead. + * + * Explicit fencing is supported: When atomic modesetting is being used and + * the driver supports it. (Driver has IN_FENCE_FD plane and OUT_FENCE_PTR crtc + * properties) + * + * @param builder The KMS request builder. + * @param layer The exact details (src pos, output pos, rotation, + * framebuffer) of the layer that should be shown on + * screen. + * @param release_callback Called when the framebuffer of this layer is no + * longer being shown on screen. This is called with the + * drmdev locked, so make sure to use _locked variants + * of any drmdev calls. + * @param deferred_release_callback (Unimplemented right now) If this is present, + * this callback might be called instead of + * @param release_callback. + * This is called with a DRM syncobj fd that is + * signaled when the framebuffer is no longer + * shown on screen. + * Legacy DRM modesetting does not support + * explicit fencing, in which case + * @param release_callback will be called + * instead. + * @param userdata Userdata pointer that's passed to the release_callback or + * deferred_release_callback as-is. + * @returns Zero on success, otherwise: + * - EINVAL: if attempting to push a second framebuffer layer, if + * driver supports atomic modesetting but legacy modesetting is + * being used. + * - EIO: if no DRM plane could be found that supports displaying + * this framebuffer layer. Either the pixel format is not + * supported, the modifier, the rotation or the drm device + * doesn't have enough planes. + * - The error returned by @ref close if closing the in_fence_fd + * fails. + */ +int kms_req_builder_push_fb_layer( + struct kms_req_builder *builder, + const struct kms_fb_layer *layer, + void_callback_t release_callback, + kmsreq_syncfile_cb_t deferred_release_callback, + void *userdata +); + +/** + * @brief Push a "fake" layer that just keeps one zpos free, incase something + * other than KMS wants to display contents there. (e.g. omxplayer) + * + * @param builder The KMS request builder. + * @param zpos_out Filled with the zpos that won't be occupied by the request + * builder. + * @returns Zero. + */ +int kms_req_builder_push_zpos_placeholder_layer(struct kms_req_builder *builder, int64_t *zpos_out); + +/** + * @brief A KMS request (atomic or legacy modesetting) that can be committed to + * change the state of a single CRTC. + * + * Only way to construct this is by building a KMS request using + * @ref kms_req_builder and then calling @ref kms_req_builder_build. + */ +struct kms_req; + +DECLARE_REF_OPS(kms_req) + +/** + * @brief Build the KMS request builder into an actual, immutable KMS request + * that can be committed. Internally this doesn't do much at all. + * + * @param builder The KMS request builder that should be built. + * @returns KMS request that can be committed using @ref kms_req_commit_blocking + * or @ref kms_req_commit_nonblocking. + * The returned KMS request has refcount 1. Unref using + * @ref kms_req_unref after usage. + */ +struct kms_req *kms_req_builder_build(struct kms_req_builder *builder); + +int kms_req_commit_blocking(struct kms_req *req, struct drmdev *drmdev, uint64_t *vblank_ns_out); + +int kms_req_commit_nonblocking(struct kms_req *req, struct drmdev *drmdev, kmsreq_scanout_cb_t scanout_cb, void *userdata, void_callback_t destroy_cb); + +#endif // _FLUTTERPI_SRC_MODESETTING_REQ_BUILDER_H_ diff --git a/src/kms/resources.c b/src/kms/resources.c new file mode 100644 index 00000000..fdd908c6 --- /dev/null +++ b/src/kms/resources.c @@ -0,0 +1,1495 @@ +#include "resources.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "pixel_format.h" +#include "util/bitset.h" +#include "util/list.h" +#include "util/lock_ops.h" +#include "util/logging.h" +#include "util/macros.h" +#include "util/refcounting.h" + +struct _drmModeFB2; + +struct drm_mode_fb2 { + uint32_t fb_id; + uint32_t width, height; + uint32_t pixel_format; /* fourcc code from drm_fourcc.h */ + uint64_t modifier; /* applies to all buffers */ + uint32_t flags; + + /* per-plane GEM handle; may be duplicate entries for multiple planes */ + uint32_t handles[4]; + uint32_t pitches[4]; /* bytes */ + uint32_t offsets[4]; /* bytes */ +}; + +#ifdef HAVE_FUNC_ATTRIBUTE_WEAK + extern struct _drmModeFB2 *drmModeGetFB2(int fd, uint32_t bufferId) __attribute__((weak)); + extern void drmModeFreeFB2(struct _drmModeFB2 *ptr) __attribute__((weak)); + #define HAVE_WEAK_DRM_MODE_GET_FB2 +#endif + +#ifdef HAVE_WEAK_DRM_MODE_GET_FB2 +static bool drm_fb_get_format(int drm_fd, uint32_t fb_id, enum pixfmt *format_out) { + struct drm_mode_fb2 *fb; + + // drmModeGetFB2 might not be present. + // If __attribute__((weak)) is supported by the compiler, we redefine it as + // weak above. + // If we don't have weak, we can't check for it here. + if (drmModeGetFB2 && drmModeFreeFB2) { + fb = (struct drm_mode_fb2*) drmModeGetFB2(drm_fd, fb_id); + if (fb == NULL) { + return false; + } + + for (int i = 0; i < PIXFMT_COUNT; i++) { + if (get_pixfmt_info(i)->drm_format == fb->pixel_format) { + *format_out = i; + drmModeFreeFB2((struct _drmModeFB2 *) fb); + return true; + } + } + + drmModeFreeFB2((struct _drmModeFB2 *) fb); + return false; + } else { + return false; + } +} +#else +static bool drm_fb_get_format(int drm_fd, uint32_t fb_id, enum pixfmt *format_out) { + (void) drm_fd; + (void) fb_id; + (void) format_out; + return false; +} +#endif + +static size_t sizeof_drm_format_modifier_blob(struct drm_format_modifier_blob *blob) { + return MAX3( + sizeof(struct drm_format_modifier_blob), + blob->formats_offset + sizeof(uint32_t) * blob->count_formats, + blob->modifiers_offset + sizeof(struct drm_format_modifier) * blob->count_modifiers + ); +} + + +static int drm_connector_init(int drm_fd, uint32_t connector_id, struct drm_connector *out) { + memset(out, 0, sizeof(*out)); + + drm_connector_prop_ids_init(&out->ids); + + { + drmModeConnector *connector = drmModeGetConnector(drm_fd, connector_id); + if (connector == NULL) { + return ENOMEM; + } + + out->id = connector->connector_id; + out->type = connector->connector_type; + out->type_id = connector->connector_type_id; + out->n_encoders = connector->count_encoders; + + assert(connector->count_encoders <= 32); + memcpy(out->encoders, connector->encoders, connector->count_encoders * sizeof(uint32_t)); + + out->variable_state.connection_state = (enum drm_connection_state) connector->connection; + out->variable_state.subpixel_layout = (enum drm_subpixel_layout) connector->subpixel; + out->variable_state.width_mm = connector->mmWidth; + out->variable_state.height_mm = connector->mmHeight; + + assert((connector->modes == NULL) == (connector->count_modes == 0)); + if (connector->modes != NULL) { + out->variable_state.n_modes = connector->count_modes; + out->variable_state.modes = memdup(connector->modes, connector->count_modes * sizeof(*connector->modes)); + if (out->variable_state.modes == NULL) { + drmModeFreeConnector(connector); + return ENOMEM; + } + } + + out->committed_state.encoder_id = connector->encoder_id; + + drmModeFreeConnector(connector); + } + + { + drmModeObjectProperties *props = drmModeObjectGetProperties(drm_fd, connector_id, DRM_MODE_OBJECT_CONNECTOR); + if (props == NULL) { + return ENOMEM; + } + + out->committed_state.crtc_id = DRM_ID_NONE; + for (uint32_t i = 0; i < props->count_props; i++) { + uint32_t id = props->props[i]; + + drmModePropertyRes *prop_info = drmModeGetProperty(drm_fd, id); + if (prop_info == NULL) { + drmModeFreeObjectProperties(props); + return ENOMEM; + } + + #define CHECK_ASSIGN_PROPERTY_ID(_name_str, _name) \ + if (strncmp(prop_info->name, _name_str, DRM_PROP_NAME_LEN) == 0) { \ + out->ids._name = prop_info->prop_id; \ + } else + + DRM_CONNECTOR_PROPERTIES(CHECK_ASSIGN_PROPERTY_ID) { + // this is the trailing else case + LOG_DEBUG("Unknown DRM connector property: %s\n", prop_info->name); + } + + #undef CHECK_ASSIGN_PROPERTY_ID + + if (id == out->ids.crtc_id) { + out->committed_state.crtc_id = props->prop_values[i]; + } + + drmModeFreeProperty(prop_info); + } + + drmModeFreeObjectProperties(props); + } + + return 0; +} + +static void drm_connector_fini(struct drm_connector *connector) { + free(connector->variable_state.modes); +} + +static int drm_connector_copy(struct drm_connector *dst, const struct drm_connector *src) { + *dst = *src; + + if (src->variable_state.modes != NULL) { + dst->variable_state.modes = memdup(src->variable_state.modes, src->variable_state.n_modes * sizeof(*src->variable_state.modes)); + if (dst->variable_state.modes == NULL) { + return ENOMEM; + } + } + + return 0; +} + + +static int drm_encoder_init(int drm_fd, uint32_t encoder_id, struct drm_encoder *out) { + drmModeEncoder *encoder = drmModeGetEncoder(drm_fd, encoder_id); + if (encoder == NULL) { + int ok = errno; + if (ok == 0) ok = ENOMEM; + return ok; + } + + out->id = encoder->encoder_id; + out->type = encoder->encoder_type; + if (out->type > DRM_ENCODER_TYPE_MAX) { + out->type = DRM_ENCODER_TYPE_NONE; + } + + out->possible_crtcs = encoder->possible_crtcs; + out->possible_clones = encoder->possible_clones; + + out->variable_state.crtc_id = encoder->crtc_id; + + drmModeFreeEncoder(encoder); + return 0; +} + +static int drm_encoder_copy(struct drm_encoder *dst, const struct drm_encoder *src) { + *dst = *src; + return 0; +} + +static void drm_encoder_fini(struct drm_encoder *encoder) { + (void) encoder; +} + + +static int drm_crtc_init(int drm_fd, int crtc_index, uint32_t crtc_id, struct drm_crtc *out) { + memset(out, 0, sizeof(*out)); + + drm_crtc_prop_ids_init(&out->ids); + + { + drmModeCrtc *crtc = drmModeGetCrtc(drm_fd, crtc_id); + if (crtc == NULL) { + int ok = errno; + if (ok == 0) ok = ENOMEM; + return ok; + } + + out->id = crtc->crtc_id; + out->index = crtc_index; + out->bitmask = 1u << crtc_index; + out->committed_state.has_mode = crtc->mode_valid; + out->committed_state.mode = crtc->mode; + out->committed_state.mode_blob = NULL; + + drmModeFreeCrtc(crtc); + } + + { + drmModeObjectProperties *props = drmModeObjectGetProperties(drm_fd, crtc_id, DRM_MODE_OBJECT_CRTC); + if (props == NULL) { + int ok = errno; + if (ok == 0) ok = ENOMEM; + return ok; + } + + for (int i = 0; i < props->count_props; i++) { + drmModePropertyRes *prop_info = drmModeGetProperty(drm_fd, props->props[i]); + if (prop_info == NULL) { + drmModeFreeObjectProperties(props); + int ok = errno; + if (ok == 0) ok = ENOMEM; + return ok; + } + + #define CHECK_ASSIGN_PROPERTY_ID(_name_str, _name) \ + if (strncmp(prop_info->name, _name_str, ARRAY_SIZE(prop_info->name)) == 0) { \ + out->ids._name = prop_info->prop_id; \ + } else + + DRM_CRTC_PROPERTIES(CHECK_ASSIGN_PROPERTY_ID) { + // this is the trailing else case + LOG_DEBUG("Unknown DRM crtc property: %s\n", prop_info->name); + } + + #undef CHECK_ASSIGN_PROPERTY_ID + + + drmModeFreeProperty(prop_info); + } + + drmModeFreeObjectProperties(props); + } + + return 0; +} + +static int drm_crtc_copy(struct drm_crtc *dst, const struct drm_crtc *src) { + *dst = *src; + return 0; +} + +static void drm_crtc_fini(struct drm_crtc *crtc) { + (void) crtc; +} + +bool drm_resources_any_crtc_plane_supports_format(struct drm_resources *r, uint32_t crtc_id, enum pixfmt pixel_format) { + struct drm_crtc *crtc = drm_resources_get_crtc(r, crtc_id); + if (crtc == NULL) { + return false; + } + + drm_resources_for_each_plane(r, plane) { + if (!(plane->possible_crtcs & crtc->bitmask)) { + // Only query planes that are possible to connect to the CRTC we're using. + continue; + } + + if (plane->type != DRM_PRIMARY_PLANE && plane->type != DRM_OVERLAY_PLANE) { + // We explicitly only look for primary and overlay planes. + continue; + } + + if (drm_plane_supports_unmodified_format(plane, pixel_format)) { + return true; + } + } + + return false; +} + + +static void drm_plane_init_rotation(drmModePropertyRes *info, uint64_t value, struct drm_plane *out) { + assert(out->has_rotation == false); + out->has_rotation = true; + + out->supported_rotations = PLANE_TRANSFORM_NONE; + assert(info->flags & DRM_MODE_PROP_BITMASK); + + for (int k = 0; k < info->count_enums; k++) { + out->supported_rotations.u32 |= 1 << info->enums[k].value; + } + + assert(PLANE_TRANSFORM_IS_VALID(out->supported_rotations)); + + if (info->flags & DRM_MODE_PROP_IMMUTABLE) { + out->has_hardcoded_rotation = true; + out->hardcoded_rotation.u64 = value; + } + + out->committed_state.rotation.u64 = value; +} + +static void drm_plane_init_zpos(drmModePropertyRes *info, uint64_t value, struct drm_plane *out) { + assert(out->has_zpos == false); + out->has_zpos = true; + + if (info->flags & DRM_MODE_PROP_SIGNED_RANGE) { + out->min_zpos = uint64_to_int64(info->values[0]); + out->max_zpos = uint64_to_int64(info->values[1]); + out->committed_state.zpos = uint64_to_int64(value); + assert(out->min_zpos <= out->max_zpos); + assert(out->min_zpos <= out->committed_state.zpos); + assert(out->committed_state.zpos <= out->max_zpos); + } else if (info->flags & DRM_MODE_PROP_RANGE) { + assert(info->values[0] <= INT64_MAX); + assert(info->values[1] <= INT64_MAX); + + out->min_zpos = info->values[0]; + out->max_zpos = info->values[1]; + out->committed_state.zpos = value; + assert(out->min_zpos <= out->max_zpos); + } else { + ASSERT_MSG(false, "Invalid property type for zpos property."); + } + + if (info->flags & DRM_MODE_PROP_IMMUTABLE) { + out->has_hardcoded_zpos = true; + assert(value <= INT64_MAX); + + out->hardcoded_zpos = value; + if (out->min_zpos != out->max_zpos) { + LOG_DEBUG( + "DRM plane minimum supported zpos does not equal maximum supported zpos, even though zpos is " + "immutable.\n" + ); + out->min_zpos = out->max_zpos = out->hardcoded_zpos; + } + } +} + +static int drm_plane_init_in_formats(int drm_fd, drmModePropertyRes *info, uint64_t value, struct drm_plane *out) { + drmModePropertyBlobRes *blob; + + (void) info; + + blob = drmModeGetPropertyBlob(drm_fd, value); + if (blob == NULL) { + int ok = errno; + if (ok == 0) ok = ENOMEM; + return ok; + } + + out->supports_modifiers = true; + out->supported_modified_formats_blob = memdup(blob->data, blob->length); + if (out->supported_modified_formats_blob == NULL) { + drmModeFreePropertyBlob(blob); + return ENOMEM; + } + + drmModeFreePropertyBlob(blob); + return 0; +} + +static void drm_plane_init_alpha(drmModePropertyRes *info, uint64_t value, struct drm_plane *out) { + out->has_alpha = true; + + assert(info->flags == DRM_MODE_PROP_RANGE); + assert(info->values[0] <= 0xFFFF); + assert(info->values[1] <= 0xFFFF); + assert(info->values[0] <= info->values[1]); + out->min_alpha = (uint16_t) info->values[0]; + out->max_alpha = (uint16_t) info->values[1]; + + assert(out->min_alpha <= value); + assert(value <= out->max_alpha); + out->committed_state.alpha = (uint16_t) value; +} + +static void drm_plane_init_blend_mode(drmModePropertyRes *info, uint64_t value, struct drm_plane *out) { + out->has_blend_mode = true; + assert(info->flags == DRM_MODE_PROP_ENUM); + + for (int i = 0; i < info->count_enums; i++) { + if (streq(info->enums[i].name, "None")) { + ASSERT_EQUALS(info->enums[i].value, DRM_BLEND_MODE_NONE); + out->supported_blend_modes[DRM_BLEND_MODE_NONE] = true; + } else if (streq(info->enums[i].name, "Pre-multiplied")) { + ASSERT_EQUALS(info->enums[i].value, DRM_BLEND_MODE_PREMULTIPLIED); + out->supported_blend_modes[DRM_BLEND_MODE_PREMULTIPLIED] = true; + } else if (streq(info->enums[i].name, "Coverage")) { + ASSERT_EQUALS(info->enums[i].value, DRM_BLEND_MODE_COVERAGE); + out->supported_blend_modes[DRM_BLEND_MODE_COVERAGE] = true; + } else { + LOG_DEBUG( + "Unknown KMS pixel blend mode: %s (value: %" PRIu64 ")\n", + info->enums[i].name, + (uint64_t) info->enums[i].value + ); + } + } + + out->committed_state.blend_mode = value; + assert(out->committed_state.blend_mode >= 0 && out->committed_state.blend_mode <= DRM_BLEND_MODE_MAX); + assert(out->supported_blend_modes[out->committed_state.blend_mode]); +} + +static int drm_plane_init(int drm_fd, uint32_t plane_id, struct drm_plane *out) { + bool has_type; + int ok; + + memset(out, 0, sizeof(*out)); + + drm_plane_prop_ids_init(&out->ids); + + { + drmModePlane *plane = drmModeGetPlane(drm_fd, plane_id); + if (plane == NULL) { + ok = errno; + if (ok == 0) ok = ENOMEM; + return ok; + } + + out->id = plane->plane_id; + out->possible_crtcs = plane->possible_crtcs; + out->committed_state.fb_id = plane->fb_id; + out->committed_state.crtc_id = plane->crtc_id; + + for (int i = 0; i < plane->count_formats; i++) { + for (int j = 0; j < PIXFMT_COUNT; j++) { + if (get_pixfmt_info(j)->drm_format == plane->formats[i]) { + out->supported_formats[j] = true; + break; + } + } + } + + drmModeFreePlane(plane); + } + + drmModeObjectProperties *props = drmModeObjectGetProperties(drm_fd, plane_id, DRM_MODE_OBJECT_PLANE); + if (props == NULL) { + ok = errno; + if (ok == 0) ok = ENOMEM; + return ok; + } + + has_type = false; + for (int j = 0; j < props->count_props; j++) { + uint32_t id = props->props[j]; + uint64_t value = props->prop_values[j]; + + drmModePropertyRes *info = drmModeGetProperty(drm_fd, id); + if (info == NULL) { + ok = errno; + if (ok == 0) ok = ENOMEM; + goto fail_maybe_free_supported_modified_formats_blob; + } + +#define CHECK_ASSIGN_PROPERTY_ID(_name_str, _name) \ + if (strncmp(info->name, _name_str, ARRAY_SIZE(info->name)) == 0) { \ + out->ids._name = info->prop_id; \ + } else + + DRM_PLANE_PROPERTIES(CHECK_ASSIGN_PROPERTY_ID) { + // do nothing + } + +#undef CHECK_ASSIGN_PROPERTY_ID + + + if (id == out->ids.type) { + assert(has_type == false); + has_type = true; + + out->type = value; + } else if (id == out->ids.rotation) { + drm_plane_init_rotation(info, value, out); + } else if (id == out->ids.zpos) { + drm_plane_init_zpos(info, value, out); + } else if (id == out->ids.src_x) { + out->committed_state.src_x = value; + } else if (id == out->ids.src_y) { + out->committed_state.src_y = value; + } else if (id == out->ids.src_w) { + out->committed_state.src_w = value; + } else if (id == out->ids.src_h) { + out->committed_state.src_h = value; + } else if (id == out->ids.crtc_x) { + out->committed_state.crtc_x = value; + } else if (id == out->ids.crtc_y) { + out->committed_state.crtc_y = value; + } else if (id == out->ids.crtc_w) { + out->committed_state.crtc_w = value; + } else if (id == out->ids.crtc_h) { + out->committed_state.crtc_h = value; + } else if (id == out->ids.in_formats) { + ok = drm_plane_init_in_formats(drm_fd, info, value, out); + if (ok != 0) { + drmModeFreeProperty(info); + goto fail_maybe_free_supported_modified_formats_blob; + } + } else if (id == out->ids.alpha) { + drm_plane_init_alpha(info, value, out); + } else if (id == out->ids.pixel_blend_mode) { + drm_plane_init_blend_mode(info, value, out); + } + + + drmModeFreeProperty(info); + } + + drmModeFreeObjectProperties(props); + + assert(has_type); + (void) has_type; + + out->committed_state.has_format = drm_fb_get_format(drm_fd, out->committed_state.fb_id, &out->committed_state.format); + return 0; + +fail_maybe_free_supported_modified_formats_blob: + if (out->supported_modified_formats_blob != NULL) + free(out->supported_modified_formats_blob); + + drmModeFreeObjectProperties(props); + return ok; +} + +static int drm_plane_copy(struct drm_plane *dst, const struct drm_plane *src) { + *dst = *src; + + if (src->supported_modified_formats_blob != NULL) { + /// TODO: Implement + dst->supported_modified_formats_blob = memdup(src->supported_modified_formats_blob, sizeof_drm_format_modifier_blob(src->supported_modified_formats_blob)); + if (dst->supported_modified_formats_blob == NULL) { + return ENOMEM; + } + } + + return 0; +} + +static void drm_plane_fini(struct drm_plane *plane) { + if (plane->supported_modified_formats_blob != NULL) { + free(plane->supported_modified_formats_blob); + } +} + +void drm_plane_for_each_modified_format(struct drm_plane *plane, drm_plane_modified_format_callback_t callback, void *userdata) { + struct drm_format_modifier_blob *blob; + struct drm_format_modifier *modifiers; + uint32_t *formats; + + ASSERT_NOT_NULL(plane); + ASSERT_NOT_NULL(callback); + ASSERT(plane->supports_modifiers); + ASSERT_EQUALS(plane->supported_modified_formats_blob->version, FORMAT_BLOB_CURRENT); + + blob = plane->supported_modified_formats_blob; + + modifiers = (void *) (((char *) blob) + blob->modifiers_offset); + formats = (void *) (((char *) blob) + blob->formats_offset); + + int index = 0; + for (int i = 0; i < blob->count_modifiers; i++) { + for (int j = modifiers[i].offset; (j < blob->count_formats) && (j < modifiers[i].offset + 64); j++) { + bool is_format_bit_set = (modifiers[i].formats & (1ull << (j % 64))) != 0; + if (!is_format_bit_set) { + continue; + } + + if (has_pixfmt_for_drm_format(formats[j])) { + enum pixfmt format = get_pixfmt_for_drm_format(formats[j]); + + bool should_continue = callback(plane, index, format, modifiers[i].modifier, userdata); + if (!should_continue) { + goto exit; + } + + index++; + } + } + } + +exit: + return; +} + +static bool +check_modified_format_supported(UNUSED struct drm_plane *plane, UNUSED int index, enum pixfmt format, uint64_t modifier, void *userdata) { + struct { + enum pixfmt format; + uint64_t modifier; + bool found; + } *context = userdata; + + if (format == context->format && modifier == context->modifier) { + context->found = true; + return false; + } else { + return true; + } +} + +bool drm_plane_supports_modified_formats(struct drm_plane *plane) { + return plane->supports_modifiers; +} + +bool drm_plane_supports_modified_format(struct drm_plane *plane, enum pixfmt format, uint64_t modifier) { + if (!plane->supported_modified_formats_blob) { + // return false if we want a modified format but the plane doesn't support modified formats + return false; + } + + struct { + enum pixfmt format; + uint64_t modifier; + bool found; + } context = { + .format = format, + .modifier = modifier, + .found = false, + }; + + // Check if the requested format & modifier is supported. + drm_plane_for_each_modified_format(plane, check_modified_format_supported, &context); + + return context.found; +} + +bool drm_plane_supports_unmodified_format(struct drm_plane *plane, enum pixfmt format) { + // we don't want a modified format, return false if the format is not in the list + // of supported (unmodified) formats + return plane->supported_formats[format]; +} + + +struct drm_resources *drm_resources_new(int drm_fd) { + struct drm_resources *r; + int ok; + + r = calloc(1, sizeof *r); + if (r == NULL) { + return NULL; + } + + r->n_refs = REFCOUNT_INIT_1; + + r->have_filter = false; + + drmModeRes *res = drmModeGetResources(drm_fd); + if (res == NULL) { + ok = errno; + if (ok == 0) ok = EINVAL; + LOG_ERROR("Could not get DRM device resources. drmModeGetResources: %s\n", strerror(ok)); + return NULL; + } + + r->min_width = res->min_width; + r->max_width = res->max_width; + r->min_height = res->min_height; + r->max_height = res->max_height; + + r->connectors = calloc(res->count_connectors, sizeof *(r->connectors)); + if (r->connectors == NULL) { + ok = ENOMEM; + goto fail_free_res; + } + + r->n_connectors = 0; + for (int i = 0; i < res->count_connectors; i++) { + ok = drm_connector_init(drm_fd, res->connectors[i], r->connectors + i); + if (ok != 0) { + goto fail_free_connectors; + } + + r->n_connectors++; + } + + r->encoders = calloc(res->count_encoders, sizeof *(r->encoders)); + if (r->encoders == NULL) { + ok = ENOMEM; + goto fail_free_connectors; + } + + r->n_encoders = 0; + for (int i = 0; i < res->count_encoders; i++) { + ok = drm_encoder_init(drm_fd, res->encoders[i], r->encoders + i); + if (ok != 0) { + goto fail_free_encoders; + } + + r->n_encoders++; + } + + r->crtcs = calloc(res->count_crtcs, sizeof *(r->crtcs)); + if (r->crtcs == NULL) { + ok = ENOMEM; + goto fail_free_encoders; + } + + r->n_crtcs = 0; + for (int i = 0; i < res->count_crtcs; i++) { + ok = drm_crtc_init(drm_fd, i, res->crtcs[i], r->crtcs + i); + if (ok != 0) { + goto fail_free_crtcs; + } + + r->n_crtcs++; + } + + drmModeFreeResources(res); + res = NULL; + + drmModePlaneRes *plane_res = drmModeGetPlaneResources(drm_fd); + if (plane_res == NULL) { + ok = errno; + if (ok == 0) ok = EINVAL; + LOG_ERROR("Could not get DRM device planes resources. drmModeGetPlaneResources: %s\n", strerror(ok)); + goto fail_free_crtcs; + } + + r->planes = calloc(plane_res->count_planes, sizeof *(r->planes)); + if (r->planes == NULL) { + ok = ENOMEM; + goto fail_free_plane_res; + } + + r->n_planes = 0; + for (int i = 0; i < plane_res->count_planes; i++) { + ok = drm_plane_init(drm_fd, plane_res->planes[i], r->planes + i); + if (ok != 0) { + goto fail_free_planes; + } + + r->n_planes++; + } + + drmModeFreePlaneResources(plane_res); + return r; + +fail_free_planes: + for (int i = 0; i < r->n_planes; i++) + drm_plane_fini(r->planes + i); + free(r->planes); + +fail_free_plane_res: + if (plane_res != NULL) { + drmModeFreePlaneResources(plane_res); + } + +fail_free_crtcs: + for (int i = 0; i < r->n_crtcs; i++) + drm_crtc_fini(r->crtcs + i); + free(r->crtcs); + +fail_free_encoders: + for (int i = 0; i < r->n_encoders; i++) + drm_encoder_fini(r->encoders + i); + free(r->encoders); + +fail_free_connectors: + for (int i = 0; i < r->n_connectors; i++) + drm_connector_fini(r->connectors + i); + free(r->connectors); + +fail_free_res: + if (res != NULL) { + drmModeFreeResources(res); + } + + return NULL; +} + +struct drm_resources *drm_resources_new_filtered(int drm_fd, uint32_t connector_id, uint32_t encoder_id, uint32_t crtc_id, size_t n_planes, const uint32_t *plane_ids) { + struct drm_resources *r; + int ok; + + r = calloc(1, sizeof *r); + if (r == NULL) { + return NULL; + } + + r->n_refs = REFCOUNT_INIT_1; + r->have_filter = true; + r->filter.connector_id = connector_id; + r->filter.encoder_id = encoder_id; + r->filter.crtc_id = crtc_id; + r->filter.n_planes = n_planes; + memcpy(r->filter.plane_ids, plane_ids, n_planes * sizeof *plane_ids); + + { + drmModeRes *res = drmModeGetResources(drm_fd); + if (res == NULL) { + ok = errno; + if (ok == 0) ok = EINVAL; + LOG_ERROR("Could not get DRM device resources. drmModeGetResources: %s\n", strerror(ok)); + goto fail_free_r; + } + + r->min_width = res->min_width; + r->max_width = res->max_width; + r->min_height = res->min_height; + r->max_height = res->max_height; + + drmModeFreeResources(res); + } + + r->connectors = calloc(1, sizeof *(r->connectors)); + if (r->connectors == NULL) { + ok = ENOMEM; + goto fail_free_r; + } + + ok = drm_connector_init(drm_fd, r->filter.connector_id, r->connectors + 0); + if (ok == 0) { + r->n_connectors = 1; + } else { + r->n_connectors = 0; + } + + if (r->n_connectors == 0) { + free(r->connectors); + r->connectors = NULL; + } + + r->encoders = calloc(1, sizeof *(r->encoders)); + if (r->encoders == NULL) { + ok = ENOMEM; + goto fail_free_connectors; + } + + ok = drm_encoder_init(drm_fd, r->filter.encoder_id, r->encoders + 0); + if (ok == 0) { + r->n_encoders = 1; + } else { + r->n_encoders = 0; + } + + if (r->n_encoders == 0) { + free(r->encoders); + r->encoders = NULL; + } + + r->crtcs = calloc(1, sizeof *(r->crtcs)); + if (r->crtcs == NULL) { + ok = ENOMEM; + goto fail_free_encoders; + } + + /// TODO: Implement + UNIMPLEMENTED(); + + ok = drm_crtc_init(drm_fd, (TRAP(), 0), crtc_id, r->crtcs); + if (ok == 0) { + r->n_crtcs = 1; + } else { + r->n_crtcs = 0; + } + + if (r->n_crtcs == 0) { + free(r->crtcs); + r->crtcs = NULL; + } + + r->planes = calloc(r->filter.n_planes, sizeof *(r->planes)); + if (r->planes == NULL) { + ok = ENOMEM; + goto fail_free_crtcs; + } + + r->n_planes = 0; + for (int i = 0; i < r->filter.n_planes; i++) { + assert(r->n_planes <= r->filter.n_planes); + + uint32_t plane_id = r->filter.plane_ids[i]; + struct drm_plane *plane = r->planes + r->n_planes; + + ok = drm_plane_init(drm_fd, plane_id, plane); + if (ok != 0) { + continue; + } + + r->n_planes++; + } + + if (r->n_planes == 0) { + free(r->planes); + r->planes = NULL; + } + + return 0; + + +fail_free_crtcs: + for (int i = 0; i < r->n_crtcs; i++) { + drm_crtc_fini(r->crtcs + i); + } + + if (r->crtcs != NULL) { + free(r->crtcs); + } + +fail_free_encoders: + for (int i = 0; i < r->n_encoders; i++) { + drm_encoder_fini(r->encoders + i); + } + + if (r->encoders != NULL) { + free(r->encoders); + } + +fail_free_connectors: + for (int i = 0; i < r->n_connectors; i++) { + drm_connector_fini(r->connectors + i); + } + + if (r->connectors != NULL) { + free(r->connectors); + } + +fail_free_r: + free(r); + return NULL; +} + +struct drm_resources *drm_resources_dup_filtered(struct drm_resources *res, uint32_t connector_id, uint32_t encoder_id, uint32_t crtc_id, size_t n_planes, const uint32_t *plane_ids) { + struct drm_resources *r; + int ok; + + ASSERT_NOT_NULL(res); + + r = calloc(1, sizeof *r); + if (r == NULL) { + return NULL; + } + + r->n_refs = REFCOUNT_INIT_1; + + r->have_filter = true; + r->filter.connector_id = connector_id; + r->filter.encoder_id = encoder_id; + r->filter.crtc_id = crtc_id; + r->filter.n_planes = n_planes; + memcpy(r->filter.plane_ids, plane_ids, n_planes * sizeof *plane_ids); + + r->min_width = res->min_width; + r->max_width = res->max_width; + r->min_height = res->min_height; + r->max_height = res->max_height; + + { + r->connectors = calloc(1, sizeof(struct drm_connector)); + if (r->connectors == NULL) { + ok = ENOMEM; + goto fail_free_r; + } + + struct drm_connector *conn = drm_resources_get_connector(res, connector_id); + if (conn != NULL) { + drm_connector_copy(r->connectors, conn); + r->n_connectors = 1; + } else { + r->n_connectors = 0; + } + + if (r->n_connectors == 0) { + free(r->connectors); + r->connectors = NULL; + } + } + + { + r->encoders = calloc(1, sizeof(struct drm_encoder)); + if (r->encoders == NULL) { + ok = ENOMEM; + goto fail_free_connectors; + } + + struct drm_encoder *enc = drm_resources_get_encoder(res, encoder_id); + if (enc != NULL) { + drm_encoder_copy(r->encoders, enc); + r->n_encoders = 1; + } else { + r->n_encoders = 0; + } + + if (r->n_encoders == 0) { + free(r->encoders); + r->encoders = NULL; + } + } + + { + r->crtcs = calloc(1, sizeof(struct drm_crtc)); + if (r->crtcs == NULL) { + ok = ENOMEM; + goto fail_free_encoders; + } + + struct drm_crtc *crtc = drm_resources_get_crtc(res, crtc_id); + if (crtc != NULL) { + drm_crtc_copy(r->crtcs, crtc); + r->n_crtcs = 1; + } else { + r->n_crtcs = 0; + } + + if (r->n_crtcs == 0) { + free(r->crtcs); + r->crtcs = NULL; + } + } + + r->planes = calloc(r->filter.n_planes, sizeof *(r->planes)); + if (r->planes == NULL) { + ok = ENOMEM; + goto fail_free_crtcs; + } + + r->n_planes = 0; + for (int i = 0; i < r->filter.n_planes; i++) { + assert(r->n_planes <= r->filter.n_planes); + + uint32_t plane_id = r->filter.plane_ids[i]; + struct drm_plane *dst_plane = r->planes + r->n_planes; + + struct drm_plane *src_plane = drm_resources_get_plane(res, plane_id); + if (src_plane == NULL) { + continue; + } + + ok = drm_plane_copy(dst_plane, src_plane); + if (ok != 0) { + for (int j = 0; j < r->n_planes; j++) { + drm_plane_fini(r->planes + j); + } + free(r->planes); + goto fail_free_crtcs; + } + + r->n_planes++; + } + + if (r->n_planes == 0) { + free(r->planes); + r->planes = NULL; + } + + return 0; + + +fail_free_crtcs: + for (int i = 0; i < r->n_crtcs; i++) { + drm_crtc_fini(r->crtcs + i); + } + + if (r->crtcs != NULL) { + free(r->crtcs); + } + +fail_free_encoders: + for (int i = 0; i < r->n_encoders; i++) { + drm_encoder_fini(r->encoders + i); + } + + if (r->encoders != NULL) { + free(r->encoders); + } + +fail_free_connectors: + for (int i = 0; i < r->n_connectors; i++) { + drm_connector_fini(r->connectors + i); + } + + if (r->connectors != NULL) { + free(r->connectors); + } + +fail_free_r: + free(r); + return NULL; +} + +void drm_resources_destroy(struct drm_resources *r) { + for (int i = 0; i < r->n_planes; i++) { + drm_plane_fini(r->planes + i); + } + if (r->planes != NULL) { + free(r->planes); + } + + for (int i = 0; i < r->n_crtcs; i++) { + drm_crtc_fini(r->crtcs + i); + } + if (r->crtcs != NULL) { + free(r->crtcs); + } + + for (int i = 0; i < r->n_encoders; i++) { + drm_encoder_fini(r->encoders + i); + } + if (r->encoders != NULL) { + free(r->encoders); + } + + for (int i = 0; i < r->n_connectors; i++) { + drm_connector_fini(r->connectors + i); + } + + if (r->connectors != NULL) { + free(r->connectors); + } + + free(r); +} + +DEFINE_REF_OPS(drm_resources, n_refs) + + +void drm_resources_apply_rockchip_workaround(struct drm_resources *r) { + // Rockchip driver always wants the N-th primary/cursor plane to be associated with the N-th CRTC. + // If we don't respect this, commits will work but not actually show anything on screen. + int primary_plane_index = 0; + int cursor_plane_index = 0; + drm_resources_for_each_plane(r, plane) { + if (plane->type == DRM_PLANE_TYPE_PRIMARY) { + if ((plane->possible_crtcs & (1 << primary_plane_index)) != 0) { + plane->possible_crtcs = (1 << primary_plane_index); + } else { + LOG_DEBUG("Primary plane %d does not support CRTC %d.\n", primary_plane_index, primary_plane_index); + } + + primary_plane_index++; + } else if (plane->type == DRM_PLANE_TYPE_CURSOR) { + if ((plane->possible_crtcs & (1 << cursor_plane_index)) != 0) { + plane->possible_crtcs = (1 << cursor_plane_index); + } else { + LOG_DEBUG("Cursor plane %d does not support CRTC %d.\n", cursor_plane_index, cursor_plane_index); + } + + cursor_plane_index++; + } + } +} + + +bool drm_resources_has_connector(struct drm_resources *r, uint32_t connector_id) { + ASSERT_NOT_NULL(r); + + return drm_resources_get_connector(r, connector_id) != NULL; +} + +struct drm_connector *drm_resources_get_connector(struct drm_resources *r, uint32_t connector_id) { + ASSERT_NOT_NULL(r); + + for (int i = 0; i < r->n_connectors; i++) { + if (r->connectors[i].id == connector_id) { + return r->connectors + i; + } + } + + return NULL; +} + +bool drm_resources_has_encoder(struct drm_resources *r, uint32_t encoder_id) { + ASSERT_NOT_NULL(r); + + return drm_resources_get_encoder(r, encoder_id) != NULL; +} + +struct drm_encoder *drm_resources_get_encoder(struct drm_resources *r, uint32_t encoder_id) { + ASSERT_NOT_NULL(r); + + for (int i = 0; i < r->n_encoders; i++) { + if (r->encoders[i].id == encoder_id) { + return r->encoders + i; + } + } + + return NULL; +} + +bool drm_resources_has_crtc(struct drm_resources *r, uint32_t crtc_id) { + ASSERT_NOT_NULL(r); + + return drm_resources_get_crtc(r, crtc_id) != NULL; +} + +struct drm_crtc *drm_resources_get_crtc(struct drm_resources *r, uint32_t crtc_id) { + ASSERT_NOT_NULL(r); + + for (int i = 0; i < r->n_crtcs; i++) { + if (r->crtcs[i].id == crtc_id) { + return r->crtcs + i; + } + } + + return NULL; +} + +int64_t drm_resources_get_min_zpos_for_crtc(struct drm_resources *r, uint32_t crtc_id) { + struct drm_crtc *crtc; + int64_t min_zpos; + + crtc = drm_resources_get_crtc(r, crtc_id); + if (crtc == NULL) { + return INT64_MIN; + } + + min_zpos = INT64_MAX; + for (int i = 0; i < r->n_planes; i++) { + struct drm_plane *plane = r->planes + i; + + if (plane->possible_crtcs & crtc->bitmask) { + if (plane->has_zpos && plane->min_zpos < min_zpos) { + min_zpos = plane->min_zpos; + } + } + } + + return min_zpos; +} + +uint32_t drm_resources_get_possible_planes_for_crtc(struct drm_resources *r, uint32_t crtc_id) { + struct drm_crtc *crtc; + uint32_t possible_planes; + + crtc = drm_resources_get_crtc(r, crtc_id); + if (crtc == NULL) { + return 0; + } + + possible_planes = 0; + for (int i = 0; i < r->n_planes; i++) { + struct drm_plane *plane = r->planes + i; + + if (plane->possible_crtcs & crtc->bitmask) { + possible_planes |= 1u << i; + } + } + + return possible_planes; +} + +bool drm_resources_has_plane(struct drm_resources *r, uint32_t plane_id) { + ASSERT_NOT_NULL(r); + + return drm_resources_get_plane(r, plane_id) != NULL; +} + +struct drm_plane *drm_resources_get_plane(struct drm_resources *r, uint32_t plane_id) { + ASSERT_NOT_NULL(r); + + for (int i = 0; i < r->n_planes; i++) { + if (r->planes[i].id == plane_id) { + return r->planes + i; + } + } + + return NULL; +} + +unsigned int drm_resources_get_plane_index(struct drm_resources *r, uint32_t plane_id) { + ASSERT_NOT_NULL(r); + + for (unsigned int i = 0; i < r->n_planes; i++) { + if (r->planes[i].id == plane_id) { + return i; + } + } + + return UINT_MAX; +} + + +struct drm_connector *drm_resources_connector_first(struct drm_resources *r) { + ASSERT_NOT_NULL(r); + + return r->n_connectors > 0 ? r->connectors : NULL; +} + +struct drm_connector *drm_resources_connector_end(struct drm_resources *r) { + ASSERT_NOT_NULL(r); + + return r->n_connectors > 0 ? r->connectors + r->n_connectors : NULL; +} + +struct drm_connector *drm_resources_connector_next(struct drm_resources *r, struct drm_connector *current) { + ASSERT_NOT_NULL(r); + ASSERT_NOT_NULL(current); + + return current + 1; +} + + +drmModeModeInfo *drm_connector_mode_first(struct drm_connector *c) { + ASSERT_NOT_NULL(c); + + return c->variable_state.n_modes > 0 ? c->variable_state.modes : NULL; +} + +drmModeModeInfo *drm_connector_mode_end(struct drm_connector *c) { + ASSERT_NOT_NULL(c); + + return c->variable_state.n_modes > 0 ? c->variable_state.modes + c->variable_state.n_modes : NULL; +} + +drmModeModeInfo *drm_connector_mode_next(struct drm_connector *c, drmModeModeInfo *current) { + ASSERT_NOT_NULL(c); + ASSERT_NOT_NULL(current); + + return current + 1; +} + + +struct drm_encoder *drm_resources_encoder_first(struct drm_resources *r) { + ASSERT_NOT_NULL(r); + + return r->n_encoders > 0 ? r->encoders : NULL; +} + +struct drm_encoder *drm_resources_encoder_end(struct drm_resources *r) { + ASSERT_NOT_NULL(r); + + return r->n_encoders > 0 ? r->encoders + r->n_encoders : NULL; +} + +struct drm_encoder *drm_resources_encoder_next(struct drm_resources *r, struct drm_encoder *current) { + ASSERT_NOT_NULL(r); + ASSERT_NOT_NULL(current); + + return current + 1; +} + + +struct drm_crtc *drm_resources_crtc_first(struct drm_resources *r) { + ASSERT_NOT_NULL(r); + + return r->n_crtcs > 0 ? r->crtcs : NULL; +} + +struct drm_crtc *drm_resources_crtc_end(struct drm_resources *r) { + ASSERT_NOT_NULL(r); + + return r->n_crtcs > 0 ? r->crtcs + r->n_crtcs : NULL; +} + +struct drm_crtc *drm_resources_crtc_next(struct drm_resources *r, struct drm_crtc *current) { + ASSERT_NOT_NULL(r); + ASSERT_NOT_NULL(current); + + return current + 1; +} + + +struct drm_plane *drm_resources_plane_first(struct drm_resources *r) { + ASSERT_NOT_NULL(r); + + return r->n_planes > 0 ? r->planes : NULL; +} + +struct drm_plane *drm_resources_plane_end(struct drm_resources *r) { + ASSERT_NOT_NULL(r); + + return r->n_planes > 0 ? r->planes + r->n_planes : NULL; +} + +struct drm_plane *drm_resources_plane_next(struct drm_resources *r, struct drm_plane *current) { + ASSERT_NOT_NULL(r); + ASSERT_NOT_NULL(current); + + return current + 1; +} + + +struct drm_blob { + int drm_fd; + bool close_fd; + + uint32_t blob_id; + drmModeModeInfo mode; +}; + +struct drm_blob *drm_blob_new_mode(int drm_fd, const drmModeModeInfo *mode, bool dup_fd) { + struct drm_blob *blob; + uint32_t blob_id; + int ok; + + blob = malloc(sizeof *blob); + if (blob == NULL) { + return NULL; + } + + if (dup_fd) { + blob->drm_fd = dup(drm_fd); + if (blob->drm_fd < 0) { + LOG_ERROR("Couldn't duplicate DRM fd. dup: %s\n", strerror(errno)); + goto fail_free_blob; + } + + blob->close_fd = true; + } else { + blob->drm_fd = drm_fd; + blob->close_fd = false; + } + + ok = drmModeCreatePropertyBlob(drm_fd, mode, sizeof *mode, &blob_id); + if (ok != 0) { + ok = errno; + LOG_ERROR("Couldn't upload mode to kernel. drmModeCreatePropertyBlob: %s\n", strerror(ok)); + goto fail_maybe_close_fd; + } + + blob->blob_id = blob_id; + blob->mode = *mode; + return blob; + + +fail_maybe_close_fd: + if (blob->close_fd) { + close(blob->drm_fd); + } + +fail_free_blob: + free(blob); + return NULL; +} + +void drm_blob_destroy(struct drm_blob *blob) { + int ok; + + ASSERT_NOT_NULL(blob); + + ok = drmModeDestroyPropertyBlob(blob->drm_fd, blob->blob_id); + if (ok != 0) { + ok = errno; + LOG_ERROR("Couldn't destroy mode property blob. drmModeDestroyPropertyBlob: %s\n", strerror(ok)); + } + + // we dup()-ed it in drm_blob_new_mode. + if (blob->close_fd) { + close(blob->drm_fd); + } + + free(blob); +} + +int drm_blob_get_id(struct drm_blob *blob) { + ASSERT_NOT_NULL(blob); + return blob->blob_id; +} + + diff --git a/src/modesetting.h b/src/kms/resources.h similarity index 61% rename from src/modesetting.h rename to src/kms/resources.h index bbbea305..abddc861 100644 --- a/src/modesetting.h +++ b/src/kms/resources.h @@ -1,29 +1,33 @@ -// SPDX-License-Identifier: MIT -/* - * KMS Modesetting - * - * - implements the interface to linux kernel modesetting - * - allows querying connected screens, crtcs, planes, etc - * - allows setting video modes, showing things on screen - * - * Copyright (c) 2022, Hannes Winkler - */ - -#ifndef _FLUTTERPI_SRC_MODESETTING_H -#define _FLUTTERPI_SRC_MODESETTING_H - -#include - +#ifndef _FLUTTERPI_MODESETTING_RESOURCES_H +#define _FLUTTERPI_MODESETTING_RESOURCES_H + +#include +#include +#include +#include +#include +#include + +#include +#include +#include #include +#include +#include #include #include +#include #include "pixel_format.h" -#include "util/collection.h" -#include "util/geometry.h" +#include "util/bitset.h" +#include "util/list.h" +#include "util/lock_ops.h" +#include "util/logging.h" +#include "util/macros.h" #include "util/refcounting.h" + #define DRM_ID_NONE ((uint32_t) 0xFFFFFFFF) #define DRM_ID_IS_VALID(id) ((id) != 0 && (id) != DRM_ID_NONE) @@ -216,12 +220,12 @@ #define DECLARE_PROP_ID_AS_UINT32(prop_name, prop_var_name) uint32_t prop_var_name; enum drm_blend_mode { - kPremultiplied_DrmBlendMode, - kCoverage_DrmBlendMode, - kNone_DrmBlendMode, + DRM_BLEND_MODE_PREMULTIPLIED, + DRM_BLEND_MODE_COVERAGE, + DRM_BLEND_MODE_NONE, - kMax_DrmBlendMode = kNone_DrmBlendMode, - kCount_DrmBlendMode = kMax_DrmBlendMode + 1 + DRM_BLEND_MODE_MAX = DRM_BLEND_MODE_NONE, + DRM_BLEND_MODE_COUNT = DRM_BLEND_MODE_MAX + 1 }; struct drm_connector_prop_ids { @@ -306,55 +310,52 @@ enum drm_plane_rotation { */ enum drm_plane_type { - kPrimary_DrmPlaneType = DRM_PLANE_TYPE_PRIMARY, - kOverlay_DrmPlaneType = DRM_PLANE_TYPE_OVERLAY, - kCursor_DrmPlaneType = DRM_PLANE_TYPE_CURSOR -}; - -struct drm_mode_blob { - int drm_fd; - uint32_t blob_id; - drmModeModeInfo mode; + DRM_PRIMARY_PLANE = DRM_PLANE_TYPE_PRIMARY, + DRM_OVERLAY_PLANE = DRM_PLANE_TYPE_OVERLAY, + DRM_CURSOR_PLANE = DRM_PLANE_TYPE_CURSOR }; enum drm_connector_type { - kUnknown_DrmConnectorType = DRM_MODE_CONNECTOR_Unknown, - kVGA_DrmConnectorType = DRM_MODE_CONNECTOR_VGA, - kDVII_DrmConnectorType = DRM_MODE_CONNECTOR_DVII, - kDVID_DrmConnectorType = DRM_MODE_CONNECTOR_DVID, - kDVIA_DrmConnectorType = DRM_MODE_CONNECTOR_DVIA, - kComposite_DrmConnectorType = DRM_MODE_CONNECTOR_Composite, - kSVIDEO_DrmConnectorType = DRM_MODE_CONNECTOR_SVIDEO, - kLVDS_DrmConnectorType = DRM_MODE_CONNECTOR_LVDS, - kComponent_DrmConnectorType = DRM_MODE_CONNECTOR_Component, - k9PinDIN_DrmConnectorType = DRM_MODE_CONNECTOR_9PinDIN, - kDisplayPort_DrmConnectorType = DRM_MODE_CONNECTOR_DisplayPort, - kHDMIA_DrmConnectorType = DRM_MODE_CONNECTOR_HDMIA, - kHDMIB_DrmConnectorType = DRM_MODE_CONNECTOR_HDMIB, - kTV_DrmConnectorType = DRM_MODE_CONNECTOR_TV, - keDP_DrmConnectorType = DRM_MODE_CONNECTOR_eDP, - kVIRTUAL_DrmConnectorType = DRM_MODE_CONNECTOR_VIRTUAL, - kDSI_DrmConnectorType = DRM_MODE_CONNECTOR_DSI, - kDPI_DrmConnectorType = DRM_MODE_CONNECTOR_DPI, - kWRITEBACK_DrmConnectorType = DRM_MODE_CONNECTOR_WRITEBACK, + DRM_CONNECTOR_TYPE_UNKNOWN = DRM_MODE_CONNECTOR_Unknown, + DRM_CONNECTOR_TYPE_VGA = DRM_MODE_CONNECTOR_VGA, + DRM_CONNECTOR_TYPE_DVII = DRM_MODE_CONNECTOR_DVII, + DRM_CONNECTOR_TYPE_DVID = DRM_MODE_CONNECTOR_DVID, + DRM_CONNECTOR_TYPE_DVIA = DRM_MODE_CONNECTOR_DVIA, + DRM_CONNECTOR_TYPE_COMPOSITE = DRM_MODE_CONNECTOR_Composite, + DRM_CONNECTOR_TYPE_SVIDEO = DRM_MODE_CONNECTOR_SVIDEO, + DRM_CONNECTOR_TYPE_LVDS = DRM_MODE_CONNECTOR_LVDS, + DRM_CONNECTOR_TYPE_COMPONENT = DRM_MODE_CONNECTOR_Component, + DRM_CONNECTOR_TYPE_DIN = DRM_MODE_CONNECTOR_9PinDIN, + DRM_CONNECTOR_TYPE_DISPLAYPORT = DRM_MODE_CONNECTOR_DisplayPort, + DRM_CONNECTOR_TYPE_HDMIA = DRM_MODE_CONNECTOR_HDMIA, + DRM_CONNECTOR_TYPE_HDMIB = DRM_MODE_CONNECTOR_HDMIB, + DRM_CONNECTOR_TYPE_TV = DRM_MODE_CONNECTOR_TV, + DRM_CONNECTOR_TYPE_EDP = DRM_MODE_CONNECTOR_eDP, + DRM_CONNECTOR_TYPE_VIRTUAL = DRM_MODE_CONNECTOR_VIRTUAL, + DRM_CONNECTOR_TYPE_DSI = DRM_MODE_CONNECTOR_DSI, + DRM_CONNECTOR_TYPE_DPI = DRM_MODE_CONNECTOR_DPI, + DRM_CONNECTOR_TYPE_WRITEBACK = DRM_MODE_CONNECTOR_WRITEBACK, #ifdef DRM_MODE_CONNECTOR_SPI - kSPI_DrmConnectorType = DRM_MODE_CONNECTOR_SPI + DRM_CONNECTOR_TYPE_SPI = DRM_MODE_CONNECTOR_SPI, +#endif +#ifdef DRM_MODE_CONNECTOR_USB + DRM_CONNECTOR_TYPE_USB = DRM_MODE_CONNECTOR_USB, #endif }; enum drm_connection_state { - kConnected_DrmConnectionState = DRM_MODE_CONNECTED, - kDisconnected_DrmConnectionState = DRM_MODE_DISCONNECTED, - kUnknown_DrmConnectionState = DRM_MODE_UNKNOWNCONNECTION + DRM_CONNSTATE_CONNECTED = DRM_MODE_CONNECTED, + DRM_CONNSTATE_DISCONNECTED = DRM_MODE_DISCONNECTED, + DRM_CONNSTATE_UNKNOWN = DRM_MODE_UNKNOWNCONNECTION }; enum drm_subpixel_layout { - kUnknown_DrmSubpixelLayout = DRM_MODE_SUBPIXEL_UNKNOWN, - kHorizontalRRB_DrmSubpixelLayout = DRM_MODE_SUBPIXEL_HORIZONTAL_RGB, - kHorizontalBGR_DrmSubpixelLayout = DRM_MODE_SUBPIXEL_HORIZONTAL_BGR, - kVerticalRGB_DrmSubpixelLayout = DRM_MODE_SUBPIXEL_VERTICAL_RGB, - kVerticalBGR_DrmSubpixelLayout = DRM_MODE_SUBPIXEL_VERTICAL_BGR, - kNone_DrmSubpixelLayout = DRM_MODE_SUBPIXEL_NONE + DRM_SUBPIXEL_UNKNOWN = DRM_MODE_SUBPIXEL_UNKNOWN, + DRM_SUBPIXEL_HORIZONTAL = DRM_MODE_SUBPIXEL_HORIZONTAL_RGB, + DRM_SUBPIXEL_HORIZONTAL_BGR = DRM_MODE_SUBPIXEL_HORIZONTAL_BGR, + DRM_SUBPIXEL_VERTICAL_RGB = DRM_MODE_SUBPIXEL_VERTICAL_RGB, + DRM_SUBPIXEL_VERTICAL_BGR = DRM_MODE_SUBPIXEL_VERTICAL_BGR, + DRM_SUBPIXEL_NONE = DRM_MODE_SUBPIXEL_NONE }; struct drm_connector { @@ -382,8 +383,29 @@ struct drm_connector { } committed_state; }; +enum drm_encoder_type { + DRM_ENCODER_TYPE_NONE = DRM_MODE_ENCODER_NONE, + DRM_ENCODER_TYPE_TMDS = DRM_MODE_ENCODER_TMDS, + DRM_ENCODER_TYPE_DAC = DRM_MODE_ENCODER_DAC, + DRM_ENCODER_TYPE_LVDS = DRM_MODE_ENCODER_LVDS, + DRM_ENCODER_TYPE_TVDAC = DRM_MODE_ENCODER_TVDAC, + DRM_ENCODER_TYPE_VIRTUAL = DRM_MODE_ENCODER_VIRTUAL, + DRM_ENCODER_TYPE_DSI = DRM_MODE_ENCODER_DSI, + DRM_ENCODER_TYPE_DPMST = DRM_MODE_ENCODER_DPMST, + DRM_ENCODER_TYPE_DPI = DRM_MODE_ENCODER_DPI, + DRM_ENCODER_TYPE_MAX = DRM_MODE_ENCODER_DPI, +}; + struct drm_encoder { - drmModeEncoder *encoder; + uint32_t id; + enum drm_encoder_type type; + + uint32_t possible_crtcs; + uint32_t possible_clones; + + struct { + uint32_t crtc_id; + } variable_state; }; struct drm_crtc { @@ -396,7 +418,7 @@ struct drm_crtc { struct { bool has_mode; drmModeModeInfo mode; - struct drm_mode_blob *mode_blob; + struct drm_blob *mode_blob; } committed_state; }; @@ -513,7 +535,7 @@ struct drm_plane { /// @brief The supported blend modes. /// /// Only valid if @ref has_blend_mode is true. - bool supported_blend_modes[kCount_DrmBlendMode]; + bool supported_blend_modes[DRM_BLEND_MODE_COUNT]; struct { /// @brief The committed CRTC id. @@ -601,7 +623,47 @@ typedef bool (*drm_plane_modified_format_callback_t)( void *userdata ); -struct drmdev; +/** + * @brief A set of DRM resources, e.g. connectors, encoders, CRTCs, planes. + * + * This struct is refcounted, so you should use @ref drm_resources_ref and @ref drm_resources_unref + * to manage its lifetime. + * + * DRM resources can change, e.g. when a monitor is plugged in or out, or a connector is added. + * You can update the resources with @ref drm_resources_update. + * + * @attention DRM resources are not thread-safe. They should only be accessed on a single thread + * in their entire lifetime. This includes updates using @ref drm_resources_update. + * + */ +struct drm_resources { + refcount_t n_refs; + + bool have_filter; + struct { + uint32_t connector_id; + uint32_t encoder_id; + uint32_t crtc_id; + + size_t n_planes; + uint32_t plane_ids[32]; + } filter; + + uint32_t min_width, max_width; + uint32_t min_height, max_height; + + size_t n_connectors; + struct drm_connector *connectors; + + size_t n_encoders; + struct drm_encoder *encoders; + + size_t n_crtcs; + struct drm_crtc *crtcs; + + size_t n_planes; + struct drm_plane *planes; +}; /** * @brief Iterates over every supported pixel-format & modifier pair. @@ -610,337 +672,128 @@ struct drmdev; */ void drm_plane_for_each_modified_format(struct drm_plane *plane, drm_plane_modified_format_callback_t callback, void *userdata); +bool drm_plane_supports_modified_formats(struct drm_plane *plane); + bool drm_plane_supports_modified_format(struct drm_plane *plane, enum pixfmt format, uint64_t modifier); bool drm_plane_supports_unmodified_format(struct drm_plane *plane, enum pixfmt format); -bool drm_crtc_any_plane_supports_format(struct drmdev *drmdev, struct drm_crtc *crtc, enum pixfmt pixel_format); +bool drm_resources_any_crtc_plane_supports_format(struct drm_resources *res, uint32_t crtc_id, enum pixfmt pixel_format); -struct _drmModeModeInfo; -struct drmdev_interface { - int (*open)(const char *path, int flags, void **fd_metadata_out, void *userdata); - void (*close)(int fd, void *fd_metadata, void *userdata); -}; - -struct drmdev *drmdev_new_from_interface_fd(int fd, void *fd_metadata, const struct drmdev_interface *interface, void *userdata); +/** + * @brief Create a new drm_resources object + */ +struct drm_resources *drm_resources_new(int drm_fd); -struct drmdev *drmdev_new_from_path(const char *path, const struct drmdev_interface *interface, void *userdata); +struct drm_resources *drm_resources_new_filtered(int drm_fd, uint32_t connector_id, uint32_t encoder_id, uint32_t crtc_id, size_t n_planes, const uint32_t *plane_ids); -DECLARE_REF_OPS(drmdev) +struct drm_resources *drm_resources_dup_filtered(struct drm_resources *res, uint32_t connector_id, uint32_t encoder_id, uint32_t crtc_id, size_t n_planes, const uint32_t *plane_ids); -struct drmdev; -struct _drmModeModeInfo; +void drm_resources_destroy(struct drm_resources *r); -int drmdev_get_fd(struct drmdev *drmdev); -int drmdev_get_event_fd(struct drmdev *drmdev); -bool drmdev_supports_dumb_buffers(struct drmdev *drmdev); -int drmdev_create_dumb_buffer( - struct drmdev *drmdev, - int width, - int height, - int bpp, - uint32_t *gem_handle_out, - uint32_t *pitch_out, - size_t *size_out -); -void drmdev_destroy_dumb_buffer(struct drmdev *drmdev, uint32_t gem_handle); -void *drmdev_map_dumb_buffer(struct drmdev *drmdev, uint32_t gem_handle, size_t size); -void drmdev_unmap_dumb_buffer(struct drmdev *drmdev, void *map, size_t size); -int drmdev_on_event_fd_ready(struct drmdev *drmdev); -const struct drm_connector *drmdev_get_selected_connector(struct drmdev *drmdev); -const struct drm_encoder *drmdev_get_selected_encoder(struct drmdev *drmdev); -const struct drm_crtc *drmdev_get_selected_crtc(struct drmdev *drmdev); -const struct _drmModeModeInfo *drmdev_get_selected_mode(struct drmdev *drmdev); - -struct gbm_device *drmdev_get_gbm_device(struct drmdev *drmdev); - -uint32_t drmdev_add_fb( - struct drmdev *drmdev, - uint32_t width, - uint32_t height, - enum pixfmt pixel_format, - uint32_t bo_handle, - uint32_t pitch, - uint32_t offset, - bool has_modifier, - uint64_t modifier -); +DECLARE_REF_OPS(drm_resources) -uint32_t drmdev_add_fb_multiplanar( - struct drmdev *drmdev, - uint32_t width, - uint32_t height, - enum pixfmt pixel_format, - const uint32_t bo_handles[4], - const uint32_t pitches[4], - const uint32_t offsets[4], - bool has_modifiers, - const uint64_t modifiers[4] -); - -uint32_t drmdev_add_fb_from_dmabuf( - struct drmdev *drmdev, - uint32_t width, - uint32_t height, - enum pixfmt pixel_format, - int prime_fd, - uint32_t pitch, - uint32_t offset, - bool has_modifier, - uint64_t modifier -); - -uint32_t drmdev_add_fb_from_dmabuf_multiplanar( - struct drmdev *drmdev, - uint32_t width, - uint32_t height, - enum pixfmt pixel_format, - const int prime_fds[4], - const uint32_t pitches[4], - const uint32_t offsets[4], - bool has_modifiers, - const uint64_t modifiers[4] -); - -uint32_t drmdev_add_fb_from_gbm_bo(struct drmdev *drmdev, struct gbm_bo *bo, bool cast_opaque); +/** + * @brief Apply a workaround for the Rockchip DRM driver. + * + * The rockchip driver has special requirements as to which CRTCs can be used with which planes. + * This function will restrict the possible_crtcs property for each plane to satisfy that requirement. + * + * @attention This function can only be called on un-filtered resources, and should be called after each drm_resources_update. + * + * @param r The resources to apply the workaround to. + * + */ +void drm_resources_apply_rockchip_workaround(struct drm_resources *r); -int drmdev_rm_fb_locked(struct drmdev *drmdev, uint32_t fb_id); -int drmdev_rm_fb(struct drmdev *drmdev, uint32_t fb_id); +bool drm_resources_has_connector(struct drm_resources *r, uint32_t connector_id); -int drmdev_get_last_vblank(struct drmdev *drmdev, uint32_t crtc_id, uint64_t *last_vblank_ns_out); +struct drm_connector *drm_resources_get_connector(struct drm_resources *r, uint32_t connector_id); -bool drmdev_can_modeset(struct drmdev *drmdev); +bool drm_resources_has_encoder(struct drm_resources *r, uint32_t encoder_id); -void drmdev_suspend(struct drmdev *drmdev); +struct drm_encoder *drm_resources_get_encoder(struct drm_resources *r, uint32_t encoder_id); -int drmdev_resume(struct drmdev *drmdev); +bool drm_resources_has_crtc(struct drm_resources *r, uint32_t crtc_id); -int drmdev_move_cursor(struct drmdev *drmdev, uint32_t crtc_id, struct vec2i pos); +struct drm_crtc *drm_resources_get_crtc(struct drm_resources *r, uint32_t crtc_id); -static inline double mode_get_vrefresh(const drmModeModeInfo *mode) { - return mode->clock * 1000.0 / (mode->htotal * mode->vtotal); -} +int64_t drm_resources_get_min_zpos_for_crtc(struct drm_resources *r, uint32_t crtc_id); -typedef void (*kms_scanout_cb_t)(struct drmdev *drmdev, uint64_t vblank_ns, void *userdata); +uint32_t drm_resources_get_possible_planes_for_crtc(struct drm_resources *r, uint32_t crtc_id); -struct kms_fb_layer { - uint32_t drm_fb_id; - enum pixfmt format; - bool has_modifier; - uint64_t modifier; +bool drm_resources_has_plane(struct drm_resources *r, uint32_t plane_id); - int32_t src_x, src_y, src_w, src_h; - int32_t dst_x, dst_y, dst_w, dst_h; +struct drm_plane *drm_resources_get_plane(struct drm_resources *r, uint32_t plane_id); - bool has_rotation; - drm_plane_transform_t rotation; +unsigned int drm_resources_get_plane_index(struct drm_resources *r, uint32_t plane_id); - bool has_in_fence_fd; - int in_fence_fd; - bool prefer_cursor; -}; +struct drm_connector *drm_resources_connector_first(struct drm_resources *r); -typedef void (*kms_fb_release_cb_t)(void *userdata); +struct drm_connector *drm_resources_connector_end(struct drm_resources *r); -typedef void (*kms_deferred_fb_release_cb_t)(void *userdata, int syncfile_fd); +struct drm_connector *drm_resources_connector_next(struct drm_resources *r, struct drm_connector *current); -struct kms_req_builder; -struct kms_req_builder *drmdev_create_request_builder(struct drmdev *drmdev, uint32_t crtc_id); +drmModeModeInfo *drm_connector_mode_first(struct drm_connector *c); -DECLARE_REF_OPS(kms_req_builder) +drmModeModeInfo *drm_connector_mode_end(struct drm_connector *c); -/** - * @brief Gets the @ref drmdev associated with this KMS request builder. - * - * @param builder The KMS request builder. - * @returns The drmdev associated with this KMS request builder. - */ -struct drmdev *kms_req_builder_get_drmdev(struct kms_req_builder *builder); +drmModeModeInfo *drm_connector_mode_next(struct drm_connector *c, drmModeModeInfo *current); -/** - * @brief Gets the CRTC associated with this KMS request builder. - * - * @param builder The KMS request builder. - * @returns The CRTC associated with this KMS request builder. - */ -struct drm_crtc *kms_req_builder_get_crtc(struct kms_req_builder *builder); -/** - * @brief Adds a property to the KMS request that will set the given video mode - * on this CRTC on commit, regardless of whether the currently committed output - * mode is the same. - * - * @param builder The KMS request builder. - * @param mode The output mode to set (on @ref kms_req_commit) - * @returns Zero if successful, positive errno-style error on failure. - */ -int kms_req_builder_set_mode(struct kms_req_builder *builder, const drmModeModeInfo *mode); +struct drm_encoder *drm_resources_encoder_first(struct drm_resources *r); -/** - * @brief Adds a property to the KMS request that will unset the configured - * output mode for this CRTC on commit, regardless of whether the currently - * committed output mdoe is already unset. - * - * @param builder The KMS request builder. - * @returns Zero if successful, positive errno-style error on failure. - */ -int kms_req_builder_unset_mode(struct kms_req_builder *builder); +struct drm_encoder *drm_resources_encoder_end(struct drm_resources *r); -/** - * @brief Adds a property to the KMS request that will change the connector - * that this CRTC is displaying content on to @param connector_id. - * - * @param builder The KMS request builder. - * @param connector_id The connector that this CRTC should display contents on. - * @returns Zero if successful, EINVAL if the @param connector_id is invalid. - */ -int kms_req_builder_set_connector(struct kms_req_builder *builder, uint32_t connector_id); +struct drm_encoder *drm_resources_encoder_next(struct drm_resources *r, struct drm_encoder *current); -/** - * @brief True if the next layer pushed using @ref kms_req_builder_push_fb_layer - * should be opaque, i.e. use a framebuffer which has a pixel format that has no - * alpha channel. - * - * This is true for the bottom-most layer. There are some display controllers - * that don't support non-opaque pixel formats for the bottom-most (primary) - * plane. So ignoring this might lead to an EINVAL on commit. - * - * @param builder The KMS request builder. - * @returns True if the next layer should preferably be opaque, false if there's - * no preference. - */ -bool kms_req_builder_prefer_next_layer_opaque(struct kms_req_builder *builder); -/** - * @brief Adds a new framebuffer (display) layer on top of the last layer. - * - * If this is the first layer, the framebuffer should cover the entire screen - * (CRTC). - * - * To allow the use of explicit fencing, specify an in_fence_fd in @param layer - * and a @param deferred_release_callback. - * - * If explicit fencing is supported: - * - the in_fence_fd should be a DRM syncobj fd that signals - * when the GPU has finished rendering to the framebuffer and is ready - * to be scanned out. - * - @param deferred_release_callback will be called - * with a DRM syncobj fd that is signaled once the framebuffer is no longer - * being displayed on screen (and can be rendered into again) - * - * If explicit fencing is not supported: - * - the in_fence_fd in @param layer will be closed by this procedure. - * - @param deferred_release_callback will NOT be called and - * @param release_callback will be called instead. - * - * Explicit fencing is supported: When atomic modesetting is being used and - * the driver supports it. (Driver has IN_FENCE_FD plane and OUT_FENCE_PTR crtc - * properties) - * - * @param builder The KMS request builder. - * @param layer The exact details (src pos, output pos, rotation, - * framebuffer) of the layer that should be shown on - * screen. - * @param release_callback Called when the framebuffer of this layer is no - * longer being shown on screen. This is called with the - * drmdev locked, so make sure to use _locked variants - * of any drmdev calls. - * @param deferred_release_callback (Unimplemented right now) If this is present, - * this callback might be called instead of - * @param release_callback. - * This is called with a DRM syncobj fd that is - * signaled when the framebuffer is no longer - * shown on screen. - * Legacy DRM modesetting does not support - * explicit fencing, in which case - * @param release_callback will be called - * instead. - * @param userdata Userdata pointer that's passed to the release_callback or - * deferred_release_callback as-is. - * @returns Zero on success, otherwise: - * - EINVAL: if attempting to push a second framebuffer layer, if - * driver supports atomic modesetting but legacy modesetting is - * being used. - * - EIO: if no DRM plane could be found that supports displaying - * this framebuffer layer. Either the pixel format is not - * supported, the modifier, the rotation or the drm device - * doesn't have enough planes. - * - The error returned by @ref close if closing the in_fence_fd - * fails. - */ -int kms_req_builder_push_fb_layer( - struct kms_req_builder *builder, - const struct kms_fb_layer *layer, - kms_fb_release_cb_t release_callback, - kms_deferred_fb_release_cb_t deferred_release_callback, - void *userdata -); +struct drm_crtc *drm_resources_crtc_first(struct drm_resources *r); -/** - * @brief Push a "fake" layer that just keeps one zpos free, incase something - * other than KMS wants to display contents there. (e.g. omxplayer) - * - * @param builder The KMS request builder. - * @param zpos_out Filled with the zpos that won't be occupied by the request - * builder. - * @returns Zero. - */ -int kms_req_builder_push_zpos_placeholder_layer(struct kms_req_builder *builder, int64_t *zpos_out); +struct drm_crtc *drm_resources_crtc_end(struct drm_resources *r); -/** - * @brief A KMS request (atomic or legacy modesetting) that can be committed to - * change the state of a single CRTC. - * - * Only way to construct this is by building a KMS request using - * @ref kms_req_builder and then calling @ref kms_req_builder_build. - */ -struct kms_req; +struct drm_crtc *drm_resources_crtc_next(struct drm_resources *r, struct drm_crtc *current); -DECLARE_REF_OPS(kms_req) -/** - * @brief Build the KMS request builder into an actual, immutable KMS request - * that can be committed. Internally this doesn't do much at all. - * - * @param builder The KMS request builder that should be built. - * @returns KMS request that can be committed using @ref kms_req_commit_blocking - * or @ref kms_req_commit_nonblocking. - * The returned KMS request has refcount 1. Unref using - * @ref kms_req_unref after usage. - */ -struct kms_req *kms_req_builder_build(struct kms_req_builder *builder); +struct drm_plane *drm_resources_plane_first(struct drm_resources *r); -int kms_req_commit_blocking(struct kms_req *req, uint64_t *vblank_ns_out); +struct drm_plane *drm_resources_plane_end(struct drm_resources *r); -int kms_req_commit_nonblocking(struct kms_req *req, kms_scanout_cb_t scanout_cb, void *userdata, void_callback_t destroy_cb); +struct drm_plane *drm_resources_plane_next(struct drm_resources *r, struct drm_plane *current); -struct drm_connector *__next_connector(const struct drmdev *drmdev, const struct drm_connector *connector); -struct drm_encoder *__next_encoder(const struct drmdev *drmdev, const struct drm_encoder *encoder); +#define drm_resources_for_each_connector(res, connector) \ + for (struct drm_connector *(connector) = drm_resources_connector_first(res); (connector) != drm_resources_connector_end(res); (connector) = drm_resources_connector_next((res), (connector))) -struct drm_crtc *__next_crtc(const struct drmdev *drmdev, const struct drm_crtc *crtc); +#define drm_connector_for_each_mode(connector, mode) \ + for (drmModeModeInfo *(mode) = drm_connector_mode_first(connector); (mode) != drm_connector_mode_end(connector); (mode) = drm_connector_mode_next((connector), (mode))) -struct drm_plane *__next_plane(const struct drmdev *drmdev, const struct drm_plane *plane); +#define drm_resources_for_each_encoder(res, encoder) \ + for (struct drm_encoder *(encoder) = drm_resources_encoder_first(res); (encoder) != drm_resources_encoder_end(res); (encoder) = drm_resources_encoder_next((res), (encoder))) -drmModeModeInfo *__next_mode(const struct drm_connector *connector, const drmModeModeInfo *mode); +#define drm_resources_for_each_crtc(res, crtc) \ + for (struct drm_crtc *(crtc) = drm_resources_crtc_first(res); (crtc) != drm_resources_crtc_end(res); (crtc) = drm_resources_crtc_next((res), (crtc))) -#define for_each_connector_in_drmdev(drmdev, connector) \ - for (connector = __next_connector(drmdev, NULL); connector != NULL; connector = __next_connector(drmdev, connector)) +#define drm_resources_for_each_plane(res, plane) \ + for (struct drm_plane *(plane) = drm_resources_plane_first(res); (plane) != drm_resources_plane_end(res); (plane) = drm_resources_plane_next((res), (plane))) -#define for_each_encoder_in_drmdev(drmdev, encoder) \ - for (encoder = __next_encoder(drmdev, NULL); encoder != NULL; encoder = __next_encoder(drmdev, encoder)) -#define for_each_crtc_in_drmdev(drmdev, crtc) for (crtc = __next_crtc(drmdev, NULL); crtc != NULL; crtc = __next_crtc(drmdev, crtc)) +struct drm_blob *drm_blob_new_mode(int drm_fd, const drmModeModeInfo *mode, bool dup_fd); -#define for_each_plane_in_drmdev(drmdev, plane) for (plane = __next_plane(drmdev, NULL); plane != NULL; plane = __next_plane(drmdev, plane)) +void drm_blob_destroy(struct drm_blob *blob); -#define for_each_mode_in_connector(connector, mode) \ - for (mode = __next_mode(connector, NULL); mode != NULL; mode = __next_mode(connector, mode)) +int drm_blob_get_id(struct drm_blob *blob); -#define for_each_unreserved_plane_in_atomic_req(atomic_req, plane) for_each_pointer_in_pset(&(atomic_req)->available_planes, plane) +/** + * @brief Get the precise refresh rate of a video mode. + */ +static inline double mode_get_vrefresh(const drmModeModeInfo *mode) { + return mode->clock * 1000.0 / (mode->htotal * mode->vtotal); +} -#endif // _FLUTTERPI_SRC_MODESETTING_H +#endif + \ No newline at end of file diff --git a/src/modesetting.c b/src/modesetting.c deleted file mode 100644 index 80508a1b..00000000 --- a/src/modesetting.c +++ /dev/null @@ -1,3007 +0,0 @@ -#include "modesetting.h" - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include -#include -#include -#include - -#include "pixel_format.h" -#include "util/bitset.h" -#include "util/list.h" -#include "util/lock_ops.h" -#include "util/logging.h" -#include "util/macros.h" -#include "util/refcounting.h" - -struct drm_fb { - struct list_head entry; - - uint32_t id; - - uint32_t width, height; - - enum pixfmt format; - - bool has_modifier; - uint64_t modifier; - - uint32_t flags; - - uint32_t handles[4]; - uint32_t pitches[4]; - uint32_t offsets[4]; -}; - -struct kms_req_layer { - struct kms_fb_layer layer; - - uint32_t plane_id; - struct drm_plane *plane; - - bool set_zpos; - int64_t zpos; - - bool set_rotation; - drm_plane_transform_t rotation; - - kms_fb_release_cb_t release_callback; - kms_deferred_fb_release_cb_t deferred_release_callback; - void *release_callback_userdata; -}; - -struct kms_req_builder { - refcount_t n_refs; - - struct drmdev *drmdev; - bool use_legacy; - bool supports_atomic; - - struct drm_connector *connector; - struct drm_crtc *crtc; - - BITSET_DECLARE(available_planes, 32); - drmModeAtomicReq *req; - int64_t next_zpos; - - int n_layers; - struct kms_req_layer layers[32]; - - bool unset_mode; - bool has_mode; - drmModeModeInfo mode; -}; - -COMPILE_ASSERT(BITSET_SIZE(((struct kms_req_builder *) 0)->available_planes) == 32); - -struct drmdev { - int fd; - - refcount_t n_refs; - pthread_mutex_t mutex; - bool supports_atomic_modesetting; - bool supports_dumb_buffers; - - size_t n_connectors; - struct drm_connector *connectors; - - size_t n_encoders; - struct drm_encoder *encoders; - - size_t n_crtcs; - struct drm_crtc *crtcs; - - size_t n_planes; - struct drm_plane *planes; - - drmModeRes *res; - drmModePlaneRes *plane_res; - - struct gbm_device *gbm_device; - - int event_fd; - - struct { - kms_scanout_cb_t scanout_callback; - void *userdata; - void_callback_t destroy_callback; - - struct kms_req *last_flipped; - } per_crtc_state[32]; - - int master_fd; - void *master_fd_metadata; - - struct drmdev_interface interface; - void *userdata; - - struct list_head fbs; -}; - -static bool is_drm_master(int fd) { - return drmAuthMagic(fd, 0) != -EACCES; -} - -static struct drm_mode_blob *drm_mode_blob_new(int drm_fd, const drmModeModeInfo *mode) { - struct drm_mode_blob *blob; - uint32_t blob_id; - int ok; - - blob = malloc(sizeof *blob); - if (blob == NULL) { - return NULL; - } - - ok = drmModeCreatePropertyBlob(drm_fd, mode, sizeof *mode, &blob_id); - if (ok != 0) { - ok = errno; - LOG_ERROR("Couldn't upload mode to kernel. drmModeCreatePropertyBlob: %s\n", strerror(ok)); - free(blob); - return NULL; - } - - blob->drm_fd = dup(drm_fd); - blob->blob_id = blob_id; - blob->mode = *mode; - return blob; -} - -void drm_mode_blob_destroy(struct drm_mode_blob *blob) { - int ok; - - ASSERT_NOT_NULL(blob); - - ok = drmModeDestroyPropertyBlob(blob->drm_fd, blob->blob_id); - if (ok != 0) { - ok = errno; - LOG_ERROR("Couldn't destroy mode property blob. drmModeDestroyPropertyBlob: %s\n", strerror(ok)); - } - // we dup()-ed it in drm_mode_blob_new. - close(blob->drm_fd); - free(blob); -} - -DEFINE_STATIC_LOCK_OPS(drmdev, mutex) - -static int fetch_connector(int drm_fd, uint32_t connector_id, struct drm_connector *connector_out) { - struct drm_connector_prop_ids ids; - drmModeObjectProperties *props; - drmModePropertyRes *prop_info; - drmModeConnector *connector; - drmModeModeInfo *modes; - uint32_t crtc_id; - int ok; - - drm_connector_prop_ids_init(&ids); - - connector = drmModeGetConnector(drm_fd, connector_id); - if (connector == NULL) { - ok = errno; - LOG_ERROR("Could not get DRM device connector. drmModeGetConnector"); - return ok; - } - - props = drmModeObjectGetProperties(drm_fd, connector_id, DRM_MODE_OBJECT_CONNECTOR); - if (props == NULL) { - ok = errno; - perror("[modesetting] Could not get DRM device connectors properties. drmModeObjectGetProperties"); - goto fail_free_connector; - } - - crtc_id = DRM_ID_NONE; - for (int i = 0; i < props->count_props; i++) { - prop_info = drmModeGetProperty(drm_fd, props->props[i]); - if (prop_info == NULL) { - ok = errno; - LOG_ERROR("Could not get DRM device connector properties' info. drmModeGetProperty: %s\n", strerror(ok)); - goto fail_free_props; - } - -#define CHECK_ASSIGN_PROPERTY_ID(_name_str, _name) \ - if (strncmp(prop_info->name, _name_str, DRM_PROP_NAME_LEN) == 0) { \ - ids._name = prop_info->prop_id; \ - } else - - DRM_CONNECTOR_PROPERTIES(CHECK_ASSIGN_PROPERTY_ID) { - // this is the trailing else case - LOG_DEBUG("Unknown DRM connector property: %s\n", prop_info->name); - } - -#undef CHECK_ASSIGN_PROPERTY_ID - - if (strncmp(prop_info->name, "CRTC_ID", DRM_PROP_NAME_LEN) == 0) { - crtc_id = props->prop_values[i]; - } - - drmModeFreeProperty(prop_info); - prop_info = NULL; - } - - assert((connector->modes == NULL) == (connector->count_modes == 0)); - - if (connector->modes != NULL) { - modes = memdup(connector->modes, connector->count_modes * sizeof(*connector->modes)); - if (modes == NULL) { - ok = ENOMEM; - goto fail_free_props; - } - } else { - modes = NULL; - } - - connector_out->id = connector->connector_id; - connector_out->type = connector->connector_type; - connector_out->type_id = connector->connector_type_id; - connector_out->ids = ids; - connector_out->n_encoders = connector->count_encoders; - assert(connector->count_encoders <= 32); - memcpy(connector_out->encoders, connector->encoders, connector->count_encoders * sizeof(uint32_t)); - connector_out->variable_state.connection_state = (enum drm_connection_state) connector->connection; - connector_out->variable_state.subpixel_layout = (enum drm_subpixel_layout) connector->subpixel; - connector_out->variable_state.width_mm = connector->mmWidth; - connector_out->variable_state.height_mm = connector->mmHeight; - connector_out->variable_state.n_modes = connector->count_modes; - connector_out->variable_state.modes = modes; - connector_out->committed_state.crtc_id = crtc_id; - connector_out->committed_state.encoder_id = connector->encoder_id; - drmModeFreeObjectProperties(props); - drmModeFreeConnector(connector); - return 0; - -fail_free_props: - drmModeFreeObjectProperties(props); - -fail_free_connector: - drmModeFreeConnector(connector); - return ok; -} - -static void free_connector(struct drm_connector *connector) { - free(connector->variable_state.modes); -} - -static int fetch_connectors(struct drmdev *drmdev, struct drm_connector **connectors_out, size_t *n_connectors_out) { - struct drm_connector *connectors; - int ok; - - connectors = calloc(drmdev->res->count_connectors, sizeof *connectors); - if (connectors == NULL) { - *connectors_out = NULL; - return ENOMEM; - } - - for (int i = 0; i < drmdev->res->count_connectors; i++) { - ok = fetch_connector(drmdev->fd, drmdev->res->connectors[i], connectors + i); - if (ok != 0) { - for (int j = 0; j < i; j++) - free_connector(connectors + j); - goto fail_free_connectors; - } - } - - *connectors_out = connectors; - *n_connectors_out = drmdev->res->count_connectors; - return 0; - -fail_free_connectors: - free(connectors); - *connectors_out = NULL; - *n_connectors_out = 0; - return ok; -} - -static int free_connectors(struct drm_connector *connectors, size_t n_connectors) { - for (int i = 0; i < n_connectors; i++) { - free_connector(connectors + i); - } - free(connectors); - return 0; -} - -static int fetch_encoder(int drm_fd, uint32_t encoder_id, struct drm_encoder *encoder_out) { - drmModeEncoder *encoder; - int ok; - - encoder = drmModeGetEncoder(drm_fd, encoder_id); - if (encoder == NULL) { - ok = errno; - perror("[modesetting] Could not get DRM device encoder. drmModeGetEncoder"); - return ok; - } - - encoder_out->encoder = encoder; - return 0; -} - -static void free_encoder(struct drm_encoder *encoder) { - drmModeFreeEncoder(encoder->encoder); -} - -static int fetch_encoders(struct drmdev *drmdev, struct drm_encoder **encoders_out, size_t *n_encoders_out) { - struct drm_encoder *encoders; - int n_allocated_encoders; - int ok; - - encoders = calloc(drmdev->res->count_encoders, sizeof *encoders); - if (encoders == NULL) { - *encoders_out = NULL; - *n_encoders_out = 0; - return ENOMEM; - } - - n_allocated_encoders = 0; - for (int i = 0; i < drmdev->res->count_encoders; i++, n_allocated_encoders++) { - ok = fetch_encoder(drmdev->fd, drmdev->res->encoders[i], encoders + i); - if (ok != 0) { - goto fail_free_encoders; - } - } - - *encoders_out = encoders; - *n_encoders_out = drmdev->res->count_encoders; - return 0; - -fail_free_encoders: - for (int i = 0; i < n_allocated_encoders; i++) { - drmModeFreeEncoder(encoders[i].encoder); - } - - free(encoders); - - *encoders_out = NULL; - *n_encoders_out = 0; - return ok; -} - -static void free_encoders(struct drm_encoder *encoders, size_t n_encoders) { - for (int i = 0; i < n_encoders; i++) { - free_encoder(encoders + i); - } - free(encoders); -} - -static int fetch_crtc(int drm_fd, int crtc_index, uint32_t crtc_id, struct drm_crtc *crtc_out) { - struct drm_crtc_prop_ids ids; - drmModeObjectProperties *props; - drmModePropertyRes *prop_info; - drmModeCrtc *crtc; - int ok; - - drm_crtc_prop_ids_init(&ids); - - crtc = drmModeGetCrtc(drm_fd, crtc_id); - if (crtc == NULL) { - ok = errno; - perror("[modesetting] Could not get DRM device CRTC. drmModeGetCrtc"); - return ok; - } - - props = drmModeObjectGetProperties(drm_fd, crtc_id, DRM_MODE_OBJECT_CRTC); - if (props == NULL) { - ok = errno; - perror("[modesetting] Could not get DRM device CRTCs properties. drmModeObjectGetProperties"); - goto fail_free_crtc; - } - - for (int i = 0; i < props->count_props; i++) { - prop_info = drmModeGetProperty(drm_fd, props->props[i]); - if (prop_info == NULL) { - ok = errno; - perror("[modesetting] Could not get DRM device CRTCs properties' info. drmModeGetProperty"); - goto fail_free_props; - } - -#define CHECK_ASSIGN_PROPERTY_ID(_name_str, _name) \ - if (strncmp(prop_info->name, _name_str, ARRAY_SIZE(prop_info->name)) == 0) { \ - ids._name = prop_info->prop_id; \ - } else - - DRM_CRTC_PROPERTIES(CHECK_ASSIGN_PROPERTY_ID) { - // this is the trailing else case - LOG_DEBUG("Unknown DRM crtc property: %s\n", prop_info->name); - } - -#undef CHECK_ASSIGN_PROPERTY_ID - - drmModeFreeProperty(prop_info); - prop_info = NULL; - } - - ASSUME(0 <= crtc_index && crtc_index < 32); - - crtc_out->id = crtc->crtc_id; - crtc_out->index = (uint8_t) crtc_index; - crtc_out->bitmask = 1u << crtc_index; - crtc_out->ids = ids; - crtc_out->committed_state.has_mode = crtc->mode_valid; - crtc_out->committed_state.mode = crtc->mode; - crtc_out->committed_state.mode_blob = NULL; - drmModeFreeObjectProperties(props); - drmModeFreeCrtc(crtc); - return 0; - -fail_free_props: - drmModeFreeObjectProperties(props); - -fail_free_crtc: - drmModeFreeCrtc(crtc); - return ok; -} - -static void free_crtc(struct drm_crtc *crtc) { - /// TODO: Implement - (void) crtc; -} - -static int fetch_crtcs(struct drmdev *drmdev, struct drm_crtc **crtcs_out, size_t *n_crtcs_out) { - struct drm_crtc *crtcs; - int ok; - - crtcs = calloc(drmdev->res->count_crtcs, sizeof *crtcs); - if (crtcs == NULL) { - *crtcs_out = NULL; - *n_crtcs_out = 0; - return ENOMEM; - } - - for (int i = 0; i < drmdev->res->count_crtcs; i++) { - ok = fetch_crtc(drmdev->fd, i, drmdev->res->crtcs[i], crtcs + i); - if (ok != 0) { - for (int j = 0; j < i; j++) - free_crtc(crtcs + i); - goto fail_free_crtcs; - } - } - - *crtcs_out = crtcs; - *n_crtcs_out = drmdev->res->count_crtcs; - return 0; - -fail_free_crtcs: - free(crtcs); - *crtcs_out = NULL; - *n_crtcs_out = 0; - return ok; -} - -static int free_crtcs(struct drm_crtc *crtcs, size_t n_crtcs) { - for (int i = 0; i < n_crtcs; i++) { - free_crtc(crtcs + i); - } - free(crtcs); - return 0; -} - -void drm_plane_for_each_modified_format(struct drm_plane *plane, drm_plane_modified_format_callback_t callback, void *userdata) { - struct drm_format_modifier_blob *blob; - struct drm_format_modifier *modifiers; - uint32_t *formats; - - ASSERT_NOT_NULL(plane); - ASSERT_NOT_NULL(callback); - ASSERT(plane->supports_modifiers); - ASSERT_EQUALS(plane->supported_modified_formats_blob->version, FORMAT_BLOB_CURRENT); - - blob = plane->supported_modified_formats_blob; - - modifiers = (void *) (((char *) blob) + blob->modifiers_offset); - formats = (void *) (((char *) blob) + blob->formats_offset); - - int index = 0; - for (int i = 0; i < blob->count_modifiers; i++) { - for (int j = modifiers[i].offset; (j < blob->count_formats) && (j < modifiers[i].offset + 64); j++) { - bool is_format_bit_set = (modifiers[i].formats & (1ull << (j % 64))) != 0; - if (!is_format_bit_set) { - continue; - } - - if (has_pixfmt_for_drm_format(formats[j])) { - enum pixfmt format = get_pixfmt_for_drm_format(formats[j]); - - bool should_continue = callback(plane, index, format, modifiers[i].modifier, userdata); - if (!should_continue) { - goto exit; - } - - index++; - } - } - } - -exit: - return; -} - -static bool -check_modified_format_supported(UNUSED struct drm_plane *plane, UNUSED int index, enum pixfmt format, uint64_t modifier, void *userdata) { - struct { - enum pixfmt format; - uint64_t modifier; - bool found; - } *context = userdata; - - if (format == context->format && modifier == context->modifier) { - context->found = true; - return false; - } else { - return true; - } -} - -bool drm_plane_supports_modified_format(struct drm_plane *plane, enum pixfmt format, uint64_t modifier) { - if (!plane->supported_modified_formats_blob) { - // return false if we want a modified format but the plane doesn't support modified formats - return false; - } - - struct { - enum pixfmt format; - uint64_t modifier; - bool found; - } context = { - .format = format, - .modifier = modifier, - .found = false, - }; - - // Check if the requested format & modifier is supported. - drm_plane_for_each_modified_format(plane, check_modified_format_supported, &context); - - return context.found; -} - -bool drm_plane_supports_unmodified_format(struct drm_plane *plane, enum pixfmt format) { - // we don't want a modified format, return false if the format is not in the list - // of supported (unmodified) formats - return plane->supported_formats[format]; -} - -bool drm_crtc_any_plane_supports_format(struct drmdev *drmdev, struct drm_crtc *crtc, enum pixfmt pixel_format) { - struct drm_plane *plane; - - for_each_plane_in_drmdev(drmdev, plane) { - if (!(plane->possible_crtcs & crtc->bitmask)) { - // Only query planes that are possible to connect to the CRTC we're using. - continue; - } - - if (plane->type != kPrimary_DrmPlaneType && plane->type != kOverlay_DrmPlaneType) { - // We explicitly only look for primary and overlay planes. - continue; - } - - if (drm_plane_supports_unmodified_format(plane, pixel_format)) { - return true; - } - } - - return false; -} - -struct _drmModeFB2; - -struct drm_mode_fb2 { - uint32_t fb_id; - uint32_t width, height; - uint32_t pixel_format; /* fourcc code from drm_fourcc.h */ - uint64_t modifier; /* applies to all buffers */ - uint32_t flags; - - /* per-plane GEM handle; may be duplicate entries for multiple planes */ - uint32_t handles[4]; - uint32_t pitches[4]; /* bytes */ - uint32_t offsets[4]; /* bytes */ -}; - -#ifdef HAVE_FUNC_ATTRIBUTE_WEAK -extern struct _drmModeFB2 *drmModeGetFB2(int fd, uint32_t bufferId) __attribute__((weak)); -extern void drmModeFreeFB2(struct _drmModeFB2 *ptr) __attribute__((weak)); - #define HAVE_WEAK_DRM_MODE_GET_FB2 -#endif - -static int fetch_plane(int drm_fd, uint32_t plane_id, struct drm_plane *plane_out) { - struct drm_plane_prop_ids ids; - drmModeObjectProperties *props; - drm_plane_transform_t hardcoded_rotation = PLANE_TRANSFORM_NONE, supported_rotations = PLANE_TRANSFORM_NONE, - committed_rotation = PLANE_TRANSFORM_NONE; - enum drm_blend_mode committed_blend_mode = kNone_DrmBlendMode; - enum drm_plane_type type = kPrimary_DrmPlaneType; - drmModePropertyRes *info; - drmModePlane *plane; - uint32_t comitted_crtc_x, comitted_crtc_y, comitted_crtc_w, comitted_crtc_h; - uint32_t comitted_src_x, comitted_src_y, comitted_src_w, comitted_src_h; - uint16_t committed_alpha = 0; - int64_t min_zpos = 0, max_zpos = 0, hardcoded_zpos = 0, committed_zpos = 0; - bool supported_blend_modes[kCount_DrmBlendMode] = { 0 }; - bool supported_formats[PIXFMT_COUNT] = { 0 }; - bool has_type, has_rotation, has_zpos, has_hardcoded_zpos, has_hardcoded_rotation, has_alpha, has_blend_mode; - int ok; - - drm_plane_prop_ids_init(&ids); - - plane = drmModeGetPlane(drm_fd, plane_id); - if (plane == NULL) { - ok = errno; - perror("[modesetting] Could not get DRM device plane. drmModeGetPlane"); - return ok; - } - - props = drmModeObjectGetProperties(drm_fd, plane_id, DRM_MODE_OBJECT_PLANE); - if (props == NULL) { - ok = errno; - perror("[modesetting] Could not get DRM device planes' properties. drmModeObjectGetProperties"); - goto fail_free_plane; - } - - // zero-initialize plane_out. - memset(plane_out, 0, sizeof(*plane_out)); - - has_type = false; - has_rotation = false; - has_hardcoded_rotation = false; - has_zpos = false; - has_hardcoded_zpos = false; - has_alpha = false; - has_blend_mode = false; - comitted_crtc_x = comitted_crtc_y = comitted_crtc_w = comitted_crtc_h = 0; - comitted_src_x = comitted_src_y = comitted_src_w = comitted_src_h = 0; - for (int j = 0; j < props->count_props; j++) { - info = drmModeGetProperty(drm_fd, props->props[j]); - if (info == NULL) { - ok = errno; - perror("[modesetting] Could not get DRM device planes' properties' info. drmModeGetProperty"); - goto fail_maybe_free_supported_modified_formats_blob; - } - - if (streq(info->name, "type")) { - assert(has_type == false); - has_type = true; - - type = props->prop_values[j]; - } else if (streq(info->name, "rotation")) { - assert(has_rotation == false); - has_rotation = true; - - supported_rotations = PLANE_TRANSFORM_NONE; - assert(info->flags & DRM_MODE_PROP_BITMASK); - - for (int k = 0; k < info->count_enums; k++) { - supported_rotations.u32 |= 1 << info->enums[k].value; - } - - assert(PLANE_TRANSFORM_IS_VALID(supported_rotations)); - - if (info->flags & DRM_MODE_PROP_IMMUTABLE) { - has_hardcoded_rotation = true; - hardcoded_rotation.u64 = props->prop_values[j]; - } - - committed_rotation.u64 = props->prop_values[j]; - } else if (streq(info->name, "zpos")) { - assert(has_zpos == false); - has_zpos = true; - - if (info->flags & DRM_MODE_PROP_SIGNED_RANGE) { - min_zpos = *(int64_t *) (info->values + 0); - max_zpos = *(int64_t *) (info->values + 1); - committed_zpos = *(int64_t *) (props->prop_values + j); - assert(min_zpos <= max_zpos); - assert(min_zpos <= committed_zpos); - assert(committed_zpos <= max_zpos); - } else if (info->flags & DRM_MODE_PROP_RANGE) { - assert(info->values[0] < (uint64_t) INT64_MAX); - assert(info->values[1] < (uint64_t) INT64_MAX); - min_zpos = info->values[0]; - max_zpos = info->values[1]; - committed_zpos = props->prop_values[j]; - assert(min_zpos <= max_zpos); - } else { - ASSERT_MSG(info->flags && false, "Invalid property type for zpos property."); - } - - if (info->flags & DRM_MODE_PROP_IMMUTABLE) { - has_hardcoded_zpos = true; - assert(props->prop_values[j] < (uint64_t) INT64_MAX); - hardcoded_zpos = committed_zpos; - if (min_zpos != max_zpos) { - LOG_DEBUG( - "DRM plane minimum supported zpos does not equal maximum supported zpos, even though zpos is " - "immutable.\n" - ); - min_zpos = max_zpos = hardcoded_zpos; - } - } - } else if (streq(info->name, "SRC_X")) { - comitted_src_x = props->prop_values[j]; - } else if (streq(info->name, "SRC_Y")) { - comitted_src_y = props->prop_values[j]; - } else if (streq(info->name, "SRC_W")) { - comitted_src_w = props->prop_values[j]; - } else if (streq(info->name, "SRC_H")) { - comitted_src_h = props->prop_values[j]; - } else if (streq(info->name, "CRTC_X")) { - comitted_crtc_x = props->prop_values[j]; - } else if (streq(info->name, "CRTC_Y")) { - comitted_crtc_y = props->prop_values[j]; - } else if (streq(info->name, "CRTC_W")) { - comitted_crtc_w = props->prop_values[j]; - } else if (streq(info->name, "CRTC_H")) { - comitted_crtc_h = props->prop_values[j]; - } else if (streq(info->name, "IN_FORMATS")) { - drmModePropertyBlobRes *blob; - - blob = drmModeGetPropertyBlob(drm_fd, props->prop_values[j]); - if (blob == NULL) { - ok = errno; - LOG_ERROR( - "Couldn't get list of supported format modifiers for plane %u. drmModeGetPropertyBlob: %s\n", - plane_id, - strerror(ok) - ); - drmModeFreeProperty(info); - goto fail_free_props; - } - - plane_out->supports_modifiers = true; - plane_out->supported_modified_formats_blob = memdup(blob->data, blob->length); - ASSERT_NOT_NULL(plane_out->supported_modified_formats_blob); - - drmModeFreePropertyBlob(blob); - } else if (streq(info->name, "alpha")) { - has_alpha = true; - assert(info->flags == DRM_MODE_PROP_RANGE); - assert(info->values[0] <= 0xFFFF); - assert(info->values[1] <= 0xFFFF); - assert(info->values[0] <= info->values[1]); - plane_out->min_alpha = (uint16_t) info->values[0]; - plane_out->max_alpha = (uint16_t) info->values[1]; - - uint64_t value = props->prop_values[j]; - assert(plane_out->min_alpha <= value); - assert(value <= plane_out->max_alpha); - committed_alpha = (uint16_t) value; - } else if (streq(info->name, "pixel blend mode")) { - has_blend_mode = true; - assert(info->flags == DRM_MODE_PROP_ENUM); - - for (int i = 0; i < info->count_enums; i++) { - if (streq(info->enums[i].name, "None")) { - ASSERT_EQUALS(info->enums[i].value, kNone_DrmBlendMode); - supported_blend_modes[kNone_DrmBlendMode] = true; - } else if (streq(info->enums[i].name, "Pre-multiplied")) { - ASSERT_EQUALS(info->enums[i].value, kPremultiplied_DrmBlendMode); - supported_blend_modes[kPremultiplied_DrmBlendMode] = true; - } else if (streq(info->enums[i].name, "Coverage")) { - ASSERT_EQUALS(info->enums[i].value, kCoverage_DrmBlendMode); - supported_blend_modes[kCoverage_DrmBlendMode] = true; - } else { - LOG_DEBUG( - "Unknown KMS pixel blend mode: %s (value: %" PRIu64 ")\n", - info->enums[i].name, - (uint64_t) info->enums[i].value - ); - } - } - - committed_blend_mode = props->prop_values[j]; - assert(committed_blend_mode >= 0 && committed_blend_mode <= kMax_DrmBlendMode); - assert(supported_blend_modes[committed_blend_mode]); - } - -#define CHECK_ASSIGN_PROPERTY_ID(_name_str, _name) \ - if (strncmp(info->name, _name_str, ARRAY_SIZE(info->name)) == 0) { \ - ids._name = info->prop_id; \ - } else - - DRM_PLANE_PROPERTIES(CHECK_ASSIGN_PROPERTY_ID) { - // do nothing - } - -#undef CHECK_ASSIGN_PROPERTY_ID - - drmModeFreeProperty(info); - } - - assert(has_type); - (void) has_type; - - for (int i = 0; i < plane->count_formats; i++) { - for (int j = 0; j < PIXFMT_COUNT; j++) { - if (get_pixfmt_info(j)->drm_format == plane->formats[i]) { - supported_formats[j] = true; - break; - } - } - } - - bool has_format = false; - enum pixfmt format = PIXFMT_RGB565; - - // drmModeGetFB2 might not be present. - // If __attribute__((weak)) is supported by the compiler, we redefine it as - // weak above. - // If we don't have weak, we can't check for it here. -#ifdef HAVE_WEAK_DRM_MODE_GET_FB2 - if (drmModeGetFB2 && drmModeFreeFB2) { - struct drm_mode_fb2 *fb = (struct drm_mode_fb2 *) drmModeGetFB2(drm_fd, plane->fb_id); - if (fb != NULL) { - for (int i = 0; i < PIXFMT_COUNT; i++) { - if (get_pixfmt_info(i)->drm_format == fb->pixel_format) { - has_format = true; - format = i; - break; - } - } - - drmModeFreeFB2((struct _drmModeFB2 *) fb); - } - } -#endif - - plane_out->id = plane->plane_id; - plane_out->possible_crtcs = plane->possible_crtcs; - plane_out->ids = ids; - plane_out->type = has_type ? type : kPrimary_DrmPlaneType; - plane_out->has_zpos = has_zpos; - plane_out->min_zpos = has_zpos ? min_zpos : 0; - plane_out->max_zpos = has_zpos ? max_zpos : 0; - plane_out->has_hardcoded_zpos = has_hardcoded_zpos; - plane_out->hardcoded_zpos = has_hardcoded_zpos ? hardcoded_zpos : 0; - plane_out->has_rotation = has_rotation; - plane_out->supported_rotations = supported_rotations; - plane_out->has_hardcoded_rotation = has_hardcoded_rotation; - plane_out->hardcoded_rotation = hardcoded_rotation; - memcpy(plane_out->supported_formats, supported_formats, sizeof supported_formats); - plane_out->has_alpha = has_alpha; - plane_out->has_blend_mode = has_blend_mode; - memcpy(plane_out->supported_blend_modes, supported_blend_modes, sizeof supported_blend_modes); - plane_out->committed_state.crtc_id = plane->crtc_id; - plane_out->committed_state.fb_id = plane->fb_id; - plane_out->committed_state.src_x = comitted_src_x; - plane_out->committed_state.src_y = comitted_src_y; - plane_out->committed_state.src_w = comitted_src_w; - plane_out->committed_state.src_h = comitted_src_h; - plane_out->committed_state.crtc_x = comitted_crtc_x; - plane_out->committed_state.crtc_y = comitted_crtc_y; - plane_out->committed_state.crtc_w = comitted_crtc_w; - plane_out->committed_state.crtc_h = comitted_crtc_h; - plane_out->committed_state.zpos = committed_zpos; - plane_out->committed_state.rotation = committed_rotation; - plane_out->committed_state.alpha = committed_alpha; - plane_out->committed_state.blend_mode = committed_blend_mode; - plane_out->committed_state.has_format = has_format; - plane_out->committed_state.format = format; - drmModeFreeObjectProperties(props); - drmModeFreePlane(plane); - return 0; - -fail_maybe_free_supported_modified_formats_blob: - if (plane_out->supported_modified_formats_blob != NULL) - free(plane_out->supported_modified_formats_blob); - -fail_free_props: - drmModeFreeObjectProperties(props); - -fail_free_plane: - drmModeFreePlane(plane); - return ok; -} - -static void free_plane(UNUSED struct drm_plane *plane) { - if (plane->supported_modified_formats_blob != NULL) { - free(plane->supported_modified_formats_blob); - } -} - -static int fetch_planes(struct drmdev *drmdev, struct drm_plane **planes_out, size_t *n_planes_out) { - struct drm_plane *planes; - int ok; - - planes = calloc(drmdev->plane_res->count_planes, sizeof *planes); - if (planes == NULL) { - *planes_out = NULL; - return ENOMEM; - } - - for (int i = 0; i < drmdev->plane_res->count_planes; i++) { - ok = fetch_plane(drmdev->fd, drmdev->plane_res->planes[i], planes + i); - if (ok != 0) { - for (int j = 0; j < i; j++) { - free_plane(planes + j); - } - free(planes); - return ENOMEM; - } - - ASSERT_MSG(planes[0].has_zpos == planes[i].has_zpos, "If one plane has a zpos property, all planes need to have one."); - } - - *planes_out = planes; - *n_planes_out = drmdev->plane_res->count_planes; - - return 0; -} - -static void free_planes(struct drm_plane *planes, size_t n_planes) { - for (int i = 0; i < n_planes; i++) { - free_plane(planes + i); - } - free(planes); -} - -static void assert_rotations_work(void) { - assert(PLANE_TRANSFORM_ROTATE_0.rotate_0 == true); - assert(PLANE_TRANSFORM_ROTATE_0.rotate_90 == false); - assert(PLANE_TRANSFORM_ROTATE_0.rotate_180 == false); - assert(PLANE_TRANSFORM_ROTATE_0.rotate_270 == false); - assert(PLANE_TRANSFORM_ROTATE_0.reflect_x == false); - assert(PLANE_TRANSFORM_ROTATE_0.reflect_y == false); - - assert(PLANE_TRANSFORM_ROTATE_90.rotate_0 == false); - assert(PLANE_TRANSFORM_ROTATE_90.rotate_90 == true); - assert(PLANE_TRANSFORM_ROTATE_90.rotate_180 == false); - assert(PLANE_TRANSFORM_ROTATE_90.rotate_270 == false); - assert(PLANE_TRANSFORM_ROTATE_90.reflect_x == false); - assert(PLANE_TRANSFORM_ROTATE_90.reflect_y == false); - - assert(PLANE_TRANSFORM_ROTATE_180.rotate_0 == false); - assert(PLANE_TRANSFORM_ROTATE_180.rotate_90 == false); - assert(PLANE_TRANSFORM_ROTATE_180.rotate_180 == true); - assert(PLANE_TRANSFORM_ROTATE_180.rotate_270 == false); - assert(PLANE_TRANSFORM_ROTATE_180.reflect_x == false); - assert(PLANE_TRANSFORM_ROTATE_180.reflect_y == false); - - assert(PLANE_TRANSFORM_ROTATE_270.rotate_0 == false); - assert(PLANE_TRANSFORM_ROTATE_270.rotate_90 == false); - assert(PLANE_TRANSFORM_ROTATE_270.rotate_180 == false); - assert(PLANE_TRANSFORM_ROTATE_270.rotate_270 == true); - assert(PLANE_TRANSFORM_ROTATE_270.reflect_x == false); - assert(PLANE_TRANSFORM_ROTATE_270.reflect_y == false); - - assert(PLANE_TRANSFORM_REFLECT_X.rotate_0 == false); - assert(PLANE_TRANSFORM_REFLECT_X.rotate_90 == false); - assert(PLANE_TRANSFORM_REFLECT_X.rotate_180 == false); - assert(PLANE_TRANSFORM_REFLECT_X.rotate_270 == false); - assert(PLANE_TRANSFORM_REFLECT_X.reflect_x == true); - assert(PLANE_TRANSFORM_REFLECT_X.reflect_y == false); - - assert(PLANE_TRANSFORM_REFLECT_Y.rotate_0 == false); - assert(PLANE_TRANSFORM_REFLECT_Y.rotate_90 == false); - assert(PLANE_TRANSFORM_REFLECT_Y.rotate_180 == false); - assert(PLANE_TRANSFORM_REFLECT_Y.rotate_270 == false); - assert(PLANE_TRANSFORM_REFLECT_Y.reflect_x == false); - assert(PLANE_TRANSFORM_REFLECT_Y.reflect_y == true); - - drm_plane_transform_t r = PLANE_TRANSFORM_NONE; - - r.rotate_0 = true; - r.reflect_x = true; - assert(r.u32 == (DRM_MODE_ROTATE_0 | DRM_MODE_REFLECT_X)); - - r.u32 = DRM_MODE_ROTATE_90 | DRM_MODE_REFLECT_Y; - assert(r.rotate_0 == false); - assert(r.rotate_90 == true); - assert(r.rotate_180 == false); - assert(r.rotate_270 == false); - assert(r.reflect_x == false); - assert(r.reflect_y == true); - (void) r; -} - -static int set_drm_client_caps(int fd, bool *supports_atomic_modesetting) { - int ok; - - ok = drmSetClientCap(fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1); - if (ok < 0) { - ok = errno; - LOG_ERROR("Could not set DRM client universal planes capable. drmSetClientCap: %s\n", strerror(ok)); - return ok; - } - -#ifdef USE_LEGACY_KMS - *supports_atomic_modesetting = false; -#else - ok = drmSetClientCap(fd, DRM_CLIENT_CAP_ATOMIC, 1); - if ((ok < 0) && (errno == EOPNOTSUPP)) { - if (supports_atomic_modesetting != NULL) { - *supports_atomic_modesetting = false; - } - } else if (ok < 0) { - ok = errno; - LOG_ERROR("Could not set DRM client atomic capable. drmSetClientCap: %s\n", strerror(ok)); - return ok; - } else { - if (supports_atomic_modesetting != NULL) { - *supports_atomic_modesetting = true; - } - } -#endif - - return 0; -} - -struct drmdev *drmdev_new_from_interface_fd(int fd, void *fd_metadata, const struct drmdev_interface *interface, void *userdata) { - struct gbm_device *gbm_device; - struct drmdev *drmdev; - uint64_t cap; - bool supports_atomic_modesetting; - bool supports_dumb_buffers; - int ok, master_fd, event_fd; - - assert_rotations_work(); - - drmdev = malloc(sizeof *drmdev); - if (drmdev == NULL) { - return NULL; - } - - master_fd = fd; - - ok = set_drm_client_caps(fd, &supports_atomic_modesetting); - if (ok != 0) { - goto fail_free_drmdev; - } - - cap = 0; - ok = drmGetCap(fd, DRM_CAP_DUMB_BUFFER, &cap); - if (ok < 0) { - supports_dumb_buffers = false; - } else { - supports_dumb_buffers = !!cap; - } - - drmdev->res = drmModeGetResources(fd); - if (drmdev->res == NULL) { - ok = errno; - LOG_ERROR("Could not get DRM device resources. drmModeGetResources: %s\n", strerror(ok)); - goto fail_free_drmdev; - } - - drmdev->plane_res = drmModeGetPlaneResources(fd); - if (drmdev->plane_res == NULL) { - ok = errno; - LOG_ERROR("Could not get DRM device planes resources. drmModeGetPlaneResources: %s\n", strerror(ok)); - goto fail_free_resources; - } - - drmdev->fd = fd; - - ok = fetch_connectors(drmdev, &drmdev->connectors, &drmdev->n_connectors); - if (ok != 0) { - goto fail_free_plane_resources; - } - - ok = fetch_encoders(drmdev, &drmdev->encoders, &drmdev->n_encoders); - if (ok != 0) { - goto fail_free_connectors; - } - - ok = fetch_crtcs(drmdev, &drmdev->crtcs, &drmdev->n_crtcs); - if (ok != 0) { - goto fail_free_encoders; - } - - ok = fetch_planes(drmdev, &drmdev->planes, &drmdev->n_planes); - if (ok != 0) { - goto fail_free_crtcs; - } - - // Rockchip driver always wants the N-th primary/cursor plane to be associated with the N-th CRTC. - // If we don't respect this, commits will work but not actually show anything on screen. - int primary_plane_index = 0; - int cursor_plane_index = 0; - for (int i = 0; i < drmdev->n_planes; i++) { - if (drmdev->planes[i].type == DRM_PLANE_TYPE_PRIMARY) { - if ((drmdev->planes[i].possible_crtcs & (1 << primary_plane_index)) != 0) { - drmdev->planes[i].possible_crtcs = (1 << primary_plane_index); - } else { - LOG_DEBUG("Primary plane %d does not support CRTC %d.\n", primary_plane_index, primary_plane_index); - } - - primary_plane_index++; - } else if (drmdev->planes[i].type == DRM_PLANE_TYPE_CURSOR) { - if ((drmdev->planes[i].possible_crtcs & (1 << cursor_plane_index)) != 0) { - drmdev->planes[i].possible_crtcs = (1 << cursor_plane_index); - } else { - LOG_DEBUG("Cursor plane %d does not support CRTC %d.\n", cursor_plane_index, cursor_plane_index); - } - - cursor_plane_index++; - } - } - - gbm_device = gbm_create_device(drmdev->fd); - if (gbm_device == NULL) { - LOG_ERROR("Could not create GBM device.\n"); - goto fail_free_planes; - } - - event_fd = epoll_create1(EPOLL_CLOEXEC); - if (event_fd < 0) { - LOG_ERROR("Could not create modesetting epoll instance.\n"); - goto fail_destroy_gbm_device; - } - - ok = epoll_ctl(event_fd, EPOLL_CTL_ADD, fd, &(struct epoll_event){ .events = EPOLLIN | EPOLLPRI, .data.ptr = NULL }); - if (ok != 0) { - LOG_ERROR("Could not add DRM file descriptor to epoll instance.\n"); - goto fail_close_event_fd; - } - - pthread_mutex_init(&drmdev->mutex, get_default_mutex_attrs()); - drmdev->n_refs = REFCOUNT_INIT_1; - drmdev->fd = fd; - drmdev->supports_atomic_modesetting = supports_atomic_modesetting; - drmdev->supports_dumb_buffers = supports_dumb_buffers; - drmdev->gbm_device = gbm_device; - drmdev->event_fd = event_fd; - memset(drmdev->per_crtc_state, 0, sizeof(drmdev->per_crtc_state)); - drmdev->master_fd = master_fd; - drmdev->master_fd_metadata = fd_metadata; - drmdev->interface = *interface; - drmdev->userdata = userdata; - list_inithead(&drmdev->fbs); - return drmdev; - -fail_close_event_fd: - close(event_fd); - -fail_destroy_gbm_device: - gbm_device_destroy(gbm_device); - -fail_free_planes: - free_planes(drmdev->planes, drmdev->n_planes); - -fail_free_crtcs: - free_crtcs(drmdev->crtcs, drmdev->n_crtcs); - -fail_free_encoders: - free_encoders(drmdev->encoders, drmdev->n_encoders); - -fail_free_connectors: - free_connectors(drmdev->connectors, drmdev->n_connectors); - -fail_free_plane_resources: - drmModeFreePlaneResources(drmdev->plane_res); - -fail_free_resources: - drmModeFreeResources(drmdev->res); - - // fail_close_master_fd: - // interface->close(master_fd, NULL, userdata); - -fail_free_drmdev: - free(drmdev); - - return NULL; -} - -struct drmdev *drmdev_new_from_path(const char *path, const struct drmdev_interface *interface, void *userdata) { - struct drmdev *drmdev; - void *fd_metadata; - int fd; - - ASSERT_NOT_NULL(path); - ASSERT_NOT_NULL(interface); - - fd = interface->open(path, O_RDWR, &fd_metadata, userdata); - if (fd < 0) { - LOG_ERROR("Could not open DRM device. interface->open: %s\n", strerror(errno)); - return NULL; - } - - drmdev = drmdev_new_from_interface_fd(fd, fd_metadata, interface, userdata); - if (drmdev == NULL) { - close(fd); - return NULL; - } - - return drmdev; -} - -static void drmdev_destroy(struct drmdev *drmdev) { - assert(refcount_is_zero(&drmdev->n_refs)); - - drmdev->interface.close(drmdev->master_fd, drmdev->master_fd_metadata, drmdev->userdata); - close(drmdev->event_fd); - gbm_device_destroy(drmdev->gbm_device); - free_planes(drmdev->planes, drmdev->n_planes); - free_crtcs(drmdev->crtcs, drmdev->n_crtcs); - free_encoders(drmdev->encoders, drmdev->n_encoders); - free_connectors(drmdev->connectors, drmdev->n_connectors); - drmModeFreePlaneResources(drmdev->plane_res); - drmModeFreeResources(drmdev->res); - free(drmdev); -} - -DEFINE_REF_OPS(drmdev, n_refs) - -int drmdev_get_fd(struct drmdev *drmdev) { - ASSERT_NOT_NULL(drmdev); - return drmdev->master_fd; -} - -int drmdev_get_event_fd(struct drmdev *drmdev) { - ASSERT_NOT_NULL(drmdev); - return drmdev->master_fd; -} - -bool drmdev_supports_dumb_buffers(struct drmdev *drmdev) { - return drmdev->supports_dumb_buffers; -} - -int drmdev_create_dumb_buffer( - struct drmdev *drmdev, - int width, - int height, - int bpp, - uint32_t *gem_handle_out, - uint32_t *pitch_out, - size_t *size_out -) { - struct drm_mode_create_dumb create_req; - int ok; - - ASSERT_NOT_NULL(drmdev); - ASSERT_NOT_NULL(gem_handle_out); - ASSERT_NOT_NULL(pitch_out); - ASSERT_NOT_NULL(size_out); - - memset(&create_req, 0, sizeof create_req); - create_req.width = width; - create_req.height = height; - create_req.bpp = bpp; - create_req.flags = 0; - - ok = ioctl(drmdev->fd, DRM_IOCTL_MODE_CREATE_DUMB, &create_req); - if (ok < 0) { - ok = errno; - LOG_ERROR("Could not create dumb buffer. ioctl: %s\n", strerror(errno)); - goto fail_return_ok; - } - - *gem_handle_out = create_req.handle; - *pitch_out = create_req.pitch; - *size_out = create_req.size; - return 0; - -fail_return_ok: - return ok; -} - -void drmdev_destroy_dumb_buffer(struct drmdev *drmdev, uint32_t gem_handle) { - struct drm_mode_destroy_dumb destroy_req; - int ok; - - ASSERT_NOT_NULL(drmdev); - - memset(&destroy_req, 0, sizeof destroy_req); - destroy_req.handle = gem_handle; - - ok = ioctl(drmdev->fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy_req); - if (ok < 0) { - LOG_ERROR("Could not destroy dumb buffer. ioctl: %s\n", strerror(errno)); - } -} - -void *drmdev_map_dumb_buffer(struct drmdev *drmdev, uint32_t gem_handle, size_t size) { - struct drm_mode_map_dumb map_req; - void *map; - int ok; - - ASSERT_NOT_NULL(drmdev); - - memset(&map_req, 0, sizeof map_req); - map_req.handle = gem_handle; - - ok = ioctl(drmdev->fd, DRM_IOCTL_MODE_MAP_DUMB, &map_req); - if (ok < 0) { - LOG_ERROR("Could not prepare dumb buffer mmap. ioctl: %s\n", strerror(errno)); - return NULL; - } - - map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, drmdev->fd, map_req.offset); - if (map == MAP_FAILED) { - LOG_ERROR("Could not mmap dumb buffer. mmap: %s\n", strerror(errno)); - return NULL; - } - - return map; -} - -void drmdev_unmap_dumb_buffer(struct drmdev *drmdev, void *map, size_t size) { - int ok; - - ASSERT_NOT_NULL(drmdev); - ASSERT_NOT_NULL(map); - (void) drmdev; - - ok = munmap(map, size); - if (ok < 0) { - LOG_ERROR("Couldn't unmap dumb buffer. munmap: %s\n", strerror(errno)); - } -} - -static void -drmdev_on_page_flip_locked(int fd, unsigned int sequence, unsigned int tv_sec, unsigned int tv_usec, unsigned int crtc_id, void *userdata) { - struct kms_req_builder *builder; - struct drm_crtc *crtc; - struct kms_req **last_flipped; - struct kms_req *req; - struct drmdev *drmdev; - - ASSERT_NOT_NULL(userdata); - builder = userdata; - req = userdata; - - (void) fd; - (void) sequence; - (void) crtc_id; - - drmdev = builder->drmdev; - for_each_crtc_in_drmdev(drmdev, crtc) { - if (crtc->id == crtc_id) { - break; - } - } - - ASSERT_NOT_NULL_MSG(crtc, "Invalid CRTC id"); - - if (drmdev->per_crtc_state[crtc->index].scanout_callback != NULL) { - uint64_t vblank_ns = tv_sec * 1000000000ull + tv_usec * 1000ull; - drmdev->per_crtc_state[crtc->index].scanout_callback(drmdev, vblank_ns, drmdev->per_crtc_state[crtc->index].userdata); - - // clear the scanout callback - drmdev->per_crtc_state[crtc->index].scanout_callback = NULL; - drmdev->per_crtc_state[crtc->index].destroy_callback = NULL; - drmdev->per_crtc_state[crtc->index].userdata = NULL; - } - - last_flipped = &drmdev->per_crtc_state[crtc->index].last_flipped; - if (*last_flipped != NULL) { - /// TODO: Remove this if we ever cache KMS reqs. - /// FIXME: This will fail if we're using blocking commits. - // assert(refcount_is_one(&((struct kms_req_builder*) *last_flipped)->n_refs)); - } - - kms_req_swap_ptrs(last_flipped, req); - kms_req_unref(req); -} - -static int drmdev_on_modesetting_fd_ready_locked(struct drmdev *drmdev) { - int ok; - - static drmEventContext ctx = { - .version = DRM_EVENT_CONTEXT_VERSION, - .vblank_handler = NULL, - .page_flip_handler = NULL, - .page_flip_handler2 = drmdev_on_page_flip_locked, - .sequence_handler = NULL, - }; - - ok = drmHandleEvent(drmdev->master_fd, &ctx); - if (ok != 0) { - return EIO; - } - - return 0; -} - -int drmdev_on_event_fd_ready(struct drmdev *drmdev) { - struct epoll_event events[16]; - int ok, n_events; - - ASSERT_NOT_NULL(drmdev); - - drmdev_lock(drmdev); - - while (1) { - ok = epoll_wait(drmdev->event_fd, events, ARRAY_SIZE(events), 0); - if ((ok < 0) && (errno == EINTR)) { - // retry - continue; - } else if (ok < 0) { - ok = errno; - LOG_ERROR("Could read kernel modesetting events. epoll_wait: %s\n", strerror(ok)); - goto fail_unlock; - } else { - break; - } - } - - n_events = ok; - for (int i = 0; i < n_events; i++) { - // currently this could only be the root drmdev fd. - ASSERT_EQUALS(events[i].data.ptr, NULL); - ok = drmdev_on_modesetting_fd_ready_locked(drmdev); - if (ok != 0) { - goto fail_unlock; - } - } - - drmdev_unlock(drmdev); - - return 0; - -fail_unlock: - drmdev_unlock(drmdev); - return ok; -} - -struct gbm_device *drmdev_get_gbm_device(struct drmdev *drmdev) { - ASSERT_NOT_NULL(drmdev); - return drmdev->gbm_device; -} - -int drmdev_get_last_vblank_locked(struct drmdev *drmdev, uint32_t crtc_id, uint64_t *last_vblank_ns_out) { - int ok; - - ASSERT_NOT_NULL(drmdev); - ASSERT_NOT_NULL(last_vblank_ns_out); - - ok = drmCrtcGetSequence(drmdev->fd, crtc_id, NULL, last_vblank_ns_out); - if (ok < 0) { - ok = errno; - LOG_ERROR("Could not get next vblank timestamp. drmCrtcGetSequence: %s\n", strerror(ok)); - return ok; - } - - return 0; -} - -int drmdev_get_last_vblank(struct drmdev *drmdev, uint32_t crtc_id, uint64_t *last_vblank_ns_out) { - int ok; - - drmdev_lock(drmdev); - ok = drmdev_get_last_vblank_locked(drmdev, crtc_id, last_vblank_ns_out); - drmdev_unlock(drmdev); - - return ok; -} - -uint32_t drmdev_add_fb_multiplanar_locked( - struct drmdev *drmdev, - uint32_t width, - uint32_t height, - enum pixfmt pixel_format, - const uint32_t bo_handles[4], - const uint32_t pitches[4], - const uint32_t offsets[4], - bool has_modifiers, - const uint64_t modifiers[4] -) { - struct drm_fb *fb; - uint32_t fb_id; - int ok; - - /// TODO: Code in https://elixir.bootlin.com/linux/latest/source/drivers/gpu/drm/drm_framebuffer.c#L257 - /// assumes handles, pitches, offsets and modifiers for unused planes are zero. Make sure that's the - /// case here. - ASSERT_NOT_NULL(drmdev); - assert(width > 0 && height > 0); - assert(bo_handles[0] != 0); - assert(pitches[0] != 0); - - fb = malloc(sizeof *fb); - if (fb == NULL) { - return 0; - } - - list_inithead(&fb->entry); - fb->id = 0; - fb->width = width; - fb->height = height; - fb->format = pixel_format; - fb->has_modifier = has_modifiers; - fb->modifier = modifiers[0]; - fb->flags = 0; - memcpy(fb->handles, bo_handles, sizeof(fb->handles)); - memcpy(fb->pitches, pitches, sizeof(fb->pitches)); - memcpy(fb->offsets, offsets, sizeof(fb->offsets)); - - fb_id = 0; - if (has_modifiers) { - ok = drmModeAddFB2WithModifiers( - drmdev->fd, - width, - height, - get_pixfmt_info(pixel_format)->drm_format, - bo_handles, - pitches, - offsets, - modifiers, - &fb_id, - DRM_MODE_FB_MODIFIERS - ); - if (ok < 0) { - LOG_ERROR("Couldn't add buffer as DRM fb. drmModeAddFB2WithModifiers: %s\n", strerror(-ok)); - goto fail_free_fb; - } - } else { - ok = drmModeAddFB2(drmdev->fd, width, height, get_pixfmt_info(pixel_format)->drm_format, bo_handles, pitches, offsets, &fb_id, 0); - if (ok < 0) { - LOG_ERROR("Couldn't add buffer as DRM fb. drmModeAddFB2: %s\n", strerror(-ok)); - goto fail_free_fb; - } - } - - fb->id = fb_id; - list_add(&fb->entry, &drmdev->fbs); - - assert(fb_id != 0); - return fb_id; - -fail_free_fb: - free(fb); - return 0; -} - -uint32_t drmdev_add_fb_multiplanar( - struct drmdev *drmdev, - uint32_t width, - uint32_t height, - enum pixfmt pixel_format, - const uint32_t bo_handles[4], - const uint32_t pitches[4], - const uint32_t offsets[4], - bool has_modifiers, - const uint64_t modifiers[4] -) { - uint32_t fb; - - drmdev_lock(drmdev); - - fb = drmdev_add_fb_multiplanar_locked(drmdev, width, height, pixel_format, bo_handles, pitches, offsets, has_modifiers, modifiers); - - drmdev_unlock(drmdev); - - return fb; -} - -uint32_t drmdev_add_fb_locked( - struct drmdev *drmdev, - uint32_t width, - uint32_t height, - enum pixfmt pixel_format, - uint32_t bo_handle, - uint32_t pitch, - uint32_t offset, - bool has_modifier, - uint64_t modifier -) { - return drmdev_add_fb_multiplanar_locked( - drmdev, - width, - height, - pixel_format, - (uint32_t[4]){ bo_handle, 0 }, - (uint32_t[4]){ pitch, 0 }, - (uint32_t[4]){ offset, 0 }, - has_modifier, - (const uint64_t[4]){ modifier, 0 } - ); -} - -uint32_t drmdev_add_fb( - struct drmdev *drmdev, - uint32_t width, - uint32_t height, - enum pixfmt pixel_format, - uint32_t bo_handle, - uint32_t pitch, - uint32_t offset, - bool has_modifier, - uint64_t modifier -) { - return drmdev_add_fb_multiplanar( - drmdev, - width, - height, - pixel_format, - (uint32_t[4]){ bo_handle, 0 }, - (uint32_t[4]){ pitch, 0 }, - (uint32_t[4]){ offset, 0 }, - has_modifier, - (const uint64_t[4]){ modifier, 0 } - ); -} - -uint32_t drmdev_add_fb_from_dmabuf_locked( - struct drmdev *drmdev, - uint32_t width, - uint32_t height, - enum pixfmt pixel_format, - int prime_fd, - uint32_t pitch, - uint32_t offset, - bool has_modifier, - uint64_t modifier -) { - uint32_t bo_handle; - int ok; - - ok = drmPrimeFDToHandle(drmdev->fd, prime_fd, &bo_handle); - if (ok < 0) { - LOG_ERROR("Couldn't import DMA-buffer as GEM buffer. drmPrimeFDToHandle: %s\n", strerror(errno)); - return 0; - } - - return drmdev_add_fb_locked(drmdev, width, height, pixel_format, prime_fd, pitch, offset, has_modifier, modifier); -} - -uint32_t drmdev_add_fb_from_dmabuf( - struct drmdev *drmdev, - uint32_t width, - uint32_t height, - enum pixfmt pixel_format, - int prime_fd, - uint32_t pitch, - uint32_t offset, - bool has_modifier, - uint64_t modifier -) { - uint32_t fb; - - drmdev_lock(drmdev); - - fb = drmdev_add_fb_from_dmabuf_locked(drmdev, width, height, pixel_format, prime_fd, pitch, offset, has_modifier, modifier); - - drmdev_unlock(drmdev); - - return fb; -} - -uint32_t drmdev_add_fb_from_dmabuf_multiplanar_locked( - struct drmdev *drmdev, - uint32_t width, - uint32_t height, - enum pixfmt pixel_format, - const int prime_fds[4], - const uint32_t pitches[4], - const uint32_t offsets[4], - bool has_modifiers, - const uint64_t modifiers[4] -) { - uint32_t bo_handles[4] = { 0 }; - int ok; - - for (int i = 0; (i < 4) && (prime_fds[i] != 0); i++) { - ok = drmPrimeFDToHandle(drmdev->fd, prime_fds[i], bo_handles + i); - if (ok < 0) { - LOG_ERROR("Couldn't import DMA-buffer as GEM buffer. drmPrimeFDToHandle: %s\n", strerror(errno)); - return 0; - } - } - - return drmdev_add_fb_multiplanar_locked(drmdev, width, height, pixel_format, bo_handles, pitches, offsets, has_modifiers, modifiers); -} - -uint32_t drmdev_add_fb_from_dmabuf_multiplanar( - struct drmdev *drmdev, - uint32_t width, - uint32_t height, - enum pixfmt pixel_format, - const int prime_fds[4], - const uint32_t pitches[4], - const uint32_t offsets[4], - bool has_modifiers, - const uint64_t modifiers[4] -) { - uint32_t fb; - - drmdev_lock(drmdev); - - fb = drmdev_add_fb_from_dmabuf_multiplanar_locked( - drmdev, - width, - height, - pixel_format, - prime_fds, - pitches, - offsets, - has_modifiers, - modifiers - ); - - drmdev_unlock(drmdev); - - return fb; -} - -uint32_t drmdev_add_fb_from_gbm_bo_locked(struct drmdev *drmdev, struct gbm_bo *bo, bool cast_opaque) { - enum pixfmt format; - uint32_t fourcc; - int n_planes; - - n_planes = gbm_bo_get_plane_count(bo); - ASSERT(0 <= n_planes && n_planes <= 4); - - fourcc = gbm_bo_get_format(bo); - - if (!has_pixfmt_for_gbm_format(fourcc)) { - LOG_ERROR("GBM pixel format is not supported.\n"); - return 0; - } - - format = get_pixfmt_for_gbm_format(fourcc); - - if (cast_opaque) { - format = pixfmt_opaque(format); - } - - uint32_t handles[4]; - uint32_t pitches[4]; - - // Returns DRM_FORMAT_MOD_INVALID on failure, or DRM_FORMAT_MOD_LINEAR - // for dumb buffers. - uint64_t modifier = gbm_bo_get_modifier(bo); - bool has_modifiers = modifier != DRM_FORMAT_MOD_INVALID; - - for (int i = 0; i < n_planes; i++) { - // gbm_bo_get_handle_for_plane will return -1 (in gbm_bo_handle.s32) and - // set errno on failure. - errno = 0; - union gbm_bo_handle handle = gbm_bo_get_handle_for_plane(bo, i); - if (handle.s32 == -1) { - LOG_ERROR("Could not get GEM handle for plane %d: %s\n", i, strerror(errno)); - return 0; - } - - handles[i] = handle.u32; - - // gbm_bo_get_stride_for_plane will return 0 and set errno on failure. - errno = 0; - uint32_t pitch = gbm_bo_get_stride_for_plane(bo, i); - if (pitch == 0 && errno != 0) { - LOG_ERROR("Could not get framebuffer stride for plane %d: %s\n", i, strerror(errno)); - return 0; - } - - pitches[i] = pitch; - } - - for (int i = n_planes; i < 4; i++) { - handles[i] = 0; - pitches[i] = 0; - } - - return drmdev_add_fb_multiplanar_locked( - drmdev, - gbm_bo_get_width(bo), - gbm_bo_get_height(bo), - format, - handles, - pitches, - (uint32_t[4]){ - n_planes >= 1 ? gbm_bo_get_offset(bo, 0) : 0, - n_planes >= 2 ? gbm_bo_get_offset(bo, 1) : 0, - n_planes >= 3 ? gbm_bo_get_offset(bo, 2) : 0, - n_planes >= 4 ? gbm_bo_get_offset(bo, 3) : 0, - }, - has_modifiers, - (uint64_t[4]){ - n_planes >= 1 ? modifier : 0, - n_planes >= 2 ? modifier : 0, - n_planes >= 3 ? modifier : 0, - n_planes >= 4 ? modifier : 0, - } - ); -} - -uint32_t drmdev_add_fb_from_gbm_bo(struct drmdev *drmdev, struct gbm_bo *bo, bool cast_opaque) { - uint32_t fb; - - drmdev_lock(drmdev); - - fb = drmdev_add_fb_from_gbm_bo_locked(drmdev, bo, cast_opaque); - - drmdev_unlock(drmdev); - - return fb; -} - -int drmdev_rm_fb_locked(struct drmdev *drmdev, uint32_t fb_id) { - int ok; - - list_for_each_entry(struct drm_fb, fb, &drmdev->fbs, entry) { - if (fb->id == fb_id) { - list_del(&fb->entry); - free(fb); - break; - } - } - - ok = drmModeRmFB(drmdev->fd, fb_id); - if (ok < 0) { - ok = -ok; - LOG_ERROR("Could not remove DRM framebuffer. drmModeRmFB: %s\n", strerror(ok)); - return ok; - } - - return 0; -} - -int drmdev_rm_fb(struct drmdev *drmdev, uint32_t fb_id) { - int ok; - - drmdev_lock(drmdev); - - ok = drmdev_rm_fb_locked(drmdev, fb_id); - - drmdev_unlock(drmdev); - - return ok; -} - -bool drmdev_can_modeset(struct drmdev *drmdev) { - bool can_modeset; - - ASSERT_NOT_NULL(drmdev); - - drmdev_lock(drmdev); - - can_modeset = drmdev->master_fd > 0; - - drmdev_unlock(drmdev); - - return can_modeset; -} - -void drmdev_suspend(struct drmdev *drmdev) { - ASSERT_NOT_NULL(drmdev); - - drmdev_lock(drmdev); - - if (drmdev->master_fd <= 0) { - LOG_ERROR("drmdev_suspend was called, but drmdev is already suspended\n"); - drmdev_unlock(drmdev); - return; - } - - drmdev->interface.close(drmdev->master_fd, drmdev->master_fd_metadata, drmdev->userdata); - drmdev->master_fd = -1; - drmdev->master_fd_metadata = NULL; - - drmdev_unlock(drmdev); -} - -int drmdev_resume(struct drmdev *drmdev) { - drmDevicePtr device; - void *fd_metadata; - int ok, master_fd; - - ASSERT_NOT_NULL(drmdev); - - drmdev_lock(drmdev); - - if (drmdev->master_fd > 0) { - ok = EINVAL; - LOG_ERROR("drmdev_resume was called, but drmdev is already resumed\n"); - goto fail_unlock; - } - - ok = drmGetDevice(drmdev->fd, &device); - if (ok < 0) { - ok = errno; - LOG_ERROR("Couldn't query DRM device info. drmGetDevice: %s\n", strerror(ok)); - goto fail_unlock; - } - - ok = drmdev->interface.open(device->nodes[DRM_NODE_PRIMARY], O_CLOEXEC | O_NONBLOCK, &fd_metadata, drmdev->userdata); - if (ok < 0) { - ok = -ok; - LOG_ERROR("Couldn't open DRM device.\n"); - goto fail_free_device; - } - - master_fd = ok; - - drmFreeDevice(&device); - - ok = set_drm_client_caps(master_fd, NULL); - if (ok != 0) { - goto fail_close_device; - } - - drmdev->master_fd = master_fd; - drmdev->master_fd_metadata = fd_metadata; - drmdev_unlock(drmdev); - return 0; - -fail_close_device: - drmdev->interface.close(master_fd, fd_metadata, drmdev->userdata); - goto fail_unlock; - -fail_free_device: - drmFreeDevice(&device); - -fail_unlock: - drmdev_unlock(drmdev); - return ok; -} - -int drmdev_move_cursor(struct drmdev *drmdev, uint32_t crtc_id, struct vec2i pos) { - int ok = drmModeMoveCursor(drmdev->master_fd, crtc_id, pos.x, pos.y); - if (ok < 0) { - LOG_ERROR("Couldn't move mouse cursor. drmModeMoveCursor: %s\n", strerror(-ok)); - return -ok; - } - - return 0; -} - -static void drmdev_set_scanout_callback_locked( - struct drmdev *drmdev, - uint32_t crtc_id, - kms_scanout_cb_t scanout_callback, - void *userdata, - void_callback_t destroy_callback -) { - struct drm_crtc *crtc; - - ASSERT_NOT_NULL(drmdev); - - for_each_crtc_in_drmdev(drmdev, crtc) { - if (crtc->id == crtc_id) { - break; - } - } - - ASSERT_NOT_NULL_MSG(crtc, "Could not find CRTC with given id."); - - // If there's already a scanout callback configured, this is probably a state machine error. - // The scanout callback is configured in kms_req_commit and is cleared after it was called. - // So if this is called again this mean kms_req_commit is called but the previous frame wasn't committed yet. - ASSERT_EQUALS_MSG( - drmdev->per_crtc_state[crtc->index].scanout_callback, - NULL, - "There's already a scanout callback configured for this CRTC." - ); - drmdev->per_crtc_state[crtc->index].scanout_callback = scanout_callback; - drmdev->per_crtc_state[crtc->index].destroy_callback = destroy_callback; - drmdev->per_crtc_state[crtc->index].userdata = userdata; -} - -UNUSED static struct drm_plane *get_plane_by_id(struct drmdev *drmdev, uint32_t plane_id) { - struct drm_plane *plane; - - plane = NULL; - for (int i = 0; i < drmdev->n_planes; i++) { - if (drmdev->planes[i].id == plane_id) { - plane = drmdev->planes + i; - break; - } - } - - return plane; -} - -struct drm_connector *__next_connector(const struct drmdev *drmdev, const struct drm_connector *connector) { - bool found = connector == NULL; - for (size_t i = 0; i < drmdev->n_connectors; i++) { - if (drmdev->connectors + i == connector) { - found = true; - } else if (found) { - return drmdev->connectors + i; - } - } - - return NULL; -} - -struct drm_encoder *__next_encoder(const struct drmdev *drmdev, const struct drm_encoder *encoder) { - bool found = encoder == NULL; - for (size_t i = 0; i < drmdev->n_encoders; i++) { - if (drmdev->encoders + i == encoder) { - found = true; - } else if (found) { - return drmdev->encoders + i; - } - } - - return NULL; -} - -struct drm_crtc *__next_crtc(const struct drmdev *drmdev, const struct drm_crtc *crtc) { - bool found = crtc == NULL; - for (size_t i = 0; i < drmdev->n_crtcs; i++) { - if (drmdev->crtcs + i == crtc) { - found = true; - } else if (found) { - return drmdev->crtcs + i; - } - } - - return NULL; -} - -struct drm_plane *__next_plane(const struct drmdev *drmdev, const struct drm_plane *plane) { - bool found = plane == NULL; - for (size_t i = 0; i < drmdev->n_planes; i++) { - if (drmdev->planes + i == plane) { - found = true; - } else if (found) { - return drmdev->planes + i; - } - } - - return NULL; -} - -drmModeModeInfo *__next_mode(const struct drm_connector *connector, const drmModeModeInfo *mode) { - bool found = mode == NULL; - for (int i = 0; i < connector->variable_state.n_modes; i++) { - if (connector->variable_state.modes + i == mode) { - found = true; - } else if (found) { - return connector->variable_state.modes + i; - } - } - - return NULL; -} - -#ifdef DEBUG_DRM_PLANE_ALLOCATIONS - #define LOG_DRM_PLANE_ALLOCATION_DEBUG LOG_DEBUG -#else - #define LOG_DRM_PLANE_ALLOCATION_DEBUG(...) -#endif - -static bool plane_qualifies( - // clang-format off - struct drm_plane *plane, - bool allow_primary, - bool allow_overlay, - bool allow_cursor, - enum pixfmt format, - bool has_modifier, uint64_t modifier, - bool has_zpos, int64_t zpos_lower_limit, int64_t zpos_upper_limit, - bool has_rotation, drm_plane_transform_t rotation, - bool has_id_range, uint32_t id_lower_limit - // clang-format on -) { - LOG_DRM_PLANE_ALLOCATION_DEBUG(" checking if plane with id %" PRIu32 " qualifies...\n", plane->id); - - if (plane->type == kPrimary_DrmPlaneType) { - if (!allow_primary) { - LOG_DRM_PLANE_ALLOCATION_DEBUG(" does not qualify: plane type is primary but allow_primary is false\n"); - return false; - } - } else if (plane->type == kOverlay_DrmPlaneType) { - if (!allow_overlay) { - LOG_DRM_PLANE_ALLOCATION_DEBUG(" does not qualify: plane type is overlay but allow_overlay is false\n"); - return false; - } - } else if (plane->type == kCursor_DrmPlaneType) { - if (!allow_cursor) { - LOG_DRM_PLANE_ALLOCATION_DEBUG(" does not qualify: plane type is cursor but allow_cursor is false\n"); - return false; - } - } else { - ASSERT(false); - } - - if (has_modifier) { - if (!plane->supported_modified_formats_blob) { - // return false if we want a modified format but the plane doesn't support modified formats - LOG_DRM_PLANE_ALLOCATION_DEBUG( - " does not qualify: framebuffer has modifier %" PRIu64 " but plane does not support modified formats\n", - modifier - ); - return false; - } - - struct { - enum pixfmt format; - uint64_t modifier; - bool found; - } context = { - .format = format, - .modifier = modifier, - .found = false, - }; - - // Check if the requested format & modifier is supported. - drm_plane_for_each_modified_format(plane, check_modified_format_supported, &context); - - // Otherwise fail. - if (!context.found) { - LOG_DRM_PLANE_ALLOCATION_DEBUG( - " does not qualify: plane does not support the modified format %s, %" PRIu64 ".\n", - get_pixfmt_info(format)->name, - modifier - ); - - // not found in the supported modified format list - return false; - } - } else { - // we don't want a modified format, return false if the format is not in the list - // of supported (unmodified) formats - if (!plane->supported_formats[format]) { - LOG_DRM_PLANE_ALLOCATION_DEBUG( - " does not qualify: plane does not support the (unmodified) format %s.\n", - get_pixfmt_info(format)->name - ); - return false; - } - } - - if (has_zpos) { - if (!plane->has_zpos) { - // return false if we want a zpos but the plane doesn't support one - LOG_DRM_PLANE_ALLOCATION_DEBUG(" does not qualify: zpos constraints specified but plane doesn't have a zpos property.\n"); - return false; - } else if (zpos_lower_limit > plane->max_zpos || zpos_upper_limit < plane->min_zpos) { - // return false if the zpos we want is outside the supported range of the plane - LOG_DRM_PLANE_ALLOCATION_DEBUG(" does not qualify: plane limits cannot satisfy the specified zpos constraints.\n"); - LOG_DRM_PLANE_ALLOCATION_DEBUG( - " plane zpos range: %" PRIi64 " <= zpos <= %" PRIi64 ", given zpos constraints: %" PRIi64 " <= zpos <= %" PRIi64 ".\n", - plane->min_zpos, - plane->max_zpos, - zpos_lower_limit, - zpos_upper_limit - ); - return false; - } - } - if (has_id_range && plane->id < id_lower_limit) { - LOG_DRM_PLANE_ALLOCATION_DEBUG(" does not qualify: plane id does not satisfy the given plane id constrains.\n"); - LOG_DRM_PLANE_ALLOCATION_DEBUG(" plane id: %" PRIu32 ", plane id lower limit: %" PRIu32 "\n", plane->id, id_lower_limit); - return false; - } - if (has_rotation) { - if (!plane->has_rotation) { - // return false if the plane doesn't support rotation - LOG_DRM_PLANE_ALLOCATION_DEBUG(" does not qualify: explicit rotation requested but plane has no rotation property.\n"); - return false; - } else if (plane->has_hardcoded_rotation && plane->hardcoded_rotation.u32 != rotation.u32) { - // return false if the plane has a hardcoded rotation and the rotation we want - // is not the hardcoded one - LOG_DRM_PLANE_ALLOCATION_DEBUG(" does not qualify: plane has hardcoded rotation that doesn't match the requested rotation.\n" - ); - return false; - } else if (rotation.u32 & ~plane->supported_rotations.u32) { - // return false if we can't construct the rotation using the rotation - // bits that are supported by the plane - LOG_DRM_PLANE_ALLOCATION_DEBUG(" does not qualify: requested rotation is not supported by the plane.\n"); - return false; - } - } - - LOG_DRM_PLANE_ALLOCATION_DEBUG(" does qualify.\n"); - return true; -} - -static struct drm_plane *allocate_plane( - // clang-format off - struct kms_req_builder *builder, - bool allow_primary, - bool allow_overlay, - bool allow_cursor, - enum pixfmt format, - bool has_modifier, uint64_t modifier, - bool has_zpos, int64_t zpos_lower_limit, int64_t zpos_upper_limit, - bool has_rotation, drm_plane_transform_t rotation, - bool has_id_range, uint32_t id_lower_limit - // clang-format on -) { - for (int i = 0; i < BITSET_SIZE(builder->available_planes); i++) { - struct drm_plane *plane = builder->drmdev->planes + i; - - if (BITSET_TEST(builder->available_planes, i)) { - // find out if the plane matches our criteria - bool qualifies = plane_qualifies( - plane, - allow_primary, - allow_overlay, - allow_cursor, - format, - has_modifier, - modifier, - has_zpos, - zpos_lower_limit, - zpos_upper_limit, - has_rotation, - rotation, - has_id_range, - id_lower_limit - ); - - // if it doesn't, look for the next one - if (!qualifies) { - continue; - } - - // we found one, mark it as used and return it - BITSET_CLEAR(builder->available_planes, i); - return plane; - } - } - - // we didn't find an available plane matching our criteria - return NULL; -} - -static void release_plane(struct kms_req_builder *builder, uint32_t plane_id) { - struct drm_plane *plane; - int index; - - index = 0; - for_each_plane_in_drmdev(builder->drmdev, plane) { - if (plane->id == plane_id) { - break; - } - index++; - } - - if (plane == NULL) { - LOG_ERROR("Could not release invalid plane %" PRIu32 ".\n", plane_id); - return; - } - - assert(!BITSET_TEST(builder->available_planes, index)); - BITSET_SET(builder->available_planes, index); -} - -struct kms_req_builder *drmdev_create_request_builder(struct drmdev *drmdev, uint32_t crtc_id) { - struct kms_req_builder *builder; - drmModeAtomicReq *req; - struct drm_crtc *crtc; - int64_t min_zpos; - bool supports_atomic_modesetting; - - ASSERT_NOT_NULL(drmdev); - assert(crtc_id != 0 && crtc_id != 0xFFFFFFFF); - - drmdev_lock(drmdev); - - for_each_crtc_in_drmdev(drmdev, crtc) { - if (crtc->id == crtc_id) { - break; - } - } - - if (crtc == NULL) { - LOG_ERROR("Invalid CRTC id: %" PRIu32 "\n", crtc_id); - goto fail_unlock; - } - - builder = malloc(sizeof *builder); - if (builder == NULL) { - goto fail_unlock; - } - - supports_atomic_modesetting = drmdev->supports_atomic_modesetting; - - if (supports_atomic_modesetting) { - req = drmModeAtomicAlloc(); - if (req == NULL) { - goto fail_free_builder; - } - - // set the CRTC to active - drmModeAtomicAddProperty(req, crtc->id, crtc->ids.active, 1); - } else { - req = NULL; - } - - min_zpos = INT64_MAX; - BITSET_ZERO(builder->available_planes); - for (int i = 0; i < drmdev->n_planes; i++) { - struct drm_plane *plane = drmdev->planes + i; - - if (plane->possible_crtcs & crtc->bitmask) { - BITSET_SET(builder->available_planes, i); - if (plane->has_zpos && plane->min_zpos < min_zpos) { - min_zpos = plane->min_zpos; - } - } - } - - drmdev_unlock(drmdev); - - builder->n_refs = REFCOUNT_INIT_1; - builder->drmdev = drmdev_ref(drmdev); - // right now they're the same, but they might not be in the future. - builder->use_legacy = !supports_atomic_modesetting; - builder->supports_atomic = supports_atomic_modesetting; - builder->connector = NULL; - builder->crtc = crtc; - builder->req = req; - builder->next_zpos = min_zpos; - builder->n_layers = 0; - builder->has_mode = false; - builder->unset_mode = false; - return builder; - -fail_free_builder: - free(builder); - -fail_unlock: - drmdev_unlock(drmdev); - return NULL; -} - -static void kms_req_builder_destroy(struct kms_req_builder *builder) { - /// TODO: Is this complete? - for (int i = 0; i < builder->n_layers; i++) { - if (builder->layers[i].release_callback != NULL) { - builder->layers[i].release_callback(builder->layers[i].release_callback_userdata); - } - } - if (builder->req != NULL) { - drmModeAtomicFree(builder->req); - } - drmdev_unref(builder->drmdev); - free(builder); -} - -DEFINE_REF_OPS(kms_req_builder, n_refs) - -struct drmdev *kms_req_builder_get_drmdev(struct kms_req_builder *builder) { - return builder->drmdev; -} - -struct drm_crtc *kms_req_builder_get_crtc(struct kms_req_builder *builder) { - return builder->crtc; -} - -bool kms_req_builder_prefer_next_layer_opaque(struct kms_req_builder *builder) { - ASSERT_NOT_NULL(builder); - return builder->n_layers == 0; -} - -int kms_req_builder_set_mode(struct kms_req_builder *builder, const drmModeModeInfo *mode) { - ASSERT_NOT_NULL(builder); - ASSERT_NOT_NULL(mode); - builder->has_mode = true; - builder->mode = *mode; - return 0; -} - -int kms_req_builder_unset_mode(struct kms_req_builder *builder) { - ASSERT_NOT_NULL(builder); - assert(!builder->has_mode); - builder->unset_mode = true; - return 0; -} - -int kms_req_builder_set_connector(struct kms_req_builder *builder, uint32_t connector_id) { - struct drm_connector *conn; - - ASSERT_NOT_NULL(builder); - assert(DRM_ID_IS_VALID(connector_id)); - - for_each_connector_in_drmdev(builder->drmdev, conn) { - if (conn->id == connector_id) { - break; - } - } - - if (conn == NULL) { - LOG_ERROR("Could not find connector with id %" PRIu32 "\n", connector_id); - return EINVAL; - } - - builder->connector = conn; - return 0; -} - -int kms_req_builder_push_fb_layer( - struct kms_req_builder *builder, - const struct kms_fb_layer *layer, - kms_fb_release_cb_t release_callback, - kms_deferred_fb_release_cb_t deferred_release_callback, - void *userdata -) { - struct drm_plane *plane; - int64_t zpos; - bool has_zpos; - bool close_in_fence_fd_after; - int ok, index; - - ASSERT_NOT_NULL(builder); - ASSERT_NOT_NULL(layer); - ASSERT_NOT_NULL(release_callback); - ASSERT_EQUALS_MSG(deferred_release_callback, NULL, "deferred release callbacks are not supported right now."); - - if (builder->use_legacy && builder->supports_atomic && builder->n_layers > 1) { - // if we already have a first layer and we should use legacy modesetting even though the kernel driver - // supports atomic modesetting, return EINVAL. - // if the driver supports atomic modesetting, drmModeSetPlane will block for vblank, so we can't use it, - // and we can't use drmModeAtomicCommit for non-blocking multi-plane commits of course. - // For the first layer we can use drmModePageFlip though. - LOG_DEBUG("Can't do multi-plane commits when using legacy modesetting (and driver supports atomic modesetting).\n"); - return EINVAL; - } - - close_in_fence_fd_after = false; - if (builder->use_legacy && layer->has_in_fence_fd) { - LOG_DEBUG("Explicit fencing is not supported for legacy modesetting. Implicit fencing will be used instead.\n"); - close_in_fence_fd_after = true; - } - - // Index of our layer. - index = builder->n_layers; - - // If we should prefer a cursor plane, try to find one first. - plane = NULL; - if (layer->prefer_cursor) { - plane = allocate_plane( - // clang-format off - builder, - /* allow_primary */ false, - /* allow_overlay */ false, - /* allow_cursor */ true, - /* format */ layer->format, - /* modifier */ layer->has_modifier, layer->modifier, - /* zpos */ false, 0, 0, - /* rotation */ layer->has_rotation, layer->rotation, - /* id_range */ false, 0 - // clang-format on - ); - } - - /// TODO: Not sure we can use crtc_x, crtc_y, etc with primary planes - if (plane == NULL && index == 0) { - // if this is the first layer, try using a - // primary plane for it. - - /// TODO: Use cursor_plane->max_zpos - 1 as the upper zpos limit, instead of INT64_MAX - plane = allocate_plane( - // clang-format off - builder, - /* allow_primary */ true, - /* allow_overlay */ false, - /* allow_cursor */ false, - /* format */ layer->format, - /* modifier */ layer->has_modifier, layer->modifier, - /* zpos */ false, 0, 0, - /* rotation */ layer->has_rotation, layer->rotation, - /* id_range */ false, 0 - // clang-format on - ); - - if (plane == NULL && !get_pixfmt_info(layer->format)->is_opaque) { - // maybe we can find a plane if we use the opaque version of this pixel format? - plane = allocate_plane( - // clang-format off - builder, - /* allow_primary */ true, - /* allow_overlay */ false, - /* allow_cursor */ false, - /* format */ pixfmt_opaque(layer->format), - /* modifier */ layer->has_modifier, layer->modifier, - /* zpos */ false, 0, 0, - /* rotation */ layer->has_rotation, layer->rotation, - /* id_range */ false, 0 - // clang-format on - ); - } - } else if (plane == NULL) { - // First try to find an overlay plane with a higher zpos. - plane = allocate_plane( - // clang-format off - builder, - /* allow_primary */ false, - /* allow_overlay */ true, - /* allow_cursor */ false, - /* format */ layer->format, - /* modifier */ layer->has_modifier, layer->modifier, - /* zpos */ true, builder->next_zpos, INT64_MAX, - /* rotation */ layer->has_rotation, layer->rotation, - /* id_range */ false, 0 - // clang-format on - ); - - // If we can't find one, find an overlay plane with the next highest plane_id. - // (According to some comments in the kernel, that's the fallback KMS uses for the - // occlusion order if no zpos property is supported, i.e. planes with plane id occlude - // planes with lower id) - if (plane == NULL) { - plane = allocate_plane( - // clang-format off - builder, - /* allow_primary */ false, - /* allow_overlay */ true, - /* allow_cursor */ false, - /* format */ layer->format, - /* modifier */ layer->has_modifier, layer->modifier, - /* zpos */ false, 0, 0, - /* rotation */ layer->has_rotation, layer->rotation, - /* id_range */ true, builder->layers[index - 1].plane_id + 1 - // clang-format on - ); - } - } - - if (plane == NULL) { - LOG_ERROR("Could not find a suitable unused DRM plane for pushing the framebuffer.\n"); - return EIO; - } - - // Now that we have a plane, use the minimum zpos - // that's both higher than the last layers zpos and - // also supported by the plane. - // This will also work for planes with hardcoded zpos. - has_zpos = plane->has_zpos; - if (has_zpos) { - zpos = builder->next_zpos; - if (plane->min_zpos > zpos) { - zpos = plane->min_zpos; - } - } else { - // just to silence an uninitialized use warning below. - zpos = 0; - } - - if (builder->use_legacy) { - } else { - uint32_t plane_id = plane->id; - - /// TODO: Error checking - /// TODO: Maybe add these in the kms_req_builder_commit instead? - drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.crtc_id, builder->crtc->id); - drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.fb_id, layer->drm_fb_id); - drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.crtc_x, layer->dst_x); - drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.crtc_y, layer->dst_y); - drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.crtc_w, layer->dst_w); - drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.crtc_h, layer->dst_h); - drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.src_x, layer->src_x); - drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.src_y, layer->src_y); - drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.src_w, layer->src_w); - drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.src_h, layer->src_h); - - if (plane->has_zpos && !plane->has_hardcoded_zpos) { - drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.zpos, zpos); - } - - if (layer->has_rotation && plane->has_rotation && !plane->has_hardcoded_rotation) { - drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.rotation, layer->rotation.u64); - } - - if (index == 0) { - if (plane->has_alpha) { - drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.alpha, plane->max_alpha); - } - - if (plane->has_blend_mode && plane->supported_blend_modes[kNone_DrmBlendMode]) { - drmModeAtomicAddProperty(builder->req, plane_id, plane->ids.pixel_blend_mode, kNone_DrmBlendMode); - } - } - } - - // This should be done when we're sure we're not failing. - // Because on failure it would be the callers job to close the fd. - if (close_in_fence_fd_after) { - ok = close(layer->in_fence_fd); - if (ok < 0) { - ok = errno; - LOG_ERROR("Could not close layer in_fence_fd. close: %s\n", strerror(ok)); - goto fail_release_plane; - } - } - - /// TODO: Right now we're adding zpos, rotation to the atomic request unconditionally - /// when specified in the fb layer. Ideally we would check for updates - /// on commit and only add to the atomic request when zpos / rotation changed. - builder->n_layers++; - if (has_zpos) { - builder->next_zpos = zpos + 1; - } - builder->layers[index].layer = *layer; - builder->layers[index].plane_id = plane->id; - builder->layers[index].plane = plane; - builder->layers[index].set_zpos = has_zpos; - builder->layers[index].zpos = zpos; - builder->layers[index].set_rotation = layer->has_rotation; - builder->layers[index].rotation = layer->rotation; - builder->layers[index].release_callback = release_callback; - builder->layers[index].deferred_release_callback = deferred_release_callback; - builder->layers[index].release_callback_userdata = userdata; - return 0; - -fail_release_plane: - release_plane(builder, plane->id); - return ok; -} - -int kms_req_builder_push_zpos_placeholder_layer(struct kms_req_builder *builder, int64_t *zpos_out) { - ASSERT_NOT_NULL(builder); - ASSERT_NOT_NULL(zpos_out); - *zpos_out = builder->next_zpos++; - return 0; -} - -struct kms_req *kms_req_builder_build(struct kms_req_builder *builder) { - return (struct kms_req *) kms_req_builder_ref(builder); -} - -UNUSED struct kms_req *kms_req_ref(struct kms_req *req) { - return (struct kms_req *) kms_req_builder_ref((struct kms_req_builder *) req); -} - -UNUSED void kms_req_unref(struct kms_req *req) { - kms_req_builder_unref((struct kms_req_builder *) req); -} - -UNUSED void kms_req_unrefp(struct kms_req **req) { - kms_req_builder_unrefp((struct kms_req_builder **) req); -} - -UNUSED void kms_req_swap_ptrs(struct kms_req **oldp, struct kms_req *new) { - kms_req_builder_swap_ptrs((struct kms_req_builder **) oldp, (struct kms_req_builder *) new); -} - -static bool drm_plane_is_active(struct drm_plane *plane) { - return plane->committed_state.fb_id != 0 && plane->committed_state.crtc_id != 0; -} - -static int -kms_req_commit_common(struct kms_req *req, bool blocking, kms_scanout_cb_t scanout_cb, void *userdata, void_callback_t destroy_cb) { - struct kms_req_builder *builder; - struct drm_mode_blob *mode_blob; - uint32_t flags; - bool internally_blocking; - bool update_mode; - int ok; - - internally_blocking = false; - update_mode = false; - mode_blob = NULL; - - ASSERT_NOT_NULL(req); - builder = (struct kms_req_builder *) req; - - drmdev_lock(builder->drmdev); - - if (builder->drmdev->master_fd < 0) { - LOG_ERROR("Commit requested, but drmdev doesn't have a DRM master fd right now.\n"); - ok = EBUSY; - goto fail_unlock; - } - - if (!is_drm_master(builder->drmdev->master_fd)) { - LOG_ERROR("Commit requested, but drmdev is paused right now.\n"); - ok = EBUSY; - goto fail_unlock; - } - - // only change the mode if the new mode differs from the old one - - /// TOOD: If this is not a standard mode reported by connector/CRTC, - /// is there a way to verify if it is valid? (maybe use DRM_MODE_ATOMIC_TEST) - - // this could be a single expression but this way you see a bit better what's going on. - // We need to upload the new mode blob if: - // - we have a new mode - // - and: we don't have an old mode - // - or: the old mode differs from the new mode - bool upload_mode = false; - if (builder->has_mode) { - if (!builder->crtc->committed_state.has_mode) { - upload_mode = true; - } else if (memcmp(&builder->crtc->committed_state.mode, &builder->mode, sizeof(drmModeModeInfo)) != 0) { - upload_mode = true; - } - } - - if (upload_mode) { - update_mode = true; - mode_blob = drm_mode_blob_new(builder->drmdev->fd, &builder->mode); - if (mode_blob == NULL) { - ok = EIO; - goto fail_unlock; - } - } else if (builder->unset_mode) { - update_mode = true; - mode_blob = NULL; - } - - if (builder->use_legacy) { - ASSERT_EQUALS(builder->layers[0].layer.dst_x, 0); - ASSERT_EQUALS(builder->layers[0].layer.dst_y, 0); - ASSERT_EQUALS(builder->layers[0].layer.dst_w, builder->mode.hdisplay); - ASSERT_EQUALS(builder->layers[0].layer.dst_h, builder->mode.vdisplay); - - /// TODO: Do we really need to assert this? - ASSERT_NOT_NULL(builder->connector); - - bool needs_set_crtc = update_mode; - - // check if the plane pixel format changed. - // that needs a drmModeSetCrtc for legacy KMS as well. - // get the current, committed fb for the plane, check if we have info - // for it (we can't use drmModeGetFB2 since that's not present on debian buster) - // and if we're not absolutely sure the formats match, set needs_set_crtc - // too. - if (!needs_set_crtc) { - struct kms_req_layer *layer = builder->layers + 0; - struct drm_plane *plane = layer->plane; - ASSERT_NOT_NULL(plane); - -#ifndef DEBUG - if (plane->committed_state.has_format && plane->committed_state.format == layer->layer.format) { - needs_set_crtc = false; - } else { - needs_set_crtc = true; - } -#else - drmModeFBPtr committed_fb = drmModeGetFB(builder->drmdev->master_fd, plane->committed_state.fb_id); - if (committed_fb == NULL) { - needs_set_crtc = true; - } else { - needs_set_crtc = true; - - list_for_each_entry(struct drm_fb, fb, &builder->drmdev->fbs, entry) { - if (fb->id == committed_fb->fb_id) { - ASSERT_EQUALS(fb->format, plane->committed_state.format); - - if (fb->format == layer->layer.format) { - needs_set_crtc = false; - } - } - - if (fb->id == layer->layer.drm_fb_id) { - ASSERT_EQUALS(fb->format, layer->layer.format); - } - } - } - - drmModeFreeFB(committed_fb); -#endif - } - - /// TODO: Handle {src,dst}_{x,y,w,h} here - /// TODO: Handle setting other properties as well - if (needs_set_crtc) { - /// TODO: Fetch new connector or current connector here since we seem to need it for drmModeSetCrtc - ok = drmModeSetCrtc( - builder->drmdev->master_fd, - builder->crtc->id, - builder->layers[0].layer.drm_fb_id, - 0, - 0, - (uint32_t[1]){ builder->connector->id }, - 1, - builder->unset_mode ? NULL : &builder->mode - ); - if (ok != 0) { - ok = errno; - LOG_ERROR("Could not commit display update. drmModeSetCrtc: %s\n", strerror(ok)); - goto fail_maybe_destroy_mode_blob; - } - - internally_blocking = true; - } else { - ok = drmModePageFlip( - builder->drmdev->master_fd, - builder->crtc->id, - builder->layers[0].layer.drm_fb_id, - DRM_MODE_PAGE_FLIP_EVENT, - kms_req_builder_ref(builder) - ); - if (ok != 0) { - ok = errno; - LOG_ERROR("Could not commit display update. drmModePageFlip: %s\n", strerror(ok)); - goto fail_unref_builder; - } - } - - // This should also be ensured by kms_req_builder_push_fb_layer - ASSERT_MSG( - !(builder->supports_atomic && builder->n_layers > 1), - "There can be at most one framebuffer layer when the KMS device supports atomic modesetting but we are " - "using legacy modesetting." - ); - - /// TODO: Call drmModeSetPlane for all other layers - /// TODO: Assert here - } else { - /// TODO: If we can do explicit fencing, don't use the page flip event. - /// TODO: Can we set OUT_FENCE_PTR even though we didn't set any IN_FENCE_FDs? - flags = DRM_MODE_PAGE_FLIP_EVENT | (blocking ? 0 : DRM_MODE_ATOMIC_NONBLOCK) | (update_mode ? DRM_MODE_ATOMIC_ALLOW_MODESET : 0); - - // All planes that are not used by us and are connected to our CRTC - // should be disabled. - { - int i; - BITSET_FOREACH_SET(i, builder->available_planes, 32) { - struct drm_plane *plane = builder->drmdev->planes + i; - - if (drm_plane_is_active(plane) && plane->committed_state.crtc_id == builder->crtc->id) { - drmModeAtomicAddProperty(builder->req, plane->id, plane->ids.crtc_id, 0); - drmModeAtomicAddProperty(builder->req, plane->id, plane->ids.fb_id, 0); - } - } - } - - if (builder->connector != NULL) { - // add the CRTC_ID property if that was explicitly set - drmModeAtomicAddProperty(builder->req, builder->connector->id, builder->connector->ids.crtc_id, builder->crtc->id); - } - - if (update_mode) { - if (mode_blob != NULL) { - drmModeAtomicAddProperty(builder->req, builder->crtc->id, builder->crtc->ids.mode_id, mode_blob->blob_id); - } else { - drmModeAtomicAddProperty(builder->req, builder->crtc->id, builder->crtc->ids.mode_id, 0); - } - } - - /// TODO: If we're on raspberry pi and only have one layer, we can do an async pageflip - /// on the primary plane to replace the next queued frame. (To do _real_ triple buffering - /// with fully decoupled framerate, potentially) - ok = drmModeAtomicCommit(builder->drmdev->master_fd, builder->req, flags, kms_req_builder_ref(builder)); - if (ok != 0) { - ok = errno; - LOG_ERROR("Could not commit display update. drmModeAtomicCommit: %s\n", strerror(ok)); - goto fail_unref_builder; - } - } - - // update struct drm_plane.committed_state for all planes - for (int i = 0; i < builder->n_layers; i++) { - struct drm_plane *plane = builder->layers[i].plane; - struct kms_req_layer *layer = builder->layers + i; - - plane->committed_state.crtc_id = builder->crtc->id; - plane->committed_state.fb_id = layer->layer.drm_fb_id; - plane->committed_state.src_x = layer->layer.src_x; - plane->committed_state.src_y = layer->layer.src_y; - plane->committed_state.src_w = layer->layer.src_w; - plane->committed_state.src_h = layer->layer.src_h; - plane->committed_state.crtc_x = layer->layer.dst_x; - plane->committed_state.crtc_y = layer->layer.dst_y; - plane->committed_state.crtc_w = layer->layer.dst_w; - plane->committed_state.crtc_h = layer->layer.dst_h; - - if (builder->layers[i].set_zpos) { - plane->committed_state.zpos = layer->zpos; - } - if (builder->layers[i].set_rotation) { - plane->committed_state.rotation = layer->rotation; - } - - plane->committed_state.has_format = true; - plane->committed_state.format = layer->layer.format; - - // builder->layers[i].plane->committed_state.alpha = layer->alpha; - // builder->layers[i].plane->committed_state.blend_mode = builder->layers[i].layer.blend_mode; - } - - // update struct drm_crtc.committed_state - if (update_mode) { - // destroy the old mode blob - if (builder->crtc->committed_state.mode_blob != NULL) { - /// TODO: Should we defer this to after the pageflip? - drm_mode_blob_destroy(builder->crtc->committed_state.mode_blob); - } - - // store the new mode - if (mode_blob != NULL) { - builder->crtc->committed_state.has_mode = true; - builder->crtc->committed_state.mode = builder->mode; - builder->crtc->committed_state.mode_blob = mode_blob; - } else { - builder->crtc->committed_state.has_mode = false; - builder->crtc->committed_state.mode_blob = NULL; - } - } - - // update struct drm_connector.committed_state - builder->connector->committed_state.crtc_id = builder->crtc->id; - // builder->connector->committed_state.encoder_id = 0; - - drmdev_set_scanout_callback_locked(builder->drmdev, builder->crtc->id, scanout_cb, userdata, destroy_cb); - - if (internally_blocking) { - uint64_t sequence = 0; - uint64_t ns = 0; - int ok; - - ok = drmCrtcGetSequence(builder->drmdev->fd, builder->crtc->id, &sequence, &ns); - if (ok != 0) { - ok = errno; - LOG_ERROR("Could not get vblank timestamp. drmCrtcGetSequence: %s\n", strerror(ok)); - goto fail_unref_builder; - } - - struct drmdev *drmdev = builder->drmdev; - - drmdev_on_page_flip_locked( - builder->drmdev->fd, - (unsigned int) sequence, - ns / 1000000000, - ns / 1000, - builder->crtc->id, - kms_req_ref(req) - ); - - drmdev_unlock(drmdev); - } else if (blocking) { - struct drmdev *drmdev = builder->drmdev; - - // handle the page-flip event here, rather than via the eventfd - ok = drmdev_on_modesetting_fd_ready_locked(builder->drmdev); - if (ok != 0) { - LOG_ERROR("Couldn't synchronously handle pageflip event.\n"); - goto fail_unset_scanout_callback; - } - - drmdev_unlock(drmdev); - } else { - drmdev_unlock(builder->drmdev); - } - - return 0; - -fail_unset_scanout_callback: - builder->drmdev->per_crtc_state[builder->crtc->index].scanout_callback = NULL; - builder->drmdev->per_crtc_state[builder->crtc->index].destroy_callback = NULL; - builder->drmdev->per_crtc_state[builder->crtc->index].userdata = NULL; - goto fail_unlock; - -fail_unref_builder: { - struct drmdev *drmdev = builder->drmdev; - kms_req_builder_unref(builder); - if (mode_blob != NULL) { - drm_mode_blob_destroy(mode_blob); - } - drmdev_unlock(drmdev); - return ok; -} - -fail_maybe_destroy_mode_blob: - if (mode_blob != NULL) - drm_mode_blob_destroy(mode_blob); - -fail_unlock: - drmdev_unlock(builder->drmdev); - - return ok; -} - -void set_vblank_ns(struct drmdev *drmdev, uint64_t vblank_ns, void *userdata) { - uint64_t *vblank_ns_out; - - ASSERT_NOT_NULL(userdata); - vblank_ns_out = userdata; - (void) drmdev; - - *vblank_ns_out = vblank_ns; -} - -int kms_req_commit_blocking(struct kms_req *req, uint64_t *vblank_ns_out) { - uint64_t vblank_ns; - int ok; - - vblank_ns = int64_to_uint64(-1); - ok = kms_req_commit_common(req, true, set_vblank_ns, &vblank_ns, NULL); - if (ok != 0) { - return ok; - } - - // make sure the vblank_ns is actually set - assert(vblank_ns != int64_to_uint64(-1)); - if (vblank_ns_out != NULL) { - *vblank_ns_out = vblank_ns; - } - - return 0; -} - -int kms_req_commit_nonblocking(struct kms_req *req, kms_scanout_cb_t scanout_cb, void *userdata, void_callback_t destroy_cb) { - return kms_req_commit_common(req, false, scanout_cb, userdata, destroy_cb); -} diff --git a/src/window.c b/src/window.c index 920878ee..3a8b3bd2 100644 --- a/src/window.c +++ b/src/window.c @@ -21,7 +21,8 @@ #include "cursor.h" #include "flutter-pi.h" #include "frame_scheduler.h" -#include "modesetting.h" +#include "kms/drmdev.h" +#include "kms/resources.h" #include "render_surface.h" #include "surface.h" #include "tracer.h" @@ -162,6 +163,8 @@ struct window { */ struct { struct drmdev *drmdev; + struct drm_resources *resources; + struct drm_connector *connector; struct drm_encoder *encoder; struct drm_crtc *crtc; @@ -171,6 +174,7 @@ struct window { const struct pointer_icon *pointer_icon; struct cursor_buffer *cursor; + } kms; /** @@ -701,43 +705,23 @@ static void cursor_buffer_destroy(struct cursor_buffer *buffer) { free(buffer); } -static void cursor_buffer_destroy_with_locked_drmdev(struct cursor_buffer *buffer) { - drmdev_rm_fb_locked(buffer->drmdev, buffer->drm_fb_id); - gbm_bo_destroy(buffer->bo); - drmdev_unref(buffer->drmdev); - free(buffer); -} - DEFINE_STATIC_REF_OPS(cursor_buffer, n_refs) -static void cursor_buffer_unref_with_locked_drmdev(void *userdata) { - struct cursor_buffer *cursor; - - ASSERT_NOT_NULL(userdata); - cursor = userdata; - - if (refcount_dec(&cursor->n_refs) == false) { - cursor_buffer_destroy_with_locked_drmdev(cursor); - } -} - static int select_mode( - struct drmdev *drmdev, + struct drm_resources *resources, struct drm_connector **connector_out, struct drm_encoder **encoder_out, struct drm_crtc **crtc_out, drmModeModeInfo **mode_out, const char *desired_videomode ) { - struct drm_connector *connector; - struct drm_encoder *encoder; - struct drm_crtc *crtc; - drmModeModeInfo *mode, *mode_iter; int ok; // find any connected connector - for_each_connector_in_drmdev(drmdev, connector) { - if (connector->variable_state.connection_state == kConnected_DrmConnectionState) { + struct drm_connector *connector = NULL; + drm_resources_for_each_connector(resources, connector_it) { + if (connector_it->variable_state.connection_state == DRM_CONNSTATE_CONNECTED) { + connector = connector_it; break; } } @@ -747,9 +731,9 @@ static int select_mode( return EINVAL; } - mode = NULL; + drmModeModeInfo *mode = NULL; if (desired_videomode != NULL) { - for_each_mode_in_connector(connector, mode_iter) { + drm_connector_for_each_mode(connector, mode_iter) { char *modeline = NULL, *modeline_nohz = NULL; ok = asprintf(&modeline, "%" PRIu16 "x%" PRIu16 "@%" PRIu32, mode_iter->hdisplay, mode_iter->vdisplay, mode_iter->vrefresh); @@ -786,7 +770,7 @@ static int select_mode( // Alternatively, find the mode with the highest width*height. If there are multiple modes with the same w*h, // prefer higher refresh rates. After that, prefer progressive scanout modes. if (mode == NULL) { - for_each_mode_in_connector(connector, mode_iter) { + drm_connector_for_each_mode(connector, mode_iter) { if (mode_iter->type & DRM_MODE_TYPE_PREFERRED) { mode = mode_iter; break; @@ -812,8 +796,10 @@ static int select_mode( ASSERT_NOT_NULL(mode); // Find the encoder that's linked to the connector right now - for_each_encoder_in_drmdev(drmdev, encoder) { - if (encoder->encoder->encoder_id == connector->committed_state.encoder_id) { + struct drm_encoder *encoder = NULL; + drm_resources_for_each_encoder(resources, encoder_it) { + if (encoder_it->id == connector->committed_state.encoder_id) { + encoder = encoder_it; break; } } @@ -821,13 +807,14 @@ static int select_mode( // Otherwise use use any encoder that the connector supports linking to if (encoder == NULL) { for (int i = 0; i < connector->n_encoders; i++, encoder = NULL) { - for_each_encoder_in_drmdev(drmdev, encoder) { - if (encoder->encoder->encoder_id == connector->encoders[i]) { + drm_resources_for_each_encoder(resources, encoder_it) { + if (encoder_it->id == connector->encoders[i]) { + encoder = encoder_it; break; } } - if (encoder->encoder->possible_crtcs) { + if (encoder && encoder->possible_crtcs) { // only use this encoder if there's a crtc we can use with it break; } @@ -840,17 +827,20 @@ static int select_mode( } // Find the CRTC that's currently linked to this encoder - for_each_crtc_in_drmdev(drmdev, crtc) { - if (crtc->id == encoder->encoder->crtc_id) { + struct drm_crtc *crtc = NULL; + drm_resources_for_each_crtc(resources, crtc_it) { + if (crtc_it->id == encoder->variable_state.crtc_id) { + crtc = crtc_it; break; } } // Otherwise use any CRTC that this encoder supports linking to if (crtc == NULL) { - for_each_crtc_in_drmdev(drmdev, crtc) { - if (encoder->encoder->possible_crtcs & crtc->bitmask) { + drm_resources_for_each_crtc(resources, crtc_it) { + if (encoder->possible_crtcs & crtc_it->bitmask) { // find a CRTC that is possible to use with this encoder + crtc = crtc_it; break; } } @@ -898,6 +888,7 @@ MUST_CHECK struct window *kms_window_new( bool has_explicit_dimensions, int width_mm, int height_mm, bool has_forced_pixel_format, enum pixfmt forced_pixel_format, struct drmdev *drmdev, + struct drm_resources *resources, const char *desired_videomode // clang-format on ) { @@ -910,6 +901,7 @@ MUST_CHECK struct window *kms_window_new( int ok; ASSERT_NOT_NULL(drmdev); + ASSERT_NOT_NULL(resources); #if !defined(HAVE_VULKAN) ASSUME(renderer_type != kVulkan_RendererType); @@ -930,7 +922,7 @@ MUST_CHECK struct window *kms_window_new( return NULL; } - ok = select_mode(drmdev, &selected_connector, &selected_encoder, &selected_crtc, &selected_mode, desired_videomode); + ok = select_mode(resources, &selected_connector, &selected_encoder, &selected_crtc, &selected_mode, desired_videomode); if (ok != 0) { goto fail_free_window; } @@ -989,6 +981,7 @@ MUST_CHECK struct window *kms_window_new( ); window->kms.drmdev = drmdev_ref(drmdev); + window->kms.resources = drm_resources_ref(resources); window->kms.connector = selected_connector; window->kms.encoder = selected_encoder; window->kms.crtc = selected_crtc; @@ -1079,6 +1072,7 @@ void kms_window_deinit(struct window *window) { struct frame { struct tracer *tracer; + struct drmdev *drmdev; struct kms_req *req; bool unset_should_apply_mode_on_commit; }; @@ -1093,15 +1087,11 @@ UNUSED static void on_scanout(struct drmdev *drmdev, uint64_t vblank_ns, void *u } static void on_present_frame(void *userdata) { - struct frame *frame; - int ok; - ASSERT_NOT_NULL(userdata); - - frame = userdata; + struct frame *frame = userdata; TRACER_BEGIN(frame->tracer, "kms_req_commit_nonblocking"); - ok = kms_req_commit_blocking(frame->req, NULL); + int ok = kms_req_commit_blocking(frame->req, frame->drmdev, NULL); TRACER_END(frame->tracer, "kms_req_commit_nonblocking"); if (ok != 0) { @@ -1110,6 +1100,7 @@ static void on_present_frame(void *userdata) { tracer_unref(frame->tracer); kms_req_unref(frame->req); + drmdev_unref(frame->drmdev); free(frame); } @@ -1121,6 +1112,7 @@ static void on_cancel_frame(void *userdata) { tracer_unref(frame->tracer); kms_req_unref(frame->req); + drmdev_unref(frame->drmdev); free(frame); } @@ -1151,7 +1143,7 @@ static int kms_window_push_composition_locked(struct window *window, struct fl_l /// TODO: If we don't have new revisions, we don't need to scanout anything. fl_layer_composition_swap_ptrs(&window->composition, composition); - builder = drmdev_create_request_builder(window->kms.drmdev, window->kms.crtc->id); + builder = kms_req_builder_new_atomic(window->kms.drmdev, window->kms.resources, window->kms.crtc->id); if (builder == NULL) { ok = ENOMEM; goto fail_unref_builder; @@ -1205,7 +1197,7 @@ static int kms_window_push_composition_locked(struct window *window, struct fl_l .in_fence_fd = 0, .prefer_cursor = true, }, - cursor_buffer_unref_with_locked_drmdev, + cursor_buffer_unref_void, NULL, window->kms.cursor ); @@ -1233,62 +1225,11 @@ static int kms_window_push_composition_locked(struct window *window, struct fl_l frame->req = req; frame->tracer = tracer_ref(window->tracer); + frame->drmdev = drmdev_ref(window->kms.drmdev); frame->unset_should_apply_mode_on_commit = window->kms.should_apply_mode; frame_scheduler_present_frame(window->frame_scheduler, on_present_frame, frame, on_cancel_frame); - // if (window->present_mode == kDoubleBufferedVsync_PresentMode) { - // TRACER_BEGIN(window->tracer, "kms_req_builder_commit"); - // ok = kms_req_commit(req, /* blocking: */ false); - // TRACER_END(window->tracer, "kms_req_builder_commit"); - // - // if (ok != 0) { - // LOG_ERROR("Could not commit frame request.\n"); - // goto fail_unref_window2; - // } - // - // if (window->set_set_mode) { - // window->set_mode = false; - // window->set_set_mode = false; - // } - // } else { - // ASSERT_EQUALS(window->present_mode, kTripleBufferedVsync_PresentMode); - // - // if (window->present_immediately) { - // TRACER_BEGIN(window->tracer, "kms_req_builder_commit"); - // ok = kms_req_commit(req, /* blocking: */ false); - // TRACER_END(window->tracer, "kms_req_builder_commit"); - // - // if (ok != 0) { - // LOG_ERROR("Could not commit frame request.\n"); - // goto fail_unref_window2; - // } - // - // if (window->set_set_mode) { - // window->set_mode = false; - // window->set_set_mode = false; - // } - // - // window->present_immediately = false; - // } else { - // if (window->next_frame != NULL) { - // /// FIXME: Call the release callbacks when the kms_req is destroyed, not when it's unrefed. - // /// Not sure this here will lead to the release callbacks being called multiple times. - // kms_req_call_release_callbacks(window->next_frame); - // kms_req_unref(window->next_frame); - // } - // - // window->next_frame = kms_req_ref(req); - // window->set_set_mode = window->set_mode; - // } - // } - - // KMS Req is committed now and drmdev keeps a ref - // on it internally, so we don't need to keep this one. - // kms_req_unref(req); - - // window_on_rendering_complete(window); - return 0; fail_unref_req: @@ -1393,14 +1334,13 @@ static struct render_surface *kms_window_get_render_surface_internal(struct wind // as the allowed modifiers. /// TODO: Find a way to rank pixel formats, maybe by number of planes that support them for scanout. { - struct drm_plane *plane; - for_each_plane_in_drmdev(window->kms.drmdev, plane) { + drm_resources_for_each_plane(window->kms.resources, plane) { if (!(plane->possible_crtcs & window->kms.crtc->bitmask)) { // Only query planes that are possible to connect to the CRTC we're using. continue; } - if (plane->type != kPrimary_DrmPlaneType && plane->type != kOverlay_DrmPlaneType) { + if (plane->type != DRM_PRIMARY_PLANE && plane->type != DRM_OVERLAY_PLANE) { // We explicitly only look for primary and overlay planes. continue; } diff --git a/src/window.h b/src/window.h index 7fe89d9b..1ff1f400 100644 --- a/src/window.h +++ b/src/window.h @@ -11,7 +11,7 @@ #define _FLUTTERPI_SRC_WINDOW_H #include "compositor_ng.h" -#include "modesetting.h" +#include "kms/resources.h" #include "pixel_format.h" #include "util/refcounting.h" @@ -65,6 +65,7 @@ struct window *kms_window_new( bool has_explicit_dimensions, int width_mm, int height_mm, bool has_forced_pixel_format, enum pixfmt forced_pixel_format, struct drmdev *drmdev, + struct drm_resources *resources, const char *desired_videomode // clang-format on ); From 88d5df0834720d7e842deaed1eb2ee58677c7102 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 21 Jun 2025 11:19:55 +0200 Subject: [PATCH 32/41] fix: inverted modifiers check in DRM plane allocation --- src/kms/req_builder.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kms/req_builder.c b/src/kms/req_builder.c index 173d583b..5e6a2eb8 100644 --- a/src/kms/req_builder.c +++ b/src/kms/req_builder.c @@ -115,7 +115,7 @@ static bool plane_qualifies( } if (has_modifier) { - if (drm_plane_supports_modified_formats(plane)) { + if (!drm_plane_supports_modified_formats(plane)) { // return false if we want a modified format but the plane doesn't support modified formats LOG_DRM_PLANE_ALLOCATION_DEBUG( " does not qualify: framebuffer has modifier %" PRIu64 " but plane does not support modified formats\n", From facea574cdb277a6432ff2d7eb3a02407ec7cc1a Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 21 Jun 2025 11:20:45 +0200 Subject: [PATCH 33/41] feat: use gbm_surface_create_with_modifiers2 To be able to specify more surface usage hints, i.e. `GBM_BO_USE_SCANOUT` and `GBM_BO_USE_RENDERING`. --- src/egl_gbm_render_surface.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/egl_gbm_render_surface.c b/src/egl_gbm_render_surface.c index cffaf3ed..aa2aeac9 100644 --- a/src/egl_gbm_render_surface.c +++ b/src/egl_gbm_render_surface.c @@ -150,13 +150,14 @@ static int egl_gbm_render_surface_init( int with_modifiers_errno = 0; gbm_surface = NULL; if (allowed_modifiers != NULL) { - gbm_surface = gbm_surface_create_with_modifiers( + gbm_surface = gbm_surface_create_with_modifiers2( gbm_device, size.x, size.y, get_pixfmt_info(pixel_format)->gbm_format, allowed_modifiers, - n_allowed_modifiers + n_allowed_modifiers, + GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING ); if (gbm_surface == NULL) { with_modifiers_errno = errno; From 8b746b7475fc72747ede1da85fc4d016ea78f27b Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 21 Jun 2025 11:23:33 +0200 Subject: [PATCH 34/41] fix: properly initialize drm drm_resources --- src/flutter-pi.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/flutter-pi.c b/src/flutter-pi.c index 0195c4db..946beb71 100644 --- a/src/flutter-pi.c +++ b/src/flutter-pi.c @@ -2410,6 +2410,11 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { goto fail_destroy_locales; } + resources = drmdev_query_resources(drmdev); + if (resources == NULL) { + goto fail_destroy_drmdev; + } + udev_unref(udev); gbm_device = drmdev_get_gbm_device(drmdev); From 403ec81a1ca2bc45e0ad973b599e03282532ffe9 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 21 Jun 2025 12:10:02 +0200 Subject: [PATCH 35/41] feat: frame scheduling Always do non-blocking atomic commits. To resolve issues with flutter trying to present frames too soon, while another frame was already sent to the kernel (i.e. comitted via KMS atomic commit), we either have to discard subsequent, too-early frames or queue them. In this case, we basically keep a 1-frame deep queue (or 2-frames, if you count the in-kernel waiting-for-scanout frame), with the 1 queued frame being displaced whenever a newer frame is available. (Similar to vulkans MAILBOX swapchain presentation mode) --- src/frame_scheduler.c | 81 ++++++++++++++++++++++++++++++++++++++----- src/frame_scheduler.h | 2 ++ src/window.c | 52 ++++++++++++++++++--------- 3 files changed, 111 insertions(+), 24 deletions(-) diff --git a/src/frame_scheduler.c b/src/frame_scheduler.c index 0adf7b9a..9571cde0 100644 --- a/src/frame_scheduler.c +++ b/src/frame_scheduler.c @@ -18,13 +18,21 @@ struct frame_scheduler { refcount_t n_refs; + pthread_mutex_t mutex; bool uses_frame_requests; enum present_mode present_mode; fl_vsync_callback_t vsync_cb; void *userdata; - pthread_mutex_t mutex; + bool waiting_for_scanout; + + bool has_scheduled_frame; + struct { + void_callback_t present_cb; + void_callback_t cancel_cb; + void *userdata; + } scheduled_frame; }; DEFINE_REF_OPS(frame_scheduler, n_refs) @@ -43,10 +51,16 @@ frame_scheduler_new(bool uses_frame_requests, enum present_mode present_mode, fl } scheduler->n_refs = REFCOUNT_INIT_1; + ASSERTED int ok = pthread_mutex_init(&scheduler->mutex, get_default_mutex_attrs()); + ASSERT_ZERO(ok); + scheduler->uses_frame_requests = uses_frame_requests; scheduler->present_mode = present_mode; scheduler->vsync_cb = vsync_cb; scheduler->userdata = userdata; + + scheduler->waiting_for_scanout = false; + scheduler->has_scheduled_frame = false; return scheduler; } @@ -118,14 +132,44 @@ void frame_scheduler_request_fb(struct frame_scheduler *scheduler, uint64_t scan UNIMPLEMENTED(); } -void frame_scheduler_present_frame(struct frame_scheduler *scheduler, void_callback_t present_cb, void *userdata, void_callback_t cancel_cb) { +void frame_scheduler_present_frame(struct frame_scheduler *scheduler, void_callback_t present_cb, void *userdata, void_callback_t cancel_cb) { ASSERT_NOT_NULL(scheduler); ASSERT_NOT_NULL(present_cb); - (void) scheduler; - (void) cancel_cb; + + frame_scheduler_lock(scheduler); - /// TODO: Implement - present_cb(userdata); + if (scheduler->waiting_for_scanout) { + void_callback_t cancel_prev_sched_frame = NULL; + void *prev_sched_frame_userdata = NULL; + + // We're already waiting for a scanout, so we can't present a frame right now. + // Wait till the previous frame is scanned out, and then present. + + if (scheduler->has_scheduled_frame) { + // If we already have a frame scheduled, cancel it. + cancel_prev_sched_frame = scheduler->scheduled_frame.cancel_cb; + prev_sched_frame_userdata = scheduler->scheduled_frame.userdata; + + memset(&scheduler->scheduled_frame, 0, sizeof scheduler->scheduled_frame); + } + + scheduler->has_scheduled_frame = true; + scheduler->scheduled_frame.present_cb = present_cb; + scheduler->scheduled_frame.cancel_cb = cancel_cb; + scheduler->scheduled_frame.userdata = userdata; + + frame_scheduler_unlock(scheduler); + + if (cancel_prev_sched_frame != NULL) { + cancel_prev_sched_frame(prev_sched_frame_userdata); + } + } else { + // We're not waiting for a scanout right now. + scheduler->waiting_for_scanout = true; + frame_scheduler_unlock(scheduler); + + present_cb(userdata); + } } void frame_scheduler_on_scanout(struct frame_scheduler *scheduler, bool has_timestamp, uint64_t timestamp_ns) { @@ -135,6 +179,27 @@ void frame_scheduler_on_scanout(struct frame_scheduler *scheduler, bool has_time (void) has_timestamp; (void) timestamp_ns; - /// TODO: Implement - UNIMPLEMENTED(); + void_callback_t present_cb = NULL; + void *userdata = NULL; + + frame_scheduler_lock(scheduler); + + if (scheduler->waiting_for_scanout) { + scheduler->waiting_for_scanout = false; + + if (scheduler->has_scheduled_frame) { + present_cb = scheduler->scheduled_frame.present_cb; + userdata = scheduler->scheduled_frame.userdata; + + memset(&scheduler->scheduled_frame, 0, sizeof scheduler->scheduled_frame); + + scheduler->has_scheduled_frame = false; + } + } + + frame_scheduler_unlock(scheduler); + + if (present_cb != NULL) { + present_cb(userdata); + } } diff --git a/src/frame_scheduler.h b/src/frame_scheduler.h index 229d9e59..4f66d8d6 100644 --- a/src/frame_scheduler.h +++ b/src/frame_scheduler.h @@ -80,4 +80,6 @@ void frame_scheduler_on_fb_released(struct frame_scheduler *scheduler, bool has_ */ void frame_scheduler_present_frame(struct frame_scheduler *scheduler, void_callback_t present_cb, void *userdata, void_callback_t cancel_cb); +void frame_scheduler_on_scanout(struct frame_scheduler *scheduler, bool has_timestamp, uint64_t timestamp_ns); + #endif // _FLUTTERPI_SRC_FRAME_SCHEDULER_H diff --git a/src/window.c b/src/window.c index 3a8b3bd2..d94be0a2 100644 --- a/src/window.c +++ b/src/window.c @@ -1074,42 +1074,61 @@ struct frame { struct tracer *tracer; struct drmdev *drmdev; struct kms_req *req; + struct frame_scheduler *scheduler; bool unset_should_apply_mode_on_commit; }; -UNUSED static void on_scanout(struct drmdev *drmdev, uint64_t vblank_ns, void *userdata) { - ASSERT_NOT_NULL(drmdev); - (void) drmdev; - (void) vblank_ns; - (void) userdata; +UNUSED static void on_scanout(uint64_t vblank_ns, void *userdata) { + ASSERT_NOT_NULL(userdata); + struct frame *frame = userdata; - /// TODO: What should we do here? + // This potentially presents a new frame. + frame_scheduler_on_scanout(frame->scheduler, true, vblank_ns); + + frame_scheduler_unref(frame->scheduler); + tracer_unref(frame->tracer); + kms_req_unref(frame->req); + drmdev_unref(frame->drmdev); + free(frame); } static void on_present_frame(void *userdata) { ASSERT_NOT_NULL(userdata); struct frame *frame = userdata; - TRACER_BEGIN(frame->tracer, "kms_req_commit_nonblocking"); - int ok = kms_req_commit_blocking(frame->req, frame->drmdev, NULL); - TRACER_END(frame->tracer, "kms_req_commit_nonblocking"); + int ok; + + { + // Keep our own reference on tracer, because the frame might be destroyed + // after kms_req_commit_nonblocking returns. + struct tracer *tracer = tracer_ref(frame->tracer); + + // The pageflip events might be handled on a different thread, so on_scanout + // might already be executed and the frame instance already freed once + // kms_req_commit_nonblocking returns. + TRACER_BEGIN(tracer, "kms_req_commit_nonblocking"); + ok = kms_req_commit_nonblocking(frame->req, frame->drmdev, on_scanout, frame, NULL); + TRACER_END(tracer, "kms_req_commit_nonblocking"); + + tracer_unref(tracer); + } if (ok != 0) { LOG_ERROR("Could not commit frame request.\n"); + frame_scheduler_unref(frame->scheduler); + tracer_unref(frame->tracer); + kms_req_unref(frame->req); + drmdev_unref(frame->drmdev); + free(frame); } - - tracer_unref(frame->tracer); - kms_req_unref(frame->req); - drmdev_unref(frame->drmdev); - free(frame); } static void on_cancel_frame(void *userdata) { - struct frame *frame; ASSERT_NOT_NULL(userdata); - frame = userdata; + struct frame *frame = userdata; + frame_scheduler_unref(frame->scheduler); tracer_unref(frame->tracer); kms_req_unref(frame->req); drmdev_unref(frame->drmdev); @@ -1226,6 +1245,7 @@ static int kms_window_push_composition_locked(struct window *window, struct fl_l frame->req = req; frame->tracer = tracer_ref(window->tracer); frame->drmdev = drmdev_ref(window->kms.drmdev); + frame->scheduler = frame_scheduler_ref(window->frame_scheduler); frame->unset_should_apply_mode_on_commit = window->kms.should_apply_mode; frame_scheduler_present_frame(window->frame_scheduler, on_present_frame, frame, on_cancel_frame); From 120432443f359dccc459bb87ae11e185a4b89ec9 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Wed, 4 Sep 2024 09:06:16 +0000 Subject: [PATCH 36/41] drmdev: call release callback before scanout callback Call the release callback for the commit that is no longer visible on screen before calling the scanout callback. The scanout callback might already start a new frame, and the release callback releases some resources that could be useful for building the new frame, so this helps avoid resource exhaustion. --- src/kms/drmdev.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/kms/drmdev.c b/src/kms/drmdev.c index f20078b4..92bb2795 100644 --- a/src/kms/drmdev.c +++ b/src/kms/drmdev.c @@ -306,13 +306,13 @@ static void drmdev_on_page_flip( ASSERT_ZERO(ok); } - if (cbs_copy.callbacks[cbs_copy.index].scanout_callback != NULL) { - cbs_copy.callbacks[cbs_copy.index].scanout_callback(vblank_ns, cbs_copy.callbacks[cbs_copy.index].scanout_callback_userdata); - } - if (cbs_copy.callbacks[cbs_copy.index].void_callback != NULL) { cbs_copy.callbacks[cbs_copy.index].void_callback(cbs_copy.callbacks[cbs_copy.index].void_callback_userdata); } + + if (cbs_copy.callbacks[cbs_copy.index].scanout_callback != NULL) { + cbs_copy.callbacks[cbs_copy.index].scanout_callback(vblank_ns, cbs_copy.callbacks[cbs_copy.index].scanout_callback_userdata); + } } static void on_page_flip( From 3ac7777a73f9924197998feb42fb8a6d9a0605d0 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Wed, 4 Sep 2024 09:06:54 +0000 Subject: [PATCH 37/41] kms req builder: rename destroy_cb -> release_cb --- src/kms/req_builder.c | 2 ++ src/kms/req_builder.h | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/kms/req_builder.c b/src/kms/req_builder.c index 5e6a2eb8..5a399f30 100644 --- a/src/kms/req_builder.c +++ b/src/kms/req_builder.c @@ -345,6 +345,7 @@ static void kms_req_builder_destroy(struct kms_req_builder *builder) { } if (builder->req != NULL) { drmModeAtomicFree(builder->req); + builder->req = NULL; } drm_resources_unref(builder->res); drmdev_unref(builder->drmdev); @@ -685,6 +686,7 @@ static void on_kms_req_release(void *userdata) { b->release_cb(b->release_cb_userdata); } + ASSERT(refcount_is_one(&b->n_refs)); kms_req_builder_unref(b); } diff --git a/src/kms/req_builder.h b/src/kms/req_builder.h index 7f2b7120..f9548241 100644 --- a/src/kms/req_builder.h +++ b/src/kms/req_builder.h @@ -201,6 +201,6 @@ struct kms_req *kms_req_builder_build(struct kms_req_builder *builder); int kms_req_commit_blocking(struct kms_req *req, struct drmdev *drmdev, uint64_t *vblank_ns_out); -int kms_req_commit_nonblocking(struct kms_req *req, struct drmdev *drmdev, kmsreq_scanout_cb_t scanout_cb, void *userdata, void_callback_t destroy_cb); +int kms_req_commit_nonblocking(struct kms_req *req, struct drmdev *drmdev, kmsreq_scanout_cb_t scanout_cb, void *userdata, void_callback_t release_cb); #endif // _FLUTTERPI_SRC_MODESETTING_REQ_BUILDER_H_ From a0fa32bf658443525e0d15f6e4c77ee5298c83a3 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Tue, 17 Sep 2024 11:07:03 +0000 Subject: [PATCH 38/41] fix: don't use drmIsKMS libdrm's drmIsKMS was added in libdrm 2.4.105: https://gitlab.freedesktop.org/mesa/libdrm/-/commit/523b3658aa8efa746417e916c987de23740ce313 Debian bullseye is still on 2.4.104, so it's unfortunately not present there. --- src/kms/drmdev.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/kms/drmdev.c b/src/kms/drmdev.c index 92bb2795..d92f33fe 100644 --- a/src/kms/drmdev.c +++ b/src/kms/drmdev.c @@ -110,13 +110,16 @@ static bool is_kms_device(const char *path, const struct drmdev_file_interface * return false; } - if (!drmIsKMS(fd)) { + // Ideally we'd use drmIsKMS() here, but it's not available everywhere. + + struct drm_mode_card_res res = { 0 }; + if (drmIoctl(fd, DRM_IOCTL_MODE_GETRESOURCES, &res) != 0) { interface->close(fd, fd_metadata, userdata); return false; } interface->close(fd, fd_metadata, userdata); - return true; + return res.count_crtcs > 0 && res.count_connectors > 0 && res.count_encoders > 0; } static void assert_rotations_work() { From 730475f93494f43bc25b5590d777e9eee35a0a2d Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 21 Jun 2025 12:32:36 +0200 Subject: [PATCH 39/41] fix: frame scheduler: fix incorrect tracking of waiting_for_scanout Fix waiting_for_scanout not being set when a frame is presented directly in the on_scanout callback, sometimes causing another frame to be presented, resulting in an EBUSY from drmModeAtomicCommit. --- src/frame_scheduler.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/frame_scheduler.c b/src/frame_scheduler.c index 9571cde0..682847e9 100644 --- a/src/frame_scheduler.c +++ b/src/frame_scheduler.c @@ -194,6 +194,7 @@ void frame_scheduler_on_scanout(struct frame_scheduler *scheduler, bool has_time memset(&scheduler->scheduled_frame, 0, sizeof scheduler->scheduled_frame); scheduler->has_scheduled_frame = false; + scheduler->waiting_for_scanout = true; } } From 23d39c2e5560b9812180d6ff062d70634464d390 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Wed, 4 Sep 2024 09:03:11 +0000 Subject: [PATCH 40/41] frame scheduling: forward flutter vsync requests to frame scheduler --- src/flutter-pi.c | 104 +++++++++++------------------------------- src/frame_scheduler.c | 4 +- 2 files changed, 27 insertions(+), 81 deletions(-) diff --git a/src/flutter-pi.c b/src/flutter-pi.c index 946beb71..6675c4de 100644 --- a/src/flutter-pi.c +++ b/src/flutter-pi.c @@ -262,6 +262,8 @@ struct flutterpi { bool session_active; char *desired_videomode; + + struct frame_scheduler *scheduler; }; struct device_id_and_fd { @@ -470,16 +472,13 @@ struct frame_req { }; static int on_deferred_begin_frame(void *userdata) { - FlutterEngineResult engine_result; - struct frame_req *req; - ASSERT_NOT_NULL(userdata); - req = userdata; + struct frame_req *req = userdata; assert(flutterpi_runs_platform_tasks_on_current_thread(req->flutterpi)); TRACER_INSTANT(req->flutterpi->tracer, "FlutterEngineOnVsync"); - engine_result = req->flutterpi->flutter.procs.OnVsync(req->flutterpi->flutter.engine, req->baton, req->vblank_ns, req->next_vblank_ns); + FlutterEngineResult engine_result = req->flutterpi->flutter.procs.OnVsync(req->flutterpi->flutter.engine, req->baton, req->vblank_ns, req->next_vblank_ns); free(req); @@ -491,88 +490,39 @@ static int on_deferred_begin_frame(void *userdata) { return 0; } -UNUSED static void on_begin_frame(void *userdata, uint64_t vblank_ns, uint64_t next_vblank_ns) { - FlutterEngineResult engine_result; - struct frame_req *req; - int ok; - +static void on_begin_frame(void *userdata, intptr_t baton, uint64_t vblank_ns, uint64_t next_vblank_ns) { ASSERT_NOT_NULL(userdata); - req = userdata; + struct flutterpi *flutterpi = userdata; - if (flutterpi_runs_platform_tasks_on_current_thread(req->flutterpi)) { - TRACER_INSTANT(req->flutterpi->tracer, "FlutterEngineOnVsync"); + if (flutterpi_runs_platform_tasks_on_current_thread(flutterpi)) { + TRACER_INSTANT(flutterpi->tracer, "FlutterEngineOnVsync"); - engine_result = req->flutterpi->flutter.procs.OnVsync(req->flutterpi->flutter.engine, req->baton, vblank_ns, next_vblank_ns); + FlutterEngineResult engine_result = flutterpi->flutter.procs.OnVsync(flutterpi->flutter.engine, baton, vblank_ns, next_vblank_ns); if (engine_result != kSuccess) { LOG_ERROR("Couldn't signal frame begin to flutter engine. FlutterEngineOnVsync: %s\n", FLUTTER_RESULT_TO_STRING(engine_result)); - goto fail_free_req; } - - free(req); } else { + struct frame_req *req = malloc(sizeof(struct frame_req)); + req->flutterpi = flutterpi; + req->baton = baton; req->vblank_ns = vblank_ns; req->next_vblank_ns = next_vblank_ns; - ok = flutterpi_post_platform_task(on_deferred_begin_frame, req); + + int ok = flutterpi_post_platform_task(on_deferred_begin_frame, req); if (ok != 0) { LOG_ERROR("Couldn't defer signalling frame begin.\n"); - goto fail_free_req; + free(req); } } - - return; - -fail_free_req: - free(req); - return; } -/// Called on some flutter internal thread to request a frame, -/// and also get the vblank timestamp of the pageflip preceding that frame. -UNUSED static void on_frame_request(void *userdata, intptr_t baton) { - FlutterEngineResult engine_result; - struct flutterpi *flutterpi; - struct frame_req *req; - int ok; - +static void on_frame_timings_request(void *userdata, intptr_t baton) { ASSERT_NOT_NULL(userdata); - flutterpi = userdata; + struct flutterpi *flutterpi = userdata; - TRACER_INSTANT(flutterpi->tracer, "on_frame_request"); + TRACER_INSTANT(flutterpi->tracer, "on_frame_timings_request"); - req = malloc(sizeof *req); - if (req == NULL) { - LOG_ERROR("Out of memory\n"); - return; - } - - req->flutterpi = flutterpi; - req->baton = baton; - req->vblank_ns = get_monotonic_time(); - req->next_vblank_ns = req->vblank_ns + (uint64_t) (1000000000.0 / compositor_get_refresh_rate(flutterpi->compositor)); - - if (flutterpi_runs_platform_tasks_on_current_thread(req->flutterpi)) { - TRACER_INSTANT(req->flutterpi->tracer, "FlutterEngineOnVsync"); - - engine_result = - req->flutterpi->flutter.procs.OnVsync(req->flutterpi->flutter.engine, req->baton, req->vblank_ns, req->next_vblank_ns); - if (engine_result != kSuccess) { - LOG_ERROR("Couldn't signal frame begin to flutter engine. FlutterEngineOnVsync: %s\n", FLUTTER_RESULT_TO_STRING(engine_result)); - goto fail_free_req; - } - - free(req); - } else { - ok = flutterpi_post_platform_task(on_deferred_begin_frame, req); - if (ok != 0) { - LOG_ERROR("Couldn't defer signalling frame begin.\n"); - goto fail_free_req; - } - } - - return; - -fail_free_req: - free(req); + frame_scheduler_on_fl_vsync_request(flutterpi->scheduler, baton); } UNUSED static FlutterTransformation on_get_transformation(void *userdata) { @@ -1356,7 +1306,7 @@ static FlutterEngine create_flutter_engine( project_args.update_semantics_custom_action_callback = NULL; project_args.persistent_cache_path = paths->asset_bundle_path; project_args.is_persistent_cache_read_only = false; - project_args.vsync_callback = NULL; // on_frame_request, /* broken since 2.2, kinda * + project_args.vsync_callback = on_frame_timings_request; project_args.custom_dart_entrypoint = NULL; project_args.custom_task_runners = &custom_task_runners; project_args.shutdown_dart_vm_when_done = true; @@ -2257,7 +2207,6 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { enum renderer_type renderer_type; struct texture_registry *texture_registry; struct plugin_registry *plugin_registry; - struct frame_scheduler *scheduler; struct flutter_paths *paths; struct view_geometry geometry; FlutterEngineAOTData aot_data; @@ -2278,7 +2227,7 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { char **engine_argv, *desired_videomode; int ok, engine_argc, wakeup_fd; - fpi = malloc(sizeof *fpi); + fpi = calloc(1, sizeof *fpi); if (fpi == NULL) { return NULL; } @@ -2430,8 +2379,8 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { goto fail_destroy_drmdev; } - scheduler = frame_scheduler_new(false, kDoubleBufferedVsync_PresentMode, NULL, NULL); - if (scheduler == NULL) { + fpi->scheduler = frame_scheduler_new(true, kDoubleBufferedVsync_PresentMode, on_begin_frame, fpi); + if (fpi->scheduler == NULL) { LOG_ERROR("Couldn't create frame scheduler.\n"); goto fail_unref_tracer; } @@ -2480,7 +2429,7 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { if (cmd_args.dummy_display) { window = dummy_window_new( tracer, - scheduler, + fpi->scheduler, renderer_type, gl_renderer, vk_renderer, @@ -2494,7 +2443,7 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { window = kms_window_new( // clang-format off tracer, - scheduler, + fpi->scheduler, renderer_type, gl_renderer, vk_renderer, @@ -2658,7 +2607,6 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { } // We don't need these anymore. - frame_scheduler_unref(scheduler); window_unref(window); free(cmd_args.bundle_path); @@ -2720,7 +2668,7 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { } fail_unref_scheduler: - frame_scheduler_unref(scheduler); + frame_scheduler_unref(fpi->scheduler); fail_unref_tracer: tracer_unref(tracer); diff --git a/src/frame_scheduler.c b/src/frame_scheduler.c index 682847e9..1c1e7a1d 100644 --- a/src/frame_scheduler.c +++ b/src/frame_scheduler.c @@ -40,12 +40,10 @@ DEFINE_STATIC_LOCK_OPS(frame_scheduler, mutex) struct frame_scheduler * frame_scheduler_new(bool uses_frame_requests, enum present_mode present_mode, fl_vsync_callback_t vsync_cb, void *userdata) { - struct frame_scheduler *scheduler; - // uses_frame_requests? => vsync_cb != NULL assert(!uses_frame_requests || vsync_cb != NULL); - scheduler = malloc(sizeof *scheduler); + struct frame_scheduler *scheduler = calloc(1, sizeof *scheduler); if (scheduler == NULL) { return NULL; } From 54cc8fd369df7261db4d60da6edbf3faf77558e1 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 21 Jun 2025 12:58:52 +0200 Subject: [PATCH 41/41] WIP: feat: implement frame timing Make frame_scheduler able to time frames, e.g. tell flutter when to start rendering a frame when it receives a frame request. For now, if no frame is currently waiting to be scanned out, this just responds to the frame request with the current frame and a 60Hz frame interval, i.e. starts the frame immediately. If a frame is waiting to be scanned out, this waits until the scanout to reply to the frame timings request. (With the timestamp of the scanout being used as the frame start time in the reply to the frame timings request) If another frame is already queued after that, the scheduler will wait until that other queued frame is scanned out to reply to the frame timings request. --- src/frame_scheduler.c | 47 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/frame_scheduler.c b/src/frame_scheduler.c index 1c1e7a1d..20913880 100644 --- a/src/frame_scheduler.c +++ b/src/frame_scheduler.c @@ -33,6 +33,8 @@ struct frame_scheduler { void_callback_t cancel_cb; void *userdata; } scheduled_frame; + + intptr_t pending_frame_timings_request; }; DEFINE_REF_OPS(frame_scheduler, n_refs) @@ -59,6 +61,9 @@ frame_scheduler_new(bool uses_frame_requests, enum present_mode present_mode, fl scheduler->waiting_for_scanout = false; scheduler->has_scheduled_frame = false; + + scheduler->pending_frame_timings_request = 0; + return scheduler; } @@ -94,12 +99,28 @@ void frame_scheduler_on_fl_vsync_request(struct frame_scheduler *scheduler, intp // as well if we draw too many frames at once. (Especially considering one framebuffer is probably busy with scanout right now) // - /// TODO: Implement - /// For now, just unconditionally reply if (scheduler->present_mode == kTripleBufferedVsync_PresentMode) { - scheduler->vsync_cb(scheduler->userdata, vsync_baton, 0, 0); + /// TODO: Query actual frame interval and vblank timestamp here + uint64_t now = get_monotonic_time(); + uint64_t now_plus_frame_interval = now + 1000000000/60; + scheduler->vsync_cb(scheduler->userdata, vsync_baton, now, now_plus_frame_interval); } else if (scheduler->present_mode == kDoubleBufferedVsync_PresentMode) { - scheduler->vsync_cb(scheduler->userdata, vsync_baton, 0, 0); + frame_scheduler_lock(scheduler); + + if (scheduler->waiting_for_scanout) { + // Reply to the frame timings request once the current frame has been scanned out. + ASSERT_ZERO(scheduler->pending_frame_timings_request); + scheduler->pending_frame_timings_request = vsync_baton; + } else { + /// TODO: Query actual frame interval and vblank timestamp here + uint64_t now = get_monotonic_time(); + uint64_t now_plus_frame_interval = now + 1000000000/60; + scheduler->vsync_cb(scheduler->userdata, vsync_baton, now, now_plus_frame_interval); + } + + frame_scheduler_unlock(scheduler); + } else { + UNREACHABLE(); } } @@ -179,6 +200,7 @@ void frame_scheduler_on_scanout(struct frame_scheduler *scheduler, bool has_time void_callback_t present_cb = NULL; void *userdata = NULL; + intptr_t pending_frame_timings_request = 0; frame_scheduler_lock(scheduler); @@ -193,11 +215,28 @@ void frame_scheduler_on_scanout(struct frame_scheduler *scheduler, bool has_time scheduler->has_scheduled_frame = false; scheduler->waiting_for_scanout = true; + } else if (scheduler->pending_frame_timings_request) { + // only reply to the frame timings request if we don't have another frame already + // pending. We only want to start a new frame once the previous frame has been scanned out + /// TODO: Maybe do start it before the previous one has been scanned out? + pending_frame_timings_request = scheduler->pending_frame_timings_request; + scheduler->pending_frame_timings_request = 0; } } frame_scheduler_unlock(scheduler); + if (pending_frame_timings_request) { + if (!has_timestamp) { + timestamp_ns = get_monotonic_time(); + } + + /// TODO: Use actual frame interval here + uint64_t now_plus_frame_interval = timestamp_ns + 1000000000 / 60; + + scheduler->vsync_cb(scheduler->userdata, pending_frame_timings_request, timestamp_ns, now_plus_frame_interval); + } + if (present_cb != NULL) { present_cb(userdata); }