Skip to content

Commit e27c8c9

Browse files
authored
Optimize rendering (#149)
* Draw a vertical min-max line for zoomed out signals * Use polyline * Implement downsampling pyramid * Add changelog entry
1 parent 09e6fc8 commit e27c8c9

6 files changed

Lines changed: 266 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
- Use SVG icons with support for light and dark themes
44
- Add option to toggle between showing the menubar and using a hamburger menu on Windows and Linux
55

6+
### 🌀 Changed
7+
- Faster scrolling/zooming for long recordings at high zoom-out levels (uses a per-pixel min–max envelope with automatic downsampling depending on the zoom level)
8+
69
## [0.6.5] · 2026-03-24
710
### 🌀 Changed
811
- Switch from Qt 5 to Qt 6

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ qt_add_library(sigviewer_objects OBJECT
153153
src/file_handling/evt_writer.cpp
154154
src/file_handling/evt_writer.h
155155
src/file_handling/file_handler_factory_registrator.h
156+
src/file_handling/down_sampling_thread.cpp
157+
src/file_handling/down_sampling_thread.h
156158
src/file_handling/xdf_reader.cpp
157159
src/file_handling/xdf_reader.h
158160

src/file_handling/down_sampling_thread.cpp

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,24 @@
44

55

66
#include "down_sampling_thread.h"
7-
8-
#include <fstream>
9-
10-
#include "signal_processing/SPUC/butterworth.h"
117
#include "base/fixed_data_block.h"
128
#include "gui/background_processes.h"
139

1410
#include <vector>
15-
#include <QMessageBox>
11+
#include <limits>
12+
#include <cmath>
1613
#include <QDebug>
1714

15+
// SPUC headers define macros (E, PI, ...) that must come after Qt/STL headers
16+
// to avoid corrupting Qt6's C++20 constructs in C++17 mode.
17+
#include <fstream>
18+
#include "signal_processing/SPUC/butterworth.h"
19+
1820
namespace SigViewer_
1921
{
2022

23+
using namespace sigviewer;
24+
2125
QString DownSamplingThread::PROCESS_NAME_ ("Downsampling...");
2226

2327
//-----------------------------------------------------------------------------
@@ -63,10 +67,102 @@ void DownSamplingThread::run ()
6367
running_ = false;
6468
}
6569

70+
//-------------------------------------------------------------------------
71+
void DownSamplingThread::runSynchronously ()
72+
{
73+
minMaxDownsampling ();
74+
}
75+
6676
//-------------------------------------------------------------------------
6777
void DownSamplingThread::minMaxDownsampling ()
6878
{
79+
std::set<ChannelID> channels = channel_manager_->getChannels ();
80+
size_t num_samples = channel_manager_->getNumberSamples ();
81+
double sample_rate = channel_manager_->getSampleRate ();
6982

83+
for (ChannelID id : channels)
84+
{
85+
QSharedPointer<DataBlock const> full_data =
86+
channel_manager_->getData (id, 0, static_cast<unsigned>(num_samples));
87+
if (full_data.isNull () || full_data->size () == 0)
88+
continue;
89+
90+
size_t cur_size = full_data->size ();
91+
92+
// Seed working buffers from the raw samples.
93+
// NaN values propagate: a downsampled window is NaN only when every
94+
// raw sample inside it is NaN.
95+
QVector<float32> cur_min (static_cast<int>(cur_size));
96+
QVector<float32> cur_max (static_cast<int>(cur_size));
97+
for (size_t i = 0; i < cur_size; i++)
98+
{
99+
float32 v = (*full_data)[i];
100+
cur_min[static_cast<int>(i)] = v;
101+
cur_max[static_cast<int>(i)] = v;
102+
}
103+
104+
// Build the min/max pyramid hierarchically: each level aggregates the
105+
// previous one by downsampling_step_, so total work is O(N) per channel
106+
// (geometric series: N + N/step + N/step^2 + ...).
107+
for (unsigned factor = downsampling_step_;
108+
factor < downsampling_max_ && cur_size > 1;
109+
factor *= downsampling_step_)
110+
{
111+
size_t next_size = (cur_size + downsampling_step_ - 1) / downsampling_step_;
112+
QVector<float32> next_min (static_cast<int>(next_size));
113+
QVector<float32> next_max (static_cast<int>(next_size));
114+
115+
for (size_t i = 0; i < next_size; i++)
116+
{
117+
size_t j0 = i * downsampling_step_;
118+
size_t j1 = std::min (j0 + static_cast<size_t>(downsampling_step_), cur_size);
119+
120+
float32 vmin = std::numeric_limits<float32>::quiet_NaN ();
121+
float32 vmax = std::numeric_limits<float32>::quiet_NaN ();
122+
123+
for (size_t j = j0; j < j1; j++)
124+
{
125+
float32 lo = cur_min[static_cast<int>(j)];
126+
float32 hi = cur_max[static_cast<int>(j)];
127+
if (!std::isnan (lo))
128+
{
129+
if (std::isnan (vmin))
130+
{
131+
vmin = lo;
132+
vmax = hi;
133+
}
134+
else
135+
{
136+
if (lo < vmin) vmin = lo;
137+
if (hi > vmax) vmax = hi;
138+
}
139+
}
140+
}
141+
142+
next_min[static_cast<int>(i)] = vmin;
143+
next_max[static_cast<int>(i)] = vmax;
144+
}
145+
146+
// Persist this level – copy into fresh QVectors so the working
147+
// buffers below can be moved cheaply on the next iteration.
148+
auto stored_min = QSharedPointer<QVector<float32>> (
149+
new QVector<float32> (next_min));
150+
auto stored_max = QSharedPointer<QVector<float32>> (
151+
new QVector<float32> (next_max));
152+
153+
double ds_rate = sample_rate / factor;
154+
channel_manager_->addDownsampledMinMaxVersion (
155+
id,
156+
QSharedPointer<DataBlock const> (new FixedDataBlock (stored_min, ds_rate)),
157+
QSharedPointer<DataBlock const> (new FixedDataBlock (stored_max, ds_rate)),
158+
factor);
159+
160+
// Advance working buffers to this level for the next iteration.
161+
cur_min = std::move (next_min);
162+
cur_max = std::move (next_max);
163+
cur_size = next_size;
164+
}
165+
}
70166
}
71167

72168
//-------------------------------------------------------------------------
@@ -83,14 +179,11 @@ void DownSamplingThread::downsampleAllOnBasisData ()
83179
{
84180
for (int channel = 0; channel < data_.size(); channel++)
85181
{
86-
max_channel_length = std::max (max_channel_length, basis_data_[channel]->size());
182+
max_channel_length = std::max (max_channel_length, static_cast<unsigned>(basis_data_[channel]->size()));
87183
sample_rates[downsampling_factor].append (basis_data_[channel]->getSampleRatePerUnit() / downsampling_factor);
88184

89185
QSharedPointer<QVector<float32> > raw_data_vector (new QVector<float32> (static_cast<int>(basis_data_[channel]->size () / downsampling_factor) + 1));
90186

91-
QSharedPointer<DataBlock> downsampled_data (new FixedDataBlock (raw_data_vector, sample_rates[downsampling_factor][channel]));
92-
basis_data_[channel]->addDownSampledVersion (downsampled_data, downsampling_factor);
93-
94187
raw_downsampled_data[downsampling_factor].append (raw_data_vector);
95188
low_pass_filters[downsampling_factor].append (QSharedPointer<SPUC::butterworth<float32> > (new SPUC::butterworth<float32> (0.5 / downsampling_factor, 4, 3)));
96189
}
@@ -132,7 +225,7 @@ void DownSamplingThread::downsampleOnDownsampledData ()
132225

133226
for (int channel = 0; channel < data_.size(); channel++)
134227
{
135-
max_channel_length = std::max (max_channel_length, data_[channel]->size());
228+
max_channel_length = std::max (max_channel_length, static_cast<unsigned>(data_[channel]->size()));
136229
raw_downsampled_data.append (QSharedPointer<QVector<float32> > (new QVector<float32> (static_cast<int>(data_[channel]->size () / downsampling_step_) + 1)));
137230
low_pass_filters.append (QSharedPointer<SPUC::butterworth<float32> > (new SPUC::butterworth<float32> (0.9 / downsampling_step_, 4, 3)));
138231
sample_rates_.append (data_[channel]->getSampleRatePerUnit() / downsampling_step_);
@@ -156,7 +249,6 @@ void DownSamplingThread::downsampleOnDownsampledData ()
156249
for (int channel = 0; channel < raw_downsampled_data.size(); channel++)
157250
{
158251
QSharedPointer<DataBlock> downsampled_data (new FixedDataBlock (raw_downsampled_data[channel], sample_rates_[channel]));
159-
basis_data_[channel]->addDownSampledVersion (downsampled_data, downsampling_factor);
160252
data_.append (downsampled_data);
161253
}
162254
}

src/file_handling/down_sampling_thread.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
namespace SigViewer_
1919
{
2020

21+
using sigviewer::DataBlock;
22+
using sigviewer::ChannelManager;
23+
using sigviewer::ChannelID;
24+
2125
class DownSamplingThread : public QThread
2226
{
2327
Q_OBJECT
@@ -31,6 +35,12 @@ class DownSamplingThread : public QThread
3135
//-------------------------------------------------------------------------
3236
virtual ~DownSamplingThread ();
3337

38+
//-------------------------------------------------------------------------
39+
/// Run minMaxDownsampling() directly on the calling thread.
40+
/// Use this instead of start()+wait() when already on the main thread to
41+
/// avoid Qt widgets being instantiated on a background thread.
42+
void runSynchronously ();
43+
3444
signals:
3545
//-------------------------------------------------------------------------
3646
void downsamplingDataFinished (QSharedPointer<DataBlock> data, ChannelID channel, unsigned factor);

src/gui/commands/open_file_gui_command.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "file_handling/file_signal_reader_factory.h"
1414
#include "file_handling/event_manager.h"
1515
#include "file_handling/file_channel_manager.h"
16+
#include "file_handling/down_sampling_thread.h"
1617
#include "tab_context.h"
1718
#include "file_context.h"
1819
#include "gui/main_window_model.h"
@@ -323,6 +324,16 @@ void OpenFileGuiCommand::openFileImpl (QString file_path, bool instantly)
323324

324325
ProgressBar::instance().initAndShow (channel_manager->getNumberChannels() * 3, tr("Opening ") + file_name,
325326
applicationContext());
327+
328+
// Build the min/max downsampling pyramid used by the zoomed-out rendering
329+
// path. Runs synchronously so the data is ready before the first paint.
330+
{
331+
QSharedPointer<ChannelManager> cm (channel_manager, [] (ChannelManager*) {});
332+
SigViewer_::DownSamplingThread ds_thread (cm, 2,
333+
static_cast<unsigned> (channel_manager->getNumberSamples ()) + 1u);
334+
ds_thread.runSynchronously ();
335+
}
336+
326337
QSharedPointer<EventManager> event_manager (new EventManager (*file_signal_reader));
327338
QSharedPointer<FileContext> file_context (new FileContext (file_path, event_manager,
328339
channel_manager, file_signal_reader->getBasicHeader()));

0 commit comments

Comments
 (0)