[thanos] Added GPU-accelerated particle dissolution effect for messages.

This commit is contained in:
23rd
2026-04-02 14:24:50 +03:00
parent 763fce3326
commit e75dba5c80
15 changed files with 1148 additions and 4 deletions
+4
View File
@@ -1827,6 +1827,10 @@ PRIVATE
ui/effects/message_sending_animation_controller.h
ui/effects/reaction_fly_animation.cpp
ui/effects/reaction_fly_animation.h
ui/effects/thanos_effect.cpp
ui/effects/thanos_effect.h
ui/effects/thanos_effect_renderer.cpp
ui/effects/thanos_effect_renderer.h
ui/effects/send_action_animations.cpp
ui/effects/send_action_animations.h
ui/image/image.cpp
@@ -2182,6 +2182,16 @@ rpl::producer<not_null<const HistoryItem*>> Session::itemRemoved(
});
}
void Session::notifyViewAboutToBeRemoved(
not_null<const ViewElement*> view) {
_viewAboutToBeRemoved.fire_copy(view);
}
rpl::producer<not_null<const ViewElement*>>
Session::viewAboutToBeRemoved() const {
return _viewAboutToBeRemoved.events();
}
void Session::notifyViewRemoved(not_null<const ViewElement*> view) {
_viewRemoved.fire_copy(view);
}
+3
View File
@@ -427,6 +427,8 @@ public:
[[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemRemoved() const;
[[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemRemoved(
FullMsgId itemId) const;
void notifyViewAboutToBeRemoved(not_null<const ViewElement*> view);
[[nodiscard]] rpl::producer<not_null<const ViewElement*>> viewAboutToBeRemoved() const;
void notifyViewRemoved(not_null<const ViewElement*> view);
[[nodiscard]] rpl::producer<not_null<const ViewElement*>> viewRemoved() const;
void notifyHistoryCleared(not_null<const History*> history);
@@ -1171,6 +1173,7 @@ private:
rpl::event_stream<not_null<HistoryItem*>> _itemDataChanges;
rpl::event_stream<ReactionsRemoved> _reactionsRemoved;
rpl::event_stream<not_null<const HistoryItem*>> _itemRemoved;
rpl::event_stream<not_null<const ViewElement*>> _viewAboutToBeRemoved;
rpl::event_stream<not_null<const ViewElement*>> _viewRemoved;
rpl::event_stream<not_null<const ViewElement*>> _viewPaidReactionSent;
rpl::event_stream<not_null<Calls::GroupCall*>> _callPaidReactionSent;
+1
View File
@@ -4395,6 +4395,7 @@ int HistoryBlock::resizeGetHeight(int newWidth, ResizeRequest request) {
void HistoryBlock::remove(not_null<Element*> view) {
Expects(view->block() == this);
_history->owner().notifyViewAboutToBeRemoved(view);
_history->mainViewRemoved(this, view);
const auto blockIndex = indexInHistory();
@@ -435,6 +435,10 @@ HistoryInner::HistoryInner(
) | rpl::on_next(
[this](auto item) { itemRemoved(item); },
lifetime());
session().data().viewAboutToBeRemoved(
) | rpl::on_next(
[this](auto view) { captureViewForThanosEffect(view); },
lifetime());
session().data().viewRemoved(
) | rpl::on_next(
[this](auto view) { viewRemoved(view); },
@@ -4164,6 +4168,58 @@ void HistoryInner::leaveEventHook(QEvent *e) {
return RpWidget::leaveEventHook(e);
}
void HistoryInner::captureViewForThanosEffect(
not_null<const Element*> view) {
if (!Ui::ThanosEffect::Supported()) {
return;
}
if (view->data()->history() != _history
&& view->data()->history() != _migrated) {
return;
}
const auto top = itemTop(view);
if (top < 0) {
return;
}
const auto viewHeight = view->height();
const auto viewWidth = width();
if (viewWidth <= 0 || viewHeight <= 0) {
return;
}
const auto screenTop = top - _visibleAreaTop;
if (screenTop + viewHeight <= 0
|| screenTop >= (_visibleAreaBottom - _visibleAreaTop)) {
return;
}
const auto dpr = style::DevicePixelRatio();
auto image = QImage(
QSize(viewWidth, viewHeight) * dpr,
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(dpr);
image.fill(Qt::transparent);
{
Painter p(&image);
auto clip = QRect(0, 0, viewWidth, viewHeight);
auto context = preparePaintContext(clip);
context.clip = clip;
context.outbg = view->hasOutLayout();
p.translate(0, -top);
p.translate(0, top);
view->draw(p, context);
}
if (!_thanosEffect) {
_thanosEffect = std::make_unique<Ui::ThanosEffect>(this);
}
_thanosEffect->setGeometry(
QRect(0, 0, viewWidth, _visibleAreaBottom - _visibleAreaTop));
_thanosEffect->raise();
_thanosEffect->addItem(
std::move(image),
QRect(0, screenTop, viewWidth, viewHeight));
}
HistoryInner::~HistoryInner() {
if (_overlayHost) {
_overlayHost->hide();
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/rp_widget.h"
#include "ui/controls/swipe_handler_data.h"
#include "ui/effects/animations.h"
#include "ui/effects/thanos_effect.h"
#include "ui/dragging_scroll_manager.h"
#include "ui/widgets/middle_click_autoscroll.h"
#include "ui/widgets/tooltip.h"
@@ -609,6 +610,9 @@ private:
[[nodiscard]] HistoryView::ElementOverlayHost &ensureOverlayHost();
std::unique_ptr<HistoryView::ElementOverlayHost> _overlayHost;
void captureViewForThanosEffect(not_null<const Element*> view);
std::unique_ptr<Ui::ThanosEffect> _thanosEffect;
};
[[nodiscard]] bool CanSendReply(not_null<const HistoryItem*> item);
@@ -0,0 +1,131 @@
/*
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 "ui/effects/thanos_effect.h"
#include "ui/effects/thanos_effect_renderer.h"
#include "ui/gl/gl_surface.h"
#include "ui/power_saving.h"
#include "ui/rp_widget.h"
#include "base/debug_log.h"
#include <QTimer>
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
#include <rhi/qrhi.h>
#endif
namespace Ui {
bool ThanosEffect::Supported() {
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
return !PowerSaving::On(PowerSaving::kChatEffects);
#else
return false;
#endif
}
ThanosEffect::ThanosEffect(not_null<RpWidget*> parent)
: _parent(parent) {
}
ThanosEffect::~ThanosEffect() {
stopUpdateTimer();
}
void ThanosEffect::ensureSurface() {
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
if (_surface) {
return;
}
auto renderer = std::make_unique<ThanosEffectRenderer>();
_renderer = renderer.get();
_renderer->allDone() | rpl::on_next([=] {
stopUpdateTimer();
_allDone.fire({});
}, _lifetime);
_surface = GL::CreateSurface(
_parent,
GL::ChosenRenderer{
.renderer = std::move(renderer),
.backend = GL::Backend::QRhi,
});
if (const auto w = _surface ? _surface->rpWidget() : nullptr) {
w->setGeometry(_parent->rect());
w->show();
w->raise();
}
#endif
}
void ThanosEffect::addItem(QImage snapshot, QRect rect) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
ensureSurface();
if (!_renderer) {
return;
}
_renderer->addItem({
.snapshot = std::move(snapshot),
.rect = QRectF(rect),
});
startUpdateTimer();
#endif
}
bool ThanosEffect::animating() const {
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
return _renderer && _renderer->hasActiveItems();
#else
return false;
#endif
}
rpl::producer<> ThanosEffect::allDone() const {
return _allDone.events();
}
void ThanosEffect::setGeometry(QRect rect) {
if (const auto w = _surface ? _surface->rpWidget() : nullptr) {
w->setGeometry(rect);
}
}
void ThanosEffect::raise() {
if (const auto w = _surface ? _surface->rpWidget() : nullptr) {
w->raise();
}
}
void ThanosEffect::startUpdateTimer() {
if (_updateTimer) {
return;
}
if (const auto w = _surface ? _surface->rpWidget() : nullptr) {
_updateTimer = new QTimer(w);
_updateTimer->setInterval(16);
QObject::connect(_updateTimer, &QTimer::timeout, w, [w] {
w->update();
});
_updateTimer->start();
}
}
void ThanosEffect::stopUpdateTimer() {
if (_updateTimer) {
_updateTimer->stop();
delete _updateTimer;
_updateTimer = nullptr;
}
}
} // namespace Ui
@@ -0,0 +1,56 @@
/*
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
*/
#pragma once
#include "base/unique_qptr.h"
#include <QImage>
namespace Ui {
class RpWidget;
class RpWidgetWrap;
} // namespace Ui
namespace Ui {
class ThanosEffectRenderer;
class ThanosEffect final {
public:
explicit ThanosEffect(not_null<RpWidget*> parent);
~ThanosEffect();
void addItem(QImage snapshot, QRect rect);
[[nodiscard]] bool animating() const;
[[nodiscard]] rpl::producer<> allDone() const;
void setGeometry(QRect rect);
void raise();
[[nodiscard]] static bool Supported();
private:
void ensureSurface();
void startUpdateTimer();
void stopUpdateTimer();
const not_null<RpWidget*> _parent;
std::unique_ptr<RpWidgetWrap> _surface;
[[maybe_unused]] ThanosEffectRenderer *_renderer = nullptr;
QTimer *_updateTimer = nullptr;
rpl::event_stream<> _allDone;
rpl::lifetime _lifetime;
};
} // namespace Ui
@@ -0,0 +1,586 @@
/*
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 "ui/effects/thanos_effect_renderer.h"
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
#include "ui/rhi/rhi_shader.h"
#include "ui/rp_widget.h"
#include "ui/painter.h"
#include "styles/style_basic.h"
#include "base/debug_log.h"
#include <rhi/qrhi.h>
namespace Ui {
namespace {
constexpr auto kParticleStride = int(24);
constexpr auto kQuadVertexCount = int(6);
constexpr auto kQuadVertexStride = int(2 * sizeof(float));
constexpr auto kComputeWorkgroupSize = int(64);
constexpr auto kMaxPhaseDuration = 4.0f;
const float kQuadVertices[kQuadVertexCount * 2] = {
0.f, 0.f,
1.f, 0.f,
0.f, 1.f,
1.f, 0.f,
0.f, 1.f,
1.f, 1.f,
};
struct alignas(16) ComputeInitUniforms {
uint32_t particleCountX;
uint32_t particleCountY;
uint32_t seed;
uint32_t _pad;
};
static_assert(sizeof(ComputeInitUniforms) % 16 == 0);
struct alignas(16) ComputeUpdateUniforms {
uint32_t particleCountX;
uint32_t particleCountY;
float phase;
float timeStep;
};
static_assert(sizeof(ComputeUpdateUniforms) % 16 == 0);
struct alignas(16) RenderUniforms {
float rect[4];
float size[2];
uint32_t particleResolution[2];
};
static_assert(sizeof(RenderUniforms) % 16 == 0);
[[nodiscard]] QShader LoadShader(const QString &name) {
return Rhi::ShaderFromFile(u":/shaders/"_q + name + u".qsb"_q);
}
} // namespace
ThanosEffectRenderer::ThanosEffectRenderer() {
_elapsed.start();
}
ThanosEffectRenderer::~ThanosEffectRenderer() {
releaseResources();
}
void ThanosEffectRenderer::initialize(
QRhi *rhi,
QRhiRenderTarget *rt,
QRhiCommandBuffer *cb) {
if (_initialized && _rhi == rhi) {
return;
}
releaseResources();
_rhi = rhi;
if (!rhi->isFeatureSupported(QRhi::Compute)) {
LOG(("ThanosEffect: Compute shaders not supported, disabled"));
return;
}
_quadVertexBuffer = rhi->newBuffer(
QRhiBuffer::Immutable,
QRhiBuffer::VertexBuffer,
sizeof(kQuadVertices));
_quadVertexBuffer->create();
_computeInitUniformBuffer = rhi->newBuffer(
QRhiBuffer::Dynamic,
QRhiBuffer::UniformBuffer,
sizeof(ComputeInitUniforms));
_computeInitUniformBuffer->create();
_computeUpdateUniformBuffer = rhi->newBuffer(
QRhiBuffer::Dynamic,
QRhiBuffer::UniformBuffer,
sizeof(ComputeUpdateUniforms));
_computeUpdateUniformBuffer->create();
_renderUniformBuffer = rhi->newBuffer(
QRhiBuffer::Dynamic,
QRhiBuffer::UniformBuffer,
sizeof(RenderUniforms));
_renderUniformBuffer->create();
_placeholderParticleBuffer = rhi->newBuffer(
QRhiBuffer::Immutable,
QRhiBuffer::VertexBuffer | QRhiBuffer::StorageBuffer,
kParticleStride);
_placeholderParticleBuffer->create();
_placeholderTexture = rhi->newTexture(
QRhiTexture::RGBA8,
QSize(1, 1));
_placeholderTexture->create();
_placeholderSampler = rhi->newSampler(
QRhiSampler::Linear,
QRhiSampler::Linear,
QRhiSampler::None,
QRhiSampler::ClampToEdge,
QRhiSampler::ClampToEdge);
_placeholderSampler->create();
createPipelines(rt);
auto *rub = rhi->nextResourceUpdateBatch();
rub->uploadStaticBuffer(_quadVertexBuffer, kQuadVertices);
cb->resourceUpdate(rub);
_initialized = true;
_lastFrameTime = double(_elapsed.elapsed()) / 1000.0;
LOG(("ThanosEffect: initialized, backend=%1 device=%2")
.arg(rhi->backendName())
.arg(rhi->driverInfo().deviceName));
}
void ThanosEffectRenderer::createPipelines(QRhiRenderTarget *rt) {
const auto initShader = LoadShader(u"thanos_init.comp"_q);
const auto updateShader = LoadShader(u"thanos_update.comp"_q);
const auto vertShader = LoadShader(u"thanos.vert"_q);
const auto fragShader = LoadShader(u"thanos.frag"_q);
_computeInitSrbLayout = _rhi->newShaderResourceBindings();
_computeInitSrbLayout->setBindings({
QRhiShaderResourceBinding::bufferLoadStore(
0,
QRhiShaderResourceBinding::ComputeStage,
_placeholderParticleBuffer),
QRhiShaderResourceBinding::uniformBuffer(
1,
QRhiShaderResourceBinding::ComputeStage,
_computeInitUniformBuffer),
});
_computeInitSrbLayout->create();
_computeInitPipeline = _rhi->newComputePipeline();
_computeInitPipeline->setShaderStage(
{ QRhiShaderStage::Compute, initShader });
_computeInitPipeline->setShaderResourceBindings(_computeInitSrbLayout);
_computeInitPipeline->create();
_computeUpdateSrbLayout = _rhi->newShaderResourceBindings();
_computeUpdateSrbLayout->setBindings({
QRhiShaderResourceBinding::bufferLoadStore(
0,
QRhiShaderResourceBinding::ComputeStage,
_placeholderParticleBuffer),
QRhiShaderResourceBinding::uniformBuffer(
1,
QRhiShaderResourceBinding::ComputeStage,
_computeUpdateUniformBuffer),
});
_computeUpdateSrbLayout->create();
_computeUpdatePipeline = _rhi->newComputePipeline();
_computeUpdatePipeline->setShaderStage(
{ QRhiShaderStage::Compute, updateShader });
_computeUpdatePipeline->setShaderResourceBindings(
_computeUpdateSrbLayout);
_computeUpdatePipeline->create();
_renderSrbLayout = _rhi->newShaderResourceBindings();
_renderSrbLayout->setBindings({
QRhiShaderResourceBinding::uniformBuffer(
0,
QRhiShaderResourceBinding::VertexStage,
_renderUniformBuffer),
QRhiShaderResourceBinding::sampledTexture(
1,
QRhiShaderResourceBinding::FragmentStage,
_placeholderTexture,
_placeholderSampler),
});
_renderSrbLayout->create();
_renderPipeline = _rhi->newGraphicsPipeline();
_renderPipeline->setShaderStages({
{ QRhiShaderStage::Vertex, vertShader },
{ QRhiShaderStage::Fragment, fragShader },
});
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({
{ quint32(kQuadVertexStride) },
{ quint32(kParticleStride),
QRhiVertexInputBinding::PerInstance },
});
inputLayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float2, 0 },
{ 1, 1, QRhiVertexInputAttribute::Float2, 0 },
{ 1, 2, QRhiVertexInputAttribute::Float, 16 },
});
_renderPipeline->setVertexInputLayout(inputLayout);
QRhiGraphicsPipeline::TargetBlend blend;
blend.enable = true;
blend.srcColor = QRhiGraphicsPipeline::One;
blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
blend.srcAlpha = QRhiGraphicsPipeline::One;
blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
_renderPipeline->setTargetBlends({ blend });
_renderPipeline->setTopology(QRhiGraphicsPipeline::Triangles);
_renderPipeline->setShaderResourceBindings(_renderSrbLayout);
_renderPipeline->setRenderPassDescriptor(
rt->renderPassDescriptor());
_renderPipeline->create();
}
void ThanosEffectRenderer::render(
QRhi *rhi,
QRhiRenderTarget *rt,
QRhiCommandBuffer *cb) {
if (!_initialized || !rhi->isFeatureSupported(QRhi::Compute)) {
return;
}
_rhi = rhi;
const auto now = double(_elapsed.elapsed()) / 1000.0;
const auto dt = float(std::clamp(now - _lastFrameTime, 0.001, 0.1));
_lastFrameTime = now;
addPendingItems(cb);
if (_items.empty()) {
return;
}
const auto pixelSize = rt->pixelSize();
const auto factor = style::DevicePixelRatio();
const auto viewW = float(pixelSize.width()) / factor;
const auto viewH = float(pixelSize.height()) / factor;
bool needsInit = false;
for (auto &item : _items) {
item.phase += dt * 2.0f;
if (!item.particlesInitialized) {
needsInit = true;
}
}
if (needsInit) {
auto *rub = rhi->nextResourceUpdateBatch();
for (auto &item : _items) {
if (!item.particlesInitialized) {
item.particlesInitialized = true;
ComputeInitUniforms uni;
uni.particleCountX = item.particleCountX;
uni.particleCountY = item.particleCountY;
uni.seed = _seedCounter++;
uni._pad = 0;
rub->updateDynamicBuffer(
_computeInitUniformBuffer,
0,
sizeof(uni),
&uni);
}
}
cb->beginComputePass(rub);
for (auto &item : _items) {
if (item.phase <= dt * 2.1f) {
cb->setComputePipeline(_computeInitPipeline);
cb->setShaderResources(item.computeInitSrb);
const auto count = item.particleCountX * item.particleCountY;
const auto groups = (count + kComputeWorkgroupSize - 1)
/ kComputeWorkgroupSize;
cb->dispatch(int(groups), 1, 1);
}
}
cb->endComputePass();
}
{
auto *rub = rhi->nextResourceUpdateBatch();
for (auto &item : _items) {
ComputeUpdateUniforms uni;
uni.particleCountX = item.particleCountX;
uni.particleCountY = item.particleCountY;
uni.phase = item.phase;
uni.timeStep = dt * 2.0f;
rub->updateDynamicBuffer(
_computeUpdateUniformBuffer,
0,
sizeof(uni),
&uni);
}
cb->beginComputePass(rub);
for (auto &item : _items) {
if (item.phase >= kMaxPhaseDuration) {
continue;
}
cb->setComputePipeline(_computeUpdatePipeline);
cb->setShaderResources(item.computeUpdateSrb);
const auto count = item.particleCountX * item.particleCountY;
const auto groups = (count + kComputeWorkgroupSize - 1)
/ kComputeWorkgroupSize;
cb->dispatch(int(groups), 1, 1);
}
cb->endComputePass();
}
{
const auto bg = QColor(0, 0, 0, 0);
cb->beginPass(rt, bg, { 1.0f, 0 });
for (auto &item : _items) {
if (item.phase >= kMaxPhaseDuration) {
continue;
}
RenderUniforms uni;
uni.rect[0] = float(item.rect.x()) / viewW;
uni.rect[1] = float(item.rect.y()) / viewH;
uni.rect[2] = float(item.rect.width()) / viewW;
uni.rect[3] = float(item.rect.height()) / viewH;
uni.size[0] = float(item.rect.width());
uni.size[1] = float(item.rect.height());
uni.particleResolution[0] = item.particleCountX;
uni.particleResolution[1] = item.particleCountY;
auto *rub = rhi->nextResourceUpdateBatch();
rub->updateDynamicBuffer(
_renderUniformBuffer,
0,
sizeof(uni),
&uni);
cb->resourceUpdate(rub);
cb->setGraphicsPipeline(_renderPipeline);
cb->setShaderResources(item.renderSrb);
cb->setViewport({
0, 0,
float(pixelSize.width()),
float(pixelSize.height()) });
const QRhiCommandBuffer::VertexInput vbufs[] = {
{ _quadVertexBuffer, 0 },
{ item.particleBuffer, 0 },
};
cb->setVertexInput(0, 2, vbufs);
const auto instanceCount =
item.particleCountX * item.particleCountY;
cb->draw(kQuadVertexCount, instanceCount);
}
cb->endPass();
}
// Remove finished items using deleteLater() so QRhi resources
// survive until the command buffer is fully submitted.
auto hadItems = !_items.empty();
_items.erase(
std::remove_if(_items.begin(), _items.end(), [&](auto &item) {
if (item.phase >= kMaxPhaseDuration) {
if (item.renderSrb) item.renderSrb->deleteLater();
if (item.computeUpdateSrb) item.computeUpdateSrb->deleteLater();
if (item.computeInitSrb) item.computeInitSrb->deleteLater();
if (item.renderUniformBuffer) item.renderUniformBuffer->deleteLater();
if (item.computeUpdateUniformBuffer) item.computeUpdateUniformBuffer->deleteLater();
if (item.computeInitUniformBuffer) item.computeInitUniformBuffer->deleteLater();
if (item.particleBuffer) item.particleBuffer->deleteLater();
if (item.sampler) item.sampler->deleteLater();
if (item.texture) item.texture->deleteLater();
item = {};
return true;
}
return false;
}),
_items.end());
if (hadItems && _items.empty()) {
_allDone.fire({});
}
}
void ThanosEffectRenderer::addItem(ThanosItem item) {
_pendingItems.push_back(std::move(item));
}
bool ThanosEffectRenderer::hasActiveItems() const {
return !_items.empty() || !_pendingItems.empty();
}
rpl::producer<> ThanosEffectRenderer::allDone() const {
return _allDone.events();
}
void ThanosEffectRenderer::addPendingItems(QRhiCommandBuffer *cb) {
if (_pendingItems.empty() || !_rhi) {
return;
}
auto *rub = _rhi->nextResourceUpdateBatch();
for (auto &pending : _pendingItems) {
auto animating = createAnimatingItem(std::move(pending));
if (animating.texture) {
auto image = animating.uploadImage;
if (!image.isNull()) {
rub->uploadTexture(
animating.texture,
QRhiTextureUploadDescription(
QRhiTextureUploadEntry(
0, 0,
QRhiTextureSubresourceUploadDescription(
image))));
}
animating.uploadImage = QImage();
_items.push_back(std::move(animating));
}
}
_pendingItems.clear();
if (hasUploads) {
cb->resourceUpdate(rub);
} else {
delete rub;
}
}
ThanosEffectRenderer::AnimatingItem ThanosEffectRenderer::createAnimatingItem(
ThanosItem &&item) {
AnimatingItem result;
result.rect = item.rect;
const auto w = int(item.rect.width());
const auto h = int(item.rect.height());
if (w <= 0 || h <= 0 || item.snapshot.isNull()) {
return result;
}
result.particleCountX = uint32_t(w);
result.particleCountY = uint32_t(h);
const auto particleCount =
result.particleCountX * result.particleCountY;
auto *tex = _rhi->newTexture(
QRhiTexture::RGBA8,
QSize(item.snapshot.width(), item.snapshot.height()));
tex->create();
result.texture = tex;
result.uploadImage = item.snapshot.convertToFormat(
QImage::Format_RGBA8888_Premultiplied);
auto *sampler = _rhi->newSampler(
QRhiSampler::Linear,
QRhiSampler::Linear,
QRhiSampler::None,
QRhiSampler::ClampToEdge,
QRhiSampler::ClampToEdge);
sampler->create();
result.sampler = sampler;
auto *particleBuf = _rhi->newBuffer(
QRhiBuffer::Immutable,
QRhiBuffer::VertexBuffer | QRhiBuffer::StorageBuffer,
particleCount * kParticleStride);
particleBuf->create();
result.particleBuffer = particleBuf;
result.computeInitSrb = _rhi->newShaderResourceBindings();
result.computeInitSrb->setBindings({
QRhiShaderResourceBinding::bufferLoadStore(
0,
QRhiShaderResourceBinding::ComputeStage,
particleBuf),
QRhiShaderResourceBinding::uniformBuffer(
1,
QRhiShaderResourceBinding::ComputeStage,
_computeInitUniformBuffer),
});
result.computeInitSrb->create();
result.computeUpdateSrb = _rhi->newShaderResourceBindings();
result.computeUpdateSrb->setBindings({
QRhiShaderResourceBinding::bufferLoadStore(
0,
QRhiShaderResourceBinding::ComputeStage,
particleBuf),
QRhiShaderResourceBinding::uniformBuffer(
1,
QRhiShaderResourceBinding::ComputeStage,
_computeUpdateUniformBuffer),
});
result.computeUpdateSrb->create();
result.renderSrb = _rhi->newShaderResourceBindings();
result.renderSrb->setBindings({
QRhiShaderResourceBinding::uniformBuffer(
0,
QRhiShaderResourceBinding::VertexStage,
_renderUniformBuffer),
QRhiShaderResourceBinding::sampledTexture(
1,
QRhiShaderResourceBinding::FragmentStage,
tex,
sampler),
});
result.renderSrb->create();
return result;
}
void ThanosEffectRenderer::destroyAnimatingItem(AnimatingItem &item) {
delete item.renderSrb;
delete item.computeUpdateSrb;
delete item.computeInitSrb;
delete item.particleBuffer;
delete item.sampler;
delete item.texture;
item = {};
}
void ThanosEffectRenderer::releaseResources() {
for (auto &item : _items) {
destroyAnimatingItem(item);
}
_items.clear();
delete _renderPipeline;
_renderPipeline = nullptr;
delete _renderSrbLayout;
_renderSrbLayout = nullptr;
delete _computeUpdatePipeline;
_computeUpdatePipeline = nullptr;
delete _computeUpdateSrbLayout;
_computeUpdateSrbLayout = nullptr;
delete _computeInitPipeline;
_computeInitPipeline = nullptr;
delete _computeInitSrbLayout;
_computeInitSrbLayout = nullptr;
delete _placeholderSampler;
_placeholderSampler = nullptr;
delete _placeholderTexture;
_placeholderTexture = nullptr;
delete _placeholderParticleBuffer;
_placeholderParticleBuffer = nullptr;
delete _renderUniformBuffer;
_renderUniformBuffer = nullptr;
delete _computeUpdateUniformBuffer;
_computeUpdateUniformBuffer = nullptr;
delete _computeInitUniformBuffer;
_computeInitUniformBuffer = nullptr;
delete _quadVertexBuffer;
_quadVertexBuffer = nullptr;
_initialized = false;
}
} // namespace Ui
#endif // Qt >= 6.7
@@ -0,0 +1,119 @@
/*
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
*/
#pragma once
#include "ui/rhi/rhi_renderer.h"
#include "ui/gl/gl_surface.h"
#include <QElapsedTimer>
#include <QImage>
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
class QRhi;
class QRhiBuffer;
class QRhiTexture;
class QRhiSampler;
class QRhiGraphicsPipeline;
class QRhiComputePipeline;
class QRhiShaderResourceBindings;
class QRhiRenderTarget;
class QRhiCommandBuffer;
namespace Ui {
struct ThanosItem {
QImage snapshot;
QRectF rect;
};
class ThanosEffectRenderer final
: public GL::Renderer
, public Rhi::Renderer {
public:
ThanosEffectRenderer();
~ThanosEffectRenderer();
void initialize(
QRhi *rhi,
QRhiRenderTarget *rt,
QRhiCommandBuffer *cb) override;
void render(
QRhi *rhi,
QRhiRenderTarget *rt,
QRhiCommandBuffer *cb) override;
void releaseResources() override;
QColor rhiClearColor() override {
return QColor(0, 0, 0, 0);
}
std::optional<QColor> clearColor() override {
return QColor(0, 0, 0, 0);
}
void addItem(ThanosItem item);
[[nodiscard]] bool hasActiveItems() const;
rpl::producer<> allDone() const;
private:
struct AnimatingItem {
QRhiTexture *texture = nullptr;
QRhiSampler *sampler = nullptr;
QRhiBuffer *particleBuffer = nullptr;
QRhiShaderResourceBindings *computeInitSrb = nullptr;
QRhiShaderResourceBindings *computeUpdateSrb = nullptr;
QRhiShaderResourceBindings *renderSrb = nullptr;
QImage uploadImage;
QRectF rect;
uint32_t particleCountX = 0;
uint32_t particleCountY = 0;
float phase = 0.f;
bool particlesInitialized = false;
};
void createPipelines(QRhiRenderTarget *rt);
void addPendingItems(QRhiCommandBuffer *cb);
AnimatingItem createAnimatingItem(ThanosItem &&item);
void destroyAnimatingItem(AnimatingItem &item);
QRhi *_rhi = nullptr;
QRhiBuffer *_quadVertexBuffer = nullptr;
QRhiBuffer *_computeInitUniformBuffer = nullptr;
QRhiBuffer *_computeUpdateUniformBuffer = nullptr;
QRhiBuffer *_renderUniformBuffer = nullptr;
QRhiBuffer *_placeholderParticleBuffer = nullptr;
QRhiTexture *_placeholderTexture = nullptr;
QRhiSampler *_placeholderSampler = nullptr;
QRhiShaderResourceBindings *_computeInitSrbLayout = nullptr;
QRhiShaderResourceBindings *_computeUpdateSrbLayout = nullptr;
QRhiShaderResourceBindings *_renderSrbLayout = nullptr;
QRhiComputePipeline *_computeInitPipeline = nullptr;
QRhiComputePipeline *_computeUpdatePipeline = nullptr;
QRhiGraphicsPipeline *_renderPipeline = nullptr;
std::vector<AnimatingItem> _items;
std::vector<ThanosItem> _pendingItems;
QElapsedTimer _elapsed;
double _lastFrameTime = 0.;
bool _initialized = false;
uint32_t _seedCounter = 0;
rpl::event_stream<> _allDone;
};
} // namespace Ui
#endif // Qt >= 6.7
+15 -4
View File
@@ -1,4 +1,4 @@
# Compile QRhi shaders (.vert/.frag -> .qsb) at build time.
# Compile QRhi shaders (.vert/.frag/.comp -> .qsb) at build time.
#
# Usage: include(cmake/qrhi_shaders.cmake)
# Requires: target "Telegram" and function "nice_target_sources" to exist.
@@ -15,16 +15,27 @@ if (QSB_EXECUTABLE)
set(_shader_dir "${CMAKE_CURRENT_SOURCE_DIR}/shaders")
set(_qsb_out_dir "${CMAKE_CURRENT_BINARY_DIR}/shaders")
file(MAKE_DIRECTORY ${_qsb_out_dir})
file(GLOB _shader_sources "${_shader_dir}/*.vert" "${_shader_dir}/*.frag")
file(GLOB _shader_sources
"${_shader_dir}/*.vert"
"${_shader_dir}/*.frag"
"${_shader_dir}/*.comp")
set(_qsb_outputs)
set(_qrc_entries)
foreach(_src ${_shader_sources})
get_filename_component(_name ${_src} NAME)
get_filename_component(_ext ${_src} LAST_EXT)
set(_qsb "${_qsb_out_dir}/${_name}.qsb")
if("${_ext}" STREQUAL ".comp")
set(_glsl_ver "310es,430")
else()
set(_glsl_ver "100es,120,150")
endif()
add_custom_command(
OUTPUT ${_qsb}
COMMAND ${QSB_EXECUTABLE}
--glsl "100es,120,150"
--glsl "${_glsl_ver}"
--hlsl 50
--msl 12
-o ${_qsb}
@@ -45,7 +56,7 @@ if (QSB_EXECUTABLE)
shaders.qrc
)
add_dependencies(Telegram compile_shaders)
message(STATUS "QSB: found ${QSB_EXECUTABLE}, will compile ${_shader_dir}/*.vert/*.frag")
message(STATUS "QSB: found ${QSB_EXECUTABLE}, will compile ${_shader_dir}/*.vert/*.frag/*.comp")
else()
message(STATUS "QSB: not found, shaders will not be compiled")
endif()
+16
View File
@@ -0,0 +1,16 @@
#version 450
layout(location = 0) in vec2 v_texcoord;
layout(location = 1) in float v_alpha;
layout(location = 0) out vec4 fragColor;
layout(binding = 1) uniform sampler2D tex;
void main() {
if (v_alpha <= 0.0) {
discard;
}
vec4 color = texture(tex, vec2(v_texcoord.x, 1.0 - v_texcoord.y));
fragColor = color * v_alpha;
}
+39
View File
@@ -0,0 +1,39 @@
#version 450
layout(location = 0) in vec2 inQuadPos;
layout(location = 1) in vec2 inOffset;
layout(location = 2) in float inLifetime;
layout(location = 0) out vec2 v_texcoord;
layout(location = 1) out float v_alpha;
layout(std140, binding = 0) uniform Params {
vec4 rect;
vec2 size;
uvec2 particleResolution;
};
void main() {
uint particleId = uint(gl_InstanceIndex);
uint pX = particleId % particleResolution.x;
uint pY = particleId / particleResolution.x;
vec2 particleSize = size / vec2(particleResolution);
vec2 topLeft = vec2(float(pX) * particleSize.x, float(pY) * particleSize.y);
v_texcoord = (topLeft + inQuadPos * particleSize) / size;
topLeft += inOffset;
vec2 position = topLeft + inQuadPos * particleSize;
vec2 ndc;
ndc.x = rect.x + position.x / size.x * rect.z;
ndc.y = rect.y + position.y / size.y * rect.w;
ndc.x = -1.0 + ndc.x * 2.0;
ndc.y = -1.0 + ndc.y * 2.0;
gl_Position = vec4(ndc, 0.0, 1.0);
v_alpha = clamp(inLifetime / 0.3, 0.0, 1.0);
}
+55
View File
@@ -0,0 +1,55 @@
#version 450
layout(local_size_x = 64) in;
struct Particle {
vec2 offset;
vec2 velocity;
float lifetime;
float _pad;
};
layout(std430, binding = 0) buffer Particles {
Particle particles[];
};
layout(std140, binding = 1) uniform Params {
uint particleCountX;
uint particleCountY;
uint seed;
uint _pad;
};
uint hash(uint x) {
x ^= x >> 16u;
x *= 0x45d9f3bu;
x ^= x >> 16u;
x *= 0x45d9f3bu;
x ^= x >> 16u;
return x;
}
float hashFloat(uint x) {
return float(hash(x)) / float(0xFFFFFFFFu);
}
void main() {
uint gid = gl_GlobalInvocationID.x;
uint count = particleCountX * particleCountY;
if (gid >= count) {
return;
}
uint s = gid * 3u + seed;
float direction = hashFloat(s) * (3.14159265 * 2.0);
float speed = (0.1 + hashFloat(s + 1u) * 0.1) * 420.0;
Particle p;
p.offset = vec2(0.0, 0.0);
p.velocity = vec2(cos(direction) * speed, sin(direction) * speed);
p.lifetime = 0.7 + hashFloat(s + 2u) * 0.8;
p._pad = 0.0;
particles[gid] = p;
}
+53
View File
@@ -0,0 +1,53 @@
#version 450
layout(local_size_x = 64) in;
struct Particle {
vec2 offset;
vec2 velocity;
float lifetime;
float _pad;
};
layout(std430, binding = 0) buffer Particles {
Particle particles[];
};
layout(std140, binding = 1) uniform Params {
uint particleCountX;
uint particleCountY;
float phase;
float timeStep;
};
float easeInWindow(float fraction, float t) {
float windowSize = 0.8;
float windowStart = -windowSize;
float windowEnd = 1.0;
float windowPos = (1.0 - fraction) * windowStart + fraction * windowEnd;
float windowT = clamp(t - windowPos, 0.0, windowSize) / windowSize;
return 1.0 - windowT;
}
void main() {
uint gid = gl_GlobalInvocationID.x;
uint count = particleCountX * particleCountY;
if (gid >= count) {
return;
}
float easeInDuration = 0.8;
float effectFraction = clamp(phase, 0.0, easeInDuration) / easeInDuration;
uint particleX = gid % particleCountX;
float particleXFraction = float(particleX) / float(particleCountX);
float particleFraction = easeInWindow(effectFraction, particleXFraction);
Particle p = particles[gid];
p.offset += p.velocity * timeStep * particleFraction;
p.velocity.y += 120.0 * timeStep * particleFraction;
p.lifetime = max(0.0, p.lifetime - timeStep * particleFraction);
particles[gid] = p;
}