From e75dba5c8046b9c40f202aee42617d9a4f4e7b18 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 2 Apr 2026 14:24:50 +0300 Subject: [PATCH] [thanos] Added GPU-accelerated particle dissolution effect for messages. --- Telegram/CMakeLists.txt | 4 + Telegram/SourceFiles/data/data_session.cpp | 10 + Telegram/SourceFiles/data/data_session.h | 3 + Telegram/SourceFiles/history/history.cpp | 1 + .../history/history_inner_widget.cpp | 56 ++ .../history/history_inner_widget.h | 4 + .../SourceFiles/ui/effects/thanos_effect.cpp | 131 ++++ .../SourceFiles/ui/effects/thanos_effect.h | 56 ++ .../ui/effects/thanos_effect_renderer.cpp | 586 ++++++++++++++++++ .../ui/effects/thanos_effect_renderer.h | 119 ++++ Telegram/cmake/qrhi_shaders.cmake | 19 +- Telegram/shaders/thanos.frag | 16 + Telegram/shaders/thanos.vert | 39 ++ Telegram/shaders/thanos_init.comp | 55 ++ Telegram/shaders/thanos_update.comp | 53 ++ 15 files changed, 1148 insertions(+), 4 deletions(-) create mode 100644 Telegram/SourceFiles/ui/effects/thanos_effect.cpp create mode 100644 Telegram/SourceFiles/ui/effects/thanos_effect.h create mode 100644 Telegram/SourceFiles/ui/effects/thanos_effect_renderer.cpp create mode 100644 Telegram/SourceFiles/ui/effects/thanos_effect_renderer.h create mode 100644 Telegram/shaders/thanos.frag create mode 100644 Telegram/shaders/thanos.vert create mode 100644 Telegram/shaders/thanos_init.comp create mode 100644 Telegram/shaders/thanos_update.comp diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 5d4d5b4c2e..9a19e4b882 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -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 diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 315f2c0791..2d74bb9389 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -2182,6 +2182,16 @@ rpl::producer> Session::itemRemoved( }); } +void Session::notifyViewAboutToBeRemoved( + not_null view) { + _viewAboutToBeRemoved.fire_copy(view); +} + +rpl::producer> +Session::viewAboutToBeRemoved() const { + return _viewAboutToBeRemoved.events(); +} + void Session::notifyViewRemoved(not_null view) { _viewRemoved.fire_copy(view); } diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index eee6d4ae20..265eff943f 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -427,6 +427,8 @@ public: [[nodiscard]] rpl::producer> itemRemoved() const; [[nodiscard]] rpl::producer> itemRemoved( FullMsgId itemId) const; + void notifyViewAboutToBeRemoved(not_null view); + [[nodiscard]] rpl::producer> viewAboutToBeRemoved() const; void notifyViewRemoved(not_null view); [[nodiscard]] rpl::producer> viewRemoved() const; void notifyHistoryCleared(not_null history); @@ -1171,6 +1173,7 @@ private: rpl::event_stream> _itemDataChanges; rpl::event_stream _reactionsRemoved; rpl::event_stream> _itemRemoved; + rpl::event_stream> _viewAboutToBeRemoved; rpl::event_stream> _viewRemoved; rpl::event_stream> _viewPaidReactionSent; rpl::event_stream> _callPaidReactionSent; diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index dc77de07bb..825ef68f4b 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -4395,6 +4395,7 @@ int HistoryBlock::resizeGetHeight(int newWidth, ResizeRequest request) { void HistoryBlock::remove(not_null view) { Expects(view->block() == this); + _history->owner().notifyViewAboutToBeRemoved(view); _history->mainViewRemoved(this, view); const auto blockIndex = indexInHistory(); diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index f9d57f7036..b05cdc99be 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -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 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(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(); diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 8120516f75..763db7e53e 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -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 _overlayHost; + void captureViewForThanosEffect(not_null view); + std::unique_ptr _thanosEffect; + }; [[nodiscard]] bool CanSendReply(not_null item); diff --git a/Telegram/SourceFiles/ui/effects/thanos_effect.cpp b/Telegram/SourceFiles/ui/effects/thanos_effect.cpp new file mode 100644 index 0000000000..e719523eca --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/thanos_effect.cpp @@ -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 + +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) +#include +#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 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(); + _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 diff --git a/Telegram/SourceFiles/ui/effects/thanos_effect.h b/Telegram/SourceFiles/ui/effects/thanos_effect.h new file mode 100644 index 0000000000..d901216564 --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/thanos_effect.h @@ -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 + +namespace Ui { +class RpWidget; +class RpWidgetWrap; +} // namespace Ui + +namespace Ui { + +class ThanosEffectRenderer; + +class ThanosEffect final { +public: + explicit ThanosEffect(not_null 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 _parent; + + std::unique_ptr _surface; + [[maybe_unused]] ThanosEffectRenderer *_renderer = nullptr; + + QTimer *_updateTimer = nullptr; + + rpl::event_stream<> _allDone; + rpl::lifetime _lifetime; + +}; + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/effects/thanos_effect_renderer.cpp b/Telegram/SourceFiles/ui/effects/thanos_effect_renderer.cpp new file mode 100644 index 0000000000..2a606eee21 --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/thanos_effect_renderer.cpp @@ -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 + +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 diff --git a/Telegram/SourceFiles/ui/effects/thanos_effect_renderer.h b/Telegram/SourceFiles/ui/effects/thanos_effect_renderer.h new file mode 100644 index 0000000000..d6ff5c075e --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/thanos_effect_renderer.h @@ -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 +#include + +#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 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 _items; + std::vector _pendingItems; + + QElapsedTimer _elapsed; + double _lastFrameTime = 0.; + bool _initialized = false; + uint32_t _seedCounter = 0; + + rpl::event_stream<> _allDone; + +}; + +} // namespace Ui + +#endif // Qt >= 6.7 diff --git a/Telegram/cmake/qrhi_shaders.cmake b/Telegram/cmake/qrhi_shaders.cmake index c2da4acbe6..ef8aad7fe3 100644 --- a/Telegram/cmake/qrhi_shaders.cmake +++ b/Telegram/cmake/qrhi_shaders.cmake @@ -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() diff --git a/Telegram/shaders/thanos.frag b/Telegram/shaders/thanos.frag new file mode 100644 index 0000000000..3510c87a1a --- /dev/null +++ b/Telegram/shaders/thanos.frag @@ -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; +} diff --git a/Telegram/shaders/thanos.vert b/Telegram/shaders/thanos.vert new file mode 100644 index 0000000000..dc37217025 --- /dev/null +++ b/Telegram/shaders/thanos.vert @@ -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); +} diff --git a/Telegram/shaders/thanos_init.comp b/Telegram/shaders/thanos_init.comp new file mode 100644 index 0000000000..1cfd8f4d2f --- /dev/null +++ b/Telegram/shaders/thanos_init.comp @@ -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; +} diff --git a/Telegram/shaders/thanos_update.comp b/Telegram/shaders/thanos_update.comp new file mode 100644 index 0000000000..dc1c8d6220 --- /dev/null +++ b/Telegram/shaders/thanos_update.comp @@ -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; +}