export class OceanTests { constructor( getPrimaryPipeline ) { this.getPrimaryPipeline = getPrimaryPipeline; } async testFft( ) { try { const pipeline = this.getPrimaryPipeline( ); if ( !pipeline ) { console.warn( "No primary pipeline for testFft()" ); return; } const N = pipeline.gridSize; const block = pipeline.getBlockByName( "ocean" ); const rowPass = block.getPass( "RowFFT" ); const colPass = block.getPass( "ColFFT" ); const inputReal = new Float32Array( N * N ); const inputImag = new Float32Array( N * N ); for ( let y = 0; y < N; y++ ) { for ( let x = 0; x < N; x++ ) { const idx = y * N + x; inputReal[ idx ] = Math.sin( 2 * Math.PI * x / N ) + 0.5 * Math.cos( 2 * Math.PI * y / N ); inputImag[ idx ] = 0; } } pipeline.memory.spectrumReal.set( inputReal ); pipeline.memory.spectrumImag.set( inputImag ); const spectrumPass = block.getPass( "Spectrum" ); spectrumPass.shader.setVariable( "spectrumReal", pipeline.memory.spectrumReal ); spectrumPass.shader.setVariable( "spectrumImag", pipeline.memory.spectrumImag ); await rowPass.execute( ); await colPass.execute( ); const gpuHeight = await colPass.shader.debugBuffer( "heightField" ); const cpuResult = this.cpuFft2D( inputReal, inputImag, N ); let maxDiff = 0; let rms = 0; for ( let i = 0; i < gpuHeight.length; i++ ) { const diff = gpuHeight[ i ] - cpuResult.real[ i ]; maxDiff = Math.max( maxDiff, Math.abs( diff ) ); rms += diff * diff; } rms = Math.sqrt( rms / gpuHeight.length ); console.log( "FFT test completed. maxDiff:", maxDiff, "rms:", rms ); } catch ( e ) { console.error( "FFT test failed:", e ); } } async testSpectrum( ) { try { const pipeline = this.getPrimaryPipeline( ); if ( !pipeline ) { console.warn( "No primary pipeline for testSpectrum()" ); return; } const N = pipeline.gridSize; const block = pipeline.getBlockByName( "ocean" ); const spectrumPass = block.getPass( "Spectrum" ); const h0Real = pipeline.memory.h0Real; const h0Imag = pipeline.memory.h0Imag; const time = 0; const heightScale = pipeline.memory.computeParams[ 2 ]; const cpuSpec = this.cpuSpectrum2D( h0Real, h0Imag, N, time, heightScale ); pipeline.memory.computeParams[ 0 ] = time; pipeline.memory.computeParams[ 1 ] = N; pipeline.memory.computeParams[ 2 ] = heightScale; spectrumPass.shader.setVariable( "params", pipeline.memory.computeParams ); await spectrumPass.execute( ); const gpuReal = await spectrumPass.shader.debugBuffer( "spectrumReal" ); const gpuImag = await spectrumPass.shader.debugBuffer( "spectrumImag" ); let maxDiff = 0; let rms = 0; for ( let i = 0; i < gpuReal.length; i++ ) { const dr = gpuReal[ i ] - cpuSpec.real[ i ]; const di = gpuImag[ i ] - cpuSpec.imag[ i ]; const magDiff = Math.sqrt( dr * dr + di * di ); if ( magDiff > maxDiff ) { maxDiff = magDiff; } rms += dr * dr + di * di; } rms = Math.sqrt( rms / gpuReal.length ); console.log( "Spectrum test completed. maxDiff:", maxDiff, "rms:", rms ); } catch ( e ) { console.error( "Spectrum test failed:", e ); } } cpuFft1D( realIn, imagIn, N ) { const outR = new Float32Array( N ); const outI = new Float32Array( N ); for ( let k = 0; k < N; k++ ) { let sumR = 0; let sumI = 0; for ( let n = 0; n < N; n++ ) { const angle = -2 * Math.PI * k * n / N; const c = Math.cos( angle ); const s = Math.sin( angle ); const xr = realIn[ n ]; const xi = imagIn[ n ]; sumR += xr * c - xi * s; sumI += xr * s + xi * c; } const invScale = 1 / N; outR[ k ] = sumR * invScale; outI[ k ] = sumI * invScale; } return { real: outR, imag: outI }; } cpuFft2D( realIn, imagIn, N ) { const realTmp = new Float32Array( realIn ); const imagTmp = new Float32Array( imagIn ); for ( let row = 0; row < N; row++ ) { const rowOffset = row * N; const rSlice = realTmp.subarray( rowOffset, rowOffset + N ); const iSlice = imagTmp.subarray( rowOffset, rowOffset + N ); const res = this.cpuFft1D( rSlice, iSlice, N ); for ( let x = 0; x < N; x++ ) { realTmp[ rowOffset + x ] = res.real[ x ]; imagTmp[ rowOffset + x ] = res.imag[ x ]; } } const realOut = new Float32Array( N * N ); const imagOut = new Float32Array( N * N ); for ( let col = 0; col < N; col++ ) { const rCol = new Float32Array( N ); const iCol = new Float32Array( N ); for ( let row = 0; row < N; row++ ) { const idx = row * N + col; rCol[ row ] = realTmp[ idx ]; iCol[ row ] = imagTmp[ idx ]; } const res = this.cpuFft1D( rCol, iCol, N ); for ( let row = 0; row < N; row++ ) { const idx = row * N + col; realOut[ idx ] = res.real[ row ]; imagOut[ idx ] = res.imag[ row ]; } } return { real: realOut, imag: imagOut }; } cpuSpectrum2D( h0Real, h0Imag, size, time, heightScale ) { const GRAVITY = 9.81; const realOut = new Float32Array( size * size ); const imagOut = new Float32Array( size * size ); for ( let ky = 0; ky < size; ky++ ) { for ( let kx = 0; kx < size; kx++ ) { const kxShift = kx - size / 2; const kyShift = ky - size / 2; const kLength = Math.sqrt( kxShift * kxShift + kyShift * kyShift ); const idx = ky * size + kx; if ( kLength === 0 ) { continue; } const mx = ( size - kx ) % size; const my = ( size - ky ) % size; const mirrorIdx = my * size + mx; const h0R = h0Real[ idx ]; const h0I = h0Imag[ idx ]; const h0mR = h0Real[ mirrorIdx ]; const h0mI = h0Imag[ mirrorIdx ]; const omega = Math.sqrt( GRAVITY * kLength ); const theta = omega * time; const cosT = Math.cos( theta ); const sinT = Math.sin( theta ); const hPosR = h0R * cosT - h0I * sinT; const hPosI = h0R * sinT + h0I * cosT; const hNegR = h0mR * cosT + h0mI * sinT; const hNegI = h0mI * cosT - h0mR * sinT; realOut[ idx ] = ( hPosR + hNegR ) * heightScale; imagOut[ idx ] = ( hPosI + hNegI ) * heightScale; } } return { real: realOut, imag: imagOut }; } }