Skip to content

WebGPU/WebGL rendering#5468

Open
cmdcolin wants to merge 523 commits intomainfrom
webgl-poc
Open

WebGPU/WebGL rendering#5468
cmdcolin wants to merge 523 commits intomainfrom
webgl-poc

Conversation

@cmdcolin
Copy link
Copy Markdown
Collaborator

@cmdcolin cmdcolin commented Feb 4, 2026

JBrowse (both 1 and 2) have been "optimized for side scrolling". The "static blocks" based methodology reflects this: tiles of data are prerendered side by side. However this is not really the most important type of scrolling! Smooth zoom is arguably more important!

In order to get true smooth zooming, we likely need to adopt webgl. The reason webgl is needed is that we can provide all the "coordinate data" to the webgl renderer, and the GPU just does transformations on zoom level changes or side scroll. Contrast this with canvas, where on zoom level change, we have to completely, on the CPU, re-draw everything with javascript

This is something that can not be done smoothly via canvas. Every single frame needs redrawing. The only "hacky" way to incorporate it into canvas is just applying canvas 'transforms' which distorts. We also see this in our current code with the css 'scaleX' which also distorts

We have to think quite seriously to try to overcome our limitations and get to true smooth scrolling...and that likely requires webgl

I don't know a lot about webgl and there may be some hard challenges to come up against, but this is a very early demonstration of what it might look like to use webgl for the alignments track.

Possible challenges

There are many possible challenges, that range from known challenges to unknown challenges.

Some challenges i currently forsee include:

  1. My current thinking is to try to make this just apply on a track by track basis. This means that there might be many webgl contexts on a single screen. This actually is somewhat dangerous because browsers limit the number of webgl contexts that are available. WebGL only allows 16 contexts at a time. However, it is hard to even have 16 tracks open at once. I think that if we combine this with IntersectionObserver, we will be able to make it work 99% of the time. We can consider looking into things like https://github.com/greggman/virtual-webgl

  2. It may be challenging to make our code work with our web worker architecture. There is the concept of web workers rendering to main thread canvases, but our current web worker code renders a static bitmap, sends that result to main thread. We don't dynamically control it on a per-frame basis. Furthermore, it is known that serializing too much data between the main thread and webworker is also very slow, so we can't just get automatic gains by parsing on the web worker and serializing drawing to the main thread. This demo is just main thread only.

  3. There may some clients that have browser compat issues with webgl. We may need canvas fallback

  4. Our svg export will need to have a custom mapping as we use the fact that we rendered to canvas as a mechanism to serialize those canvas commands to svg. that drawing code will need to be persistently probably maintained separately, which isn't very fun, but extreme unit testing might help cover here

Video

This video demo should be compelling to anyone who has ever zoomed in or out in jbrowse. It simply demonstrates smooth zooming. Compare against master where the entire screen blanks and re-renders at each zoom level, and the smooth zoom feels much more natural

out.mp4

Share link

this likely still has glitches and weirdness. only bam files on the alignments track are demonstrated for the time

you can use "mouse wheel vertical scroll" in the area of the bam track to scroll zoom

https://jbrowse.org/code/jb2/webgl-poc/?config=test_data%2Fvolvox%2Fconfig.json&session=share-1eTgCoKkSa&password=x4Byl

Context

This was motivated by work with Pratik on the lorax project https://lorax.ucsc.edu/ this project is deeply webgl driven and is interested in integrating with jbrowse (meeting with Pratik later this week)

Caveats

This PR is very hackily implemented, and largely claude coded. It is unclear if we will run into any blockers, however, even seeing some amount of progress i hope helps to see that it could be quite compelling to implement this

@cmdcolin cmdcolin force-pushed the webgl-poc branch 6 times, most recently from 08e9db7 to 34b5fa3 Compare February 11, 2026 16:30
@cmdcolin cmdcolin changed the title Early proof of concept using WebGL to render WebGL rendering Feb 13, 2026
@cmdcolin cmdcolin force-pushed the webgl-poc branch 3 times, most recently from 9223ca5 to f22e9a4 Compare February 15, 2026 20:50
cmdcolin and others added 4 commits March 20, 2026 12:14
Extract WebGPU code from AlignmentsRenderer into WebGPUAlignmentsRenderer,
making AlignmentsRenderer a thin facade that selects between WebGPU, WebGL,
and Canvas2D backends via a shared AlignmentsBackend interface. Move
RenderState and upload data types into rendererTypes.ts, eliminating the
Parameters<WebGLRenderer[...]> indirection and Canvas2D→WebGL type coupling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the dual glFallback/canvas2dFallback fields with a single
fallback field in wiggle, hic, canvas, synteny, dotplot, variant,
LD, and variant matrix renderers. This matches the pattern established
in the alignments renderer refactoring.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cmdcolin and others added 3 commits March 20, 2026 16:37
Consolidate duplicated createShader, createProgram, splitPositionWithFrac,
and cacheUniforms functions from 6 separate copies across wiggle, hic,
variants, alignments, canvas, sequence, dotplot, and synteny plugins into
a single shared module in packages/core.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix effectiveShowDescriptions to respect config default (was only
checking raw trackShowDescriptions override). Fix handleMouseMove
empty-data branch missing featureIdUnderMouse cleanup. Remove
unnecessary double cast on colorByCDS since LGV already has the
property.

Extract regionKeys getter to deduplicate computation, use
useEffectEvent for renderWithBlocks instead of useCallback+ref,
extract hitTestAtEvent/clearHoverState helpers, flatten
selectFeatureById, consolidate preProcessSnapshot destructuring.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract WebGPU code from mixed facade/backend classes into dedicated
WebGPU*Renderer files implementing shared *Backend interfaces. Each
renderer system now follows the same pattern: a facade class handles
the fallback chain (WebGPU → WebGL → Canvas2D) and delegates to a
backend interface, matching the architecture established in alignments.

Affected plugins: linear-comparative-view (synteny), dotplot-view,
variants (VariantMatrix, Variant, LD), hic, canvas (features), wiggle.

Also normalizes destroy() → dispose() across all backends, fixes
method name mismatches (uploadForRegion → uploadRegion, pruneStaleRegions
→ pruneRegions), removes unnecessary null checks on constructor-
guaranteed fields, and adds Canvas2D renderer tests for dotplot and hic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cmdcolin and others added 21 commits March 20, 2026 19:34
Remove more unnecessary useEffects

- GroupByDialog: move state clearing to onChange handler
- ImportForm: replace useEffect sync with derived value pattern (userValue ?? r0)
- RegionWidthEditorDialog: remove dead useEffect (dialog opens fresh each time)
- Loading: fix missing [] deps (timer was resetting every render)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Fix spreadsheet NumberEditor: call model setter when clamping to 1

When user enters 0 or negative, the display resets to 1 but the model
setter was not being called, unlike the original useEffect-based code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Fix lint warnings in AddConnectionWidget/ConnectionTypeSelect

- Remove unused ConnectionType import
- Remove unnecessary optional chains and conditionals now that
  connectionType is always defined
- Tighten setConnectionType callback type from optional to required

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use color-bits directly in GPU hot paths, add colorBits re-export

- Add @jbrowse/core/util/colorBits module re-exporting color-bits with
  helpers for named CSS color support (parseCssColor, cssColorToNormalizedRgb,
  cssColorToRgba, cssColorToNormalizedRgba)
- Update 11 GPU rendering files to import from colorBits directly,
  bypassing the colord wrapper object allocation
- Remove unused colord dependency from @jbrowse/plugin-wiggle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Add tests for colord wrapper and colorBits helpers

Tests cover parsing (hex, named colors, rgb/hsl strings, transparent,
case insensitivity), formatting (toHex, toRgbString, toHslString),
manipulation (alpha, darken, lighten, mix), and the direct colorBits
helper functions (cssColorToNormalizedRgb, cssColorToRgba, etc).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Fix unused variable lint error in colorBits test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Remove unnecessary conditionals in AddConnectionWidget

configModel is always truthy since it comes from useMemo calling
configSchema.create(), so the conditional checks were redundant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The reversed field was being dropped in mergedVisibleRegions and
bufferedVisibleRegions, causing horizontally flipped views to render
with wrong strand orientation. Also fixes DisplayedRegionsChange
autorun not detecting flip changes (missing reversed in comparison key).

Introduces RegionWithNumber type and flattens the { region, regionNumber }
nesting throughout the fetch pipeline, removing gratuitous wrapping/
unwrapping across all display plugins.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All RPC renameRegionsIfNeeded methods manually reconstructed region
objects, dropping reversed (and any other extra fields). Use spread
from the renamed result instead, which preserves all fields since the
core renameRegionIfNeeded already spreads.

Also adds reversed to RenderPileupDataArgs type and updates the
integration test to use RegionWithNumber.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The reversed flag was only used for strand arrow direction but not for
the actual x-positioning of features. When a region is horizontally
flipped, lower bp should appear on the right and higher bp on the left.

Adds a reversed field to FeatureRenderBlock and implements coordinate
flipping in all three rendering backends:
- WebGL: u_reversed uniform negates clip-space X via mix()
- WebGPU: same approach in WGSL hp_to_clip_x
- Canvas2D: mirrors bpToScreenX from screenEndPx instead of screenStartPx

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lipX

Negating inside hpToClipX broke the min-width check (sx2 - sx1 goes
negative when reversed, collapsing all rects to 1px). Instead, keep
hpToClipX unmodified and apply flipX() on the final gl_Position.x /
out.position.x. This preserves all intermediate math (min-width,
snapping, chevron spacing) while correctly mirroring the output.

Also fixes JavaScript-side bp-to-px conversions for floating labels,
amino acid overlays, and hit detection via a shared bpToScreenPx
helper that handles reversed regions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The shader's flipX() handles all spatial mirroring for reversed
regions. The RPC layer was also computing effectiveStrand (negating
strand when reversed), which caused a double-flip — arrows, chevrons,
and amino acids stayed in their original positions instead of mirroring.

Now the RPC layer uses raw strand for all direction and position
calculations. getStrandArrowPadding no longer takes a reversed param.
Child layout offset calculations no longer flip for reversed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add flip_x() to all GLSL/WGSL shaders (reads, CIGAR, coverage, arcs,
connecting lines) across WebGL, WebGPU, and Canvas2D backends. Pass
reversed flag from view regions through RenderBlock to each renderer.

Fix hit testing coordinate conversion to account for reversed regions
so mouse hover/click resolves to the correct genomic position.

Remove reversed from displayed-regions-change comparison so flipping
a region doesn't trigger a data re-fetch and visual blank.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move duplicated high-precision position shader snippets (HP_WGSL_CORE,
HP_GLSL_CORE) to a single source of truth in core. Canvas and variant
plugins now import from core instead of defining inline copies.
Standardize GLSL HP function names to snake_case to match WGSL convention.
Fix broken Canvas2D constant import names (HEAD_HALF_H_PX_PX, etc).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add flip_x() to GLSL/WGSL shaders and pass reversed flag through
RenderBlock for LinearWiggleDisplay, MultiLinearWiggleDisplay, and
MultiVariantDisplay across WebGL, WebGPU, and Canvas2D backends.

Fix hit testing coordinate conversion in both plugins to account
for reversed regions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pull non-graph/non-MultiLGVSyntenyDisplay changes from the
webgl-poc-multisyn branch:

- Remove baseUrl from all tsconfig.build.esm.json files
- Bump dependency versions (@mui/x-data-grid, dockview, storybook,
  typescript 6.0.2, eslint, canvas, sanitize-filename, etc.)
- Remove react-refresh webpack plugin and add hot:false to dev server
- Remove d3-hierarchy2 vendored code from variants plugin, replaced
  by @jbrowse/tree-sidebar package
- Remove MultiLGVSyntenyDisplay and MultiSyntenyTrack from
  linear-comparative-view (these belong on the multisyn branch)
- Add FLIP_GLSL export to alignments shaders/utils.ts
- Add test/data and *.log to .gitignore and eslint ignore
- Update root package.json: remove react-refresh, add serve-static

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…debar

Replace inline hierarchy/cluster code and TreeSidebar component with
shared @jbrowse/tree-sidebar package. Fix renderSvg color parameter
naming (posColor/negColor) and add renderSvgColors test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The RPC layer still uses clusterData/toNewick from @gmod/hclust directly.
The dynamic import of treeDrawingAutorun now points to @jbrowse/tree-sidebar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bufferedVisibleRegions returns flat objects with regionNumber as a
top-level property alongside refName/start/end/assemblyName. Update
all onFetchNeeded signatures and destructuring to match this flat
format instead of expecting a nested { region, regionNumber } wrapper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Export RegionWithNumber = Region & { regionNumber: number } from
MultiRegionDisplayMixin. Use this alias in onFetchNeeded,
withFetchLifecycle, and computeAndSetArcs signatures across
alignments, canvas, and linear-genome-view plugins.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Simplify computeAndSetArcs to push flat RegionWithNumber directly
instead of extracting .region properties. Pass mergedVisibleRegions
directly to computeAndSetArcs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…DisplayModel import

- Add missing reversed field when constructing RenderBlock objects in
  alignments and canvas plugins
- Import WiggleDisplayModel type so it's in scope for the component prop

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… $nonEmptyObject leak

Spreading MST snapshot objects carries the $nonEmptyObject symbol into
inferred return types, causing TS4058 declaration emit errors. Switch
RegionWithNumber from a flat intersection (Region & { regionNumber })
to a nested interface { region: Region; regionNumber: number } that
keeps the plain Region separate from the metadata.

Update all consumers: bufferedVisibleRegions, onFetchNeeded,
withFetchLifecycle, computeAndSetArcs, wiggle fetch helpers, and tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts:
#	.github/workflows/push.yml
#	.gitignore
#	package.json
#	packages/core/src/util/io/RemoteFileWithRangeCache.ts
#	plugins/alignments/src/LinearPileupDisplay/SharedLinearPileupDisplayMixin.ts
#	plugins/alignments/src/LinearPileupDisplay/components/GroupByDialog.test.tsx
#	plugins/alignments/src/LinearPileupDisplay/model.ts
#	plugins/alignments/src/LinearSNPCoverageDisplay/model.ts
#	plugins/wiggle/src/shared/SharedWiggleMixin.ts
#	plugins/wiggle/src/util.ts
#	pnpm-lock.yaml
#	products/jbrowse-web/browser-tests/__snapshots__/alignments-bam.png
#	products/jbrowse-web/browser-tests/__snapshots__/alignments-volvox-sv.png
#	products/jbrowse-web/browser-tests/__snapshots__/breakpoint_split_view_snapshot.png
#	products/jbrowse-web/browser-tests/__snapshots__/main-thread-rpc-bam.png
#	products/jbrowse-web/browser-tests/__snapshots__/methylation_snapshot.png
#	products/jbrowse-web/browser-tests/__snapshots__/session-spec-display-snapshot-type.png
#	products/jbrowse-web/browser-tests/__snapshots__/workspaces-add-view.png
#	products/jbrowse-web/browser-tests/__snapshots__/workspaces-layout-custom-sizes.png
#	products/jbrowse-web/browser-tests/__snapshots__/workspaces-layout-url-param.png
#	products/jbrowse-web/browser-tests/__snapshots__/workspaces-new-tab.png
#	products/jbrowse-web/browser-tests/__snapshots__/workspaces-split-view.png
#	products/jbrowse-web/browser-tests/runner.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant