Skip to content

Commit 7e454db

Browse files
committed
Bake line-FFT/transpose uniforms at creation time
1 parent f68fb2a commit 7e454db

File tree

16 files changed

+758
-347
lines changed

16 files changed

+758
-347
lines changed

apps/typegpu-docs/src/examples/image-processing/camera-fft/index.ts

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { oklabGamutClipSlot, oklabToLinearRgb } from '@typegpu/color';
1+
import { oklabGamutClip, oklabToLinearRgb } from '@typegpu/color';
22
import {
33
createFft2d,
44
createStockhamRadix2LineStrategy,
@@ -196,7 +196,7 @@ const filterKernel = tgpu.computeFn({
196196
const cutoff = filterLayout.$.params.cutoffRadius * rMax;
197197
const r = std.sqrt(r2);
198198
const mask = std.select(d.f32(0), d.f32(1), r <= cutoff);
199-
const c = filterLayout.$.spectrum[tid] as d.v2f;
199+
const c = filterLayout.$.spectrum[tid];
200200
filterLayout.$.spectrum[tid] = d.vec2f(c.x * mask, c.y * mask);
201201
});
202202

@@ -248,17 +248,17 @@ const magKernel = tgpu.computeFn({
248248
srcX * padH + srcY,
249249
magLayout.$.params.swapSpectrumAxes !== d.u32(0),
250250
);
251-
const cShift = magLayout.$.spectrum[srcTid] as d.v2f;
251+
const cShift = magLayout.$.spectrum[srcTid];
252252
const len = std.sqrt(cShift.x * cShift.x + cShift.y * cShift.y);
253253
const logv = std.log(1.0 + len) * magLayout.$.params.gain;
254254
const cv = std.clamp(logv, 0.0, 1.0);
255-
/** Perceptual lightness from log-magnitude; chroma scales with magnitude; phasehue in the `a,b` plane. */
256-
const eps = 1e-8;
257-
const hue = std.atan2(cShift.y, cShift.x);
258-
const chroma = std.select(cv * 0.16, d.f32(0), len < eps);
259-
const L = 0.04 + cv * 0.88;
260-
const lab = d.vec3f(L, chroma * std.cos(hue), chroma * std.sin(hue));
261-
const rgb = oklabToLinearRgb(oklabGamutClipSlot.$(lab));
255+
/** `cv` from log-magnitude; L ∈ [0.04, 1]; chroma0 at max `cv` so peaks go neutral white. */
256+
const L = 0.04 + cv * (1.0 - 0.04);
257+
const chroma = cv * (1.0 - cv) * 0.32;
258+
/** Oklab a,b = chroma × unit phase — same as chroma·(cos θ, sin θ) with θ = atan2(im, re), without trig. */
259+
const invLen = 1.0 / std.max(len, 1e-20);
260+
const lab = d.vec3f(L, chroma * invLen * cShift.x, chroma * invLen * cShift.y);
261+
const rgb = oklabToLinearRgb(oklabGamutClip.adaptiveL05(lab));
262262
std.textureStore(magLayout.$.outTex, d.vec2u(xLin, yLin), d.vec4f(rgb, 1));
263263
});
264264

@@ -295,7 +295,7 @@ const spatialKernel = tgpu.computeFn({
295295
return;
296296
}
297297

298-
const c = spatialLayout.$.spectrum[tid] as d.v2f;
298+
const c = spatialLayout.$.spectrum[tid];
299299
const inv = spatialLayout.$.params.invSize;
300300
const g = std.clamp(c.x * inv, 0.0, 1.0);
301301
const padWLog2 = spatialLayout.$.params.padWLog2;
@@ -481,9 +481,12 @@ let gainValue = 0.2;
481481
let cutoffRadiusNorm = 1;
482482
/** Separable Hann window on camera ROI before FFT (reduces periodic-boundary cross in spectrum). */
483483
let applyEdgeWindow = false;
484+
let lastFillUniformKey = '';
484485
let lastMagUniformKey = '';
485486
let lastSpatialUniformKey = '';
486487
let lastFilterUniformKey = '';
488+
let lastDisplayFbKey = '';
489+
let lastVideoBlitKey = '';
487490

488491
function effectiveFrameSize(frameW: number, frameH: number): { effW: number; effH: number } {
489492
let effW: number;
@@ -553,16 +556,22 @@ function ensureResources(frameW: number, frameH: number) {
553556
padW = nextPadW;
554557
padH = nextPadH;
555558
const lineFftStrategyFactory =
556-
lineFftMode === 'radix-2' ? createStockhamRadix2LineStrategy : createStockhamRadix4LineStrategy;
559+
lineFftMode === 'radix-2'
560+
? createStockhamRadix2LineStrategy
561+
: createStockhamRadix4LineStrategy;
557562
fft = createFft2d(root, {
558563
width: padW,
559564
height: padH,
560565
skipFinalTranspose: SKIP_FINAL_FFT_TRANSPOSE,
561566
lineFftStrategyFactory,
562567
});
563568
fftLineFftMode = lineFftMode;
569+
lastFillUniformKey = '';
564570
lastMagUniformKey = '';
571+
lastSpatialUniformKey = '';
565572
lastFilterUniformKey = '';
573+
lastDisplayFbKey = '';
574+
lastVideoBlitKey = '';
566575

567576
displayTexture = createPaddedDisplayTexture(padW, padH);
568577

@@ -682,7 +691,11 @@ function processVideoFrame(_: number, metadata: VideoFrameCallbackMetadata) {
682691

683692
const { effW, effH } = effectiveFrameSize(frameWidth, frameHeight);
684693

685-
videoBlitTargetPx.write({ w: effW, h: effH });
694+
const videoBlitKey = `${effW}x${effH}`;
695+
if (videoBlitKey !== lastVideoBlitKey) {
696+
lastVideoBlitKey = videoBlitKey;
697+
videoBlitTargetPx.write({ w: effW, h: effH });
698+
}
686699
const videoBlitBindGroup = root.createBindGroup(videoBlitLayout, {
687700
inputTexture: device.importExternalTexture({ source: video }),
688701
targetPx: videoBlitTargetPx,
@@ -696,15 +709,19 @@ function processVideoFrame(_: number, metadata: VideoFrameCallbackMetadata) {
696709
.draw(3);
697710

698711
const padWLog2 = log2Int(padW);
699-
fillParams.write({
700-
videoW: effW,
701-
videoH: effH,
702-
padW,
703-
padH,
704-
padWLog2,
705-
padWMask: padW - 1,
706-
edgeWindow: applyEdgeWindow ? 1 : 0,
707-
});
712+
const fillUniformKey = `${effW}x${effH}x${padW}x${padH}x${applyEdgeWindow}`;
713+
if (fillUniformKey !== lastFillUniformKey) {
714+
lastFillUniformKey = fillUniformKey;
715+
fillParams.write({
716+
videoW: effW,
717+
videoH: effH,
718+
padW,
719+
padH,
720+
padWLog2,
721+
padWMask: padW - 1,
722+
edgeWindow: applyEdgeWindow ? 1 : 0,
723+
});
724+
}
708725

709726
const magUniformKey = `${padW}x${padH}x${gainValue}x${activeFft.skipFinalTranspose}`;
710727
if (magUniformKey !== lastMagUniformKey) {
@@ -752,15 +769,19 @@ function processVideoFrame(_: number, metadata: VideoFrameCallbackMetadata) {
752769
const gpuCanvas = context.canvas;
753770
const fbW = Math.max(1, gpuCanvas.width);
754771
const fbH = Math.max(1, gpuCanvas.height);
755-
displayFb.write({
756-
fbW,
757-
fbH,
758-
padW,
759-
padH,
760-
effW,
761-
effH,
762-
viewMode: applyInverseFft ? 1 : 0,
763-
});
772+
const displayFbKey = `${fbW}x${fbH}x${padW}x${padH}x${effW}x${effH}x${applyInverseFft}`;
773+
if (displayFbKey !== lastDisplayFbKey) {
774+
lastDisplayFbKey = displayFbKey;
775+
displayFb.write({
776+
fbW,
777+
fbH,
778+
padW,
779+
padH,
780+
effW,
781+
effH,
782+
viewMode: applyInverseFft ? 1 : 0,
783+
});
784+
}
764785

765786
const enc = device.createCommandEncoder({ label: 'camera-fft frame' });
766787
{
@@ -806,12 +827,14 @@ export const controls = defineControls({
806827
initial: false,
807828
onToggleChange: (value) => {
808829
applyInverseFft = value;
830+
lastDisplayFbKey = '';
809831
},
810832
},
811833
edgeWindow: {
812834
initial: false,
813835
onToggleChange: (value) => {
814836
applyEdgeWindow = value;
837+
lastFillUniformKey = '';
815838
},
816839
},
817840
fftMaxSide: {
1.5 MB
Loading

apps/typegpu-docs/src/examples/image-processing/camera-thresholding/meta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"title": "Camera Thresholding",
33
"category": "image-processing",
44
"tags": ["camera", "color", "ecosystem", "filtering", "segmentation"],
5-
"coolFactor": 4
5+
"coolFactor": 5
66
}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1-
<p>Open DevTools → Console for PASS/FAIL, CPU vs GPU error, factory parity, radix-4 parity, and GPU timing table.</p>
1+
<p>
2+
Open DevTools → Console for PASS/FAIL, CPU vs GPU error, factory parity, radix-4 parity, and GPU
3+
timing table.
4+
</p>
25
<canvas width="8" height="8"></canvas>

0 commit comments

Comments
 (0)