fix: a recompute that renumbered its ring, and readouts frozen under a zoom
Three defects, all of them a window describing data that is not there. A math edit refilled the derived ring with `clear()`, which renumbers it from 0, leaving `produced()` short by the inputs' retained base. Since `live_window_locked()` clamps against every channel's `produced()`, that one lagging channel answered `n = 0` for the live view, every pause view and every trigger capture until a whole ring of fresh data had gone by -- the reported freeze, invisible until the ring first wraps, hence its intermittence. It now restarts with `clear_to(base)`, and `ChannelBuffer::republish_from(base)` puts the published window back: `acquire()` is incremental, so a rewrite in place published nothing and the pyramid went on describing the samples the edit replaced. The trigger's channel is an index, re-resolved by name after a renumbering -- but only in `rebuild_enabled_locked()`, so a math add or delete went through `set_math_channels()` with no restore, the index was clamped to -1, and `process()` returned at once while the panel still named the channel. The restore moves into `rebuild_channels()`, which *is* the renumbering, so no caller can forget it. And `update_measures()` suppressed its "the window moved" test while an event was held -- the one mode where a gesture is the only thing that can move it. Zooming a latched event left the pane describing the whole captured event. A pinned window needs no exemption: it lands on the range it already measured and fails the 2 % test by itself. The checks that missed all three: `UDPSCOPE_PROBE_MATHWRAP_MS` edits a graph whose ring has already wrapped, which `math_probe` structurally cannot reach; `[measure-trace]` now prints the mode, the window on screen and the window the numbers describe; and the dump's `marks_in_window` counts against the window on screen instead of against the one the marks were computed over, where it was a tautology. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
074cb611bc
commit
d91957a2fe
15 files changed
+374
-45
No files matched your search
+37
-18
@@ -449,6 +449,14 @@ void ScopeModel::recompute_math_locked(const std::vector<bool>& affected) {
|
||||
const std::size_t slots = static_cast<std::size_t>(store.time().size());
|
||||
if (slots == 0) return;
|
||||
|
||||
// The refill replays the inputs' *retained* window, which starts at this absolute index and not at
|
||||
// zero, so an emptied derived ring has to resume its count there: `clear()` would renumber it from 0
|
||||
// and leave its `produced()` short by `base` for a whole ring's worth of fresh data. `live_window_locked()`
|
||||
// clamps the window against every channel's `produced()`, so that one lagging channel collapses the
|
||||
// live view, every pause view and every trigger capture to nothing -- the "acquisition freezes after a
|
||||
// math edit" defect, invisible until the ring has wrapped once (`base == 0` before that).
|
||||
const std::size_t base = static_cast<std::size_t>(store.time().base());
|
||||
|
||||
std::vector<bool> derived(channels_.size(), false);
|
||||
for (std::size_t m = 0; m < math_.size(); ++m) {
|
||||
const int channel = m < math_channel_.size() ? math_channel_[m] : -1;
|
||||
@@ -456,14 +464,18 @@ void ScopeModel::recompute_math_locked(const std::vector<bool>& affected) {
|
||||
derived[static_cast<std::size_t>(channel)] = true;
|
||||
// Only the affected channels start again. The rest keep their samples *and* their runtimes: their graphs
|
||||
// are untouched, so the state they hold is the answer to the inputs they have always had.
|
||||
if (affected[m]) store.values(static_cast<std::size_t>(channel)).clear();
|
||||
if (!affected[m]) continue;
|
||||
store.values(static_cast<std::size_t>(channel)).clear_to(base);
|
||||
// And the published window with them: this is a rewrite in place, so `produced()` lands back where it
|
||||
// started and the incremental `acquire()` would publish nothing at all -- leaving the pyramid, which is
|
||||
// what the renderer and every measurement read, describing the samples the edit just replaced.
|
||||
store.channel(static_cast<std::size_t>(channel)).republish_from(base);
|
||||
}
|
||||
|
||||
// One bulk copy per channel per chunk, through the ring's own reader. A derived channel is fed a zero:
|
||||
// `math_apply_locked()` overwrites its value as it walks the list in order, and reading its half-rebuilt
|
||||
// ring here would read out of range.
|
||||
constexpr std::size_t kChunk = 1u << 14;
|
||||
const std::size_t base = static_cast<std::size_t>(store.time().base());
|
||||
std::vector<std::vector<f32>> inputs(channels_.size());
|
||||
for (std::size_t c = 0; c < channels_.size(); ++c) {
|
||||
if (!derived[c]) inputs[c].resize(kChunk);
|
||||
@@ -482,10 +494,22 @@ void ScopeModel::recompute_math_locked(const std::vector<bool>& affected) {
|
||||
math_apply_locked(slot, &affected, /*timed=*/false);
|
||||
}
|
||||
}
|
||||
// The pyramid is the consumer's and the tick publishes it; the samples are written here.
|
||||
// The pyramid is the consumer's and the tick publishes it (`republish_from()` above put its window
|
||||
// back at `base` so that publish is one window's worth of work); the samples are written here.
|
||||
}
|
||||
|
||||
void ScopeModel::rebuild_channels() {
|
||||
// The trigger names a channel by its display name, which is stable across the renumbering this
|
||||
// function *is*; the index is not. Captured and restored here rather than at the call sites
|
||||
// because every caller renumbers and every caller needs it: adding a math channel appends a
|
||||
// signal and shifts nothing, but removing one, or a CONFIG that reorders the stream's own
|
||||
// signals, moves every index after it. `rebuild_locked()` then only clamps an out-of-range
|
||||
// index to -1, and `TriggerEngine::process()` returns immediately on a channel of -1 -- so the
|
||||
// panel went on naming the channel while nothing could ever fire again.
|
||||
const std::string trigger_name =
|
||||
(trigger_.channel >= 0 && trigger_.channel < static_cast<int>(channels_.size()))
|
||||
? channels_[static_cast<std::size_t>(trigger_.channel)].name
|
||||
: std::string();
|
||||
channels_.clear();
|
||||
// Which channel each math channel ended up with, by name: a disabled one has none, and the
|
||||
// producer must not push a derived sample into another channel's ring.
|
||||
@@ -524,6 +548,15 @@ void ScopeModel::rebuild_channels() {
|
||||
}
|
||||
channels_.push_back(std::move(ch));
|
||||
}
|
||||
|
||||
// A name that is no longer a channel -- its signal was disabled, or the stream stopped
|
||||
// declaring it -- leaves the trigger unassigned, which is a wait rather than a silent
|
||||
// mis-aim; re-enabling the signal brings it back here.
|
||||
trigger_.channel = -1;
|
||||
if (trigger_name.empty()) return;
|
||||
for (std::size_t i = 0; i < channels_.size(); ++i) {
|
||||
if (channels_[i].name == trigger_name) trigger_.channel = static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
|
||||
void ScopeModel::rebuild_locked() {
|
||||
@@ -639,22 +672,8 @@ bool ScopeModel::enable_locked(std::size_t signal, bool on) {
|
||||
}
|
||||
|
||||
void ScopeModel::rebuild_enabled_locked() {
|
||||
// The trigger names a channel by its display name, which is stable across
|
||||
// renumbering: when its own signal is disabled the channel vanishes (the trigger
|
||||
// then waits, and comes back when the signal is re-enabled). Read before
|
||||
// `rebuild_channels()`, which is what renumbers.
|
||||
const int old_trigger = trigger_.channel;
|
||||
const std::string trigger_name =
|
||||
(old_trigger >= 0 && old_trigger < static_cast<int>(channels_.size()))
|
||||
? channels_[static_cast<std::size_t>(old_trigger)].name
|
||||
: std::string();
|
||||
// `rebuild_channels()` carries the trigger's channel across the renumbering by name.
|
||||
rebuild_channels();
|
||||
trigger_.channel = -1;
|
||||
if (!trigger_name.empty()) {
|
||||
for (std::size_t i = 0; i < channels_.size(); ++i) {
|
||||
if (channels_[i].name == trigger_name) trigger_.channel = static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
rebuild_locked();
|
||||
generation_.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user