gstreamer/subprojects/gst-plugins-good/ext/qt6/gstqsg6material.cc
Matthew Waters e2d388000c qt(6)/material: ensure that we always update the context in setBuffer()
Scenario is that there are two (or more) GstGLContext's wrapping Qt's GL
context from either multiple qml(6)glsink or qml(6)glsrc elements.  Call flow is this:

1. material 1 setBuffer()
2. material 1 bind()
3. material 2 setBuffer()
4. material 2 bind()

If the call to setBuffer() reuses the same buffer as previous call, then the
qt context is not updated in the material.  If however the previously used qt
context by the material had been deactivated or freed, then bind() would fail
and could result in a critical like so:

gst_gl_context_thread_add: assertion 'context->priv->active_thread == g_thread_self ()' failed

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/7970>
2024-11-26 22:57:51 +00:00

694 lines
18 KiB
C++

/*
* GStreamer
* Copyright (C) 2023 Matthew Waters <matthew@centricular.com>
*
* 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.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <vector>
#include <stdio.h>
#include <gst/video/video.h>
#include <gst/gl/gl.h>
#include <gst/gl/gstglfuncs.h>
#include "gstqsg6material.h"
#include <QtGui/private/qrhi_p.h>
#define GST_CAT_DEFAULT gst_qsg_texture_debug
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
/* matrix colour conversion code from vkvideoconvert.c */
typedef struct
{
double dm[4][4];
} Matrix4;
static void
matrix_debug (const Matrix4 * s)
{
GST_DEBUG ("[%f %f %f %f]", s->dm[0][0], s->dm[0][1], s->dm[0][2],
s->dm[0][3]);
GST_DEBUG ("[%f %f %f %f]", s->dm[1][0], s->dm[1][1], s->dm[1][2],
s->dm[1][3]);
GST_DEBUG ("[%f %f %f %f]", s->dm[2][0], s->dm[2][1], s->dm[2][2],
s->dm[2][3]);
GST_DEBUG ("[%f %f %f %f]", s->dm[3][0], s->dm[3][1], s->dm[3][2],
s->dm[3][3]);
}
static void
matrix_to_float (const Matrix4 * m, float *ret)
{
int i, j;
for (i = 0; i < 4; i++) {
for (j = 0; j < 4; j++) {
ret[j * 4 + i] = m->dm[i][j];
}
}
}
static void
matrix_set_identity (Matrix4 * m)
{
int i, j;
for (i = 0; i < 4; i++) {
for (j = 0; j < 4; j++) {
m->dm[i][j] = (i == j);
}
}
}
static void
matrix_copy (Matrix4 * d, const Matrix4 * s)
{
gint i, j;
for (i = 0; i < 4; i++)
for (j = 0; j < 4; j++)
d->dm[i][j] = s->dm[i][j];
}
/* Perform 4x4 matrix multiplication:
* - @dst@ = @a@ * @b@
* - @dst@ may be a pointer to @a@ andor @b@
*/
static void
matrix_multiply (Matrix4 * dst, Matrix4 * a, Matrix4 * b)
{
Matrix4 tmp;
int i, j, k;
for (i = 0; i < 4; i++) {
for (j = 0; j < 4; j++) {
double x = 0;
for (k = 0; k < 4; k++) {
x += a->dm[i][k] * b->dm[k][j];
}
tmp.dm[i][j] = x;
}
}
matrix_copy (dst, &tmp);
}
static void
matrix_offset_components (Matrix4 * m, double a1, double a2, double a3)
{
Matrix4 a;
matrix_set_identity (&a);
a.dm[0][3] = a1;
a.dm[1][3] = a2;
a.dm[2][3] = a3;
matrix_debug (&a);
matrix_multiply (m, &a, m);
}
static void
matrix_scale_components (Matrix4 * m, double a1, double a2, double a3)
{
Matrix4 a;
matrix_set_identity (&a);
a.dm[0][0] = a1;
a.dm[1][1] = a2;
a.dm[2][2] = a3;
matrix_multiply (m, &a, m);
}
static void
matrix_YCbCr_to_RGB (Matrix4 * m, double Kr, double Kb)
{
double Kg = 1.0 - Kr - Kb;
Matrix4 k = {
{
{1., 0., 2 * (1 - Kr), 0.},
{1., -2 * Kb * (1 - Kb) / Kg, -2 * Kr * (1 - Kr) / Kg, 0.},
{1., 2 * (1 - Kb), 0., 0.},
{0., 0., 0., 1.},
}
};
matrix_multiply (m, &k, m);
}
static void
convert_to_RGB (GstVideoInfo *info, Matrix4 * m)
{
{
const GstVideoFormatInfo *uinfo;
gint offset[4], scale[4], depth[4];
guint i;
uinfo = gst_video_format_get_info (GST_VIDEO_INFO_FORMAT (info));
/* bring color components to [0..1.0] range */
gst_video_color_range_offsets (info->colorimetry.range, uinfo, offset,
scale);
for (i = 0; i < uinfo->n_components; i++)
depth[i] = (1 << uinfo->depth[i]) - 1;
matrix_offset_components (m, -offset[0] / (float) depth[0],
-offset[1] / (float) depth[1], -offset[2] / (float) depth[2]);
matrix_scale_components (m, depth[0] / ((float) scale[0]),
depth[1] / ((float) scale[1]), depth[2] / ((float) scale[2]));
GST_DEBUG ("to RGB scale/offset matrix");
matrix_debug (m);
}
if (GST_VIDEO_INFO_IS_YUV (info)) {
gdouble Kr, Kb;
if (gst_video_color_matrix_get_Kr_Kb (info->colorimetry.matrix, &Kr, &Kb))
matrix_YCbCr_to_RGB (m, Kr, Kb);
GST_DEBUG ("to RGB matrix");
matrix_debug (m);
}
}
class GstQSGTexture : public QSGTexture {
public:
GstQSGTexture(QRhiTexture *);
~GstQSGTexture();
qint64 comparisonKey() const override;
bool hasAlphaChannel() const override;
bool hasMipmaps() const override { return false; };
bool isAtlasTexture() const override { return false; };
QSize textureSize() const override;
QRhiTexture *rhiTexture() const override;
private:
QRhiTexture *m_texture;
bool m_has_alpha;
};
GstQSGTexture::GstQSGTexture(QRhiTexture * texture)
: m_texture(texture)
{
switch (texture->format()) {
case QRhiTexture::RGBA8:
#if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0))
case QRhiTexture::RGB10A2:
#endif
case QRhiTexture::RGBA16F:
case QRhiTexture::RGBA32F:
this->m_has_alpha = true;
break;
default:
this->m_has_alpha = false;
}
}
GstQSGTexture::~GstQSGTexture()
{
if (m_texture) {
m_texture->deleteLater();
m_texture = nullptr;
}
}
qint64
GstQSGTexture::comparisonKey() const
{
if (this->m_texture)
return qint64(qintptr(this->m_texture));
return qint64(qintptr(this));
}
bool
GstQSGTexture::hasAlphaChannel() const
{
return m_has_alpha;
}
QSize
GstQSGTexture::textureSize() const
{
// XXX: currently unused
return QSize(0, 0);
}
QRhiTexture *
GstQSGTexture::rhiTexture() const
{
return m_texture;
}
class GstQSG6MaterialShader : public QSGMaterialShader {
public:
GstQSG6MaterialShader(GstVideoFormat v_format, GstGLTextureTarget target);
~GstQSG6MaterialShader();
bool updateUniformData(RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override;
void updateSampledImage(RenderState &state, int binding, QSGTexture **texture, QSGMaterial *newMaterial, QSGMaterial *) override;
private:
GstVideoFormat v_format;
QSGTexture *m_textures[GST_VIDEO_MAX_PLANES];
};
GstQSG6MaterialShader::GstQSG6MaterialShader(GstVideoFormat v_format,
GstGLTextureTarget target)
: v_format(v_format)
{
const gchar *frag_shader;
setShaderFileName(VertexStage, ":/org/freedesktop/gstreamer/qml6/vertex.vert.qsb");
switch (v_format) {
case GST_VIDEO_FORMAT_RGBA:
case GST_VIDEO_FORMAT_BGRA:
case GST_VIDEO_FORMAT_RGB:
if (target == GST_GL_TEXTURE_TARGET_EXTERNAL_OES)
frag_shader = ":/org/freedesktop/gstreamer/qml6/RGBA.frag.qsb.external";
else
frag_shader = ":/org/freedesktop/gstreamer/qml6/RGBA.frag.qsb";
break;
case GST_VIDEO_FORMAT_YV12:
frag_shader = ":/org/freedesktop/gstreamer/qml6/YUV_TRIPLANAR.frag.qsb";
break;
case GST_VIDEO_FORMAT_NV12:
frag_shader = ":/org/freedesktop/gstreamer/qml6/YUV_BIPLANAR.frag.qsb";
break;
default:
g_assert_not_reached ();
}
GST_DEBUG("load fragment shader: %s", frag_shader);
setShaderFileName(FragmentStage, frag_shader);
m_textures[0] = nullptr;
m_textures[1] = nullptr;
m_textures[2] = nullptr;
m_textures[3] = nullptr;
}
GstQSG6MaterialShader::~GstQSG6MaterialShader()
{
for (int i = 0; i < 4; i++) {
if (m_textures[i]) {
delete m_textures[i];
m_textures[i] = nullptr;
}
}
}
bool
GstQSG6MaterialShader::updateUniformData(RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial)
{
const GstVideoFormatInfo *finfo = gst_video_format_get_info (v_format);
bool changed = false;
QByteArray *buf = state.uniformData();
Q_ASSERT(buf->size() >= 84);
GST_TRACE ("%p new material %p old material %p", this, newMaterial, oldMaterial);
if (state.isMatrixDirty()) {
const QMatrix4x4 m = state.combinedMatrix();
memcpy(buf->data(), m.constData(), 64);
changed = true;
}
if (state.isOpacityDirty()) {
const float opacity = state.opacity();
memcpy(buf->data() + 144, &opacity, 4);
changed = true;
}
auto *mat = static_cast<GstQSG6Material *>(newMaterial);
if (oldMaterial != newMaterial || mat->uniforms.dirty) {
memcpy(buf->data() + 64, &mat->uniforms.input_swizzle, 4 * sizeof (int));
memcpy(buf->data() + 80, mat->uniforms.color_matrix.constData(), 64);
mat->uniforms.dirty = false;
changed = true;
}
for (guint i = 0; i < GST_VIDEO_MAX_PLANES; i++) {
if (this->m_textures[i]) {
delete this->m_textures[i];
this->m_textures[i] = nullptr;
}
if (i < finfo->n_planes)
this->m_textures[i] = mat->bind(this, state.rhi(), state.resourceUpdateBatch(), i, v_format);
}
return changed;
}
void
GstQSG6MaterialShader::updateSampledImage(RenderState &state, int binding, QSGTexture **texture,
QSGMaterial *newMaterial, QSGMaterial *)
{
*texture = this->m_textures[binding - 1];
GST_TRACE ("%p binding:%d texture %p", this, binding, *texture);
}
#define DEFINE_MATERIAL(format) \
class G_PASTE(GstQSG6Material_,format) : public GstQSG6Material { \
public: \
G_PASTE(GstQSG6Material_,format)(); \
~G_PASTE(GstQSG6Material_,format)(); \
QSGMaterialType *type() const override { static QSGMaterialType type; return &type; }; \
}; \
G_PASTE(GstQSG6Material_,format)::G_PASTE(GstQSG6Material_,format)() {} \
G_PASTE(GstQSG6Material_,format)::~G_PASTE(GstQSG6Material_,format)() {}
DEFINE_MATERIAL(RGBA_SWIZZLE);
DEFINE_MATERIAL(YUV_TRIPLANAR);
DEFINE_MATERIAL(YUV_BIPLANAR);
GstQSG6Material *
GstQSG6Material::new_for_format(GstVideoFormat format)
{
const GstVideoFormatInfo *finfo = gst_video_format_get_info (format);
if (GST_VIDEO_FORMAT_INFO_IS_RGB (finfo) && finfo->n_planes == 1) {
return static_cast<GstQSG6Material *>(new GstQSG6Material_RGBA_SWIZZLE());
}
switch (format) {
case GST_VIDEO_FORMAT_YV12:
return static_cast<GstQSG6Material *>(new GstQSG6Material_YUV_TRIPLANAR());
case GST_VIDEO_FORMAT_NV12:
return static_cast<GstQSG6Material *>(new GstQSG6Material_YUV_BIPLANAR());
default:
g_assert_not_reached ();
}
}
GstQSG6Material::GstQSG6Material ()
{
static gsize _debug;
if (g_once_init_enter (&_debug)) {
GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "qtqsg6material", 0,
"Qt6 Scenegraph Material");
g_once_init_leave (&_debug, 1);
}
g_weak_ref_init (&this->qt_context_ref_, NULL);
gst_video_info_init (&this->v_info);
memset (&this->v_frame, 0, sizeof (this->v_frame));
this->buffer_ = NULL;
this->buffer_was_bound = false;
this->sync_buffer_ = gst_buffer_new ();
this->uniforms.dirty = true;
}
GstQSG6Material::~GstQSG6Material ()
{
g_weak_ref_clear (&this->qt_context_ref_);
gst_buffer_replace (&this->buffer_, NULL);
gst_buffer_replace (&this->sync_buffer_, NULL);
this->buffer_was_bound = false;
if (this->v_frame.buffer) {
gst_video_frame_unmap (&this->v_frame);
memset (&this->v_frame, 0, sizeof (this->v_frame));
}
}
bool
GstQSG6Material::compatibleWith(GstVideoInfo * v_info)
{
if (GST_VIDEO_INFO_FORMAT (&this->v_info) != GST_VIDEO_INFO_FORMAT (v_info))
return false;
return true;
}
QSGMaterialShader *
GstQSG6Material::createShader(QSGRendererInterface::RenderMode renderMode) const
{
GstVideoFormat v_format = GST_VIDEO_INFO_FORMAT (&this->v_info);
GstGLTextureTarget target = this->tex_target;
return new GstQSG6MaterialShader(v_format, target);
}
/* only called from the streaming thread with scene graph thread blocked */
void
GstQSG6Material::setCaps (GstCaps * caps)
{
GstStructure *s;
const gchar *target_str;
GST_LOG ("%p setCaps %" GST_PTR_FORMAT, this, caps);
gst_video_info_from_caps (&this->v_info, caps);
s = gst_caps_get_structure (caps, 0);
target_str = gst_structure_get_string (s, "texture-target");
if (!target_str)
target_str = GST_GL_TEXTURE_TARGET_2D_STR;
this->tex_target = gst_gl_texture_target_from_string(target_str);
}
/* only called from the streaming thread with scene graph thread blocked */
gboolean
GstQSG6Material::setBuffer (GstBuffer * buffer)
{
GstGLContext *qt_context = gst_gl_context_get_current ();
GST_LOG ("%p setBuffer %" GST_PTR_FORMAT " with qt context %" GST_PTR_FORMAT,
this, buffer, qt_context);
/* FIXME: update more state here */
g_weak_ref_set (&this->qt_context_ref_, qt_context);
if (!gst_buffer_replace (&this->buffer_, buffer))
return FALSE;
this->buffer_was_bound = false;
if (this->v_frame.buffer) {
gst_video_frame_unmap (&this->v_frame);
memset (&this->v_frame, 0, sizeof (this->v_frame));
}
if (this->buffer_) {
if (!gst_video_frame_map (&this->v_frame, &this->v_info, this->buffer_,
(GstMapFlags) (GST_MAP_READ | GST_MAP_GL))) {
g_assert_not_reached ();
return FALSE;
}
gst_gl_video_format_swizzle(GST_VIDEO_INFO_FORMAT (&this->v_info), this->uniforms.input_swizzle);
Matrix4 m;
float matrix_data[16] = { 0.0, };
matrix_set_identity (&m);
convert_to_RGB (&this->v_info, &m);
matrix_debug (&m);
matrix_to_float (&m, matrix_data);
this->uniforms.color_matrix = QMatrix4x4(matrix_data);
this->uniforms.dirty = true;
}
return TRUE;
}
/* only called from the streaming thread with scene graph thread blocked */
GstBuffer *
GstQSG6Material::getBuffer (bool * was_bound)
{
GstBuffer *buffer = NULL;
if (this->buffer_)
buffer = gst_buffer_ref (this->buffer_);
if (was_bound)
*was_bound = this->buffer_was_bound;
return buffer;
}
void
GstQSG6Material::setFiltering(QSGTexture::Filtering filtering)
{
m_filtering = filtering;
}
static QRhiTexture::Format
video_format_to_rhi_format (GstVideoFormat format, guint plane)
{
switch (format) {
case GST_VIDEO_FORMAT_RGBA:
case GST_VIDEO_FORMAT_BGRA:
case GST_VIDEO_FORMAT_RGB:
return QRhiTexture::RGBA8;
case GST_VIDEO_FORMAT_YV12:
return QRhiTexture::R8;
case GST_VIDEO_FORMAT_NV12:
return (plane == 0 ? QRhiTexture::R8 : QRhiTexture::RG8);
default:
g_assert_not_reached ();
}
}
static int
video_format_to_texel_size (GstVideoFormat format, guint plane)
{
switch (format) {
case GST_VIDEO_FORMAT_RGBA:
case GST_VIDEO_FORMAT_BGRA:
case GST_VIDEO_FORMAT_RGB:
return 4;
case GST_VIDEO_FORMAT_YV12:
return 1;
case GST_VIDEO_FORMAT_NV12:
return (plane == 0 ? 1 : 2);
default:
g_assert_not_reached ();
}
}
QSGTexture *
GstQSG6Material::bind(GstQSG6MaterialShader *shader, QRhi * rhi, QRhiResourceUpdateBatch *res_updates, guint plane, GstVideoFormat v_format)
{
GstGLContext *qt_context = NULL, *context;
GstMemory *mem;
GstGLMemory *gl_mem;
GstGLSyncMeta *sync_meta;
gboolean use_dummy_tex = TRUE;
guint tex_id;
GstQSGTexture *ret;
QRhiTexture *rhi_tex;
QSize tex_size;
QRhiTexture::Flags flags = {};
if (!this->buffer_)
goto out;
if (GST_VIDEO_INFO_FORMAT (&this->v_info) == GST_VIDEO_FORMAT_UNKNOWN)
goto out;
qt_context = GST_GL_CONTEXT (g_weak_ref_get (&this->qt_context_ref_));
if (!qt_context)
goto out;
GST_DEBUG ("%p attempting to bind with context %" GST_PTR_FORMAT, this, qt_context);
mem = gst_buffer_peek_memory (this->buffer_, plane);
g_assert (gst_is_gl_memory (mem));
gl_mem = (GstGLMemory *) mem;
context = ((GstGLBaseMemory *)mem)->context;
/* Texture was successfully bound, so we do not need
* to use the dummy texture */
use_dummy_tex = FALSE;
this->buffer_was_bound = true;
tex_id = *(guint *) this->v_frame.data[plane];
tex_size = QSize(gst_gl_memory_get_texture_width(gl_mem), gst_gl_memory_get_texture_height (gl_mem));
if (gl_mem->tex_target == GST_GL_TEXTURE_TARGET_EXTERNAL_OES)
flags |= QRhiTexture::ExternalOES;
rhi_tex = rhi->newTexture (video_format_to_rhi_format (v_format, plane), tex_size, 1, flags);
rhi_tex->createFrom({(guint64) tex_id, 0});
sync_meta = gst_buffer_get_gl_sync_meta (this->sync_buffer_);
if (!sync_meta)
sync_meta = gst_buffer_add_gl_sync_meta (context, this->sync_buffer_);
gst_gl_sync_meta_set_sync_point (sync_meta, context);
gst_gl_sync_meta_wait (sync_meta, qt_context);
GST_LOG ("%p binding GL texture %u (%s) for plane %d",
this, tex_id, gst_gl_texture_target_to_string(gl_mem->tex_target), plane);
out:
if (G_UNLIKELY (use_dummy_tex)) {
/* Create dummy texture if not already present.
* Use the Qt RHI functions instead of the GstGL ones.
*/
/* Make this a black 64x64 pixel RGBA texture.
* This size and format is supported pretty much everywhere, so these
* are a safe pick. (64 pixel sidelength must be supported according
* to the GLES2 spec, table 6.18.) */
const int tex_sidelength = 64;
rhi_tex = rhi->newTexture (video_format_to_rhi_format (v_format, plane), QSize(tex_sidelength, tex_sidelength), 1, {});
g_assert (rhi_tex->create());
int ts = video_format_to_texel_size (v_format, plane);
QByteArray dummy_data (tex_sidelength * tex_sidelength * ts, 0);
char *data = dummy_data.data();
switch (v_format) {
case GST_VIDEO_FORMAT_RGBA:
case GST_VIDEO_FORMAT_BGRA:
case GST_VIDEO_FORMAT_RGB:
for (gsize j = 0; j < tex_sidelength; j++) {
for (gsize k = 0; k < tex_sidelength; k++) {
data[(j * tex_sidelength + k) * ts + 3] = 0xFF; // opaque
}
}
break;
case GST_VIDEO_FORMAT_YV12:
if (plane == 1 || plane == 2) {
for (gsize j = 0; j < tex_sidelength; j++) {
for (gsize k = 0; k < tex_sidelength; k++) {
data[(j * tex_sidelength + k) * ts + 0] = 0x7F;
}
}
}
break;
case GST_VIDEO_FORMAT_NV12:
if (plane == 1) {
for (gsize j = 0; j < tex_sidelength; j++) {
for (gsize k = 0; k < tex_sidelength; k++) {
data[(j * tex_sidelength + k) * ts + 0] = 0x7F;
data[(j * tex_sidelength + k) * ts + 1] = 0x7F;
}
}
}
break;
default:
g_assert_not_reached ();
break;
}
QRhiTextureSubresourceUploadDescription sub_desc(dummy_data);
QRhiTextureUploadEntry entry(0, 0, sub_desc);
QRhiTextureUploadDescription desc(entry);
res_updates->uploadTexture(rhi_tex, desc);
GST_LOG ("%p binding for plane %d fallback dummy Qt texture", this, plane);
}
ret = new GstQSGTexture(rhi_tex);
ret->setFiltering(m_filtering);
gst_clear_object (&qt_context);
return static_cast<QSGTexture *>(ret);
}