fix(data): draw the partial edge buckets at LOD levels, correct the pyramid's memory and precondition docs
Findings from the review of the sliding-window change: - A level >= 1 gather emitted only complete, wholly-retained buckets, so the geometry stopped at the last bucket boundary inside the range: the newest samples of a live window -- up to `span - 1` of them, about a pixel at the level level_for() picks -- were not drawn, on the right edge, which is the part of a scrolling trace that gets watched, and the oldest retained samples fell outside the geometry too (the level-1 gather of a sliding window started 51 samples in). The two partial edge buckets are now gathered as raw samples through copy_raw() and bracketed around the envelope -- the same exactness split reduce/sum/sumsq already use -- so the emitted geometry spans the requested range exactly. The pyramid still stores no partial bucket, so append stays amortised O(1); at 1 MS/s the demo stages 511 extra sub-pixel segments and still draws at 1.05 ms/frame, and the screenshot's trace now reaches one column further at each end. - `drop_before`'s doc claimed a no-op "for an empty pyramid": false, and backwards -- the restart branch is exactly what an empty pyramid hits, which is what StreamSeries::acquire() depends on when the ring saturated before the first acquire. The precondition is `absolute_sample <= window_base()`, and the restart is documented as what it is. - The pyramid's memory figure was 3x optimistic: an entry is 24 bytes (MinMax plus the exact sum/sumsq doubles), so level 0 costs 0.375n bytes and the whole pyramid ~0.75n -- about 19% of 4-byte raw samples, not the 6% a min/max-only index would cost. Corrected in the header, spec 4.2 and 4.5, with the trade stated (the doubles buy O(log n) sum/mean/RMS/integral from whole buckets), and the plan's Task 3 sketch no longer describes per-level prefix sums, a level() span or an unaligned-end widening that does not exist. Tests: `gather_at_a_lod_level_reaches_both_ends_of_the_window` pins that the first and last emitted points are the oldest and newest retained samples -- with spike values no bucket envelope can produce -- at level 1, at the level a dense view picks, and for a static series whose end is unaligned. It fails against the previous commit (first point at the first whole bucket), and so does the sliding gather mapping check, which now accounts for the raw edge pairs.
This commit is contained in:
1 parent
c8a97e4a1b
commit
fb2c2f5e4d
6 files changed
+199
-56
No files matched your search
@@ -102,8 +102,14 @@ separate code path or pre-transformed buffer is needed.
|
||||
### 4.2 Min/max LOD pyramid
|
||||
|
||||
Every series owns a mip chain: level 0 buckets `kBucket = 64` consecutive samples
|
||||
and stores one `(min, max)` pair per bucket; each subsequent level halves the
|
||||
bucket count. Memory cost is `n/8` bytes at level 0 (2 floats per 64 samples) times the geometric sum `1 + ½ + ¼ + …`, giving `n/4` bytes total — ≈6% of the 4n bytes of raw samples, not 2×.
|
||||
and stores one entry per bucket; each subsequent level halves the bucket count.
|
||||
Each entry is 24 bytes — a `MinMax` pair (8 bytes) plus the exact `sum` and `sumsq`
|
||||
doubles the range queries fold over (16 bytes) — so level 0 costs
|
||||
`24n/64 = 0.375n` bytes and the geometric sum `1 + ½ + ¼ + …` brings the pyramid to
|
||||
≈`0.75n` bytes, about 19% of the `4n` bytes of raw samples, not 2×. A min/max-only
|
||||
index would be `n/4`, ≈6%: the two doubles are the trade that makes an interior
|
||||
sum/mean/RMS/integral O(log n) whole-bucket additions instead of a scan of raw
|
||||
samples, at every level, for every series that uses the measurement index.
|
||||
|
||||
- Built incrementally, in **absolute bucket space**: level `l` bucket `k` covers
|
||||
samples `[k · (kBucket << l), (k + 1) · (kBucket << l))`. A bucket's span
|
||||
@@ -158,7 +164,7 @@ is `reduce(0, size())` over the window rather than a scan of it.
|
||||
|
||||
| | Approach | Verdict |
|
||||
|---|---|---|
|
||||
| **A** | **CPU min/max pyramid, incremental** | **chosen** — works on GL 3.3, deterministic, unit-testable with no GPU, O(1) amortised per sample, and it doubles as the measurement index. Cost: one CPU pass per sample, plus ≈6% memory. |
|
||||
| **A** | **CPU min/max pyramid, incremental** | **chosen** — works on GL 3.3, deterministic, unit-testable with no GPU, O(1) amortised per sample, and it doubles as the measurement index. Cost: one CPU pass per sample, plus ≈19% memory (24-byte buckets: extremes plus exact per-bucket sums). |
|
||||
| B | GPU compute decimation every frame | rejected — re-reads the whole series each frame (real bandwidth at 1M samples/frame), no 3.3 fallback, untestable headlessly. |
|
||||
| C | no decimation | rejected — dies above ~500k samples. |
|
||||
|
||||
|
||||
Reference in new issue
Block a user