434 lines
17 KiB
C++
434 lines
17 KiB
C++
// Plasma (immersive shader scene, mode 1) — the QRhi renderer behind
|
|
// PlasmaItem. Ports the Slint feedback loop (crates/qbz/src/
|
|
// shader_underlay.rs:1003-1025: render the frame sampling the history
|
|
// texture, then copy it back) as a PING-PONG over two owned RGBA8 textures
|
|
// (the reference's TEX_FORMAT Rgba8Unorm, shader_underlay.rs:39): pass 1
|
|
// renders the plasma into history[write] sampling history[read], pass 2
|
|
// blits history[write] into the item's render target, then the indices
|
|
// swap. Owning both targets means the item's texture is never read back and
|
|
// no Qt-internal attachment is touched.
|
|
//
|
|
// Resource cadence (the pulse law): `synchronize` snapshots the QML-side
|
|
// properties when the item updates (pulse tick only); per frame the GPU
|
|
// work is one uniform upload + two fullscreen triangles — cheaper than any
|
|
// of the ShaderEffect scenes.
|
|
//
|
|
// History size tracks the item's physical size CAPPED at 2560x1440 (the
|
|
// reference's offscreen-target ceiling, shader_underlay.rs:37-39; the
|
|
// ShaderSceneLayer resW/resH rule); on a bigger screen the blit upscales.
|
|
// A resize rebuilds the pair and re-clears it (the field restarts — the
|
|
// same thing the reference gets when its window-size-tracking resources
|
|
// rebuild, shader_underlay.rs:522-523).
|
|
|
|
#include "plasma_item.h"
|
|
|
|
#include <QtCore/QCoreApplication>
|
|
#include <QtCore/QFile>
|
|
#include <QtQml/qqml.h>
|
|
#include <rhi/qrhi.h>
|
|
|
|
#include <cstring>
|
|
|
|
namespace {
|
|
|
|
// The history-target ceiling (see the header).
|
|
constexpr int MAX_W = 2560;
|
|
constexpr int MAX_H = 1440;
|
|
|
|
// The FULL 144-byte std140 scene block (spec 01 §1) — plasma.frag declares
|
|
// every field even though it reads only some; the unread ones stay zero.
|
|
struct UniformData
|
|
{
|
|
float time; // 0
|
|
float phase; // 4
|
|
float beat; // 8
|
|
float level; // 12
|
|
float res[2]; // 16
|
|
float levelSmooth; // 24
|
|
float transient; // 28
|
|
float energyLo[4]; // 32
|
|
float energyHi[4]; // 48
|
|
float bandsLo[4]; // 64
|
|
float bandsHi[4]; // 80
|
|
float primary[4]; // 96
|
|
float secondary[4];// 112
|
|
float accent[4]; // 128
|
|
};
|
|
static_assert(sizeof(UniformData) == 144, "std140 scene block");
|
|
|
|
QShader loadShader(const QString &qrcPath)
|
|
{
|
|
QFile f(qrcPath);
|
|
if (f.open(QIODevice::ReadOnly)) {
|
|
const QShader s = QShader::fromSerialized(f.readAll());
|
|
if (!s.isValid())
|
|
qWarning("[plasma] shader %s failed to deserialize", qPrintable(qrcPath));
|
|
return s;
|
|
}
|
|
qWarning("[plasma] shader %s not found in the qrc", qPrintable(qrcPath));
|
|
return QShader();
|
|
}
|
|
|
|
QVector4D colorVec(const QColor &c)
|
|
{
|
|
return QVector4D(float(c.redF()), float(c.greenF()), float(c.blueF()), 1.0f);
|
|
}
|
|
|
|
class PlasmaRenderer : public QQuickRhiItemRenderer
|
|
{
|
|
public:
|
|
~PlasmaRenderer() override
|
|
{
|
|
// The render pass descriptors are OURS (the RTs borrow them).
|
|
delete m_histRpDesc[0];
|
|
delete m_histRpDesc[1];
|
|
}
|
|
|
|
void initialize(QRhiCommandBuffer *cb) override
|
|
{
|
|
QRhi *r = cb->rhi();
|
|
if (m_vbuf) // already built (initialize can re-run on rhi changes;
|
|
return; // the rebuild path drops everything first)
|
|
// The fullscreen triangle (rhi_triangle.vert replaces gl_VertexID
|
|
// with this buffer — the GLES 3.0-portable spelling, linebed
|
|
// idiom). Shared by the feedback AND blit passes.
|
|
static const float TRI[] = { -1.0f, -1.0f, 3.0f, -1.0f, -1.0f, 3.0f };
|
|
m_vbuf.reset(r->newBuffer(QRhiBuffer::Immutable,
|
|
QRhiBuffer::VertexBuffer,
|
|
quint32(sizeof(TRI))));
|
|
m_ubuf.reset(r->newBuffer(QRhiBuffer::Dynamic,
|
|
QRhiBuffer::UniformBuffer,
|
|
quint32(sizeof(UniformData))));
|
|
m_sampler.reset(r->newSampler(QRhiSampler::Linear,
|
|
QRhiSampler::Linear,
|
|
QRhiSampler::None,
|
|
QRhiSampler::ClampToEdge,
|
|
QRhiSampler::ClampToEdge));
|
|
if (!m_vbuf->create() || !m_ubuf->create() || !m_sampler->create()) {
|
|
qWarning("[plasma] resource creation failed — the scene stays dark");
|
|
m_vbuf.reset();
|
|
return;
|
|
}
|
|
QRhiResourceUpdateBatch *u = r->nextResourceUpdateBatch();
|
|
u->uploadStaticBuffer(m_vbuf.get(), TRI);
|
|
cb->resourceUpdate(u);
|
|
}
|
|
|
|
void synchronize(QQuickRhiItem *item) override
|
|
{
|
|
// GUI thread: snapshot the item's properties. Runs on item update()
|
|
// (the pulse tick) and on geometry/rhi changes.
|
|
auto *pl = static_cast<PlasmaItem *>(item);
|
|
m_time = pl->m_time;
|
|
m_beat = pl->m_beat;
|
|
m_level = pl->m_level;
|
|
m_levelSmooth = pl->m_levelSmooth;
|
|
m_energyLo = pl->m_energyLo;
|
|
m_energyHi = pl->m_energyHi;
|
|
m_transientAmp = pl->m_transientAmp;
|
|
m_bandsLo = pl->m_bandsLo;
|
|
m_bandsHi = pl->m_bandsHi;
|
|
m_primary = colorVec(pl->m_primary);
|
|
m_secondary = colorVec(pl->m_secondary);
|
|
m_accent = colorVec(pl->m_accent);
|
|
}
|
|
|
|
void render(QRhiCommandBuffer *cb) override
|
|
{
|
|
if (!m_vbuf)
|
|
return; // resource creation failed in initialize
|
|
QRhiRenderTarget *rt = renderTarget();
|
|
const QSize ps = rt->pixelSize();
|
|
if (ps.isEmpty())
|
|
return;
|
|
const QSize hist = capped(ps);
|
|
if (hist != m_histSize)
|
|
rebuildHistory(hist);
|
|
if (!m_hist[0])
|
|
return; // history creation failed — stay dark
|
|
|
|
// Pipeline A (plasma) renders into the history pair's pass; pipeline
|
|
// B (blit) into the item's. Rebuild each when its descriptor moves.
|
|
if (!m_plasmaPipe || m_histRt[0]->renderPassDescriptor() != m_plasmaPassDescriptor) {
|
|
m_plasmaPassDescriptor = m_histRt[0]->renderPassDescriptor();
|
|
buildPlasmaPipeline();
|
|
if (!m_plasmaPipe)
|
|
return;
|
|
}
|
|
if (!m_blitPipe || rt->renderPassDescriptor() != m_blitPassDescriptor) {
|
|
m_blitPassDescriptor = rt->renderPassDescriptor();
|
|
buildBlitPipeline();
|
|
if (!m_blitPipe)
|
|
return;
|
|
}
|
|
|
|
QRhiResourceUpdateBatch *u = rhi()->nextResourceUpdateBatch();
|
|
UniformData ud = {};
|
|
ud.time = m_time;
|
|
ud.beat = m_beat;
|
|
ud.level = m_level;
|
|
ud.res[0] = float(hist.width());
|
|
ud.res[1] = float(hist.height());
|
|
ud.levelSmooth = m_levelSmooth;
|
|
ud.transient = m_transientAmp;
|
|
std::memcpy(ud.energyLo, &m_energyLo, sizeof(ud.energyLo));
|
|
std::memcpy(ud.energyHi, &m_energyHi, sizeof(ud.energyHi));
|
|
std::memcpy(ud.bandsLo, &m_bandsLo, sizeof(ud.bandsLo));
|
|
std::memcpy(ud.bandsHi, &m_bandsHi, sizeof(ud.bandsHi));
|
|
std::memcpy(ud.primary, &m_primary, sizeof(ud.primary));
|
|
std::memcpy(ud.secondary, &m_secondary, sizeof(ud.secondary));
|
|
std::memcpy(ud.accent, &m_accent, sizeof(ud.accent));
|
|
u->updateDynamicBuffer(m_ubuf.get(), 0, quint32(sizeof(ud)), &ud);
|
|
|
|
// A fresh pair starts BLACK (the reference's history starts
|
|
// zero-filled too — an unsampled-cleared first frame would advect
|
|
// garbage).
|
|
if (m_needClear) {
|
|
m_needClear = false;
|
|
for (int i = 0; i < 2; ++i) {
|
|
cb->beginPass(m_histRt[i].get(), QColor(0, 0, 0, 255),
|
|
QRhiDepthStencilClearValue(1.0f, 0), nullptr);
|
|
cb->endPass();
|
|
}
|
|
}
|
|
|
|
const int read = m_read;
|
|
const int write = 1 - m_read;
|
|
|
|
// Pass 1: plasma into history[write], sampling history[read].
|
|
cb->beginPass(m_histRt[write].get(), QColor(0, 0, 0, 255),
|
|
QRhiDepthStencilClearValue(1.0f, 0), u);
|
|
cb->setGraphicsPipeline(m_plasmaPipe.get());
|
|
cb->setViewport(QRhiViewport(0, 0, float(hist.width()), float(hist.height())));
|
|
cb->setShaderResources(m_srbPlasma[read].get());
|
|
const QRhiCommandBuffer::VertexInput tri(m_vbuf.get(), 0);
|
|
cb->setVertexInput(0, 1, &tri);
|
|
cb->draw(3);
|
|
cb->endPass();
|
|
|
|
// Pass 2: blit history[write] into the item target (the composite).
|
|
cb->beginPass(rt, QColor(0, 0, 0, 255), QRhiDepthStencilClearValue(1.0f, 0), nullptr);
|
|
cb->setGraphicsPipeline(m_blitPipe.get());
|
|
cb->setViewport(QRhiViewport(0, 0, float(ps.width()), float(ps.height())));
|
|
cb->setShaderResources(m_srbBlit[write].get());
|
|
cb->setVertexInput(0, 1, &tri);
|
|
cb->draw(3);
|
|
cb->endPass();
|
|
|
|
m_read = write;
|
|
}
|
|
|
|
private:
|
|
static QSize capped(const QSize &ps)
|
|
{
|
|
if (ps.width() <= MAX_W && ps.height() <= MAX_H)
|
|
return ps;
|
|
const double s = qMin(double(MAX_W) / ps.width(), double(MAX_H) / ps.height());
|
|
return QSize(qMax(1, int(ps.width() * s)), qMax(1, int(ps.height() * s)));
|
|
}
|
|
|
|
// (Re)build the ping-pong pair + its render targets + both SRB sets.
|
|
// Old resources go through deleteLater() — an in-flight frame may still
|
|
// reference them (QRhiResource::deleteLater is the documented deferral).
|
|
void rebuildHistory(const QSize &size)
|
|
{
|
|
QRhi *r = rhi();
|
|
for (int i = 0; i < 2; ++i) {
|
|
if (m_hist[i])
|
|
m_hist[i].release()->deleteLater();
|
|
if (m_histRt[i])
|
|
m_histRt[i].release()->deleteLater();
|
|
if (m_srbPlasma[i])
|
|
m_srbPlasma[i].release()->deleteLater();
|
|
if (m_srbBlit[i])
|
|
m_srbBlit[i].release()->deleteLater();
|
|
}
|
|
if (m_plasmaPipe)
|
|
m_plasmaPipe.release()->deleteLater();
|
|
m_plasmaPassDescriptor = nullptr;
|
|
m_histSize = size;
|
|
m_needClear = true;
|
|
m_read = 0;
|
|
|
|
for (int i = 0; i < 2; ++i) {
|
|
// These textures are both sampled and rendered into. Vulkan's
|
|
// driver path happened to accept the missing usage bit, while the
|
|
// D3D backend correctly refused the texture render target and left
|
|
// Plasma black on Windows.
|
|
m_hist[i].reset(r->newTexture(QRhiTexture::RGBA8,
|
|
size,
|
|
1,
|
|
QRhiTexture::RenderTarget));
|
|
if (!m_hist[i]->create()) {
|
|
qWarning("[plasma] history texture creation failed — the scene stays dark");
|
|
m_hist[0].reset();
|
|
m_hist[1].reset();
|
|
return;
|
|
}
|
|
// Vulkan/D3D/Metal REQUIRE a render pass descriptor before
|
|
// create(); without it the RT creation fails and the later
|
|
// beginPass corrupts the whole window (2026-08-15 owner smoke:
|
|
// Plasma left a transparent, unusable window). The RT does NOT
|
|
// own the descriptor — free the previous one on rebuild.
|
|
if (m_histRpDesc[i]) {
|
|
delete m_histRpDesc[i];
|
|
m_histRpDesc[i] = nullptr;
|
|
}
|
|
m_histRt[i].reset(r->newTextureRenderTarget({ m_hist[i].get() }));
|
|
m_histRpDesc[i] = m_histRt[i]->newCompatibleRenderPassDescriptor();
|
|
m_histRt[i]->setRenderPassDescriptor(m_histRpDesc[i]);
|
|
if (!m_histRt[i]->create()) {
|
|
qWarning("[plasma] history render target failed — the scene stays dark");
|
|
m_hist[0].reset();
|
|
m_hist[1].reset();
|
|
return;
|
|
}
|
|
}
|
|
// SRB sets: plasma binds ubuf(0) + the READ texture(1); the blit
|
|
// binds only the WRITE texture(1) (its shader declares nothing
|
|
// else). Both bilinear (the reference's prev_samp is bilinear).
|
|
for (int i = 0; i < 2; ++i) {
|
|
m_srbPlasma[i].reset(r->newShaderResourceBindings());
|
|
m_srbPlasma[i]->setBindings({
|
|
QRhiShaderResourceBinding::uniformBuffer(
|
|
0,
|
|
QRhiShaderResourceBinding::VertexStage
|
|
| QRhiShaderResourceBinding::FragmentStage,
|
|
m_ubuf.get()),
|
|
QRhiShaderResourceBinding::sampledTexture(
|
|
1, QRhiShaderResourceBinding::FragmentStage,
|
|
m_hist[i].get(), m_sampler.get()),
|
|
});
|
|
m_srbBlit[i].reset(r->newShaderResourceBindings());
|
|
m_srbBlit[i]->setBindings({
|
|
QRhiShaderResourceBinding::sampledTexture(
|
|
1, QRhiShaderResourceBinding::FragmentStage,
|
|
m_hist[i].get(), m_sampler.get()),
|
|
});
|
|
if (!m_srbPlasma[i]->create() || !m_srbBlit[i]->create()) {
|
|
qWarning("[plasma] shader resource bindings failed — the scene stays dark");
|
|
m_hist[0].reset();
|
|
m_hist[1].reset();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void setVertexLayout(QRhiGraphicsPipeline *pipe)
|
|
{
|
|
QRhiVertexInputLayout inputLayout;
|
|
inputLayout.setBindings({ QRhiVertexInputBinding(quint32(2 * sizeof(float))) });
|
|
inputLayout.setAttributes({
|
|
{ 0, 0, QRhiVertexInputAttribute::Float2, 0 }, // pos
|
|
});
|
|
pipe->setVertexInputLayout(inputLayout);
|
|
}
|
|
|
|
void buildPlasmaPipeline()
|
|
{
|
|
if (!m_plasmaPipe)
|
|
m_plasmaPipe.reset(rhi()->newGraphicsPipeline());
|
|
m_plasmaPipe->setTopology(QRhiGraphicsPipeline::Triangles);
|
|
m_plasmaPipe->setCullMode(QRhiGraphicsPipeline::None);
|
|
m_plasmaPipe->setDepthTest(false);
|
|
m_plasmaPipe->setDepthWrite(false);
|
|
// OPAQUE output (the FS writes alpha 1; the scene owns the
|
|
// background while active — the linebed header documents the same
|
|
// choice for its explicit blend).
|
|
m_plasmaPipe->setShaderStages({
|
|
{ QRhiShaderStage::Vertex,
|
|
loadShader(QStringLiteral(
|
|
":/qt/qml/com/blitzfc/qbz/qml/assets/shaders/rhi_triangle.vert.qsb")) },
|
|
{ QRhiShaderStage::Fragment,
|
|
loadShader(QStringLiteral(
|
|
":/qt/qml/com/blitzfc/qbz/qml/assets/shaders/plasma.frag.qsb")) },
|
|
});
|
|
setVertexLayout(m_plasmaPipe.get());
|
|
m_plasmaPipe->setShaderResourceBindings(m_srbPlasma[0].get());
|
|
m_plasmaPipe->setRenderPassDescriptor(m_plasmaPassDescriptor);
|
|
if (!m_plasmaPipe->create()) {
|
|
qWarning("[plasma] pipeline creation failed — the scene stays dark");
|
|
m_plasmaPipe.reset();
|
|
}
|
|
}
|
|
|
|
void buildBlitPipeline()
|
|
{
|
|
if (!m_blitPipe)
|
|
m_blitPipe.reset(rhi()->newGraphicsPipeline());
|
|
m_blitPipe->setTopology(QRhiGraphicsPipeline::Triangles);
|
|
m_blitPipe->setCullMode(QRhiGraphicsPipeline::None);
|
|
m_blitPipe->setDepthTest(false);
|
|
m_blitPipe->setDepthWrite(false);
|
|
m_blitPipe->setShaderStages({
|
|
{ QRhiShaderStage::Vertex,
|
|
loadShader(QStringLiteral(
|
|
":/qt/qml/com/blitzfc/qbz/qml/assets/shaders/rhi_triangle.vert.qsb")) },
|
|
{ QRhiShaderStage::Fragment,
|
|
loadShader(QStringLiteral(
|
|
":/qt/qml/com/blitzfc/qbz/qml/assets/shaders/plasma_blit.frag.qsb")) },
|
|
});
|
|
setVertexLayout(m_blitPipe.get());
|
|
m_blitPipe->setShaderResourceBindings(m_srbBlit[0].get());
|
|
m_blitPipe->setRenderPassDescriptor(m_blitPassDescriptor);
|
|
if (!m_blitPipe->create()) {
|
|
qWarning("[plasma] blit pipeline creation failed — the scene stays dark");
|
|
m_blitPipe.reset();
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<QRhiBuffer> m_vbuf;
|
|
std::unique_ptr<QRhiBuffer> m_ubuf;
|
|
std::unique_ptr<QRhiSampler> m_sampler;
|
|
std::unique_ptr<QRhiTexture> m_hist[2];
|
|
std::unique_ptr<QRhiTextureRenderTarget> m_histRt[2];
|
|
// Render pass descriptors owned BY US (the RT borrows them) — see the
|
|
// rebuild loop. Raw pointers, freed on rebuild.
|
|
QRhiRenderPassDescriptor *m_histRpDesc[2] = { nullptr, nullptr };
|
|
std::unique_ptr<QRhiShaderResourceBindings> m_srbPlasma[2];
|
|
std::unique_ptr<QRhiShaderResourceBindings> m_srbBlit[2];
|
|
std::unique_ptr<QRhiGraphicsPipeline> m_plasmaPipe;
|
|
std::unique_ptr<QRhiGraphicsPipeline> m_blitPipe;
|
|
QRhiRenderPassDescriptor *m_plasmaPassDescriptor = nullptr;
|
|
QRhiRenderPassDescriptor *m_blitPassDescriptor = nullptr;
|
|
QSize m_histSize;
|
|
int m_read = 0;
|
|
bool m_needClear = true;
|
|
|
|
// synchronize() snapshots.
|
|
float m_time = 0.0f;
|
|
float m_beat = 0.0f;
|
|
float m_level = 0.0f;
|
|
float m_levelSmooth = 0.0f;
|
|
QVector4D m_energyLo{ 0.0f, 0.0f, 0.0f, 0.0f };
|
|
QVector4D m_energyHi{ 0.0f, 0.0f, 0.0f, 0.0f };
|
|
float m_transientAmp = 0.0f;
|
|
QVector4D m_bandsLo{ 0.0f, 0.0f, 0.0f, 0.0f };
|
|
QVector4D m_bandsHi{ 0.0f, 0.0f, 0.0f, 0.0f };
|
|
QVector4D m_primary{ 0.0f, 0.863f, 0.784f, 1.0f };
|
|
QVector4D m_secondary{ 0.588f, 0.196f, 1.0f, 1.0f };
|
|
QVector4D m_accent{ 0.247f, 0.851f, 0.784f, 1.0f };
|
|
};
|
|
|
|
} // namespace
|
|
|
|
QQuickRhiItemRenderer *PlasmaItem::createRenderer()
|
|
{
|
|
return new PlasmaRenderer;
|
|
}
|
|
|
|
// QML type registration — the linebed_item.cpp idiom: the module is STATIC,
|
|
// so a startup function runs at QGuiApplication construction, BEFORE the
|
|
// engine loads the QML that names PlasmaItem.
|
|
static bool g_plasmaRegistered = false;
|
|
|
|
extern "C" void qbz_plasma_register_qml_type()
|
|
{
|
|
if (g_plasmaRegistered)
|
|
return;
|
|
g_plasmaRegistered = true;
|
|
qmlRegisterType<PlasmaItem>("com.blitzfc.qbz", 1, 0, "PlasmaItem");
|
|
}
|
|
|
|
Q_COREAPP_STARTUP_FUNCTION(qbz_plasma_register_qml_type)
|