/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "calls/group/calls_group_viewport_opengl.h" #include "calls/group/calls_group_viewport_tile.h" #include "webrtc/webrtc_video_track.h" #include "media/view/media_view_pip.h" #include "calls/group/calls_group_members_row.h" #include "ui/gl/gl_shader.h" #include "data/data_peer.h" #include "styles/style_calls.h" #include namespace Calls::Group { namespace { using namespace Ui::GL; constexpr auto kScaleForBlurTextureIndex = 3; constexpr auto kFirstBlurPassTextureIndex = 4; constexpr auto kBlurTextureSizeFactor = 1.7; constexpr auto kBlurOpacity = 0.5; ShaderPart FragmentBlurTexture(bool vertical, char prefix = 'v') { const auto offsets = (vertical ? QString("0, 1") : QString("1, 0")); const auto name = prefix + QString("_texcoord"); return { .header = R"( varying vec2 )" + name + R"(; uniform sampler2D b_texture; uniform float texelOffset; const vec3 satLuminanceWeighting = vec3(0.2126, 0.7152, 0.0722); const vec2 offsets = vec2()" + offsets + R"(); const int radius = 15; const int diameter = 2 * radius + 1; )", .body = R"( vec4 accumulated = vec4(0.); for (int i = 0; i != diameter; i++) { float stepOffset = float(i - radius) * texelOffset; vec2 offset = vec2(stepOffset) * offsets; vec4 sampled = vec4(texture2D(b_texture, )" + name + R"( + offset)); float fradius = float(radius); float boxWeight = fradius + 1.0 - abs(float(i) - fradius); accumulated += sampled * boxWeight; } vec3 blurred = accumulated.rgb / accumulated.a; float satLuminance = dot(blurred, satLuminanceWeighting); vec3 mixinColor = vec3(satLuminance); result = vec4(clamp(mix(mixinColor, blurred, 1.1), 0.0, 1.0), 1.0); )", }; } // Depends on FragmetSampleTexture(). [[nodiscard]] ShaderPart FragmentFrameColor() { const auto blur = FragmentBlurTexture(true, 'b'); return { .header = R"( uniform vec4 frameBg; uniform vec3 shadow; // fullHeight, shown, maxOpacity const float backgroundOpacity = )" + QString::number(kBlurOpacity) + R"(; float insideTexture() { vec2 textureHalf = vec2(0.5, 0.5); vec2 fromTextureCenter = abs(v_texcoord - textureHalf); vec2 fromTextureEdge = max(fromTextureCenter, textureHalf) - textureHalf; float outsideCheck = dot(fromTextureEdge, fromTextureEdge); return step(outsideCheck, 0); } )" + blur.header + R"( vec4 background() { vec4 result; )" + blur.body + R"( return result; } )", .body = R"( float inside = insideTexture(); result = result * inside + (1. - inside) * (backgroundOpacity * background() + (1. - backgroundOpacity) * frameBg); float shadowCoord = gl_FragCoord.y - roundRect.y; float shadowValue = max(1. - (shadowCoord / shadow.x), 0.); float shadowShown = shadowValue * shadow.y * shadow.z; result = vec4(result.rgb * (1. - shadowShown), result.a); )", }; } [[nodiscard]] QSize NonEmpty(QSize size) { return QSize(std::max(size.width(), 1), std::max(size.height(), 1)); } [[nodiscard]] QSize CountBlurredSize( QSize unscaled, QSize viewport, float factor) { factor *= kBlurTextureSizeFactor; // The more the scale - more blurred the image. const auto area = viewport / int(std::round(factor * cScale() / 100)); const auto scaled = unscaled.scaled(area, Qt::KeepAspectRatio); return (scaled.width() > unscaled.width() || scaled.height() > unscaled.height()) ? unscaled : NonEmpty(scaled); } [[nodiscard]] std::array, 4> CountTexCoords( QSize unscaled, QSize size, bool expand, bool swap = false) { const auto scaled = NonEmpty(unscaled.scaled( size, expand ? Qt::KeepAspectRatioByExpanding : Qt::KeepAspectRatio)); const auto left = (size.width() - scaled.width()) / 2; const auto top = (size.height() - scaled.height()) / 2; const auto right = left + scaled.width(); const auto bottom = top + scaled.height(); auto dleft = float(left) / scaled.width(); auto dright = float(size.width() - left) / scaled.width(); auto dtop = float(top) / scaled.height(); auto dbottom = float(size.height() - top) / scaled.height(); if (swap) { std::swap(dleft, dtop); std::swap(dright, dbottom); } return { { { { -dleft, 1.f + dtop } }, { { dright, 1.f + dtop } }, { { dright, 1.f - dbottom } }, { { -dleft, 1.f - dbottom } }, } }; } void FillRectVertices(GLfloat *coords, Rect rect) { coords[0] = coords[10] = rect.left(); coords[1] = coords[11] = rect.top(); coords[2] = rect.right(); coords[3] = rect.top(); coords[4] = coords[6] = rect.right(); coords[5] = coords[7] = rect.bottom(); coords[8] = rect.left(); coords[9] = rect.bottom(); } void FillTriangles( QOpenGLFunctions &f, gsl::span coords, not_null buffer, not_null program, QSize viewportWithFactor, const QColor &color, Fn additional = nullptr) { Expects(coords.size() % 6 == 0); if (coords.empty()) { return; } buffer->bind(); buffer->allocate(coords.data(), coords.size() * sizeof(GLfloat)); f.glUseProgram(program->programId()); program->setUniformValue("viewport", QSizeF(viewportWithFactor)); program->setUniformValue("s_color", Uniform(color)); GLint position = program->attributeLocation("position"); f.glVertexAttribPointer( position, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), nullptr); f.glEnableVertexAttribArray(position); if (additional) { additional(); } f.glDrawArrays(GL_TRIANGLES, 0, coords.size() / 2); f.glDisableVertexAttribArray(position); } void FillTexturedRectangle( QOpenGLFunctions &f, not_null program, int skipVertices = 0) { const auto shift = [&](int elements) { return reinterpret_cast( (skipVertices * 4 + elements) * sizeof(GLfloat)); }; GLint position = program->attributeLocation("position"); f.glVertexAttribPointer( position, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), shift(0)); f.glEnableVertexAttribArray(position); GLint texcoord = program->attributeLocation("v_texcoordIn"); f.glVertexAttribPointer( texcoord, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), shift(2)); f.glEnableVertexAttribArray(texcoord); f.glDrawArrays(GL_TRIANGLE_FAN, 0, 4); f.glDisableVertexAttribArray(position); f.glDisableVertexAttribArray(texcoord); } } // namespace Viewport::RendererGL::RendererGL(not_null owner) : _owner(owner) , _pinIcon(st::groupCallLargeVideo.pin) , _muteIcon(st::groupCallLargeVideoCrossLine) , _pinBackground( (st::groupCallLargeVideo.pinPadding.top() + st::groupCallLargeVideo.pin.icon.height() + st::groupCallLargeVideo.pinPadding.bottom()) / 2, st::radialBg) { style::PaletteChanged( ) | rpl::start_with_next([=] { _buttons.invalidate(); }, _lifetime); } void Viewport::RendererGL::init( not_null widget, QOpenGLFunctions &f) { _factor = widget->devicePixelRatio(); _frameBuffer.emplace(); _frameBuffer->setUsagePattern(QOpenGLBuffer::DynamicDraw); _frameBuffer->create(); _frameBuffer->bind(); constexpr auto kQuads = 6; constexpr auto kQuadVertices = kQuads * 4; constexpr auto kQuadValues = kQuadVertices * 4; constexpr auto kValues = kQuadValues + 8; // Blur texture coordinates. _frameBuffer->allocate(kValues * sizeof(GLfloat)); _downscaleProgram.yuv420.emplace(); _downscaleVertexShader = LinkProgram( &*_downscaleProgram.yuv420, VertexShader({ VertexPassTextureCoord(), }), FragmentShader({ FragmentSampleYUV420Texture(), })).vertex; _blurProgram.emplace(); LinkProgram( &*_blurProgram, _downscaleVertexShader, FragmentShader({ FragmentBlurTexture(false), })); _frameProgram.yuv420.emplace(); _frameVertexShader = LinkProgram( &*_frameProgram.yuv420, VertexShader({ VertexViewportTransform(), VertexPassTextureCoord(), VertexPassTextureCoord('b'), }), FragmentShader({ FragmentSampleYUV420Texture(), FragmentFrameColor(), FragmentRoundCorners(), })).vertex; _bgBuffer.emplace(); _bgBuffer->setUsagePattern(QOpenGLBuffer::DynamicDraw); _bgBuffer->create(); _bgProgram.emplace(); LinkProgram( &*_bgProgram, VertexShader({ VertexViewportTransform() }), FragmentShader({ FragmentStaticColor() })); _imageProgram.emplace(); LinkProgram( &*_imageProgram, _frameVertexShader, FragmentShader({ FragmentSampleARGB32Texture(), })); } void Viewport::RendererGL::ensureARGB32Program() { Expects(_downscaleVertexShader != nullptr); Expects(_frameVertexShader != nullptr); _downscaleProgram.argb32.emplace(); LinkProgram( &*_downscaleProgram.argb32, _downscaleVertexShader, FragmentShader({ FragmentSampleARGB32Texture(), })); _frameProgram.argb32.emplace(); LinkProgram( &*_frameProgram.argb32, _frameVertexShader, FragmentShader({ FragmentSampleARGB32Texture(), FragmentFrameColor(), FragmentRoundCorners(), })); } void Viewport::RendererGL::deinit( not_null widget, QOpenGLFunctions &f) { _bgBuffer = std::nullopt; _frameBuffer = std::nullopt; _frameVertexShader = nullptr; _bgProgram = std::nullopt; _imageProgram = std::nullopt; _downscaleProgram.argb32 = std::nullopt; _downscaleProgram.yuv420 = std::nullopt; _blurProgram = std::nullopt; _frameProgram.argb32 = std::nullopt; _frameProgram.yuv420 = std::nullopt; for (auto &data : _tileData) { data.textures.destroy(f); } _tileData.clear(); _tileDataIndices.clear(); _buttons.destroy(f); } void Viewport::RendererGL::resize( not_null widget, QOpenGLFunctions &f, int w, int h) { _factor = widget->devicePixelRatio(); _viewport = QSize(w, h); setDefaultViewport(f); } void Viewport::RendererGL::setDefaultViewport(QOpenGLFunctions &f) { const auto size = _viewport * _factor; f.glViewport(0, 0, size.width(), size.height()); } void Viewport::RendererGL::paint( not_null widget, QOpenGLFunctions &f) { _factor = widget->devicePixelRatio(); validateDatas(); fillBackground(f); const auto defaultFramebufferObject = widget->defaultFramebufferObject(); auto index = 0; for (const auto &tile : _owner->_tiles) { paintTile( f, defaultFramebufferObject, tile.get(), _tileData[_tileDataIndices[index++]]); } } void Viewport::RendererGL::fillBackground(QOpenGLFunctions &f) { const auto radius = st::roundRadiusLarge; const auto radiuses = QMargins{ radius, radius, radius, radius }; auto bg = QRegion(QRect(QPoint(), _viewport)); for (const auto &tile : _owner->_tiles) { bg -= tile->geometry().marginsRemoved(radiuses); } if (bg.isEmpty()) { return; } _bgTriangles.resize((bg.end() - bg.begin()) * 12); auto coords = _bgTriangles.data(); for (const auto rect : bg) { FillRectVertices(coords, transformRect(rect)); coords += 12; } FillTriangles( f, _bgTriangles, &*_bgBuffer, &*_bgProgram, _viewport * _factor, st::groupCallBg->c); } void Viewport::RendererGL::paintTile( QOpenGLFunctions &f, GLuint defaultFramebufferObject, not_null tile, TileData &tileData) { const auto track = tile->track(); const auto data = track->frameWithInfo(false); if (data.format == Webrtc::FrameFormat::None) { return; } Assert(!data.yuv420->size.isEmpty()); const auto geometry = tile->geometry(); if (geometry.isEmpty()) { return; } _rgbaFrame = (data.format == Webrtc::FrameFormat::ARGB32); const auto x = geometry.x(); const auto y = geometry.y(); const auto width = geometry.width(); const auto height = geometry.height(); const auto &st = st::groupCallLargeVideo; const auto shown = _owner->_controlsShownRatio; const auto fullNameShift = st.namePosition.y() + st::normalFont->height; const auto nameShift = anim::interpolate(fullNameShift, 0, shown); const auto row = tile->row(); const auto style = row->computeIconState(MembersRowStyle::LargeVideo); validateOutlineAnimation(tile, tileData); const auto outline = tileData.outlined.value(tileData.outline ? 1. : 0.); ensureButtonsImage(); // Frame. const auto unscaled = Media::View::FlipSizeByRotation( data.yuv420->size, data.rotation); const auto swap = (((data.rotation / 90) % 2) == 1); const auto expand = !_owner->wide()/* && !tile->screencast()*/; auto texCoords = CountTexCoords(unscaled, geometry.size(), expand, swap); auto blurTexCoords = expand ? texCoords : CountTexCoords(unscaled, geometry.size(), true); const auto rect = transformRect(geometry); auto toBlurTexCoords = std::array, 4> { { { { 0.f, 1.f } }, { { 1.f, 1.f } }, { { 1.f, 0.f } }, { { 0.f, 0.f } }, } }; if (const auto shift = (data.rotation / 90); shift > 0) { std::rotate( toBlurTexCoords.begin(), toBlurTexCoords.begin() + shift, toBlurTexCoords.end()); std::rotate( texCoords.begin(), texCoords.begin() + shift, texCoords.end()); } // Pin. const auto pin = _buttons.texturedRect( tile->pinInner().translated(x, y), tile->pinned() ? _pinOn : _pinOff, geometry); const auto pinRect = transformRect(pin.geometry); // Mute. const auto &icon = st::groupCallLargeVideoCrossLine.icon; const auto iconLeft = x + width - st.iconPosition.x() - icon.width(); const auto iconTop = y + (height - st.iconPosition.y() - icon.height() + nameShift); const auto mute = _buttons.texturedRect( QRect(iconLeft, iconTop, icon.width(), icon.height()), (row->state() == MembersRow::State::Active ? _muteOff : _muteOn), geometry); const auto muteRect = transformRect(mute.geometry); // Name. const auto namePosition = QPoint( x + st.namePosition.x(), y + (height - st.namePosition.y() - st::semiboldFont->height + nameShift)); const auto name = _names.texturedRect( QRect(namePosition, tileData.nameRect.size() / cIntRetinaFactor()), tileData.nameRect, geometry); const auto nameRect = transformRect(name.geometry); const GLfloat coords[] = { // YUV -> RGB-for-blur quad. -1.f, 1.f, toBlurTexCoords[0][0], toBlurTexCoords[0][1], 1.f, 1.f, toBlurTexCoords[1][0], toBlurTexCoords[1][1], 1.f, -1.f, toBlurTexCoords[2][0], toBlurTexCoords[2][1], -1.f, -1.f, toBlurTexCoords[3][0], toBlurTexCoords[3][1], // First RGB -> RGB blur pass. -1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, -1.f, 1.f, 0.f, -1.f, -1.f, 0.f, 0.f, // Second blur pass + paint final frame. rect.left(), rect.top(), texCoords[0][0], texCoords[0][1], rect.right(), rect.top(), texCoords[1][0], texCoords[1][1], rect.right(), rect.bottom(), texCoords[2][0], texCoords[2][1], rect.left(), rect.bottom(), texCoords[3][0], texCoords[3][1], // Additional blurred background texture coordinates. blurTexCoords[0][0], blurTexCoords[0][1], blurTexCoords[1][0], blurTexCoords[1][1], blurTexCoords[2][0], blurTexCoords[2][1], blurTexCoords[3][0], blurTexCoords[3][1], // Pin button. pinRect.left(), pinRect.top(), pin.texture.left(), pin.texture.bottom(), pinRect.right(), pinRect.top(), pin.texture.right(), pin.texture.bottom(), pinRect.right(), pinRect.bottom(), pin.texture.right(), pin.texture.top(), pinRect.left(), pinRect.bottom(), pin.texture.left(), pin.texture.top(), // Mute icon. muteRect.left(), muteRect.top(), mute.texture.left(), mute.texture.bottom(), muteRect.right(), muteRect.top(), mute.texture.right(), mute.texture.bottom(), muteRect.right(), muteRect.bottom(), mute.texture.right(), mute.texture.top(), muteRect.left(), muteRect.bottom(), mute.texture.left(), mute.texture.top(), // Name. nameRect.left(), nameRect.top(), name.texture.left(), name.texture.bottom(), nameRect.right(), nameRect.top(), name.texture.right(), name.texture.bottom(), nameRect.right(), nameRect.bottom(), name.texture.right(), name.texture.top(), nameRect.left(), nameRect.bottom(), name.texture.left(), name.texture.top(), }; const auto blurSize = CountBlurredSize(unscaled, _viewport, _factor); prepareObjects(f, tileData, blurSize); f.glViewport(0, 0, blurSize.width(), blurSize.height()); _frameBuffer->bind(); _frameBuffer->write(0, coords, sizeof(coords)); bindFrame(f, data, tileData, _downscaleProgram); tile->track()->markFrameShown(); drawDownscalePass(f, tileData); drawFirstBlurPass(f, tileData, blurSize); f.glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebufferObject); setDefaultViewport(f); bindFrame(f, data, tileData, _frameProgram); const auto program = _rgbaFrame ? &*_frameProgram.argb32 : &*_frameProgram.yuv420; const auto uniformViewport = QSizeF(_viewport * _factor); program->setUniformValue("viewport", uniformViewport); program->setUniformValue("frameBg", Uniform(st::groupCallBg->c)); program->setUniformValue("radiusOutline", QVector2D( GLfloat(st::roundRadiusLarge * _factor), (outline > 0) ? (st::groupCallOutline * _factor) : 0.f)); program->setUniformValue("roundRect", Uniform(rect)); program->setUniformValue("roundBg", Uniform(st::groupCallBg->c)); program->setUniformValue("outlineFg", QVector4D( st::groupCallMemberActiveIcon->c.redF(), st::groupCallMemberActiveIcon->c.greenF(), st::groupCallMemberActiveIcon->c.blueF(), st::groupCallMemberActiveIcon->c.alphaF() * outline)); const auto shadowHeight = st.shadowHeight * _factor; const auto shadowAlpha = kShadowMaxAlpha / 255.f; program->setUniformValue( "shadow", QVector3D(shadowHeight, shown, shadowAlpha)); f.glActiveTexture(_rgbaFrame ? GL_TEXTURE1 : GL_TEXTURE3); tileData.textures.bind( f, tileData.textureIndex * 5 + kFirstBlurPassTextureIndex); program->setUniformValue("b_texture", GLint(_rgbaFrame ? 1 : 3)); program->setUniformValue( "texelOffset", GLfloat(1.f / blurSize.height())); GLint blurTexcoord = program->attributeLocation("b_texcoordIn"); f.glVertexAttribPointer( blurTexcoord, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), reinterpret_cast(48 * sizeof(GLfloat))); f.glEnableVertexAttribArray(blurTexcoord); FillTexturedRectangle(f, program, 8); f.glDisableVertexAttribArray(blurTexcoord); const auto pinVisible = _owner->wide() && (pin.geometry.bottom() > y); if (nameShift == fullNameShift && !pinVisible) { return; } f.glEnable(GL_BLEND); f.glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); const auto guard = gsl::finally([&] { f.glDisable(GL_BLEND); }); f.glUseProgram(_imageProgram->programId()); _imageProgram->setUniformValue("viewport", uniformViewport); _imageProgram->setUniformValue("s_texture", GLint(0)); f.glActiveTexture(GL_TEXTURE0); _buttons.bind(f); if (pinVisible) { FillTexturedRectangle(f, &*_imageProgram, 14); } if (nameShift == fullNameShift) { return; } // Mute. if (!muteRect.empty()) { FillTexturedRectangle(f, &*_imageProgram, 18); } // Name. if (!nameRect.empty()) { _names.bind(f); FillTexturedRectangle(f, &*_imageProgram, 22); } } void Viewport::RendererGL::prepareObjects( QOpenGLFunctions &f, TileData &tileData, QSize blurSize) { tileData.textures.ensureCreated(f); tileData.framebuffers.ensureCreated(f); const auto create = [&](int index) { tileData.textures.bind(f, tileData.textureIndex * 5 + index); f.glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB, blurSize.width(), blurSize.height(), 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr); }; create(kScaleForBlurTextureIndex); create(kFirstBlurPassTextureIndex); } void Viewport::RendererGL::bindFrame( QOpenGLFunctions &f, const Webrtc::FrameWithInfo &data, TileData &tileData, Program &program) { const auto upload = (tileData.trackIndex != data.index); tileData.trackIndex = data.index; const auto uploadOne = [&]( GLint internalformat, GLint format, QSize size, int stride, const void *data) { f.glPixelStorei(GL_UNPACK_ROW_LENGTH, stride); f.glTexImage2D( GL_TEXTURE_2D, 0, internalformat, size.width(), size.height(), 0, format, GL_UNSIGNED_BYTE, data); f.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); }; //if (upload) { // tileData.textureIndex = 1 - tileData.textureIndex; //} if (_rgbaFrame) { ensureARGB32Program(); f.glUseProgram(program.argb32->programId()); f.glActiveTexture(GL_TEXTURE0); tileData.textures.bind(f, tileData.textureIndex); if (upload) { const auto &image = data.original; const auto stride = image.bytesPerLine() / 4; const auto data = image.constBits(); uploadOne(GL_RGB, GL_RGBA, image.size(), stride, data); } program.argb32->setUniformValue("s_texture", GLint(0)); } else { const auto yuv = data.yuv420; const auto otherSize = yuv->chromaSize; f.glUseProgram(program.yuv420->programId()); f.glActiveTexture(GL_TEXTURE0); tileData.textures.bind(f, tileData.textureIndex * 5 + 0); if (upload) { f.glPixelStorei(GL_UNPACK_ALIGNMENT, 1); uploadOne(GL_RED, GL_RED, yuv->size, yuv->y.stride, yuv->y.data); } f.glActiveTexture(GL_TEXTURE1); tileData.textures.bind(f, tileData.textureIndex * 5 + 1); if (upload) { uploadOne(GL_RED, GL_RED, otherSize, yuv->u.stride, yuv->u.data); } f.glActiveTexture(GL_TEXTURE2); tileData.textures.bind(f, tileData.textureIndex * 5 + 2); if (upload) { uploadOne(GL_RED, GL_RED, otherSize, yuv->v.stride, yuv->v.data); f.glPixelStorei(GL_UNPACK_ALIGNMENT, 4); } program.yuv420->setUniformValue("y_texture", GLint(0)); program.yuv420->setUniformValue("u_texture", GLint(1)); program.yuv420->setUniformValue("v_texture", GLint(2)); } } void Viewport::RendererGL::drawDownscalePass( QOpenGLFunctions &f, TileData &tileData) { tileData.framebuffers.bind(f, 0); f.glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tileData.textures.id( tileData.textureIndex * 5 + kScaleForBlurTextureIndex), 0); const auto program = _rgbaFrame ? &*_downscaleProgram.argb32 : &*_downscaleProgram.yuv420; FillTexturedRectangle(f, program); } void Viewport::RendererGL::drawFirstBlurPass( QOpenGLFunctions &f, TileData &tileData, QSize blurSize) { tileData.framebuffers.bind(f, 1); f.glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tileData.textures.id( tileData.textureIndex * 5 + kFirstBlurPassTextureIndex), 0); f.glUseProgram(_blurProgram->programId()); f.glActiveTexture(GL_TEXTURE0); tileData.textures.bind( f, tileData.textureIndex * 5 + kScaleForBlurTextureIndex); _blurProgram->setUniformValue("b_texture", GLint(0)); _blurProgram->setUniformValue( "texelOffset", GLfloat(1.f / blurSize.width())); FillTexturedRectangle(f, &*_blurProgram, 4); } Rect Viewport::RendererGL::transformRect(const Rect &raster) const { return { raster.left() * _factor, float(_viewport.height() - raster.bottom()) * _factor, raster.width() * _factor, raster.height() * _factor, }; } Rect Viewport::RendererGL::transformRect(const QRect &raster) const { return { raster.x() * _factor, (_viewport.height() - raster.y() - raster.height()) * _factor, raster.width() * _factor, raster.height() * _factor, }; } void Viewport::RendererGL::ensureButtonsImage() { if (_buttons) { return; } const auto factor = cIntRetinaFactor(); const auto pinOnSize = VideoTile::PinInnerSize(true); const auto pinOffSize = VideoTile::PinInnerSize(false); const auto muteSize = st::groupCallLargeVideoCrossLine.icon.size(); const auto fullSize = QSize( std::max({ pinOnSize.width(), pinOffSize.width(), 2 * muteSize.width(), }), pinOnSize.height() + pinOffSize.height() + muteSize.height()); const auto imageSize = fullSize * factor; auto image = _buttons.takeImage(); if (image.size() != imageSize) { image = QImage(imageSize, QImage::Format_ARGB32_Premultiplied); } image.fill(Qt::transparent); image.setDevicePixelRatio(cRetinaFactor()); { auto p = Painter(&image); auto hq = PainterHighQualityEnabler(p); _pinOn = QRect(QPoint(), pinOnSize * factor); VideoTile::PaintPinButton( p, true, 0, 0, fullSize.width(), &_pinBackground, &_pinIcon); _pinOff = QRect( QPoint(0, pinOnSize.height()) * factor, pinOffSize * factor); VideoTile::PaintPinButton( p, false, 0, pinOnSize.height(), fullSize.width(), &_pinBackground, &_pinIcon); const auto muteTop = pinOnSize.height() + pinOffSize.height(); _muteOn = QRect(QPoint(0, muteTop) * factor, muteSize * factor); _muteIcon.paint(p, { 0, muteTop }, 1.); _muteOff = QRect( QPoint(muteSize.width(), muteTop) * factor, muteSize * factor); _muteIcon.paint(p, { muteSize.width(), muteTop }, 0.); } _buttons.setImage(std::move(image)); } void Viewport::RendererGL::validateDatas() { const auto &tiles = _owner->_tiles; const auto &st = st::groupCallLargeVideo; const auto count = int(tiles.size()); const auto factor = cIntRetinaFactor(); const auto nameHeight = st::semiboldFont->height * factor; struct Request { int index = 0; bool updating = false; }; auto requests = std::vector(); auto available = _names.image().width(); for (auto &data : _tileData) { data.stale = true; } _tileDataIndices.resize(count); const auto nameWidth = [&](int i) { const auto row = tiles[i]->row(); const auto hasWidth = tiles[i]->geometry().width() - st.iconPosition.x() - st::groupCallLargeVideoCrossLine.icon.width() - st.namePosition.x(); return std::clamp(row->name().maxWidth(), 1, hasWidth) * factor; }; for (auto i = 0; i != count; ++i) { tiles[i]->row()->lazyInitialize(st::groupCallMembersListItem); const auto width = nameWidth(i); if (width <= 0) { continue; } if (width > available) { available = width; } const auto peer = tiles[i]->row()->peer(); const auto j = ranges::find(_tileData, peer, &TileData::peer); if (j != end(_tileData)) { j->stale = false; const auto index = (j - begin(_tileData)); _tileDataIndices[i] = index; if (peer->nameVersion != j->nameVersion || width != j->nameRect.width()) { const auto nameTop = index * nameHeight; j->nameRect = QRect(0, nameTop, width, nameHeight); requests.push_back({ .index = i, .updating = true }); } } else { _tileDataIndices[i] = -1; requests.push_back({ .index = i, .updating = false }); } } if (requests.empty()) { return; } auto maybeStaleAfter = begin(_tileData); auto maybeStaleEnd = end(_tileData); for (auto &request : requests) { const auto i = request.index; if (_tileDataIndices[i] >= 0) { continue; } const auto peer = tiles[i]->row()->peer(); auto index = int(_tileData.size()); maybeStaleAfter = ranges::find( maybeStaleAfter, maybeStaleEnd, true, &TileData::stale); if (maybeStaleAfter != maybeStaleEnd) { index = (maybeStaleAfter - begin(_tileData)); maybeStaleAfter->peer = peer; maybeStaleAfter->stale = false; request.updating = true; } else { // This invalidates maybeStale*, but they're already equal. _tileData.push_back({ .peer = peer }); } _tileData[index].nameVersion = peer->nameVersion; _tileData[index].nameRect = QRect( 0, index * nameHeight, nameWidth(i), nameHeight); _tileDataIndices[i] = index; } auto image = _names.takeImage(); const auto imageSize = QSize( available * factor, _tileData.size() * nameHeight); const auto allocate = (image.size() != imageSize); auto paintToImage = allocate ? QImage(imageSize, QImage::Format_ARGB32_Premultiplied) : base::take(image); paintToImage.setDevicePixelRatio(factor); if (allocate && image.isNull()) { paintToImage.fill(Qt::transparent); } { auto p = Painter(&paintToImage); if (!image.isNull()) { p.setCompositionMode(QPainter::CompositionMode_Source); p.drawImage(0, 0, image); if (paintToImage.width() > image.width()) { p.fillRect( image.width() / factor, 0, (paintToImage.width() - image.width()) / factor, image.height() / factor, Qt::transparent); } if (paintToImage.height() > image.height()) { p.fillRect( 0, image.height() / factor, paintToImage.width() / factor, (paintToImage.height() - image.height()) / factor, Qt::transparent); } p.setCompositionMode(QPainter::CompositionMode_SourceOver); } p.setPen(st::groupCallVideoTextFg); for (const auto &request : requests) { const auto i = request.index; const auto index = _tileDataIndices[i]; const auto &data = _tileData[_tileDataIndices[i]]; const auto row = tiles[i]->row(); if (request.updating) { p.setCompositionMode(QPainter::CompositionMode_Source); p.fillRect( 0, data.nameRect.y() / factor, paintToImage.width() / factor, nameHeight / factor, Qt::transparent); p.setCompositionMode(QPainter::CompositionMode_SourceOver); } row->name().drawLeftElided( p, 0, data.nameRect.y() / factor, data.nameRect.width() / factor, paintToImage.width() / factor); } } _names.setImage(std::move(paintToImage)); } void Viewport::RendererGL::validateOutlineAnimation( not_null tile, TileData &data) { const auto outline = tile->row()->speaking(); if (data.outline == outline) { return; } data.outline = outline; data.outlined.start( [=] { _owner->widget()->update(); }, outline ? 0. : 1., outline ? 1. : 0., st::fadeWrapDuration); } } // namespace Calls::Group