Files
qbz/crates/qbz-qt/cxx/linebed_item.cpp
T
vicrodh e6c058c2f2 feat(qt): immersive shader scenes — RHI items, scene menu, GL link pairing
Port the immersive GPU scenes: Plasma, Tunnel, Aurora, Ambient, Spectral
Ribbon and Line Bed as QQuickRhiItems/ShaderEffects under a pulse-driven
ShaderSceneLayer, plus the Qt-only Tunnel Flow (Tauri Canvas2D port) fed
by the Viz16 stream. Scene rows live in the header view menu (no Off row);
g cycles the ring. A fresh immersive open defaults to Split > Lyrics
(owner ruling).

OpenGL link fix (owner smoke: Tunnel black with "uniform _22 declared as
type buf and type buf"): every ShaderEffect fragment with its own uniform
layout now ships a paired vertex stage declaring the identical block —
the shared spectrum.vert only ever linked under Vulkan.

RHI hardening: render pass descriptors on the ping-pong targets (the
Tunnel Flow segfault / Plasma dead window), mirrorVertically tied to the
backend (!== OpenGL), ribbon sub-uploads carry setSourceSize.

km
2026-08-16 08:22:57 -06:00

287 lines
12 KiB
C++

// Line Bed (immersive shader scene, mode 5) — the QRhi renderer behind
// LineBedItem. Ports the Slint line-strip pipeline
// (crates/qbz/src/shader_underlay.rs:689-727) and its draw call
// (:995-998): 200 INSTANCED line strips of (255*6 + 1) = 1531 vertices
// each, alpha blending, the heights ring as an R32F texture sampled in the
// VERTEX stage. The vertex/instance ids the WGSL took from
// @builtin(vertex_index)/@builtin(instance_index) arrive here as two float
// vertex attributes (1531-vert template buffer + a 200-entry per-instance
// buffer) — the maximally portable spelling across GL/GLES/Vulkan/Metal/
// D3D under QRhi.
//
// Resource cadence (the pulse law): `synchronize` snapshots the QML-side
// properties when the item updates (pulse tick only), `render` uploads the
// heights texture only when a NEW ring arrived, and the draw itself is
// ~306K line vertices at 30 Hz — trivial next to a fullscreen fragment
// scene.
#include "linebed_item.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QFile>
#include <QtGui/QVector4D>
#include <QtQml/qqml.h>
#include <rhi/qrhi.h>
#include <cstring>
#include <vector>
namespace {
// The lattice — pinned by FOUR files that must agree: here, linebed.vert
// (NUM_BANDS/SUBDIV), src/linebed_qt.rs (LINEBED_LINES/LINEBED_BANDS) and
// the Slint reference (shader_underlay.rs:47-54).
constexpr int LINEBED_LINES = 200;
constexpr int LINEBED_BANDS = 256;
constexpr int LINEBED_SUBDIV = 6;
constexpr int VERTS_PER_LINE = (LINEBED_BANDS - 1) * LINEBED_SUBDIV + 1; // 1531
constexpr qsizetype HEIGHTS_BYTES = LINEBED_LINES * LINEBED_BANDS * qsizetype(sizeof(float));
// std140: vec2 + vec4 + vec4. The GLSL block (linebed.vert/.frag, binding 0):
// layout(std140, binding = 0) uniform buf {
// vec2 resolution; // 0
// vec4 primary; // 16
// vec4 accent; // 32
// };
// The WGSL reference reads the full 144-byte scene block but uses only
// resolution (VS) and primary/accent (FS) — line_bed.wgsl is the one scene
// with no time/level/energy input, so the block is trimmed to what the
// shader reads. Same field VALUES at the same meaning.
struct UniformData
{
float res[2];
float pad[2];
float primary[4];
float accent[4];
};
QShader loadShader(const QString &qrcPath)
{
QFile f(qrcPath);
if (f.open(QIODevice::ReadOnly)) {
const QShader s = QShader::fromSerialized(f.readAll());
if (!s.isValid())
qWarning("[linebed] shader %s failed to deserialize", qPrintable(qrcPath));
return s;
}
qWarning("[linebed] shader %s not found in the qrc", qPrintable(qrcPath));
return QShader();
}
class LineBedRenderer : public QQuickRhiItemRenderer
{
public:
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 below drops everything first)
m_vbuf.reset(r->newBuffer(QRhiBuffer::Immutable,
QRhiBuffer::VertexBuffer,
VERTS_PER_LINE * quint32(sizeof(float))));
m_ibuf.reset(r->newBuffer(QRhiBuffer::Immutable,
QRhiBuffer::VertexBuffer,
LINEBED_LINES * quint32(sizeof(float))));
m_ubuf.reset(r->newBuffer(QRhiBuffer::Dynamic,
QRhiBuffer::UniformBuffer,
quint32(sizeof(UniformData))));
m_tex.reset(r->newTexture(QRhiTexture::R32F, QSize(LINEBED_BANDS, LINEBED_LINES)));
m_sampler.reset(r->newSampler(QRhiSampler::Nearest,
QRhiSampler::Nearest,
QRhiSampler::None,
QRhiSampler::ClampToEdge,
QRhiSampler::ClampToEdge));
if (!m_vbuf->create() || !m_ibuf->create() || !m_ubuf->create()
|| !m_tex->create() || !m_sampler->create()) {
qWarning("[linebed] resource creation failed — the scene stays dark");
m_vbuf.reset();
return;
}
m_srb.reset(r->newShaderResourceBindings());
m_srb->setBindings({
QRhiShaderResourceBinding::uniformBuffer(
0,
QRhiShaderResourceBinding::VertexStage
| QRhiShaderResourceBinding::FragmentStage,
m_ubuf.get()),
// Binding 4, like the reference's @binding(4) heights_tex.
QRhiShaderResourceBinding::sampledTexture(
4, QRhiShaderResourceBinding::VertexStage, m_tex.get(), m_sampler.get()),
});
if (!m_srb->create()) {
qWarning("[linebed] shader resource bindings failed — the scene stays dark");
m_vbuf.reset();
return;
}
QRhiResourceUpdateBatch *u = r->nextResourceUpdateBatch();
// The vertex templates: vid = subdivided point index along the line
// (0..1530), line = the depth row per instance (0..199).
std::vector<float> vid(VERTS_PER_LINE);
for (int i = 0; i < VERTS_PER_LINE; ++i)
vid[i] = float(i);
u->uploadStaticBuffer(m_vbuf.get(), 0,
quint32(vid.size() * sizeof(float)), vid.data());
std::vector<float> lines(LINEBED_LINES);
for (int i = 0; i < LINEBED_LINES; ++i)
lines[i] = float(i);
u->uploadStaticBuffer(m_ibuf.get(), 0,
quint32(lines.size() * sizeof(float)), lines.data());
// Flat bed until the first ring publish (the Rust ring starts
// zeroed too — linebed_qt.rs LineBedState::default).
const QByteArray zeros(int(HEIGHTS_BYTES), 0);
QRhiTextureSubresourceUploadDescription sub(zeros);
sub.setDataStride(LINEBED_BANDS * quint32(sizeof(float)));
u->uploadTexture(m_tex.get(),
QRhiTextureUploadDescription(QRhiTextureUploadEntry(0, 0, sub)));
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 *lb = static_cast<LineBedItem *>(item);
const QByteArray h = lb->heights();
if (h.size() == HEIGHTS_BYTES && h != m_heights) {
m_heights = h;
m_heightsDirty = true;
}
const QColor p = lb->primary();
const QColor a = lb->accent();
m_primary = QVector4D(float(p.redF()), float(p.greenF()), float(p.blueF()), 1.0f);
m_accent = QVector4D(float(a.redF()), float(a.greenF()), float(a.blueF()), 1.0f);
}
void render(QRhiCommandBuffer *cb) override
{
if (!m_vbuf)
return; // resource creation failed in initialize
QRhiRenderTarget *rt = renderTarget();
if (!m_pipeline || rt->renderPassDescriptor() != m_passDescriptor) {
m_passDescriptor = rt->renderPassDescriptor();
buildPipeline();
if (!m_pipeline)
return;
}
QRhiResourceUpdateBatch *u = rhi()->nextResourceUpdateBatch();
if (m_heightsDirty) {
m_heightsDirty = false;
QRhiTextureSubresourceUploadDescription sub(m_heights);
sub.setDataStride(LINEBED_BANDS * quint32(sizeof(float)));
u->uploadTexture(m_tex.get(),
QRhiTextureUploadDescription(QRhiTextureUploadEntry(0, 0, sub)));
}
const QSize ps = rt->pixelSize();
UniformData ud = {};
ud.res[0] = float(ps.width());
ud.res[1] = float(ps.height());
std::memcpy(ud.primary, &m_primary, sizeof(ud.primary));
std::memcpy(ud.accent, &m_accent, sizeof(ud.accent));
u->updateDynamicBuffer(m_ubuf.get(), 0, quint32(sizeof(ud)), &ud);
// Opaque black clear, like the reference's LoadOp::Clear(BLACK):
// the scene OWNS the background while active (the atmosphere and
// panels gate themselves off), and the item composites opaquely
// (alphaBlending stays false). The FS alpha rides the explicit
// SrcAlpha blend of the pipeline, exactly wgpu ALPHA_BLENDING.
cb->beginPass(rt, QColor(0, 0, 0, 255), QRhiDepthStencilClearValue(1.0f, 0), u);
cb->setGraphicsPipeline(m_pipeline.get());
cb->setViewport(QRhiViewport(0, 0, float(ps.width()), float(ps.height())));
const QRhiCommandBuffer::VertexInput vbufs[] = {
QRhiCommandBuffer::VertexInput(m_vbuf.get(), 0),
QRhiCommandBuffer::VertexInput(m_ibuf.get(), 0),
};
cb->setShaderResources(m_srb.get());
cb->setVertexInput(0, 2, vbufs);
cb->draw(VERTS_PER_LINE, LINEBED_LINES); // 200 instanced strips of 1531
cb->endPass();
}
private:
void buildPipeline()
{
if (!m_pipeline)
m_pipeline.reset(rhi()->newGraphicsPipeline());
m_pipeline->setTopology(QRhiGraphicsPipeline::LineStrip);
m_pipeline->setCullMode(QRhiGraphicsPipeline::None);
m_pipeline->setDepthTest(false);
m_pipeline->setDepthWrite(false);
// wgpu BlendState::ALPHA_BLENDING verbatim.
QRhiGraphicsPipeline::TargetBlend blend;
blend.enable = true;
blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
blend.srcAlpha = QRhiGraphicsPipeline::One;
blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
m_pipeline->setTargetBlends({ blend });
m_pipeline->setShaderStages({
{ QRhiShaderStage::Vertex,
loadShader(QStringLiteral(
":/qt/qml/com/blitzfc/qbz/qml/assets/shaders/linebed.vert.qsb")) },
{ QRhiShaderStage::Fragment,
loadShader(QStringLiteral(
":/qt/qml/com/blitzfc/qbz/qml/assets/shaders/linebed.frag.qsb")) },
});
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({
QRhiVertexInputBinding(quint32(sizeof(float))),
QRhiVertexInputBinding(quint32(sizeof(float)),
QRhiVertexInputBinding::PerInstance),
});
inputLayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float, 0 }, // vid
{ 1, 1, QRhiVertexInputAttribute::Float, 0 }, // line (per instance)
});
m_pipeline->setVertexInputLayout(inputLayout);
m_pipeline->setShaderResourceBindings(m_srb.get());
m_pipeline->setRenderPassDescriptor(m_passDescriptor);
if (!m_pipeline->create()) {
qWarning("[linebed] pipeline creation failed — the scene stays dark");
m_pipeline.reset();
}
}
std::unique_ptr<QRhiBuffer> m_vbuf;
std::unique_ptr<QRhiBuffer> m_ibuf;
std::unique_ptr<QRhiBuffer> m_ubuf;
std::unique_ptr<QRhiTexture> m_tex;
std::unique_ptr<QRhiSampler> m_sampler;
std::unique_ptr<QRhiShaderResourceBindings> m_srb;
std::unique_ptr<QRhiGraphicsPipeline> m_pipeline;
QRhiRenderPassDescriptor *m_passDescriptor = nullptr;
QByteArray m_heights;
bool m_heightsDirty = false;
// The Slint default palette (see the header) until synchronize() runs.
QVector4D m_primary{ 0.0f, 0.863f, 0.784f, 1.0f };
QVector4D m_accent{ 0.247f, 0.851f, 0.784f, 1.0f };
};
} // namespace
QQuickRhiItemRenderer *LineBedItem::createRenderer()
{
return new LineBedRenderer;
}
// QML type registration. The QML module is STATIC (linked into the
// binary), so there is no plugin load to hang this on: the startup function
// runs at QGuiApplication construction, BEFORE the engine loads the QML
// that names LineBedItem (ShaderSceneLayer.qml is compiled during the
// initial load; Component.onCompleted — where the singleton boots run — is
// already too late).
static bool g_linebedRegistered = false;
extern "C" void qbz_linebed_register_qml_type()
{
if (g_linebedRegistered)
return;
g_linebedRegistered = true;
qmlRegisterType<LineBedItem>("com.blitzfc.qbz", 1, 0, "LineBedItem");
}
Q_COREAPP_STARTUP_FUNCTION(qbz_linebed_register_qml_type)