@vitessce/spatial-accelerated 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +52925 -0
  3. package/dist-tsc/SpatialWrapper.d.ts +2 -0
  4. package/dist-tsc/SpatialWrapper.d.ts.map +1 -0
  5. package/dist-tsc/SpatialWrapper.js +45 -0
  6. package/dist-tsc/VolumeDataManager.d.ts +170 -0
  7. package/dist-tsc/VolumeDataManager.d.ts.map +1 -0
  8. package/dist-tsc/VolumeDataManager.js +1273 -0
  9. package/dist-tsc/VolumeDataManager.test.d.ts +2 -0
  10. package/dist-tsc/VolumeDataManager.test.d.ts.map +1 -0
  11. package/dist-tsc/VolumeDataManager.test.js +164 -0
  12. package/dist-tsc/VolumeRenderManager.d.ts +265 -0
  13. package/dist-tsc/VolumeRenderManager.d.ts.map +1 -0
  14. package/dist-tsc/VolumeRenderManager.js +396 -0
  15. package/dist-tsc/VolumeShader.d.ts +262 -0
  16. package/dist-tsc/VolumeShader.d.ts.map +1 -0
  17. package/dist-tsc/VolumeShader.js +73 -0
  18. package/dist-tsc/VolumeView.d.ts +2 -0
  19. package/dist-tsc/VolumeView.d.ts.map +1 -0
  20. package/dist-tsc/VolumeView.js +521 -0
  21. package/dist-tsc/index.d.ts +2 -0
  22. package/dist-tsc/index.d.ts.map +1 -0
  23. package/dist-tsc/index.js +1 -0
  24. package/dist-tsc/shaders/GaussianFragmentShader.d.ts +2 -0
  25. package/dist-tsc/shaders/GaussianFragmentShader.d.ts.map +1 -0
  26. package/dist-tsc/shaders/GaussianFragmentShader.js +242 -0
  27. package/dist-tsc/shaders/GaussianVertexShader.d.ts +2 -0
  28. package/dist-tsc/shaders/GaussianVertexShader.d.ts.map +1 -0
  29. package/dist-tsc/shaders/GaussianVertexShader.js +51 -0
  30. package/dist-tsc/shaders/VolumeFragmentShader.d.ts +2 -0
  31. package/dist-tsc/shaders/VolumeFragmentShader.d.ts.map +1 -0
  32. package/dist-tsc/shaders/VolumeFragmentShader.js +1817 -0
  33. package/dist-tsc/shaders/VolumeVertexShader.d.ts +2 -0
  34. package/dist-tsc/shaders/VolumeVertexShader.d.ts.map +1 -0
  35. package/dist-tsc/shaders/VolumeVertexShader.js +110 -0
  36. package/package.json +63 -0
  37. package/src/SpatialWrapper.js +60 -0
  38. package/src/VolumeDataManager.js +1515 -0
  39. package/src/VolumeDataManager.test.js +180 -0
  40. package/src/VolumeRenderManager.js +589 -0
  41. package/src/VolumeShader.js +74 -0
  42. package/src/VolumeView.js +651 -0
  43. package/src/index.js +1 -0
  44. package/src/shaders/GaussianFragmentShader.js +243 -0
  45. package/src/shaders/GaussianVertexShader.js +52 -0
  46. package/src/shaders/VolumeFragmentShader.js +1818 -0
  47. package/src/shaders/VolumeVertexShader.js +111 -0
@@ -0,0 +1,1273 @@
1
+ /* eslint-disable no-bitwise */
2
+ /* eslint-disable no-underscore-dangle */
3
+ /* eslint-disable max-len */
4
+ /* eslint-disable no-unused-vars */
5
+ /* eslint-disable camelcase */
6
+ import { Data3DTexture, RedFormat, RedIntegerFormat, UnsignedByteType, UnsignedIntType, LinearFilter, NearestFilter, } from 'three';
7
+ import { isEqual } from 'lodash-es';
8
+ import { log, atLeastLogLevel, LogLevel } from '@vitessce/globals';
9
+ // NOTES:
10
+ // 2048 x 2048 x 32 = 4096 bricks, around 134 MB
11
+ // 2048 x 2048 x 64 = 8192 bricks, around 268 MB
12
+ // 2048 x 2048 x 128 = 16384 bricks, around 537 MB **
13
+ // 2048 x 2048 x 256 = 32768 bricks, around 1.074 GB
14
+ // 2048 x 2048 x 512 = 65536 bricks, around 2.148 GB
15
+ // 2048 x 2048 x 1024 = 131072 bricks, around 4.295 GB
16
+ // Default chunk sizes
17
+ // const CHUNK_SIZE = 32;
18
+ // const CHUNK_TOTAL_SIZE = 32768; // 32*32*32 = 32768
19
+ const BRICK_SIZE = 32;
20
+ const BRICK_CACHE_SIZE_X = 64;
21
+ const BRICK_CACHE_SIZE_Y = 64;
22
+ const BRICK_CACHE_SIZE_Z = 4;
23
+ const TOTAL_NUM_BRICKS = BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y * BRICK_CACHE_SIZE_Z;
24
+ const BRICK_CACHE_SIZE_VOXELS_X = BRICK_CACHE_SIZE_X * BRICK_SIZE;
25
+ const BRICK_CACHE_SIZE_VOXELS_Y = BRICK_CACHE_SIZE_Y * BRICK_SIZE;
26
+ const BRICK_CACHE_SIZE_VOXELS_Z = BRICK_CACHE_SIZE_Z * BRICK_SIZE;
27
+ const PAGE_TABLE_ADDRESS_SIZE = 'uint32';
28
+ const BRICK_CACHE_ADDRESS_SIZE = 'uint16';
29
+ const MAX_RESOLUTION_LEVELS = 10; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
30
+ const MAX_CHANNELS = 7; // [0, 1, 2, 3, 4, 5, 6]
31
+ // const manualChannelSelection = 0;
32
+ // Add a constant for initialization status
33
+ const INIT_STATUS = {
34
+ NOT_STARTED: 'not_started',
35
+ IN_PROGRESS: 'in_progress',
36
+ COMPLETE: 'complete',
37
+ FAILED: 'failed',
38
+ };
39
+ function logWithColor(message) {
40
+ if (atLeastLogLevel(LogLevel.DEBUG)) {
41
+ console.warn(`%cDM: ${message}`, 'background: blue; color: white; padding: 2px; border-radius: 3px;');
42
+ }
43
+ }
44
+ /* Begin extracted functions */
45
+ export function _resolutionStatsToShapes(multiResolutionStats) {
46
+ return multiResolutionStats.map((stats) => {
47
+ const { dims } = stats;
48
+ const shape = [
49
+ // Other spatial-accelerated code assumes TCZYX dimension order
50
+ dims.t, // TODO: standardize lowercase/uppercase dim names at store-level
51
+ dims.c, // TODO: handle case when dimension(s) are missing
52
+ dims.z,
53
+ dims.y,
54
+ dims.x,
55
+ ];
56
+ return shape;
57
+ });
58
+ }
59
+ export function _resolutionStatsToBrickLayout(multiResolutionStats) {
60
+ return multiResolutionStats.map((stats) => {
61
+ const shape = [
62
+ stats.depth, // z
63
+ stats.height, // y
64
+ stats.width, // x
65
+ ];
66
+ // TODO: abstract chunkSize to always be 32x32x32 at store-level
67
+ // const chunkSize = array0.chunks || [BRICK_SIZE, BRICK_SIZE, BRICK_SIZE];
68
+ return [
69
+ Math.ceil((shape[0] || 1) / BRICK_SIZE),
70
+ Math.ceil((shape[1] || 1) / BRICK_SIZE),
71
+ Math.ceil((shape[2] || 1) / BRICK_SIZE),
72
+ ];
73
+ });
74
+ }
75
+ /**
76
+ *
77
+ * @param {[number, number, number][]} zarrStoreBrickLayout
78
+ * @param {number} channelsZarrMappingsLength
79
+ */
80
+ export function _initMRMCPT(zarrStoreBrickLayout, channelsZarrMappingsLength) {
81
+ log.debug('_initMRMCPT', zarrStoreBrickLayout, channelsZarrMappingsLength);
82
+ // Page table
83
+ const PT = {
84
+ channelOffsets: [
85
+ [0, 0, 1],
86
+ [0, 1, 0],
87
+ [0, 1, 1],
88
+ [1, 0, 0],
89
+ [1, 0, 1],
90
+ [1, 1, 0],
91
+ [1, 1, 1],
92
+ ],
93
+ anchors: [],
94
+ offsets: [],
95
+ xExtent: 0, // includes the offset inclusive
96
+ yExtent: 0, // includes the offset inclusive
97
+ zExtent: 0, // includes the offset inclusive
98
+ z0Extent: 0, // l0 z extent
99
+ zTotal: 0, // original z extent plus the l0 z extent times the channel count
100
+ };
101
+ // Calculate PT extents first
102
+ PT.xExtent = 1;
103
+ PT.yExtent = 1;
104
+ PT.zExtent = 1;
105
+ const l0z = zarrStoreBrickLayout[0][0]; // Get z extent from highest resolution
106
+ PT.z0Extent = l0z;
107
+ // log.debug('PT', PT);
108
+ // log.debug('PT anchors', PT.anchors);
109
+ // log.debug('PT anchors with 0', PT.anchors);
110
+ PT.lowestDataRes = zarrStoreBrickLayout.length - 1;
111
+ // log.debug('lowestDataRes', PT.lowestDataRes);
112
+ // Sum up the extents from all resolution levels excluding the l0
113
+ for (let i = zarrStoreBrickLayout.length - 1; i > 0; i--) {
114
+ PT.anchors.push([
115
+ PT.xExtent,
116
+ PT.yExtent,
117
+ PT.zExtent,
118
+ ]);
119
+ PT.xExtent += zarrStoreBrickLayout[i][2];
120
+ PT.yExtent += zarrStoreBrickLayout[i][1];
121
+ PT.zExtent += zarrStoreBrickLayout[i][0];
122
+ }
123
+ PT.anchors.push([0, 0, PT.zExtent]);
124
+ PT.anchors.reverse();
125
+ // log.debug('PT anchors', PT.anchors);
126
+ // Calculate total Z extent including channel offsets
127
+ // PT.zTotal = PT.zExtent + (this.zarrStore.channelCount * l0z);
128
+ PT.zTotal = PT.zExtent + channelsZarrMappingsLength * l0z;
129
+ // log.debug('number of PT channels', channelsZarrMappingsLength);
130
+ // log.debug('Page Table Extents:', {
131
+ // x: PT.xExtent,
132
+ // y: PT.yExtent,
133
+ // z: PT.zExtent,
134
+ // z0: PT.z0Extent,
135
+ // zTotal: PT.zTotal,
136
+ // });
137
+ // Initialize the BrickCache as a 3D texture (uint8)
138
+ const brickCacheData = new Uint8Array(BRICK_CACHE_SIZE_VOXELS_X
139
+ * BRICK_CACHE_SIZE_VOXELS_Y
140
+ * BRICK_CACHE_SIZE_VOXELS_Z);
141
+ brickCacheData.fill(0);
142
+ // Initialize the PageTable data using calculated extents
143
+ const pageTableData = new Uint32Array(PT.xExtent * PT.yExtent * PT.zTotal);
144
+ pageTableData.fill(0);
145
+ const bcTHREE = new Data3DTexture(brickCacheData, BRICK_CACHE_SIZE_VOXELS_X, BRICK_CACHE_SIZE_VOXELS_Y, BRICK_CACHE_SIZE_VOXELS_Z);
146
+ bcTHREE.format = RedFormat;
147
+ bcTHREE.type = UnsignedByteType;
148
+ bcTHREE.internalFormat = 'R8';
149
+ // bcTHREE.minFilter = NearestFilter;
150
+ // bcTHREE.magFilter = NearestFilter;
151
+ bcTHREE.minFilter = LinearFilter;
152
+ bcTHREE.magFilter = LinearFilter;
153
+ bcTHREE.generateMipmaps = false;
154
+ bcTHREE.needsUpdate = true;
155
+ const ptTHREE = new Data3DTexture(pageTableData, PT.xExtent, PT.yExtent, PT.zTotal);
156
+ ptTHREE.format = RedIntegerFormat;
157
+ ptTHREE.type = UnsignedIntType;
158
+ ptTHREE.internalFormat = 'R32UI';
159
+ ptTHREE.minFilter = NearestFilter;
160
+ ptTHREE.magFilter = NearestFilter;
161
+ ptTHREE.generateMipmaps = false;
162
+ ptTHREE.needsUpdate = true;
163
+ log.debug('_initMRMCPT', PT, ptTHREE, bcTHREE);
164
+ return {
165
+ PT,
166
+ ptTHREE,
167
+ bcTHREE,
168
+ };
169
+ }
170
+ /* ------------------------------------------------------------- *
171
+ * 3. Pack PT entry (flags | min | max | bcX | bcY | bcZ) *
172
+ * ------------------------------------------------------------- */
173
+ export function _packPT(min, max, bcX, bcY, bcZ) {
174
+ // Scale down to 7-bit range (0-127) by dividing by 2
175
+ const clamp7 = v => Math.max(0, Math.min(127, Math.floor(v / 2)));
176
+ // log.debug('Scaled min/max:', clamp7(min), clamp7(max));
177
+ return ((1 << 31) // bit‑31 = resident
178
+ | (1 << 30) // bit‑30 = init‑done
179
+ | (clamp7(min) << 23) // 7 bits
180
+ | (clamp7(max) << 16) // 7 bits
181
+ | ((bcX & 0x3F) << 10) // 6 bits
182
+ | ((bcY & 0x3F) << 4) // 6 bits
183
+ | (bcZ & 0x0F) // 4 bits
184
+ ) >>> 0; // keep unsigned
185
+ }
186
+ // Pack PT entry indicating "not resident but init" with min/max preserved
187
+ // Note: this is not used anywhere?
188
+ /*
189
+ export function _packPTNotResident(min, max, bcX, bcY, bcZ) {
190
+ // Scale down to 7-bit range (0-127) by dividing by 2
191
+ const clamp7 = v => Math.max(0, Math.min(127, Math.floor(v / 2)));
192
+
193
+ return (
194
+ (0 << 31) // bit-31 = NOT resident
195
+ | (1 << 30) // bit-30 = init-done
196
+ | (clamp7(min) << 23) // 7 bits
197
+ | (clamp7(max) << 16) // 7 bits
198
+ | ((bcX & 0x3F) << 10) // 6 bits
199
+ | ((bcY & 0x3F) << 4) // 6 bits
200
+ | (bcZ & 0x0F) // 4 bits
201
+ ) >>> 0; // keep unsigned
202
+ }
203
+ */
204
+ export function _ptToZarr(ptx, pty, ptz, ptInfo) {
205
+ const { PT_zExtent, PT_z0Extent, PT_anchors } = ptInfo;
206
+ let channel = -1;
207
+ let resolution = -1;
208
+ let x = -1;
209
+ let y = -1;
210
+ let z = -1;
211
+ // log.debug('pt', ptx, pty, ptz);
212
+ // log.debug('pt', ptx, pty, ptz);
213
+ // log.debug('ptz', this.PT.z, this.PT.l0z);
214
+ if (ptz >= PT_zExtent) {
215
+ // log.debug('ptz >= this.PT.zExtent');
216
+ resolution = 0;
217
+ x = ptx;
218
+ y = pty;
219
+ z = (ptz - PT_zExtent) % PT_z0Extent;
220
+ channel = Math.floor((ptz - PT_zExtent) / PT_z0Extent);
221
+ }
222
+ else {
223
+ // log.debug('ptz < this.PT.zExtent');
224
+ // log.debug('this.PT.anchors', this.PT.anchors);
225
+ for (let i = 1; i < PT_anchors.length; i++) {
226
+ if (ptx < PT_anchors[i][0] && pty < PT_anchors[i][1] && ptz < PT_anchors[i][2]) {
227
+ // all PT coordinates are less than the anchor -> one res lower
228
+ // log.debug('all PT coordinates are less than the anchor -> one res lower');
229
+ }
230
+ else {
231
+ // log.debug('not all PT coordinates are less than the anchor ', this.PT.anchors[i]);
232
+ resolution = i;
233
+ const channelMask = [0, 0, 0];
234
+ // log.debug('ptx', ptx);
235
+ // log.debug('pty', pty);
236
+ // log.debug('ptz', ptz);
237
+ // log.debug('this.PT.anchors[i]', this.PT.anchors[i]);
238
+ if (ptx >= PT_anchors[i][0]) {
239
+ channelMask[0] = 1;
240
+ }
241
+ if (pty >= PT_anchors[i][1]) {
242
+ channelMask[1] = 1;
243
+ }
244
+ if (ptz >= PT_anchors[i][2]) {
245
+ channelMask[2] = 1;
246
+ }
247
+ const binaryChannel = (channelMask[0] << 2) | (channelMask[1] << 1) | channelMask[2];
248
+ channel = Math.max(1, Math.min(7, binaryChannel)) - 1;
249
+ // log.debug('channel', channel);
250
+ const thisOffset = channelMask.map((v, j) => v * PT_anchors[i][j]);
251
+ // log.debug('thisOffset', thisOffset);
252
+ x = ptx - thisOffset[0];
253
+ y = pty - thisOffset[1];
254
+ z = ptz - thisOffset[2];
255
+ // log.debug('x', x);
256
+ // log.debug('y', y);
257
+ // log.debug('z', z);
258
+ break;
259
+ }
260
+ }
261
+ }
262
+ // log.debug(channel, resolution, x, y, z);
263
+ return {
264
+ channel,
265
+ resolution,
266
+ x,
267
+ y,
268
+ z,
269
+ };
270
+ }
271
+ export function _requestBufferToRequestObjects(buffer, k) {
272
+ const counts = new Map();
273
+ for (let i = 0; i < buffer.length; i += 4) {
274
+ const r = buffer[i];
275
+ const g = buffer[i + 1];
276
+ const b = buffer[i + 2];
277
+ const a = buffer[i + 3];
278
+ if ((r | g | b | a) === 0) {
279
+ // eslint-disable-next-line no-continue
280
+ continue;
281
+ }
282
+ const packed = ((r << 24) | (g << 16) | (b << 8) | a) >>> 0;
283
+ counts.set(packed, (counts.get(packed) || 0) + 1);
284
+ }
285
+ // log.debug('counts', counts);
286
+ // log.debug('this.k', this.k);
287
+ // Get the top K (potentially fewer than K) page table requests.
288
+ const requests = [...counts.entries()]
289
+ .sort((a, b) => b[1] - a[1])
290
+ .slice(0, k)
291
+ .map(([packed]) => ({
292
+ x: (packed >> 22) & 0x3FF,
293
+ y: (packed >> 12) & 0x3FF,
294
+ z: packed & 0xFFF,
295
+ }));
296
+ return { requests, origRequestCount: counts.size };
297
+ }
298
+ /* End extracted functions */
299
+ export class VolumeDataManager {
300
+ constructor(glParam) {
301
+ logWithColor('CLASS INITIALIZING');
302
+ // this.store = new zarrita.FetchStore(url); // TODO: use this.images instead
303
+ const gl = glParam.getContext?.() || glParam;
304
+ const renderer = glParam;
305
+ // this.images = images;
306
+ // this.imageLayerScopes = imageLayerScopes;
307
+ // Handle both WebGLRenderer and WebGL context
308
+ if (gl.domElement && gl.getContext) {
309
+ // This is likely a THREE.WebGLRenderer
310
+ this.gl = gl.getContext();
311
+ }
312
+ else if (gl.isWebGLRenderer) {
313
+ // Explicit THREE.WebGLRenderer check
314
+ this.gl = gl.getContext();
315
+ }
316
+ else {
317
+ // Assume it's already a WebGL context
318
+ this.gl = gl;
319
+ }
320
+ // const maxFragmentUniformVectors = this.gl.getParameter(this.gl.MAX_FRAGMENT_UNIFORM_VECTORS);
321
+ // log.debug('maxFragmentUniformVectors', maxFragmentUniformVectors);
322
+ // 1024
323
+ this.renderer = renderer;
324
+ // Create a mock context if we couldn't get a real one
325
+ if (!this.gl || typeof this.gl.getParameter !== 'function') {
326
+ log.debug('Unable to get WebGL context, using mock context');
327
+ this.gl = {
328
+ getParameter: (param) => {
329
+ // Return reasonable defaults
330
+ const defaults = {
331
+ MAX_TEXTURE_SIZE: 4096,
332
+ MAX_3D_TEXTURE_SIZE: 256,
333
+ MAX_RENDERBUFFER_SIZE: 4096,
334
+ MAX_UNIFORM_BUFFER_BINDINGS: 16,
335
+ };
336
+ return defaults[param] || 0;
337
+ },
338
+ MAX_TEXTURE_SIZE: 'MAX_TEXTURE_SIZE',
339
+ MAX_3D_TEXTURE_SIZE: 'MAX_3D_TEXTURE_SIZE',
340
+ MAX_RENDERBUFFER_SIZE: 'MAX_RENDERBUFFER_SIZE',
341
+ MAX_UNIFORM_BUFFER_BINDINGS: 'MAX_UNIFORM_BUFFER_BINDINGS',
342
+ };
343
+ }
344
+ log.debug('GL CONSTANTS');
345
+ log.debug(this.gl);
346
+ log.debug(this.gl.TEXTURE0);
347
+ log.debug(this.gl.textures);
348
+ log.debug('RENDERER');
349
+ log.debug(this.renderer);
350
+ this.deviceLimits = {
351
+ maxTextureSize: this.gl.getParameter(this.gl.MAX_TEXTURE_SIZE),
352
+ max3DTextureSize: this.gl.getParameter(this.gl.MAX_3D_TEXTURE_SIZE),
353
+ maxRenderbufferSize: this.gl.getParameter(this.gl.MAX_RENDERBUFFER_SIZE),
354
+ maxUniformBufferBindings: this.gl.getParameter(this.gl.MAX_UNIFORM_BUFFER_BINDINGS),
355
+ };
356
+ this.zarrStore = {
357
+ resolutions: null, // 6 (the number of resolutions aka. pyramid levels in the file)
358
+ chunkSize: [], // [32, 32, 32]
359
+ shapes: [], // [[795, 1024, 1024], ..., [64, 64, 64], [32, 32, 32]]
360
+ arrays: [], // [array0, array1, array2, array3, array4, array5]
361
+ dtype: '', // 'uint8'
362
+ physicalSizeTotal: [], // [795 x 0.0688, 1024 x 0.03417, 1024 x 0.03417]
363
+ physicalSizeVoxel: [], // [0.0688, 0.03417, 0.03417]
364
+ brickLayout: [], // [[25, 32, 32],[13, 16, 16], ..., [2,2,2],[1,1,1]]
365
+ // store: '', // ref to this.store
366
+ // group: '', // ref to this.group
367
+ channelCount: 1, // MAX 7 TODO: get from zarr metadata
368
+ scales: [], // downsample ratios, [x,y,z] per resolution level
369
+ lowestDataRes: 0, // lowest resolution level with data
370
+ };
371
+ this.ptTHREE = null;
372
+ this.bcTHREE = null;
373
+ this.channels = {
374
+ maxChannels: 7, // lower when dataset has fewer, dictates page table size
375
+ zarrMappings: [], // stores the zarr channel index for every one of the up to 7 channels
376
+ colorMappings: [], // stores the PT slot for every color
377
+ downsampleMin: [], // stores the downsample min for every one of the up to 7 channels
378
+ downsampleMax: [], // stores the downsample max for every one of the up to 7 channels
379
+ };
380
+ this.PT = {
381
+ channelOffsets: [
382
+ [0, 0, 1],
383
+ [0, 1, 0],
384
+ [0, 1, 1],
385
+ [1, 0, 0],
386
+ [1, 0, 1],
387
+ [1, 1, 0],
388
+ [1, 1, 1],
389
+ ],
390
+ anchors: [],
391
+ offsets: [],
392
+ xExtent: 0, // includes the offset inclusive
393
+ yExtent: 0, // includes the offset inclusive
394
+ zExtent: 0, // includes the offset inclusive
395
+ z0Extent: 0, // l0 z extent
396
+ zTotal: 0, // original z extent plus the l0 z extent times the channel count
397
+ };
398
+ // this.minimumMin = 255;
399
+ // this.maximumMin = 0;
400
+ // this.minimumMax = 255;
401
+ // this.maximumMax = 0;
402
+ // this.bricksAllocated = 0;
403
+ this.bricksEverLoaded = new Set();
404
+ // Properties for volume rendering
405
+ // this.originalScale = [1, 1, 1]; // Original dimensions
406
+ // this.physicalScale = [{ size: 1 }, { size: 1 }, { size: 2.1676 }]; // Physical size scaling
407
+ // this.physicalScale = [{ size: 1 }, { size: 1 }, { size: 1 }]; // Physical size scaling
408
+ // this.testArray = [];
409
+ // this.ptArray = [];
410
+ // this.ptManOffsets = [];
411
+ // top k page table addresses
412
+ // this.requestsStack = [];
413
+ this.isBusy = false;
414
+ // brick cache structure for internal calculations
415
+ this.BCTimeStamps = new Array(TOTAL_NUM_BRICKS).fill(0);
416
+ this.BCMinMax = new Array(TOTAL_NUM_BRICKS).fill([0, 0]);
417
+ this.BCFull = false;
418
+ this.BCUnusedIndex = 0;
419
+ // brick cache to page table mapping
420
+ this.bc2pt = new Array(TOTAL_NUM_BRICKS).fill(null);
421
+ // top k unused brick cache addresses
422
+ this.LRUStack = [];
423
+ // this.LRUReady = false;
424
+ this.triggerUsage = true;
425
+ this.triggerRequest = false;
426
+ this.timeStamp = 0;
427
+ this.k = 40;
428
+ this.noNewRequests = false;
429
+ this.needsBailout = false; // Flag to indicate if we need to bail out of processing requests
430
+ // Add initialization status
431
+ this.initStatus = INIT_STATUS.NOT_STARTED;
432
+ this.initError = null;
433
+ logWithColor('VolumeDataManager constructor complete');
434
+ }
435
+ initImages(images, imageLayerScopes) {
436
+ logWithColor('INIT IMAGES');
437
+ this.images = images;
438
+ this.imageLayerScopes = imageLayerScopes;
439
+ }
440
+ /**
441
+ * Initialize the VolumeDataManager with Zarr store details and device limits
442
+ * This should be called ONCE at website initialization
443
+ * TODO(mark): merge this with the constructor?
444
+ * @returns {Promise<Object>} Object with Zarr store details and device limits
445
+ */
446
+ async init(config) {
447
+ logWithColor('INIT()');
448
+ // Prevent multiple initializations
449
+ if (this.initStatus !== INIT_STATUS.NOT_STARTED) {
450
+ log.debug('VolumeDataManager init() was called more than once!');
451
+ if (this.initStatus === INIT_STATUS.COMPLETE) {
452
+ // Return existing initialized data
453
+ return {
454
+ success: true,
455
+ deviceLimits: this.deviceLimits,
456
+ zarrStore: this.zarrStore,
457
+ // physicalScale: this.physicalScale,
458
+ physicalSizeTotal: this.zarrStore.physicalSizeTotal,
459
+ physicalSizeVoxel: this.zarrStore.physicalSizeVoxel,
460
+ error: null,
461
+ };
462
+ }
463
+ if (this.initStatus === INIT_STATUS.FAILED) {
464
+ return {
465
+ success: false,
466
+ error: this.initError || 'Unknown initialization error',
467
+ };
468
+ }
469
+ // Still in progress, return a pending state
470
+ return {
471
+ success: false,
472
+ pending: true,
473
+ error: 'Initialization in progress',
474
+ };
475
+ }
476
+ this.initStatus = INIT_STATUS.IN_PROGRESS;
477
+ logWithColor('INIT() IN PROGRESS');
478
+ try {
479
+ // Query Zarr store details
480
+ // Get all resolution levels
481
+ const imageWrapper = this.images?.[this.imageLayerScopes?.[0]]?.image?.instance;
482
+ // TODO(mark): Do not access vivLoader.metadata directly. Generalize to non-NGFF metadata.
483
+ this.ngffMetadata = imageWrapper.vivLoader.metadata;
484
+ log.debug('ngffMetadata', this.ngffMetadata);
485
+ if (!imageWrapper || imageWrapper.getType() !== 'ome-zarr') {
486
+ throw new Error('Invalid imageWrapper or not an OME-Zarr image');
487
+ }
488
+ const multiResolutionStats = imageWrapper.getMultiResolutionStats();
489
+ const shapes = _resolutionStatsToShapes(multiResolutionStats);
490
+ const resolutions = multiResolutionStats.length;
491
+ this.zarrStore.resolutions = resolutions;
492
+ // TODO: filter to only those resolutions below the 16k x 16x x 4k limit?
493
+ const vivData = imageWrapper.getData();
494
+ if (!Array.isArray(vivData) || vivData.length < 1) {
495
+ throw new Error('Not a multiresolution loader');
496
+ }
497
+ if (!isEqual(vivData[0].labels, ['t', 'c', 'z', 'y', 'x'])) {
498
+ // TODO: relax this and remove the error.
499
+ throw new Error('Expected OME-Zarr data with dimensions [t, c, z, y, x]');
500
+ }
501
+ log.debug('vivData', vivData);
502
+ // TODO: add a better getter to ImageWrapper to avoid accessing the private _data field here?
503
+ const arrays = vivData.map(resolutionData => resolutionData._data);
504
+ const scales = new Array(resolutions).fill(null);
505
+ // Try to load each resolution level in order
506
+ /*
507
+ const loadPromises = [];
508
+ for (let i = 0; i < resolutions; i++) {
509
+ loadPromises.push(this.tryLoadResolution(i, arrays));
510
+ }
511
+
512
+ const results = await Promise.all(loadPromises);
513
+
514
+ log.debug('results', results);
515
+
516
+ // Verify all resolutions were loaded successfully
517
+ const failedResolutions = results.filter(r => !r.success);
518
+ if (failedResolutions.length > 0) {
519
+ throw new Error(`Failed to load ${failedResolutions.length} resolution levels: ${failedResolutions.map(r => r.level).join(', ')}`);
520
+ }
521
+
522
+ console.info(`Successfully loaded ${resolutions} resolution levels.`);
523
+ */
524
+ // console.info('shapes', shapes);
525
+ // console.info('arrays', arrays);
526
+ // Get the first array for metadata
527
+ if (arrays.length > 0) {
528
+ const array0 = arrays[0];
529
+ // Update store information
530
+ this.zarrStore = {
531
+ resolutions,
532
+ chunkSize: array0.chunks,
533
+ shapes,
534
+ arrays,
535
+ dtype: array0.dtype,
536
+ physicalSizeTotal: [], // Will be populated if metadata exists
537
+ physicalSizeVoxel: [], // Will be populated if metadata exists
538
+ brickLayout: [], // Calculate from shapes and chunk sizes
539
+ // store: this.store,
540
+ // group: this.group,
541
+ channelCount: shapes[0][1],
542
+ scales,
543
+ };
544
+ // initializing channel mappings and slots as empty
545
+ this.channels.colorMappings = new Array(Math.min(this.zarrStore.channelCount, 7)).fill(-1);
546
+ this.channels.zarrMappings = new Array(Math.min(this.zarrStore.channelCount, 7)).fill(undefined);
547
+ this.channels.downsampleMin = new Array(Math.min(this.zarrStore.channelCount, 7)).fill(undefined);
548
+ this.channels.downsampleMax = new Array(Math.min(this.zarrStore.channelCount, 7)).fill(undefined);
549
+ // log.debug('zarrMappings in init', this.channels.zarrMappings);
550
+ // log.debug('downsampleMin in init', this.channels.downsampleMin);
551
+ // log.debug('downsampleMax in init', this.channels.downsampleMax);
552
+ // Extract physical size information if available
553
+ if (array0.meta && array0.meta.physicalSizes) {
554
+ const { x, y, z } = array0.meta.physicalSizes;
555
+ // Get size data or use default value of 1
556
+ const zSize = z?.size || 1;
557
+ const ySize = y?.size || 1;
558
+ const xSize = x?.size || 1;
559
+ this.zarrStore.physicalSizeVoxel = [zSize, ySize, xSize];
560
+ // log.debug('avail physicalSizeVoxel', this.zarrStore.physicalSizeVoxel);
561
+ // Calculate total physical size
562
+ if (array0.shape && array0.shape.length >= 5) {
563
+ this.zarrStore.physicalSizeTotal = [
564
+ (array0.shape[2] || 1) * zSize,
565
+ (array0.shape[3] || 1) * ySize,
566
+ (array0.shape[4] || 1) * xSize,
567
+ ];
568
+ }
569
+ }
570
+ else {
571
+ // log.debug('no physicalSizeVoxel');
572
+ this.zarrStore.physicalSizeVoxel = [1, 1, 1];
573
+ this.zarrStore.physicalSizeTotal = [
574
+ array0.shape[2] || 1,
575
+ array0.shape[3] || 1,
576
+ array0.shape[4] || 1,
577
+ ];
578
+ // log.debug('default physicalSizeVoxel', this.zarrStore.physicalSizeVoxel);
579
+ // log.debug('default physicalSizeTotal', this.zarrStore.physicalSizeTotal);
580
+ }
581
+ // log.debug('group.attrs', this.group.attrs);
582
+ const { multiscales } = this.ngffMetadata;
583
+ if (!multiscales) {
584
+ throw new Error('Expected multiscales metadata in group.attrs');
585
+ }
586
+ // Check the per-resolution coordinate transformations
587
+ if (multiscales?.[0]?.datasets?.[0]?.coordinateTransformations) {
588
+ for (let i = 0; i < resolutions; i++) {
589
+ if (multiscales?.[0]?.datasets?.[i]?.coordinateTransformations?.[0]?.scale) {
590
+ const { scale } = multiscales[0].datasets[i].coordinateTransformations[0];
591
+ scales[i] = [scale[4], scale[3], scale[2]];
592
+ // log.debug('scale', i, scales[i]);
593
+ }
594
+ }
595
+ // TODO: if the scales are not powers of two, throw an error?
596
+ }
597
+ else {
598
+ log.error('no coordinateTransformations available, assuming downsampling ratio of 2 per dimension');
599
+ for (let i = 0; i < resolutions; i++) {
600
+ // 2 to the power of i
601
+ const scale = 2 ** i;
602
+ scales[i] = [scale, scale, scale];
603
+ }
604
+ }
605
+ // log.debug('scales', scales);
606
+ this.zarrStore.scales = scales;
607
+ // Use the top-level coordinateTransformations scale if available
608
+ const { coordinateTransformations } = this.ngffMetadata;
609
+ if (coordinateTransformations?.[0]?.scale) {
610
+ const { scale } = coordinateTransformations[0];
611
+ // Assuming scale is in format [..., zscale, yscale, xscale]
612
+ const scaleLength = scale.length;
613
+ if (scaleLength >= 3) {
614
+ const zScale = scale[scaleLength - 3];
615
+ const yScale = scale[scaleLength - 2];
616
+ const xScale = scale[scaleLength - 1];
617
+ // Update physicalScale
618
+ // this.physicalScale = [
619
+ // { size: xScale },
620
+ // { size: yScale },
621
+ // { size: zScale },
622
+ // ];
623
+ // log.debug('physicalScale', this.physicalScale);
624
+ // Update zarrStore's physicalSizeVoxel
625
+ this.zarrStore.physicalSizeVoxel = [zScale, yScale, xScale];
626
+ // Calculate total physical size if shape exists
627
+ if (array0.shape && array0.shape.length >= 5) {
628
+ this.zarrStore.physicalSizeTotal = [
629
+ (array0.shape[2] || 1) * zScale,
630
+ (array0.shape[3] || 1) * yScale,
631
+ (array0.shape[4] || 1) * xScale,
632
+ ];
633
+ }
634
+ // log.debug('Using scale from coordinateTransformations:', this.physicalScale);
635
+ }
636
+ }
637
+ // Calculate brick layout for each resolution
638
+ this.zarrStore.brickLayout = _resolutionStatsToBrickLayout(imageWrapper.getMultiResolutionStats());
639
+ // log.debug('zarrStore', this.zarrStore);
640
+ // init channel mappings
641
+ // log.debug('initializing channel mappings');
642
+ log.debug('config', config);
643
+ // for each key in config, add to channel mappings
644
+ const { omero } = this.ngffMetadata || {};
645
+ if (!omero) {
646
+ // TODO(mark): do not use omero metadata directly, use the ImageWrapper channel methods.
647
+ throw new Error('Expected omero metadata in ngffMetadata');
648
+ }
649
+ log.debug('omero', omero);
650
+ Object.keys(config).forEach((key, i) => {
651
+ const configChannel = config[key].spatialTargetC;
652
+ this.channels.zarrMappings[i] = configChannel;
653
+ this.channels.colorMappings[i] = i;
654
+ this.channels.downsampleMin[i] = omero?.channels?.[configChannel]?.window?.min || 0;
655
+ this.channels.downsampleMax[i] = omero?.channels?.[configChannel]?.window?.max || 65535;
656
+ });
657
+ log.debug('zarrMappings after init', this.channels.zarrMappings);
658
+ log.debug('colorMappings after init', this.channels.colorMappings);
659
+ log.debug('downsampleMin after init', this.channels.downsampleMin);
660
+ log.debug('downsampleMax after init', this.channels.downsampleMax);
661
+ // Initialize MRMCPT textures after we have all the necessary information
662
+ this.initMRMCPT();
663
+ // this.populateMRMCPT();
664
+ // this.testTexture();
665
+ }
666
+ this.initStatus = INIT_STATUS.COMPLETE;
667
+ logWithColor('INIT() COMPLETE');
668
+ // log.debug(this.zarrStore);
669
+ /*
670
+ if (this.ngffMetadata.coordinateTransformations
671
+ && this.ngffMetadata.coordinateTransformations[0]
672
+ && this.ngffMetadata.coordinateTransformations[0].scale) {
673
+ // log.debug('scale', this.ngffMetadata.coordinateTransformations[0].scale);
674
+ }
675
+ */
676
+ // Return data that can be displayed in the HUD
677
+ return {
678
+ success: true,
679
+ deviceLimits: this.deviceLimits,
680
+ zarrStore: this.zarrStore,
681
+ // physicalScale: this.physicalScale,
682
+ physicalSizeTotal: this.zarrStore.physicalSizeTotal,
683
+ physicalSizeVoxel: this.zarrStore.physicalSizeVoxel,
684
+ error: null,
685
+ };
686
+ }
687
+ catch (error) {
688
+ logWithColor('INIT() FAILED');
689
+ log.error('Error initializing VolumeDataManager:', error);
690
+ this.initStatus = INIT_STATUS.FAILED;
691
+ this.initError = error.message || 'Unknown error';
692
+ return {
693
+ success: false,
694
+ error: this.initError,
695
+ };
696
+ }
697
+ }
698
+ /**
699
+ * Initialize the BrickCache and PageTable
700
+ * MRMCPT: multi-resolution multi-channel page table
701
+ *
702
+ * Depends on:
703
+ * - zarrStore.brickLayout
704
+ * - zarrMappings.length (zarrMappings: the zarr channel index for every one of the up to 7 channels)
705
+ * -
706
+ */
707
+ initMRMCPT() {
708
+ logWithColor('initMRMCPT');
709
+ // log.debug('initMRMCPT', this.zarrStore.shapes[0]);
710
+ // log.debug('initMRMCPT', this.zarrStore.channelCount);
711
+ // log.debug('initMRMCPT', this.channels.zarrMappings);
712
+ // log.debug('initMRMCPT', this.channels.colorMappings);
713
+ const { PT, ptTHREE, bcTHREE } = _initMRMCPT(this.zarrStore.brickLayout, this.channels.zarrMappings.length);
714
+ this.PT = PT;
715
+ this.ptTHREE = ptTHREE;
716
+ this.bcTHREE = bcTHREE;
717
+ // log.debug('gl', this.gl);
718
+ // log.debug('renderer', this.renderer);
719
+ logWithColor('initMRMCPT() COMPLETE');
720
+ }
721
+ /*
722
+ testTexture() {
723
+ log.debug('testTexture pt', this.ptTHREE);
724
+ log.debug('testTexture bc', this.bcTHREE);
725
+ }
726
+ */
727
+ async initTexture() {
728
+ const requests = [
729
+ { x: 0, y: 0, z: 1 },
730
+ ];
731
+ await this.handleBrickRequests(requests);
732
+ }
733
+ updateChannels(channelProps) {
734
+ logWithColor('updateChannels');
735
+ log.debug('channelProps', channelProps);
736
+ // log.error('TODO: init channel mappings first ');
737
+ log.debug('this.channels.zarrMappings', this.channels.zarrMappings);
738
+ log.debug('this.channels.colorMappings', this.channels.colorMappings);
739
+ log.debug('this.channels.downsampleMin', this.channels.downsampleMin);
740
+ log.debug('this.channels.downsampleMax', this.channels.downsampleMax);
741
+ if (this.channels.zarrMappings.length === 0) {
742
+ log.debug('channels not initialized yet');
743
+ return;
744
+ }
745
+ // Early exit check: compare requested mappings with current mappings
746
+ const requestedZarrChannels = Object.values(channelProps)
747
+ .map(channelData => channelData.spatialTargetC)
748
+ .filter(targetC => targetC !== undefined);
749
+ const currentZarrChannels = this.channels.zarrMappings
750
+ .filter(mapping => mapping !== undefined);
751
+ // Create sorted arrays for comparison
752
+ const requestedSorted = [...new Set(requestedZarrChannels)].sort((a, b) => a - b);
753
+ const currentSorted = [...new Set(currentZarrChannels)].sort((a, b) => a - b);
754
+ // Early exit if mappings haven't changed
755
+ if (requestedSorted.length === currentSorted.length
756
+ && requestedSorted.every((val, index) => val === currentSorted[index])) {
757
+ log.debug('Channel mappings unchanged, skipping update');
758
+ // return;
759
+ }
760
+ log.debug('Channel mappings changed:', {
761
+ current: currentSorted,
762
+ requested: requestedSorted,
763
+ });
764
+ // Instead of for...of loop, use Object.entries() with forEach
765
+ Object.entries(channelProps).forEach(([uiChannelKey, channelData]) => {
766
+ const targetZarrChannel = channelData.spatialTargetC;
767
+ log.debug(`UI channel "${uiChannelKey}" wants zarr channel ${targetZarrChannel}`);
768
+ // Check if this zarr channel is already mapped
769
+ const existingSlotIndex = this.channels.zarrMappings.indexOf(targetZarrChannel);
770
+ if (existingSlotIndex === -1) {
771
+ // Need to allocate a new slot for this zarr channel
772
+ const nextFreeSlot = this.channels.zarrMappings.findIndex(slot => slot === undefined);
773
+ if (nextFreeSlot !== -1) {
774
+ this.channels.zarrMappings[nextFreeSlot] = targetZarrChannel;
775
+ log.debug('channelData', channelData);
776
+ log.debug('this.ngffMetadata?.omero?.channels', this.ngffMetadata?.omero?.channels);
777
+ log.debug('targetZarrChannel', targetZarrChannel);
778
+ this.channels.downsampleMin[nextFreeSlot] = this.ngffMetadata?.omero?.channels?.[targetZarrChannel]?.window?.min || 0;
779
+ this.channels.downsampleMax[nextFreeSlot] = this.ngffMetadata?.omero?.channels?.[targetZarrChannel]?.window?.max || 65535;
780
+ log.debug(`Mapped zarr channel ${targetZarrChannel} to slot ${nextFreeSlot}`);
781
+ log.debug('channels', this.channels);
782
+ }
783
+ else {
784
+ log.debug('No free slots found, looking for unused mapped channels');
785
+ // Find zarr channels that are currently mapped but no longer requested
786
+ const currentlyMapped = this.channels.zarrMappings.filter(mapping => mapping !== undefined);
787
+ const stillRequested = requestedZarrChannels; // We calculated this earlier
788
+ const unusedMappedChannels = currentlyMapped.filter(mappedChannel => !stillRequested.includes(mappedChannel));
789
+ log.debug('Currently mapped:', currentlyMapped);
790
+ log.debug('Still requested:', stillRequested);
791
+ log.debug('Unused mapped channels:', unusedMappedChannels);
792
+ if (unusedMappedChannels.length > 0) {
793
+ // Find the first slot that maps to an unused zarr channel
794
+ const slotToReuse = this.channels.zarrMappings.findIndex(mapping => unusedMappedChannels.includes(mapping));
795
+ if (slotToReuse !== -1) {
796
+ const oldZarrChannel = this.channels.zarrMappings[slotToReuse];
797
+ this.channels.zarrMappings[slotToReuse] = targetZarrChannel;
798
+ this.channels.downsampleMin[slotToReuse] = this.ngffMetadata?.omero?.channels?.[targetZarrChannel]?.window?.min || 0;
799
+ this.channels.downsampleMax[slotToReuse] = this.ngffMetadata?.omero?.channels?.[targetZarrChannel]?.window?.max || 65535;
800
+ log.debug(`Reused slot ${slotToReuse}: ${oldZarrChannel} -> ${targetZarrChannel}`);
801
+ this._purgeChannel(slotToReuse);
802
+ }
803
+ else {
804
+ log.error('Could not find slot to reuse - this should not happen');
805
+ }
806
+ }
807
+ else {
808
+ log.error('All slots are full and all mapped channels are still in use');
809
+ }
810
+ }
811
+ }
812
+ else {
813
+ log.debug(`Zarr channel ${targetZarrChannel} already mapped to slot ${existingSlotIndex}`);
814
+ }
815
+ });
816
+ const newColorMappings = Object.values(channelProps).map((ch) => {
817
+ const slot = this.channels.zarrMappings.indexOf(ch.spatialTargetC);
818
+ return slot !== -1 ? slot : -1;
819
+ });
820
+ while (newColorMappings.length < 7) {
821
+ newColorMappings.push(-1);
822
+ }
823
+ log.debug('newColorMappings', newColorMappings);
824
+ this.channels.colorMappings = newColorMappings;
825
+ log.debug('updatedChannels', this.channels);
826
+ }
827
+ /**
828
+ * Try to load a resolution level
829
+ * @param {number} resolutionIndex - The resolution level to load
830
+ * @param {Array} arrays - Array to store the loaded arrays
831
+ * @returns {Promise} Promise resolving when the resolution is loaded or rejected
832
+ */
833
+ /*
834
+ async tryLoadResolution(resolutionIndex, arrays) {
835
+ logWithColor('tryLoadResolution');
836
+ log.debug(resolutionIndex, arrays);
837
+ try {
838
+ const array = await zarrita.open(this.group.resolve(String(resolutionIndex)));
839
+ // Create new arrays to avoid modifying parameters directly
840
+ const newArrays = [...arrays];
841
+ newArrays[resolutionIndex] = array;
842
+ // Update the original arrays
843
+ Object.assign(arrays, newArrays);
844
+ logWithColor('tryLoadResolution() COMPLETE');
845
+ return { success: true, level: resolutionIndex };
846
+ } catch (err) {
847
+ log.error(`Failed to load resolution ${resolutionIndex}:`, err);
848
+ return { success: false, level: resolutionIndex, error: err.message };
849
+ }
850
+ }
851
+ */
852
+ /**
853
+ * Get physical dimensions
854
+ * @returns {Array} Physical dimensions [X, Y, Z]
855
+ */
856
+ getPhysicalDimensionsXYZ() {
857
+ log.debug('getPhysicalDimensionsXYZ');
858
+ log.debug('this.zarrStore.physicalSizeTotal', this.zarrStore.physicalSizeTotal);
859
+ const out = [this.zarrStore.physicalSizeTotal[2],
860
+ this.zarrStore.physicalSizeTotal[1],
861
+ this.zarrStore.physicalSizeTotal[0]];
862
+ log.debug('out', out);
863
+ return out;
864
+ }
865
+ /**
866
+ * Get the maximum resolution
867
+ * @returns {number} Maximum resolution
868
+ */
869
+ getMaxResolutionXYZ() {
870
+ log.debug('getMaxResolutionXYZ');
871
+ log.debug('this.zarrStore.shapes', this.zarrStore.shapes);
872
+ const out = [this.zarrStore.shapes[0][4],
873
+ this.zarrStore.shapes[0][3],
874
+ this.zarrStore.shapes[0][2]];
875
+ log.debug('out', out);
876
+ return out;
877
+ }
878
+ getOriginalScaleXYZ() {
879
+ logWithColor('getOriginalScaleXYZ');
880
+ log.debug('this.zarrStore.physicalSizeVoxel', this.zarrStore.physicalSizeVoxel);
881
+ const out = [this.zarrStore.physicalSizeVoxel[2],
882
+ this.zarrStore.physicalSizeVoxel[1],
883
+ this.zarrStore.physicalSizeVoxel[0]];
884
+ log.debug('out', out);
885
+ return out;
886
+ }
887
+ getNormalizedScaleXYZ() {
888
+ log.debug('getNormalizedScaleXYZ');
889
+ const out = [
890
+ 1.0,
891
+ this.zarrStore.physicalSizeVoxel[1] / this.zarrStore.physicalSizeVoxel[2],
892
+ this.zarrStore.physicalSizeVoxel[0] / this.zarrStore.physicalSizeVoxel[0],
893
+ ];
894
+ log.debug('out', out);
895
+ return out;
896
+ }
897
+ getBoxDimensionsXYZ() {
898
+ log.debug('getBoxDimensionsXYZ');
899
+ log.debug('this.zarrStore.shapes', this.zarrStore.shapes);
900
+ const out = [1,
901
+ this.zarrStore.shapes[0][3] / this.zarrStore.shapes[0][4],
902
+ this.zarrStore.shapes[0][2] / this.zarrStore.shapes[0][4],
903
+ ];
904
+ log.debug('out', out);
905
+ return out;
906
+ }
907
+ /**
908
+ * Load a specific Zarr chunk based on [t,c,z,y,x] coordinates
909
+ * @param {number} t - Time point (default 0)
910
+ * @param {number} c - Channel (default 0)
911
+ * @param {number} z - Z coordinate
912
+ * @param {number} y - Y coordinate
913
+ * @param {number} x - X coordinate
914
+ * @param {number} resolution - Resolution level
915
+ * @returns {Promise<Uint8Array>} 32x32x32 chunk data
916
+ */
917
+ async loadZarrChunk(t = 0, c = 0, z, y, x, resolution) {
918
+ // logWithColor('loadZarrChunk');
919
+ if (!this.zarrStore || !this.zarrStore.arrays[resolution]) {
920
+ throw new Error('Zarr store or resolution not initialized');
921
+ }
922
+ // log.debug('loadZarrChunk', t, c, z, y, x, resolution);
923
+ const array = this.zarrStore.arrays[resolution];
924
+ // TODO: Use zarrita's get(arr, slice) method (rather than arr.getChunk),
925
+ // which will enable reading from arrays with other chunk shapes
926
+ // (i.e., not strictly 32x32x32 chunks).
927
+ // To do so, will need to determine the slice indices corresponding
928
+ // to each "assumed 32x32x32 chunk key".
929
+ // Reference: https://zarrita.dev/slicing.html
930
+ const chunkEntry = await array.getChunk([t, c, z, y, x]);
931
+ // log.debug('chunkEntry', chunkEntry);
932
+ if (!chunkEntry) {
933
+ throw new Error(`No chunk found at coordinates [${t},${c},${z},${y},${x}]`);
934
+ }
935
+ // Ensure the chunk is the expected size
936
+ if (chunkEntry.data.length !== BRICK_SIZE * BRICK_SIZE * BRICK_SIZE) {
937
+ throw new Error(`Unexpected chunk size: ${chunkEntry.data.length}`);
938
+ }
939
+ return chunkEntry.data;
940
+ }
941
+ async processRequestData(buffer) {
942
+ if (this.isBusy) {
943
+ log.debug('processRequestData: already busy, skipping');
944
+ return;
945
+ }
946
+ this.isBusy = true;
947
+ this.triggerRequest = false;
948
+ const { requests, origRequestCount } = _requestBufferToRequestObjects(buffer, this.k);
949
+ if (requests.length === 0) {
950
+ this.noNewRequests = true;
951
+ }
952
+ log.debug(`processRequestData: handling ${requests.length} requests of ${origRequestCount}`);
953
+ await this.handleBrickRequests(requests);
954
+ // We want processRequestData to alternate with processUsageData.
955
+ this.triggerUsage = true;
956
+ this.isBusy = false;
957
+ }
958
+ async processUsageData(buffer) {
959
+ if (this.isBusy) {
960
+ log.debug('processUsageData: already busy, skipping');
961
+ this.needsBailout = true; // Set a flag to indicate we need to bail out of processing requests
962
+ return;
963
+ }
964
+ this.isBusy = true;
965
+ this.triggerUsage = false;
966
+ const now = ++this.timeStamp;
967
+ const usedBricks = new Set();
968
+ // Process buffer to identify used bricks
969
+ for (let i = 0; i < buffer.length; i += 4) {
970
+ const x = buffer[i];
971
+ const y = buffer[i + 1];
972
+ const z = buffer[i + 2];
973
+ // Skip empty entries
974
+ if ((x | y | z) === 0) {
975
+ // eslint-disable-next-line no-continue
976
+ continue;
977
+ }
978
+ // Calculate brick index
979
+ const bcIndex = z * BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y + y * BRICK_CACHE_SIZE_X + x;
980
+ // Only add valid indices
981
+ if (bcIndex < this.BCTimeStamps.length) {
982
+ usedBricks.add(bcIndex);
983
+ }
984
+ }
985
+ // Update timestamps for all used bricks at once
986
+ Array.from(usedBricks).forEach((bcIndex) => {
987
+ this.BCTimeStamps[bcIndex] = now;
988
+ });
989
+ // Check if brick cache is full based on allocation index
990
+ // this.BCFull = this.BCUnusedIndex >= this.BCTimeStamps.length;
991
+ // If cache is full or we want to always maintain the LRU list
992
+ // if (this.BCFull || this.BCUnusedIndex >= this.BCTimeStamps.length - this.k) { // Always update LRU list
993
+ // do NOT overwrite BCFull; it is latched in _allocateBCSlots()
994
+ if (this.BCFull)
995
+ this._buildLRU(); // The brick cache is full, so update the list of least recently used bricks, as it will be needed for eviction during the next _allocateBCSlots
996
+ // log.debug('this.BCTimeStamps', this.BCTimeStamps);
997
+ // log.debug('this.timeStamp', this.timeStamp);
998
+ // We want processRequestData to alternate with processUsageData.
999
+ this.triggerRequest = true;
1000
+ this.isBusy = false;
1001
+ }
1002
+ // Helper method to update PT entries for evicted bricks
1003
+ _evictBrick(bcIndex) {
1004
+ const pt = this.bc2pt[bcIndex];
1005
+ if (!pt)
1006
+ return; // brick was never resident
1007
+ const [min, max] = this.BCMinMax[bcIndex] || [0, 0];
1008
+ const ptVal = ((0 << 31) // not-resident
1009
+ | (1 << 30) // init-done
1010
+ | (Math.min(127, min >> 1) << 23)
1011
+ | (Math.min(127, max >> 1) << 16)) >>> 0;
1012
+ this._updatePTEntry(pt.x, pt.y, pt.z, ptVal); // ← PT coord!
1013
+ this.bc2pt[bcIndex] = null; // slot now free
1014
+ }
1015
+ _purgeChannel(ptChannelIndex) {
1016
+ log.debug('purging channel', ptChannelIndex);
1017
+ log.debug('corresponding zarr channel', this.channels.zarrMappings[ptChannelIndex]);
1018
+ if (!this.ptTHREE) {
1019
+ log.error('pagetable texture not initialized');
1020
+ return;
1021
+ }
1022
+ this.channels.downsampleMin[ptChannelIndex] = undefined;
1023
+ this.channels.downsampleMax[ptChannelIndex] = undefined;
1024
+ this.channels.zarrMappings[ptChannelIndex] = undefined;
1025
+ const channelMask = this.PT.channelOffsets[ptChannelIndex];
1026
+ log.debug('channelMask', channelMask);
1027
+ log.error('TODO: not tested yet');
1028
+ const { gl } = this;
1029
+ const texPT = this.renderer.properties.get(this.ptTHREE).__webglTexture;
1030
+ gl.activeTexture(gl.TEXTURE0);
1031
+ gl.bindTexture(gl.TEXTURE_3D, texPT);
1032
+ for (let r = 0; r < this.zarrStore.resolutions; r++) {
1033
+ const anchor = [
1034
+ this.PT.anchors[r][2] * channelMask[2],
1035
+ this.PT.anchors[r][1] * channelMask[1],
1036
+ this.PT.anchors[r][0] * channelMask[0],
1037
+ ];
1038
+ log.debug('anchor', anchor);
1039
+ const extents = this.zarrStore.brickLayout[r];
1040
+ const size = extents[0] * extents[1] * extents[2];
1041
+ log.debug('extents', extents);
1042
+ log.debug('size', size);
1043
+ // texsub 3D
1044
+ // replace with a block of zeros
1045
+ gl.texSubImage3D(gl.TEXTURE_3D, 0, anchor[0], anchor[1], anchor[2], extents[0], extents[1], extents[2], gl.RED_INTEGER, gl.UNSIGNED_INT, new Uint32Array(size));
1046
+ }
1047
+ gl.bindTexture(gl.TEXTURE_3D, null);
1048
+ }
1049
+ // Update a PT entry
1050
+ _updatePTEntry(ptX, ptY, ptZ, ptVal) {
1051
+ if (!this.ptTHREE)
1052
+ return;
1053
+ const { gl } = this;
1054
+ const texPT = this.renderer.properties.get(this.ptTHREE).__webglTexture;
1055
+ gl.activeTexture(gl.TEXTURE0);
1056
+ gl.bindTexture(gl.TEXTURE_3D, texPT);
1057
+ gl.texSubImage3D(gl.TEXTURE_3D, 0, ptX, ptY, ptZ, 1, 1, 1, gl.RED_INTEGER, gl.UNSIGNED_INT, new Uint32Array([ptVal]));
1058
+ gl.bindTexture(gl.TEXTURE_3D, null);
1059
+ }
1060
+ /* ------------------------------------------------------------- *
1061
+ * 2. Allocate the next n free bricks in the brick cache *
1062
+ * ------------------------------------------------------------- */
1063
+ /**
1064
+ *
1065
+ * @param {number} n The number of slots to allocate
1066
+ * @returns {{ bcIndex, x, y, z }[]} Array of brick cache coordinates for the allocated slots.
1067
+ */
1068
+ _allocateBCSlots(n) {
1069
+ let slots = [];
1070
+ const total = BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y * BRICK_CACHE_SIZE_Z;
1071
+ if (!this.BCFull && this.BCUnusedIndex + n > total) {
1072
+ // The brick cache was not already full,
1073
+ // but allocating N more bricks will put us over the limit.
1074
+ this.BCFull = true;
1075
+ log.debug('BRICK CACHE FULL');
1076
+ }
1077
+ // Do the allocation.
1078
+ if (!this.BCFull) {
1079
+ for (let i = 0; i < n; ++i) {
1080
+ const bcIndex = (this.BCUnusedIndex + i) % total; // wrap for now
1081
+ const z = Math.floor(bcIndex / (BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y));
1082
+ const rem = bcIndex - z * BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y;
1083
+ const y = Math.floor(rem / BRICK_CACHE_SIZE_X);
1084
+ const x = rem % BRICK_CACHE_SIZE_X;
1085
+ slots.push({ bcIndex, x, y, z });
1086
+ }
1087
+ this.BCUnusedIndex += n;
1088
+ }
1089
+ else { // ---- BC already full ----
1090
+ // Before proceeding to choose old slots from the LRU stack,
1091
+ // we need to ensure that the LRU stack is at least as long as N
1092
+ // (since N is the number of new brick slots to allocate).
1093
+ if (this.LRUStack.length < n)
1094
+ this._buildLRU(); // top-up list
1095
+ // Use the LRU stack to evict the least-recently-used bricks
1096
+ // and return their coordinates for the new bricks to use.
1097
+ slots = this.LRUStack.splice(0, n).map((bcIndex) => {
1098
+ this._evictBrick(bcIndex);
1099
+ const z = Math.floor(bcIndex / (BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y));
1100
+ const rem = bcIndex - z * BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y;
1101
+ const y = Math.floor(rem / BRICK_CACHE_SIZE_X);
1102
+ const x = rem % BRICK_CACHE_SIZE_X;
1103
+ return { bcIndex, x, y, z };
1104
+ });
1105
+ }
1106
+ return slots;
1107
+ }
1108
+ /* ------------------------------------------------------------- *
1109
+ * 4. Upload one brick + PT entry *
1110
+ * ------------------------------------------------------------- */
1111
+ async _uploadBrick(ptCoord, bcSlot) {
1112
+ // log.debug('uploading brick', ptCoord, bcSlot);
1113
+ if (ptCoord.x >= this.PT.xExtent
1114
+ || ptCoord.y >= this.PT.yExtent
1115
+ || ptCoord.z >= this.PT.zTotal
1116
+ || ptCoord.x < 0
1117
+ || ptCoord.y < 0
1118
+ || ptCoord.z < 0) {
1119
+ log.error('this.PT', this.PT);
1120
+ log.error('ptCoord out of bounds', ptCoord);
1121
+ return;
1122
+ }
1123
+ // log.debug('ptCoord', ptCoord);
1124
+ // log.debug('this.PT', this.PT);
1125
+ /* 4.1 fetch chunk from Zarr */
1126
+ const { channel, resolution, x, y, z } = _ptToZarr(ptCoord.x, ptCoord.y, ptCoord.z, { PT_zExtent: this.PT.zExtent, PT_z0Extent: this.PT.z0Extent, PT_anchors: this.PT.anchors });
1127
+ const zarrChannel = this.channels.zarrMappings[channel];
1128
+ if (zarrChannel === undefined || zarrChannel === -1) {
1129
+ log.error('zarrChannel is undefined or -1', zarrChannel);
1130
+ return;
1131
+ }
1132
+ log.debug('starting to load zarr chunk', { resolution, z, y, x, zarrChannel });
1133
+ let chunk = await this.loadZarrChunk(0, zarrChannel, z, y, x, resolution);
1134
+ // log.debug('chunk', chunk);
1135
+ // TODO(mark): is there a better way than iterating over all of the chunk values?
1136
+ if (chunk instanceof Uint16Array) {
1137
+ log.debug('chunk is Uint16Array, converting to Uint8Array');
1138
+ if (this.channels.downsampleMin[channel] === undefined) {
1139
+ // get the channel ID from this.channels.zarrMappings
1140
+ const channelId = this.channels.zarrMappings[channel];
1141
+ log.debug('channelId was not found in this.channels.downsampleMin[channel]', channelId);
1142
+ // get the downsample min and max for the channel from omero
1143
+ this.channels.downsampleMin[channel] = this.ngffMetadata?.omero?.channels?.[channelId]?.window?.min || 0;
1144
+ this.channels.downsampleMax[channel] = this.ngffMetadata?.omero?.channels?.[channelId]?.window?.max || 65535;
1145
+ log.debug('this.channels.downsampleMin[channel]', this.channels.downsampleMin[channel]);
1146
+ log.debug('this.channels.downsampleMax[channel]', this.channels.downsampleMax[channel]);
1147
+ }
1148
+ // TODO(mark): to reduce memory usage, create the Uint8Array as a view of the Uint16Array
1149
+ // and use bitwise operations. Then scale the values.
1150
+ const uint8Chunk = new Uint8Array(chunk.length);
1151
+ for (let i = 0; i < chunk.length; i++) {
1152
+ // Scale from 0-65535 to 0-255
1153
+ uint8Chunk[i] = Math.floor((chunk[i] - this.channels.downsampleMin[channel]) / (this.channels.downsampleMax[channel] - this.channels.downsampleMin[channel]) * 255);
1154
+ }
1155
+ chunk = uint8Chunk;
1156
+ }
1157
+ if (!(chunk instanceof Uint8Array)) {
1158
+ throw new Error(`Unsupported chunk type: ${chunk.constructor.name}. Expected Uint8Array.`);
1159
+ }
1160
+ let min = 255;
1161
+ let max = 0;
1162
+ for (let i = 0; i < chunk.length; ++i) {
1163
+ const v = chunk[i];
1164
+ if (v < min)
1165
+ min = v;
1166
+ if (v > max)
1167
+ max = v;
1168
+ }
1169
+ /* 4.3 brick‑cache upload */
1170
+ const { gl } = this;
1171
+ const texBC = this.renderer.properties.get(this.bcTHREE).__webglTexture;
1172
+ gl.activeTexture(gl.TEXTURE2);
1173
+ gl.bindTexture(gl.TEXTURE_3D, texBC);
1174
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
1175
+ gl.texSubImage3D(gl.TEXTURE_3D, 0, bcSlot.x * BRICK_SIZE, bcSlot.y * BRICK_SIZE, bcSlot.z * BRICK_SIZE, BRICK_SIZE, BRICK_SIZE, BRICK_SIZE, gl.RED, gl.UNSIGNED_BYTE, chunk);
1176
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);
1177
+ gl.bindTexture(gl.TEXTURE_3D, null);
1178
+ // log.debug('uploaded brick');
1179
+ const error = gl.getError();
1180
+ if (error !== gl.NO_ERROR) {
1181
+ log.error('WebGL error during brick upload:', error, chunk);
1182
+ }
1183
+ /* 4.4 PT entry upload */
1184
+ if (channel >= this.channels.zarrMappings.length) {
1185
+ log.debug('channel is out of bounds', channel);
1186
+ min = 255;
1187
+ max = 255;
1188
+ }
1189
+ const ptVal = _packPT(min, max, bcSlot.x, bcSlot.y, bcSlot.z);
1190
+ const texPT = this.renderer.properties.get(this.ptTHREE).__webglTexture;
1191
+ gl.activeTexture(gl.TEXTURE0);
1192
+ gl.bindTexture(gl.TEXTURE_3D, texPT);
1193
+ gl.texSubImage3D(gl.TEXTURE_3D, 0, ptCoord.x, ptCoord.y, ptCoord.z, 1, 1, 1, gl.RED_INTEGER, gl.UNSIGNED_INT, new Uint32Array([ptVal]));
1194
+ gl.bindTexture(gl.TEXTURE_3D, null);
1195
+ // log.debug('uploaded PT entry');
1196
+ // log.debug('ptVal', ptVal);
1197
+ // log.debug('min', min);
1198
+ // log.debug('max', max);
1199
+ // log.debug('bcSlot', bcSlot);
1200
+ // log.debug('channel', channel);
1201
+ // log.debug('resolution', resolution);
1202
+ // log.debug('x', x);
1203
+ // log.debug('y', y);
1204
+ // log.debug('z', z);
1205
+ // log.debug('ptCoord', ptCoord);
1206
+ const error2 = gl.getError();
1207
+ if (error2 !== gl.NO_ERROR) {
1208
+ log.error('WebGL error during pagetable upload:', error2, chunk);
1209
+ }
1210
+ /* 4.5 bookkeeping */
1211
+ // const now = ++this.timeStamp;
1212
+ this.BCTimeStamps[bcSlot.bcIndex] = this.timeStamp;
1213
+ this.BCMinMax[bcSlot.bcIndex] = [min, max];
1214
+ this.bc2pt[bcSlot.bcIndex] = ptCoord;
1215
+ // log.debug('bookkeeping');
1216
+ }
1217
+ /* ------------------------------------------------------------- *
1218
+ * 5. Public: handle a batch of PT requests (array of {x,y,z}) *
1219
+ * ------------------------------------------------------------- */
1220
+ async handleBrickRequests(ptRequests) {
1221
+ // log.debug('handleBrickRequests');
1222
+ if (ptRequests.length === 0)
1223
+ return;
1224
+ // log.debug('ptRequests', ptRequests);
1225
+ // log.debug('this.BCFull', this.BCFull);
1226
+ // log.debug('this.BCUnusedIndex', this.BCUnusedIndex);
1227
+ // log.debug('this.BCTimeStamps', this.BCTimeStamps);
1228
+ // log.debug('this.LRUStack', this.LRUStack);
1229
+ /* <= k requests, allocate same number of bricks */
1230
+ const slots = this._allocateBCSlots(ptRequests.length);
1231
+ // log.debug('slots', slots);
1232
+ /* upload each (sequentially or Promise.all if you prefer IO overlap) */
1233
+ log.debug('handleBrickRequests: starting for loop');
1234
+ for (let i = 0; i < ptRequests.length; ++i) {
1235
+ log.debug('uploading brick', ptRequests[i], slots[i]);
1236
+ // eslint-disable-next-line no-await-in-loop
1237
+ await this._uploadBrick(ptRequests[i], slots[i]);
1238
+ // this.bricksAllocated++;
1239
+ const rlength = this.bricksEverLoaded.size;
1240
+ this.bricksEverLoaded.add(`${ptRequests[i].x},${ptRequests[i].y},${ptRequests[i].z}`);
1241
+ if (rlength === this.bricksEverLoaded.size) {
1242
+ log.debug('DUPLICATE BRICK LOADED', ptRequests[i]);
1243
+ }
1244
+ if (this.needsBailout) {
1245
+ log.debug('Bailing out of handleBrickRequests early due to needsBailout flag');
1246
+ this.needsBailout = false; // Reset the flag
1247
+ break; // Exit the loop early
1248
+ // TODO(mark): do something with the allocated slots that were not used?
1249
+ }
1250
+ }
1251
+ // log.debug('this.bricksAllocated', this.bricksAllocated);
1252
+ log.debug('this.bricksEverLoaded', this.bricksEverLoaded);
1253
+ // log.debug('uploaded bricks');
1254
+ /* let Three.js know textures changed */
1255
+ // this.bcTHREE.needsUpdate = true;
1256
+ // this.ptTHREE.needsUpdate = true;
1257
+ }
1258
+ /* --------------------------------------------------------- *
1259
+ * Rebuild the LRUStack with the k least-recently-used bricks *
1260
+ * --------------------------------------------------------- */
1261
+ _buildLRU() {
1262
+ // Update this.LRUStack based on BCTimeStamps.
1263
+ // BCTimeStamps is updated during both initial brick upload and processUsageData.
1264
+ // However it is not until _buildLRU is called that the timestamps are sorted
1265
+ // to update this.LRUStack.
1266
+ const brickIndicesWithTimes = this.BCTimeStamps
1267
+ .map((time, index) => ({ index, time }));
1268
+ this.LRUStack = brickIndicesWithTimes
1269
+ .sort((a, b) => a.time - b.time)
1270
+ .slice(0, this.k)
1271
+ .map(item => item.index);
1272
+ }
1273
+ }