@vitessce/spatial-accelerated 3.8.5 → 3.8.7

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/dist/index.js CHANGED
@@ -49364,6 +49364,7 @@ function _requestBufferToRequestObjects(buffer, k) {
49364
49364
  class VolumeDataManager {
49365
49365
  constructor(glParam) {
49366
49366
  logWithColor$2("CLASS INITIALIZING");
49367
+ log.debug("VolumeDataManager constructor", { glParam, glParamContext: glParam.getContext?.() });
49367
49368
  const gl = glParam.getContext?.() || glParam;
49368
49369
  const renderer = glParam;
49369
49370
  if (gl.domElement && gl.getContext) {
@@ -49386,12 +49387,20 @@ class VolumeDataManager {
49386
49387
  };
49387
49388
  return defaults[param] || 0;
49388
49389
  },
49390
+ isContextLost: () => false,
49389
49391
  MAX_TEXTURE_SIZE: "MAX_TEXTURE_SIZE",
49390
49392
  MAX_3D_TEXTURE_SIZE: "MAX_3D_TEXTURE_SIZE",
49391
49393
  MAX_RENDERBUFFER_SIZE: "MAX_RENDERBUFFER_SIZE",
49392
49394
  MAX_UNIFORM_BUFFER_BINDINGS: "MAX_UNIFORM_BUFFER_BINDINGS"
49393
49395
  };
49394
49396
  }
49397
+ this._originalGlParam = glParam;
49398
+ this._isContextLost = false;
49399
+ this._contextRestoredCallbacks = [];
49400
+ if (this.gl && this.gl.canvas) {
49401
+ this.gl.canvas.addEventListener("webglcontextlost", this._handleContextLost.bind(this));
49402
+ this.gl.canvas.addEventListener("webglcontextrestored", this._handleContextRestored.bind(this));
49403
+ }
49395
49404
  log.debug("GL CONSTANTS");
49396
49405
  log.debug(this.gl);
49397
49406
  log.debug(this.gl.TEXTURE0);
@@ -49483,8 +49492,77 @@ class VolumeDataManager {
49483
49492
  this.needsBailout = false;
49484
49493
  this.initStatus = INIT_STATUS.NOT_STARTED;
49485
49494
  this.initError = null;
49495
+ this._lastChannelConfig = null;
49486
49496
  logWithColor$2("VolumeDataManager constructor complete");
49487
49497
  }
49498
+ /**
49499
+ * Handle WebGL context loss
49500
+ */
49501
+ _handleContextLost(event) {
49502
+ logWithColor$2("CONTEXT LOST");
49503
+ log.warn("WebGL context lost, preventing default and setting flag");
49504
+ event.preventDefault();
49505
+ this._isContextLost = true;
49506
+ if (this.channels && this.channels.zarrMappings) {
49507
+ this._lastChannelConfig = {
49508
+ zarrMappings: [...this.channels.zarrMappings],
49509
+ colorMappings: [...this.channels.colorMappings],
49510
+ downsampleMin: [...this.channels.downsampleMin],
49511
+ downsampleMax: [...this.channels.downsampleMax]
49512
+ };
49513
+ }
49514
+ }
49515
+ /**
49516
+ * Handle WebGL context restoration
49517
+ */
49518
+ _handleContextRestored(event) {
49519
+ logWithColor$2("CONTEXT RESTORED");
49520
+ log.warn("WebGL context restored, reinitializing textures");
49521
+ this._isContextLost = false;
49522
+ if (this._originalGlParam && this._originalGlParam.getContext) {
49523
+ this.gl = this._originalGlParam.getContext();
49524
+ }
49525
+ if (this._lastChannelConfig) {
49526
+ this.channels.zarrMappings = [...this._lastChannelConfig.zarrMappings];
49527
+ this.channels.colorMappings = [...this._lastChannelConfig.colorMappings];
49528
+ this.channels.downsampleMin = [...this._lastChannelConfig.downsampleMin];
49529
+ this.channels.downsampleMax = [...this._lastChannelConfig.downsampleMax];
49530
+ log.debug("Restored channel configuration after context loss");
49531
+ }
49532
+ if (this.PT && this.zarrStore && this.zarrStore.brickLayout) {
49533
+ try {
49534
+ this.initMRMCPT();
49535
+ log.debug("Successfully reinitialized MRMCPT after context restoration");
49536
+ } catch (error) {
49537
+ log.error("Failed to reinitialize MRMCPT after context restoration:", error);
49538
+ }
49539
+ }
49540
+ this._contextRestoredCallbacks.forEach((callback) => {
49541
+ try {
49542
+ callback();
49543
+ } catch (error) {
49544
+ log.error("Error in context restored callback:", error);
49545
+ }
49546
+ });
49547
+ }
49548
+ /**
49549
+ * Check if WebGL context is lost
49550
+ */
49551
+ isContextLost() {
49552
+ if (this._isContextLost) return true;
49553
+ if (this.gl && typeof this.gl.isContextLost === "function") {
49554
+ return this.gl.isContextLost();
49555
+ }
49556
+ return false;
49557
+ }
49558
+ /**
49559
+ * Register a callback to be called when context is restored
49560
+ */
49561
+ onContextRestored(callback) {
49562
+ if (typeof callback === "function") {
49563
+ this._contextRestoredCallbacks.push(callback);
49564
+ }
49565
+ }
49488
49566
  initImages(images, imageLayerScopes) {
49489
49567
  logWithColor$2("INIT IMAGES");
49490
49568
  this.images = images;
@@ -49778,6 +49856,12 @@ class VolumeDataManager {
49778
49856
  log.debug("newColorMappings", newColorMappings);
49779
49857
  this.channels.colorMappings = newColorMappings;
49780
49858
  log.debug("updatedChannels", this.channels);
49859
+ this._lastChannelConfig = {
49860
+ zarrMappings: [...this.channels.zarrMappings],
49861
+ colorMappings: [...this.channels.colorMappings],
49862
+ downsampleMin: [...this.channels.downsampleMin],
49863
+ downsampleMax: [...this.channels.downsampleMax]
49864
+ };
49781
49865
  }
49782
49866
  /**
49783
49867
  * Try to load a resolution level
@@ -49895,6 +49979,10 @@ class VolumeDataManager {
49895
49979
  log.debug("processRequestData: already busy, skipping");
49896
49980
  return;
49897
49981
  }
49982
+ if (this.isContextLost()) {
49983
+ log.debug("processRequestData: WebGL context is lost, skipping");
49984
+ return;
49985
+ }
49898
49986
  this.isBusy = true;
49899
49987
  this.triggerRequest = false;
49900
49988
  const { requests, origRequestCount } = _requestBufferToRequestObjects(buffer, this.k);
@@ -49912,6 +50000,10 @@ class VolumeDataManager {
49912
50000
  this.needsBailout = true;
49913
50001
  return;
49914
50002
  }
50003
+ if (this.isContextLost()) {
50004
+ log.debug("processUsageData: WebGL context is lost, skipping");
50005
+ return;
50006
+ }
49915
50007
  this.isBusy = true;
49916
50008
  this.triggerUsage = false;
49917
50009
  const now2 = ++this.timeStamp;
@@ -49951,6 +50043,10 @@ class VolumeDataManager {
49951
50043
  log.error("pagetable texture not initialized");
49952
50044
  return;
49953
50045
  }
50046
+ if (this.isContextLost()) {
50047
+ log.warn("WebGL context is lost, skipping channel purge");
50048
+ return;
50049
+ }
49954
50050
  this.channels.downsampleMin[ptChannelIndex] = void 0;
49955
50051
  this.channels.downsampleMax[ptChannelIndex] = void 0;
49956
50052
  this.channels.zarrMappings[ptChannelIndex] = void 0;
@@ -49991,6 +50087,10 @@ class VolumeDataManager {
49991
50087
  // Update a PT entry
49992
50088
  _updatePTEntry(ptX, ptY, ptZ, ptVal) {
49993
50089
  if (!this.ptTHREE) return;
50090
+ if (this.isContextLost()) {
50091
+ log.warn("WebGL context is lost, skipping PT entry update");
50092
+ return;
50093
+ }
49994
50094
  const { gl } = this;
49995
50095
  const texPT = this.renderer.properties.get(this.ptTHREE).__webglTexture;
49996
50096
  gl.activeTexture(gl.TEXTURE0);
@@ -50052,6 +50152,11 @@ class VolumeDataManager {
50052
50152
  * 4. Upload one brick + PT entry *
50053
50153
  * ------------------------------------------------------------- */
50054
50154
  async _uploadBrick(ptCoord, bcSlot) {
50155
+ log.debug("uploading brick", ptCoord, bcSlot);
50156
+ if (this.isContextLost()) {
50157
+ log.warn("WebGL context is lost, skipping brick upload");
50158
+ return;
50159
+ }
50055
50160
  if (ptCoord.x >= this.PT.xExtent || ptCoord.y >= this.PT.yExtent || ptCoord.z >= this.PT.zTotal || ptCoord.x < 0 || ptCoord.y < 0 || ptCoord.z < 0) {
50056
50161
  log.error("this.PT", this.PT);
50057
50162
  log.error("ptCoord out of bounds", ptCoord);
@@ -50063,10 +50168,40 @@ class VolumeDataManager {
50063
50168
  ptCoord.z,
50064
50169
  { PT_zExtent: this.PT.zExtent, PT_z0Extent: this.PT.z0Extent, PT_anchors: this.PT.anchors }
50065
50170
  );
50171
+ if (!this.channels || !this.channels.zarrMappings || this.channels.zarrMappings.length === 0) {
50172
+ log.error("Channel mappings not initialized, skipping brick upload");
50173
+ return;
50174
+ }
50175
+ if (channel < 0 || channel >= this.channels.zarrMappings.length) {
50176
+ log.error("Channel index out of bounds", { channel, mappingsLength: this.channels.zarrMappings.length });
50177
+ return;
50178
+ }
50066
50179
  const zarrChannel = this.channels.zarrMappings[channel];
50067
50180
  if (zarrChannel === void 0 || zarrChannel === -1) {
50068
- log.error("zarrChannel is undefined or -1", zarrChannel);
50069
- return;
50181
+ log.warn("zarrChannel is undefined or -1", {
50182
+ zarrChannel,
50183
+ channel,
50184
+ ptCoord,
50185
+ channelMappings: this.channels.zarrMappings,
50186
+ contextLost: this.isContextLost()
50187
+ });
50188
+ if (this._lastChannelConfig && this._lastChannelConfig.zarrMappings[channel] !== void 0) {
50189
+ log.warn("Attempting to use last known channel config");
50190
+ this.channels.zarrMappings = [...this._lastChannelConfig.zarrMappings];
50191
+ this.channels.colorMappings = [...this._lastChannelConfig.colorMappings];
50192
+ this.channels.downsampleMin = [...this._lastChannelConfig.downsampleMin];
50193
+ this.channels.downsampleMax = [...this._lastChannelConfig.downsampleMax];
50194
+ const restoredZarrChannel = this.channels.zarrMappings[channel];
50195
+ if (restoredZarrChannel !== void 0 && restoredZarrChannel !== -1) {
50196
+ log.debug("Successfully restored channel mapping, continuing with upload");
50197
+ } else {
50198
+ log.error("Could not restore valid channel mapping, aborting brick upload");
50199
+ return;
50200
+ }
50201
+ } else {
50202
+ log.error("No fallback channel config available, aborting brick upload");
50203
+ return;
50204
+ }
50070
50205
  }
50071
50206
  log.debug("starting to load zarr chunk", { resolution, z, y, x, zarrChannel });
50072
50207
  let chunk = await this.loadZarrChunk(0, zarrChannel, z, y, x, resolution);
@@ -50157,6 +50292,7 @@ class VolumeDataManager {
50157
50292
  async handleBrickRequests(ptRequests) {
50158
50293
  if (ptRequests.length === 0) return;
50159
50294
  const slots = this._allocateBCSlots(ptRequests.length);
50295
+ log.debug("Handling brick requests:", { requestCount: ptRequests.length, slotCount: slots.length });
50160
50296
  log.debug("handleBrickRequests: starting for loop");
50161
50297
  for (let i2 = 0; i2 < ptRequests.length; ++i2) {
50162
50298
  log.debug("uploading brick", ptRequests[i2], slots[i2]);
@@ -52815,6 +52951,7 @@ function VolumeView(props) {
52815
52951
  const RENDER_PRIORITY = 1;
52816
52952
  useFrame((state, delta, xrFrame) => {
52817
52953
  if (!mrtRef.current || !dataManager || !renderManager) return;
52954
+ if (!renderState.shader) return;
52818
52955
  const {
52819
52956
  gl: frameGl,
52820
52957
  camera: frameCamera,
@@ -39,6 +39,9 @@ export class VolumeDataManager {
39
39
  constructor(glParam: any);
40
40
  gl: any;
41
41
  renderer: any;
42
+ _originalGlParam: any;
43
+ _isContextLost: boolean;
44
+ _contextRestoredCallbacks: any[];
42
45
  deviceLimits: {
43
46
  maxTextureSize: any;
44
47
  max3DTextureSize: any;
@@ -93,6 +96,33 @@ export class VolumeDataManager {
93
96
  needsBailout: boolean;
94
97
  initStatus: string;
95
98
  initError: any;
99
+ _lastChannelConfig: {
100
+ zarrMappings: never[];
101
+ colorMappings: never[];
102
+ downsampleMin: never[];
103
+ downsampleMax: never[];
104
+ } | {
105
+ zarrMappings: never[];
106
+ colorMappings: never[];
107
+ downsampleMin: never[];
108
+ downsampleMax: never[];
109
+ } | null;
110
+ /**
111
+ * Handle WebGL context loss
112
+ */
113
+ _handleContextLost(event: any): void;
114
+ /**
115
+ * Handle WebGL context restoration
116
+ */
117
+ _handleContextRestored(event: any): void;
118
+ /**
119
+ * Check if WebGL context is lost
120
+ */
121
+ isContextLost(): any;
122
+ /**
123
+ * Register a callback to be called when context is restored
124
+ */
125
+ onContextRestored(callback: any): void;
96
126
  initImages(images: any, imageLayerScopes: any): void;
97
127
  images: any;
98
128
  imageLayerScopes: any;
@@ -1 +1 @@
1
- {"version":3,"file":"VolumeDataManager.d.ts","sourceRoot":"","sources":["../src/VolumeDataManager.js"],"names":[],"mappings":"AA8DA,yEAaC;AACD,8EAeC;AAED;;;;GAIG;AACH,kDAHW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,8BAC1B,MAAM;;;;;;;;;;;;;EAyHhB;AAMD,kFAaC;AAqBD;;;;;;EA2DC;AAED;;;;;;;EA0BC;AAID;IACE,0BAyJC;IA5IG,QAAyB;IAa3B,cAAwB;IA8BxB;;;;;MAKC;IAED;;;;;;;;;;;;MAcC;IAED,8BAAmB;IACnB,8BAAmB;IAEnB;;;;;;MAMC;IAED;;;;;;;;;MAiBC;IAQD,2BAAiC;IAYjC,gBAAmB;IAGnB,oBAAuD;IACvD,gBAAwD;IACxD,gBAAmB;IACnB,sBAAsB;IAGtB,aAAmD;IAGnD,gBAAkB;IAElB,sBAAwB;IACxB,wBAA2B;IAC3B,kBAAkB;IAClB,UAAW;IACX,uBAA0B;IAE1B,sBAAyB;IAGzB,mBAAyC;IACzC,eAAqB;IAIvB,qDAIC;IAFC,YAAoB;IACpB,sBAAwC;IAG1C;;;;;OAKG;IACH,mBAFa,OAAO,CAAC,MAAM,CAAC,CA8S3B;IAjQG,kBAAmD;IAmQvD;;;;;;;;OAQG;IACH,mBAqBC;IASD,6BAKC;IAED,wCA6GC;IAED;;;;;OAKG;IAqBH;;;OAGG;IACH,kCAQC;IAED;;;OAGG;IACH,uBAFa,MAAM,CAUlB;IAED,+BAQC;IAED,kCASC;IAED,gCASC;IAED;;;;;;;;;OASG;IACH,iBARW,MAAM,iBACN,MAAM,iBACN,MAAM,KACN,MAAM,KACN,MAAM,cACN,MAAM,GACJ,OAAO,CAAC,UAAU,CAAC,CAiC/B;IAED,+CAmBC;IAGD,6CAmDC;IAGD,gCAcC;IAED,yCA6CC;IAGD,+DAeC;IAKD;;;;OAIG;IACH,oBAHW,MAAM,GACJ;QAAE,OAAO,MAAC;QAAC,CAAC,MAAC;QAAC,CAAC,MAAC;QAAC,CAAC,MAAA;KAAE,EAAE,CAyClC;IAKD,uDA0IC;IAKD,oDAyCC;IAKD,kBAWC;CACF;8BA79CM,OAAO"}
1
+ {"version":3,"file":"VolumeDataManager.d.ts","sourceRoot":"","sources":["../src/VolumeDataManager.js"],"names":[],"mappings":"AA8DA,yEAaC;AACD,8EAeC;AAED;;;;GAIG;AACH,kDAHW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,8BAC1B,MAAM;;;;;;;;;;;;;EAyHhB;AAMD,kFAaC;AAqBD;;;;;;EA2DC;AAED;;;;;;;EA0BC;AAID;IACE,0BA2KC;IA5JG,QAAyB;IAa3B,cAAwB;IAyBxB,sBAA+B;IAC/B,wBAA2B;IAC3B,iCAAmC;IAenC;;;;;MAKC;IAED;;;;;;;;;;;;MAcC;IAED,8BAAmB;IACnB,8BAAmB;IAEnB;;;;;;MAMC;IAED;;;;;;;;;MAiBC;IAQD,2BAAiC;IAYjC,gBAAmB;IAGnB,oBAAuD;IACvD,gBAAwD;IACxD,gBAAmB;IACnB,sBAAsB;IAGtB,aAAmD;IAGnD,gBAAkB;IAElB,sBAAwB;IACxB,wBAA2B;IAC3B,kBAAkB;IAClB,UAAW;IACX,uBAA0B;IAE1B,sBAAyB;IAGzB,mBAAyC;IACzC,eAAqB;IAGrB;;;;;;;;;;aAA8B;IAKhC;;OAEG;IACH,qCAeC;IAED;;OAEG;IACH,yCAqCC;IAED;;OAEG;IACH,qBAMC;IAED;;OAEG;IACH,uCAIC;IAED,qDAIC;IAFC,YAAoB;IACpB,sBAAwC;IAG1C;;;;;OAKG;IACH,mBAFa,OAAO,CAAC,MAAM,CAAC,CA8S3B;IAjQG,kBAAmD;IAmQvD;;;;;;;;OAQG;IACH,mBAqBC;IASD,6BAKC;IAED,wCAqHC;IAED;;;;;OAKG;IAqBH;;;OAGG;IACH,kCAQC;IAED;;;OAGG;IACH,uBAFa,MAAM,CAUlB;IAED,+BAQC;IAED,kCASC;IAED,gCASC;IAED;;;;;;;;;OASG;IACH,iBARW,MAAM,iBACN,MAAM,iBACN,MAAM,KACN,MAAM,KACN,MAAM,cACN,MAAM,GACJ,OAAO,CAAC,UAAU,CAAC,CAiC/B;IAED,+CA0BC;IAGD,6CA0DC;IAGD,gCAcC;IAED,yCAmDC;IAGD,+DAqBC;IAKD;;;;OAIG;IACH,oBAHW,MAAM,GACJ;QAAE,OAAO,MAAC;QAAC,CAAC,MAAC;QAAC,CAAC,MAAC;QAAC,CAAC,MAAA;KAAE,EAAE,CAyClC;IAKD,uDAqLC;IAKD,oDA2CC;IAKD,kBAWC;CACF;8BAhpDM,OAAO"}
@@ -300,6 +300,7 @@ export class VolumeDataManager {
300
300
  constructor(glParam) {
301
301
  logWithColor('CLASS INITIALIZING');
302
302
  // this.store = new zarrita.FetchStore(url); // TODO: use this.images instead
303
+ log.debug('VolumeDataManager constructor', { glParam, glParamContext: glParam.getContext?.() });
303
304
  const gl = glParam.getContext?.() || glParam;
304
305
  const renderer = glParam;
305
306
  // this.images = images;
@@ -335,12 +336,22 @@ export class VolumeDataManager {
335
336
  };
336
337
  return defaults[param] || 0;
337
338
  },
339
+ isContextLost: () => false,
338
340
  MAX_TEXTURE_SIZE: 'MAX_TEXTURE_SIZE',
339
341
  MAX_3D_TEXTURE_SIZE: 'MAX_3D_TEXTURE_SIZE',
340
342
  MAX_RENDERBUFFER_SIZE: 'MAX_RENDERBUFFER_SIZE',
341
343
  MAX_UNIFORM_BUFFER_BINDINGS: 'MAX_UNIFORM_BUFFER_BINDINGS',
342
344
  };
343
345
  }
346
+ // Store references for context loss handling
347
+ this._originalGlParam = glParam;
348
+ this._isContextLost = false;
349
+ this._contextRestoredCallbacks = [];
350
+ // Add context loss event listeners if we have a real WebGL context
351
+ if (this.gl && this.gl.canvas) {
352
+ this.gl.canvas.addEventListener('webglcontextlost', this._handleContextLost.bind(this));
353
+ this.gl.canvas.addEventListener('webglcontextrestored', this._handleContextRestored.bind(this));
354
+ }
344
355
  log.debug('GL CONSTANTS');
345
356
  log.debug(this.gl);
346
357
  log.debug(this.gl.TEXTURE0);
@@ -430,8 +441,86 @@ export class VolumeDataManager {
430
441
  // Add initialization status
431
442
  this.initStatus = INIT_STATUS.NOT_STARTED;
432
443
  this.initError = null;
444
+ // Store the last known configuration for context restoration
445
+ this._lastChannelConfig = null;
433
446
  logWithColor('VolumeDataManager constructor complete');
434
447
  }
448
+ /**
449
+ * Handle WebGL context loss
450
+ */
451
+ _handleContextLost(event) {
452
+ logWithColor('CONTEXT LOST');
453
+ log.warn('WebGL context lost, preventing default and setting flag');
454
+ event.preventDefault();
455
+ this._isContextLost = true;
456
+ // Store current channel configuration for restoration
457
+ if (this.channels && this.channels.zarrMappings) {
458
+ this._lastChannelConfig = {
459
+ zarrMappings: [...this.channels.zarrMappings],
460
+ colorMappings: [...this.channels.colorMappings],
461
+ downsampleMin: [...this.channels.downsampleMin],
462
+ downsampleMax: [...this.channels.downsampleMax],
463
+ };
464
+ }
465
+ }
466
+ /**
467
+ * Handle WebGL context restoration
468
+ */
469
+ _handleContextRestored(event) {
470
+ logWithColor('CONTEXT RESTORED');
471
+ log.warn('WebGL context restored, reinitializing textures');
472
+ this._isContextLost = false;
473
+ // Reinitialize WebGL context reference
474
+ if (this._originalGlParam && this._originalGlParam.getContext) {
475
+ this.gl = this._originalGlParam.getContext();
476
+ }
477
+ // Restore channel configuration if available
478
+ if (this._lastChannelConfig) {
479
+ this.channels.zarrMappings = [...this._lastChannelConfig.zarrMappings];
480
+ this.channels.colorMappings = [...this._lastChannelConfig.colorMappings];
481
+ this.channels.downsampleMin = [...this._lastChannelConfig.downsampleMin];
482
+ this.channels.downsampleMax = [...this._lastChannelConfig.downsampleMax];
483
+ log.debug('Restored channel configuration after context loss');
484
+ }
485
+ // Reinitialize textures if they were previously created
486
+ if (this.PT && this.zarrStore && this.zarrStore.brickLayout) {
487
+ try {
488
+ this.initMRMCPT();
489
+ log.debug('Successfully reinitialized MRMCPT after context restoration');
490
+ }
491
+ catch (error) {
492
+ log.error('Failed to reinitialize MRMCPT after context restoration:', error);
493
+ }
494
+ }
495
+ // Call any registered restoration callbacks
496
+ this._contextRestoredCallbacks.forEach((callback) => {
497
+ try {
498
+ callback();
499
+ }
500
+ catch (error) {
501
+ log.error('Error in context restored callback:', error);
502
+ }
503
+ });
504
+ }
505
+ /**
506
+ * Check if WebGL context is lost
507
+ */
508
+ isContextLost() {
509
+ if (this._isContextLost)
510
+ return true;
511
+ if (this.gl && typeof this.gl.isContextLost === 'function') {
512
+ return this.gl.isContextLost();
513
+ }
514
+ return false;
515
+ }
516
+ /**
517
+ * Register a callback to be called when context is restored
518
+ */
519
+ onContextRestored(callback) {
520
+ if (typeof callback === 'function') {
521
+ this._contextRestoredCallbacks.push(callback);
522
+ }
523
+ }
435
524
  initImages(images, imageLayerScopes) {
436
525
  logWithColor('INIT IMAGES');
437
526
  this.images = images;
@@ -823,6 +912,13 @@ export class VolumeDataManager {
823
912
  log.debug('newColorMappings', newColorMappings);
824
913
  this.channels.colorMappings = newColorMappings;
825
914
  log.debug('updatedChannels', this.channels);
915
+ // Store the updated configuration for context restoration
916
+ this._lastChannelConfig = {
917
+ zarrMappings: [...this.channels.zarrMappings],
918
+ colorMappings: [...this.channels.colorMappings],
919
+ downsampleMin: [...this.channels.downsampleMin],
920
+ downsampleMax: [...this.channels.downsampleMax],
921
+ };
826
922
  }
827
923
  /**
828
924
  * Try to load a resolution level
@@ -943,6 +1039,11 @@ export class VolumeDataManager {
943
1039
  log.debug('processRequestData: already busy, skipping');
944
1040
  return;
945
1041
  }
1042
+ // Check for context loss
1043
+ if (this.isContextLost()) {
1044
+ log.debug('processRequestData: WebGL context is lost, skipping');
1045
+ return;
1046
+ }
946
1047
  this.isBusy = true;
947
1048
  this.triggerRequest = false;
948
1049
  const { requests, origRequestCount } = _requestBufferToRequestObjects(buffer, this.k);
@@ -961,6 +1062,11 @@ export class VolumeDataManager {
961
1062
  this.needsBailout = true; // Set a flag to indicate we need to bail out of processing requests
962
1063
  return;
963
1064
  }
1065
+ // Check for context loss
1066
+ if (this.isContextLost()) {
1067
+ log.debug('processUsageData: WebGL context is lost, skipping');
1068
+ return;
1069
+ }
964
1070
  this.isBusy = true;
965
1071
  this.triggerUsage = false;
966
1072
  const now = ++this.timeStamp;
@@ -1019,6 +1125,11 @@ export class VolumeDataManager {
1019
1125
  log.error('pagetable texture not initialized');
1020
1126
  return;
1021
1127
  }
1128
+ // Check for context loss before proceeding
1129
+ if (this.isContextLost()) {
1130
+ log.warn('WebGL context is lost, skipping channel purge');
1131
+ return;
1132
+ }
1022
1133
  this.channels.downsampleMin[ptChannelIndex] = undefined;
1023
1134
  this.channels.downsampleMax[ptChannelIndex] = undefined;
1024
1135
  this.channels.zarrMappings[ptChannelIndex] = undefined;
@@ -1050,6 +1161,11 @@ export class VolumeDataManager {
1050
1161
  _updatePTEntry(ptX, ptY, ptZ, ptVal) {
1051
1162
  if (!this.ptTHREE)
1052
1163
  return;
1164
+ // Check for context loss before proceeding
1165
+ if (this.isContextLost()) {
1166
+ log.warn('WebGL context is lost, skipping PT entry update');
1167
+ return;
1168
+ }
1053
1169
  const { gl } = this;
1054
1170
  const texPT = this.renderer.properties.get(this.ptTHREE).__webglTexture;
1055
1171
  gl.activeTexture(gl.TEXTURE0);
@@ -1109,7 +1225,12 @@ export class VolumeDataManager {
1109
1225
  * 4. Upload one brick + PT entry *
1110
1226
  * ------------------------------------------------------------- */
1111
1227
  async _uploadBrick(ptCoord, bcSlot) {
1112
- // log.debug('uploading brick', ptCoord, bcSlot);
1228
+ log.debug('uploading brick', ptCoord, bcSlot);
1229
+ // Check for context loss before proceeding
1230
+ if (this.isContextLost()) {
1231
+ log.warn('WebGL context is lost, skipping brick upload');
1232
+ return;
1233
+ }
1113
1234
  if (ptCoord.x >= this.PT.xExtent
1114
1235
  || ptCoord.y >= this.PT.yExtent
1115
1236
  || ptCoord.z >= this.PT.zTotal
@@ -1124,10 +1245,45 @@ export class VolumeDataManager {
1124
1245
  // log.debug('this.PT', this.PT);
1125
1246
  /* 4.1 fetch chunk from Zarr */
1126
1247
  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 });
1248
+ // Check if channel mappings are valid
1249
+ if (!this.channels || !this.channels.zarrMappings || this.channels.zarrMappings.length === 0) {
1250
+ log.error('Channel mappings not initialized, skipping brick upload');
1251
+ return;
1252
+ }
1253
+ if (channel < 0 || channel >= this.channels.zarrMappings.length) {
1254
+ log.error('Channel index out of bounds', { channel, mappingsLength: this.channels.zarrMappings.length });
1255
+ return;
1256
+ }
1127
1257
  const zarrChannel = this.channels.zarrMappings[channel];
1128
1258
  if (zarrChannel === undefined || zarrChannel === -1) {
1129
- log.error('zarrChannel is undefined or -1', zarrChannel);
1130
- return;
1259
+ log.warn('zarrChannel is undefined or -1', {
1260
+ zarrChannel,
1261
+ channel,
1262
+ ptCoord,
1263
+ channelMappings: this.channels.zarrMappings,
1264
+ contextLost: this.isContextLost(),
1265
+ });
1266
+ // If this happens due to context loss, try to restore from last known config
1267
+ if (this._lastChannelConfig && this._lastChannelConfig.zarrMappings[channel] !== undefined) {
1268
+ log.warn('Attempting to use last known channel config');
1269
+ this.channels.zarrMappings = [...this._lastChannelConfig.zarrMappings];
1270
+ this.channels.colorMappings = [...this._lastChannelConfig.colorMappings];
1271
+ this.channels.downsampleMin = [...this._lastChannelConfig.downsampleMin];
1272
+ this.channels.downsampleMax = [...this._lastChannelConfig.downsampleMax];
1273
+ // Retry with restored config
1274
+ const restoredZarrChannel = this.channels.zarrMappings[channel];
1275
+ if (restoredZarrChannel !== undefined && restoredZarrChannel !== -1) {
1276
+ log.debug('Successfully restored channel mapping, continuing with upload');
1277
+ }
1278
+ else {
1279
+ log.error('Could not restore valid channel mapping, aborting brick upload');
1280
+ return;
1281
+ }
1282
+ }
1283
+ else {
1284
+ log.error('No fallback channel config available, aborting brick upload');
1285
+ return;
1286
+ }
1131
1287
  }
1132
1288
  log.debug('starting to load zarr chunk', { resolution, z, y, x, zarrChannel });
1133
1289
  let chunk = await this.loadZarrChunk(0, zarrChannel, z, y, x, resolution);
@@ -1229,6 +1385,7 @@ export class VolumeDataManager {
1229
1385
  /* <= k requests, allocate same number of bricks */
1230
1386
  const slots = this._allocateBCSlots(ptRequests.length);
1231
1387
  // log.debug('slots', slots);
1388
+ log.debug('Handling brick requests:', { requestCount: ptRequests.length, slotCount: slots.length });
1232
1389
  /* upload each (sequentially or Promise.all if you prefer IO overlap) */
1233
1390
  log.debug('handleBrickRequests: starting for loop');
1234
1391
  for (let i = 0; i < ptRequests.length; ++i) {
@@ -1 +1 @@
1
- {"version":3,"file":"VolumeView.d.ts","sourceRoot":"","sources":["../src/VolumeView.js"],"names":[],"mappings":"AAkLA,2DAwdC"}
1
+ {"version":3,"file":"VolumeView.d.ts","sourceRoot":"","sources":["../src/VolumeView.js"],"names":[],"mappings":"AAkLA,2DA4dC"}
@@ -407,6 +407,10 @@ export function VolumeView(props) {
407
407
  // Reference: https://r3f.docs.pmnd.rs/advanced/pitfalls#%E2%9D%8C-setstate-in-useframe-is-bad
408
408
  if (!mrtRef.current || !dataManager || !renderManager)
409
409
  return;
410
+ // Don't render to MRT if shader is not ready - this prevents WebGL warnings
411
+ // about missing fragment shader outputs for multiple render targets
412
+ if (!renderState.shader)
413
+ return;
410
414
  // Receive the same state as the useThree hook.
411
415
  const { gl: frameGl, camera: frameCamera, scene: frameScene, clock, } = state;
412
416
  if (!stillRef.current) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitessce/spatial-accelerated",
3
- "version": "3.8.5",
3
+ "version": "3.8.7",
4
4
  "author": "HIDIVE Lab at HMS",
5
5
  "homepage": "http://vitessce.io",
6
6
  "repository": {
@@ -28,16 +28,16 @@
28
28
  "short-number": "^1.0.6",
29
29
  "three": "^0.154.0",
30
30
  "zarrita": "0.5.4",
31
- "@vitessce/constants-internal": "3.8.5",
32
- "@vitessce/gl": "3.8.5",
33
- "@vitessce/legend": "3.8.5",
34
- "@vitessce/sets-utils": "3.8.5",
35
- "@vitessce/spatial-utils": "3.8.5",
36
- "@vitessce/tooltip": "3.8.5",
37
- "@vitessce/utils": "3.8.5",
38
- "@vitessce/vit-s": "3.8.5",
39
- "@vitessce/styles": "3.8.5",
40
- "@vitessce/globals": "3.8.5"
31
+ "@vitessce/constants-internal": "3.8.7",
32
+ "@vitessce/gl": "3.8.7",
33
+ "@vitessce/legend": "3.8.7",
34
+ "@vitessce/sets-utils": "3.8.7",
35
+ "@vitessce/spatial-utils": "3.8.7",
36
+ "@vitessce/tooltip": "3.8.7",
37
+ "@vitessce/utils": "3.8.7",
38
+ "@vitessce/vit-s": "3.8.7",
39
+ "@vitessce/styles": "3.8.7",
40
+ "@vitessce/globals": "3.8.7"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/three": "^0.154.0",
@@ -351,6 +351,8 @@ export class VolumeDataManager {
351
351
  logWithColor('CLASS INITIALIZING');
352
352
  // this.store = new zarrita.FetchStore(url); // TODO: use this.images instead
353
353
 
354
+ log.debug('VolumeDataManager constructor', { glParam, glParamContext: glParam.getContext?.() });
355
+
354
356
  const gl = glParam.getContext?.() || glParam;
355
357
  const renderer = glParam;
356
358
 
@@ -389,6 +391,7 @@ export class VolumeDataManager {
389
391
  };
390
392
  return defaults[param] || 0;
391
393
  },
394
+ isContextLost: () => false,
392
395
  MAX_TEXTURE_SIZE: 'MAX_TEXTURE_SIZE',
393
396
  MAX_3D_TEXTURE_SIZE: 'MAX_3D_TEXTURE_SIZE',
394
397
  MAX_RENDERBUFFER_SIZE: 'MAX_RENDERBUFFER_SIZE',
@@ -396,6 +399,17 @@ export class VolumeDataManager {
396
399
  };
397
400
  }
398
401
 
402
+ // Store references for context loss handling
403
+ this._originalGlParam = glParam;
404
+ this._isContextLost = false;
405
+ this._contextRestoredCallbacks = [];
406
+
407
+ // Add context loss event listeners if we have a real WebGL context
408
+ if (this.gl && this.gl.canvas) {
409
+ this.gl.canvas.addEventListener('webglcontextlost', this._handleContextLost.bind(this));
410
+ this.gl.canvas.addEventListener('webglcontextrestored', this._handleContextRestored.bind(this));
411
+ }
412
+
399
413
  log.debug('GL CONSTANTS');
400
414
  log.debug(this.gl);
401
415
  log.debug(this.gl.TEXTURE0);
@@ -499,9 +513,95 @@ export class VolumeDataManager {
499
513
  // Add initialization status
500
514
  this.initStatus = INIT_STATUS.NOT_STARTED;
501
515
  this.initError = null;
516
+
517
+ // Store the last known configuration for context restoration
518
+ this._lastChannelConfig = null;
519
+
502
520
  logWithColor('VolumeDataManager constructor complete');
503
521
  }
504
522
 
523
+ /**
524
+ * Handle WebGL context loss
525
+ */
526
+ _handleContextLost(event) {
527
+ logWithColor('CONTEXT LOST');
528
+ log.warn('WebGL context lost, preventing default and setting flag');
529
+ event.preventDefault();
530
+ this._isContextLost = true;
531
+
532
+ // Store current channel configuration for restoration
533
+ if (this.channels && this.channels.zarrMappings) {
534
+ this._lastChannelConfig = {
535
+ zarrMappings: [...this.channels.zarrMappings],
536
+ colorMappings: [...this.channels.colorMappings],
537
+ downsampleMin: [...this.channels.downsampleMin],
538
+ downsampleMax: [...this.channels.downsampleMax],
539
+ };
540
+ }
541
+ }
542
+
543
+ /**
544
+ * Handle WebGL context restoration
545
+ */
546
+ _handleContextRestored(event) {
547
+ logWithColor('CONTEXT RESTORED');
548
+ log.warn('WebGL context restored, reinitializing textures');
549
+ this._isContextLost = false;
550
+
551
+ // Reinitialize WebGL context reference
552
+ if (this._originalGlParam && this._originalGlParam.getContext) {
553
+ this.gl = this._originalGlParam.getContext();
554
+ }
555
+
556
+ // Restore channel configuration if available
557
+ if (this._lastChannelConfig) {
558
+ this.channels.zarrMappings = [...this._lastChannelConfig.zarrMappings];
559
+ this.channels.colorMappings = [...this._lastChannelConfig.colorMappings];
560
+ this.channels.downsampleMin = [...this._lastChannelConfig.downsampleMin];
561
+ this.channels.downsampleMax = [...this._lastChannelConfig.downsampleMax];
562
+ log.debug('Restored channel configuration after context loss');
563
+ }
564
+
565
+ // Reinitialize textures if they were previously created
566
+ if (this.PT && this.zarrStore && this.zarrStore.brickLayout) {
567
+ try {
568
+ this.initMRMCPT();
569
+ log.debug('Successfully reinitialized MRMCPT after context restoration');
570
+ } catch (error) {
571
+ log.error('Failed to reinitialize MRMCPT after context restoration:', error);
572
+ }
573
+ }
574
+
575
+ // Call any registered restoration callbacks
576
+ this._contextRestoredCallbacks.forEach((callback) => {
577
+ try {
578
+ callback();
579
+ } catch (error) {
580
+ log.error('Error in context restored callback:', error);
581
+ }
582
+ });
583
+ }
584
+
585
+ /**
586
+ * Check if WebGL context is lost
587
+ */
588
+ isContextLost() {
589
+ if (this._isContextLost) return true;
590
+ if (this.gl && typeof this.gl.isContextLost === 'function') {
591
+ return this.gl.isContextLost();
592
+ }
593
+ return false;
594
+ }
595
+
596
+ /**
597
+ * Register a callback to be called when context is restored
598
+ */
599
+ onContextRestored(callback) {
600
+ if (typeof callback === 'function') {
601
+ this._contextRestoredCallbacks.push(callback);
602
+ }
603
+ }
604
+
505
605
  initImages(images, imageLayerScopes) {
506
606
  logWithColor('INIT IMAGES');
507
607
  this.images = images;
@@ -971,6 +1071,14 @@ export class VolumeDataManager {
971
1071
  this.channels.colorMappings = newColorMappings;
972
1072
 
973
1073
  log.debug('updatedChannels', this.channels);
1074
+
1075
+ // Store the updated configuration for context restoration
1076
+ this._lastChannelConfig = {
1077
+ zarrMappings: [...this.channels.zarrMappings],
1078
+ colorMappings: [...this.channels.colorMappings],
1079
+ downsampleMin: [...this.channels.downsampleMin],
1080
+ downsampleMax: [...this.channels.downsampleMax],
1081
+ };
974
1082
  }
975
1083
 
976
1084
  /**
@@ -1107,6 +1215,13 @@ export class VolumeDataManager {
1107
1215
  log.debug('processRequestData: already busy, skipping');
1108
1216
  return;
1109
1217
  }
1218
+
1219
+ // Check for context loss
1220
+ if (this.isContextLost()) {
1221
+ log.debug('processRequestData: WebGL context is lost, skipping');
1222
+ return;
1223
+ }
1224
+
1110
1225
  this.isBusy = true;
1111
1226
  this.triggerRequest = false;
1112
1227
 
@@ -1130,6 +1245,13 @@ export class VolumeDataManager {
1130
1245
  this.needsBailout = true; // Set a flag to indicate we need to bail out of processing requests
1131
1246
  return;
1132
1247
  }
1248
+
1249
+ // Check for context loss
1250
+ if (this.isContextLost()) {
1251
+ log.debug('processUsageData: WebGL context is lost, skipping');
1252
+ return;
1253
+ }
1254
+
1133
1255
  this.isBusy = true;
1134
1256
  this.triggerUsage = false;
1135
1257
 
@@ -1203,6 +1325,12 @@ export class VolumeDataManager {
1203
1325
  return;
1204
1326
  }
1205
1327
 
1328
+ // Check for context loss before proceeding
1329
+ if (this.isContextLost()) {
1330
+ log.warn('WebGL context is lost, skipping channel purge');
1331
+ return;
1332
+ }
1333
+
1206
1334
  this.channels.downsampleMin[ptChannelIndex] = undefined;
1207
1335
  this.channels.downsampleMax[ptChannelIndex] = undefined;
1208
1336
  this.channels.zarrMappings[ptChannelIndex] = undefined;
@@ -1245,6 +1373,12 @@ export class VolumeDataManager {
1245
1373
  _updatePTEntry(ptX, ptY, ptZ, ptVal) {
1246
1374
  if (!this.ptTHREE) return;
1247
1375
 
1376
+ // Check for context loss before proceeding
1377
+ if (this.isContextLost()) {
1378
+ log.warn('WebGL context is lost, skipping PT entry update');
1379
+ return;
1380
+ }
1381
+
1248
1382
  const { gl } = this;
1249
1383
  const texPT = this.renderer.properties.get(this.ptTHREE).__webglTexture;
1250
1384
  gl.activeTexture(gl.TEXTURE0);
@@ -1312,7 +1446,13 @@ export class VolumeDataManager {
1312
1446
  * 4. Upload one brick + PT entry *
1313
1447
  * ------------------------------------------------------------- */
1314
1448
  async _uploadBrick(ptCoord, bcSlot) {
1315
- // log.debug('uploading brick', ptCoord, bcSlot);
1449
+ log.debug('uploading brick', ptCoord, bcSlot);
1450
+
1451
+ // Check for context loss before proceeding
1452
+ if (this.isContextLost()) {
1453
+ log.warn('WebGL context is lost, skipping brick upload');
1454
+ return;
1455
+ }
1316
1456
 
1317
1457
  if (ptCoord.x >= this.PT.xExtent
1318
1458
  || ptCoord.y >= this.PT.yExtent
@@ -1336,11 +1476,48 @@ export class VolumeDataManager {
1336
1476
  { PT_zExtent: this.PT.zExtent, PT_z0Extent: this.PT.z0Extent, PT_anchors: this.PT.anchors },
1337
1477
  );
1338
1478
 
1479
+ // Check if channel mappings are valid
1480
+ if (!this.channels || !this.channels.zarrMappings || this.channels.zarrMappings.length === 0) {
1481
+ log.error('Channel mappings not initialized, skipping brick upload');
1482
+ return;
1483
+ }
1484
+
1485
+ if (channel < 0 || channel >= this.channels.zarrMappings.length) {
1486
+ log.error('Channel index out of bounds', { channel, mappingsLength: this.channels.zarrMappings.length });
1487
+ return;
1488
+ }
1489
+
1339
1490
  const zarrChannel = this.channels.zarrMappings[channel];
1340
1491
 
1341
1492
  if (zarrChannel === undefined || zarrChannel === -1) {
1342
- log.error('zarrChannel is undefined or -1', zarrChannel);
1343
- return;
1493
+ log.warn('zarrChannel is undefined or -1', {
1494
+ zarrChannel,
1495
+ channel,
1496
+ ptCoord,
1497
+ channelMappings: this.channels.zarrMappings,
1498
+ contextLost: this.isContextLost(),
1499
+ });
1500
+
1501
+ // If this happens due to context loss, try to restore from last known config
1502
+ if (this._lastChannelConfig && this._lastChannelConfig.zarrMappings[channel] !== undefined) {
1503
+ log.warn('Attempting to use last known channel config');
1504
+ this.channels.zarrMappings = [...this._lastChannelConfig.zarrMappings];
1505
+ this.channels.colorMappings = [...this._lastChannelConfig.colorMappings];
1506
+ this.channels.downsampleMin = [...this._lastChannelConfig.downsampleMin];
1507
+ this.channels.downsampleMax = [...this._lastChannelConfig.downsampleMax];
1508
+
1509
+ // Retry with restored config
1510
+ const restoredZarrChannel = this.channels.zarrMappings[channel];
1511
+ if (restoredZarrChannel !== undefined && restoredZarrChannel !== -1) {
1512
+ log.debug('Successfully restored channel mapping, continuing with upload');
1513
+ } else {
1514
+ log.error('Could not restore valid channel mapping, aborting brick upload');
1515
+ return;
1516
+ }
1517
+ } else {
1518
+ log.error('No fallback channel config available, aborting brick upload');
1519
+ return;
1520
+ }
1344
1521
  }
1345
1522
 
1346
1523
  log.debug('starting to load zarr chunk', { resolution, z, y, x, zarrChannel });
@@ -1467,6 +1644,8 @@ export class VolumeDataManager {
1467
1644
  const slots = this._allocateBCSlots(ptRequests.length);
1468
1645
  // log.debug('slots', slots);
1469
1646
 
1647
+ log.debug('Handling brick requests:', { requestCount: ptRequests.length, slotCount: slots.length });
1648
+
1470
1649
  /* upload each (sequentially or Promise.all if you prefer IO overlap) */
1471
1650
  log.debug('handleBrickRequests: starting for loop');
1472
1651
  for (let i = 0; i < ptRequests.length; ++i) {
package/src/VolumeView.js CHANGED
@@ -495,6 +495,10 @@ export function VolumeView(props) {
495
495
  // Reference: https://r3f.docs.pmnd.rs/advanced/pitfalls#%E2%9D%8C-setstate-in-useframe-is-bad
496
496
  if (!mrtRef.current || !dataManager || !renderManager) return;
497
497
 
498
+ // Don't render to MRT if shader is not ready - this prevents WebGL warnings
499
+ // about missing fragment shader outputs for multiple render targets
500
+ if (!renderState.shader) return;
501
+
498
502
  // Receive the same state as the useThree hook.
499
503
  const {
500
504
  gl: frameGl,