From aaec3118d30f302b9f3b351260f3f36ce4826c55 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Mar 2026 05:37:04 +0000 Subject: [PATCH] Fix rendering freeze when loading a new module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cells buffer update effect was using `renderParamsRef.current.matrix` as a React dep — a mutable ref read that can be ambiguous when React evaluates the dependency array. This meant the GPU cells buffer could fail to refresh on module load, leaving stale pattern data on screen. Fix: pass `matrix` and `padTopChannel` as explicit parameters to `useWebGPURender` and use them directly as effect deps. React now reliably detects the new-module identity change and rebuilds the cells buffer and bind group. Same fix applied to the channels buffer effect (matrix?.numChannels → matrix?.numChannels via direct param). https://claude.ai/code/session_01GnHw4NyaiJ3PA7TUzui6t4 --- components/PatternDisplay.tsx | 5 +++-- hooks/useWebGPURender.ts | 17 ++++++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/components/PatternDisplay.tsx b/components/PatternDisplay.tsx index 8634d01..cb2ed7e 100644 --- a/components/PatternDisplay.tsx +++ b/components/PatternDisplay.tsx @@ -234,10 +234,11 @@ export const PatternDisplay: React.FC = ({ ...(playbackStateRef ? { playbackStateRef } : {}), }, setDebugInfo); - // WebGPU render hook + // WebGPU render hook — matrix and padTopChannel passed directly so React tracks them as + // explicit deps, guaranteeing the cells buffer is rebuilt when a new module is loaded. const { gpuReady, render, deviceRef: gpuDevRef } = useWebGPURender( canvasRef, glCanvasRef, shaderFile, - syncCanvasSize, renderParamsRef, setDebugInfo, setWebgpuAvailable + syncCanvasSize, renderParamsRef, matrix, padTopChannel, setDebugInfo, setWebgpuAvailable ); // Keep resize reconfiguration refs in sync diff --git a/hooks/useWebGPURender.ts b/hooks/useWebGPURender.ts index b9cba4d..d92f169 100644 --- a/hooks/useWebGPURender.ts +++ b/hooks/useWebGPURender.ts @@ -85,6 +85,8 @@ export function useWebGPURender( shaderFile: string, syncCanvasSize: (canvas: HTMLCanvasElement, gl: HTMLCanvasElement | null) => void, renderParamsRef: React.MutableRefObject, + matrix: import('../types').PatternMatrix | null, + padTopChannel: boolean, setDebugInfo: React.Dispatch>, setWebgpuAvailable: (v: boolean) => void ) { @@ -361,16 +363,17 @@ export function useWebGPURender( }; }, [shaderFile, syncCanvasSize]); // eslint-disable-line react-hooks/exhaustive-deps - // Update cells buffer when matrix changes + // Update cells buffer when matrix changes. + // Uses `matrix` and `padTopChannel` as direct React deps (not via renderParamsRef) so React + // reliably detects new-module loads even if the ref mutation timing is ambiguous. useEffect(() => { const device = deviceRef.current; if (!device || !gpuReady) return; - const p = renderParamsRef.current; - console.log(`[PatternDisplay] Updating cells buffer: matrix=${p.matrix ? 'yes' : 'null'}, rows=${p.matrix?.numRows}, channels=${p.matrix?.numChannels}`); + console.log(`[PatternDisplay] Updating cells buffer: matrix=${matrix ? 'yes' : 'null'}, rows=${matrix?.numRows}, channels=${matrix?.numChannels}`); if (cellsBufferRef.current) cellsBufferRef.current.destroy(); const isHighPrec = shaderFile.includes('v0.36') || shaderFile.includes('v0.37') || shaderFile.includes('v0.38') || shaderFile.includes('v0.39') || shaderFile.includes('v0.40') || shaderFile.includes('v0.42') || shaderFile.includes('v0.43') || shaderFile.includes('v0.44') || shaderFile.includes('v0.45') || shaderFile.includes('v0.46') || shaderFile.includes('v0.47') || shaderFile.includes('v0.48') || shaderFile.includes('v0.49') || shaderFile.includes('v0.50'); const packFunc = isHighPrec ? packPatternMatrixHighPrecision : packPatternMatrix; - const packedData = packFunc(p.matrix, p.padTopChannel); + const packedData = packFunc(matrix, padTopChannel); let noteCount = 0; for (let i = 0; i < packedData.length; i += 2) { const note = ((packedData[i] ?? 0) >> 24) & 0xFF; @@ -379,7 +382,7 @@ export function useWebGPURender( console.log(`[PatternDisplay] Packed data contains ${noteCount} notes in ${packedData.length / 2} cells`); cellsBufferRef.current = createBufferWithData(device, packedData, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST); if (layoutTypeRef.current === 'extended') { - const numRows = p.matrix?.numRows ?? DEFAULT_ROWS; + const numRows = matrix?.numRows ?? DEFAULT_ROWS; const flags = buildRowFlags(numRows); if (!rowFlagsBufferRef.current || rowFlagsBufferRef.current.size < flags.byteLength) { rowFlagsBufferRef.current?.destroy(); @@ -389,7 +392,7 @@ export function useWebGPURender( } } refreshBindGroup(device); - }, [renderParamsRef.current.matrix, gpuReady, shaderFile, refreshBindGroup]); // eslint-disable-line react-hooks/exhaustive-deps + }, [matrix, padTopChannel, gpuReady, shaderFile, refreshBindGroup]); // eslint-disable-line react-hooks/exhaustive-deps // Update channel states buffer useEffect(() => { @@ -415,7 +418,7 @@ export function useWebGPURender( } if (recreated) refreshBindGroup(device); } - }, [renderParamsRef.current.channels, renderParamsRef.current.matrix?.numChannels, gpuReady, refreshBindGroup]); // eslint-disable-line react-hooks/exhaustive-deps + }, [renderParamsRef.current.channels, matrix?.numChannels, padTopChannel, gpuReady, refreshBindGroup]); // eslint-disable-line react-hooks/exhaustive-deps // Stable render function — reads from renderParamsRef to avoid stale closures const render = useCallback(() => {