Skip to content

Commit 4f11e6e

Browse files
committed
perf: Only create the texture on the parent layer (not sub-layers)
1 parent 74cad37 commit 4f11e6e

File tree

4 files changed

+84
-58
lines changed

4 files changed

+84
-58
lines changed

src/io.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ export function initLayerStateFromSource(source: SourceData & { id: string }): L
227227
loader: label.loader,
228228
modelMatrix: label.modelMatrix,
229229
opacity: 1,
230-
lut: label.lut,
230+
colors: label.colors,
231231
})),
232232
transformSourceSelection: getTransformSourceSelectionFromLabels(
233233
source.labels.map((label) => label.loader[0]),

src/layers/label-layer.ts

+78-51
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
import { getImageSize } from "@hms-dbmi/viv";
2-
import { BitmapLayer, TileLayer, type UpdateParameters } from "deck.gl";
1+
import { BitmapLayer, TileLayer } from "deck.gl";
32
import * as utils from "../utils";
43

4+
import type { Layer, UpdateParameters } from "deck.gl";
55
import { type Matrix4, clamp } from "math.gl";
66
import type * as zarr from "zarrita";
77
import type { ZarrPixelSource } from "../ZarrPixelSource";
88

99
type Texture = ReturnType<BitmapLayer["context"]["device"]["createTexture"]>;
1010

11-
export type LabelLayerLut = Readonly<Record<number, readonly [number, number, number, number]>>;
11+
export type OmeColors = Readonly<Record<number, readonly [number, number, number, number]>>;
1212
export interface LabelLayerProps {
1313
id: string;
1414
loader: Array<ZarrPixelSource>;
1515
selection: Array<number>;
1616
opacity: number;
1717
modelMatrix: Matrix4;
18-
lut?: LabelLayerLut;
18+
colors?: OmeColors;
1919
}
2020

2121
/**
@@ -35,12 +35,18 @@ type LabelPixelData = {
3535
height: number;
3636
};
3737

38-
export class LabelLayer extends TileLayer<LabelPixelData> {
38+
export class LabelLayer extends TileLayer<LabelPixelData, LabelLayerProps> {
3939
static layerName = "VizarrLabelLayer";
40+
// @ts-expect-error - only way to extend the base state type
41+
state!: { colorTexture: Texture } & TileLayer["state"];
4042

4143
constructor(props: LabelLayerProps) {
4244
const resolutions = props.loader;
43-
const dimensions = getImageSize(resolutions[0]);
45+
const dimensions = {
46+
height: resolutions[0].shape.at(-2),
47+
width: resolutions[0].shape.at(-1),
48+
};
49+
utils.assert(dimensions.width && dimensions.height);
4450
const tileSize = getTileSizeForResolutions(resolutions);
4551
super({
4652
id: `labels-${props.id}`,
@@ -50,6 +56,7 @@ export class LabelLayer extends TileLayer<LabelPixelData> {
5056
opacity: props.opacity,
5157
maxZoom: 0,
5258
modelMatrix: props.modelMatrix,
59+
colors: props.colors,
5360
zoomOffset: Math.round(Math.log2(props.modelMatrix ? props.modelMatrix.getScale()[0] : 1)),
5461
updateTriggers: {
5562
getTileData: [props.loader, props.selection],
@@ -65,29 +72,68 @@ export class LabelLayer extends TileLayer<LabelPixelData> {
6572
);
6673
return { data, width, height };
6774
},
68-
renderSubLayers({ tile, data }) {
69-
const [[left, bottom], [right, top]] = tile.boundingBox;
70-
const { width, height } = dimensions;
71-
return new GrayscaleBitmapLayer({
72-
id: `tile-${tile.index.x}.${tile.index.y}.${tile.index.z}-${props.id}`,
73-
pixelData: data,
74-
opacity: props.opacity,
75-
modelMatrix: props.modelMatrix,
76-
lut: props.lut,
77-
bounds: [clamp(left, 0, width), clamp(top, 0, height), clamp(right, 0, width), clamp(bottom, 0, height)],
78-
// For underlying class
79-
image: new ImageData(data.width, data.height),
80-
pickable: false,
81-
});
82-
},
8375
});
8476
}
77+
78+
renderSubLayers(
79+
params: TileLayer["props"] & {
80+
data: LabelPixelData;
81+
tile: {
82+
index: { x: number; y: number; z: number };
83+
boundingBox: [min: Array<number>, max: Array<number>];
84+
};
85+
},
86+
): Layer {
87+
const { tile, data, ...props } = params;
88+
const [[left, bottom], [right, top]] = tile.boundingBox;
89+
utils.assert(props.extent, "missing extent");
90+
const [_x0, _y0, width, height] = props.extent;
91+
return new GrayscaleBitmapLayer({
92+
id: `tile-${tile.index.x}.${tile.index.y}.${tile.index.z}-${props.id}`,
93+
pixelData: data,
94+
opacity: props.opacity,
95+
modelMatrix: props.modelMatrix,
96+
colorTexture: this.state.colorTexture,
97+
bounds: [clamp(left, 0, width), clamp(top, 0, height), clamp(right, 0, width), clamp(bottom, 0, height)],
98+
// For underlying class
99+
image: new ImageData(data.width, data.height),
100+
pickable: false,
101+
});
102+
}
103+
104+
updateState({ props, oldProps, changeFlags, ...rest }: UpdateParameters<this>): void {
105+
super.updateState({ props, oldProps, changeFlags, ...rest });
106+
// we make the colorTexture on this layer so we can share it amoung all the sublayers
107+
if (props.colors !== oldProps.colors || !this.state.colorTexture) {
108+
this.state.colorTexture?.destroy();
109+
const colorTexture = createColorTexture({
110+
source: props.colors,
111+
maxTextureDimension2D: this.context.device.limits.maxTextureDimension2D,
112+
});
113+
this.setState({
114+
colorTexture: this.context.device.createTexture({
115+
width: colorTexture.width,
116+
height: colorTexture.height,
117+
data: colorTexture.data,
118+
dimension: "2d",
119+
mipmaps: false,
120+
sampler: {
121+
minFilter: "nearest",
122+
magFilter: "nearest",
123+
addressModeU: "clamp-to-edge",
124+
addressModeV: "clamp-to-edge",
125+
},
126+
format: "rgba8unorm",
127+
}),
128+
});
129+
}
130+
}
85131
}
86132

87-
export class GrayscaleBitmapLayer extends BitmapLayer<{ pixelData: LabelPixelData; lut?: LabelLayerLut }> {
133+
export class GrayscaleBitmapLayer extends BitmapLayer<{ pixelData: LabelPixelData; colorTexture: Texture }> {
88134
static layerName = "VizarrGrayscaleBitmapLayer";
89135
// @ts-expect-error - only way to extend the base state type
90-
state!: { texture: Texture; colorTexture: Texture } & BitmapLayer["state"];
136+
state!: { texture: Texture } & BitmapLayer["state"];
91137

92138
getShaders() {
93139
const sampler = (
@@ -162,40 +208,21 @@ void main() {
162208
}),
163209
});
164210
}
165-
166-
if (props.lut !== oldProps.lut || !this.state.colorTexture) {
167-
this.state.colorTexture?.destroy();
168-
const colorTexture = createColorTexture({
169-
source: props.lut,
170-
maxTextureDimension2D: this.context.device.limits.maxTextureDimension2D,
171-
});
172-
this.setState({
173-
colorTexture: this.context.device.createTexture({
174-
width: colorTexture.width,
175-
height: colorTexture.height,
176-
data: colorTexture.data,
177-
dimension: "2d",
178-
mipmaps: false,
179-
sampler: {
180-
minFilter: "nearest",
181-
magFilter: "nearest",
182-
addressModeU: "clamp-to-edge",
183-
addressModeV: "clamp-to-edge",
184-
},
185-
format: "rgba8unorm",
186-
}),
187-
});
188-
}
189211
}
190212

191213
draw(opts: unknown) {
192-
const { model, texture, colorTexture } = this.state;
214+
const { model, texture } = this.state;
215+
const { colorTexture } = this.props;
216+
193217
if (model && texture && colorTexture) {
194218
model.setUniforms({
195219
colorTextureWidth: colorTexture.width,
196220
colorTextureHeight: colorTexture.height,
197221
});
198-
model.setBindings({ grayscaleTexture: texture, colorTexture: colorTexture });
222+
model.setBindings({
223+
grayscaleTexture: texture,
224+
colorTexture: colorTexture,
225+
});
199226
}
200227
super.draw(opts);
201228
}
@@ -210,7 +237,7 @@ function getTileSizeForResolutions(resolutions: Array<ZarrPixelSource>): number
210237
return tileSize;
211238
}
212239

213-
const SEEN_LUTS = new WeakSet<LabelLayerLut>();
240+
const SEEN_LUTS = new WeakSet<OmeColors>();
214241

215242
/**
216243
* Creates a color lookup table (LUT) as a 2D texture.
@@ -219,7 +246,7 @@ const SEEN_LUTS = new WeakSet<LabelLayerLut>();
219246
* @param options.maxTextureDimension2D - The maximum texture dimension size.
220247
*/
221248
function createColorTexture(options: {
222-
source?: LabelLayerLut;
249+
source?: OmeColors;
223250
maxTextureDimension2D: number;
224251
}) {
225252
const { source, maxTextureDimension2D } = options;

src/ome.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as zarr from "zarrita";
33
import type { ImageLabels, ImageLayerConfig, OnClickData, SourceData } from "./state";
44

55
import { ZarrPixelSource } from "./ZarrPixelSource";
6-
import type { LabelLayerLut } from "./layers/label-layer";
6+
import type { OmeColors } from "./layers/label-layer";
77
import * as utils from "./utils";
88

99
export async function loadWell(
@@ -283,9 +283,9 @@ async function loadOmeImageLabel(root: zarr.Location<zarr.Readable>, name: strin
283283
const labels = utils.getNgffAxisLabels(axes);
284284
return {
285285
name,
286-
lut: resolveImageLabelsLut(attrs["image-label"]),
287286
modelMatrix: utils.coordinateTransformationsToMatrix(attrs.multiscales),
288287
loader: data.map((arr) => new ZarrPixelSource(arr, { labels, tileSize })),
288+
colors: resolveImageLabelsLut(attrs["image-label"]),
289289
};
290290
}
291291

@@ -360,7 +360,7 @@ function parseOmeroMeta({ rdefs, channels, name }: Ome.Omero, axes: Ome.Axis[]):
360360
};
361361
}
362362

363-
function resolveImageLabelsLut(attrs: Ome.ImageLabel): LabelLayerLut | undefined {
363+
function resolveImageLabelsLut(attrs: Ome.ImageLabel): OmeColors | undefined {
364364
if (!attrs.colors || attrs.colors.length === 0) {
365365
return undefined;
366366
}

src/state.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { ZarrPixelSource } from "./ZarrPixelSource";
1111
import { initLayerStateFromSource } from "./io";
1212

1313
import { GridLayer, type GridLayerProps, type GridLoader } from "./layers/grid-layer";
14-
import { LabelLayer, type LabelLayerLut, type LabelLayerProps } from "./layers/label-layer";
14+
import { LabelLayer, type LabelLayerProps, type OmeColors } from "./layers/label-layer";
1515
import {
1616
ImageLayer,
1717
type ImageLayerProps,
@@ -59,7 +59,7 @@ export type ImageLabels = Array<{
5959
name: string;
6060
loader: ZarrPixelSource[];
6161
modelMatrix: Matrix4;
62-
lut?: LabelLayerLut;
62+
colors?: OmeColors;
6363
}>;
6464

6565
export type SourceData = {
@@ -187,7 +187,6 @@ export const layerAtoms = atom((get) => {
187187
const { layerProps, transformSourceSelection } = layer.labels;
188188
const selection = transformSourceSelection(layer.layerProps.selections[0]);
189189
const imageLabelLayers = layerProps.map((props) => new LabelLayer({ ...props, selection }));
190-
// @ts-expect-error - TS can't resolve that Array<ImageLabelLayer> == Array<Layer<ImageLabelLayerProps>>
191190
layers.push(...imageLabelLayers);
192191
}
193192
return layers;

0 commit comments

Comments
 (0)