/* GStreamer * Copyright (C) 2025 Seungha Yang * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, * Boston, MA 02110-1301, USA. */ /** * SECTION:element-d3d12overlaycompositor * @title: d3d12overlaycompositor * * A Direct3D12-based overlay composing element * * Since: 1.28 */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "gstd3d12overlaycompositor.h" #include "gstd3d12overlayblender.h" #include "gstd3d12pluginutils.h" #include #include #include /* *INDENT-OFF* */ using namespace Microsoft::WRL; /* *INDENT-ON* */ GST_DEBUG_CATEGORY_STATIC (gst_d3d12_overlay_compositor_debug); #define GST_CAT_DEFAULT gst_d3d12_overlay_compositor_debug static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE_WITH_FEATURES (GST_CAPS_FEATURE_MEMORY_D3D12_MEMORY, GST_D3D12_ALL_FORMATS) "; " GST_VIDEO_CAPS_MAKE_WITH_FEATURES (GST_CAPS_FEATURE_MEMORY_D3D12_MEMORY "," GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION, GST_D3D12_ALL_FORMATS))); static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE_WITH_FEATURES (GST_CAPS_FEATURE_MEMORY_D3D12_MEMORY, GST_D3D12_ALL_FORMATS) "; " GST_VIDEO_CAPS_MAKE_WITH_FEATURES (GST_CAPS_FEATURE_MEMORY_D3D12_MEMORY "," GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION, GST_D3D12_ALL_FORMATS))); enum BlendMode { BLEND_MODE_PASSTHROUGH, BLEND_MODE_BLEND, BLEND_MODE_CONVERT_BLEND, }; /* *INDENT-OFF* */ struct OverlayBlendCtx { OverlayBlendCtx (GstD3D12Device * dev) { device = (GstD3D12Device *) gst_object_ref (dev); auto device_handle = gst_d3d12_device_get_device_handle (device); ca_pool = gst_d3d12_cmd_alloc_pool_new (device_handle, D3D12_COMMAND_LIST_TYPE_DIRECT); } ~OverlayBlendCtx () { if (fence_val > 0) { gst_d3d12_device_fence_wait (device, D3D12_COMMAND_LIST_TYPE_DIRECT, fence_val); } if (blend_pool) gst_buffer_pool_set_active (blend_pool, FALSE); gst_clear_object (&blend_pool); gst_clear_object (&ca_pool); gst_clear_object (&pre_conv); gst_clear_object (&post_conv); gst_clear_object (&device); } GstD3D12Device *device = nullptr; ComPtr cl; GstD3D12CmdAllocPool *ca_pool; guint64 fence_val = 0; GstD3D12OverlayBlender *blender = nullptr; GstBufferPool *blend_pool = nullptr; GstVideoInfo origin_info; GstVideoInfo blend_info; GstD3D12Converter *pre_conv = nullptr; GstD3D12Converter *post_conv = nullptr; }; struct GstD3D12OverlayCompositorPrivate { GstD3D12OverlayCompositorPrivate () { fence_data_pool = gst_d3d12_fence_data_pool_new (); } ~GstD3D12OverlayCompositorPrivate () { gst_object_unref (fence_data_pool); } GstD3D12FenceDataPool *fence_data_pool; std::shared_ptr ctx; gboolean downstream_supports_meta = FALSE; BlendMode blend_mode = BLEND_MODE_PASSTHROUGH; }; /* *INDENT-ON* */ struct _GstD3D12OverlayCompositor { GstD3D12BaseFilter parent; GstD3D12OverlayCompositorPrivate *priv; }; #define gst_d3d12_overlay_compositor_parent_class parent_class G_DEFINE_TYPE (GstD3D12OverlayCompositor, gst_d3d12_overlay_compositor, GST_TYPE_D3D12_BASE_FILTER); static void gst_d3d12_overlay_compositor_finalize (GObject * object); static gboolean gst_d3d12_overlay_compositor_stop (GstBaseTransform * trans); static GstCaps *gst_d3d12_overlay_compositor_transform_caps (GstBaseTransform * trans, GstPadDirection direction, GstCaps * caps, GstCaps * filter); static GstCaps *gst_d3d12_overlay_compositor_fixate_caps (GstBaseTransform * trans, GstPadDirection direction, GstCaps * caps, GstCaps * othercaps); static GstFlowReturn gst_d3d12_overlay_compositor_transform (GstBaseTransform * trans, GstBuffer * inbuf, GstBuffer * outbuf); static GstFlowReturn gst_d3d12_overlay_compositor_generate_output (GstBaseTransform * trans, GstBuffer ** buffer); static gboolean gst_d3d12_overlay_compositor_set_info (GstD3D12BaseFilter * filter, GstD3D12Device * device, GstCaps * incaps, GstVideoInfo * in_info, GstCaps * outcaps, GstVideoInfo * out_info); static gboolean gst_d3d12_overlay_compositor_propose_allocation (GstD3D12BaseFilter * filter, GstD3D12Device * device, GstQuery * decide_query, GstQuery * query); static void gst_d3d12_overlay_compositor_class_init (GstD3D12OverlayCompositorClass * klass) { auto object_class = G_OBJECT_CLASS (klass); auto element_class = GST_ELEMENT_CLASS (klass); auto trans_class = GST_BASE_TRANSFORM_CLASS (klass); auto filter_class = GST_D3D12_BASE_FILTER_CLASS (klass); object_class->finalize = gst_d3d12_overlay_compositor_finalize; gst_element_class_set_static_metadata (element_class, "Direct3D12 Overlay Compositor", "Filter/Effect/Video/Hardware", "Blend overlay into stream", "Seungha Yang "); gst_element_class_add_static_pad_template (element_class, &src_template); gst_element_class_add_static_pad_template (element_class, &sink_template); trans_class->passthrough_on_same_caps = FALSE; trans_class->stop = GST_DEBUG_FUNCPTR (gst_d3d12_overlay_compositor_stop); trans_class->transform_caps = GST_DEBUG_FUNCPTR (gst_d3d12_overlay_compositor_transform_caps); trans_class->fixate_caps = GST_DEBUG_FUNCPTR (gst_d3d12_overlay_compositor_fixate_caps); trans_class->transform = GST_DEBUG_FUNCPTR (gst_d3d12_overlay_compositor_transform); trans_class->generate_output = GST_DEBUG_FUNCPTR (gst_d3d12_overlay_compositor_generate_output); filter_class->set_info = GST_DEBUG_FUNCPTR (gst_d3d12_overlay_compositor_set_info); filter_class->propose_allocation = GST_DEBUG_FUNCPTR (gst_d3d12_overlay_compositor_propose_allocation); GST_DEBUG_CATEGORY_INIT (gst_d3d12_overlay_compositor_debug, "d3d12overlaycompositor", 0, "d3d12overlaycompositor"); } static void gst_d3d12_overlay_compositor_init (GstD3D12OverlayCompositor * self) { self->priv = new GstD3D12OverlayCompositorPrivate (); } static void gst_d3d12_overlay_compositor_finalize (GObject * object) { auto self = GST_D3D12_OVERLAY_COMPOSITOR (object); delete self->priv; G_OBJECT_CLASS (parent_class)->finalize (object); } static gboolean gst_d3d12_overlay_compositor_stop (GstBaseTransform * trans) { auto self = GST_D3D12_OVERLAY_COMPOSITOR (trans); auto priv = self->priv; priv->ctx = nullptr; return GST_BASE_TRANSFORM_CLASS (parent_class)->stop (trans); } static gboolean gst_d3d12_overlay_compositor_propose_allocation (GstD3D12BaseFilter * filter, GstD3D12Device * device, GstQuery * decide_query, GstQuery * query) { if (!GST_D3D12_BASE_FILTER_CLASS (parent_class)->propose_allocation (filter, device, decide_query, query)) { return FALSE; } gst_query_add_allocation_meta (query, GST_VIDEO_OVERLAY_COMPOSITION_META_API_TYPE, nullptr); return TRUE; } static GstCaps * add_feature (GstCaps * caps) { auto new_caps = gst_caps_new_empty (); auto caps_size = gst_caps_get_size (caps); for (guint i = 0; i < caps_size; i++) { auto s = gst_caps_get_structure (caps, i); auto f = gst_caps_features_copy (gst_caps_get_features (caps, i)); auto c = gst_caps_new_full (gst_structure_copy (s), nullptr); if (!gst_caps_features_is_any (f) && !gst_caps_features_contains (f, GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION)) { gst_caps_features_add (f, GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION); } gst_caps_set_features (c, 0, f); gst_caps_append (new_caps, c); } return new_caps; } static GstCaps * remove_feature (GstCaps * caps) { auto new_caps = gst_caps_new_empty (); auto caps_size = gst_caps_get_size (caps); for (guint i = 0; i < caps_size; i++) { auto s = gst_caps_get_structure (caps, i); auto f = gst_caps_features_copy (gst_caps_get_features (caps, i)); auto c = gst_caps_new_full (gst_structure_copy (s), nullptr); gst_caps_features_remove (f, GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION); gst_caps_set_features (c, 0, f); gst_caps_append (new_caps, c); } return new_caps; } static GstCaps * gst_d3d12_overlay_compositor_transform_caps (GstBaseTransform * trans, GstPadDirection direction, GstCaps * caps, GstCaps * filter) { GstCaps *result, *tmp; GST_DEBUG_OBJECT (trans, "Transforming caps %" GST_PTR_FORMAT " in direction %s", caps, (direction == GST_PAD_SINK) ? "sink" : "src"); if (direction == GST_PAD_SINK) { tmp = remove_feature (caps); tmp = gst_caps_merge (tmp, gst_caps_ref (caps)); } else { tmp = add_feature (caps); tmp = gst_caps_merge (gst_caps_ref (caps), tmp); } if (filter) { result = gst_caps_intersect_full (filter, tmp, GST_CAPS_INTERSECT_FIRST); gst_caps_unref (tmp); } else { result = tmp; } GST_DEBUG_OBJECT (trans, "returning caps: %" GST_PTR_FORMAT, result); return result; } static GstCaps * gst_d3d12_overlay_compositor_fixate_caps (GstBaseTransform * trans, GstPadDirection direction, GstCaps * caps, GstCaps * othercaps) { GstCaps *overlay_caps = nullptr; auto caps_size = gst_caps_get_size (othercaps); GstCaps *ret; GST_DEBUG_OBJECT (trans, "Fixate caps in direction %s, caps %" GST_PTR_FORMAT ", other caps %" GST_PTR_FORMAT, (direction == GST_PAD_SINK) ? "sink" : "src", caps, othercaps); /* Prefer overlaycomposition caps */ for (guint i = 0; i < caps_size; i++) { auto f = gst_caps_get_features (othercaps, i); if (f && !gst_caps_features_is_any (f) && gst_caps_features_contains (f, GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION)) { auto s = gst_caps_get_structure (othercaps, i); overlay_caps = gst_caps_new_full (gst_structure_copy (s), nullptr); gst_caps_set_features_simple (overlay_caps, gst_caps_features_copy (f)); break; } } if (overlay_caps) { gst_caps_unref (othercaps); ret = gst_caps_fixate (overlay_caps); } else { ret = gst_caps_fixate (othercaps); } GST_DEBUG_OBJECT (trans, "Fixated caps %" GST_PTR_FORMAT, ret); return ret; } static gboolean gst_d3d12_overlay_compositor_set_info (GstD3D12BaseFilter * filter, GstD3D12Device * device, GstCaps * incaps, GstVideoInfo * in_info, GstCaps * outcaps, GstVideoInfo * out_info) { auto self = GST_D3D12_OVERLAY_COMPOSITOR (filter); auto priv = self->priv; priv->ctx = nullptr; priv->blend_mode = BLEND_MODE_PASSTHROUGH; auto features = gst_caps_get_features (outcaps, 0); if (gst_caps_features_contains (features, GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION)) { /* Let downstream blend */ priv->blend_mode = BLEND_MODE_PASSTHROUGH; } else { auto format = GST_VIDEO_INFO_FORMAT (in_info); GstVideoFormat blend_format = GST_VIDEO_FORMAT_UNKNOWN; GstVideoColorRange range = GST_VIDEO_COLOR_RANGE_0_255; switch (format) { case GST_VIDEO_FORMAT_RGBA: case GST_VIDEO_FORMAT_BGRA: case GST_VIDEO_FORMAT_RGBA64_LE: case GST_VIDEO_FORMAT_VUYA: priv->blend_mode = BLEND_MODE_BLEND; range = in_info->colorimetry.range; blend_format = format; break; default: priv->blend_mode = BLEND_MODE_CONVERT_BLEND; if (GST_VIDEO_INFO_IS_YUV (in_info)) { if (GST_VIDEO_INFO_COMP_DEPTH (in_info, 0) <= 8) blend_format = GST_VIDEO_FORMAT_VUYA; else blend_format = GST_VIDEO_FORMAT_RGBA64_LE; } else { if (GST_VIDEO_INFO_COMP_DEPTH (in_info, 0) <= 8) blend_format = GST_VIDEO_FORMAT_RGBA; else blend_format = GST_VIDEO_FORMAT_RGBA64_LE; } break; } auto ctx = std::make_shared < OverlayBlendCtx > (device); ctx->origin_info = *in_info; gst_video_info_set_format (&ctx->blend_info, blend_format, in_info->width, in_info->height); ctx->blend_info.colorimetry.range = range; ctx->blender = gst_d3d12_overlay_blender_new (device, &ctx->blend_info); if (priv->blend_mode == BLEND_MODE_CONVERT_BLEND) { ctx->pre_conv = gst_d3d12_converter_new (device, nullptr, &ctx->origin_info, &ctx->blend_info, nullptr, nullptr, nullptr); ctx->post_conv = gst_d3d12_converter_new (device, nullptr, &ctx->blend_info, &ctx->origin_info, nullptr, nullptr, nullptr); } auto blend_caps = gst_video_info_to_caps (&ctx->blend_info); ctx->blend_pool = gst_d3d12_buffer_pool_new (device); auto config = gst_buffer_pool_get_config (ctx->blend_pool); gst_buffer_pool_config_set_params (config, blend_caps, 0, 0, 0); gst_caps_unref (blend_caps); if (!gst_buffer_pool_set_config (ctx->blend_pool, config)) { GST_ERROR_OBJECT (self, "Couldn't set config"); return FALSE; } if (!gst_buffer_pool_set_active (ctx->blend_pool, TRUE)) { GST_ERROR_OBJECT (self, "Couldn't set config"); return FALSE; } priv->ctx = ctx; } GST_DEBUG_OBJECT (self, "Selected blend mode: %d", priv->blend_mode); return TRUE; } static gboolean foreach_meta (GstBuffer * buffer, GstMeta ** meta, gpointer user_data) { if ((*meta)->info->api == GST_VIDEO_OVERLAY_COMPOSITION_META_API_TYPE) *meta = nullptr; return TRUE; } static gboolean buffer_has_overlay_rect (GstBuffer * buf) { gboolean has_rect = FALSE; gpointer state = nullptr; GstMeta *meta; while ((meta = gst_buffer_iterate_meta_filtered (buf, &state, GST_VIDEO_OVERLAY_COMPOSITION_META_API_TYPE)) != nullptr) { auto ometa = (GstVideoOverlayCompositionMeta *) meta; if (gst_video_overlay_composition_n_rectangles (ometa->overlay) > 0) { has_rect = TRUE; break; } } return has_rect; } static GstFlowReturn gst_d3d12_overlay_compositor_generate_output (GstBaseTransform * trans, GstBuffer ** buffer) { auto self = GST_D3D12_OVERLAY_COMPOSITOR (trans); auto priv = self->priv; if (!trans->queued_buf) return GST_FLOW_OK; auto buf = trans->queued_buf; trans->queued_buf = nullptr; auto has_rect = buffer_has_overlay_rect (buf); if (priv->blend_mode == BLEND_MODE_PASSTHROUGH || !has_rect) { *buffer = buf; return GST_FLOW_OK; } auto & ctx = priv->ctx; gst_d3d12_overlay_blender_upload (ctx->blender, buf); GstD3D12CmdAlloc *gst_ca; if (!gst_d3d12_cmd_alloc_pool_acquire (ctx->ca_pool, &gst_ca)) { GST_ERROR_OBJECT (self, "Couldn't acquire command allocator"); gst_buffer_unref (buf); return GST_FLOW_ERROR; } auto ca = gst_d3d12_cmd_alloc_get_handle (gst_ca); auto hr = ca->Reset (); if (!gst_d3d12_result (hr, ctx->device)) { GST_ERROR_OBJECT (self, "Couldn't reset command allocator"); gst_d3d12_cmd_alloc_unref (gst_ca); gst_buffer_unref (buf); return GST_FLOW_ERROR; } if (!ctx->cl) { auto device = gst_d3d12_device_get_device_handle (ctx->device); hr = device->CreateCommandList (0, D3D12_COMMAND_LIST_TYPE_DIRECT, ca, nullptr, IID_PPV_ARGS (&ctx->cl)); if (!gst_d3d12_result (hr, priv->ctx->device)) { GST_ERROR_OBJECT (self, "Couldn't create command list"); gst_d3d12_cmd_alloc_unref (gst_ca); gst_buffer_unref (buf); return GST_FLOW_ERROR; } } else { hr = ctx->cl->Reset (ca, nullptr); if (!gst_d3d12_result (hr, ctx->device)) { GST_ERROR_OBJECT (self, "Couldn't reset command list"); gst_d3d12_cmd_alloc_unref (gst_ca); gst_buffer_unref (buf); return GST_FLOW_ERROR; } } GstD3D12FenceData *fence_data; gst_d3d12_fence_data_pool_acquire (priv->fence_data_pool, &fence_data); gst_d3d12_fence_data_push (fence_data, FENCE_NOTIFY_MINI_OBJECT (gst_ca)); buf = gst_buffer_make_writable (buf); if (priv->blend_mode == BLEND_MODE_BLEND) { /* Ensure writable memory */ GstD3D12Frame frame; if (!gst_d3d12_frame_map (&frame, &priv->ctx->origin_info, buf, GST_MAP_WRITE_D3D12, GST_D3D12_FRAME_MAP_FLAG_RTV)) { GST_WARNING_OBJECT (self, "Couldn't map buffer"); GstBuffer *fallback_buf = nullptr; gst_buffer_pool_acquire_buffer (ctx->blend_pool, &fallback_buf, nullptr); if (!fallback_buf) { GST_ERROR_OBJECT (self, "Couldn't acquire fallback buffer"); ctx->cl->Close (); gst_d3d12_fence_data_unref (fence_data); gst_buffer_unref (buf); return GST_FLOW_ERROR; } if (!gst_d3d12_buffer_copy_into (fallback_buf, buf, &ctx->origin_info)) { GST_ERROR_OBJECT (self, "Couldn't copy to fallback buffer"); ctx->cl->Close (); gst_d3d12_fence_data_unref (fence_data); gst_buffer_unref (buf); gst_buffer_unref (fallback_buf); return GST_FLOW_ERROR; } gst_buffer_copy_into (fallback_buf, buf, GST_BUFFER_COPY_METADATA, 0, -1); gst_buffer_unref (buf); buf = fallback_buf; } else { gst_d3d12_frame_unmap (&frame); } gst_d3d12_overlay_blender_draw (ctx->blender, buf, fence_data, ctx->cl.Get ()); } else { GstBuffer *blend_buf = nullptr; GstBuffer *out_buf = nullptr; gst_buffer_pool_acquire_buffer (ctx->blend_pool, &blend_buf, nullptr); if (!blend_buf) { GST_ERROR_OBJECT (self, "Couldn't acquire blend buffer"); ctx->cl->Close (); gst_d3d12_fence_data_unref (fence_data); gst_buffer_unref (buf); return GST_FLOW_ERROR; } auto ret = GST_BASE_TRANSFORM_CLASS (parent_class)->prepare_output_buffer (trans, buf, &out_buf); if (ret != GST_FLOW_OK) { ctx->cl->Close (); gst_d3d12_fence_data_unref (fence_data); gst_buffer_unref (buf); gst_buffer_unref (blend_buf); return ret; } gst_d3d12_converter_convert_buffer (ctx->pre_conv, buf, blend_buf, fence_data, ctx->cl.Get (), TRUE); gst_d3d12_overlay_blender_draw (ctx->blender, blend_buf, fence_data, ctx->cl.Get ()); auto dmem = (GstD3D12Memory *) gst_buffer_peek_memory (blend_buf, 0); auto resource = gst_d3d12_memory_get_resource_handle (dmem); auto barrier = CD3DX12_RESOURCE_BARRIER::Transition (resource, D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); ctx->cl->ResourceBarrier (1, &barrier); gst_d3d12_converter_convert_buffer (ctx->post_conv, blend_buf, out_buf, fence_data, ctx->cl.Get (), FALSE); /* fence data will hold all source buffers */ gst_buffer_unref (buf); gst_buffer_unref (blend_buf); buf = out_buf; } hr = ctx->cl->Close (); if (!gst_d3d12_result (hr, ctx->device)) { GST_ERROR_OBJECT (self, "Couldn't close command list"); gst_d3d12_fence_data_unref (fence_data); gst_buffer_unref (buf); return GST_FLOW_ERROR; } ID3D12CommandList *cmd_list[] = { priv->ctx->cl.Get () }; hr = gst_d3d12_device_execute_command_lists (ctx->device, D3D12_COMMAND_LIST_TYPE_DIRECT, 1, cmd_list, &ctx->fence_val); if (!gst_d3d12_result (hr, ctx->device)) { GST_ERROR_OBJECT (self, "Couldn't execute command list"); gst_d3d12_fence_data_unref (fence_data); gst_buffer_unref (buf); return GST_FLOW_ERROR; } auto fence = gst_d3d12_device_get_fence_handle (ctx->device, D3D12_COMMAND_LIST_TYPE_DIRECT); gst_d3d12_buffer_set_fence (buf, fence, priv->ctx->fence_val, FALSE); gst_d3d12_device_set_fence_notify (ctx->device, D3D12_COMMAND_LIST_TYPE_DIRECT, priv->ctx->fence_val, FENCE_NOTIFY_MINI_OBJECT (fence_data)); gst_buffer_foreach_meta (buf, foreach_meta, nullptr); *buffer = buf; return GST_FLOW_OK; } static GstFlowReturn gst_d3d12_overlay_compositor_transform (GstBaseTransform * trans, GstBuffer * inbuf, GstBuffer * outbuf) { g_assert_not_reached (); return GST_FLOW_ERROR; }