Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
95dff4f
factor context creation out so to remove the qt dependency
toloudis Jun 28, 2026
d1253dd
add plan
toloudis Jul 30, 2026
bedba28
async volume loading API + reader per-timestep efficiency
toloudis Jul 30, 2026
ff58387
report cache statistics in the GUI statistics panel
toloudis Jul 30, 2026
dd800d9
cache pinning and eviction notification
toloudis Jul 30, 2026
eb697b2
add TimeSeriesLoader: async time-series loading with forward prefetch
toloudis Jul 30, 2026
0ccc87a
make time stepping asynchronous in the GUI
toloudis Jul 30, 2026
9fe611d
treat a channel-count mismatch between time steps as an error
toloudis Jul 30, 2026
ef54ac0
add time-series playback: play/pause, stop, frame rate, loop
toloudis Jul 30, 2026
ef72f3e
show cached time steps as a strip along the time slider
toloudis Jul 31, 2026
9c8475e
expose prefetch and playback settings in the UI
toloudis Jul 31, 2026
38435e9
fix crash on accepting the load dialog for a time series
toloudis Jul 31, 2026
bb804fc
fix playback stalling at the end of the cached run
toloudis Jul 31, 2026
659c1fd
make prefetch use the disk cache, and write to it off the hot path
toloudis Jul 31, 2026
e9f12ce
make prefetch wrap so looping playback works
toloudis Jul 31, 2026
cfdeef0
clamp the prefetch window to what the cache can hold
toloudis Jul 31, 2026
306ba57
read zarr channels concurrently and skip the scratch copy for 16-bit …
toloudis Jul 31, 2026
c8c25c5
speed up the vulkan volume upload path
toloudis Jul 31, 2026
7d6ed5f
update implementation status with phases 3 and 11 and the bugs found …
toloudis Jul 31, 2026
0130d30
fix volume rotating the wrong way in vulkan raymarch orthographic mode
toloudis Jul 31, 2026
a5ea716
reuse the file reader across SetTime commands
toloudis Jul 31, 2026
646279b
record phase 10, the shader fix, and why phase 12 is gated on measure…
toloudis Jul 31, 2026
27783be
Revert "fix volume rotating the wrong way in vulkan raymarch orthogra…
toloudis Jul 31, 2026
9d32809
Revert "speed up the vulkan volume upload path"
toloudis Jul 31, 2026
103060c
Reapply "fix volume rotating the wrong way in vulkan raymarch orthogr…
toloudis Jul 31, 2026
01242d5
Revert "Reapply "fix volume rotating the wrong way in vulkan raymarch…
toloudis Jul 31, 2026
0e18406
Reapply "Reapply "fix volume rotating the wrong way in vulkan raymarc…
toloudis Jul 31, 2026
4b9de58
Revert "Reapply "Reapply "fix volume rotating the wrong way in vulkan…
toloudis Jul 31, 2026
11d7a55
fix volume mirrored vertically in vulkan raymarch orthographic mode
toloudis Jul 31, 2026
717b1cd
Reapply "speed up the vulkan volume upload path"
toloudis Jul 31, 2026
71494d3
stop prefetch looping forever; warm memory near, disk far
toloudis Aug 1, 2026
fa11040
paint the DiskCached state on the time slider strip
toloudis Aug 1, 2026
4023d17
warm the disk cache without pushing time steps through memory
toloudis Aug 1, 2026
953f20d
docs: settle the time-series caching design and plan its implementation
toloudis Aug 1, 2026
0cfcb1f
reserve disk cache space for queued writes and stop dropping them
toloudis Aug 1, 2026
85022f4
report disk cache evictions so a dropped entry stops reporting as cached
toloudis Aug 1, 2026
9166cb5
retire prefetch depth and fillCache, and stop RAM-throttling disk war…
toloudis Aug 1, 2026
2d5d135
size the memory window to the RAM budget and clamp the disk warm set
toloudis Aug 1, 2026
234d38b
recognise a warm disk cache without dragging it through memory
toloudis Aug 1, 2026
302c5ab
report pending disk-write bytes, and record the implementation outcome
toloudis Aug 1, 2026
96c5998
stop prefetch wanting the whole series before the frame size is known
toloudis Aug 1, 2026
b05654b
make the load dialog prefetch checkbox actually turn prefetch off
toloudis Aug 1, 2026
1f58e22
load the frame playback is waiting for when prefetch is not doing it
toloudis Aug 1, 2026
513c6de
fix prefetch checkbox
toloudis Aug 1, 2026
b7a041b
the main change here is the zarr loader is made more cancellable whic…
toloudis Aug 2, 2026
66819f8
slider fixup
toloudis Aug 2, 2026
acad2ea
a little more testing and cleanup
toloudis Aug 2, 2026
432f7ce
gui cleanup
toloudis Aug 2, 2026
9655077
simplify excess comments for obvious code
toloudis Aug 2, 2026
4c4e163
restructure settings
toloudis Aug 2, 2026
0625e15
rename time slider
toloudis Aug 2, 2026
4e0d079
cleanup
toloudis Aug 2, 2026
9d6458f
cleanup
toloudis Aug 3, 2026
7dc831e
cmake policy
toloudis Aug 3, 2026
3e24748
remove dead code
toloudis Aug 3, 2026
d6b5646
fix policy
toloudis Aug 3, 2026
13028a1
remove stop button
toloudis Aug 3, 2026
13787f1
build fix
toloudis Aug 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
101 changes: 70 additions & 31 deletions agave_app/CacheSettings.cpp → agave_app/AgaveSettings.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#include "CacheSettings.h"
#include "AgaveSettings.h"

#include "renderlib/CacheManager.h"
#include "renderlib/Logging.h"
Expand All @@ -10,6 +10,8 @@

#include <nlohmann/json.hpp>

#include <algorithm>

namespace {

::CacheConfig
Expand Down Expand Up @@ -50,19 +52,38 @@ toRenderlibConfig(const CacheSettingsData& data)
return config;
}

const nlohmann::json*
objectIfPresent(const nlohmann::json& parent, const char* key)
{
auto it = parent.find(key);
if (it != parent.end() && it->is_object()) {
return &(*it);
}
return nullptr;
}

template<typename T>
void
readIfPresent(const nlohmann::json& object, const char* key, T& value)
{
if (object.contains(key)) {
value = object[key].get<T>();
}
}

} // namespace

CacheSettings::CacheSettings() = default;
AgaveSettings::AgaveSettings() = default;

CacheSettingsData
CacheSettings::defaultSettings() const
AgaveSettingsData
AgaveSettings::defaultSettings() const
{
// Tunable defaults come from CacheSettingsData's in-class initializers.
// Tunable defaults come from the settings structs' in-class initializers.
return {};
}

std::string
CacheSettings::configPath() const
AgaveSettings::configPath() const
{
QString baseDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
if (baseDir.isEmpty()) {
Expand All @@ -72,52 +93,70 @@ CacheSettings::configPath() const
baseDir = QDir::currentPath();
}
QDir().mkpath(baseDir);
return QDir(baseDir).filePath("cache_settings.json").toStdString();
return QDir(baseDir).filePath("agave_settings.json").toStdString();
}

CacheSettingsData
CacheSettings::load()
void
AgaveSettings::load()
{
CacheSettingsData data = defaultSettings();
AgaveSettingsData data = defaultSettings();
QString path = QString::fromStdString(configPath());
QFile file(path);
if (!file.exists()) {
return data;
m_data = data;
return;
}
if (!file.open(QIODevice::ReadOnly)) {
return data;
m_data = data;
return;
}

QByteArray raw = file.readAll();
try {
nlohmann::json doc = nlohmann::json::parse(raw.toStdString());
if (doc.contains("enabled")) {
data.enabled = doc["enabled"].get<bool>();
}
if (doc.contains("enableDisk")) {
data.enableDisk = doc["enableDisk"].get<bool>();
if (const nlohmann::json* cache = objectIfPresent(doc, "cache")) {
readIfPresent(*cache, "enabled", data.cache.enabled);
readIfPresent(*cache, "enableDisk", data.cache.enableDisk);
readIfPresent(*cache, "maxRamBytes", data.cache.maxRamBytes);
readIfPresent(*cache, "maxDiskBytes", data.cache.maxDiskBytes);
}
if (doc.contains("maxRamBytes")) {
data.maxRamBytes = doc["maxRamBytes"].get<std::uint64_t>();
}
if (doc.contains("maxDiskBytes")) {
data.maxDiskBytes = doc["maxDiskBytes"].get<std::uint64_t>();

if (const nlohmann::json* timeSeries = objectIfPresent(doc, "timeSeries")) {
readIfPresent(*timeSeries, "prefetchEnabled", data.timeSeries.prefetchEnabled);

if (const nlohmann::json* playback = objectIfPresent(*timeSeries, "playback")) {
readIfPresent(*playback, "fps", data.timeSeries.playback.fps);
readIfPresent(*playback, "loop", data.timeSeries.playback.loop);
readIfPresent(*playback, "dropFrames", data.timeSeries.playback.dropFrames);
}
}
} catch (...) {
return defaultSettings();
m_data = defaultSettings();
return;
}

return data;
m_data = data;
}

bool
CacheSettings::save(const CacheSettingsData& data) const
AgaveSettings::save() const
{
nlohmann::json doc;
doc["enabled"] = data.enabled;
doc["enableDisk"] = data.enableDisk;
doc["maxRamBytes"] = data.maxRamBytes;
doc["maxDiskBytes"] = data.maxDiskBytes;
doc["cache"] = {
{ "enabled", m_data.cache.enabled },
{ "enableDisk", m_data.cache.enableDisk },
{ "maxRamBytes", m_data.cache.maxRamBytes },
{ "maxDiskBytes", m_data.cache.maxDiskBytes },
};
doc["timeSeries"] = {
{ "prefetchEnabled", m_data.timeSeries.prefetchEnabled },
{ "playback",
{
{ "fps", m_data.timeSeries.playback.fps },
{ "loop", m_data.timeSeries.playback.loop },
{ "dropFrames", m_data.timeSeries.playback.dropFrames },
} },
};

QString path = QString::fromStdString(configPath());
QFile file(path);
Expand All @@ -130,13 +169,13 @@ CacheSettings::save(const CacheSettingsData& data) const
}

void
CacheSettings::applyToRenderlib(const CacheSettingsData& data) const
AgaveSettings::applyCacheToRenderlib() const
{
// The cache directory (and its writability) is settled once at startup in
// CacheManager::initialize(); if it wasn't writable, the manager left its root
// unset, so a disk-enabled config here is simply honored as RAM-only. We only
// push the runtime tunables.
::CacheConfig config = toRenderlibConfig(data);
::CacheConfig config = toRenderlibConfig(m_data.cache);
LOG_INFO << "Cache config: enabled=" << (config.enabled ? 1 : 0) << " ram_bytes=" << config.maxRamBytes
<< " disk_enabled=" << (config.enableDisk ? 1 : 0) << " disk_bytes=" << config.maxDiskBytes
<< " cache_dir=" << CacheManager::instance().getCacheDirectory();
Expand Down
57 changes: 57 additions & 0 deletions agave_app/AgaveSettings.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#pragma once

#include <cstdint>
#include <string>

struct CacheSettingsData
{
bool enabled = true;
bool enableDisk = true;
std::uint64_t maxRamBytes = 4ULL * 1024ULL * 1024ULL * 1024ULL;
std::uint64_t maxDiskBytes = 100ULL * 1024ULL * 1024ULL * 1024ULL;
};

struct PlaybackSettingsData
{
float fps = 10.0f;
bool loop = true;
// True keeps a steady frame rate by skipping time steps that are not loaded;
// false waits for every one.
bool dropFrames = false;
};

struct TimeSeriesSettingsData
{
// Fill memory and disk with time steps in the background. With this off,
// slider-driven loads are still cached in both tiers. How much gets warmed is
// bounded by the RAM and disk cache limits, so there is no separate depth or
// fill-cache setting.
bool prefetchEnabled = true;
PlaybackSettingsData playback;
};

struct AgaveSettingsData
{
CacheSettingsData cache;
TimeSeriesSettingsData timeSeries;
};

class AgaveSettings
{
public:
AgaveSettings();

void load();
bool save() const;

void applyCacheToRenderlib() const;

AgaveSettingsData& data() { return m_data; }
const AgaveSettingsData& data() const { return m_data; }

AgaveSettingsData defaultSettings() const;
std::string configPath() const;

private:
AgaveSettingsData m_data;
};
8 changes: 6 additions & 2 deletions agave_app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,14 @@ target_sources(agaveapp PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/CameraDockWidget.h"
"${CMAKE_CURRENT_SOURCE_DIR}/CameraWidget.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/CameraWidget.h"
"${CMAKE_CURRENT_SOURCE_DIR}/CacheSettings.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/CacheSettings.h"
"${CMAKE_CURRENT_SOURCE_DIR}/AgaveSettings.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/AgaveSettings.h"
"${CMAKE_CURRENT_SOURCE_DIR}/CacheSettingsDockWidget.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/CacheSettingsDockWidget.h"
"${CMAKE_CURRENT_SOURCE_DIR}/CacheSettingsWidget.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/CacheSettingsWidget.h"
"${CMAKE_CURRENT_SOURCE_DIR}/TimeSlider.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/TimeSlider.h"
"${CMAKE_CURRENT_SOURCE_DIR}/citationDialog.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/citationDialog.h"
"${CMAKE_CURRENT_SOURCE_DIR}/cgiparser.cpp"
Expand Down Expand Up @@ -103,6 +105,8 @@ target_sources(agaveapp PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/streamtestclient.h"
"${CMAKE_CURRENT_SOURCE_DIR}/TimelineDockWidget.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/TimelineDockWidget.h"
"${CMAKE_CURRENT_SOURCE_DIR}/TimeSeriesLoaderBridge.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/TimeSeriesLoaderBridge.h"
"${CMAKE_CURRENT_SOURCE_DIR}/ViewerState.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/ViewerState.h"
"${CMAKE_CURRENT_SOURCE_DIR}/ViewToolbar.cpp"
Expand Down
26 changes: 0 additions & 26 deletions agave_app/CacheSettings.h

This file was deleted.

3 changes: 2 additions & 1 deletion agave_app/CacheSettingsDockWidget.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#include "CacheSettingsDockWidget.h"

CacheSettingsDockWidget::CacheSettingsDockWidget(QWidget* parent)
CacheSettingsDockWidget::CacheSettingsDockWidget(QWidget* parent, AgaveSettingsData* settings)
: QDockWidget(parent)
, m_settingsWidget(this, settings)
{
setWindowTitle(tr("Advanced Cache Settings"));
setWidget(&m_settingsWidget);
Expand Down
2 changes: 1 addition & 1 deletion agave_app/CacheSettingsDockWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class CacheSettingsDockWidget : public QDockWidget
Q_OBJECT

public:
explicit CacheSettingsDockWidget(QWidget* parent = nullptr);
explicit CacheSettingsDockWidget(QWidget* parent = nullptr, AgaveSettingsData* settings = nullptr);

CacheSettingsWidget* widget() { return &m_settingsWidget; }

Expand Down
52 changes: 38 additions & 14 deletions agave_app/CacheSettingsWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

#include <QFormLayout>

CacheSettingsWidget::CacheSettingsWidget(QWidget* parent)
CacheSettingsWidget::CacheSettingsWidget(QWidget* parent, AgaveSettingsData* settings)
: QWidget(parent)
, m_settings(settings)
{
auto* layout = new QFormLayout(this);

Expand All @@ -31,32 +32,55 @@ CacheSettingsWidget::CacheSettingsWidget(QWidget* parent)
m_applyButton = new QPushButton(tr("Apply"), this);
m_clearDiskButton = new QPushButton(tr("Clear disk cache"), this);

m_prefetchEnabled = new QCheckBox(tr("Prefetch time steps"), this);
m_prefetchEnabled->setToolTip(
tr("Fill memory and disk with time steps ahead of the current one, in the background. How far ahead is bounded by "
"the RAM and disk limits above."));
m_prefetchEnabled->setStatusTip(
tr("Fill memory and disk with time steps ahead of the current one, in the background"));

connect(m_enableCache, &QCheckBox::toggled, this, [this](bool) { writeToSettings(); });
connect(m_enableDisk, &QCheckBox::toggled, this, [this](bool) { writeToSettings(); });
connect(m_ramLimitMB, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int) { writeToSettings(); });
connect(m_diskLimitGB, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int) { writeToSettings(); });
connect(m_prefetchEnabled, &QCheckBox::toggled, this, [this](bool) { writeToSettings(); });

layout->addRow(m_enableCache);
layout->addRow(m_enableDisk);
layout->addRow(tr("RAM limit"), m_ramLimitMB);
layout->addRow(tr("Disk limit"), m_diskLimitGB);
layout->addRow(tr("Cache directory"), m_cacheDirLabel);
layout->addRow(new QLabel(tr("<b>Prefetch</b>"), this));
layout->addRow(m_prefetchEnabled);
layout->addRow(QString(), m_applyButton);
layout->addRow(QString(), m_clearDiskButton);
setLayout(layout);
}

void
CacheSettingsWidget::setSettings(const CacheSettingsData& data)
CacheSettingsWidget::updateUiFromSettings()
{
m_enableCache->setChecked(data.enabled);
m_enableDisk->setChecked(data.enableDisk);
m_ramLimitMB->setValue(static_cast<int>(data.maxRamBytes / (1024ULL * 1024ULL)));
m_diskLimitGB->setValue(static_cast<int>(data.maxDiskBytes / (1024ULL * 1024ULL * 1024ULL)));
if (!m_settings) {
return;
}
m_refreshingSettings = true;
m_enableCache->setChecked(m_settings->cache.enabled);
m_enableDisk->setChecked(m_settings->cache.enableDisk);
m_ramLimitMB->setValue(static_cast<int>(m_settings->cache.maxRamBytes / (1024ULL * 1024ULL)));
m_diskLimitGB->setValue(static_cast<int>(m_settings->cache.maxDiskBytes / (1024ULL * 1024ULL * 1024ULL)));
m_prefetchEnabled->setChecked(m_settings->timeSeries.prefetchEnabled);
m_refreshingSettings = false;
}

CacheSettingsData
CacheSettingsWidget::getSettings() const
void
CacheSettingsWidget::writeToSettings()
{
CacheSettingsData data;
data.enabled = m_enableCache->isChecked();
data.enableDisk = m_enableDisk->isChecked();
data.maxRamBytes = static_cast<std::uint64_t>(m_ramLimitMB->value()) * 1024ULL * 1024ULL;
data.maxDiskBytes = static_cast<std::uint64_t>(m_diskLimitGB->value()) * 1024ULL * 1024ULL * 1024ULL;
return data;
if (!m_settings || m_refreshingSettings) {
return;
}
m_settings->cache.enabled = m_enableCache->isChecked();
m_settings->cache.enableDisk = m_enableDisk->isChecked();
m_settings->cache.maxRamBytes = static_cast<std::uint64_t>(m_ramLimitMB->value()) * 1024ULL * 1024ULL;
m_settings->cache.maxDiskBytes = static_cast<std::uint64_t>(m_diskLimitGB->value()) * 1024ULL * 1024ULL * 1024ULL;
m_settings->timeSeries.prefetchEnabled = m_prefetchEnabled->isChecked();
}
Loading
Loading