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
cbe4d6d
factor context creation out so to remove the qt dependency
toloudis Jun 28, 2026
d03e26b
add plan
toloudis Jul 30, 2026
8a65913
async volume loading API + reader per-timestep efficiency
toloudis Jul 30, 2026
ef17af8
report cache statistics in the GUI statistics panel
toloudis Jul 30, 2026
2f344d0
cache pinning and eviction notification
toloudis Jul 30, 2026
c287abb
add TimeSeriesLoader: async time-series loading with forward prefetch
toloudis Jul 30, 2026
068761a
make time stepping asynchronous in the GUI
toloudis Jul 30, 2026
0bace38
treat a channel-count mismatch between time steps as an error
toloudis Jul 30, 2026
4ffb9a6
add time-series playback: play/pause, stop, frame rate, loop
toloudis Jul 30, 2026
630dd0f
show cached time steps as a strip along the time slider
toloudis Jul 31, 2026
2af1548
expose prefetch and playback settings in the UI
toloudis Jul 31, 2026
afd1022
fix crash on accepting the load dialog for a time series
toloudis Jul 31, 2026
51eea23
fix playback stalling at the end of the cached run
toloudis Jul 31, 2026
14b8ceb
make prefetch use the disk cache, and write to it off the hot path
toloudis Jul 31, 2026
0192ef3
make prefetch wrap so looping playback works
toloudis Jul 31, 2026
b67c21a
clamp the prefetch window to what the cache can hold
toloudis Jul 31, 2026
74cf910
read zarr channels concurrently and skip the scratch copy for 16-bit …
toloudis Jul 31, 2026
b991508
speed up the vulkan volume upload path
toloudis Jul 31, 2026
84c1cbf
update implementation status with phases 3 and 11 and the bugs found …
toloudis Jul 31, 2026
8113f6d
fix volume rotating the wrong way in vulkan raymarch orthographic mode
toloudis Jul 31, 2026
19dfc02
reuse the file reader across SetTime commands
toloudis Jul 31, 2026
55ab29c
record phase 10, the shader fix, and why phase 12 is gated on measure…
toloudis Jul 31, 2026
30e5267
Revert "fix volume rotating the wrong way in vulkan raymarch orthogra…
toloudis Jul 31, 2026
6b299b6
Revert "speed up the vulkan volume upload path"
toloudis Jul 31, 2026
f57e7d4
Reapply "fix volume rotating the wrong way in vulkan raymarch orthogr…
toloudis Jul 31, 2026
7e04446
Revert "Reapply "fix volume rotating the wrong way in vulkan raymarch…
toloudis Jul 31, 2026
682f876
Reapply "Reapply "fix volume rotating the wrong way in vulkan raymarc…
toloudis Jul 31, 2026
cd6f706
Revert "Reapply "Reapply "fix volume rotating the wrong way in vulkan…
toloudis Jul 31, 2026
cf199a1
fix volume mirrored vertically in vulkan raymarch orthographic mode
toloudis Jul 31, 2026
b6724c1
Reapply "speed up the vulkan volume upload path"
toloudis Jul 31, 2026
93e9666
stop prefetch looping forever; warm memory near, disk far
toloudis Aug 1, 2026
a7f8f5a
paint the DiskCached state on the time slider strip
toloudis Aug 1, 2026
6ed3fcd
warm the disk cache without pushing time steps through memory
toloudis Aug 1, 2026
5469b13
docs: settle the time-series caching design and plan its implementation
toloudis Aug 1, 2026
2cad7b3
reserve disk cache space for queued writes and stop dropping them
toloudis Aug 1, 2026
8aa88f3
report disk cache evictions so a dropped entry stops reporting as cached
toloudis Aug 1, 2026
dc4607a
retire prefetch depth and fillCache, and stop RAM-throttling disk war…
toloudis Aug 1, 2026
977cffd
size the memory window to the RAM budget and clamp the disk warm set
toloudis Aug 1, 2026
21b9791
recognise a warm disk cache without dragging it through memory
toloudis Aug 1, 2026
35ff77b
report pending disk-write bytes, and record the implementation outcome
toloudis Aug 1, 2026
a4c8fe4
stop prefetch wanting the whole series before the frame size is known
toloudis Aug 1, 2026
818014c
make the load dialog prefetch checkbox actually turn prefetch off
toloudis Aug 1, 2026
d035685
load the frame playback is waiting for when prefetch is not doing it
toloudis Aug 1, 2026
5a031c9
fix prefetch checkbox
toloudis Aug 1, 2026
f97cb8f
the main change here is the zarr loader is made more cancellable whic…
toloudis Aug 2, 2026
0904f12
slider fixup
toloudis Aug 2, 2026
cfc4117
a little more testing and cleanup
toloudis Aug 2, 2026
28ecee8
gui cleanup
toloudis Aug 2, 2026
43f2aa5
simplify excess comments for obvious code
toloudis Aug 2, 2026
548d588
restructure settings
toloudis Aug 2, 2026
8101081
rename time slider
toloudis Aug 2, 2026
6143185
cleanup
toloudis Aug 2, 2026
164b0a8
cleanup
toloudis Aug 3, 2026
2b79a00
cmake policy
toloudis Aug 3, 2026
876ef38
remove dead code
toloudis Aug 3, 2026
f42be59
fix policy
toloudis Aug 3, 2026
eced6e9
remove stop button
toloudis Aug 3, 2026
e83a969
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 .github/agents/reviewer.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ When invoked, determine what to review:
### Architecture

- `renderlib/` must not depend on Qt. Flag any `Q*` types, `QObject`, signals/slots, or Qt headers leaking into `renderlib/`.
- `renderlib/gfxOpenGL/` and `renderlib/gfxVulkan/` are the only places that should contain graphics API calls. Flag any OpenGL or Vulkan calls outside those directories.
- GUI logic belongs in `agave_app/`. Rendering, I/O, scene, and serialization belong in `renderlib/`. It is preferable to have anything not necessary for the GUI to be pushed down into `renderlib/`.
- New commands must be added to **all** of: `renderlib/command.h`, `renderlib/command.cpp`, `agave_app/commandBuffer.cpp`, `test/test_commands.cpp`, `agave_pyclient/agave_pyclient/commandbuffer.py`, `agave_pyclient/agave_pyclient/agave.py`, `webclient/src/commandbuffer.ts`, `webclient/src/agave.ts`. Verify the integer ID is unique and consistent, and that the argument list/order matches across all locations.

Expand Down
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