From c297f5e0162600733eb980cc034a83a7243826c5 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Wed, 13 May 2026 12:44:56 +0200 Subject: [PATCH 1/8] feat: add first version of frag shader culling for skel chunks --- src/skeleton/frontend.ts | 112 ++++++++++++++++++++++++++++----------- 1 file changed, 81 insertions(+), 31 deletions(-) diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index 7335e7398a..22c4f3436b 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -124,7 +124,7 @@ import { DataType } from "#src/util/data_type.js"; import { RefCounted } from "#src/util/disposable.js"; import type { ValueOrError } from "#src/util/error.js"; import { makeValueOrError, valueOrThrow } from "#src/util/error.js"; -import { kOneVec4, mat4, type vec4 } from "#src/util/geom.js"; +import { kOneVec4, mat4, vec3, type vec4 } from "#src/util/geom.js"; import { verifyFinitePositiveFloat } from "#src/util/json.js"; import * as matrix from "#src/util/matrix.js"; import { getObjectId } from "#src/util/object_id.js"; @@ -218,10 +218,16 @@ const vertexPositionTextureFormat = computeTextureFormat( 3, ); +interface VisibleChunk { + chunk: SpatiallyIndexedSkeletonChunk; + chunkLayout: ChunkLayout; +} + interface SkeletonShaderParameters { dynamicSegmentAppearance: boolean; hasSegmentStatedColors: boolean; hasSegmentDefaultColor: boolean; + spatialChunkCulling: boolean; } interface SkeletonShaderContext { @@ -324,6 +330,17 @@ class RenderHelper extends RefCounted { if (skeletonParams.dynamicSegmentAppearance) { builder.addVarying("highp uint", "vSegmentValue", "flat"); } + if (skeletonParams.spatialChunkCulling) { + builder.addUniform("highp vec3", "uChunkOrigin"); + builder.addUniform("highp vec3", "uChunkBound"); + builder.addVarying("highp vec3", "vCullPos"); + builder.addFragmentCode(` +void spatialChunkCull() { + if (any(lessThan(vCullPos, uChunkOrigin)) || + any(greaterThanEqual(vCullPos, uChunkBound))) discard; +} +`); + } } // TODO (SKM): segmentAttribute is UINT32 but segments can be UINT64. @@ -369,8 +386,16 @@ class RenderHelper extends RefCounted { } builder.setVertexMain(vertexMain); addControlsToBuilder(shaderBuilderState, builder); - builder.setFragmentMainFunction( - shaderCodeWithLineDirective(shaderBuilderState.parseResult.code), + builder.addFragmentCode(`void userMain();\n`); + builder.addFragmentCode( + "#define main userMain\n" + + shaderCodeWithLineDirective(shaderBuilderState.parseResult.code) + + "\n#undef main\n", + ); + builder.setFragmentMain( + skeletonParams.spatialChunkCulling + ? "spatialChunkCull();\nuserMain();" + : "userMain();", ); } @@ -606,6 +631,9 @@ emitLine(uProjection, vertexA, vertexB, uLineWidth); highp uint lineEndpointIndex = getLineEndpointIndex(); highp uint vertexIndex = aVertexIndex.x * (1u - lineEndpointIndex) + aVertexIndex.y * lineEndpointIndex; `; + if (skeletonParams.spatialChunkCulling) { + vertexMain += `vCullPos = mix(vertexA, vertexB, float(lineEndpointIndex));\n`; + } if ( skeletonParams.dynamicSegmentAppearance && this.segmentAttributeIndex !== undefined @@ -720,9 +748,18 @@ highp uint pickOffset = vertexIndex * uPickInstanceStride; vPickID = uPickID + pickOffset; highp vec3 vertexPosition = readAttribute0(vertexIndex); `; + if (skeletonParams.spatialChunkCulling) { + vertexMain += `vCullPos = vertexPosition;\n`; + } if (this.selectedNodeAttributeIndex !== undefined) { vertexMain += `vSelectedNode = readAttribute${this.selectedNodeAttributeIndex}(vertexIndex);\n`; } + if ( + skeletonParams.dynamicSegmentAppearance && + this.segmentAttributeIndex !== undefined + ) { + vertexMain += `vSegmentValue = toRaw(readAttribute${this.segmentAttributeIndex}(vertexIndex));\n`; + } vertexMain += ` emitCircle( uProjection * vec4(vertexPosition, 1.0), @@ -730,13 +767,6 @@ emitCircle( ${selectedOutlineWidthExpression} ); `; - if ( - skeletonParams.dynamicSegmentAppearance && - this.segmentAttributeIndex !== undefined - ) { - vertexMain += `vSegmentValue = toRaw(readAttribute${this.segmentAttributeIndex}(vertexIndex));\n`; - } - const segmentColorExpression = this.getSegmentColorExpression(); if ( skeletonParams.dynamicSegmentAppearance && @@ -751,6 +781,7 @@ emitCircle( selectedNodeExpression === undefined ? "renderColor" : `((${selectedNodeExpression} > 0.5) ? vec4(${SELECTED_NODE_OUTLINE_COLOR_RGB}, renderColor.a) : renderColor)`; + // TODO (SKM) interaction between default and emitRGBA looks odd builder.addFragmentCode(` vec4 segmentColor() { return getSegmentAppearance(${segmentExpression}); @@ -891,6 +922,16 @@ void emitDefault() { gl.uniform1ui(shader.uniform("uPickInstanceStride"), stride); } + setChunkBounds( + gl: GL, + shader: ShaderProgram, + origin: Float32Array, + upperBound: Float32Array, + ) { + gl.uniform3fv(shader.uniform("uChunkOrigin"), origin); + gl.uniform3fv(shader.uniform("uChunkBound"), upperBound); + } + drawSkeletons( gl: GL, edgeShader: ShaderProgram, @@ -1165,6 +1206,7 @@ export class SkeletonLayer extends RefCounted implements SkeletonShaderContext { dynamicSegmentAppearance: false, hasSegmentStatedColors: false, hasSegmentDefaultColor: false, + spatialChunkCulling: false, }); fallbackShaderParameters = new WatchableValue( getFallbackBuilderState(parseShaderUiControls(DEFAULT_FRAGMENT_MAIN)), @@ -2017,6 +2059,7 @@ export class SpatiallyIndexedSkeletonLayer selectedNodeAttributeIndex: number | undefined; readonly browsePassLayerView: SkeletonShaderContext; readonly skeletonShaderParameters: WatchableValue; + readonly browsePassSkeletonShaderParameters: WatchableValueInterface; fallbackShaderParameters = new WatchableValue( getFallbackBuilderState(parseShaderUiControls(DEFAULT_FRAGMENT_MAIN)), ); @@ -2404,6 +2447,7 @@ export class SpatiallyIndexedSkeletonLayer dynamicSegmentAppearance: true, hasSegmentStatedColors: false, hasSegmentDefaultColor: false, + spatialChunkCulling: false, }); this.registerDisposer( registerNested((context, colorGroupState) => { @@ -2414,6 +2458,7 @@ export class SpatiallyIndexedSkeletonLayer colorGroupState.segmentStatedColors.size !== 0, hasSegmentDefaultColor: colorGroupState.segmentDefaultColor.value !== undefined, + spatialChunkCulling: false, }; }; context.registerDisposer( @@ -2425,6 +2470,13 @@ export class SpatiallyIndexedSkeletonLayer update(); }, this.displayState.segmentationColorGroupState), ); + this.browsePassSkeletonShaderParameters = this.registerDisposer( + makeCachedLazyDerivedWatchableValue( + (params) => ({ ...params, spatialChunkCulling: true }), + this.skeletonShaderParameters, + ), + ); + // Browse pass uses uniform-based dynamic segment color (not per-vertex attribute), // so segmentColorAttributeIndex is intentionally undefined here. this.browsePassLayerView = { @@ -2433,7 +2485,7 @@ export class SpatiallyIndexedSkeletonLayer gl: this.gl, fallbackShaderParameters: this.fallbackShaderParameters, displayState: this.displayState, - skeletonShaderParameters: this.skeletonShaderParameters, + skeletonShaderParameters: this.browsePassSkeletonShaderParameters, }; const selectedNodeIndex = this.vertexAttributes.findIndex( (x) => x.name === selectedNodeAttribute.name, @@ -2664,7 +2716,7 @@ export class SpatiallyIndexedSkeletonLayer transformedSources: readonly TransformedSource[][], projectionParameters: any, lod: number | undefined, - ): SpatiallyIndexedSkeletonChunk[] { + ): VisibleChunk[] { if (lod === undefined) { return []; } @@ -2674,7 +2726,7 @@ export class SpatiallyIndexedSkeletonLayer ), ); const lodSuffix = `:${lod}`; - const result: SpatiallyIndexedSkeletonChunk[] = []; + const result: VisibleChunk[] = []; const seenChunkKeysBySource = new Map>(); for (const scales of transformedSources) { for (const tsource of scales) { @@ -2697,7 +2749,7 @@ export class SpatiallyIndexedSkeletonLayer tsource.source as SpatiallyIndexedSkeletonSource; const chunk = chunkSource.chunks.get(chunkKey); if (chunk?.state !== ChunkState.GPU_MEMORY) return; - result.push(chunk); + result.push({ chunk, chunkLayout: tsource.chunkLayout }); }, ); } @@ -2910,7 +2962,7 @@ export class SpatiallyIndexedSkeletonLayer modelMatrix: mat4, lineWidth: number, pointDiameter: number, - visibleChunks: SpatiallyIndexedSkeletonChunk[], + visibleChunks: VisibleChunk[], ) { if (visibleChunks.length === 0) return; const hasExcludedSegments = @@ -2926,7 +2978,9 @@ export class SpatiallyIndexedSkeletonLayer if (passState === undefined) return; const { gl, edgeShader, nodeShader, skeletonParams } = passState; - for (const chunk of visibleChunks) { + const chunkOrigin = vec3.create(); + const chunkBound = vec3.create(); + for (const { chunk, chunkLayout } of visibleChunks) { if (renderContext.emitPickID) { let edgePickId = 0; let edgePickStride = 0; @@ -2959,9 +3013,17 @@ export class SpatiallyIndexedSkeletonLayer edgeShader.bind(); renderHelper.setPickID(gl, edgeShader, edgePickId); renderHelper.setPickInstanceStride(gl, edgeShader, edgePickStride); + if (skeletonParams.spatialChunkCulling) { + vec3.mul(chunkOrigin, chunk.chunkGridPosition, chunkLayout.size); + vec3.add(chunkBound, chunkOrigin, chunkLayout.size); + renderHelper.setChunkBounds(gl, edgeShader, chunkOrigin, chunkBound); + } nodeShader.bind(); renderHelper.setPickID(gl, nodeShader, nodePickId); renderHelper.setPickInstanceStride(gl, nodeShader, nodePickStride); + if (skeletonParams.spatialChunkCulling) { + renderHelper.setChunkBounds(gl, nodeShader, chunkOrigin, chunkBound); + } } // Render each chunk with different node/edge colors for debugging if (DEBUG_SPATIAL_SKELETON_CHUNKS) { @@ -3101,7 +3163,7 @@ export class SpatiallyIndexedSkeletonLayer browseRenderHelper: RenderHelper, renderOptions: ViewSpecificSkeletonRenderingOptions, modelMatrix: mat4, - visibleChunks: SpatiallyIndexedSkeletonChunk[], + visibleChunks: VisibleChunk[], ) { const { displayState } = this; if ( @@ -3438,7 +3500,7 @@ export class PerspectiveViewSpatiallyIndexedSkeletonLayer extends PerspectiveVie private drawChunkBoundsWireframe( renderContext: PerspectiveViewRenderContext, - visibleChunks: SpatiallyIndexedSkeletonChunk[], + visibleChunks: VisibleChunk[], modelMatrix?: mat4, ) { if ( @@ -3448,15 +3510,6 @@ export class PerspectiveViewSpatiallyIndexedSkeletonLayer extends PerspectiveVie ) return; - const chunkLayoutBySource = new Map(); - for (const scales of this.transformedSources) { - for (const tsource of scales) { - if (!chunkLayoutBySource.has(tsource.source)) { - chunkLayoutBySource.set(tsource.source, tsource.chunkLayout); - } - } - } - const { gl } = this.base; const wireframeHelper = ChunkWireframeHelper.get(gl); const shader = wireframeHelper.getShader(renderContext.emitter); @@ -3466,10 +3519,7 @@ export class PerspectiveViewSpatiallyIndexedSkeletonLayer extends PerspectiveVie mat4.multiply(tempMat4, viewProjectionMat, modelMatrix); gl.uniformMatrix4fv(shader.uniform("uChunkToClip"), false, tempMat4); - for (const chunk of visibleChunks) { - const chunkLayout = chunkLayoutBySource.get(chunk.source); - if (chunkLayout === undefined) continue; - + for (const { chunk, chunkLayout } of visibleChunks) { wireframeHelper.setChunkUniforms( gl, shader, From 8f58667eb9b32a9aee6df7bb410a57b03c99d9c3 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Wed, 13 May 2026 12:50:04 +0200 Subject: [PATCH 2/8] refactor: clarify emitDefault and emitRGBA relation --- src/skeleton/frontend.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index 22c4f3436b..16e30f1b95 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -781,7 +781,6 @@ emitCircle( selectedNodeExpression === undefined ? "renderColor" : `((${selectedNodeExpression} > 0.5) ? vec4(${SELECTED_NODE_OUTLINE_COLOR_RGB}, renderColor.a) : renderColor)`; - // TODO (SKM) interaction between default and emitRGBA looks odd builder.addFragmentCode(` vec4 segmentColor() { return getSegmentAppearance(${segmentExpression}); @@ -799,13 +798,7 @@ void emitRGB(vec3 color) { emitRGBA(vec4(color, 1.0)); } void emitDefault() { - vec4 baseColor = segmentColor(); - highp float alpha = baseColor.a; - if (alpha <= 0.0) discard; - vec4 renderColor = vec4(baseColor.rgb, alpha); - vec4 borderColor = ${borderColorExpression}; - vec4 circleColor = getCircleColor(renderColor, borderColor); - emit(vec4(circleColor.rgb * circleColor.a, circleColor.a), vPickID); + emitRGBA(vec4(segmentColor().rgb, 1.0)); } `); } else if (this.segmentColorAttributeIndex === undefined) { From 9aa58dea0ba87ff70cd00b57df531141cb80e430 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Wed, 13 May 2026 12:55:16 +0200 Subject: [PATCH 3/8] fix: move chunk setting outside of pick branch --- src/skeleton/frontend.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index 16e30f1b95..207ccb0d11 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -2974,6 +2974,14 @@ export class SpatiallyIndexedSkeletonLayer const chunkOrigin = vec3.create(); const chunkBound = vec3.create(); for (const { chunk, chunkLayout } of visibleChunks) { + if (skeletonParams.spatialChunkCulling) { + vec3.mul(chunkOrigin, chunk.chunkGridPosition, chunkLayout.size); + vec3.add(chunkBound, chunkOrigin, chunkLayout.size); + edgeShader.bind(); + renderHelper.setChunkBounds(gl, edgeShader, chunkOrigin, chunkBound); + nodeShader.bind(); + renderHelper.setChunkBounds(gl, nodeShader, chunkOrigin, chunkBound); + } if (renderContext.emitPickID) { let edgePickId = 0; let edgePickStride = 0; @@ -3006,17 +3014,9 @@ export class SpatiallyIndexedSkeletonLayer edgeShader.bind(); renderHelper.setPickID(gl, edgeShader, edgePickId); renderHelper.setPickInstanceStride(gl, edgeShader, edgePickStride); - if (skeletonParams.spatialChunkCulling) { - vec3.mul(chunkOrigin, chunk.chunkGridPosition, chunkLayout.size); - vec3.add(chunkBound, chunkOrigin, chunkLayout.size); - renderHelper.setChunkBounds(gl, edgeShader, chunkOrigin, chunkBound); - } nodeShader.bind(); renderHelper.setPickID(gl, nodeShader, nodePickId); renderHelper.setPickInstanceStride(gl, nodeShader, nodePickStride); - if (skeletonParams.spatialChunkCulling) { - renderHelper.setChunkBounds(gl, nodeShader, chunkOrigin, chunkBound); - } } // Render each chunk with different node/edge colors for debugging if (DEBUG_SPATIAL_SKELETON_CHUNKS) { From 8e90a2ff9d3ba2479a71278bc0e04c29924ae9bf Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Wed, 13 May 2026 12:57:42 +0200 Subject: [PATCH 4/8] fix: correct debug on skeleton chunks --- src/skeleton/frontend.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index 207ccb0d11..7a13000ac2 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -2450,7 +2450,8 @@ export class SpatiallyIndexedSkeletonLayer hasSegmentStatedColors: colorGroupState.segmentStatedColors.size !== 0, hasSegmentDefaultColor: - colorGroupState.segmentDefaultColor.value !== undefined, + colorGroupState.segmentDefaultColor.value !== undefined || + DEBUG_SPATIAL_SKELETON_CHUNKS, spatialChunkCulling: false, }; }; From c15d272071c4aaaa5323345d11506513942cd3d6 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Wed, 13 May 2026 14:42:35 +0200 Subject: [PATCH 5/8] feat: add hover highlight and seg highlight also indicate via comments diff paths and clear seg hasSelectedSegment --- src/skeleton/frontend.ts | 101 ++++++++++++++++++++++++++++++--------- 1 file changed, 78 insertions(+), 23 deletions(-) diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index 7a13000ac2..55d441fa52 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -227,6 +227,7 @@ interface SkeletonShaderParameters { dynamicSegmentAppearance: boolean; hasSegmentStatedColors: boolean; hasSegmentDefaultColor: boolean; + hoverHighlight: boolean; spatialChunkCulling: boolean; } @@ -327,9 +328,6 @@ class RenderHelper extends RefCounted { if (skeletonParams.dynamicSegmentAppearance) { this.defineDynamicSegmentAppearance(builder, skeletonParams); } - if (skeletonParams.dynamicSegmentAppearance) { - builder.addVarying("highp uint", "vSegmentValue", "flat"); - } if (skeletonParams.spatialChunkCulling) { builder.addUniform("highp vec3", "uChunkOrigin"); builder.addUniform("highp vec3", "uChunkBound"); @@ -441,9 +439,14 @@ void spatialChunkCull() { } builder.addUniform("highp float", "uVisibleAlpha"); builder.addUniform("highp float", "uHiddenAlpha"); + builder.addUniform("highp float", "uSaturation"); if (params.hasSegmentDefaultColor) { builder.addUniform("highp vec3", "uSegmentDefaultColor"); } + if (params.hoverHighlight) { + builder.addUniform("highp uvec2", "uHoveredSegmentId"); + } + builder.addVarying("highp uint", "vSegmentValue", "flat"); const statedColorFragment = params.hasSegmentStatedColors ? ` @@ -457,14 +460,29 @@ void spatialChunkCull() { ? " return uSegmentDefaultColor;" : ` ${colorExpression}`; + const hoverAdjustFragment = params.hoverHighlight + ? ` + if (segmentId.value.x == uHoveredSegmentId.x && + segmentId.value.y == uHoveredSegmentId.y) { + if (saturation > 0.5) { saturation -= 0.5; } + else { saturation += 0.5; } + }` + : ""; + builder.addFragmentCode(` uint64_t getSegmentAppearanceId(highp uint segmentValue) { return uint64_t(uvec2(segmentValue, 0u)); } -vec3 getSegmentLookupColor(uint64_t segmentId) { +vec3 getSegmentBaseColor(uint64_t segmentId) { ${statedColorFragment} ${defaultColorFragment} } +vec3 getSegmentLookupColor(uint64_t segmentId) { + vec3 baseColor = getSegmentBaseColor(segmentId); + float saturation = uSaturation; +${hoverAdjustFragment} + return mix(vec3(1.0, 1.0, 1.0), baseColor, saturation); +} float getSegmentLookupAlpha(uint64_t segmentId) { if (${this.excludedSegmentsShaderManager.hasFunctionName}(segmentId)) { return ${excludedSegmentAlpha}; @@ -537,6 +555,19 @@ vec4 getSegmentAppearance(highp uint segmentValue) { this.gpuSegmentStatedColorHashTable, ); } + + const { saturation, segmentSelectionState } = this.base.displayState; + gl.uniform1f(shader.uniform("uSaturation"), saturation.value); + if (skeletonParams.hoverHighlight) { + const seg = segmentSelectionState.hasSelectedSegment + ? segmentSelectionState.selectedSegment + : 0n; + gl.uniform2ui( + shader.uniform("uHoveredSegmentId"), + Number(seg & 0xffff_ffffn), + Number((seg >> 32n) & 0xffff_ffffn), + ); + } } maybeDisableDynamicSegmentAppearance( @@ -647,6 +678,9 @@ highp uint vertexIndex = aVertexIndex.x * (1u - lineEndpointIndex) + aVertexInde ? "uColor.a" : `${segmentColorExpression}.a`; if (skeletonParams.dynamicSegmentAppearance) { + // Dynamic path (spatial skeletons): per-segment color, visibility, + // saturation and hover highlight all resolved in the shader via + // getSegmentAppearance(). uColor is unused in this path. builder.addFragmentCode(` vec4 segmentColor() { return getSegmentAppearance(vSegmentValue); @@ -665,10 +699,9 @@ void emitDefault() { } `); } else if (this.segmentColorAttributeIndex === undefined) { - // Preserve legacy skeleton behavior where `uColor` is already - // premultiplied by `objectAlpha` in `getObjectColor`. - // in this path, whole skeletons are drawn at one time - // as opposed to multiple skeletons + // Legacy path (non-spatial skeletons): one skeleton drawn per call; + // uColor is set per-skeleton by the CPU via getObjectColor(), which + // already incorporates saturation and hover highlighting. builder.addFragmentCode(` vec4 segmentColor() { return ${segmentColorExpression}; @@ -681,6 +714,8 @@ void emitDefault() { } `); } else { + // Per-vertex color attribute path: color comes from a per-vertex + // attribute; alpha is taken from uColor. builder.addFragmentCode(` vec4 segmentColor() { return ${segmentColorExpression}; @@ -772,6 +807,9 @@ emitCircle( skeletonParams.dynamicSegmentAppearance && this.segmentAttributeIndex !== undefined ) { + // Dynamic path (spatial skeletons): per-segment color, visibility, + // saturation and hover highlight all resolved in the shader via + // getSegmentAppearance(). uColor is unused in this path. const segmentExpression = `vSegmentValue`; const selectedNodeExpression = this.selectedNodeAttributeIndex === undefined @@ -802,7 +840,9 @@ void emitDefault() { } `); } else if (this.segmentColorAttributeIndex === undefined) { - // Preserve legacy skeleton behavior for non-spatial skeletons. + // Legacy path (non-spatial skeletons): one skeleton drawn per call; + // uColor is set per-skeleton by the CPU via getObjectColor(), which + // already incorporates saturation and hover highlighting. builder.addFragmentCode(` vec4 segmentColor() { return ${segmentColorExpression}; @@ -819,6 +859,8 @@ void emitDefault() { } `); } else { + // Per-vertex color attribute path: color comes from a per-vertex + // attribute; alpha is taken from the attribute's alpha component. const selectedNodeExpression = this.selectedNodeAttributeIndex === undefined ? undefined @@ -1199,6 +1241,7 @@ export class SkeletonLayer extends RefCounted implements SkeletonShaderContext { dynamicSegmentAppearance: false, hasSegmentStatedColors: false, hasSegmentDefaultColor: false, + hoverHighlight: false, spatialChunkCulling: false, }); fallbackShaderParameters = new WatchableValue( @@ -2440,30 +2483,42 @@ export class SpatiallyIndexedSkeletonLayer dynamicSegmentAppearance: true, hasSegmentStatedColors: false, hasSegmentDefaultColor: false, + hoverHighlight: false, spatialChunkCulling: false, }); + const updateSkeletonShaderParameters = () => { + const colorGroupState = + this.displayState.segmentationColorGroupState.value; + this.skeletonShaderParameters.value = { + dynamicSegmentAppearance: true, + hasSegmentStatedColors: colorGroupState.segmentStatedColors.size !== 0, + hasSegmentDefaultColor: + colorGroupState.segmentDefaultColor.value !== undefined || + DEBUG_SPATIAL_SKELETON_CHUNKS, + hoverHighlight: this.displayState.hoverHighlight.value, + spatialChunkCulling: false, + }; + }; this.registerDisposer( registerNested((context, colorGroupState) => { - const update = () => { - this.skeletonShaderParameters.value = { - dynamicSegmentAppearance: true, - hasSegmentStatedColors: - colorGroupState.segmentStatedColors.size !== 0, - hasSegmentDefaultColor: - colorGroupState.segmentDefaultColor.value !== undefined || - DEBUG_SPATIAL_SKELETON_CHUNKS, - spatialChunkCulling: false, - }; - }; context.registerDisposer( - colorGroupState.segmentStatedColors.changed.add(update), + colorGroupState.segmentStatedColors.changed.add( + updateSkeletonShaderParameters, + ), ); context.registerDisposer( - colorGroupState.segmentDefaultColor.changed.add(update), + colorGroupState.segmentDefaultColor.changed.add( + updateSkeletonShaderParameters, + ), ); - update(); + updateSkeletonShaderParameters(); }, this.displayState.segmentationColorGroupState), ); + this.registerDisposer( + this.displayState.hoverHighlight.changed.add( + updateSkeletonShaderParameters, + ), + ); this.browsePassSkeletonShaderParameters = this.registerDisposer( makeCachedLazyDerivedWatchableValue( (params) => ({ ...params, spatialChunkCulling: true }), From 9a7ed1af598ab12400bc2707c1fc7fe2b007a7f2 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Wed, 13 May 2026 15:09:14 +0200 Subject: [PATCH 6/8] fix: don't consider lod undefined as not ready --- src/skeleton/frontend.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index 55d441fa52..38be264c58 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -2817,7 +2817,11 @@ export class SpatiallyIndexedSkeletonLayer ) { return true; } - if (lod === undefined || transformedSources.length === 0) { + if (lod === undefined) { + // No LOD configured — draw() renders nothing in this case, so nothing to wait for. + return true; + } + if (transformedSources.length === 0) { return false; } const lodSuffix = `:${lod}`; From 6506b38407b4133f446026f5e027df984439d4cc Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Wed, 13 May 2026 15:13:08 +0200 Subject: [PATCH 7/8] fix: isReady considers only current LOD level --- src/skeleton/frontend.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index 38be264c58..fa9c964fa3 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -2807,6 +2807,8 @@ export class SpatiallyIndexedSkeletonLayer } private areVisibleChunksReady( + view: SpatiallyIndexedSkeletonView, + gridLevel: number | undefined, transformedSources: readonly TransformedSource[][], projectionParameters: ProjectionParameters, lod: number | undefined, @@ -2824,12 +2826,18 @@ export class SpatiallyIndexedSkeletonLayer if (transformedSources.length === 0) { return false; } + const selectedSourceIds = new Set( + this.selectSourcesForViewAndGrid(view, gridLevel).map((s) => + getObjectId(s.chunkSource), + ), + ); const lodSuffix = `:${lod}`; const seenChunkKeysBySource = new Map>(); let ready = true; for (const scales of transformedSources) { for (const tsource of scales) { const sourceId = getObjectId(tsource.source); + if (!selectedSourceIds.has(sourceId)) continue; let seenChunkKeys = seenChunkKeysBySource.get(sourceId); if (seenChunkKeys === undefined) { seenChunkKeys = new Set(); @@ -3252,14 +3260,15 @@ export class SpatiallyIndexedSkeletonLayer } isReady( + view: SpatiallyIndexedSkeletonView, + gridLevel: number | undefined, transformedSources: readonly TransformedSource[][], projectionParameters: ProjectionParameters, lod?: number, ) { - // TODO (SKM) I don't think this is getting - // called as expected, for example, I think - // the screenshot should call this but it doesn't seem to return this.areVisibleChunksReady( + view, + gridLevel, transformedSources, projectionParameters, lod, @@ -3591,11 +3600,12 @@ export class PerspectiveViewSpatiallyIndexedSkeletonLayer extends PerspectiveVie >, ) { const { displayState } = this.base; - const lodValue = displayState.skeletonLod?.value; return this.base.isReady( + "3d", + displayState.spatialSkeletonGridLevel3d?.value, this.transformedSources, renderContext.projectionParameters, - lodValue, + displayState.skeletonLod?.value, ); } } @@ -3734,11 +3744,12 @@ export class SliceViewPanelSpatiallyIndexedSkeletonLayer extends SliceViewPanelR >, ) { const { displayState } = this.base; - const lodValue = displayState.spatialSkeletonLod2d?.value; return this.base.isReady( + "2d", + displayState.spatialSkeletonGridLevel2d?.value, this.transformedSources, renderContext.projectionParameters, - lodValue, + displayState.spatialSkeletonLod2d?.value, ); } } From c36306fd0e80eb620ce527aadfb50920eb552f24 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Wed, 13 May 2026 15:54:13 +0200 Subject: [PATCH 8/8] refactor: remove uneeded seen set management, combine chunk manage paths this allows us to be more maintainable. This bug stemmed from the isReady using a different path for chunk manage then the rendering did --- src/skeleton/frontend.ts | 130 +++++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 66 deletions(-) diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index fa9c964fa3..4867bf1a56 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -2759,50 +2759,77 @@ export class SpatiallyIndexedSkeletonLayer }; } - getVisibleChunksInCurrentViewAndLod( + // Iterates every chunk slot in view for the given view/gridLevel/lod. + // Callback receives (chunkKey, chunkSource, chunkLayout); return false to stop early. + private forEachVisibleChunkSlot( view: SpatiallyIndexedSkeletonView, gridLevel: number | undefined, transformedSources: readonly TransformedSource[][], - projectionParameters: any, - lod: number | undefined, - ): VisibleChunk[] { - if (lod === undefined) { - return []; - } + projectionParameters: ProjectionParameters, + lod: number, + callback: ( + chunkKey: string, + chunkSource: SpatiallyIndexedSkeletonSource, + chunkLayout: ChunkLayout, + ) => boolean | void, + ) { const selectedSourceIds = new Set( this.selectSourcesForViewAndGrid(view, gridLevel).map((s) => getObjectId(s.chunkSource), ), ); const lodSuffix = `:${lod}`; - const result: VisibleChunk[] = []; - const seenChunkKeysBySource = new Map>(); + let shouldContinue = true; for (const scales of transformedSources) { for (const tsource of scales) { - const sourceId = getObjectId(tsource.source); - if (!selectedSourceIds.has(sourceId)) continue; - let seenChunkKeys = seenChunkKeysBySource.get(sourceId); - if (seenChunkKeys === undefined) { - seenChunkKeys = new Set(); - seenChunkKeysBySource.set(sourceId, seenChunkKeys); - } + if (!shouldContinue) return; + if (!selectedSourceIds.has(getObjectId(tsource.source))) continue; forEachVisibleVolumetricChunk( projectionParameters, this.localPosition.value, tsource, (positionInChunks) => { + if (!shouldContinue) return; const chunkKey = `${positionInChunks.join()}${lodSuffix}`; - if (seenChunkKeys!.has(chunkKey)) return; - seenChunkKeys!.add(chunkKey); - const chunkSource = - tsource.source as SpatiallyIndexedSkeletonSource; - const chunk = chunkSource.chunks.get(chunkKey); - if (chunk?.state !== ChunkState.GPU_MEMORY) return; - result.push({ chunk, chunkLayout: tsource.chunkLayout }); + if ( + callback( + chunkKey, + tsource.source as SpatiallyIndexedSkeletonSource, + tsource.chunkLayout, + ) === false + ) { + shouldContinue = false; + } }, ); } } + } + + getVisibleChunksInCurrentViewAndLod( + view: SpatiallyIndexedSkeletonView, + gridLevel: number | undefined, + transformedSources: readonly TransformedSource[][], + projectionParameters: any, + lod: number | undefined, + ): VisibleChunk[] { + if (lod === undefined) { + return []; + } + const result: VisibleChunk[] = []; + this.forEachVisibleChunkSlot( + view, + gridLevel, + transformedSources, + projectionParameters, + lod, + (chunkKey, chunkSource, chunkLayout) => { + const chunk = chunkSource.chunks.get(chunkKey); + if (chunk?.state === ChunkState.GPU_MEMORY) { + result.push({ chunk, chunkLayout }); + } + }, + ); return result; } @@ -2826,52 +2853,23 @@ export class SpatiallyIndexedSkeletonLayer if (transformedSources.length === 0) { return false; } - const selectedSourceIds = new Set( - this.selectSourcesForViewAndGrid(view, gridLevel).map((s) => - getObjectId(s.chunkSource), - ), - ); - const lodSuffix = `:${lod}`; - const seenChunkKeysBySource = new Map>(); let ready = true; - for (const scales of transformedSources) { - for (const tsource of scales) { - const sourceId = getObjectId(tsource.source); - if (!selectedSourceIds.has(sourceId)) continue; - let seenChunkKeys = seenChunkKeysBySource.get(sourceId); - if (seenChunkKeys === undefined) { - seenChunkKeys = new Set(); - seenChunkKeysBySource.set(sourceId, seenChunkKeys); - } - forEachVisibleVolumetricChunk( - projectionParameters, - this.localPosition.value, - tsource, - (positionInChunks) => { - if (!ready) { - return; - } - const chunkKey = `${positionInChunks.join()}${lodSuffix}`; - if (seenChunkKeys!.has(chunkKey)) { - return; - } - seenChunkKeys!.add(chunkKey); - const chunkSource = - tsource.source as SpatiallyIndexedSkeletonSource; - const chunk = chunkSource.chunks.get(chunkKey) as - | SpatiallyIndexedSkeletonChunk - | undefined; - if (chunk?.state !== ChunkState.GPU_MEMORY) { - ready = false; - } - }, - ); - if (!ready) { + this.forEachVisibleChunkSlot( + view, + gridLevel, + transformedSources, + projectionParameters, + lod, + (chunkKey, chunkSource, _) => { + const chunk = chunkSource.chunks.get(chunkKey); + if (chunk?.state !== ChunkState.GPU_MEMORY) { + ready = false; return false; } - } - } - return true; + return true; + }, + ); + return ready; } getNode(