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