libkwineffects: simplify gltexture
Instead of using custom private classes for taking care of backend specific stuff, store that directly in the GLTexture subclasses
This commit is contained in:
parent
16fb2848ed
commit
20b4f26045
8 changed files with 58 additions and 150 deletions
|
@ -418,41 +418,29 @@ void EglSurfaceTextureX11::update(const QRegion ®ion)
|
|||
}
|
||||
|
||||
EglPixmapTexture::EglPixmapTexture(EglBackend *backend)
|
||||
: GLTexture(std::make_unique<EglPixmapTexturePrivate>(this, backend))
|
||||
{
|
||||
}
|
||||
|
||||
bool EglPixmapTexture::create(SurfacePixmapX11 *texture)
|
||||
{
|
||||
Q_D(EglPixmapTexture);
|
||||
return d->create(texture);
|
||||
}
|
||||
|
||||
EglPixmapTexturePrivate::EglPixmapTexturePrivate(EglPixmapTexture *texture, EglBackend *backend)
|
||||
: q(texture)
|
||||
: GLTexture(GL_TEXTURE_2D)
|
||||
, m_backend(backend)
|
||||
{
|
||||
m_target = GL_TEXTURE_2D;
|
||||
}
|
||||
|
||||
EglPixmapTexturePrivate::~EglPixmapTexturePrivate()
|
||||
EglPixmapTexture::~EglPixmapTexture()
|
||||
{
|
||||
if (m_image != EGL_NO_IMAGE_KHR) {
|
||||
eglDestroyImageKHR(m_backend->eglDisplay(), m_image);
|
||||
}
|
||||
}
|
||||
|
||||
bool EglPixmapTexturePrivate::create(SurfacePixmapX11 *pixmap)
|
||||
bool EglPixmapTexture::create(SurfacePixmapX11 *pixmap)
|
||||
{
|
||||
const xcb_pixmap_t nativePixmap = pixmap->pixmap();
|
||||
if (nativePixmap == XCB_NONE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
glGenTextures(1, &m_texture);
|
||||
q->setWrapMode(GL_CLAMP_TO_EDGE);
|
||||
q->setFilter(GL_LINEAR);
|
||||
q->bind();
|
||||
glGenTextures(1, &d->m_texture);
|
||||
setWrapMode(GL_CLAMP_TO_EDGE);
|
||||
setFilter(GL_LINEAR);
|
||||
bind();
|
||||
const EGLint attribs[] = {
|
||||
EGL_IMAGE_PRESERVED_KHR, EGL_TRUE,
|
||||
EGL_NONE};
|
||||
|
@ -464,18 +452,18 @@ bool EglPixmapTexturePrivate::create(SurfacePixmapX11 *pixmap)
|
|||
|
||||
if (EGL_NO_IMAGE_KHR == m_image) {
|
||||
qCDebug(KWIN_X11STANDALONE) << "failed to create egl image";
|
||||
q->unbind();
|
||||
unbind();
|
||||
return false;
|
||||
}
|
||||
glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, static_cast<GLeglImageOES>(m_image));
|
||||
q->unbind();
|
||||
q->setContentTransform(TextureTransform::MirrorY);
|
||||
m_size = pixmap->size();
|
||||
updateMatrix();
|
||||
unbind();
|
||||
setContentTransform(TextureTransform::MirrorY);
|
||||
d->m_size = pixmap->size();
|
||||
d->updateMatrix();
|
||||
return true;
|
||||
}
|
||||
|
||||
void EglPixmapTexturePrivate::onDamage()
|
||||
void EglPixmapTexture::onDamage()
|
||||
{
|
||||
if (options->isGlStrictBinding()) {
|
||||
// This is just implemented to be consistent with
|
||||
|
@ -483,7 +471,6 @@ void EglPixmapTexturePrivate::onDamage()
|
|||
eglWaitNative(EGL_CORE_NATIVE_ENGINE);
|
||||
glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, static_cast<GLeglImageOES>(m_image));
|
||||
}
|
||||
GLTexturePrivate::onDamage();
|
||||
}
|
||||
|
||||
} // namespace KWin
|
||||
|
|
|
@ -78,27 +78,14 @@ class EglPixmapTexture : public GLTexture
|
|||
{
|
||||
public:
|
||||
explicit EglPixmapTexture(EglBackend *backend);
|
||||
~EglPixmapTexture() override;
|
||||
|
||||
bool create(SurfacePixmapX11 *texture);
|
||||
|
||||
private:
|
||||
Q_DECLARE_PRIVATE(EglPixmapTexture)
|
||||
};
|
||||
|
||||
class EglPixmapTexturePrivate : public GLTexturePrivate
|
||||
{
|
||||
public:
|
||||
EglPixmapTexturePrivate(EglPixmapTexture *texture, EglBackend *backend);
|
||||
~EglPixmapTexturePrivate() override;
|
||||
|
||||
bool create(SurfacePixmapX11 *texture);
|
||||
|
||||
protected:
|
||||
void onDamage() override;
|
||||
|
||||
private:
|
||||
EglPixmapTexture *q;
|
||||
EglBackend *m_backend;
|
||||
EglBackend *const m_backend;
|
||||
EGLImageKHR m_image = EGL_NO_IMAGE_KHR;
|
||||
};
|
||||
|
||||
|
|
|
@ -871,24 +871,13 @@ void GlxSurfaceTextureX11::update(const QRegion ®ion)
|
|||
}
|
||||
|
||||
GlxPixmapTexture::GlxPixmapTexture(GlxBackend *backend)
|
||||
: GLTexture(std::make_unique<GlxPixmapTexturePrivate>(this, backend))
|
||||
{
|
||||
}
|
||||
|
||||
bool GlxPixmapTexture::create(SurfacePixmapX11 *texture)
|
||||
{
|
||||
Q_D(GlxPixmapTexture);
|
||||
return d->create(texture);
|
||||
}
|
||||
|
||||
GlxPixmapTexturePrivate::GlxPixmapTexturePrivate(GlxPixmapTexture *texture, GlxBackend *backend)
|
||||
: m_backend(backend)
|
||||
, q(texture)
|
||||
: GLTexture(GL_TEXTURE_2D)
|
||||
, m_backend(backend)
|
||||
, m_glxPixmap(None)
|
||||
{
|
||||
}
|
||||
|
||||
GlxPixmapTexturePrivate::~GlxPixmapTexturePrivate()
|
||||
GlxPixmapTexture::~GlxPixmapTexture()
|
||||
{
|
||||
if (m_glxPixmap != None) {
|
||||
if (!options->isGlStrictBinding()) {
|
||||
|
@ -899,16 +888,7 @@ GlxPixmapTexturePrivate::~GlxPixmapTexturePrivate()
|
|||
}
|
||||
}
|
||||
|
||||
void GlxPixmapTexturePrivate::onDamage()
|
||||
{
|
||||
if (options->isGlStrictBinding() && m_glxPixmap) {
|
||||
glXReleaseTexImageEXT(m_backend->display(), m_glxPixmap, GLX_FRONT_LEFT_EXT);
|
||||
glXBindTexImageEXT(m_backend->display(), m_glxPixmap, GLX_FRONT_LEFT_EXT, nullptr);
|
||||
}
|
||||
GLTexturePrivate::onDamage();
|
||||
}
|
||||
|
||||
bool GlxPixmapTexturePrivate::create(SurfacePixmapX11 *texture)
|
||||
bool GlxPixmapTexture::create(SurfacePixmapX11 *texture)
|
||||
{
|
||||
if (texture->pixmap() == XCB_NONE || texture->size().isEmpty() || texture->visual() == XCB_NONE) {
|
||||
return false;
|
||||
|
@ -920,39 +900,47 @@ bool GlxPixmapTexturePrivate::create(SurfacePixmapX11 *texture)
|
|||
}
|
||||
|
||||
if (info.texture_targets & GLX_TEXTURE_2D_BIT_EXT) {
|
||||
m_target = GL_TEXTURE_2D;
|
||||
m_scale.setWidth(1.0f / m_size.width());
|
||||
m_scale.setHeight(1.0f / m_size.height());
|
||||
d->m_target = GL_TEXTURE_2D;
|
||||
d->m_scale.setWidth(1.0f / d->m_size.width());
|
||||
d->m_scale.setHeight(1.0f / d->m_size.height());
|
||||
} else {
|
||||
Q_ASSERT(info.texture_targets & GLX_TEXTURE_RECTANGLE_BIT_EXT);
|
||||
|
||||
m_target = GL_TEXTURE_RECTANGLE;
|
||||
m_scale.setWidth(1.0f);
|
||||
m_scale.setHeight(1.0f);
|
||||
d->m_target = GL_TEXTURE_RECTANGLE;
|
||||
d->m_scale.setWidth(1.0f);
|
||||
d->m_scale.setHeight(1.0f);
|
||||
}
|
||||
|
||||
const int attrs[] = {
|
||||
GLX_TEXTURE_FORMAT_EXT, info.bind_texture_format,
|
||||
GLX_MIPMAP_TEXTURE_EXT, false,
|
||||
GLX_TEXTURE_TARGET_EXT, m_target == GL_TEXTURE_2D ? GLX_TEXTURE_2D_EXT : GLX_TEXTURE_RECTANGLE_EXT,
|
||||
GLX_TEXTURE_TARGET_EXT, d->m_target == GL_TEXTURE_2D ? GLX_TEXTURE_2D_EXT : GLX_TEXTURE_RECTANGLE_EXT,
|
||||
0};
|
||||
|
||||
m_glxPixmap = glXCreatePixmap(m_backend->display(), info.fbconfig, texture->pixmap(), attrs);
|
||||
m_size = texture->size();
|
||||
q->setContentTransform(info.y_inverted ? TextureTransform::MirrorY : TextureTransforms());
|
||||
m_canUseMipmaps = false;
|
||||
d->m_size = texture->size();
|
||||
setContentTransform(info.y_inverted ? TextureTransform::MirrorY : TextureTransforms());
|
||||
d->m_canUseMipmaps = false;
|
||||
|
||||
glGenTextures(1, &m_texture);
|
||||
glGenTextures(1, &d->m_texture);
|
||||
|
||||
q->setDirty();
|
||||
q->setFilter(GL_LINEAR);
|
||||
q->setWrapMode(GL_CLAMP_TO_EDGE);
|
||||
setDirty();
|
||||
setFilter(GL_LINEAR);
|
||||
setWrapMode(GL_CLAMP_TO_EDGE);
|
||||
|
||||
glBindTexture(m_target, m_texture);
|
||||
glBindTexture(d->m_target, d->m_texture);
|
||||
glXBindTexImageEXT(m_backend->display(), m_glxPixmap, GLX_FRONT_LEFT_EXT, nullptr);
|
||||
|
||||
updateMatrix();
|
||||
d->updateMatrix();
|
||||
return true;
|
||||
}
|
||||
|
||||
void GlxPixmapTexture::onDamage()
|
||||
{
|
||||
if (options->isGlStrictBinding() && m_glxPixmap) {
|
||||
glXReleaseTexImageEXT(m_backend->display(), m_glxPixmap, GLX_FRONT_LEFT_EXT);
|
||||
glXBindTexImageEXT(m_backend->display(), m_glxPixmap, GLX_FRONT_LEFT_EXT, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
|
|
@ -134,34 +134,21 @@ private:
|
|||
X11StandaloneBackend *m_backend;
|
||||
std::unique_ptr<VsyncMonitor> m_vsyncMonitor;
|
||||
std::unique_ptr<GlxLayer> m_layer;
|
||||
friend class GlxPixmapTexturePrivate;
|
||||
friend class GlxPixmapTexture;
|
||||
};
|
||||
|
||||
class GlxPixmapTexture final : public GLTexture
|
||||
{
|
||||
public:
|
||||
explicit GlxPixmapTexture(GlxBackend *backend);
|
||||
~GlxPixmapTexture();
|
||||
|
||||
bool create(SurfacePixmapX11 *texture);
|
||||
|
||||
private:
|
||||
Q_DECLARE_PRIVATE(GlxPixmapTexture)
|
||||
};
|
||||
|
||||
class GlxPixmapTexturePrivate final : public GLTexturePrivate
|
||||
{
|
||||
public:
|
||||
GlxPixmapTexturePrivate(GlxPixmapTexture *texture, GlxBackend *backend);
|
||||
~GlxPixmapTexturePrivate() override;
|
||||
|
||||
bool create(SurfacePixmapX11 *texture);
|
||||
|
||||
protected:
|
||||
void onDamage() override;
|
||||
|
||||
private:
|
||||
GlxBackend *m_backend;
|
||||
GlxPixmapTexture *q;
|
||||
GlxBackend *const m_backend;
|
||||
GLXPixmap m_glxPixmap;
|
||||
};
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ EGLImageTexture::EGLImageTexture(::EGLDisplay display, EGLImage image, uint text
|
|||
, m_image(image)
|
||||
, m_display(display)
|
||||
{
|
||||
d_ptr->m_foreign = false;
|
||||
d->m_foreign = false;
|
||||
setContentTransform(TextureTransform::MirrorY);
|
||||
}
|
||||
|
||||
|
|
|
@ -83,24 +83,16 @@ struct
|
|||
};
|
||||
|
||||
GLTexture::GLTexture(GLenum target)
|
||||
: d_ptr(new GLTexturePrivate())
|
||||
: d(std::make_unique<GLTexturePrivate>())
|
||||
{
|
||||
Q_D(GLTexture);
|
||||
d->m_target = target;
|
||||
}
|
||||
|
||||
GLTexture::GLTexture(std::unique_ptr<GLTexturePrivate> &&dd)
|
||||
: d_ptr(std::move(dd))
|
||||
{
|
||||
}
|
||||
|
||||
GLTexture::GLTexture(GLuint textureId, GLenum internalFormat, const QSize &size, int levels, bool isImmutable)
|
||||
: d_ptr(new GLTexturePrivate())
|
||||
: GLTexture(GL_TEXTURE_2D)
|
||||
{
|
||||
Q_D(GLTexture);
|
||||
d->m_foreign = true;
|
||||
d->m_texture = textureId;
|
||||
d->m_target = GL_TEXTURE_2D;
|
||||
d->m_scale.setWidth(1.0 / size.width());
|
||||
d->m_scale.setHeight(1.0 / size.height());
|
||||
d->m_size = size;
|
||||
|
@ -119,7 +111,6 @@ GLTexture::~GLTexture()
|
|||
|
||||
bool GLTexture::create()
|
||||
{
|
||||
Q_D(GLTexture);
|
||||
if (!isNull()) {
|
||||
return true;
|
||||
}
|
||||
|
@ -191,19 +182,16 @@ void GLTexturePrivate::cleanup()
|
|||
|
||||
bool GLTexture::isNull() const
|
||||
{
|
||||
Q_D(const GLTexture);
|
||||
return GL_NONE == d->m_texture;
|
||||
}
|
||||
|
||||
QSize GLTexture::size() const
|
||||
{
|
||||
Q_D(const GLTexture);
|
||||
return d->m_size;
|
||||
}
|
||||
|
||||
void GLTexture::setSize(const QSize &size)
|
||||
{
|
||||
Q_D(GLTexture);
|
||||
if (!isNull()) {
|
||||
return;
|
||||
}
|
||||
|
@ -217,7 +205,6 @@ void GLTexture::update(const QImage &image, const QPoint &offset, const QRect &s
|
|||
return;
|
||||
}
|
||||
|
||||
Q_D(GLTexture);
|
||||
Q_ASSERT(!d->m_foreign);
|
||||
|
||||
GLenum glFormat;
|
||||
|
@ -289,13 +276,12 @@ void GLTexture::update(const QImage &image, const QPoint &offset, const QRect &s
|
|||
|
||||
void GLTexture::bind()
|
||||
{
|
||||
Q_D(GLTexture);
|
||||
Q_ASSERT(d->m_texture);
|
||||
|
||||
glBindTexture(d->m_target, d->m_texture);
|
||||
|
||||
if (d->m_markedDirty) {
|
||||
d->onDamage();
|
||||
onDamage();
|
||||
}
|
||||
if (d->m_filterChanged) {
|
||||
GLenum minFilter = GL_NEAREST;
|
||||
|
@ -337,8 +323,6 @@ void GLTexture::bind()
|
|||
|
||||
void GLTexture::generateMipmaps()
|
||||
{
|
||||
Q_D(GLTexture);
|
||||
|
||||
if (d->m_canUseMipmaps && d->s_supportsFramebufferObjects) {
|
||||
glGenerateMipmap(d->m_target);
|
||||
}
|
||||
|
@ -346,7 +330,6 @@ void GLTexture::generateMipmaps()
|
|||
|
||||
void GLTexture::unbind()
|
||||
{
|
||||
Q_D(GLTexture);
|
||||
glBindTexture(d->m_target, 0);
|
||||
}
|
||||
|
||||
|
@ -357,14 +340,12 @@ void GLTexture::render(const QSizeF &size, qreal scale)
|
|||
|
||||
void GLTexture::render(const QRegion ®ion, const QSizeF &targetSize, double scale, bool hardwareClipping)
|
||||
{
|
||||
Q_D(GLTexture);
|
||||
const auto rotatedSize = d->m_textureToBufferMatrix.mapRect(QRect(QPoint(), size())).size();
|
||||
render(QRectF(QPoint(), rotatedSize), region, targetSize, scale, hardwareClipping);
|
||||
}
|
||||
|
||||
void GLTexture::render(const QRectF &source, const QRegion ®ion, const QSizeF &targetSize, double scale, bool hardwareClipping)
|
||||
{
|
||||
Q_D(GLTexture);
|
||||
if (targetSize.isEmpty()) {
|
||||
return; // nothing to paint and m_vbo is likely nullptr and d->m_cachedSize empty as well, #337090
|
||||
}
|
||||
|
@ -416,31 +397,26 @@ void GLTexture::render(const QRectF &source, const QRegion ®ion, const QSizeF
|
|||
|
||||
GLuint GLTexture::texture() const
|
||||
{
|
||||
Q_D(const GLTexture);
|
||||
return d->m_texture;
|
||||
}
|
||||
|
||||
GLenum GLTexture::target() const
|
||||
{
|
||||
Q_D(const GLTexture);
|
||||
return d->m_target;
|
||||
}
|
||||
|
||||
GLenum GLTexture::filter() const
|
||||
{
|
||||
Q_D(const GLTexture);
|
||||
return d->m_filter;
|
||||
}
|
||||
|
||||
GLenum GLTexture::internalFormat() const
|
||||
{
|
||||
Q_D(const GLTexture);
|
||||
return d->m_internalFormat;
|
||||
}
|
||||
|
||||
void GLTexture::clear()
|
||||
{
|
||||
Q_D(GLTexture);
|
||||
Q_ASSERT(!d->m_foreign);
|
||||
if (!GLTexturePrivate::s_fbo && GLFramebuffer::supported() && GLPlatform::instance()->driver() != Driver_Catalyst) { // fail. -> bug #323065
|
||||
glGenFramebuffers(1, &GLTexturePrivate::s_fbo);
|
||||
|
@ -478,13 +454,11 @@ void GLTexture::clear()
|
|||
|
||||
bool GLTexture::isDirty() const
|
||||
{
|
||||
Q_D(const GLTexture);
|
||||
return d->m_markedDirty;
|
||||
}
|
||||
|
||||
void GLTexture::setFilter(GLenum filter)
|
||||
{
|
||||
Q_D(GLTexture);
|
||||
if (filter != d->m_filter) {
|
||||
d->m_filter = filter;
|
||||
d->m_filterChanged = true;
|
||||
|
@ -493,24 +467,21 @@ void GLTexture::setFilter(GLenum filter)
|
|||
|
||||
void GLTexture::setWrapMode(GLenum mode)
|
||||
{
|
||||
Q_D(GLTexture);
|
||||
if (mode != d->m_wrapMode) {
|
||||
d->m_wrapMode = mode;
|
||||
d->m_wrapModeChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GLTexturePrivate::onDamage()
|
||||
{
|
||||
// No-op
|
||||
}
|
||||
|
||||
void GLTexture::setDirty()
|
||||
{
|
||||
Q_D(GLTexture);
|
||||
d->m_markedDirty = true;
|
||||
}
|
||||
|
||||
void GLTexture::onDamage()
|
||||
{
|
||||
}
|
||||
|
||||
void GLTexturePrivate::updateMatrix()
|
||||
{
|
||||
m_textureToBufferMatrix.setToIdentity();
|
||||
|
@ -551,7 +522,6 @@ void GLTexturePrivate::updateMatrix()
|
|||
|
||||
void GLTexture::setContentTransform(TextureTransforms transform)
|
||||
{
|
||||
Q_D(GLTexture);
|
||||
if (d->m_textureToBufferTransform != transform) {
|
||||
d->m_textureToBufferTransform = transform;
|
||||
d->updateMatrix();
|
||||
|
@ -560,20 +530,16 @@ void GLTexture::setContentTransform(TextureTransforms transform)
|
|||
|
||||
TextureTransforms GLTexture::contentTransforms() const
|
||||
{
|
||||
Q_D(const GLTexture);
|
||||
return d->m_textureToBufferTransform;
|
||||
}
|
||||
|
||||
QMatrix4x4 GLTexture::contentTransformMatrix() const
|
||||
{
|
||||
Q_D(const GLTexture);
|
||||
return d->m_textureToBufferMatrix;
|
||||
}
|
||||
|
||||
void GLTexture::setSwizzle(GLenum red, GLenum green, GLenum blue, GLenum alpha)
|
||||
{
|
||||
Q_D(GLTexture);
|
||||
|
||||
if (!GLPlatform::instance()->isGLES()) {
|
||||
const GLuint swizzle[] = {red, green, blue, alpha};
|
||||
glTexParameteriv(d->m_target, GL_TEXTURE_SWIZZLE_RGBA, (const GLint *)swizzle);
|
||||
|
@ -587,19 +553,16 @@ void GLTexture::setSwizzle(GLenum red, GLenum green, GLenum blue, GLenum alpha)
|
|||
|
||||
int GLTexture::width() const
|
||||
{
|
||||
Q_D(const GLTexture);
|
||||
return d->m_size.width();
|
||||
}
|
||||
|
||||
int GLTexture::height() const
|
||||
{
|
||||
Q_D(const GLTexture);
|
||||
return d->m_size.height();
|
||||
}
|
||||
|
||||
QMatrix4x4 GLTexture::matrix(TextureCoordinateType type) const
|
||||
{
|
||||
Q_D(const GLTexture);
|
||||
return d->m_matrix[type];
|
||||
}
|
||||
|
||||
|
@ -672,7 +635,7 @@ std::unique_ptr<GLTexture> GLTexture::allocate(GLenum internalFormat, const QSiz
|
|||
}
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
auto ret = std::make_unique<GLTexture>(texture, internalFormat, size, levels, immutable);
|
||||
ret->d_ptr->m_foreign = false;
|
||||
ret->d->m_foreign = false;
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
@ -737,7 +700,7 @@ std::unique_ptr<GLTexture> GLTexture::upload(const QImage &image)
|
|||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
auto ret = std::make_unique<GLTexture>(texture, internalFormat, image.size(), 1, immutable);
|
||||
ret->setContentTransform(TextureTransform::MirrorY);
|
||||
ret->d_ptr->m_foreign = false;
|
||||
ret->d->m_foreign = false;
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
|
|
@ -158,11 +158,9 @@ public:
|
|||
static std::unique_ptr<GLTexture> upload(const QPixmap &pixmap);
|
||||
|
||||
protected:
|
||||
const std::unique_ptr<GLTexturePrivate> d_ptr;
|
||||
GLTexture(std::unique_ptr<GLTexturePrivate> &&dd);
|
||||
const std::unique_ptr<GLTexturePrivate> d;
|
||||
|
||||
private:
|
||||
Q_DECLARE_PRIVATE(GLTexture)
|
||||
virtual void onDamage();
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
|
|
@ -33,8 +33,6 @@ public:
|
|||
GLTexturePrivate();
|
||||
virtual ~GLTexturePrivate();
|
||||
|
||||
virtual void onDamage();
|
||||
|
||||
void updateMatrix();
|
||||
|
||||
GLuint m_texture;
|
||||
|
|
Loading…
Reference in a new issue