From 4e68b2acffd50ff4615b3079561cc6b78b122014 Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Mon, 3 Aug 2026 05:51:52 -0400 Subject: [PATCH 1/2] refactor(benchmarks): extract live viewport controllers --- apps/benchmarks/src/app.tsx | 991 +----------------- .../benchmark/bitmap-text-viewport.tsx | 468 +++++++++ .../benchmark/live-text-viewport-contracts.ts | 22 + .../surfaces/benchmark/sdf-text-viewports.tsx | 648 ++++++++++++ .../benchmark/viewport-boundary.test.ts | 21 + 5 files changed, 1164 insertions(+), 986 deletions(-) create mode 100644 apps/benchmarks/src/surfaces/benchmark/bitmap-text-viewport.tsx create mode 100644 apps/benchmarks/src/surfaces/benchmark/live-text-viewport-contracts.ts create mode 100644 apps/benchmarks/src/surfaces/benchmark/sdf-text-viewports.tsx create mode 100644 apps/benchmarks/src/surfaces/benchmark/viewport-boundary.test.ts diff --git a/apps/benchmarks/src/app.tsx b/apps/benchmarks/src/app.tsx index f4b3a71..da88c88 100644 --- a/apps/benchmarks/src/app.tsx +++ b/apps/benchmarks/src/app.tsx @@ -78,16 +78,8 @@ import { type WorkloadOption, } from './benchmark/workloads'; import { Chip } from './components/ui'; -import type { - BitmapTextLiveStats, - BitmapTextPersistentScene, - BitmapTextPreviewSnapshot, - BitmapTextPreviewUpdate, -} from './renderer/bitmap-text'; -import type { MtsdfTextLiveStats, MtsdfTextPersistentScene } from './renderer/mtsdf-text'; -import type { SlugTextLiveStats, SlugTextPersistentScene } from './renderer/slug-text'; +import type { BitmapTextPreviewUpdate } from './renderer/bitmap-text'; import type { PersistentRenderJob } from './renderer/persistent-render-host'; -import { createLatestAsyncQueue, type LatestAsyncQueue } from './renderer/latest-async-queue'; import type { ComparisonWorkloadConfiguration, ComparisonWorkloadId, @@ -100,16 +92,14 @@ import { isBenchmarkWorkloadId, type BenchmarkWorkloadId, } from './workloads/catalog'; -import { - benchmarkContentWidth, - BENCHMARK_CONTENT_INSET, - BENCHMARK_CONTENT_MINIMUM_VIEWPORT_WIDTH, -} from './workloads/shared/text-style'; +import { BENCHMARK_CONTENT_INSET, BENCHMARK_CONTENT_MINIMUM_VIEWPORT_WIDTH } from './workloads/shared/text-style'; import { PersistentRenderHostProvider, usePersistentRenderHost } from './renderer/persistent-render-host-context'; import { ConformanceSurface } from './surfaces/conformance/conformance-surface'; import { BakeProgressOverlay, useBakeProgress } from './surfaces/benchmark/bake-progress-overlay'; +import { BitmapTextViewport } from './surfaces/benchmark/bitmap-text-viewport'; +import { MtsdfTextViewport, SlugTextViewport } from './surfaces/benchmark/sdf-text-viewports'; +import type { LiveTextConfiguration } from './surfaces/benchmark/live-text-viewport-contracts'; import { LiveBenchmarkSurface } from './surfaces/benchmark/live-benchmark-surface'; -import type { BakeProgress } from '@pmndrs/text'; import { Route, Switch, useLocation } from 'wouter'; type LiveTextStats = RuntimeLiveStats; @@ -207,25 +197,6 @@ function liveSceneAssetResource( return resource; } -interface LiveTextConfiguration extends Omit { - readonly animatePresentation: boolean; - readonly fontFixture: BenchmarkFontFixture; - readonly expectedGlyphCount: number | undefined; - readonly timelineTick: number | undefined; -} - -interface RetainedLiveTextUpdate extends BitmapTextPreviewUpdate { - readonly timelineTick: number | undefined; - readonly workload: BenchmarkWorkloadId; -} - -interface PresentationEvidence { - readonly revision: number; - readonly progress: 0 | 1; - readonly matchedGlyphs: number; - readonly targetGlyphs: number; -} - interface ActivityWorkloads { readonly benchmark: BenchmarkWorkloadId; readonly conformance: ConformanceWorkloadId; @@ -238,7 +209,6 @@ const INITIAL_CONFORMANCE_VIEW: ConformanceView = { }; const EMPTY_FONT_FEATURES: BitmapTextPreviewUpdate['features'] = []; -const GLYPH_POSITION_TRANSITION_MS = 110; function techniqueLabel(technique: RasterTechnique): 'Bitmap' | 'MSDF' | 'Slug' { return technique === 'mtsdf' ? 'MSDF' : technique === 'slug' ? 'Slug' : 'Bitmap'; } @@ -1316,957 +1286,6 @@ function BenchmarkSurface({ ); } -function bitmapViewportEvidence({ - anchor, - fontFixture, - grid, - layoutWidthRatio, - presentationEvidence, - settledRevision, - settledTextLength, - settledTimelineTick, - stats, - text, - textAlign, -}: { - readonly anchor: LiveTextConfiguration['anchor']; - readonly fontFixture: BenchmarkFontFixture; - readonly grid: boolean; - readonly layoutWidthRatio: number; - readonly presentationEvidence: PresentationEvidence; - readonly settledRevision: number; - readonly settledTextLength: number; - readonly settledTimelineTick: number | undefined; - readonly stats: BitmapTextLiveStats | undefined; - readonly text: string; - readonly textAlign: LiveTextConfiguration['textAlign']; -}): Record<`data-${string}`, string | number | boolean | undefined> { - return { - 'data-canvas-grid': String(grid), - 'data-anchor': anchor, - 'data-layout-width': stats?.layoutWidth, - 'data-layout-width-ratio': layoutWidthRatio, - 'data-content-inset': BENCHMARK_CONTENT_INSET, - 'data-content-min-width': BENCHMARK_CONTENT_MINIMUM_VIEWPORT_WIDTH * layoutWidthRatio, - 'data-content-policy': 'bounded-pan', - 'data-line-count': stats?.lineCount, - 'data-frame-count': stats?.frameCount, - 'data-frames-per-second': stats?.framesPerSecond, - 'data-font-fixture': fontFixture, - 'data-median-submit-ms': stats?.medianSubmitMs, - 'data-p95-submit-ms': stats?.p95SubmitMs, - 'data-gpu-frame-ms': stats?.gpuFrameMs, - 'data-median-gpu-ms': stats?.medianGpuMs, - 'data-p95-gpu-ms': stats?.p95GpuMs, - 'data-submit-history-length': stats?.submitHistoryLength, - 'data-fps-history-length': stats?.fpsHistoryLength, - 'data-glyph-count': stats?.glyphCount, - 'data-missing-glyph-count': stats?.missingGlyphCount, - 'data-draw-count': stats?.drawCount, - 'data-renderer-init-ms': stats?.rendererInitMs, - 'data-strike-ppem': stats?.strikePpem, - 'data-css-font-size': stats?.cssFontSize, - 'data-rendered-device-px': stats?.renderedPpem, - 'data-scale-ratio': stats?.scaleRatio, - 'data-font-load-ms': stats?.fontLoadMs, - 'data-text-ready-ms': stats?.textReadyMs, - 'data-first-draw-ms': stats?.firstDrawMs, - 'data-upload-frame-gpu-ms': stats?.uploadFrameGpuMs, - 'data-upload-frame-complete-ms': stats?.uploadFrameCompleteMs, - 'data-startup-ms': stats?.startupMs, - 'data-source-text-length': text.length, - 'data-text-align': textAlign, - 'data-artifact-bytes': stats?.artifactBytes, - 'data-atlas-gpu-bytes': stats?.atlasGpuBytes, - 'data-total-gpu-bytes': stats?.totalGpuBytes, - 'data-settled-revision': settledRevision, - 'data-settled-text-length': settledTextLength, - 'data-settled-tick': settledTimelineTick, - 'data-presentation-matched-glyphs': presentationEvidence.matchedGlyphs, - 'data-presentation-progress': presentationEvidence.progress, - 'data-presentation-revision': presentationEvidence.revision, - 'data-presentation-target-glyphs': presentationEvidence.targetGlyphs, - 'data-backend': stats?.backend, - 'data-dpr': stats?.dpr, - 'data-font-delivery': stats?.delivery, - 'data-core-bake-ms': stats?.coreBakeMs, - 'data-raster-bake-ms': stats?.rasterBakeMs, - 'data-source-font-bytes': stats?.sourceFontBytes, - 'data-core-artifact-bytes': stats?.coreArtifactBytes, - 'data-raster-artifact-bytes': stats?.rasterArtifactBytes, - 'data-gpu-history-length': stats?.gpuHistoryLength, - 'data-gpu-timing-supported': stats?.gpuTimingSupported, - }; -} - -interface BitmapTextViewportProps { - readonly backend: GraphicsBackend; - readonly delivery: FontDelivery; - readonly dpr: 1 | 2; - readonly fontSize: number; - readonly grid: boolean; - readonly suppressLoading: boolean; - readonly stats: BitmapTextLiveStats | undefined; - readonly surfaceAnchorRef: RefObject; - readonly textConfiguration: LiveTextConfiguration; - readonly workload: BenchmarkWorkloadId; - readonly onStats: (stats: BitmapTextLiveStats) => void; -} - -function BitmapTextViewport({ - backend, - delivery, - dpr, - fontSize, - grid, - suppressLoading, - stats, - surfaceAnchorRef, - textConfiguration, - workload, - onStats, -}: BitmapTextViewportProps) { - const { activateSurface } = usePersistentRenderHost(); - const activatePersistentSurface = useEffectEvent(activateSurface); - const containerRef = useRef(null); - const previewRef = useRef(undefined); - const updateQueueRef = useRef>(undefined); - const pendingSettledWorkloadRef = useRef(undefined); - const [settledRevision, setSettledRevision] = useState(0); - const [settledTextLength, setSettledTextLength] = useState(0); - const [settledTimelineTick, setSettledTimelineTick] = useState(); - const [settledWorkload, setSettledWorkload] = useState(); - const [presentationEvidence, setPresentationEvidence] = useState({ - revision: 0, - progress: 1, - matchedGlyphs: 0, - targetGlyphs: 0, - }); - const [error, setError] = useState(); - const { - active: bakeProgressActive, - finish: finishBakeProgress, - publish: publishBakeProgress, - value: bakeProgressValue, - } = useBakeProgress('bitmap'); - const { - anchor, - animatePresentation, - direction, - expectedGlyphCount, - features, - fontFixture, - language, - layoutWidthRatio, - text, - textAlign, - timelineTick, - } = textConfiguration; - const publishStats = useEffectEvent((next: BitmapTextLiveStats) => { - finishBakeProgress(); - onStats(next); - setError(undefined); - const pendingWorkload = pendingSettledWorkloadRef.current; - if (pendingWorkload !== undefined) { - pendingSettledWorkloadRef.current = undefined; - setSettledWorkload(pendingWorkload); - } - }); - const publishError = useEffectEvent((caught: unknown) => { - if (caught instanceof DOMException && caught.name === 'AbortError') return; - finishBakeProgress(); - setError(caught instanceof Error ? caught.message : String(caught)); - }); - const previewConfiguration = useEffectEvent(() => ({ - anchor, - direction, - expectedGlyphCount, - features, - fontFixture, - fontSize, - language, - layoutWidthRatio, - showGrid: grid, - text, - textAlign, - timelineTick, - workload, - })); - const publishSettledRevision = useEffectEvent((revision: number) => { - setSettledRevision(revision); - }); - const publishSettledTimelineTick = useEffectEvent((tick: number | undefined) => { - setSettledTimelineTick(tick); - }); - const publishSettledTextLength = useEffectEvent((length: number) => { - setSettledTextLength(length); - }); - const publishSettledWorkload = useEffectEvent((value: string) => { - pendingSettledWorkloadRef.current = value; - }); - const publishPresentation = useEffectEvent((snapshot: BitmapTextPreviewSnapshot, progress: 0 | 1) => { - setPresentationEvidence({ - revision: snapshot.revision, - progress, - matchedGlyphs: snapshot.matchedGlyphs, - targetGlyphs: snapshot.targetGlyphs, - }); - }); - - useEffect(() => { - const container = containerRef.current; - const surfaceAnchor = surfaceAnchorRef.current; - if (container === null || surfaceAnchor === null) return; - const controller = new AbortController(); - const configuration = previewConfiguration(); - let preview: BitmapTextPersistentScene | undefined; - let updateQueue: LatestAsyncQueue | undefined; - let surfaceLease: Awaited> | undefined; - let cancelled = false; - const initialization = (async () => { - const { createBitmapTextPersistentScene } = await loadBitmapTextRenderer(); - if (cancelled) return; - const created = createBitmapTextPersistentScene({ - anchor: configuration.anchor, - backend, - delivery, - ...(configuration.expectedGlyphCount === undefined - ? {} - : { expectedGlyphCount: configuration.expectedGlyphCount }), - fontFixture: configuration.fontFixture, - fontSize: configuration.fontSize, - showGrid: configuration.showGrid, - layoutWidth: benchmarkContentWidth(container.clientWidth, configuration.layoutWidthRatio), - layoutWidthRatio: configuration.layoutWidthRatio, - text: configuration.text, - language: configuration.language, - direction: configuration.direction, - features: configuration.features, - textAlign: configuration.textAlign, - onError: publishError, - onStats: publishStats, - onBakeProgress: publishBakeProgress, - }); - preview = created; - previewRef.current = created; - updateQueue = createLatestAsyncQueue((update: RetainedLiveTextUpdate) => created.update(update)); - updateQueueRef.current = updateQueue; - surfaceLease = await activatePersistentSurface( - { - anchor: surfaceAnchor, - controller: previewRef, - label: `Live bitmap benchmark using ${backend}`, - pan: true, - scene: created, - zoom: false, - }, - controller.signal, - ); - if (cancelled) { - await surfaceLease.release(); - return; - } - const committed = await updateQueue.enqueue(previewConfiguration()); - publishSettledTimelineTick(committed.input.timelineTick); - publishSettledTextLength(committed.input.text.length); - publishSettledWorkload(committed.input.workload); - })(); - void initialization.catch(publishError); - return () => { - cancelled = true; - controller.abort(); - void initialization.then( - async () => { - const current = preview; - preview = undefined; - if (previewRef.current === current) previewRef.current = undefined; - if (updateQueueRef.current === updateQueue) updateQueueRef.current = undefined; - await surfaceLease?.release(); - }, - () => { - const current = preview; - preview = undefined; - if (previewRef.current === current) previewRef.current = undefined; - if (updateQueueRef.current === updateQueue) updateQueueRef.current = undefined; - }, - ); - }; - }, [backend, delivery, publishBakeProgress, surfaceAnchorRef]); - - useEffect(() => { - previewRef.current?.setGridVisible(grid); - }, [grid]); - - useEffect(() => { - const preview = previewRef.current; - const updateQueue = updateQueueRef.current; - if (preview === undefined || updateQueue === undefined) return; - let cancelled = false; - let animationFrame: number | undefined; - const publishSettled = (snapshot: BitmapTextPreviewSnapshot, input: RetainedLiveTextUpdate): void => { - if (cancelled) return; - publishPresentation(snapshot, 1); - publishSettledRevision(snapshot.revision); - publishSettledTimelineTick(input.timelineTick); - publishSettledTextLength(input.text.length); - publishSettledWorkload(input.workload); - }; - void updateQueue - .enqueue({ - anchor, - fontFixture, - fontSize, - layoutWidthRatio, - text, - language, - direction, - expectedGlyphCount, - features, - textAlign, - timelineTick, - workload, - }) - .then(({ input, output: snapshot }) => { - if (cancelled) return; - publishPresentation(snapshot, 0); - if (!animatePresentation) { - publishSettled(preview.finishPresentation(snapshot.revision), input); - return; - } - const startedAt = performance.now(); - const animate = (timestamp: number): void => { - if (cancelled) return; - const linearProgress = Math.min(1, Math.max(0, (timestamp - startedAt) / GLYPH_POSITION_TRANSITION_MS)); - const easedProgress = linearProgress * linearProgress * (3 - 2 * linearProgress); - const presented = preview.setPresentationProgress(snapshot.revision, easedProgress); - if (linearProgress === 1) { - publishSettled(presented, input); - return; - } - animationFrame = requestAnimationFrame(animate); - }; - animationFrame = requestAnimationFrame(animate); - }) - .catch(publishError); - return () => { - cancelled = true; - if (animationFrame !== undefined) cancelAnimationFrame(animationFrame); - }; - }, [ - anchor, - animatePresentation, - direction, - dpr, - expectedGlyphCount, - features, - fontFixture, - fontSize, - language, - layoutWidthRatio, - text, - textAlign, - timelineTick, - workload, - ]); - - return ( -
- -
- ); -} - -function BitmapViewportChrome({ - backend, - bakeProgressActive, - bakeProgressValue, - delivery, - dpr, - error, - fontSize, - stats, - suppressLoading, -}: { - readonly backend: GraphicsBackend; - readonly bakeProgressActive: boolean; - readonly bakeProgressValue: BakeProgress | undefined; - readonly delivery: FontDelivery; - readonly dpr: 1 | 2; - readonly error: string | undefined; - readonly fontSize: number; - readonly stats: BitmapTextLiveStats | undefined; - readonly suppressLoading: boolean; -}) { - return ( - <> -
- - {delivery === 'runtime' ? 'RUNTIME' : 'BAKED'} {stats?.strikePpem ?? 16} PPEM · {fontSize} CSS PX /{' '} - {stats?.renderedPpem ?? fontSize * dpr} DEVICE PX · {(stats?.scaleRatio ?? 1).toFixed(2)}× - - {dpr}× DPR -
- {!suppressLoading && (stats === undefined || bakeProgressActive) && error === undefined && ( - - )} - {error !== undefined && ( -
- {error} -
- )} - - ); -} - -function MtsdfTextViewport({ - backend, - delivery, - dpr, - fontSize, - grid, - suppressLoading, - stats, - surfaceAnchorRef, - textConfiguration, - workload, - onStats, -}: { - readonly backend: GraphicsBackend; - readonly delivery: FontDelivery; - readonly dpr: 1 | 2; - readonly fontSize: number; - readonly grid: boolean; - readonly suppressLoading: boolean; - readonly stats: MtsdfTextLiveStats | undefined; - readonly surfaceAnchorRef: RefObject; - readonly textConfiguration: LiveTextConfiguration; - readonly workload: BenchmarkWorkloadId; - readonly onStats: (stats: LiveTextStats) => void; -}) { - const { activateSurface } = usePersistentRenderHost(); - const activatePersistentSurface = useEffectEvent(activateSurface); - const containerRef = useRef(null); - const previewRef = useRef(undefined); - const updateQueueRef = useRef>(undefined); - const pendingSettledWorkloadRef = useRef(undefined); - const [error, setError] = useState(); - const [settledWorkload, setSettledWorkload] = useState(); - const { - active: bakeProgressActive, - finish: finishBakeProgress, - publish: publishBakeProgress, - value: bakeProgressValue, - } = useBakeProgress('MSDF'); - const { anchor, direction, features, fontFixture, language, layoutWidthRatio, text, textAlign, timelineTick } = - textConfiguration; - const publishStats = useEffectEvent((next: MtsdfTextLiveStats) => { - finishBakeProgress(); - onStats(next); - setError(undefined); - const pendingWorkload = pendingSettledWorkloadRef.current; - if (pendingWorkload !== undefined) { - pendingSettledWorkloadRef.current = undefined; - setSettledWorkload(pendingWorkload); - } - }); - const publishError = useEffectEvent((caught: unknown) => { - if (caught instanceof DOMException && caught.name === 'AbortError') return; - finishBakeProgress(); - setError(caught instanceof Error ? caught.message : String(caught)); - }); - const previewConfiguration = useEffectEvent(() => ({ - anchor, - direction, - features, - fontFixture, - fontSize, - language, - layoutWidthRatio, - showGrid: grid, - text, - textAlign, - timelineTick, - workload, - })); - const publishSettledWorkload = useEffectEvent((value: string) => { - pendingSettledWorkloadRef.current = value; - }); - - useEffect(() => { - const container = containerRef.current; - const surfaceAnchor = surfaceAnchorRef.current; - if (container === null || surfaceAnchor === null) return; - const controller = new AbortController(); - const configuration = previewConfiguration(); - let preview: MtsdfTextPersistentScene | undefined; - let updateQueue: LatestAsyncQueue | undefined; - let surfaceLease: Awaited> | undefined; - let cancelled = false; - const initialization = (async () => { - const { createMtsdfTextPersistentScene } = await loadMtsdfTextRenderer(); - if (cancelled) return; - const created = createMtsdfTextPersistentScene({ - anchor: configuration.anchor, - backend, - delivery, - fontSize: configuration.fontSize, - fontFixture: configuration.fontFixture, - showGrid: configuration.showGrid, - layoutWidth: benchmarkContentWidth(container.clientWidth, configuration.layoutWidthRatio), - layoutWidthRatio: configuration.layoutWidthRatio, - text: configuration.text, - textAlign: configuration.textAlign, - language: configuration.language, - direction: configuration.direction, - features: configuration.features, - onError: publishError, - onStats: publishStats, - onBakeProgress: publishBakeProgress, - }); - preview = created; - previewRef.current = created; - updateQueue = createLatestAsyncQueue((update: RetainedLiveTextUpdate) => created.update(update)); - updateQueueRef.current = updateQueue; - surfaceLease = await activatePersistentSurface( - { - anchor: surfaceAnchor, - controller: previewRef, - label: `Live MSDF benchmark using ${backend}`, - pan: true, - scene: created, - zoom: false, - }, - controller.signal, - ); - if (cancelled) await surfaceLease.release(); - else { - const committed = await updateQueue.enqueue(previewConfiguration()); - publishSettledWorkload(committed.input.workload); - } - })(); - void initialization.catch(publishError); - return () => { - cancelled = true; - controller.abort(); - void initialization.then( - async () => { - const current = preview; - preview = undefined; - if (previewRef.current === current) previewRef.current = undefined; - if (updateQueueRef.current === updateQueue) updateQueueRef.current = undefined; - await surfaceLease?.release(); - }, - () => { - const current = preview; - preview = undefined; - if (previewRef.current === current) previewRef.current = undefined; - if (updateQueueRef.current === updateQueue) updateQueueRef.current = undefined; - }, - ); - }; - }, [backend, delivery, publishBakeProgress, surfaceAnchorRef]); - - useEffect(() => { - previewRef.current?.setGridVisible(grid); - }, [grid]); - - useEffect(() => { - const updateQueue = updateQueueRef.current; - if (updateQueue === undefined) return; - void updateQueue - .enqueue({ - anchor, - direction, - features, - fontFixture, - fontSize, - language, - layoutWidthRatio, - text, - textAlign, - timelineTick, - workload, - }) - .then(({ input }) => publishSettledWorkload(input.workload)) - .catch(publishError); - }, [ - anchor, - direction, - dpr, - features, - fontFixture, - fontSize, - language, - layoutWidthRatio, - text, - textAlign, - timelineTick, - workload, - ]); - - return ( -
-
- - MTSDF {stats?.rasterEmSize ?? '—'} PX/EM · {fontSize} CSS PX / {stats?.renderedPpem ?? '—'} DEVICE PX ·{' '} - {stats?.scaleRatio.toFixed(2) ?? '—'}× - - {dpr}× DPR -
- {!suppressLoading && (stats === undefined || bakeProgressActive) && error === undefined && ( - - )} - {error !== undefined && ( -
- {error} -
- )} -
- ); -} - -function SlugTextViewport({ - backend, - delivery, - dpr, - fontSize, - grid, - suppressLoading, - stats, - surfaceAnchorRef, - textConfiguration, - workload, - onStats, -}: { - readonly backend: GraphicsBackend; - readonly delivery: FontDelivery; - readonly dpr: 1 | 2; - readonly fontSize: number; - readonly grid: boolean; - readonly suppressLoading: boolean; - readonly stats: SlugTextLiveStats | undefined; - readonly surfaceAnchorRef: RefObject; - readonly textConfiguration: LiveTextConfiguration; - readonly workload: BenchmarkWorkloadId; - readonly onStats: (stats: LiveTextStats) => void; -}) { - const { activateSurface } = usePersistentRenderHost(); - const activatePersistentSurface = useEffectEvent(activateSurface); - const containerRef = useRef(null); - const previewRef = useRef(undefined); - const updateQueueRef = useRef>(undefined); - const pendingSettledWorkloadRef = useRef(undefined); - const [error, setError] = useState(); - const [settledWorkload, setSettledWorkload] = useState(); - const { - active: bakeProgressActive, - finish: finishBakeProgress, - publish: publishBakeProgress, - value: bakeProgressValue, - } = useBakeProgress('Slug'); - const { anchor, direction, features, fontFixture, language, layoutWidthRatio, text, textAlign, timelineTick } = - textConfiguration; - const publishStats = useEffectEvent((next: SlugTextLiveStats) => { - finishBakeProgress(); - onStats(next); - setError(undefined); - const pendingWorkload = pendingSettledWorkloadRef.current; - if (pendingWorkload !== undefined) { - pendingSettledWorkloadRef.current = undefined; - setSettledWorkload(pendingWorkload); - } - }); - const publishError = useEffectEvent((caught: unknown) => { - if (caught instanceof DOMException && caught.name === 'AbortError') return; - finishBakeProgress(); - setError(caught instanceof Error ? caught.message : String(caught)); - }); - const previewConfiguration = useEffectEvent(() => ({ - anchor, - direction, - features, - fontFixture, - fontSize, - language, - layoutWidthRatio, - showGrid: grid, - text, - textAlign, - timelineTick, - workload, - })); - const publishSettledWorkload = useEffectEvent((value: string) => { - pendingSettledWorkloadRef.current = value; - }); - - useEffect(() => { - const container = containerRef.current; - const surfaceAnchor = surfaceAnchorRef.current; - if (container === null || surfaceAnchor === null) return; - const controller = new AbortController(); - const configuration = previewConfiguration(); - let preview: SlugTextPersistentScene | undefined; - let updateQueue: LatestAsyncQueue | undefined; - let surfaceLease: Awaited> | undefined; - let cancelled = false; - const initialization = (async () => { - const { createSlugTextPersistentScene } = await loadSlugTextRenderer(); - if (cancelled) return; - const created = createSlugTextPersistentScene({ - anchor: configuration.anchor, - backend, - delivery, - fontSize: configuration.fontSize, - fontFixture: configuration.fontFixture, - showGrid: configuration.showGrid, - layoutWidthRatio: configuration.layoutWidthRatio, - text: configuration.text, - textAlign: configuration.textAlign, - language: configuration.language, - direction: configuration.direction, - features: configuration.features, - onError: publishError, - onStats: publishStats, - onBakeProgress: publishBakeProgress, - }); - preview = created; - previewRef.current = created; - updateQueue = createLatestAsyncQueue((update: RetainedLiveTextUpdate) => created.update(update)); - updateQueueRef.current = updateQueue; - surfaceLease = await activatePersistentSurface( - { - anchor: surfaceAnchor, - controller: previewRef, - label: `Live Slug benchmark using ${backend}`, - pan: true, - scene: created, - zoom: false, - }, - controller.signal, - ); - if (cancelled) await surfaceLease.release(); - else { - const committed = await updateQueue.enqueue(previewConfiguration()); - publishSettledWorkload(committed.input.workload); - } - })(); - void initialization.catch(publishError); - return () => { - cancelled = true; - controller.abort(); - void initialization.then( - async () => { - const current = preview; - preview = undefined; - if (previewRef.current === current) previewRef.current = undefined; - if (updateQueueRef.current === updateQueue) updateQueueRef.current = undefined; - await surfaceLease?.release(); - }, - () => { - const current = preview; - preview = undefined; - if (previewRef.current === current) previewRef.current = undefined; - if (updateQueueRef.current === updateQueue) updateQueueRef.current = undefined; - }, - ); - }; - }, [backend, delivery, publishBakeProgress, surfaceAnchorRef]); - - useEffect(() => { - previewRef.current?.setGridVisible(grid); - }, [grid]); - - useEffect(() => { - const updateQueue = updateQueueRef.current; - if (updateQueue === undefined) return; - void updateQueue - .enqueue({ - anchor, - direction, - features, - fontFixture, - fontSize, - language, - layoutWidthRatio, - text, - textAlign, - timelineTick, - workload, - }) - .then(({ input }) => publishSettledWorkload(input.workload)) - .catch(publishError); - }, [ - anchor, - direction, - dpr, - features, - fontFixture, - fontSize, - language, - layoutWidthRatio, - text, - textAlign, - timelineTick, - workload, - ]); - - return ( -
-
- - SLUG ANALYTIC · {stats?.slugPageCount ?? '—'} PAGE - {stats?.slugPageCount === 1 ? '' : 'S'} · {fontSize} CSS PX / {stats?.renderedPpem ?? '—'} DEVICE PX - - {dpr}× DPR -
- {!suppressLoading && (stats === undefined || bakeProgressActive) && error === undefined && ( - - )} - {error !== undefined && ( -
- {error} -
- )} -
- ); -} - function comparisonViewportEvidence({ layoutWidthRatio, stats, diff --git a/apps/benchmarks/src/surfaces/benchmark/bitmap-text-viewport.tsx b/apps/benchmarks/src/surfaces/benchmark/bitmap-text-viewport.tsx new file mode 100644 index 0000000..8546f41 --- /dev/null +++ b/apps/benchmarks/src/surfaces/benchmark/bitmap-text-viewport.tsx @@ -0,0 +1,468 @@ +import { useEffect, useEffectEvent, useRef, useState, type RefObject } from 'react'; + +import type { BenchmarkFontFixture } from '../../benchmark/font-fixtures'; +import type { FontDelivery, GraphicsBackend } from '../../benchmark/url-state'; +import type { + BitmapTextLiveStats, + BitmapTextPersistentScene, + BitmapTextPreviewSnapshot, +} from '../../renderer/bitmap-text'; +import { createLatestAsyncQueue, type LatestAsyncQueue } from '../../renderer/latest-async-queue'; +import { usePersistentRenderHost } from '../../renderer/persistent-render-host-context'; +import { + benchmarkContentWidth, + BENCHMARK_CONTENT_INSET, + BENCHMARK_CONTENT_MINIMUM_VIEWPORT_WIDTH, +} from '../../workloads/shared/text-style'; +import type { BenchmarkWorkloadId } from '../../workloads/catalog'; +import type { BakeProgress } from '@pmndrs/text'; + +import { BakeProgressOverlay, useBakeProgress } from './bake-progress-overlay'; +import type { + LiveTextConfiguration, + PresentationEvidence, + RetainedLiveTextUpdate, +} from './live-text-viewport-contracts'; + +const GLYPH_POSITION_TRANSITION_MS = 110; + +function loadBitmapTextRenderer() { + return import('../../renderer/bitmap-text'); +} + +function bitmapViewportEvidence({ + anchor, + fontFixture, + grid, + layoutWidthRatio, + presentationEvidence, + settledRevision, + settledTextLength, + settledTimelineTick, + stats, + text, + textAlign, +}: { + readonly anchor: LiveTextConfiguration['anchor']; + readonly fontFixture: BenchmarkFontFixture; + readonly grid: boolean; + readonly layoutWidthRatio: number; + readonly presentationEvidence: PresentationEvidence; + readonly settledRevision: number; + readonly settledTextLength: number; + readonly settledTimelineTick: number | undefined; + readonly stats: BitmapTextLiveStats | undefined; + readonly text: string; + readonly textAlign: LiveTextConfiguration['textAlign']; +}): Record<`data-${string}`, string | number | boolean | undefined> { + return { + 'data-canvas-grid': String(grid), + 'data-anchor': anchor, + 'data-layout-width': stats?.layoutWidth, + 'data-layout-width-ratio': layoutWidthRatio, + 'data-content-inset': BENCHMARK_CONTENT_INSET, + 'data-content-min-width': BENCHMARK_CONTENT_MINIMUM_VIEWPORT_WIDTH * layoutWidthRatio, + 'data-content-policy': 'bounded-pan', + 'data-line-count': stats?.lineCount, + 'data-frame-count': stats?.frameCount, + 'data-frames-per-second': stats?.framesPerSecond, + 'data-font-fixture': fontFixture, + 'data-median-submit-ms': stats?.medianSubmitMs, + 'data-p95-submit-ms': stats?.p95SubmitMs, + 'data-gpu-frame-ms': stats?.gpuFrameMs, + 'data-median-gpu-ms': stats?.medianGpuMs, + 'data-p95-gpu-ms': stats?.p95GpuMs, + 'data-submit-history-length': stats?.submitHistoryLength, + 'data-fps-history-length': stats?.fpsHistoryLength, + 'data-glyph-count': stats?.glyphCount, + 'data-missing-glyph-count': stats?.missingGlyphCount, + 'data-draw-count': stats?.drawCount, + 'data-renderer-init-ms': stats?.rendererInitMs, + 'data-strike-ppem': stats?.strikePpem, + 'data-css-font-size': stats?.cssFontSize, + 'data-rendered-device-px': stats?.renderedPpem, + 'data-scale-ratio': stats?.scaleRatio, + 'data-font-load-ms': stats?.fontLoadMs, + 'data-text-ready-ms': stats?.textReadyMs, + 'data-first-draw-ms': stats?.firstDrawMs, + 'data-upload-frame-gpu-ms': stats?.uploadFrameGpuMs, + 'data-upload-frame-complete-ms': stats?.uploadFrameCompleteMs, + 'data-startup-ms': stats?.startupMs, + 'data-source-text-length': text.length, + 'data-text-align': textAlign, + 'data-artifact-bytes': stats?.artifactBytes, + 'data-atlas-gpu-bytes': stats?.atlasGpuBytes, + 'data-total-gpu-bytes': stats?.totalGpuBytes, + 'data-settled-revision': settledRevision, + 'data-settled-text-length': settledTextLength, + 'data-settled-tick': settledTimelineTick, + 'data-presentation-matched-glyphs': presentationEvidence.matchedGlyphs, + 'data-presentation-progress': presentationEvidence.progress, + 'data-presentation-revision': presentationEvidence.revision, + 'data-presentation-target-glyphs': presentationEvidence.targetGlyphs, + 'data-backend': stats?.backend, + 'data-dpr': stats?.dpr, + 'data-font-delivery': stats?.delivery, + 'data-core-bake-ms': stats?.coreBakeMs, + 'data-raster-bake-ms': stats?.rasterBakeMs, + 'data-source-font-bytes': stats?.sourceFontBytes, + 'data-core-artifact-bytes': stats?.coreArtifactBytes, + 'data-raster-artifact-bytes': stats?.rasterArtifactBytes, + 'data-gpu-history-length': stats?.gpuHistoryLength, + 'data-gpu-timing-supported': stats?.gpuTimingSupported, + }; +} + +export interface BitmapTextViewportProps { + readonly backend: GraphicsBackend; + readonly delivery: FontDelivery; + readonly dpr: 1 | 2; + readonly fontSize: number; + readonly grid: boolean; + readonly suppressLoading: boolean; + readonly stats: BitmapTextLiveStats | undefined; + readonly surfaceAnchorRef: RefObject; + readonly textConfiguration: LiveTextConfiguration; + readonly workload: BenchmarkWorkloadId; + readonly onStats: (stats: BitmapTextLiveStats) => void; +} + +export function BitmapTextViewport({ + backend, + delivery, + dpr, + fontSize, + grid, + suppressLoading, + stats, + surfaceAnchorRef, + textConfiguration, + workload, + onStats, +}: BitmapTextViewportProps) { + const { activateSurface } = usePersistentRenderHost(); + const activatePersistentSurface = useEffectEvent(activateSurface); + const containerRef = useRef(null); + const previewRef = useRef(undefined); + const updateQueueRef = useRef>(undefined); + const pendingSettledWorkloadRef = useRef(undefined); + const [settledRevision, setSettledRevision] = useState(0); + const [settledTextLength, setSettledTextLength] = useState(0); + const [settledTimelineTick, setSettledTimelineTick] = useState(); + const [settledWorkload, setSettledWorkload] = useState(); + const [presentationEvidence, setPresentationEvidence] = useState({ + revision: 0, + progress: 1, + matchedGlyphs: 0, + targetGlyphs: 0, + }); + const [error, setError] = useState(); + const { + active: bakeProgressActive, + finish: finishBakeProgress, + publish: publishBakeProgress, + value: bakeProgressValue, + } = useBakeProgress('bitmap'); + const { + anchor, + animatePresentation, + direction, + expectedGlyphCount, + features, + fontFixture, + language, + layoutWidthRatio, + text, + textAlign, + timelineTick, + } = textConfiguration; + const publishStats = useEffectEvent((next: BitmapTextLiveStats) => { + finishBakeProgress(); + onStats(next); + setError(undefined); + const pendingWorkload = pendingSettledWorkloadRef.current; + if (pendingWorkload !== undefined) { + pendingSettledWorkloadRef.current = undefined; + setSettledWorkload(pendingWorkload); + } + }); + const publishError = useEffectEvent((caught: unknown) => { + if (caught instanceof DOMException && caught.name === 'AbortError') return; + finishBakeProgress(); + setError(caught instanceof Error ? caught.message : String(caught)); + }); + const previewConfiguration = useEffectEvent(() => ({ + anchor, + direction, + expectedGlyphCount, + features, + fontFixture, + fontSize, + language, + layoutWidthRatio, + showGrid: grid, + text, + textAlign, + timelineTick, + workload, + })); + const publishSettledRevision = useEffectEvent((revision: number) => { + setSettledRevision(revision); + }); + const publishSettledTimelineTick = useEffectEvent((tick: number | undefined) => { + setSettledTimelineTick(tick); + }); + const publishSettledTextLength = useEffectEvent((length: number) => { + setSettledTextLength(length); + }); + const publishSettledWorkload = useEffectEvent((value: string) => { + pendingSettledWorkloadRef.current = value; + }); + const publishPresentation = useEffectEvent((snapshot: BitmapTextPreviewSnapshot, progress: 0 | 1) => { + setPresentationEvidence({ + revision: snapshot.revision, + progress, + matchedGlyphs: snapshot.matchedGlyphs, + targetGlyphs: snapshot.targetGlyphs, + }); + }); + + useEffect(() => { + const container = containerRef.current; + const surfaceAnchor = surfaceAnchorRef.current; + if (container === null || surfaceAnchor === null) return; + const controller = new AbortController(); + const configuration = previewConfiguration(); + let preview: BitmapTextPersistentScene | undefined; + let updateQueue: LatestAsyncQueue | undefined; + let surfaceLease: Awaited> | undefined; + let cancelled = false; + const initialization = (async () => { + const { createBitmapTextPersistentScene } = await loadBitmapTextRenderer(); + if (cancelled) return; + const created = createBitmapTextPersistentScene({ + anchor: configuration.anchor, + backend, + delivery, + ...(configuration.expectedGlyphCount === undefined + ? {} + : { expectedGlyphCount: configuration.expectedGlyphCount }), + fontFixture: configuration.fontFixture, + fontSize: configuration.fontSize, + showGrid: configuration.showGrid, + layoutWidth: benchmarkContentWidth(container.clientWidth, configuration.layoutWidthRatio), + layoutWidthRatio: configuration.layoutWidthRatio, + text: configuration.text, + language: configuration.language, + direction: configuration.direction, + features: configuration.features, + textAlign: configuration.textAlign, + onError: publishError, + onStats: publishStats, + onBakeProgress: publishBakeProgress, + }); + preview = created; + previewRef.current = created; + updateQueue = createLatestAsyncQueue((update: RetainedLiveTextUpdate) => created.update(update)); + updateQueueRef.current = updateQueue; + surfaceLease = await activatePersistentSurface( + { + anchor: surfaceAnchor, + controller: previewRef, + label: `Live bitmap benchmark using ${backend}`, + pan: true, + scene: created, + zoom: false, + }, + controller.signal, + ); + if (cancelled) { + await surfaceLease.release(); + return; + } + const committed = await updateQueue.enqueue(previewConfiguration()); + publishSettledTimelineTick(committed.input.timelineTick); + publishSettledTextLength(committed.input.text.length); + publishSettledWorkload(committed.input.workload); + })(); + void initialization.catch(publishError); + return () => { + cancelled = true; + controller.abort(); + void initialization.then( + async () => { + const current = preview; + preview = undefined; + if (previewRef.current === current) previewRef.current = undefined; + if (updateQueueRef.current === updateQueue) updateQueueRef.current = undefined; + await surfaceLease?.release(); + }, + () => { + const current = preview; + preview = undefined; + if (previewRef.current === current) previewRef.current = undefined; + if (updateQueueRef.current === updateQueue) updateQueueRef.current = undefined; + }, + ); + }; + }, [backend, delivery, publishBakeProgress, surfaceAnchorRef]); + + useEffect(() => { + previewRef.current?.setGridVisible(grid); + }, [grid]); + + useEffect(() => { + const preview = previewRef.current; + const updateQueue = updateQueueRef.current; + if (preview === undefined || updateQueue === undefined) return; + let cancelled = false; + let animationFrame: number | undefined; + const publishSettled = (snapshot: BitmapTextPreviewSnapshot, input: RetainedLiveTextUpdate): void => { + if (cancelled) return; + publishPresentation(snapshot, 1); + publishSettledRevision(snapshot.revision); + publishSettledTimelineTick(input.timelineTick); + publishSettledTextLength(input.text.length); + publishSettledWorkload(input.workload); + }; + void updateQueue + .enqueue({ + anchor, + fontFixture, + fontSize, + layoutWidthRatio, + text, + language, + direction, + expectedGlyphCount, + features, + textAlign, + timelineTick, + workload, + }) + .then(({ input, output: snapshot }) => { + if (cancelled) return; + publishPresentation(snapshot, 0); + if (!animatePresentation) { + publishSettled(preview.finishPresentation(snapshot.revision), input); + return; + } + const startedAt = performance.now(); + const animate = (timestamp: number): void => { + if (cancelled) return; + const linearProgress = Math.min(1, Math.max(0, (timestamp - startedAt) / GLYPH_POSITION_TRANSITION_MS)); + const easedProgress = linearProgress * linearProgress * (3 - 2 * linearProgress); + const presented = preview.setPresentationProgress(snapshot.revision, easedProgress); + if (linearProgress === 1) { + publishSettled(presented, input); + return; + } + animationFrame = requestAnimationFrame(animate); + }; + animationFrame = requestAnimationFrame(animate); + }) + .catch(publishError); + return () => { + cancelled = true; + if (animationFrame !== undefined) cancelAnimationFrame(animationFrame); + }; + }, [ + anchor, + animatePresentation, + direction, + dpr, + expectedGlyphCount, + features, + fontFixture, + fontSize, + language, + layoutWidthRatio, + text, + textAlign, + timelineTick, + workload, + ]); + + return ( +
+ +
+ ); +} + +function BitmapViewportChrome({ + backend, + bakeProgressActive, + bakeProgressValue, + delivery, + dpr, + error, + fontSize, + stats, + suppressLoading, +}: { + readonly backend: GraphicsBackend; + readonly bakeProgressActive: boolean; + readonly bakeProgressValue: BakeProgress | undefined; + readonly delivery: FontDelivery; + readonly dpr: 1 | 2; + readonly error: string | undefined; + readonly fontSize: number; + readonly stats: BitmapTextLiveStats | undefined; + readonly suppressLoading: boolean; +}) { + return ( + <> +
+ + {delivery === 'runtime' ? 'RUNTIME' : 'BAKED'} {stats?.strikePpem ?? 16} PPEM · {fontSize} CSS PX /{' '} + {stats?.renderedPpem ?? fontSize * dpr} DEVICE PX · {(stats?.scaleRatio ?? 1).toFixed(2)}× + + {dpr}× DPR +
+ {!suppressLoading && (stats === undefined || bakeProgressActive) && error === undefined && ( + + )} + {error !== undefined && ( +
+ {error} +
+ )} + + ); +} diff --git a/apps/benchmarks/src/surfaces/benchmark/live-text-viewport-contracts.ts b/apps/benchmarks/src/surfaces/benchmark/live-text-viewport-contracts.ts new file mode 100644 index 0000000..adc1a1c --- /dev/null +++ b/apps/benchmarks/src/surfaces/benchmark/live-text-viewport-contracts.ts @@ -0,0 +1,22 @@ +import type { BenchmarkFontFixture } from '../../benchmark/font-fixtures'; +import type { BitmapTextPreviewUpdate } from '../../renderer/bitmap-text'; +import type { BenchmarkWorkloadId } from '../../workloads/catalog'; + +export interface LiveTextConfiguration extends Omit { + readonly animatePresentation: boolean; + readonly fontFixture: BenchmarkFontFixture; + readonly expectedGlyphCount: number | undefined; + readonly timelineTick: number | undefined; +} + +export interface RetainedLiveTextUpdate extends BitmapTextPreviewUpdate { + readonly timelineTick: number | undefined; + readonly workload: BenchmarkWorkloadId; +} + +export interface PresentationEvidence { + readonly revision: number; + readonly progress: 0 | 1; + readonly matchedGlyphs: number; + readonly targetGlyphs: number; +} diff --git a/apps/benchmarks/src/surfaces/benchmark/sdf-text-viewports.tsx b/apps/benchmarks/src/surfaces/benchmark/sdf-text-viewports.tsx new file mode 100644 index 0000000..398c6e2 --- /dev/null +++ b/apps/benchmarks/src/surfaces/benchmark/sdf-text-viewports.tsx @@ -0,0 +1,648 @@ +import { useEffect, useEffectEvent, useRef, useState, type RefObject } from 'react'; + +import type { FontDelivery, GraphicsBackend } from '../../benchmark/url-state'; +import { createLatestAsyncQueue, type LatestAsyncQueue } from '../../renderer/latest-async-queue'; +import type { MtsdfTextLiveStats, MtsdfTextPersistentScene } from '../../renderer/mtsdf-text'; +import { usePersistentRenderHost } from '../../renderer/persistent-render-host-context'; +import type { SlugTextLiveStats, SlugTextPersistentScene } from '../../renderer/slug-text'; +import { + benchmarkContentWidth, + BENCHMARK_CONTENT_INSET, + BENCHMARK_CONTENT_MINIMUM_VIEWPORT_WIDTH, +} from '../../workloads/shared/text-style'; +import type { BenchmarkWorkloadId } from '../../workloads/catalog'; + +import { BakeProgressOverlay, useBakeProgress } from './bake-progress-overlay'; +import type { LiveTextConfiguration, RetainedLiveTextUpdate } from './live-text-viewport-contracts'; + +function loadMtsdfTextRenderer() { + return import('../../renderer/mtsdf-text'); +} + +function loadSlugTextRenderer() { + return import('../../renderer/slug-text'); +} + +interface SdfTextViewportProps { + readonly backend: GraphicsBackend; + readonly delivery: FontDelivery; + readonly dpr: 1 | 2; + readonly fontSize: number; + readonly grid: boolean; + readonly suppressLoading: boolean; + readonly stats: TStats | undefined; + readonly surfaceAnchorRef: RefObject; + readonly textConfiguration: LiveTextConfiguration; + readonly workload: BenchmarkWorkloadId; + readonly onStats: (stats: TStats) => void; +} + +export function MtsdfTextViewport(props: SdfTextViewportProps) { + const { + backend, + delivery, + dpr, + fontSize, + grid, + suppressLoading, + stats, + surfaceAnchorRef, + textConfiguration, + workload, + onStats, + } = props; + const { activateSurface } = usePersistentRenderHost(); + const activatePersistentSurface = useEffectEvent(activateSurface); + const containerRef = useRef(null); + const previewRef = useRef(undefined); + const updateQueueRef = useRef>(undefined); + const pendingSettledWorkloadRef = useRef(undefined); + const [error, setError] = useState(); + const [settledWorkload, setSettledWorkload] = useState(); + const { + active: bakeProgressActive, + finish: finishBakeProgress, + publish: publishBakeProgress, + value: bakeProgressValue, + } = useBakeProgress('MSDF'); + const { anchor, direction, features, fontFixture, language, layoutWidthRatio, text, textAlign, timelineTick } = + textConfiguration; + const publishStats = useEffectEvent((next: MtsdfTextLiveStats) => { + finishBakeProgress(); + onStats(next); + setError(undefined); + const pendingWorkload = pendingSettledWorkloadRef.current; + if (pendingWorkload !== undefined) { + pendingSettledWorkloadRef.current = undefined; + setSettledWorkload(pendingWorkload); + } + }); + const publishError = useEffectEvent((caught: unknown) => { + if (caught instanceof DOMException && caught.name === 'AbortError') return; + finishBakeProgress(); + setError(caught instanceof Error ? caught.message : String(caught)); + }); + const previewConfiguration = useEffectEvent(() => ({ + anchor, + direction, + features, + fontFixture, + fontSize, + language, + layoutWidthRatio, + showGrid: grid, + text, + textAlign, + timelineTick, + workload, + })); + const publishSettledWorkload = useEffectEvent((value: string) => { + pendingSettledWorkloadRef.current = value; + }); + useEffect(() => { + const container = containerRef.current; + const surfaceAnchor = surfaceAnchorRef.current; + if (container === null || surfaceAnchor === null) return; + const controller = new AbortController(); + const configuration = previewConfiguration(); + let preview: MtsdfTextPersistentScene | undefined; + let updateQueue: LatestAsyncQueue | undefined; + let surfaceLease: Awaited> | undefined; + let cancelled = false; + const initialization = (async () => { + const { createMtsdfTextPersistentScene } = await loadMtsdfTextRenderer(); + if (cancelled) return; + const created = createMtsdfTextPersistentScene({ + anchor: configuration.anchor, + backend, + delivery, + fontSize: configuration.fontSize, + fontFixture: configuration.fontFixture, + showGrid: configuration.showGrid, + layoutWidth: benchmarkContentWidth(container.clientWidth, configuration.layoutWidthRatio), + layoutWidthRatio: configuration.layoutWidthRatio, + text: configuration.text, + textAlign: configuration.textAlign, + language: configuration.language, + direction: configuration.direction, + features: configuration.features, + onError: publishError, + onStats: publishStats, + onBakeProgress: publishBakeProgress, + }); + preview = created; + previewRef.current = created; + updateQueue = createLatestAsyncQueue((update: RetainedLiveTextUpdate) => created.update(update)); + updateQueueRef.current = updateQueue; + surfaceLease = await activatePersistentSurface( + { + anchor: surfaceAnchor, + controller: previewRef, + label: `Live MSDF benchmark using ${backend}`, + pan: true, + scene: created, + zoom: false, + }, + controller.signal, + ); + if (cancelled) await surfaceLease.release(); + else { + const committed = await updateQueue.enqueue(previewConfiguration()); + publishSettledWorkload(committed.input.workload); + } + })(); + void initialization.catch(publishError); + return () => { + cancelled = true; + controller.abort(); + void initialization.then( + async () => { + const current = preview; + preview = undefined; + if (previewRef.current === current) previewRef.current = undefined; + if (updateQueueRef.current === updateQueue) updateQueueRef.current = undefined; + await surfaceLease?.release(); + }, + () => { + const current = preview; + preview = undefined; + if (previewRef.current === current) previewRef.current = undefined; + if (updateQueueRef.current === updateQueue) updateQueueRef.current = undefined; + }, + ); + }; + }, [backend, delivery, publishBakeProgress, surfaceAnchorRef]); + useEffect(() => { + previewRef.current?.setGridVisible(grid); + }, [grid]); + useEffect(() => { + const updateQueue = updateQueueRef.current; + if (updateQueue === undefined) return; + void updateQueue + .enqueue({ + anchor, + direction, + features, + fontFixture, + fontSize, + language, + layoutWidthRatio, + text, + textAlign, + timelineTick, + workload, + }) + .then(({ input }) => publishSettledWorkload(input.workload)) + .catch(publishError); + }, [ + anchor, + direction, + dpr, + features, + fontFixture, + fontSize, + language, + layoutWidthRatio, + text, + textAlign, + timelineTick, + workload, + ]); + return ( + + ); +} + +function MtsdfViewportChrome({ + anchor, + containerRef, + backend, + bakeProgressActive, + bakeProgressValue, + dpr, + error, + fontFixture, + fontSize, + grid, + layoutWidthRatio, + stats, + suppressLoading, + text, + textAlign, + timelineTick, + workload, + settledWorkload, +}: { + readonly anchor: LiveTextConfiguration['anchor']; + readonly containerRef: RefObject; + readonly backend: GraphicsBackend; + readonly bakeProgressActive: boolean; + readonly bakeProgressValue: ReturnType['value']; + readonly dpr: 1 | 2; + readonly error: string | undefined; + readonly fontFixture: string; + readonly fontSize: number; + readonly grid: boolean; + readonly layoutWidthRatio: number; + readonly stats: MtsdfTextLiveStats | undefined; + readonly suppressLoading: boolean; + readonly text: string; + readonly textAlign: LiveTextConfiguration['textAlign']; + readonly timelineTick: number | undefined; + readonly workload: BenchmarkWorkloadId; + readonly settledWorkload: string | undefined; +}) { + return ( +
+
+ + MTSDF {stats?.rasterEmSize ?? '—'} PX/EM · {fontSize} CSS PX / {stats?.renderedPpem ?? '—'} DEVICE PX ·{' '} + {stats?.scaleRatio.toFixed(2) ?? '—'}× + + {dpr}× DPR +
+ {!suppressLoading && (stats === undefined || bakeProgressActive) && error === undefined && ( + + )} + {error !== undefined && ( +
+ {error} +
+ )} +
+ ); +} + +export function SlugTextViewport(props: SdfTextViewportProps) { + const { + backend, + delivery, + dpr, + fontSize, + grid, + suppressLoading, + stats, + surfaceAnchorRef, + textConfiguration, + workload, + onStats, + } = props; + const { activateSurface } = usePersistentRenderHost(); + const activatePersistentSurface = useEffectEvent(activateSurface); + const containerRef = useRef(null); + const previewRef = useRef(undefined); + const updateQueueRef = useRef>(undefined); + const pendingSettledWorkloadRef = useRef(undefined); + const [error, setError] = useState(); + const [settledWorkload, setSettledWorkload] = useState(); + const { + active: bakeProgressActive, + finish: finishBakeProgress, + publish: publishBakeProgress, + value: bakeProgressValue, + } = useBakeProgress('Slug'); + const { anchor, direction, features, fontFixture, language, layoutWidthRatio, text, textAlign, timelineTick } = + textConfiguration; + const publishStats = useEffectEvent((next: SlugTextLiveStats) => { + finishBakeProgress(); + onStats(next); + setError(undefined); + const pendingWorkload = pendingSettledWorkloadRef.current; + if (pendingWorkload !== undefined) { + pendingSettledWorkloadRef.current = undefined; + setSettledWorkload(pendingWorkload); + } + }); + const publishError = useEffectEvent((caught: unknown) => { + if (caught instanceof DOMException && caught.name === 'AbortError') return; + finishBakeProgress(); + setError(caught instanceof Error ? caught.message : String(caught)); + }); + const previewConfiguration = useEffectEvent(() => ({ + anchor, + direction, + features, + fontFixture, + fontSize, + language, + layoutWidthRatio, + showGrid: grid, + text, + textAlign, + timelineTick, + workload, + })); + const publishSettledWorkload = useEffectEvent((value: string) => { + pendingSettledWorkloadRef.current = value; + }); + useEffect(() => { + const container = containerRef.current; + const surfaceAnchor = surfaceAnchorRef.current; + if (container === null || surfaceAnchor === null) return; + const controller = new AbortController(); + const configuration = previewConfiguration(); + let preview: SlugTextPersistentScene | undefined; + let updateQueue: LatestAsyncQueue | undefined; + let surfaceLease: Awaited> | undefined; + let cancelled = false; + const initialization = (async () => { + const { createSlugTextPersistentScene } = await loadSlugTextRenderer(); + if (cancelled) return; + const created = createSlugTextPersistentScene({ + anchor: configuration.anchor, + backend, + delivery, + fontSize: configuration.fontSize, + fontFixture: configuration.fontFixture, + showGrid: configuration.showGrid, + layoutWidthRatio: configuration.layoutWidthRatio, + text: configuration.text, + textAlign: configuration.textAlign, + language: configuration.language, + direction: configuration.direction, + features: configuration.features, + onError: publishError, + onStats: publishStats, + onBakeProgress: publishBakeProgress, + }); + preview = created; + previewRef.current = created; + updateQueue = createLatestAsyncQueue((update: RetainedLiveTextUpdate) => created.update(update)); + updateQueueRef.current = updateQueue; + surfaceLease = await activatePersistentSurface( + { + anchor: surfaceAnchor, + controller: previewRef, + label: `Live Slug benchmark using ${backend}`, + pan: true, + scene: created, + zoom: false, + }, + controller.signal, + ); + if (cancelled) await surfaceLease.release(); + else { + const committed = await updateQueue.enqueue(previewConfiguration()); + publishSettledWorkload(committed.input.workload); + } + })(); + void initialization.catch(publishError); + return () => { + cancelled = true; + controller.abort(); + void initialization.then( + async () => { + const current = preview; + preview = undefined; + if (previewRef.current === current) previewRef.current = undefined; + if (updateQueueRef.current === updateQueue) updateQueueRef.current = undefined; + await surfaceLease?.release(); + }, + () => { + const current = preview; + preview = undefined; + if (previewRef.current === current) previewRef.current = undefined; + if (updateQueueRef.current === updateQueue) updateQueueRef.current = undefined; + }, + ); + }; + }, [backend, delivery, publishBakeProgress, surfaceAnchorRef]); + useEffect(() => { + previewRef.current?.setGridVisible(grid); + }, [grid]); + useEffect(() => { + const updateQueue = updateQueueRef.current; + if (updateQueue === undefined) return; + void updateQueue + .enqueue({ + anchor, + direction, + features, + fontFixture, + fontSize, + language, + layoutWidthRatio, + text, + textAlign, + timelineTick, + workload, + }) + .then(({ input }) => publishSettledWorkload(input.workload)) + .catch(publishError); + }, [ + anchor, + direction, + dpr, + features, + fontFixture, + fontSize, + language, + layoutWidthRatio, + text, + textAlign, + timelineTick, + workload, + ]); + return ( + + ); +} + +function SlugViewportChrome({ + anchor, + containerRef, + backend, + bakeProgressActive, + bakeProgressValue, + dpr, + error, + fontFixture, + fontSize, + grid, + layoutWidthRatio, + stats, + suppressLoading, + text, + textAlign, + timelineTick, + workload, + settledWorkload, +}: { + readonly anchor: LiveTextConfiguration['anchor']; + readonly containerRef: RefObject; + readonly backend: GraphicsBackend; + readonly bakeProgressActive: boolean; + readonly bakeProgressValue: ReturnType['value']; + readonly dpr: 1 | 2; + readonly error: string | undefined; + readonly fontFixture: string; + readonly fontSize: number; + readonly grid: boolean; + readonly layoutWidthRatio: number; + readonly stats: SlugTextLiveStats | undefined; + readonly suppressLoading: boolean; + readonly text: string; + readonly textAlign: LiveTextConfiguration['textAlign']; + readonly timelineTick: number | undefined; + readonly workload: BenchmarkWorkloadId; + readonly settledWorkload: string | undefined; +}) { + return ( +
+
+ + SLUG ANALYTIC · {stats?.slugPageCount ?? '—'} PAGE{stats?.slugPageCount === 1 ? '' : 'S'} · {fontSize} CSS PX + / {stats?.renderedPpem ?? '—'} DEVICE PX + + {dpr}× DPR +
+ {!suppressLoading && (stats === undefined || bakeProgressActive) && error === undefined && ( + + )} + {error !== undefined && ( +
+ {error} +
+ )} +
+ ); +} diff --git a/apps/benchmarks/src/surfaces/benchmark/viewport-boundary.test.ts b/apps/benchmarks/src/surfaces/benchmark/viewport-boundary.test.ts new file mode 100644 index 0000000..1211c5e --- /dev/null +++ b/apps/benchmarks/src/surfaces/benchmark/viewport-boundary.test.ts @@ -0,0 +1,21 @@ +import { readFile } from 'node:fs/promises'; + +import { describe, expect, it } from 'vitest'; + +describe('live-text viewport renderer boundaries', () => { + it.each([ + ['bitmap-text-viewport.tsx', '../../renderer/bitmap-text'], + ['sdf-text-viewports.tsx', '../../renderer/mtsdf-text'], + ['sdf-text-viewports.tsx', '../../renderer/slug-text'], + ] as const)('keeps %s references to %s lazy or type-only', async (file, rendererModule) => { + const source = await readFile(new URL(file, import.meta.url), 'utf8'); + const staticImports = source + .split(';') + .map((statement) => statement.trim()) + .filter((statement) => statement.startsWith('import ') && statement.includes(`'${rendererModule}'`)); + + expect(staticImports).not.toHaveLength(0); + expect(staticImports.every((statement) => statement.startsWith('import type '))).toBe(true); + expect(source).toContain(`import('${rendererModule}')`); + }); +}); From 85c42c5c8e132138e0e8803bcb2c4666fb51dfbe Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Mon, 3 Aug 2026 05:52:09 -0400 Subject: [PATCH 2/2] docs(benchmarks): record viewport hierarchy evidence --- docs/log.md | 1 + docs/packages/benchmarks.md | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/log.md b/docs/log.md index 7ea0cb1..30aa308 100644 --- a/docs/log.md +++ b/docs/log.md @@ -2,6 +2,7 @@ ## 2026-08-03 +- **Persistent viewport hierarchy** — Moved the Bitmap, MTSDF, and Slug live-text viewport controllers, their warm update queues, loading chrome, telemetry attributes, and shared contracts from the route coordinator into `surfaces/benchmark`. Review caught mixed type imports collapsing the lazy renderer boundaries; explicit `import type` declarations restored separate Bitmap, MTSDF, and Slug production chunks. The complete deterministic benchmark check and React Doctor remained clean, all 42 retained-scene cells passed with visible pixels and one renderer, and both 60-second timed demos traversed Advanced Shaping and returned to Off-axis / 3D with one renderer at 60.02 WebGPU and 60.36 forced-WebGL2 Icon Grid FPS. - **Workload instance and low-level target isolation** — Moved Icon Grid's active virtual window, recycle epochs, pan/autopan smoothing, refresh suspension, visibility, and metrics into one per-mount workload instance while leaving renderer, RAF, font transactions, scene attachment, and telemetry in the persistent host. Extracted Main/Presentation composition, runtime control binding, benchmark surface chrome, and bake progress into named React modules. Added a common target-owned MTSDF/Slug conformance session that forwards the borrowed renderer and abort signal while retaining renderer-private resources behind adapters. The first browser pass exposed Text Ladder's offscreen scene transform leaking into Zoom Text; explicit per-workload scene initialization corrected the black frame. The repeated 42-cell dual-backend Presentation matrix then completed with visible pixels and one renderer per lane, all 19 isolated headless conformance scenarios passed, React Doctor reported zero diagnostics, and the complete deterministic benchmark gate passed 301 tests. - **Complete workload phase ownership** — Added typed animation and retained-configuration hooks to all seven retained workload definitions using preallocated host scratch, eliminating the renderer's remaining create/layout/animate/apply dispatch switches without moving RAF, telemetry, or renderer lifecycle into workload code. Moved the Benchmark Ipsum corpus and Advanced Shaping timeline beside the other authored workloads, and moved the self-contained Advanced Shaping conformance target under `benchmark/targets/conformance` behind the same literal selected-target dynamic import. Coupled raster targets remain in place pending a session adapter that preserves their warm load/run/dispose lifecycle. The post-move Chromium run completed all 42 dual-backend Presentation workload cells with visible pixels and one renderer per lane; all 19 isolated headless conformance scenarios passed, including three exact 68-frame Advanced Shaping timelines with zero warm readiness waits. - **Workload controller boundary** — Made all seven retained workload definitions own typed app-private construction and layout hooks, so the renderer selects those behaviors only through the exhaustive registry. Moved Icon Grid layout, virtual-window calculation, assignment validation, recycled-entry positioning, pan integration, and frame-delta smoothing beside its canonical public `Text` example. Main and Presentation now derive descriptions, amount labels, fixed/selectable font behavior, preload grouping, and pan/zoom capability from the typed catalog instead of duplicating workload-ID policy. Renderer, canvas, RAF, GPU timer, telemetry, font transactions, active pool ownership, and teardown remain host infrastructure; no public package API was added. Chromium 149 completed all 42 Bitmap/MTSDF/Slug × WebGPU/WebGL2 × seven-workload Presentation cells with visible pixels, one renderer per lane, and no reported console failure. diff --git a/docs/packages/benchmarks.md b/docs/packages/benchmarks.md index 8abd3a6..09375fd 100644 --- a/docs/packages/benchmarks.md +++ b/docs/packages/benchmarks.md @@ -5,7 +5,7 @@ description: Provides the shared interactive and automated benchmark product sur resource: ../../apps/benchmarks workspace_package: '@pmndrs/text-benchmarks' documentation_type: reference -source_digest: 'sha256:b8bff40a525909b92d99699e6f2e4c5b257ede6bf15fa36570d43846e9c560fb' +source_digest: 'sha256:194ab5d165f6c30cd8142d8cec7f3aa1a9697b2c7ace5b1611aff5367cb9a8a9' tags: [package, benchmarks, react, vite, product-e2e] sources: - id: manifest @@ -62,6 +62,12 @@ sources: - id: harness-layout resource: ../../apps/benchmarks/src/components/harness-layout.tsx title: Main and Presentation route composition + - id: bitmap-live-viewport + resource: ../../apps/benchmarks/src/surfaces/benchmark/bitmap-text-viewport.tsx + title: Bitmap persistent live-text viewport controller + - id: sdf-live-viewports + resource: ../../apps/benchmarks/src/surfaces/benchmark/sdf-text-viewports.tsx + title: MTSDF and Slug persistent live-text viewport controllers - id: raster-conformance-session resource: ../../apps/benchmarks/src/benchmark/targets/conformance/raster/contracts.ts title: Low-level raster conformance session contract @@ -82,7 +88,7 @@ sources: title: Realtime comparison product probe generated: by: openai-codex/gpt-5.6 - at: '2026-08-03T09:23:50Z' + at: '2026-08-03T09:51:35Z' --- # Package reference: `@pmndrs/text-benchmarks` @@ -120,7 +126,7 @@ Main and Presentation are exclusive URL-selected root presentations. Presentatio Every live benchmark identity resolves through one typed catalog under `apps/benchmarks/src/workloads/`. The catalog owns labels, descriptions, exact Main and Presentation defaults, font policy, controls and ranges, pan/zoom capability, preload policy, and surface kind; URL parsing normalizes an unknown workload inside its selected mode before font or control policy executes. Main and Presentation derive scene descriptions, amount labels, font selection, preload grouping, and pan/zoom capability from that authority rather than repeating workload-ID switches. Benchmark Ipsum and Advanced Shaping keep their authored corpus and timeline in the same workload hierarchy as Text Ladder, Zoom Text, Icon Grid, Off-axis / 3D, Dynamic Layout, Paragraph Stress, and Paint & Effects. The seven retained comparison definitions own construction, layout, animation, and retained configuration hooks; no workload-specific dispatch switch remains for those phases. Icon Grid additionally owns one per-mount instance containing virtual-window epochs, pool assignment and recycling, scroll and auto-pan state, frame smoothing, refresh suspension, visibility, and metrics. The host exposes only generic cold pool resize/readiness, scene attachment, and disposal; renderer, canvas, RAF, GPU timer, font transactions, and telemetry history remain route infrastructure. Each workload mount explicitly initializes the shared scene transform, preventing Text Ladder's authored offscreen exit or Icon Grid pan from polluting the next workload. Their technique-invariant content-width, text-style, and color-cycle utilities live below `workloads/shared`; a source-boundary test rejects static or dynamic imports from any workload module back into renderer implementation files. -Main and Presentation composition now lives in `components/harness-layout.tsx`, runtime control binding in `components/runtime-controls.tsx`, and benchmark surface/bake status in `surfaces/benchmark`; the root application remains the route coordinator rather than the sole component hierarchy. Conformance React composition lives under `surfaces/conformance`: both the retained comparison and finite captures can receive only the host-owned renderer, while finite low-level dispatch stays below `benchmark/targets/conformance` and `benchmark/targets/measurement`. Advanced Shaping lives in that target hierarchy behind the registry's literal selected-target dynamic import. MTSDF and Slug sampling plus source-outline targets share a target-owned raster conformance session that preserves warm `load → run → dispose` reuse and forwards the borrowed renderer and abort signal unchanged; renderer-private capture resources remain inside their adapters. The targets share the explicitly named `targets/shared/direct-wasm.ts` dependency adapter only after target selection. The public missing-sibling loader Worker is conformance because it proves authenticated Worker bytes and loader fallback behavior; it is not a rendering product target. Boundary tests reject workload imports back into renderer implementation, isolate renderer imports to the raster adapters, authenticate the literal Advanced Shaping target import, and reject direct font-baker or Wasm URL imports outside the shared adapter, preventing raw tooling from leaking into the normal Presentation module graph. +Main and Presentation composition lives in `components/harness-layout.tsx`, runtime control binding in `components/runtime-controls.tsx`, and benchmark surface/bake status in `surfaces/benchmark`; the root application remains the route coordinator rather than the sole component hierarchy. The three persistent Bitmap, MTSDF, and Slug live-text viewport controllers also live under `surfaces/benchmark`, keeping their host lease, warm update queue, loading state, telemetry, and probe contract beside the rendered surface. Their renderer imports remain literal dynamic boundaries: type-only references use `import type`, so the production build retains separate technique chunks rather than pulling renderer implementations into the route entry. Conformance React composition lives under `surfaces/conformance`: both the retained comparison and finite captures can receive only the host-owned renderer, while finite low-level dispatch stays below `benchmark/targets/conformance` and `benchmark/targets/measurement`. Advanced Shaping lives in that target hierarchy behind the registry's literal selected-target dynamic import. MTSDF and Slug sampling plus source-outline targets share a target-owned raster conformance session that preserves warm `load → run → dispose` reuse and forwards the borrowed renderer and abort signal unchanged; renderer-private capture resources remain inside their adapters. The targets share the explicitly named `targets/shared/direct-wasm.ts` dependency adapter only after target selection. The public missing-sibling loader Worker is conformance because it proves authenticated Worker bytes and loader fallback behavior; it is not a rendering product target. Boundary tests reject workload imports back into renderer implementation, isolate renderer imports to the raster adapters, authenticate the literal Advanced Shaping target import, and reject direct font-baker or Wasm URL imports outside the shared adapter, preventing raw tooling from leaking into the normal Presentation module graph. Timed playback compares each frame with the latest requested location rather than the last committed scene, so an in-flight preload receives exactly one request and cannot be superseded by a duplicate transition that skips workload-default initialization. Presentation captures Space at the window capture boundary to start or stop timed playback even while a button, switch, slider, select, or combobox owns focus; matching key-up activation is suppressed, while inputs, textareas, and editable text retain ordinary space entry. Arrow navigation remains disabled on interactive controls.