feat(visualizer): add stereo scopes
Add a theme-aware goniometer and pitch-locked oscilloscope to the large dock and immersive focus modes. Gate scope DSP and scenegraph updates by actual panel visibility so hidden modes remain idle, including while shader scenes cover immersive panels. cx
This commit is contained in:
26 files changed
+1009
-53
No files matched your search
@@ -302,6 +302,7 @@ fn build_rhi_items() {
|
||||
"plasma_item",
|
||||
"ribbon_item",
|
||||
"tunnelflow_item",
|
||||
"scope_trace_item",
|
||||
"local_tracks_model",
|
||||
"local_albums_model",
|
||||
"local_artists_model",
|
||||
@@ -445,6 +446,7 @@ fn main() {
|
||||
// Per-page scroll memory for the back/forward stack; mounted
|
||||
// beside each view's scroll container.
|
||||
"qml/controls/ScrollMemory.qml",
|
||||
"qml/controls/ScopePanel.qml",
|
||||
"qml/controls/QbzSegToggle.qml",
|
||||
"qml/controls/QbzSelect.qml",
|
||||
"qml/controls/QbzSlider.qml",
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
#include "scope_trace_item.h"
|
||||
|
||||
#include <QtCore/QCoreApplication>
|
||||
#include <QtQml/qqml.h>
|
||||
#include <QtQuick/QSGFlatColorMaterial>
|
||||
#include <QtQuick/QSGGeometryNode>
|
||||
|
||||
ScopeTraceItem::ScopeTraceItem(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
{
|
||||
setFlag(ItemHasContents, true);
|
||||
setAntialiasing(true);
|
||||
}
|
||||
|
||||
void ScopeTraceItem::setPoints(const QVariantList &points)
|
||||
{
|
||||
m_points = points;
|
||||
update();
|
||||
}
|
||||
|
||||
void ScopeTraceItem::setTraceColor(const QColor &color)
|
||||
{
|
||||
if (m_traceColor == color)
|
||||
return;
|
||||
m_traceColor = color;
|
||||
update();
|
||||
}
|
||||
|
||||
void ScopeTraceItem::setMode(int mode)
|
||||
{
|
||||
mode = mode == 1 ? 1 : 0;
|
||||
if (m_mode == mode)
|
||||
return;
|
||||
m_mode = mode;
|
||||
update();
|
||||
}
|
||||
|
||||
void ScopeTraceItem::setLineWidth(qreal width)
|
||||
{
|
||||
width = qMax<qreal>(1.0, width);
|
||||
if (qFuzzyCompare(m_lineWidth, width))
|
||||
return;
|
||||
m_lineWidth = width;
|
||||
update();
|
||||
}
|
||||
|
||||
QSGNode *ScopeTraceItem::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
|
||||
{
|
||||
const int count = m_mode == 0 ? m_points.size() / 2 : m_points.size();
|
||||
if (count < 2 || width() <= 0.0 || height() <= 0.0) {
|
||||
delete oldNode;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto *node = static_cast<QSGGeometryNode *>(oldNode);
|
||||
if (!node) {
|
||||
node = new QSGGeometryNode;
|
||||
auto *geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), count);
|
||||
geometry->setDrawingMode(QSGGeometry::DrawLineStrip);
|
||||
geometry->setLineWidth(static_cast<float>(m_lineWidth));
|
||||
node->setGeometry(geometry);
|
||||
node->setFlag(QSGNode::OwnsGeometry);
|
||||
auto *material = new QSGFlatColorMaterial;
|
||||
material->setColor(m_traceColor);
|
||||
node->setMaterial(material);
|
||||
node->setFlag(QSGNode::OwnsMaterial);
|
||||
}
|
||||
|
||||
auto *geometry = node->geometry();
|
||||
if (geometry->vertexCount() != count)
|
||||
geometry->allocate(count);
|
||||
geometry->setDrawingMode(QSGGeometry::DrawLineStrip);
|
||||
geometry->setLineWidth(static_cast<float>(m_lineWidth));
|
||||
auto *vertices = geometry->vertexDataAsPoint2D();
|
||||
const float pad = 4.0f;
|
||||
const float drawWidth = qMax(1.0f, static_cast<float>(width()) - pad * 2.0f);
|
||||
const float drawHeight = qMax(1.0f, static_cast<float>(height()) - pad * 2.0f);
|
||||
|
||||
for (int index = 0; index < count; ++index) {
|
||||
float x;
|
||||
float y;
|
||||
if (m_mode == 0) {
|
||||
const float side = qBound(-1.0f, m_points[index * 2].toFloat(), 1.0f);
|
||||
const float mid = qBound(-1.0f, m_points[index * 2 + 1].toFloat(), 1.0f);
|
||||
x = pad + (0.5f + side * 0.46f) * drawWidth;
|
||||
y = pad + (0.5f - mid * 0.46f) * drawHeight;
|
||||
} else {
|
||||
const float value = qBound(-1.0f, m_points[index].toFloat(), 1.0f);
|
||||
x = pad + index * drawWidth / qMax(1, count - 1);
|
||||
y = pad + (0.5f - value * 0.46f) * drawHeight;
|
||||
}
|
||||
vertices[index].set(x, y);
|
||||
}
|
||||
|
||||
static_cast<QSGFlatColorMaterial *>(node->material())->setColor(m_traceColor);
|
||||
node->markDirty(QSGNode::DirtyGeometry | QSGNode::DirtyMaterial);
|
||||
return node;
|
||||
}
|
||||
|
||||
static bool g_scopeTraceRegistered = false;
|
||||
|
||||
extern "C" void qbz_scope_trace_register_qml_type()
|
||||
{
|
||||
if (g_scopeTraceRegistered)
|
||||
return;
|
||||
g_scopeTraceRegistered = true;
|
||||
qmlRegisterType<ScopeTraceItem>("com.blitzfc.qbz", 1, 0, "ScopeTraceItem");
|
||||
}
|
||||
|
||||
Q_COREAPP_STARTUP_FUNCTION(qbz_scope_trace_register_qml_type)
|
||||
@@ -0,0 +1,40 @@
|
||||
#ifndef QBZ_SCOPE_TRACE_ITEM_H
|
||||
#define QBZ_SCOPE_TRACE_ITEM_H
|
||||
|
||||
#include <QtCore/QVariantList>
|
||||
#include <QtGui/QColor>
|
||||
#include <QtQuick/QQuickItem>
|
||||
|
||||
class ScopeTraceItem : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QVariantList points READ points WRITE setPoints)
|
||||
Q_PROPERTY(QColor traceColor READ traceColor WRITE setTraceColor)
|
||||
Q_PROPERTY(int mode READ mode WRITE setMode)
|
||||
Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth)
|
||||
|
||||
public:
|
||||
explicit ScopeTraceItem(QQuickItem *parent = nullptr);
|
||||
|
||||
QVariantList points() const { return m_points; }
|
||||
void setPoints(const QVariantList &points);
|
||||
QColor traceColor() const { return m_traceColor; }
|
||||
void setTraceColor(const QColor &color);
|
||||
int mode() const { return m_mode; }
|
||||
void setMode(int mode);
|
||||
qreal lineWidth() const { return m_lineWidth; }
|
||||
void setLineWidth(qreal width);
|
||||
|
||||
protected:
|
||||
QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override;
|
||||
|
||||
private:
|
||||
QVariantList m_points;
|
||||
QColor m_traceColor{ 63, 217, 200 };
|
||||
int m_mode = 0;
|
||||
qreal m_lineWidth = 1.5;
|
||||
};
|
||||
|
||||
extern "C" void qbz_scope_trace_register_qml_type();
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,66 @@
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
import "../theme"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
// 0 = goniometer, 1 = pitch-locked oscilloscope.
|
||||
property int scopeMode: 0
|
||||
property color traceColor: theme.accent
|
||||
property bool compact: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
radius: theme.radiusMd
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: 8
|
||||
width: 1
|
||||
height: parent.height - 16
|
||||
color: theme.borderSubtle
|
||||
}
|
||||
Rectangle {
|
||||
x: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 16
|
||||
height: 1
|
||||
color: theme.borderSubtle
|
||||
}
|
||||
Rectangle {
|
||||
visible: root.scopeMode === 0
|
||||
anchors.centerIn: parent
|
||||
width: parent.width * 0.5
|
||||
height: parent.height * 0.5
|
||||
color: "transparent"
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
radius: width / 2
|
||||
}
|
||||
|
||||
ScopeTraceItem {
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.compact ? 3 : 8
|
||||
mode: root.scopeMode
|
||||
points: root.scopeMode === 0 ? QbzViz.goniometer : QbzViz.oscilloscope
|
||||
traceColor: root.traceColor
|
||||
lineWidth: root.compact ? 1.0 : 1.5
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.scopeMode === 0 && !root.compact
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 12
|
||||
text: QbzViz.stereoCorrelation.toFixed(2)
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
@@ -207,8 +207,8 @@ Item {
|
||||
}
|
||||
Repeater {
|
||||
// FOCUS rows IN SLINT ORDER (:586-650): Album Reactive(0),
|
||||
// Static(1), Coverflow(2), Spectrum(3), Wave Bed(6), Lyrics(4),
|
||||
// Queue(5).
|
||||
// Static(1), Coverflow(2), Spectrum(3), Wave Bed(6),
|
||||
// Goniometer(7), Oscilloscope(8), Lyrics(4), Queue(5).
|
||||
model: [
|
||||
{ "vm": 0, "m": 0, "sp": -1, "entry": "",
|
||||
"label": QbzSession.tr("Album Reactive", QbzSession.trRev) },
|
||||
@@ -220,6 +220,10 @@ Item {
|
||||
"label": QbzSession.tr("Spectrum", QbzSession.trRev) },
|
||||
{ "vm": 0, "m": 6, "sp": -1, "entry": "",
|
||||
"label": QbzSession.tr("Wave Bed", QbzSession.trRev) },
|
||||
{ "vm": 0, "m": 7, "sp": -1, "entry": "",
|
||||
"label": QbzSession.tr("Goniometer", QbzSession.trRev) },
|
||||
{ "vm": 0, "m": 8, "sp": -1, "entry": "",
|
||||
"label": QbzSession.tr("Oscilloscope", QbzSession.trRev) },
|
||||
{ "vm": 0, "m": 4, "sp": -1, "entry": "",
|
||||
"label": QbzSession.tr("Lyrics", QbzSession.trRev) },
|
||||
{ "vm": 0, "m": 5, "sp": -1, "entry": "queue",
|
||||
|
||||
@@ -47,6 +47,8 @@ import "../theme"
|
||||
Item {
|
||||
id: root
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
// Self-gated on the bridge open flag (§5.1: closed = invisible,
|
||||
// non-interactive, zero-cost). Every open/close transition funnels
|
||||
// through QbzImmersive's open setter (§3/D3) — this item only reacts.
|
||||
@@ -300,6 +302,40 @@ Item {
|
||||
visible: QbzShaderScene.scene === 0
|
||||
&& QbzImmersive.viewMode === 0 && QbzImmersive.mode === 6
|
||||
}
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: QbzShaderScene.scene === 0
|
||||
&& QbzImmersive.viewMode === 0 && QbzImmersive.mode === 7
|
||||
visible: active
|
||||
sourceComponent: Item {
|
||||
ScopePanel {
|
||||
width: Math.max(1, Math.min(parent.width - 96, parent.height - 196))
|
||||
height: width
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 132
|
||||
scopeMode: 0
|
||||
traceColor: theme.accent
|
||||
}
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: QbzShaderScene.scene === 0
|
||||
&& QbzImmersive.viewMode === 0 && QbzImmersive.mode === 8
|
||||
visible: active
|
||||
sourceComponent: Item {
|
||||
ScopePanel {
|
||||
width: Math.max(1, Math.min(parent.width - 96, parent.height - 196))
|
||||
height: width
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 132
|
||||
scopeMode: 1
|
||||
traceColor: theme.accent
|
||||
}
|
||||
}
|
||||
}
|
||||
// FOCUS Lyrics (mode 4, B4 §6.6): the giant-line variant. It shares the
|
||||
// immersive LyricsSyncEngine with the split lyrics mount (trap 22 — the
|
||||
// gate covers BOTH lyric surfaces and nothing else). No entry load: Qt
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
// Mounted as a ROOT overlay by AppShell, pinned flush to the window
|
||||
// bottom-left (NOT inside the sidebar — that floated it above the bar).
|
||||
// Layout, bottom-up: a square album cover (= the fed width) at the very
|
||||
// bottom; ABOVE it a compact spectrum band the user can hide with the
|
||||
// top-right eye button on the cover.
|
||||
// bottom; ABOVE it either the compact spectrum band or a square scope the
|
||||
// user can hide with the top-right eye button on the cover.
|
||||
//
|
||||
// HEIGHT IS LOAD-BEARING. `QbzShell.largeDockHeight` (shell_bridge.rs) is the
|
||||
// single source of truth and the layout below is derived from the SAME
|
||||
// constants:
|
||||
// ON = 9 top pad + 42 band + 10 gap + 208 art + 4 bottom gap = 273
|
||||
// OFF = 9 top pad + 208 art + 4 bottom gap = 221
|
||||
// constants and the persisted render mode: legacy modes are 42px high;
|
||||
// scope modes use the same square size as the artwork and grow upward.
|
||||
// Sidebar.qml reserves `largeDockHeight - npb height` so the playlist list
|
||||
// stops ABOVE the cover, and AppShell pins this overlay at
|
||||
// `height - largeDockHeight`. Change the arithmetic in one place and the
|
||||
@@ -42,7 +41,8 @@ Item {
|
||||
property bool ambientOn: false
|
||||
|
||||
readonly property bool bandOn: QbzShell.largeVisualizerOn
|
||||
readonly property int bandHeight: 42
|
||||
readonly property int bandHeight: QbzShell.largeSpectrumMode >= 3
|
||||
? Math.round(root.width) : 42
|
||||
readonly property int padTop: 9
|
||||
readonly property int bandGap: 10
|
||||
// The cover's top edge: below the band when it is shown.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// SpectrumBand — the compact 42px spectrum strip above the Large-NPB cover.
|
||||
// Split out of SidebarNowPlayingDock.qml: the dock owns the layout arithmetic
|
||||
// and the cover, this file owns the three render modes and their motion.
|
||||
// and the cover, this file owns the five render modes and their motion.
|
||||
// QML port of the `spectrum := Rectangle` block in
|
||||
// crates/qbz-ui/ui/shell/SidebarNowPlayingDock.slint:122-183 (VBar / WaveCol).
|
||||
//
|
||||
@@ -88,6 +88,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import com.blitzfc.qbz
|
||||
import "../controls"
|
||||
import "../theme"
|
||||
|
||||
Rectangle {
|
||||
@@ -436,7 +437,29 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Click the band → cycle Bars -> Waveform -> Energy (persisted).
|
||||
// Modes 3/4 — real-time scopes. Each Loader owns a single scene-graph
|
||||
// line strip; when inactive the item does not exist and Rust also disables
|
||||
// that scope's DSP bit.
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: band.shown && QbzShell.largeSpectrumMode === 3
|
||||
sourceComponent: ScopePanel {
|
||||
compact: true
|
||||
scopeMode: 0
|
||||
traceColor: theme.accent
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: band.shown && QbzShell.largeSpectrumMode === 4
|
||||
sourceComponent: ScopePanel {
|
||||
compact: true
|
||||
scopeMode: 1
|
||||
traceColor: theme.accent
|
||||
}
|
||||
}
|
||||
|
||||
// Click the band → cycle all five modes (persisted).
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
//! (`qbz-nix-docs/qt-frontend/2026-08-02-immersive-port/00-CONTRACT.md` §3).
|
||||
//! All enums are `i32` with the Slint numbering verbatim
|
||||
//! (`state.slint:4238,4249,4254`): view_mode 0 FOCUS / 1 SPLIT; mode 0 Album
|
||||
//! Reactive, 1 Static, 2 Coverflow, 3 Spectrum, 4 Lyrics, 5 Queue, 6 WaveBed;
|
||||
//! Reactive, 1 Static, 2 Coverflow, 3 Spectrum, 4 Lyrics, 5 Queue, 6 WaveBed,
|
||||
//! 7 Goniometer, 8 Oscilloscope;
|
||||
//! split_panel 0 Lyrics, 1 TrackInfo, 2 Suggestions, 3 Queue.
|
||||
//!
|
||||
//! The `open` property uses a CUSTOM WRITE (`set_open`) so EVERY write — the
|
||||
@@ -61,7 +62,8 @@ pub mod qbz_immersive {
|
||||
// restore — QML never writes these directly (§3.2).
|
||||
#[qproperty(i32, view_mode)]
|
||||
// FOCUS foreground: 0 AlbumReactive, 1 Static, 2 Coverflow, 3
|
||||
// Spectrum, 4 Lyrics, 5 Queue, 6 WaveBed.
|
||||
// Spectrum, 4 Lyrics, 5 Queue, 6 WaveBed, 7 Goniometer,
|
||||
// 8 Oscilloscope.
|
||||
#[qproperty(i32, mode)]
|
||||
// SPLIT active panel: 0 Lyrics, 1 TrackInfo, 2 Suggestions, 3 Queue.
|
||||
#[qproperty(i32, split_panel)]
|
||||
@@ -297,6 +299,7 @@ fn apply_open(mut this: Pin<&mut QbzImmersive>, value: bool) {
|
||||
}
|
||||
// Unknown key: Slint's `_ => {}` — leave the current view alone.
|
||||
crate::viz_qt::immersive_opened();
|
||||
crate::viz_qt::set_immersive_view(this.view_mode, this.mode);
|
||||
// Shader scenes (block A1): the open edge decides the scene. Under
|
||||
// "remember" it restores the last one (2026-08-19 — the triple above
|
||||
// never covered the scenes, so Plasma / Ribbon / Line Bed / Tunnel
|
||||
@@ -356,6 +359,7 @@ impl qbz_immersive::QbzImmersive {
|
||||
self.as_mut().set_view_mode(view_mode);
|
||||
self.as_mut().set_mode(mode);
|
||||
self.as_mut().set_split_panel(split_panel);
|
||||
crate::viz_qt::set_immersive_view(view_mode, mode);
|
||||
if crate::settings_qt::pref_str("immersive_default_view", "remember") == "remember" {
|
||||
// JSON numbers — co-owned file, Slint declares the keys i32 (see
|
||||
// apply_open's comment).
|
||||
|
||||
@@ -1997,14 +1997,19 @@ pub(crate) fn large_toggle_visualizer() {
|
||||
});
|
||||
}
|
||||
|
||||
/// Large dock, band click: cycle Bars -> Waveform -> Energy.
|
||||
/// Large dock, band click: cycle Bars -> Waveform -> Energy -> Goniometer ->
|
||||
/// Oscilloscope.
|
||||
pub(crate) fn large_cycle_spectrum() {
|
||||
let mode = settings_qt::set_large_spectrum_mode((settings_qt::large_spectrum_mode() + 1) % 3);
|
||||
let mode = settings_qt::set_large_spectrum_mode((settings_qt::large_spectrum_mode() + 1) % 5);
|
||||
let height = shell_bridge::large_dock_height(settings_qt::large_visualizer_on());
|
||||
log::info!("[qbz-qt] large spectrum mode -> {mode}");
|
||||
// Point the drain at the new stream BEFORE the UI switches, so the first
|
||||
// frame the new mode renders already has data.
|
||||
viz_qt::set_mode(mode);
|
||||
shell_bridge::ui(move |mut b| b.as_mut().set_large_spectrum_mode(mode));
|
||||
shell_bridge::ui(move |mut b| {
|
||||
b.as_mut().set_large_spectrum_mode(mode);
|
||||
b.as_mut().set_large_dock_height(height);
|
||||
});
|
||||
}
|
||||
|
||||
/// Artist-network row click: resolve a musician NAME and open the right
|
||||
|
||||
@@ -840,7 +840,8 @@ pub fn set_large_visualizer_on(on: bool) -> bool {
|
||||
on
|
||||
}
|
||||
|
||||
/// The band's render mode: 0 Bars / 1 Waveform / 2 Energy.
|
||||
/// The band's render mode: 0 Bars / 1 Waveform / 2 Energy / 3 Goniometer /
|
||||
/// 4 Oscilloscope.
|
||||
pub fn large_spectrum_mode() -> i32 {
|
||||
let Some(path) = prefs_path() else {
|
||||
return 0;
|
||||
@@ -848,14 +849,17 @@ pub fn large_spectrum_mode() -> i32 {
|
||||
std::fs::read_to_string(path)
|
||||
.ok()
|
||||
.and_then(|text| serde_json::from_str::<serde_json::Value>(&text).ok())
|
||||
.and_then(|v| v.get("large_spectrum_mode").and_then(serde_json::Value::as_i64))
|
||||
.map(|m| (m as i32).clamp(0, 2))
|
||||
.and_then(|v| {
|
||||
v.get("large_spectrum_mode")
|
||||
.and_then(serde_json::Value::as_i64)
|
||||
})
|
||||
.map(|m| (m as i32).clamp(0, 4))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Persist the band mode (clamped 0-2); returns the stored index.
|
||||
/// Persist the band mode (clamped 0-4); returns the stored index.
|
||||
pub fn set_large_spectrum_mode(mode: i32) -> i32 {
|
||||
let mode = mode.clamp(0, 2);
|
||||
let mode = mode.clamp(0, 4);
|
||||
save_pref(
|
||||
"large_spectrum_mode",
|
||||
serde_json::Value::Number(mode.into()),
|
||||
|
||||
@@ -352,6 +352,7 @@ fn is_shipped(scene: i32) -> bool {
|
||||
/// notify.
|
||||
pub(crate) fn apply_on_immersive_open() {
|
||||
let target = remembered_scene();
|
||||
crate::viz_qt::set_immersive_scene_active(target != 0);
|
||||
ui(move |mut s| {
|
||||
if s.scene == target {
|
||||
return;
|
||||
@@ -477,6 +478,7 @@ impl qbz_shader_scene::QbzShaderScene {
|
||||
}
|
||||
self.as_mut().rust_mut().scene = value;
|
||||
self.as_mut().scene_changed();
|
||||
crate::viz_qt::set_immersive_scene_active(value != 0);
|
||||
if RESTORING.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -944,13 +944,11 @@ impl Default for QbzShellRust {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Large-NPB dock geometry
|
||||
// ---------------------------------------------------------------------------
|
||||
// SidebarNowPlayingDock.slint's height formula, verbatim. The cover is square
|
||||
// and as wide as the sidebar's content column (240 sidebar - 16 left - 16
|
||||
// right gutters = 208), so the totals are constants:
|
||||
// ON = 9 top pad + 42 band + 10 gap + 208 art + 4 bottom gap = 273
|
||||
// OFF = 9 top pad + 208 art + 4 bottom gap = 221
|
||||
// SidebarNowPlayingDock.qml lays itself out from these SAME numbers — if you
|
||||
// change one, change both, or the cover shifts off the window bottom edge.
|
||||
// The cover is square and as wide as the sidebar's content column (240
|
||||
// sidebar - 16 left - 16 right gutters = 208). Legacy spectrum modes use a
|
||||
// compact 42px band; scope modes use the same 208px square as the artwork and
|
||||
// grow upward from their bottom edge. SidebarNowPlayingDock.qml derives its
|
||||
// band height from the same mode boundary.
|
||||
pub(crate) const DOCK_ART_SIZE: f32 = 208.0;
|
||||
pub(crate) const DOCK_BAND_HEIGHT: f32 = 42.0;
|
||||
pub(crate) const DOCK_PAD_TOP: f32 = 9.0;
|
||||
@@ -961,7 +959,12 @@ pub(crate) const DOCK_BAND_GAP: f32 = 10.0;
|
||||
pub(crate) fn large_dock_height(visualizer_on: bool) -> f32 {
|
||||
let base = DOCK_PAD_TOP + DOCK_ART_SIZE + DOCK_PAD_BOTTOM;
|
||||
if visualizer_on {
|
||||
base + DOCK_BAND_HEIGHT + DOCK_BAND_GAP
|
||||
let band_height = if crate::settings_qt::large_spectrum_mode() >= 3 {
|
||||
DOCK_ART_SIZE
|
||||
} else {
|
||||
DOCK_BAND_HEIGHT
|
||||
};
|
||||
base + band_height + DOCK_BAND_GAP
|
||||
} else {
|
||||
base
|
||||
}
|
||||
|
||||
@@ -58,7 +58,11 @@ pub mod qbz_viz {
|
||||
// 256 L samples followed by 256 R (VizFrame::Wave256x2). The dock's
|
||||
// Waveform mode downsamples the L half (indices 0..235 step 5).
|
||||
#[qproperty(QList_f32, waveform)]
|
||||
|
||||
// Packed x/y mid-side points for the goniometer.
|
||||
#[qproperty(QList_f32, goniometer)]
|
||||
// Pitch-locked mono scope samples.
|
||||
#[qproperty(QList_f32, oscilloscope)]
|
||||
#[qproperty(f32, stereo_correlation)]
|
||||
type QbzViz = super::QbzVizRust;
|
||||
|
||||
/// Registers this object's Qt-thread hop (Main.qml boots EVERY
|
||||
@@ -87,6 +91,7 @@ type QListF32 = QList<f32>;
|
||||
pub(crate) const BARS_LEN: usize = 16;
|
||||
pub(crate) const ENERGY_LEN: usize = 5;
|
||||
pub(crate) const WAVEFORM_LEN: usize = 512;
|
||||
pub(crate) const SCOPE_LEN: usize = 512;
|
||||
|
||||
fn zeros(n: usize) -> QListF32 {
|
||||
let mut list = QListF32::default();
|
||||
@@ -101,6 +106,9 @@ pub struct QbzVizRust {
|
||||
bars: QListF32,
|
||||
energy: QListF32,
|
||||
waveform: QListF32,
|
||||
goniometer: QListF32,
|
||||
oscilloscope: QListF32,
|
||||
stereo_correlation: f32,
|
||||
}
|
||||
|
||||
impl Default for QbzVizRust {
|
||||
@@ -109,6 +117,9 @@ impl Default for QbzVizRust {
|
||||
bars: zeros(BARS_LEN),
|
||||
energy: zeros(ENERGY_LEN),
|
||||
waveform: zeros(WAVEFORM_LEN),
|
||||
goniometer: zeros(SCOPE_LEN),
|
||||
oscilloscope: zeros(SCOPE_LEN),
|
||||
stereo_correlation: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+210
-24
@@ -15,7 +15,7 @@
|
||||
//! aliasing, and zero idle wakeups (cheaper than the old poll, not just
|
||||
//! smoother).
|
||||
//!
|
||||
//! Cost controls (the Slint install() has the same three, and they are why the
|
||||
//! Cost controls (the frozen reference install() has the same three, and they are why the
|
||||
//! ambient field sits under 3% CPU during playback):
|
||||
//! 1. The tap starts DISABLED — nothing is captured and the producer idles
|
||||
//! until the dock's eye toggle calls `set_enabled(true)`.
|
||||
@@ -26,8 +26,10 @@
|
||||
//! Plus the mode gate: only the ONE stream the visible mode renders is
|
||||
//! marshalled into a `QList` (in Bars mode the 512-float waveform is latched
|
||||
//! for an instant mode switch but never published) — UNLESS the immersive
|
||||
//! overlay is open, which owns the marshalling set and publishes all three
|
||||
//! streams every frame (§4.2 of the immersive-port contract). Block A1
|
||||
//! overlay is open, which owns the legacy marshalling set and publishes bars,
|
||||
//! energy and waveform every frame (§4.2 of the immersive-port contract).
|
||||
//! The two scope streams are requested independently and only while their
|
||||
//! panel is visible. Block A1
|
||||
//! (2026-08-15 immersive-completion) extends the immersive set: `Spectral512`
|
||||
//! and `Transient1` are LATCHED too (still never dock-marshalled), and the
|
||||
//! drain derives the shader-scenes uniform pack from them HOST-SIDE
|
||||
@@ -49,7 +51,9 @@ use std::sync::{Arc, Mutex, OnceLock};
|
||||
use std::thread::Thread;
|
||||
|
||||
use cxx_qt_lib::QList;
|
||||
use qbz_audio::visualizer::{spawn_visualizer_thread, VizFrame, VizSink};
|
||||
use qbz_audio::visualizer::{
|
||||
spawn_visualizer_thread, VizFrame, VizSink, GONIOMETER_BIT, OSCILLOSCOPE_BIT,
|
||||
};
|
||||
use qbz_audio::VisualizerTap;
|
||||
|
||||
use crate::viz_bridge;
|
||||
@@ -83,6 +87,8 @@ struct VizCells {
|
||||
// (visualizer.rs:146-178).
|
||||
spectral: Mutex<Option<Vec<f32>>>,
|
||||
transient: Mutex<Option<f32>>,
|
||||
goniometer: Mutex<Option<(Box<[f32; 512]>, f32)>>,
|
||||
oscilloscope: Mutex<Option<Box<[f32; 512]>>>,
|
||||
}
|
||||
|
||||
/// Producer-side sink: latches frames into the shared cells and signals the
|
||||
@@ -135,6 +141,17 @@ impl VizSink for QtVizSink {
|
||||
}
|
||||
false
|
||||
}
|
||||
VizFrame::Goniometer {
|
||||
points,
|
||||
correlation,
|
||||
} => {
|
||||
*self.cells.goniometer.lock().unwrap() = Some((points, correlation));
|
||||
stream_wakes(mode, all, Stream::Goniometer)
|
||||
}
|
||||
VizFrame::Oscilloscope(points) => {
|
||||
*self.cells.oscilloscope.lock().unwrap() = Some(points);
|
||||
stream_wakes(mode, all, Stream::Oscilloscope)
|
||||
}
|
||||
};
|
||||
if wake {
|
||||
wake_drain();
|
||||
@@ -155,7 +172,8 @@ static DRAIN_THREAD: OnceLock<Thread> = OnceLock::new();
|
||||
/// This is the EFFECTIVE enable — the OR of the per-source bits in
|
||||
/// `ENABLED_MASK` (§4.2a); nothing writes it except `apply_effective_enabled`.
|
||||
static ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
/// The band's render mode (0 Bars / 1 Waveform / 2 Energy). The drain only
|
||||
/// The band's render mode (0 Bars / 1 Waveform / 2 Energy / 3 Goniometer /
|
||||
/// 4 Oscilloscope). The drain only
|
||||
/// publishes the ONE stream that mode consumes: in Bars mode, marshalling the
|
||||
/// 512-float waveform into a QList 30 times a second would be pure waste.
|
||||
static ACTIVE_MODE: AtomicI32 = AtomicI32::new(0);
|
||||
@@ -176,8 +194,13 @@ static ENABLED_MASK: AtomicU32 = AtomicU32::new(0);
|
||||
/// bars AND energy AND waveform publish every frame, regardless of
|
||||
/// `ACTIVE_MODE`. Dock mode-cycle writes while it is set still record
|
||||
/// `ACTIVE_MODE` (and the dock's pref) but have no effect on the marshalled
|
||||
/// set; the close edge restores `ACTIVE_MODE` from the pref anyway.
|
||||
/// legacy set; the close edge restores `ACTIVE_MODE` from the pref anyway.
|
||||
/// Scope DSP remains visibility-gated by the two scope masks below.
|
||||
static MARSHAL_ALL: AtomicBool = AtomicBool::new(false);
|
||||
static DOCK_SCOPE_MASK: AtomicU32 = AtomicU32::new(0);
|
||||
static IMMERSIVE_PANEL_SCOPE_MASK: AtomicU32 = AtomicU32::new(0);
|
||||
static IMMERSIVE_SCOPE_MASK: AtomicU32 = AtomicU32::new(0);
|
||||
static IMMERSIVE_SCENE_ACTIVE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// A consumer of the FFT tap (§4.2a).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
@@ -196,31 +219,73 @@ impl VizSource {
|
||||
}
|
||||
|
||||
fn mask_with(mask: u32, bit: u32, on: bool) -> u32 {
|
||||
if on { mask | bit } else { mask & !bit }
|
||||
if on {
|
||||
mask | bit
|
||||
} else {
|
||||
mask & !bit
|
||||
}
|
||||
}
|
||||
|
||||
fn mask_enabled(mask: u32) -> bool {
|
||||
mask != 0
|
||||
}
|
||||
|
||||
/// The three marshalled streams (the wake table's rows).
|
||||
/// The five dock render streams (the wake table's rows).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum Stream {
|
||||
Bars,
|
||||
Waveform,
|
||||
Energy,
|
||||
Goniometer,
|
||||
Oscilloscope,
|
||||
}
|
||||
|
||||
/// Wake table for the drain signal: marshal-all wakes on EVERY stream;
|
||||
/// otherwise exactly the ONE stream the dock's active mode renders.
|
||||
/// Wake table for the drain signal: immersive marshal-all wakes on every
|
||||
/// legacy stream; scope frames hitchhike on that cadence. Otherwise exactly
|
||||
/// the ONE stream the dock's active mode renders wakes the drain.
|
||||
fn stream_wakes(mode: i32, marshal_all: bool, stream: Stream) -> bool {
|
||||
if marshal_all {
|
||||
if marshal_all && matches!(stream, Stream::Bars | Stream::Waveform | Stream::Energy) {
|
||||
return true;
|
||||
}
|
||||
match stream {
|
||||
Stream::Bars => !matches!(mode, 1 | 2),
|
||||
Stream::Bars => mode == 0,
|
||||
Stream::Waveform => mode == 1,
|
||||
Stream::Energy => mode == 2,
|
||||
Stream::Goniometer => !marshal_all && mode == 3,
|
||||
Stream::Oscilloscope => !marshal_all && mode == 4,
|
||||
}
|
||||
}
|
||||
|
||||
fn scope_mask_for_mode(mode: i32) -> u32 {
|
||||
match mode {
|
||||
3 | 7 => GONIOMETER_BIT,
|
||||
4 | 8 => OSCILLOSCOPE_BIT,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn visible_scope_mask(requested: u32, scene_active: bool) -> u32 {
|
||||
if scene_active {
|
||||
0
|
||||
} else {
|
||||
requested
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_immersive_scope_mask() {
|
||||
let mask = visible_scope_mask(
|
||||
IMMERSIVE_PANEL_SCOPE_MASK.load(Ordering::Relaxed),
|
||||
IMMERSIVE_SCENE_ACTIVE.load(Ordering::Relaxed),
|
||||
);
|
||||
IMMERSIVE_SCOPE_MASK.store(mask, Ordering::Relaxed);
|
||||
apply_scope_mask();
|
||||
}
|
||||
|
||||
fn apply_scope_mask() {
|
||||
if let Some(handles) = HANDLES.get() {
|
||||
handles.tap.set_scope_mask(
|
||||
DOCK_SCOPE_MASK.load(Ordering::Relaxed) | IMMERSIVE_SCOPE_MASK.load(Ordering::Relaxed),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,13 +483,24 @@ fn publish_active(cells: &VizCells, pack: &mut ShaderPackState, linebed: &mut li
|
||||
if let Some(b) = waveform {
|
||||
viz_bridge::ui(move |mut v| v.as_mut().set_waveform(to_qlist(b.as_ref())));
|
||||
}
|
||||
let goniometer = cells.goniometer.lock().unwrap().take();
|
||||
if let Some((points, correlation)) = goniometer {
|
||||
viz_bridge::ui(move |mut v| {
|
||||
v.as_mut().set_goniometer(to_qlist(points.as_ref()));
|
||||
v.as_mut().set_stereo_correlation(correlation);
|
||||
});
|
||||
}
|
||||
let oscilloscope = cells.oscilloscope.lock().unwrap().take();
|
||||
if let Some(points) = oscilloscope {
|
||||
viz_bridge::ui(move |mut v| {
|
||||
v.as_mut().set_oscilloscope(to_qlist(points.as_ref()));
|
||||
});
|
||||
}
|
||||
// A1: the scene-only streams, latched by the sink without waking.
|
||||
let spectral = cells.spectral.lock().unwrap().take();
|
||||
let transient = cells.transient.lock().unwrap().take();
|
||||
if fresh_frame {
|
||||
crate::shader_scene_bridge::publish_pack(
|
||||
pack.tick(transient, spectral.as_deref()),
|
||||
);
|
||||
crate::shader_scene_bridge::publish_pack(pack.tick(transient, spectral.as_deref()));
|
||||
}
|
||||
// A4 (Line Bed, mode 5): push the SAME latched Spectral512 frame
|
||||
// into the depth ring (fresh frames only, like the reference's
|
||||
@@ -471,6 +547,23 @@ fn publish_active(cells: &VizCells, pack: &mut ShaderPackState, linebed: &mut li
|
||||
viz_bridge::ui(move |mut v| v.as_mut().set_energy(to_qlist(&b)));
|
||||
}
|
||||
}
|
||||
3 => {
|
||||
let frame = cells.goniometer.lock().unwrap().take();
|
||||
if let Some((points, correlation)) = frame {
|
||||
viz_bridge::ui(move |mut v| {
|
||||
v.as_mut().set_goniometer(to_qlist(points.as_ref()));
|
||||
v.as_mut().set_stereo_correlation(correlation);
|
||||
});
|
||||
}
|
||||
}
|
||||
4 => {
|
||||
let frame = cells.oscilloscope.lock().unwrap().take();
|
||||
if let Some(points) = frame {
|
||||
viz_bridge::ui(move |mut v| {
|
||||
v.as_mut().set_oscilloscope(to_qlist(points.as_ref()));
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let frame = cells.bars.lock().unwrap().take();
|
||||
if let Some(b) = frame {
|
||||
@@ -489,6 +582,11 @@ pub fn install(tap: VisualizerTap) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The startup macro is normally sufficient, but the explicit symbol
|
||||
// reference keeps the custom item linked from static archives on every
|
||||
// platform and guarantees registration before the QML module loads.
|
||||
register_scope_qml_item();
|
||||
|
||||
let cells = Arc::new(VizCells::default());
|
||||
|
||||
// The drain: park until the sink says a frame of the visible stream is
|
||||
@@ -543,6 +641,13 @@ pub fn install(tap: VisualizerTap) {
|
||||
/// a tap the other consumer still needs.
|
||||
pub fn set_enabled(on: bool) {
|
||||
set_enabled_source(VizSource::Dock, on);
|
||||
let mask = if on {
|
||||
scope_mask_for_mode(ACTIVE_MODE.load(Ordering::Relaxed))
|
||||
} else {
|
||||
0
|
||||
};
|
||||
DOCK_SCOPE_MASK.store(mask, Ordering::Relaxed);
|
||||
apply_scope_mask();
|
||||
}
|
||||
|
||||
/// §4.2a two-source enable: set one consumer's bit; the EFFECTIVE enable is
|
||||
@@ -604,6 +709,9 @@ pub(crate) fn immersive_opened() {
|
||||
/// while immersive was open already updated it via
|
||||
/// `settings_qt::set_large_spectrum_mode`).
|
||||
pub(crate) fn immersive_closed() {
|
||||
IMMERSIVE_PANEL_SCOPE_MASK.store(0, Ordering::Relaxed);
|
||||
IMMERSIVE_SCOPE_MASK.store(0, Ordering::Relaxed);
|
||||
apply_scope_mask();
|
||||
set_enabled_source(VizSource::Immersive, false);
|
||||
MARSHAL_ALL.store(false, Ordering::Relaxed);
|
||||
set_mode(crate::settings_qt::large_spectrum_mode());
|
||||
@@ -612,7 +720,18 @@ pub(crate) fn immersive_closed() {
|
||||
/// Point the drain at the stream the band is actually rendering. Called at
|
||||
/// install time (from the persisted pref) and on every mode cycle.
|
||||
pub fn set_mode(mode: i32) {
|
||||
ACTIVE_MODE.store(mode.clamp(0, 2), Ordering::Relaxed);
|
||||
let mode = mode.clamp(0, 4);
|
||||
ACTIVE_MODE.store(mode, Ordering::Relaxed);
|
||||
let dock_visible = ENABLED_MASK.load(Ordering::Relaxed) & DOCK_BIT != 0;
|
||||
DOCK_SCOPE_MASK.store(
|
||||
if dock_visible {
|
||||
scope_mask_for_mode(mode)
|
||||
} else {
|
||||
0
|
||||
},
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
apply_scope_mask();
|
||||
// The new stream may already have a frame latched from before the switch;
|
||||
// publish it now instead of showing the old mode's last frame until the
|
||||
// producer's next tick (and it is what unblocks a switch made while the
|
||||
@@ -622,6 +741,36 @@ pub fn set_mode(mode: i32) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the scope requested by the active immersive FOCUS panel. Split
|
||||
/// layouts and every existing FOCUS mode leave both scope producers idle.
|
||||
pub(crate) fn set_immersive_view(view_mode: i32, mode: i32) {
|
||||
let mask = if MARSHAL_ALL.load(Ordering::Relaxed) && view_mode == 0 {
|
||||
scope_mask_for_mode(mode)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
IMMERSIVE_PANEL_SCOPE_MASK.store(mask, Ordering::Relaxed);
|
||||
apply_immersive_scope_mask();
|
||||
}
|
||||
|
||||
/// A shader scene replaces every FOCUS panel. Keep the desired scope mode so
|
||||
/// returning to the panel is instant, but disable its DSP and Qt publishes
|
||||
/// while the scene is covering it.
|
||||
pub(crate) fn set_immersive_scene_active(active: bool) {
|
||||
IMMERSIVE_SCENE_ACTIVE.store(active, Ordering::Relaxed);
|
||||
apply_immersive_scope_mask();
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
fn qbz_scope_trace_register_qml_type();
|
||||
}
|
||||
|
||||
fn register_scope_qml_item() {
|
||||
// SAFETY: no arguments; registration is guarded and occurs before QML is
|
||||
// loaded. The explicit reference also keeps the static-library object.
|
||||
unsafe { qbz_scope_trace_register_qml_type() };
|
||||
}
|
||||
|
||||
/// Mirror the transport onto the tap so a paused player parks the producer.
|
||||
/// Called from the playback path on every playing-state flip (the Slint side
|
||||
/// does this from playback.rs).
|
||||
@@ -668,11 +817,18 @@ mod tests {
|
||||
assert!(!mask_enabled(none));
|
||||
}
|
||||
|
||||
/// The pre-immersive wake table: exactly ONE stream per dock mode.
|
||||
/// The dock wake table: exactly ONE stream per render mode.
|
||||
#[test]
|
||||
fn single_stream_wake_table() {
|
||||
for mode in 0..=2 {
|
||||
let wakes: Vec<bool> = [Stream::Bars, Stream::Waveform, Stream::Energy]
|
||||
let streams = [
|
||||
Stream::Bars,
|
||||
Stream::Waveform,
|
||||
Stream::Energy,
|
||||
Stream::Goniometer,
|
||||
Stream::Oscilloscope,
|
||||
];
|
||||
for mode in 0..=4 {
|
||||
let wakes: Vec<bool> = streams
|
||||
.into_iter()
|
||||
.map(|s| stream_wakes(mode, false, s))
|
||||
.collect();
|
||||
@@ -681,20 +837,35 @@ mod tests {
|
||||
assert!(stream_wakes(0, false, Stream::Bars));
|
||||
assert!(stream_wakes(1, false, Stream::Waveform));
|
||||
assert!(stream_wakes(2, false, Stream::Energy));
|
||||
assert!(stream_wakes(3, false, Stream::Goniometer));
|
||||
assert!(stream_wakes(4, false, Stream::Oscilloscope));
|
||||
}
|
||||
|
||||
/// §4.2b: marshal-all wakes on EVERY stream regardless of ACTIVE_MODE —
|
||||
/// which is also why dock mode-cycle writes while immersive is open have
|
||||
/// no effect on the marshalled set.
|
||||
/// §4.2b: marshal-all wakes on every legacy stream regardless of
|
||||
/// ACTIVE_MODE. Scope frames hitchhike on that fixed cadence, avoiding
|
||||
/// extra drain wakeups while immersive is open.
|
||||
#[test]
|
||||
fn marshal_all_wakes_every_stream() {
|
||||
for mode in 0..=2 {
|
||||
for s in [Stream::Bars, Stream::Waveform, Stream::Energy] {
|
||||
assert!(stream_wakes(mode, true, s), "mode {mode} stream {s:?}");
|
||||
}
|
||||
assert!(!stream_wakes(mode, true, Stream::Goniometer));
|
||||
assert!(!stream_wakes(mode, true, Stream::Oscilloscope));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shader_scene_suppresses_hidden_scope() {
|
||||
assert_eq!(visible_scope_mask(GONIOMETER_BIT, false), GONIOMETER_BIT);
|
||||
assert_eq!(
|
||||
visible_scope_mask(OSCILLOSCOPE_BIT, false),
|
||||
OSCILLOSCOPE_BIT
|
||||
);
|
||||
assert_eq!(visible_scope_mask(GONIOMETER_BIT, true), 0);
|
||||
assert_eq!(visible_scope_mask(OSCILLOSCOPE_BIT, true), 0);
|
||||
}
|
||||
|
||||
/// The hooks against the REAL statics (kept in ONE test so parallel test
|
||||
/// threads cannot race the shared atomics): open sets the immersive bit +
|
||||
/// marshal-all; close clears both and restores ACTIVE_MODE from the
|
||||
@@ -702,19 +873,33 @@ mod tests {
|
||||
/// application is a no-op and only the masks/flags move.
|
||||
#[test]
|
||||
fn immersive_hooks_switch_and_restore() {
|
||||
// Selecting a scope while the dock is hidden only records the mode;
|
||||
// its DSP request stays off until the dock becomes visible.
|
||||
set_mode(3);
|
||||
assert_eq!(DOCK_SCOPE_MASK.load(Ordering::Relaxed), 0);
|
||||
|
||||
// Dock was on (the common case: dock drives the tap, then immersive
|
||||
// opens on top).
|
||||
set_enabled(true);
|
||||
assert_eq!(ENABLED_MASK.load(Ordering::Relaxed), DOCK_BIT);
|
||||
assert_eq!(DOCK_SCOPE_MASK.load(Ordering::Relaxed), GONIOMETER_BIT);
|
||||
|
||||
immersive_opened();
|
||||
assert!(MARSHAL_ALL.load(Ordering::Relaxed));
|
||||
assert_eq!(ENABLED_MASK.load(Ordering::Relaxed), DOCK_BIT | IMMERSIVE_BIT);
|
||||
assert_eq!(
|
||||
ENABLED_MASK.load(Ordering::Relaxed),
|
||||
DOCK_BIT | IMMERSIVE_BIT
|
||||
);
|
||||
|
||||
// A dock mode-cycle while immersive is open records ACTIVE_MODE but
|
||||
// the wake table stays all-three (marshal-all short-circuits it).
|
||||
set_mode(2);
|
||||
assert!(stream_wakes(2, MARSHAL_ALL.load(Ordering::Relaxed), Stream::Bars));
|
||||
assert_eq!(DOCK_SCOPE_MASK.load(Ordering::Relaxed), 0);
|
||||
assert!(stream_wakes(
|
||||
2,
|
||||
MARSHAL_ALL.load(Ordering::Relaxed),
|
||||
Stream::Bars
|
||||
));
|
||||
|
||||
immersive_closed();
|
||||
assert!(!MARSHAL_ALL.load(Ordering::Relaxed));
|
||||
@@ -729,6 +914,7 @@ mod tests {
|
||||
// Leave the statics as found for any later test in the process.
|
||||
set_enabled(false);
|
||||
assert_eq!(ENABLED_MASK.load(Ordering::Relaxed), 0);
|
||||
assert_eq!(DOCK_SCOPE_MASK.load(Ordering::Relaxed), 0);
|
||||
}
|
||||
|
||||
// --- A1: the shader-pack derivation (visualizer.rs:203-269 parity) -----
|
||||
|
||||
Reference in new issue
Block a user