TWO FINDINGS, both of which falsified something that was being assumed.
FLUIDITY IS NOT FRAME RATE. Driven at QBZ_PULSE_MS=10 the shell presented ~100
frames/s — qt-batches confirmed it, and the script now prints the pulse it ran
with so two logs cannot be confused — and the bars looked exactly as they had at
30. They would: every published frame was applied as a STEP, so the extra
presents repainted the same discrete values three times over. What the Slint
build does differently is not rate, it is `animate bar-h { 90ms }` — it moves
BETWEEN values.
So the interpolation is back, ON THE PULSE, and it costs nothing. The old
interpolator was expensive because of its private 16 ms Timer, not because it
interpolated: it presented at its own rate, unsynchronised with everything else.
Easing on the pulse presents exactly as often as stepping on the pulse did — one
repaint per period either way, only the value differs. Self-parking survives: a
converged ease writes nothing and schedules no repaint. The price is lag, and it
is the honest one — k = 0.45 is ~83% there in three pulses (~100 ms), matching
the reference's 90 ms, against 46 ms of FFT window and 77 ms of Rust smoothing
already in the chain.
The from/to/progress triple is gone with it, and its consumers moved to `cur`.
Note for the next reader: `SpectrumBand` drove all three of its modes from
`Connections` on those properties, and a Connections handler naming a property
that no longer exists is NOT a compile error — QML warns at runtime and simply
never fires. That is how the whole band went dead at once; grep for
`on<Name>Changed` when removing a property, not only for direct reads.
PLASMA WAS DRIVEN BY THE SLOWEST SIGNALS IN THE PIPELINE while the fast ones
travelled beside it unconnected. `transientAmp`, `bandsLo` and `bandsHi` were
published, parsed and stored, and never handed to the item — the shader declared
them and received zeros — so everything ran off the energy bands, which decay
x0.85 per frame (~473 ms) against the bars' x0.65 (~178 ms).
Worse, `beat` stops being a beat on dense material: it decays x0.88 per tick, so
above ~3 hits/s it never falls far enough to come back as a spike and settles
into a shallow ripple around 0.7. A splat multiplied by it becomes a steady
glow, and the busier the music the flatter the picture — the most frenetic input
produced the least motion. `beatAc` subtracts a slow density floor, so a hit
reads as a hit at any tempo.
The scene itself was rebuilt around the reference the owner filmed (Butterchurn
on a percussion-heavy track). Its recipe is three parts: one thin annulus
injected per frame that the feedback warp marches inward, so the concentric
rings ARE the last N frames of audio laid out in space; retention high enough
for that to stack (this held 0.85, where a trail died in ~15 frames and nothing
ever accumulated); and angular structure from reading the spectrum BY ANGLE.
But a CLOSED ring was wrong, and the owner named why: a perfect inner
circumference reads as a drawn shape, hard-edges the field, and at this
retention successive rings pack into one solid disc — which is what killed the
ambient feel. The annulus is gated by angle now, so a hit paints arcs the
conveyor carries inward, the circle never closes, and the gaps stay open for the
background. Noise bends, frays and thins those arcs so they are made of plasma
rather than of compass.
The worms are back at full strength with tighter cores and a contrast curve — a
raw gaussian is all shoulder and reads as haze — and their orbits now reach past
the arc band instead of being trapped inside it. Colour comes from ONE
album-derived triad, so everything used to be a shade of the same hue; a
Rodrigues hue turn splits it into worms, arcs and an ambient wash that sit apart
whatever album is playing. That wash is what stops the background being void for
the foreground to be a cut-out against. A soft limiter replaces the hard clamp:
at 0.945 retention a clamp pins the middle to white and the structure vanishes.
Release build clean, offscreen smoke green, 421/421.
cc
427 lines
17 KiB
C++
427 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) {
|
|
m_hist[i].reset(r->newTexture(QRhiTexture::RGBA8, size));
|
|
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)
|