| name | architecture |
|---|---|
| description | How JBrowse renders a track — display stacks, the worker→main fetch pipeline, SVG export, and the invariants. Read when touching a display or its data flow; the render backend itself is in reference/GPU_RENDERING.md. |
The canonical reference for how JBrowse renders a track. Read the TL;DR for the mental model, then jump to the section for whatever you're touching. Deep subsystems that come up only on a specific task live in their own docs, collected under See also at the end.
- Adapters fetch and parse in an RPC worker; the main thread renders. Worker output is absolute genomic uint32 — never pixels, never region-relative.
- A display is an MST model.
attachRenderingBackend(backend, { upload, render })spawns two MobX autoruns: upload bytes on data change, redraw on any visible change. Pan/zoom is a redraw, not a refetch. - Rendering picks WebGPU → WebGL2 → Canvas2D at runtime behind the HAL. A Canvas2D draw fn is the floor for canvas-based displays because SVG export runs it; the shader path is an optional accelerator.
- Two fetch foundations cover every LGV display:
MultiRegionDisplayMixin(per region, its own autoruns) andGlobalFetchMixin(one dataset, display installs its own autorun). The non-LGV views (synteny, dotplot) compose neither and hand-roll fetch, debounce and staleness. DisplayChromeowns every terminal state — loading, error, render error, region-too-large — via the singledisplayPhasegetter.- Shaders are
.slangcompiled bypnpm gen:shaders. Never hand-edit*.generated.ts. rpcProps()= user settings that invalidate the fetch. Putting a fetch result in it is an infinite loop; see the trap.
A display is the object that draws one track inside a view — the pileup in an alignments track, the bars in a wiggle track, the matrix in a Hi-C track. Whatever it draws, it follows the same shape: a worker fetches and parses off the UI thread, the main thread uploads the result once and then redraws it every frame, and the frame goes through whichever of three interchangeable backends the runtime picked.
worker: adapter → features (absolute uint32 bp)
│ RPC, off the UI thread
▼
main: model.rpcDataMap (MST node, observable)
│ upload autorun — fires when the data changes
▼
GPU buffers (HAL: WebGPU → WebGL2 → Canvas2D)
│ render autorun — fires when anything visible changes
▼
<canvas> on screen
SVG export reuses the same Canvas2D draw fn — never the shader.
Every canvas-drawing display must provide a Canvas2D draw function; the GPU shader path is an optional accelerator layered on top. Because SVG export runs the Canvas2D path, on-screen and exported pixels can't drift. Arc is the one non-canvas class — it paints JSX SVG on both paths; see Display stacks.
Terms used throughout this doc:
- Display — the subject of most of this doc, defined above. Composed from MST mixins that supply its behavior: fetch, render lifecycle, height.
- Backend — the per-display object that actually draws, either GPU
(
GpuXxxRenderer) or Canvas2D (Canvas2DXxxRenderer), produced by a factory that picks one at runtime. - Region / block — the visible genome is split into regions
(
view.displayedRegions) and finer render blocks; a display fetches and draws per region.displayedRegionIndexis the join key between the model's data map and the GPU buffers. - HAL — hardware abstraction layer; hides the WebGPU-vs-WebGL2 difference.
- RPC / worker — the off-thread context where adapters fetch and parse data.
- MST model / autorun — a display is a
mobx-state-treenode;autorunis the MobX primitive that re-runs a function whenever the observables it read change.
JBrowse uses 0-based half-open intervals [start, end) internally, matching
BED/BAM. Worker output is absolute genomic uint32 — no regionStart-relative
arithmetic crosses the worker boundary. The precision machinery that makes this
work on a float32 GPU is in reference/BP_PRECISION.md.
The hand-written walkthroughs in website/docs/developer_guides/ —
plotting_features.md
(Canvas2D),
creating_gpu_display.md
(GPU), and
data_fetching.md
— turn the sections below into step-by-step tutorials and link back to them. When
the lifecycle, mixins, or upload patterns here change, update those guides in
the same pass. pnpm lint-docs-check (which runs
website/scripts/check-doc-imports.ts) validates the cross-links both ways but
not the prose.
Which mixins do you compose to build a display, and why? Linear-genome-view
displays are built from a small set of foundation mixins on BaseDisplay,
all sharing baseLinearDisplayConfigSchema as their config base. Which mixins a
display composes is the primary axis of code sharing; how it renders (GPU vs
Canvas2D) is a separate axis layered on top. Two fetch foundations — per-region
(MultiRegionDisplayMixin) and single-global (GlobalFetchMixin) — cover every
display that lives in an LGV:
Foundation (composed on BaseDisplay) |
Brings | Displays |
|---|---|---|
MultiRegionDisplayMixin() |
RenderLifecycleMixin + FetchMixin + RegionTooLargeMixin + the fetch autoruns + rpcProps()→refetch wiring |
LinearWiggleDisplay, MultiLinearWiggleDisplay, LinearManhattanDisplay, LinearAlignmentsDisplay, both multi-sample variant displays, LinearReferenceSequenceDisplay, LinearMafDisplay, LinearMultiRowFeatureDisplay, and via LinearCanvasBaseDisplay the LinearBasicDisplay / LinearVariantDisplay pair |
GlobalDataDisplayMixin() = GlobalFetchMixin() + RenderLifecycleMixin |
the single-global fetch foundation plus GPU render lifecycle and displayPhase. No fetch autoruns: each display installs its own afterAttach autorun via installGlobalFetchAutorun |
HiC (LinearHicDisplay), LD (plugins/variants/src/LDDisplay) |
GlobalFetchMixin() bare (via arc's ArcFetchModel) + main-thread SVG render |
the same fetch foundation (RegionTooLargeMixin + FetchMixin + reloadCounter) with no RenderLifecycleMixin — a non-GPU display shouldn't drag in the render lifecycle to get fetch/cancel/too-large/reload |
LinearArcDisplay, LinearPairedArcDisplay |
GlobalFetchMixin is the rendering-agnostic fetch foundation shared by the last
two rows: GPU global displays layer RenderLifecycleMixin on top of it
(GlobalDataDisplayMixin), while arc composes it bare and paints main-thread SVG.
displayPhase lives in GlobalDataDisplayMixin, not GlobalFetchMixin, because
it reads renderError — the one genuinely GPU-only piece. RegionTooLargeMixin's
gate is derived and opt-in; arc's ArcFetchModel enables it like every other
byte-gated display (see the region-too-large
gate).
The non-LGV views are a third shape, not a row in that table. Multi-LGV
synteny (LinearSyntenyDisplay) composes only BaseDisplay and owns its fetch;
dotplot puts RenderLifecycleMixin at the view level. Neither gets
FetchMixin's cancel/stale machinery, RegionTooLargeMixin or loadedRegions,
so each re-implements a fetching flag and its own leading-edge debounce
(leadingEdgeDebounce, the same scheduler installGlobalFetchAutorun uses).
They do answer the
shared dataCurrent freshness question and run the shared computeSvgReady
policy, just via a signature compare (isDataCurrent over dotplotFetchKey /
synteny's currentFetchKey) rather than spatial coverage — which is where the
stale-capture bugs lived
(reference/SVG_EXPORT.md §"On-screen capture gate").
The remaining duplication is the fetch state machine itself, not freshness.
Both scope their fetch through the shared syntenyFetchRegions
(@jbrowse/synteny-core): the visible blocks widened by a pan buffer and
snapped to a buffer-sized grid, so a pan inside the buffer neither refetches nor
exposes an unfetched strip, and the freshness key stays stable across the
gesture. Synteny scopes its query axis, dotplot its h axis; neither scopes the
other axis, because the fetch is one-dimensional in both.
Both put their RenderLifecycleMixin on the view, so one canvas is shared by
every display in it. That is what makes their upload callbacks keyed rather than
per-region: they diff through createKeyedUploadSync and delete each departed
key individually, because an active-set prune computed from one display's map
would wipe its siblings' buffers.
Circular view's ChordVariantDisplay is a fourth shape, off this axis
entirely: it paints main-thread JSX SVG (radial, so it keeps a bespoke
<DisplayError> instead of SvgChrome), composes none of the fetch
foundations, and answers freshness with its own ready getter — one chord fetch
covers the whole view, so there is no spatial or signature axis to compare. It
still runs the shared computeSvgReady / awaitSvgReady export gate
(reference/SVG_EXPORT.md).
LinearCanvasBaseDisplay (plugins/canvas) is not a peer of these. It is a
canvas-feature specialization layered on MultiRegionDisplayMixin, and only
LinearBasicDisplay + LinearVariantDisplay extend it. Everything else —
wiggle, Manhattan, alignments, MAF, and even the canvas plugin's own
LinearMultiRowFeatureDisplay — composes MultiRegionDisplayMixin directly.
Arc is the one display class that draws neither GPU canvas nor Canvas2D: its
components emit JSX <path> elements, on screen and in SVG export alike. So it
composes no RenderLifecycleMixin, and instead of DisplayChrome it renders
BaseDisplayComponent (plugins/arc), which re-uses the shared
computeDisplayPhase precedence plus the shared DisplayErrorBar /
DisplayLoadingOverlay / TooLargeMessage so its chrome stays identical to a
GPU display's. features !== undefined || !!error is its canvasDrawn
analogue — the first-paint signal that gates the -done testid and the loading
anti-flash. The stricter, staleness-aware svgReady is the export gate.
installGlobalFetchAutorun reads the viewport, isMinimized, the
rpcProps() cache key (a computed, for the reason in "the cache key is the
return value, not the reads") and reloadCounter at the top of its body,
before the display's shouldFetch()
gate, and that ordering is load-bearing. MobX rebuilds the dependency set on
every run, so a read placed inside the gate drops out of it on any run that
decides not to fetch — and can then never wake the autorun again. Arc is the
shape that exposes this: its shouldFetch is !regionTooLarge && !dataCurrent,
so it goes false on every successful fetch, and with reloadCounter read under
the gate reload() was silently dead. The display's own shouldFetch is the
only gate in the skeleton; each display's fetch re-checks isMinimized /
view.initialized / an empty viewport for its direct callers.
The general rule, which the other fetch autoruns already satisfy: a gated
trigger read is safe only if the gate is itself an observable that flips on the
transition you want to wake up on. if (self.isMinimized) return above the
tracked deps (synteny, tree-sidebar, the variant sources autorun) is fine —
un-minimizing re-runs the body and re-reads everything. A pure signal like
reloadCounter, whose only job is to say "go again" and which no gate consults,
is the dangerous case: nothing else will ever re-run the body on its behalf.
installGlobalFetchAutorun.test.ts pins this.
A global display whose shouldFetch gates on its own dataCurrent must also
invalidate that freshness signal in reload() — bumping reloadCounter alone
re-runs the autorun but leaves shouldFetch false. ArcFetchModel.reload()
clears loadedRegionSignature for exactly this reason (keeping features, so
the stale arcs stay under the loading overlay instead of blanking).
Render path is a separate axis. GPU-canvas vs Canvas2D is chosen per frame at the backend factory (GPU_RENDERING.md § RenderingBackend interfaces per plugin), not by which foundation a display composes.
The public
data fetching pipeline guide
is the tutorial version of this section (the fetchNeeded → fetchEachRegion
wrapper, rpcProps, cancellation, byte gate).
MultiRegionDisplayMixin (in
plugins/linear-genome-view/src/BaseLinearDisplay/) drives RPC fetches for all
LGV displays (alignments, canvas, wiggle, variants) via these autoruns:
| Autorun | Trigger | Action |
|---|---|---|
DisplayedRegionsChange |
view.displayedRegions change |
clearAllRpcData() |
FetchVisibleRegions |
viewport / fetchGeneration (600ms debounce) |
fetchNeeded(needed) for uncovered buffered regions; gated by error/regionTooLarge/fetchCanceled and skipped while the track is minimized |
SettingsInvalidate |
rpcProps() payload change |
clearAllRpcData() |
ClearBlockingStateOnViewportChange |
viewport change while error or fetchCanceled is set |
clearAllRpcData() to unblock retry (the derived regionTooLarge self-releases, so it's not part of this) |
ClearHoverOnRegionTooLarge |
regionTooLarge becomes true |
fires the overridable onRegionTooLarge() hook (no-op base; alignments clears its hover) |
Subclasses override fetchNeeded to call self.fetchRegions(needed, work).
fetchRegions runs an optional pre-flight byte estimate (via
getByteEstimateConfig → checkByteEstimate → the CoreGetRegionByteEstimate
RPC) before invoking the work callback. Oversize regions surface a banner:
DisplayChrome renders TooLargeMessage from the model's
regionTooLargeReason.
The error/fetchCanceled reads in ClearBlockingStateOnViewportChange are
untracked for correctness — tracking either would let set… re-fire the
autorun and wipe the flag before any viewport change.
Variants are the exception to per-region granularity:
MultiSampleVariantGetCellData returns one batched payload covering all visible
regions, so variants' fetchNeeded ignores needed and derives its own region
set (fetchRegionsForMode), marking them all loaded together when the work
callback returns. Which set depends on the mode: regular mode takes
bufferedVisibleRegions (off-screen variants simply clip), matrix mode takes
visibleRegions only — its columns lay out by feature index across the visible
width, so a buffered feature would be crammed into the viewport and draw a
connector to an off-screen position.
regionTooLarge raises the "region too large" banner and holds off the fetch.
It's a derived getter on RegionTooLargeMixin — a pure function of the
cached byte estimate rescaled to the current viewport — so it self-releases on
zoom-in with no imperative clear and doesn't flicker on pan. Displays opt in by
overriding hooks (derivedRegionTooLargeEnabled, configuredFetchSizeLimit,
densityTooLargeForDerivedGate) rather than shadowing the getter. Canvas folds
its byte check into the feature-fetch RPC instead of a separate pre-flight
estimate, and adds the density axis, via CanvasFeatureGateMixin
(plugins/canvas/src/shared/), which both canvas feature displays compose; the
shared verdict/threshold/banner-text primitives live in
plugins/linear-genome-view/src/shared/regionTooLargeUtils.ts so the two paths
can't drift.
Full detail — the byte gate, the opt-in hooks, how the verdict is built, and the shared decision primitives: reference/REGION_TOO_LARGE.md.
Including any fetch-result derivative in rpcProps() creates an infinite loop:
setCellData → <derived value> changes → rpcProps() changes
→ SettingsInvalidate → clearAllRpcData → cellData cleared
→ <derived value> changes → rpcProps() changes → …
The fix is to split the computation: rpcProps() gets a cache-key version
computed from user-controlled inputs only; any part that needs fetch-result data
is kept in a separate view used only for rendering or passed directly to the
server.
In the variant case, rpcProps().sources calls getSources with
renderingMode: 'alleleCount' internally so haplotype expansion (which needs
sampleInfo) is never triggered. The client's sources view still reads
sampleInfo for rendering — safe because it is not in rpcProps(). The server
receives the unexpanded sources and expands them after computing sampleInfo
from features; sources from clustering already carry HP and pass through
unchanged.
Rule: rpcProps() must contain only user-controlled settings. Never include
cellData, sampleInfo, or any getter that reads them.
Because both families key on the returned payload (see "the cache key is the
return value, not the reads"), the loop needs a fetch-derived value to reach the
return — one merely consulted while building can't loop. That is the whole
reason HiC gets away with activeNormalization reading fetched
availableNormalizations. It also sets where the loop shows up: per-region it is
a synchronous freeze, caught by makeSettingsLoopGuard's within-tick counter;
on the global family installGlobalFetchAutorun reads the key and fetches in one
debounced body, so it loops on the async-fetch cadence instead, which no
within-tick counter can tell apart from fast interaction. See
plugins/linear-genome-view/src/BaseLinearDisplay/CLAUDE.md for the overridable
hook list and test-file mapping.
BAM/CRAM decode against the reference (CRAM to reconstruct bases, BAM to compute
mismatches without an MD tag), but a track's adapter config doesn't carry the
reference — it belongs to the assembly. So the assembly's sequence adapter config
rides alongside adapterConfig as a sibling RPC arg (never spliced into it)
and is stashed on the resolved adapter instance via setSequenceAdapterConfig;
the adapter lazily builds it through getSubAdapter on first
getSequenceAdapter(). Client side, getSequenceAdapterConfig(assembly) (in
assemblyManager/assembly.ts) produces the snapshot; worker side,
getFeatureAdapter() (in data_adapters/getFeatureAdapter.ts) is the shared
prologue that resolves the feature adapter and primes it in one step.
The subtlety: the adapter cache (dataAdapterCache) keys on adapterConfig
alone, not on the sequence adapter. So the first RPC to resolve a given
adapter primes its sequence config for the lifetime of that cached instance
(setSequenceAdapterConfig is set-once). This is why CoreGetRefNames — usually
the first call for a track — passes it, and why every reference-needing fetch also
passes it rather than relying on ordering. A fetch that legitimately doesn't need
the reference (e.g. PileupGetGlobalValueForTag, which reads BAM tags directly)
omits it.
Invariant: any feature RPC that decodes against the reference must pass
sequenceAdapter. Don't assume a prior call primed the instance. Note that
setSequenceAdapterConfig does not propagate through wrapper adapters (there
is no wrapper-over-BAM/CRAM today; if one is reintroduced, plumb inheritance
through getSubAdapter).
This section is the summary; full detail lives in reference/GPU_RENDERING.md.
A GPU display composes RenderLifecycleMixin and calls
self.attachRenderingBackend(backend, { upload, render }) in its
startRenderingBackend(backend) action. The mixin spawns two autoruns tied to
the model's lifetime: upload(backend) pushes bytes to the GPU when the data
changes, render(backend) draws a frame when anything visible changes. MobX
auto-tracks every observable read inside each callback, so nothing declares
dependencies by hand. React components are thin bridges — create a canvas, hand
the backend to the model via useRenderingBackend (called inside
DisplayChrome), render JSX.
The rendering primitives live in @jbrowse/render-core
(packages/render-core): the HAL, RenderLifecycleMixin, the backend base
classes, the React backend hooks, and the clip/canvas/hp-math utilities. It is a
leaf package (deps: mobx + @jbrowse/mobx-state-tree + react peer; no
@jbrowse/core), so a third-party display can depend on it directly. The GPU
API is static-import-only — never exposed via the runtime ReExports
registry (ADR-030).
What the GPU doc covers, so you can jump straight in:
| Section | Read when |
|---|---|
| The core contract / The API / What the mixin owns | Wiring a new display's render lifecycle |
| Life of a frame | Debugging "why didn't it redraw", context loss, tab visibility |
| RenderingBackend interfaces per plugin | Writing a backend factory; going Canvas2D-only |
| Keeping the two backends in parity | Touching either a .slang or a Canvas2D draw fn |
Three upload patterns / installPerRegionLifecycle |
Choosing how a display shovels bytes; O(N²) upload bugs |
| HAL / Renderers stay stateless | Touching packages/render-core/src/hal/ or renderer state |
| Shaders (Slang codegen) | Editing a .slang or a generated module |
Canvas scaling & hi-DPI / displayedRegionIndex |
Blurry canvases; region↔buffer join keys |
| Adding a new GPU display type | The end-to-end checklist |
DisplayChrome branches on model.displayPhase. For the renderError /
tooLarge banners it early-returns the banner as its entire output,
replacing the display subtree, rather than keeping the container <div> mounted
and swapping the banner in beside the canvas. This looks like a leak — the
caller's className/ref/mouse handlers are absent in those two states — but a
benign one: a too-large region has no canvas to interact with, and the ref
re-attaches on force-load. What makes it the right shape:
- Clean GPU dispose/re-init. Early-
returnunmounts the canvas subtree, which firescanvasRef(null)→ effect cleanup →backend.dispose()+stopRenderingBackend(); force-load remounts and re-inits via the callback ref. Nesting the banner beside a still-mounted canvas would skip that cycle. Unmounting is safe precisely because that full dispose→re-init cycle runs. - The loading term stays lazy.
computeDisplayPhase(self, loading)takesloadingas a thunk and calls it only after ruling out the terminal flags, so when a banner is up the chrome's observer tracks only that flag, not the view's churningvisibleRegions/loadedRegions. - React Compiler opt-out.
DisplayChromeInnercarries'use no memo', so babel-plugin-react-compiler doesn't compile it and can't memoize a MobX read onmodel's stable identity. That opt-out is also whyreturn-vs-ternary is now a style choice: what stays load-bearing is replacing the subtree, not how the replacement is spelled. Full analysis: reference/COMPILER_TERNARY_FINDING.md.
The rest of the shared chrome — the phase precedence, the retry affordances, the overlay components — is in reference/DISPLAYCHROME.md.
SVG export and on-screen rendering share the same pure Canvas2D draw functions,
so a shader-only tweak can't silently diverge the export. Every display's
renderSvg.tsx follows one shape: await awaitSvgReady(model), then mount
SvgChrome (the single terminal-state gate) around a sync body that paints via
paintLayer. The full contract — the svgReady/settled freshness gates, the
one permitted TypeScript narrow, paintLayer's raster-vs-vector dispatch, the
JSX-SVG exception classes, and model-scoped clip ids — is in
reference/SVG_EXPORT.md.
Domain-named methods that enumerate what affects rendering output. Both are
MST view methods (not getters), so subclasses extend them via the standard super
capture pattern, mirroring renderProps.
| Method | Consumer | Invalidation route |
|---|---|---|
rpcProps() |
rpcManager.call(..., { ...self.rpcProps(), ... }) — RPC payload |
The serialized payload, in both families — per-region SettingsInvalidate reads self.rpcPropsCacheKey → clearAllRpcData → refetch; global installGlobalFetchAutorun reads a computed over the same function → refetch. See "the cache key is the return value" below |
gpuProps() |
buildSourceRenderData(data, self.gpuProps()) — encoder input |
Upload callback reads it — MobX re-uploads without an RPC roundtrip |
| Derived region map | Upload callback iterates it in place of raw rpcDataMap |
Upload autorun reads it — MobX re-uploads without an RPC roundtrip |
renderState |
backend.render(state) per frame |
Render callback reads it — re-fires when deps shift |
rpcProps() returns user-controlled settings only. Structural args
(adapterConfig, sequenceAdapter, region(s), bpPerPx, stopToken) are
spread in at the RPC call site, keeping rpcProps() focused on its purpose:
cache keys for SettingsInvalidate. Every display follows the same call shape:
rpcManager.call(sessionId, 'RenderXxxData', {
adapterConfig: self.adapterConfig, // inherited from BaseDisplayModel
regions, bpPerPx, // per-call values
...self.rpcProps(), // user settings (cache keys)
stopToken, statusCallback,
})sessionId belongs in the first argument only — RpcManager.call injects
it into the payload, and RpcCallArgs Omits it from the typed args for that
reason. Passing it again in the object is redundant (several older call sites
still do).
adapterConfig is provided by BaseDisplayModel (via
getConf(this.parentTrack, 'adapter')) — no display redefines it.
rpcProps() is the only extension point for the RPC payload. Each display
defines its own typed shape; subclasses that layer on fields capture super and
spread:
.views(self => {
const { rpcProps: superRpcProps } = self
return {
rpcProps() {
const base = superRpcProps()
return {
...base,
displayConfig: { ...base.displayConfig, geneGlyphMode: self.effectiveGeneGlyphMode },
showOnlyGenes: self.showOnlyGenes,
}
},
}
})MultiRegionDisplayMixin does not provide a base default — declaring one
would widen the typed return through MST's .views() chain and force consumers to
re-spread named fields. The mixin's SettingsInvalidate autorun looks up
rpcProps dynamically and is installed only when the method exists, so a
per-region display with no settings-driven refetch (e.g.
LinearReferenceSequenceDisplay) can simply not define it. HiC and LD compose
GlobalDataDisplayMixin rather than MultiRegion, and both do define
rpcProps().
Both families invalidate on the serialized payload — never on the raw call —
and serializeRpcProps is the one implementation of that. The per-region family
exposes it as the rpcPropsCacheKey getter (watched by SettingsInvalidate);
installGlobalFetchAutorun wraps the same function in a computed for its
trigger list.
The reason is that building the payload reads far more observables than it returns, so tracking the call tracks all of them:
- canvas builds it from a whole config snapshot (
resolvePromotableConfigSnapshot), which reads every slot on the display config — so ashowLabels,heightModeor compact/normaldisplayModeflip, all deliberately excluded from the payload, would refetch - HiC's
activeNormalizationconsultsavailableNormalizations, which is fetched (CoreGetInfo) — a read that has nothing to do with user intent
Serializing collapses both: only a change in what's returned invalidates. And it
has to be a string rather than a .rpcProps() comparison, because a fresh object
never compares equal.
The inverse hazard, since JSON.stringify is the comparison: a field whose
distinct states serialize identically is a silently dead cache axis — changing
it refetches nothing and raises no error. A class instance needs a toJSON or it
flattens to {} (SerializableFilterChain has one, which is what makes the
variant displays' filters field a real key), and an undefined value drops its
key entirely, so it can't be distinguished from a sibling state that also drops.
Prefer primitives and plain arrays. Regression-tested in
installGlobalFetchAutorun.test.ts ("ignores an observable rpcProps() reads but
does not return"), which fails if the trigger goes back to the raw call.
gpuProps() exists wherever the main thread encodes the GPU buffer — wiggle,
multi-wiggle and MAF (and GC-content, which inherits wiggle's wholesale). HiC and
multi-LGV synteny fill the same role without the method: HiC's upload callback
reads self.colorScheme straight into generateColorRamp, and synteny's
computedColors getter is its re-upload-without-refetch half. Canvas's worker
pre-builds the buffer, so canvas has only rpcProps(). This splits refetch from re-upload: wiggle color change →
re-encode only; bicolorPivot change → worker output differs → rpcProps() →
refetch.
Derived region maps apply when upload needs whole fresh per-region payloads, not
just encoder parameters. Alignments' laidOutByGroup returns, per group, shallow
clones of that group's rpcDataMap entries with freshly-allocated Y arrays from
main-thread layout (+ connecting-line / Flatbush in chain mode); sourceSections
pairs each with its arc feed and is what the upload callback iterates.
(laidOutPileupMap is now just the first group of that map, kept for the
single-section consumers.) Raw rpcDataMap is never mutated. Use derived maps
when settings change the shape/contents of per-region data; use gpuProps() for
scalars fed to an encoder.
Color palettes are a pure function of the active theme, so derive them in a model
getter — buildColorPaletteFromTheme(getSession(self).theme) — that gpuProps()
/ renderState read directly. Do not stage them in a volatile that a React
useEffect pushes in via a setColorPalette action: the effect runs only on
mount, so SVG export and RPC — neither of which has a component — see a null
palette and render blank. As a getter the value is always present and MobX
recomputes it only when the theme changes: same re-encode invalidation, no mount
dependency. This applies equally to alignments, MAF, and the reference sequence
display.
session.theme is the resolved MUI Theme, required on
AbstractSessionModel. Embedded products without ThemeManagerSessionMixin
supply a minimal get theme() = createJBrowseTheme(getConf(self, 'theme')).
SVG export still overrides the palette with the export theme (opts.theme).
All worker position output is absolute genomic uint32, so data stays valid under zoom. The exceptions are for zoom-dependent content, not coords:
- Wiggle: BigWig has discrete zoom levels; the worker picks one based on
bpPerPx / resolution.isCacheValiduses strict equality (view.bpPerPx === loadedBpPerPx) — any zoom change refetches all visible regions together. See ADR-008. - Canvas: the amino-acid overlay is the only
bpPerPx-dependent worker decision.isCacheValidreturnsfalsewhenrpcDataMaphas no entry for the region, and otherwise refetches only when the viewport crossesshouldRenderPeptideBackground's discrete threshold.laidOutDataMapusescoarseBpPerPx(debounced 500ms) so Y-row packing doesn't recompute on every animation frame during smooth zoom. - MAF: zoom picks which fetch runs, not a resolution — zoomed out with a
configured summary adapter it pulls cheap per-species summary rows, zoomed in
the full alignment. Crossing that threshold inside an already-loaded region
wouldn't move the region bounds, so
isCacheValidkeys on which map holds the region (summaryDataMapvsrpcDataMap). - Multi-sample variant matrix: columns lay out by feature index across the
visible width, so which features show is a function of the current zoom even
when the viewport stays spatially inside loaded data. Strict
bpPerPxequality, same rule as wiggle. The regular variant display draws each variant at its genomic position and keeps the default.
Those are the only four zoom-dependent overrides. Other displays either leave
the default () => true or override on presence alone (LinearMultiRowFeatureDisplay
returns rpcDataMap.has(idx), so a too-large region — marked loaded but holding
no data — refetches the moment the gate releases).
MultiRegionDisplayMixin's FetchVisibleRegions autorun calls the override per
region and refetches stale ones.
- Don't put upload/render logic in React
useEffect/useLayoutEffect— it belongs in the MST autorun pair spawned byattachRenderingBackend. - Don't destructure model methods; call on the model.
- Don't use
useMemofor observable-dependent values; use a cached MST view. - Don't mutate per-region values in place; emit fresh objects.
- Don't make a renderer class the owner of per-region data. The model's
rpcDataMap/laidOutDataMapis the single source of truth. Most displays pass it in per frame (renderBlocks(blocks, regions, state)), and that is the default to reach for. A renderer-heldprivate regionsmap is legal only when it is written exclusively by the upload callback and never mutated in place:RenderLifecycleMixinbumpsrenderTickafter every upload, so the render autorun re-fires and the cache cannot stale. Alignments is the one display built that way (sync(sources)on both its GPU and Canvas2D backends, because the GPU side must hold buffers anyway and the two share oneAlignmentsRenderingBackendinterface). What is still forbidden is a cache populated from anywhere else, or one whose entries get patched in place. For GPU buffer lifecycle delegate tohal.pruneRegions(active). - Don't add or redefine volatiles/actions owned by the slot mixin (
canvasDrawn,renderTick,currentRenderingBackend,renderError,markCanvasDrawn,resetCanvasDrawn,renderNow,setRenderError,stopRenderingBackend, etc.) or theisReadyview owned byMultiRegionDisplayMixin.renderErrorin particular is the single source for therenderErrorterminal phase — don't fork it into a display-local volatile. - Don't hand-edit
*.generated.tsor hand-maintain WGSL/GLSL/offset tables. Edit.slangand runpnpm gen:shaders; CI'sgit diff --exit-codecatches stale outputs. Consume generated constants by name from TS — never copy a literal offset into a renderer. - Don't put fetch-result derivatives (
cellData,sampleInfo, etc.) intorpcProps(); it is an infinite fetch loop. See the trap. - Don't diverge the two render backends. Import shader constants into TS rather
than retyping them, put shared glyph geometry/color math in one draw helper, and
keep multi-layer order/gating in one exhaustively-keyed registry. And don't go
the other way: a Canvas2D sub-pixel overdraw (fudge factor /
f2) or stroke-vs-fill swap is deliberate AA compensation with no shader equivalent — don't port it into a.slang. See GPU_RENDERING.md § Keeping the two backends in parity.
Deep subsystems, each read on its own task (also linked inline where they come up):
- reference/ARCHITECTURAL_LIMITS.md — the live register of what this architecture can't do: the WebGL2 context budget, worker stickiness, the couplings we accept, the correctness surfaces no type protects. Read before scaling work, or when a symptom smells like a ceiling.
- reference/GPU_RENDERING.md — the render lifecycle in depth: the mixin, the upload/render autoruns, per-plugin backends, the three upload patterns, the HAL, Slang shaders, and the new-display checklist.
- reference/SVG_EXPORT.md — SVG export pipeline, the
svgReady/settledreadiness gates,paintLayer, model-scoped clip ids. - reference/BP_PRECISION.md — the absolute-uint32 convention, the three coordinate families (LGV bp / window-relative cumBp / screen space), hi/lo float math, genome-size limits.
- reference/PROGRESS_REPORTING.md — the worker→UI status channel, determinate bars, concurrent-fetch aggregation, cancel.
- reference/REGION_TOO_LARGE.md — the byte/density
gate: the derived
regionTooLargegetter, the opt-in hooks, and the shared verdict primitives. - reference/SYNTENY_LOD.md — the two PIF tiers (fine/coarse), the profiled cost model, and why read-time binning is capped.
- reference/HISTORICAL.md — the old server-side block system, bugs that shaped the current design, corrections to old writeups.
- reference/GPU_GLOSSARY.md — plain-language GPU glossary and a Canvas2D→GPU primer.
- reference/CONFIG_PATTERN.md — how config reaches the renderer (config → MST snapshot → plain object → RPC).
- reference/DISPLAYCHROME.md — the shared loading/error/retry chrome every display renders through.