11// Decoded-frame cache for cine playback.
22//
3- // Holds CPU-visible RGBA bytes (not ImageBitmaps) because VTK's scalar buffer
4- // needs to copy them in . Bounded by a byte budget; LRU eviction. Per-image
5- // instance — disposed alongside the DicomCineImage that owns it.
3+ // Holds CPU-visible RGB bytes packed for direct copy into VTK's 3-component
4+ // scalar buffer . Bounded by a byte budget; LRU eviction. Per-image instance —
5+ // disposed alongside the DicomCineImage that owns it.
66
77import { decodeJpegInWorker } from './jpegDecodePool' ;
88
99export type DecodedFrame = {
1010 width : number ;
1111 height : number ;
12- // RGBA, 8-bit per channel, row-major (matches OffscreenCanvas getImageData).
13- rgba : Uint8ClampedArray ;
12+ // RGB, 8-bit per channel, row-major. Packed in the decoder (worker for JPEG)
13+ // so the main thread can publish frames with a single buffer copy.
14+ rgb : Uint8Array ;
1415} ;
1516
16- // Copy a decoded RGBA frame into a 3-component RGB buffer (VTK scalar shape).
1717// Returns false when the buffers disagree on pixel count, so callers can skip
1818// the publish step instead of rendering a partial frame.
1919export function copyDecodedFrameToRgb (
2020 frame : DecodedFrame ,
2121 out : Uint8Array
2222) : boolean {
23- const { rgba } = frame ;
24- const pixels = out . length / 3 ;
25- if ( rgba . length !== pixels * 4 ) return false ;
26- for ( let i = 0 ; i < pixels ; i ++ ) {
27- const src = i * 4 ;
28- const dst = i * 3 ;
29- out [ dst ] = rgba [ src ] ;
30- out [ dst + 1 ] = rgba [ src + 1 ] ;
31- out [ dst + 2 ] = rgba [ src + 2 ] ;
32- }
23+ if ( frame . rgb . length !== out . length ) return false ;
24+ out . set ( frame . rgb ) ;
3325 return true ;
3426}
3527
@@ -59,11 +51,11 @@ export class FrameCache {
5951 set ( frameIndex : number , frame : DecodedFrame ) : void {
6052 const existing = this . entries . get ( frameIndex ) ;
6153 if ( existing ) {
62- this . bytesInUse -= existing . rgba . byteLength ;
54+ this . bytesInUse -= existing . rgb . byteLength ;
6355 this . entries . delete ( frameIndex ) ;
6456 }
6557 this . entries . set ( frameIndex , frame ) ;
66- this . bytesInUse += frame . rgba . byteLength ;
58+ this . bytesInUse += frame . rgb . byteLength ;
6759 this . evictUntilUnderBudget ( ) ;
6860 }
6961
@@ -90,7 +82,7 @@ export class FrameCache {
9082 for ( const key of this . entries . keys ( ) ) {
9183 if ( this . bytesInUse <= this . budgetBytes ) break ;
9284 const entry = this . entries . get ( key ) ! ;
93- this . bytesInUse -= entry . rgba . byteLength ;
85+ this . bytesInUse -= entry . rgb . byteLength ;
9486 this . entries . delete ( key ) ;
9587 }
9688 }
@@ -114,7 +106,7 @@ type NativeFrameLayout = {
114106 planarConfiguration : number ;
115107} ;
116108
117- // Convert a raw uncompressed frame to RGBA . Supports the two photometric
109+ // Convert a raw uncompressed frame to packed RGB . Supports the two photometric
118110// interpretations our sample corpus shows in the native PixelData path:
119111// - 'RGB' with samplesPerPixel=3 (interleaved RGB)
120112// - 'MONOCHROME2' with samplesPerPixel=1 (grayscale, replicated to RGB)
@@ -125,17 +117,16 @@ export function decodeNativeFrame(
125117 const { width, height, samplesPerPixel, photometric, planarConfiguration } =
126118 layout ;
127119 const pixelCount = width * height ;
128- const out = new Uint8ClampedArray ( pixelCount * 4 ) ;
120+ const out = new Uint8Array ( pixelCount * 3 ) ;
129121
130122 if ( samplesPerPixel === 1 ) {
131- // Grayscale — replicate luminance to R, G, B; alpha = 255 .
123+ // Grayscale — replicate luminance to R, G, B.
132124 for ( let i = 0 ; i < pixelCount ; i ++ ) {
133125 const v = bytes [ i ] ?? 0 ;
134- const j = i * 4 ;
126+ const j = i * 3 ;
135127 out [ j ] = v ;
136128 out [ j + 1 ] = v ;
137129 out [ j + 2 ] = v ;
138- out [ j + 3 ] = 255 ;
139130 }
140131 } else if (
141132 samplesPerPixel === 3 &&
@@ -145,20 +136,17 @@ export function decodeNativeFrame(
145136 // RRRR...GGGG...BBBB...
146137 const plane = pixelCount ;
147138 for ( let i = 0 ; i < pixelCount ; i ++ ) {
148- const j = i * 4 ;
139+ const j = i * 3 ;
149140 out [ j ] = bytes [ i ] ?? 0 ;
150141 out [ j + 1 ] = bytes [ plane + i ] ?? 0 ;
151142 out [ j + 2 ] = bytes [ 2 * plane + i ] ?? 0 ;
152- out [ j + 3 ] = 255 ;
153143 }
154144 } else {
155145 for ( let i = 0 ; i < pixelCount ; i ++ ) {
156146 const k = i * 3 ;
157- const j = i * 4 ;
158- out [ j ] = bytes [ k ] ?? 0 ;
159- out [ j + 1 ] = bytes [ k + 1 ] ?? 0 ;
160- out [ j + 2 ] = bytes [ k + 2 ] ?? 0 ;
161- out [ j + 3 ] = 255 ;
147+ out [ k ] = bytes [ k ] ?? 0 ;
148+ out [ k + 1 ] = bytes [ k + 1 ] ?? 0 ;
149+ out [ k + 2 ] = bytes [ k + 2 ] ?? 0 ;
162150 }
163151 }
164152 } else {
@@ -167,5 +155,5 @@ export function decodeNativeFrame(
167155 ) ;
168156 }
169157
170- return { width, height, rgba : out } ;
158+ return { width, height, rgb : out } ;
171159}
0 commit comments