Ship raw channel values; map domains in the shader (protocol v7)#187
Conversation
Continuous color/size channels shipped pre-normalized [0,1] f32, so any domain change - streaming growth, a colormap re-bind - invalidated every encoded value and forced an O(N) re-encode and re-ship. Now they ship raw data-unit f32 with enc:"raw" and the client maps them through the spec domain in the vertex shader ((v - d0) * 1/span into the LUT / px-range lookup); a domain change becomes a uniform update. This is the prerequisite for O(K) delta appends: a grown channel domain no longer touches shipped bytes. - New native kernel xy_sanitize_f32 (ABI v38): fused f64->f32 cast with non-finite scrubbed to the domain floor, keeping the §19 invariant (NaN never reaches a vertex buffer) without CPU normalization. The shader maps the floor to LUT coordinate 0 - the legacy visual for NaN rows. - f32-hostile domains (magnitude beyond f32, degenerate span) fall back to the legacy unit encode with no enc marker; the client's identity map draws both encodings pixel-identically. - Client: u_cvalMap/u_svalMap uniforms in every continuous-channel program (points, segments, mesh, rect, bar, pick), derived per bound channel spec (first paint, drill, sample overlay); standalone hover readouts return exact data units for raw buffers and denormalize only unit-encoded ones (heatmap grids unchanged). - Protocol bumps to v7: a pre-v7 bundle would clamp raw values as unit coordinates and render wrong colors silently - the class of failure the first-paint handshake exists to catch. Verified pixel-identical in a browser probe: the same 4k-point continuous-color+size scatter rendered from the raw wire and from a hand-built legacy unit wire differs in 0 of 541k pixels (max channel delta 0). Full suite 2200 passed; abi_smoke 123 checks.
Merging this PR will not alter performance
Comparing Footnotes
|
Greptile SummaryThis PR moves continuous channel mapping to the shader and adds delta-based streaming appends. The main changes are:
Confidence Score: 5/5No new blocking issue qualifies for this follow-up review. No distinct incomplete fix, unsafe fix, or separate paired failure was found in the latest changes.
What T-Rex did
Important Files Changed
|
* Streaming appends ship O(K) delta frames (protocol v8) A direct-tier append still re-shipped the whole affected trace: appending one point to a 100k scatter cost ~783 KiB. Now the kernel emits an append_rows delta - only the K new rows cross the wire - when the shipped representation is a pure tail extension of what the client holds: - Kernel: _append_rows_message checks strict, recorded eligibility (direct tier, no tier flip, no keyed/configured animation, no per-point style channels, no step/smooth expansion, no log axes, fully-finite rows, raw-encoded channels) and an offset-drift budget: tails encode with the offsets the client holds, capped at 1024x the encoded half-span recorded at the last full ship (~0.25 px worst case at 2048 px), after which one full re-ship re-centers like deep zoom (§4/§16). Every fallback reason rides the full message as append.delta_fallback (§28). - Client: _applyAppendRows extends GPU buffers (capacity-doubling bufferSubData) and retained CPU spans (same doubling - context restore stays whole) in place, advances spec metas, applies grown channel domains as uniform updates (the v7 raw wire is what makes this possible), keeps enabled attribute ranges covering n (selection mask zero-fill), and follows append's home/live-edge/history policy from the fresh axis ranges. prev_marks and encode params must match exactly; any disagreement sends one refresh instead of writing a torn tail. - Fixed in passing: appending to a trace whose x and y alias one deduplicated canonical column (scatter(v, v)) silently interleaved both axes into it; now rejected like cross-trace sharing, with a test. Protocol bumps to v8 (a pre-v8 bundle would drop delta frames on the floor mid-stream). Measured (median/tick, data-level): direct scatter 100k appending 1 pt: 783 KiB -> 383 bytes (~2000x); 10x50k dashboard: 397 KiB -> ~0.4 KiB; kernel build 0.20 -> 0.14 ms. Decimated/density tiers unchanged by design - their full payload is already O(pixels). Closes #163 for the streaming path. * fix(client): append_rows keeps the selection mask and never writes a torn tail Two review findings on the delta apply path: - Growing selBuf re-uploaded an all-zero prefix on every capacity realloc (including the first delta, since _applySelMask never recorded a cap), visually clearing the live selection; and a selection reply's bufferData shrank the allocation behind a recorded cap, so the next delta's bufferSubData wrote past the end (GL_INVALID_VALUE) and left the attrib buffer smaller than n. _applySelMask now retains the mask as a CPU mirror and records the true allocation size; the delta grow path extends the mirror with zeros and re-uploads it on realloc. - The role loop validated each column only after earlier roles were already extended, so a bad y/channel reference left x's meta.len advanced against an unchanged g.n — a torn tail the pending refresh could not un-write. All roles (including tail byte length vs len*bpv) now validate before any state mutates. Verified with a browser probe against real kernel payloads: pre-fix bundle wipes the mask, raises GL 1281, and over-extends x after a bad delta; fixed bundle preserves the mask across both grow paths, refuses the bad delta with exactly one refresh and no mutation, and applies the next valid delta cleanly.
Stacked on #186 (base:
feat/append-skip-unchanged). Second PR of the streaming-append stack; prerequisite for the O(K) delta-append frames PR.What
Continuous color/size channels shipped pre-normalized
[0,1]f32, so any domain change — streaming growth, a colormap re-bind — invalidated every encoded value and forced an O(N) re-encode and re-ship. Now:enc: "raw"; the client maps them through the specdomainin the vertex shader ((v - d0) * 1/spanfeeding the existing LUT / px-range lookup, newu_cvalMap/u_svalMapuniforms in every continuous-channel program: points, segments, mesh, rect, bar, pick). A domain change is now a uniform update.xy_sanitize_f32(ABI v38) does one fused f64→f32 cast with non-finite scrubbed to the domain floor — the shader maps the floor to LUT coordinate 0, the legacy visual for NaN rows.encmarker; the client's identity map draws both encodings through the same shader path.channels.ship_continuous) and the same client map derivation, so every ship site agrees. Standalone hover readouts now return exact data units for raw buffers (previously denormalized approximations); heatmap grids are unchanged (that's Heatmaps ship 4-16 bytes/cell for what the client uploads as 8-bit textures #164).Why it matters for the stack
With unit encoding, appending rows that extend a channel's domain (a new max temperature, say) re-encodes all N channel values. With raw encoding the shipped bytes are domain-independent — the delta-append PR can ship only the K new rows and update the domain uniform.
Verified
cargo test104 passed (new fuzz test forsanitize_f32_into);abi_smoke123 checks (2 new for the export); ruff / format / pre-commit /ty(25 pre-existing, none new);node js/build.mjs.Spec
wire-protocol.md§5 (raw-channel encode contract) + §7 (v7 note); ABI bump insrc/lib.rs+_native.pyper the documented procedure. CHANGELOG under Unreleased → Changed.🤖 Generated with Claude Code