diff --git a/apps/benchmarks/doctor.config.json b/apps/benchmarks/doctor.config.json index 2cf2541a..b4fd7e47 100644 --- a/apps/benchmarks/doctor.config.json +++ b/apps/benchmarks/doctor.config.json @@ -4,7 +4,12 @@ "enabled": false }, "ignore": { - "files": ["vitexec/**", "src/benchmark/bake-host-baseline.ts", "src/benchmark/worker-queue-evidence.ts"], + "files": [ + "vitexec/**", + "src/benchmark/bake-host-baseline.ts", + "src/benchmark/worker-queue-evidence.ts", + "src/benchmark/targets/measurement/comparison-preview.ts" + ], "overrides": [ { "files": ["src/benchmark/paragraph-layout-digest.ts"], diff --git a/apps/benchmarks/src/app.tsx b/apps/benchmarks/src/app.tsx index ee7c6305..8152bff9 100644 --- a/apps/benchmarks/src/app.tsx +++ b/apps/benchmarks/src/app.tsx @@ -82,7 +82,7 @@ import type { ComparisonWorkloadId, ComparisonWorkloadPersistentScene, ComparisonWorkloadStats, -} from './renderer/comparison-workload'; +} from './workloads/comparison/scene'; import { benchmarkWorkloadDefinition, comparisonWorkloadId, @@ -108,7 +108,7 @@ let comparisonWorkloadModule: ReturnType | unde const liveSceneAssetResources = new Map>(); function importComparisonWorkload() { - return import('./renderer/comparison-workload'); + return import('./workloads/comparison/scene'); } function loadBenchmarkFontAssets() { diff --git a/apps/benchmarks/src/benchmark/targets/measurement/comparison-preview.ts b/apps/benchmarks/src/benchmark/targets/measurement/comparison-preview.ts new file mode 100644 index 00000000..80fa7dcc --- /dev/null +++ b/apps/benchmarks/src/benchmark/targets/measurement/comparison-preview.ts @@ -0,0 +1,80 @@ +import { + createComparisonWorkloadPersistentScene, + type ComparisonWorkloadConfiguration, + type ComparisonWorkloadId, + type ComparisonWorkloadPersistentSceneOptions, + type ComparisonWorkloadStats, +} from '../../../workloads/comparison/scene'; +import { createPersistentRenderHost } from '../../../renderer/persistent-render-host'; + +/** + * Measurement-only adapter for probes that need an isolated canvas. The retained + * workload scene remains renderer-agnostic; this adapter owns the host lifecycle. + */ +export interface ComparisonWorkloadPreview { + dispose(): Promise; + panBy(deltaX: number, deltaY: number): { readonly deltaX: number; readonly deltaY: number } | void; + resetView(): void; + resize(width: number, height: number): void; + update(configuration: ComparisonWorkloadConfiguration): Promise; + zoomBy(factor: number): void; +} + +export interface ComparisonWorkloadPreviewOptions extends ComparisonWorkloadPersistentSceneOptions { + readonly canvas: HTMLCanvasElement; + readonly dpr: number; + readonly height: number; + readonly signal?: AbortSignal; + readonly width: number; +} + +export type { ComparisonWorkloadConfiguration, ComparisonWorkloadId, ComparisonWorkloadStats }; + +export async function createComparisonWorkloadPreview( + options: ComparisonWorkloadPreviewOptions, +): Promise { + const { canvas, dpr, height, signal, width, ...sceneOptions } = options; + const host = await createPersistentRenderHost({ + backend: options.backend, + canvas, + dpr, + height, + onError: options.onError, + width, + }); + const scene = createComparisonWorkloadPersistentScene(sceneOptions); + try { + const lease = await host.replaceScene(scene, signal); + let disposal: Promise | undefined; + return { + panBy(deltaX, deltaY) { + return scene.panBy(deltaX, deltaY); + }, + resetView() { + scene.resetView(); + }, + resize(nextWidth, nextHeight) { + host.resize(nextWidth, nextHeight); + }, + update(configuration) { + return scene.update(configuration); + }, + zoomBy(factor) { + scene.zoomBy(factor); + }, + dispose() { + disposal ??= (async () => { + try { + await lease.release(); + } finally { + await host.dispose(); + } + })(); + return disposal; + }, + }; + } catch (error) { + await host.dispose(); + throw error; + } +} diff --git a/apps/benchmarks/src/components/workload-rail.tsx b/apps/benchmarks/src/components/workload-rail.tsx index 8c96f6fd..c9946bad 100644 --- a/apps/benchmarks/src/components/workload-rail.tsx +++ b/apps/benchmarks/src/components/workload-rail.tsx @@ -24,7 +24,7 @@ import { TechniqueSwitcher } from './technique-switcher'; let comparisonWorkloadModule: ReturnType | undefined; function importComparisonWorkload() { - return import('../renderer/comparison-workload'); + return import('../workloads/comparison/scene'); } function preloadComparisonWorkload(): ReturnType { diff --git a/apps/benchmarks/src/renderer/comparison-workload.test.ts b/apps/benchmarks/src/workloads/comparison/scene.test.ts similarity index 99% rename from apps/benchmarks/src/renderer/comparison-workload.test.ts rename to apps/benchmarks/src/workloads/comparison/scene.test.ts index 6e9a6c36..e667927c 100644 --- a/apps/benchmarks/src/renderer/comparison-workload.test.ts +++ b/apps/benchmarks/src/workloads/comparison/scene.test.ts @@ -32,7 +32,7 @@ import { zoomTextMaximumScale, type ComparisonWorkloadConfiguration, type IconGridAutoPanState, -} from './comparison-workload'; +} from './scene'; const baseConfiguration: ComparisonWorkloadConfiguration = { amount: 50, diff --git a/apps/benchmarks/src/renderer/comparison-workload.ts b/apps/benchmarks/src/workloads/comparison/scene.ts similarity index 87% rename from apps/benchmarks/src/renderer/comparison-workload.ts rename to apps/benchmarks/src/workloads/comparison/scene.ts index 4a099acb..00f91239 100644 --- a/apps/benchmarks/src/renderer/comparison-workload.ts +++ b/apps/benchmarks/src/workloads/comparison/scene.ts @@ -2,15 +2,16 @@ import { FontRegistry, type AnyRasterInput, type ParagraphLayout, type Registere import * as THREE from 'three/webgpu'; import { selectBitmapStrikePpem } from '@pmndrs/text/raster/bitmap'; -import type { BenchmarkFontFixture, RasterConformanceSpecimen } from '../benchmark/font-fixtures'; -import { ICON_GRID_FONT_FIXTURE } from '../benchmark/font-fixtures'; -import type { FontDelivery, RasterTechnique } from '../benchmark/url-state'; +import type { BenchmarkFontFixture, RasterConformanceSpecimen } from '../../benchmark/font-fixtures'; +import { ICON_GRID_FONT_FIXTURE } from '../../benchmark/font-fixtures'; +import type { RuntimeLiveStats } from '../../benchmark/runtime-world'; +import type { FontDelivery, RasterTechnique } from '../../benchmark/url-state'; import { comparisonWorkloadDefinition, comparisonWorkloadRequiresIconWindowSuspension as registryRequiresIconWindowSuspension, comparisonWorkloadUpdateKind as registryUpdateKind, -} from '../workloads/registry'; -import { DYNAMIC_LAYOUT_TEXT, dynamicLayoutWidths } from '../workloads/dynamic-layout'; +} from '../registry'; +import { DYNAMIC_LAYOUT_TEXT, dynamicLayoutWidths } from '../dynamic-layout'; import { ICON_GRID_ITEMS, ICON_GRID_LABEL_SIZE, @@ -21,57 +22,47 @@ import { resizeIconGridEntries, type IconGridEntryPool, type IconGridWorkloadInstance, -} from '../workloads/icon-grid'; -import type { MutableTextLadderScenePosition } from '../workloads/text-ladder'; -import { ZOOM_TEXT_BASE_CSS_PX } from '../workloads/zoom-text'; -import type { ZoomTextAnimationState } from '../workloads/zoom-text'; +} from '../icon-grid'; +import type { MutableTextLadderScenePosition } from '../text-ladder'; +import { ZOOM_TEXT_BASE_CSS_PX } from '../zoom-text'; +import type { ZoomTextAnimationState } from '../zoom-text'; import type { ComparisonWorkloadAnimationScratch, ComparisonWorkloadConfiguration, ComparisonWorkloadId, -} from '../workloads/contracts'; -import { committedTextLayout, type ComparisonWorkloadEntry } from '../workloads/factory-contracts'; -import { registeredBitmapAtlas } from '../benchmark/low-level/raster/bitmap-atlas'; +} from '../contracts'; +import { committedTextLayout, type ComparisonWorkloadEntry } from '../factory-contracts'; +import { registeredBitmapAtlas, type BitmapAtlasPageStats } from '../../benchmark/low-level/raster/bitmap-atlas'; import { registeredMtsdfConfiguration, type MtsdfRasterConfiguration, -} from '../benchmark/low-level/raster/mtsdf-configuration'; +} from '../../benchmark/low-level/raster/mtsdf-configuration'; import { registeredSlugConfiguration, type SlugRasterConfiguration, -} from '../benchmark/low-level/raster/slug-configuration'; -import type { BitmapTextLiveStats } from './bitmap-text'; -import { createCanvasSurface } from './canvas-surface'; -import { createGpuFrameTimer, type GpuFrameTimer } from './gpu-frame-timer'; -import { createLiveFrameTelemetry, type LiveFrameTelemetrySnapshot } from './live-frame-telemetry'; -import { createTextUpdateTelemetry } from './text-update-telemetry'; -import { - loadBenchmarkFontAsset, - type BakedSlugArtifactSource, - type FontDeliveryMetrics, -} from '../workloads/font-assets'; -import { benchmarkContentWidth } from '../workloads/shared/text-style'; -import type { MtsdfTextLiveStats } from './mtsdf-text'; -import type { SlugTextLiveStats } from './slug-text'; -import { - createConfiguredRenderer, - disposeConfiguredRenderer, - readRendererViewportState, - type RendererBackend, -} from './webgpu-renderer'; +} from '../../benchmark/low-level/raster/slug-configuration'; +import { createCanvasSurface } from '../../renderer/canvas-surface'; +import type { LiveFrameTelemetrySnapshot } from '../../renderer/live-frame-telemetry'; +import { createTextUpdateTelemetry } from '../../renderer/text-update-telemetry'; +import { loadBenchmarkFontAsset, type BakedSlugArtifactSource, type FontDeliveryMetrics } from '../font-assets'; +import { benchmarkContentWidth } from '../shared/text-style'; +import type { RendererBackend } from '../../renderer/webgpu-renderer'; import type { PersistentRenderFrameContext, PersistentRenderScene, PersistentRenderSceneContext, PersistentRenderViewport, -} from './persistent-render-host'; -import { createPersistentSceneActivation } from './persistent-scene-activation'; -import { createRetainedFontFixtureController, type RetainedFontFixtureController } from './retained-font-fixture'; +} from '../../renderer/persistent-render-host'; +import { createPersistentSceneActivation } from '../../renderer/persistent-scene-activation'; +import { + createRetainedFontFixtureController, + type RetainedFontFixtureController, +} from '../../renderer/retained-font-fixture'; type WorkloadEntry = ComparisonWorkloadEntry; -export type { ComparisonWorkloadConfiguration, ComparisonWorkloadId, IconGridView } from '../workloads/contracts'; -export type { MutableTextLadderScenePosition } from '../workloads/text-ladder'; +export type { ComparisonWorkloadConfiguration, ComparisonWorkloadId, IconGridView } from '../contracts'; +export type { MutableTextLadderScenePosition } from '../text-ladder'; export { advanceIconGridAutoPan, iconGridAssignmentSignature, @@ -81,8 +72,8 @@ export { iconGridVirtualWindow, iconGridViewportUpdateKind, smoothIconGridFrameDelta, -} from '../workloads/icon-grid'; -export type { IconGridAutoPanState } from '../workloads/icon-grid'; +} from '../icon-grid'; +export type { IconGridAutoPanState } from '../icon-grid'; export { dynamicLayoutWidths, ladderCssSizes, @@ -97,9 +88,9 @@ export { ZOOM_TEXT_PHRASES, zoomTextAnimationState, zoomTextMaximumScale, -} from '../workloads/index'; +} from '../index'; -export type ComparisonWorkloadStats = (BitmapTextLiveStats | MtsdfTextLiveStats | SlugTextLiveStats) & { +export type ComparisonWorkloadStats = RuntimeLiveStats & { readonly configurationRevision: number; readonly appliedFontFixture: BenchmarkFontFixture; readonly cameraKind: 'orthographic' | 'perspective'; @@ -150,26 +141,14 @@ export type ComparisonWorkloadStats = (BitmapTextLiveStats | MtsdfTextLiveStats readonly zoomMaximumScale: number; }; -export interface ComparisonWorkloadPreview { - resize(width: number, height: number): void; - panBy(deltaX: number, deltaY: number): { readonly deltaX: number; readonly deltaY: number } | void; - resetView(): void; - zoomBy(factor: number): void; - update(configuration: ComparisonWorkloadConfiguration): Promise; - dispose(): Promise; -} - -export interface ComparisonWorkloadPreviewOptions { +export interface ComparisonWorkloadPersistentSceneOptions { readonly amount: number; readonly animationEnabled: boolean; readonly animationSpeed: number; readonly backend: RendererBackend; - readonly canvas: HTMLCanvasElement; - readonly dpr: number; readonly fontSize: number; readonly fontFixture: BenchmarkFontFixture; readonly delivery: FontDelivery; - readonly height: number; readonly layoutWidthRatio: number; readonly paintOpacity: number; readonly paintShadowEnabled: boolean; @@ -177,22 +156,16 @@ export interface ComparisonWorkloadPreviewOptions { readonly showGrid: boolean; readonly showLayoutBounds: boolean; readonly textLadderExitEnabled: boolean; - readonly signal?: AbortSignal; readonly slugBakedArtifact?: BakedSlugArtifactSource; readonly technique: RasterTechnique; readonly textLadderSpecimen?: RasterConformanceSpecimen; - readonly width: number; readonly workload: ComparisonWorkloadId; readonly onError: (error: unknown) => void; readonly onStats: (stats: ComparisonWorkloadStats) => void; readonly onBakeProgress?: import('@pmndrs/text').BakeProgressListener; + readonly id?: string; } -export type ComparisonWorkloadPersistentSceneOptions = Omit< - ComparisonWorkloadPreviewOptions, - 'canvas' | 'dpr' | 'height' | 'signal' | 'width' -> & { readonly id?: string }; - export interface ComparisonWorkloadPersistentScene extends PersistentRenderScene { panBy(deltaX: number, deltaY: number): { readonly deltaX: number; readonly deltaY: number } | void; resetView(): void; @@ -232,7 +205,7 @@ interface MutableLoadedFontMetrics { interface LoadedTechniqueFont { readonly artifactBytes: number; readonly atlasGpuBytes: number; - readonly atlasPages: BitmapTextLiveStats['atlasPages']; + readonly atlasPages: readonly BitmapAtlasPageStats[]; readonly bitmapStrikes: readonly { readonly ppem: number }[]; readonly font: RegisteredFont; readonly fontLoadMs: number; @@ -251,15 +224,17 @@ interface PendingConfigurationUpdate { }>; } -interface ComparisonWorkloadRuntime extends ComparisonWorkloadPreview { - persistentFrame(context: PersistentRenderFrameContext): void; - persistentTelemetry(snapshot: LiveFrameTelemetrySnapshot, viewport: PersistentRenderViewport): void; +interface ComparisonWorkloadRuntime { + dispose(): Promise; + frame(context: PersistentRenderFrameContext): void; + panBy(deltaX: number, deltaY: number): { readonly deltaX: number; readonly deltaY: number } | void; + resetView(): void; + resize(width: number, height: number): void; + telemetry(snapshot: LiveFrameTelemetrySnapshot, viewport: PersistentRenderViewport): void; + update(configuration: ComparisonWorkloadConfiguration): Promise; + zoomBy(factor: number): void; } -type ComparisonWorkloadRuntimeOptions = Omit & { - readonly canvas?: HTMLCanvasElement; -}; - export function createComparisonWorkloadPersistentScene( options: ComparisonWorkloadPersistentSceneOptions, ): ComparisonWorkloadPersistentScene { @@ -280,16 +255,7 @@ export function createComparisonWorkloadPersistentScene( throw new DOMException('The comparison workload scene cannot be activated twice', 'InvalidStateError'); activated = true; try { - runtime = await createComparisonWorkloadRuntime( - { - ...options, - dpr: context.viewport.dpr, - height: context.viewport.height, - signal: context.signal, - width: context.viewport.width, - }, - context, - ); + runtime = await createComparisonWorkloadRuntime(options, context); activation.resolve(runtime); } catch (error) { activation.reject(error); @@ -297,10 +263,10 @@ export function createComparisonWorkloadPersistentScene( } }, frame(context) { - active().persistentFrame(context); + active().frame(context); }, telemetry(snapshot, viewport) { - active().persistentTelemetry(snapshot, viewport); + active().telemetry(snapshot, viewport); }, resize(viewport) { active().resize(viewport.width, viewport.height); @@ -327,20 +293,13 @@ export function createComparisonWorkloadPersistentScene( }; } -export async function createComparisonWorkloadPreview( - options: ComparisonWorkloadPreviewOptions, -): Promise { - return createComparisonWorkloadRuntime(options); -} - async function createComparisonWorkloadRuntime( - options: ComparisonWorkloadRuntimeOptions, - persistentContext?: PersistentRenderSceneContext, + options: ComparisonWorkloadPersistentSceneOptions, + persistentContext: PersistentRenderSceneContext, ): Promise { - const persistent = persistentContext !== undefined; const { backend, onError, onStats, technique } = options; - const signal = persistentContext?.signal ?? options.signal; - const dpr = persistentContext?.viewport.dpr ?? options.dpr; + const { signal } = persistentContext; + const { height: viewportHeight, width: viewportWidth } = persistentContext.viewport; signal?.throwIfAborted(); if (options.slugBakedArtifact !== undefined && technique !== 'slug') { throw new TypeError('a Slug candidate artifact requires the Slug technique'); @@ -348,36 +307,18 @@ async function createComparisonWorkloadRuntime( if (options.slugBakedArtifact !== undefined && options.delivery !== 'baked') { throw new TypeError('a retained Slug candidate artifact requires baked delivery'); } - let width = positive(persistentContext?.viewport.width ?? options.width, 'comparison workload width'); - let height = positive(persistentContext?.viewport.height ?? options.height, 'comparison workload height'); + let width = positive(viewportWidth, 'comparison workload width'); + let height = positive(viewportHeight, 'comparison workload height'); let configuration = validateConfiguration(options); const startupStarted = performance.now(); - const rendererStarted = performance.now(); - if (!persistent && options.canvas === undefined) { - throw new TypeError('a standalone comparison workload requires a canvas'); - } - const renderer = - persistentContext === undefined - ? await createConfiguredRenderer({ - backend, - canvas: options.canvas!, - dpr, - height, - trackGpuTimestamps: backend === 'webgpu', - width, - }) - : (persistentContext.renderer as THREE.WebGPURenderer); - let rendererViewport = - persistentContext === undefined - ? readRendererViewportState(renderer) - : { - drawingBufferHeight: persistentContext.viewport.drawingBufferHeight, - drawingBufferWidth: persistentContext.viewport.drawingBufferWidth, - pixelRatio: persistentContext.viewport.dpr, - }; + const renderer = persistentContext.renderer as THREE.WebGPURenderer; + let rendererViewport = { + drawingBufferHeight: persistentContext.viewport.drawingBufferHeight, + drawingBufferWidth: persistentContext.viewport.drawingBufferWidth, + pixelRatio: persistentContext.viewport.dpr, + }; const canvasSurface = createCanvasSurface(renderer, width, height, configuration.showGrid); - let gpuFrameTimer: GpuFrameTimer | undefined; - const rendererInitMs = persistentContext?.rendererInitMs ?? performance.now() - rendererStarted; + const rendererInitMs = persistentContext.rendererInitMs; let font: LoadedTechniqueFont | undefined; let iconFont: LoadedTechniqueFont | undefined; let selectedFontController: RetainedFontFixtureController | undefined; @@ -403,10 +344,6 @@ async function createComparisonWorkloadRuntime( }; const scene = new THREE.Scene(); let camera = createWorkloadCamera(configuration.workload, width, height); - gpuFrameTimer = persistent ? undefined : createGpuFrameTimer({ backend, renderer, onError }); - const telemetry = persistent - ? undefined - : createLiveFrameTelemetry({ gpuTimingSupported: gpuFrameTimer?.supported ?? false }); const textUpdateTelemetry = createTextUpdateTelemetry(); const visibleEntryMetrics: MutableVisibleEntryMetrics = { drawCount: 0, @@ -486,8 +423,8 @@ async function createComparisonWorkloadRuntime( }; let cachedBitmapAtlasPrimary: LoadedTechniqueFont | undefined; let cachedBitmapAtlasSecondary: LoadedTechniqueFont | undefined; - let cachedBitmapAtlasPages: BitmapTextLiveStats['atlasPages'] = []; - const bitmapAtlasPages = (fonts: readonly LoadedTechniqueFont[]): BitmapTextLiveStats['atlasPages'] => { + let cachedBitmapAtlasPages: readonly BitmapAtlasPageStats[] = []; + const bitmapAtlasPages = (fonts: readonly LoadedTechniqueFont[]): readonly BitmapAtlasPageStats[] => { const primary = fonts[0]; const secondary = fonts[1]; if (primary !== cachedBitmapAtlasPrimary || secondary !== cachedBitmapAtlasSecondary) { @@ -791,7 +728,7 @@ async function createComparisonWorkloadRuntime( function enqueueUpdate(next: ComparisonWorkloadConfiguration, viewportChanged = false): Promise { if (closing || disposed) { - return Promise.reject(new DOMException('The comparison preview is disposed', 'AbortError')); + return Promise.reject(new DOMException('The comparison workload scene is disposed', 'AbortError')); } requestedConfiguration = next; if ( @@ -812,15 +749,6 @@ async function createComparisonWorkloadRuntime( startUpdateDrain(); }); } - const uploadFrameStarted = performance.now(); - if (!persistent) { - canvasSurface.render(scene, camera); - firstDrawMs = performance.now() - uploadFrameStarted; - if (backend === 'webgpu' && gpuTimingSupported) { - uploadFrameGpuMs = await renderer.resolveTimestampsAsync(THREE.TimestampQuery.RENDER); - uploadFrameCompleteMs = performance.now() - uploadFrameStarted; - } - } const startupMs = performance.now() - startupStarted; const recordReflow = (duration: number): void => { reflowCount += 1; @@ -830,14 +758,6 @@ async function createComparisonWorkloadRuntime( const renderFrame = (timestamp: number, renderScene = true): void => { if (closing || disposed) return; try { - const cpuFrameStarted = performance.now(); - if (gpuFrameTimer !== undefined) { - for (const measurement of gpuFrameTimer.poll()) { - if (measurement.durationMs === undefined) telemetry?.discardGpu(measurement.frameId); - else telemetry?.recordGpu(measurement.frameId, measurement.durationMs); - } - } - const frameId = telemetry?.beginFrame(timestamp); if (renderScene && configuration.workload === 'icon-grid') { iconGridInstance?.frame( configuration, @@ -884,19 +804,17 @@ async function createComparisonWorkloadRuntime( ); } const started = performance.now(); - if (frameId !== undefined && telemetry?.gpuTimingSupported === true) gpuFrameTimer?.beginFrame(frameId); - try { - canvasSurface.render(scene, camera); - } finally { - if (frameId !== undefined && telemetry?.gpuTimingSupported === true) gpuFrameTimer?.endFrame(); - } + canvasSurface.render(scene, camera); const submitMs = performance.now() - started; - if (firstDrawMs === 0) firstDrawMs = submitMs; + if (firstDrawMs === 0) { + firstDrawMs = submitMs; + uploadFrameCompleteMs = submitMs; + } } - const cpuFrameMs = performance.now() - cpuFrameStarted; - const snapshot = frameId === undefined ? persistentSnapshot : telemetry?.endFrame(frameId, cpuFrameMs); + const snapshot = persistentSnapshot; if (snapshot === undefined) return; - if (persistent) persistentSnapshot = undefined; + persistentSnapshot = undefined; + uploadFrameGpuMs ??= snapshot.gpuFrameMs; const activeZoomEntry = configuration.workload === 'zoom-text' ? entries[zoomAnimationState.phraseIndex] : undefined; const zoomScale = activeZoomEntry?.node.scale.x ?? 1; @@ -1031,7 +949,6 @@ async function createComparisonWorkloadRuntime( } }; animationEpoch = performance.now(); - if (!persistent) await renderer.setAnimationLoop((timestamp) => renderFrame(timestamp)); return { resize(nextWidth, nextHeight) { @@ -1041,10 +958,6 @@ async function createComparisonWorkloadRuntime( if (validatedWidth === width && validatedHeight === height) return; width = validatedWidth; height = validatedHeight; - if (!persistent) { - renderer.setSize(width, height, false); - rendererViewport = readRendererViewportState(renderer); - } canvasSurface.resize(width, height); resizeWorkloadCamera(camera, width, height); void enqueueUpdate(requestedConfiguration, true).catch(onError); @@ -1076,12 +989,10 @@ async function createComparisonWorkloadRuntime( update(next) { return enqueueUpdate(validateConfiguration(next)); }, - persistentFrame(context) { - if (!persistent) return; + frame(context) { renderFrame(context.timestamp); }, - persistentTelemetry(snapshot, viewport) { - if (!persistent) return; + telemetry(snapshot, viewport) { rendererViewport = { drawingBufferHeight: viewport.drawingBufferHeight, drawingBufferWidth: viewport.drawingBufferWidth, @@ -1095,38 +1006,28 @@ async function createComparisonWorkloadRuntime( if (disposal !== undefined) return disposal; closing = true; revision += 1; - const stopRendering = persistent ? Promise.resolve() : renderer.setAnimationLoop(null); - const disposalReason = new DOMException('The comparison preview is disposed', 'AbortError'); + const disposalReason = new DOMException('The comparison workload scene is disposed', 'AbortError'); for (const waiter of pendingUpdate?.waiters ?? []) waiter.reject(disposalReason); pendingUpdate = undefined; disposal = (async () => { - await stopRendering; await updateDrain; disposed = true; iconGridInstance?.dispose(); - await gpuFrameTimer?.dispose(); - if (!persistent) { - renderer.setRenderTarget(null); - renderer.clear(); - } disposeEntries(entries); entries = []; activeSelectedFont.dispose(); iconFont?.font.dispose(); canvasSurface.dispose(); - if (!persistent) await disposeConfiguredRenderer(renderer); })(); return disposal; }, }; } catch (error) { - await gpuFrameTimer?.dispose(); disposeEntries(entries); iconFont?.font.dispose(); if (selectedFontController === undefined) font?.font.dispose(); else selectedFontController.dispose(); canvasSurface.dispose(); - if (!persistent) await disposeConfiguredRenderer(renderer); throw error; } } @@ -1381,7 +1282,7 @@ function iconGridStats( }; } -function combineBitmapAtlasPages(fonts: readonly LoadedTechniqueFont[]): BitmapTextLiveStats['atlasPages'] { +function combineBitmapAtlasPages(fonts: readonly LoadedTechniqueFont[]): readonly BitmapAtlasPageStats[] { const pagesPerStrike = new Map(); return fonts.flatMap(({ atlasPages }) => atlasPages.map((page) => { diff --git a/apps/benchmarks/src/workloads/workload-boundary.test.ts b/apps/benchmarks/src/workloads/workload-boundary.test.ts index ab84e6cf..052cf942 100644 --- a/apps/benchmarks/src/workloads/workload-boundary.test.ts +++ b/apps/benchmarks/src/workloads/workload-boundary.test.ts @@ -4,7 +4,16 @@ import { describe, expect, it } from 'vitest'; const workloadDirectory = fileURLToPath(new URL('.', import.meta.url)); const rendererDependencyPattern = - /(?:\bfrom\s*|\bimport\s*\(\s*(?:\/\*[\s\S]*?\*\/\s*)*)['"](?:\.\.\/)+renderer(?:\/|['"])/; + /(?:\bfrom\s*|\bimport\s*\(\s*(?:\/\*[\s\S]*?\*\/\s*)*)['"](?:\.\.\/)+renderer\/([^'"]+)['"]/g; +const allowedRendererDependencies = new Set([ + 'canvas-surface', + 'live-frame-telemetry', + 'persistent-render-host', + 'persistent-scene-activation', + 'retained-font-fixture', + 'text-update-telemetry', + 'webgpu-renderer', +]); async function sourceFiles(directory: string): Promise { const entries = await readdir(directory, { withFileTypes: true }); @@ -18,24 +27,42 @@ async function sourceFiles(directory: string): Promise { return nested.flat(); } +function rendererDependencies(source: string): readonly string[] { + return [...source.matchAll(rendererDependencyPattern)].map((match) => match[1]!); +} + describe('workload source boundaries', () => { it('recognizes direct, nested, and dynamic renderer imports', () => { - const renderer = 'renderer/text'; - expect(rendererDependencyPattern.test(`import { text } from '../${renderer}';`)).toBe(true); - expect(rendererDependencyPattern.test(`import { text } from '../../${renderer}';`)).toBe(true); - expect(rendererDependencyPattern.test(`await import('../${renderer}');`)).toBe(true); - expect(rendererDependencyPattern.test(`import(/* eager */ '../../${renderer}');`)).toBe(true); + const renderer = 'renderer/bitmap-text'; + expect(rendererDependencies(`import { text } from '../${renderer}';`)).toEqual(['bitmap-text']); + expect(rendererDependencies(`import { text } from '../../${renderer}';`)).toEqual(['bitmap-text']); + expect(rendererDependencies(`await import('../${renderer}');`)).toEqual(['bitmap-text']); + expect(rendererDependencies(`import(/* eager */ '../../${renderer}');`)).toEqual(['bitmap-text']); }); - it('does not depend on renderer implementation modules', async () => { + it('depends only on generic renderer host and scene primitives', async () => { const files = await sourceFiles(workloadDirectory); const offenders = await Promise.all( files.map(async (file) => { const source = await readFile(file, 'utf8'); - return rendererDependencyPattern.test(source) ? file.slice(workloadDirectory.length + 1) : undefined; + const unsupported = rendererDependencies(source).filter( + (dependency) => !allowedRendererDependencies.has(dependency), + ); + return unsupported.length === 0 + ? undefined + : `${file.slice(workloadDirectory.length + 1)}: ${unsupported.join(', ')}`; }), ); expect(offenders.filter((file): file is string => file !== undefined)).toEqual([]); }); + + it('keeps the retained comparison scene local and the standalone adapter target-owned', async () => { + const source = await readFile(new URL('./comparison/scene.ts', import.meta.url), 'utf8'); + + expect(source).not.toContain('createComparisonWorkloadPreview'); + expect(source).not.toContain('createConfiguredRenderer'); + expect(source).not.toContain('setAnimationLoop'); + expect(source).not.toContain('createGpuFrameTimer'); + }); }); diff --git a/apps/benchmarks/vitexec/slug-adaptive32-performance.probe.ts b/apps/benchmarks/vitexec/slug-adaptive32-performance.probe.ts index 58701d98..8a5f661d 100644 --- a/apps/benchmarks/vitexec/slug-adaptive32-performance.probe.ts +++ b/apps/benchmarks/vitexec/slug-adaptive32-performance.probe.ts @@ -1,11 +1,14 @@ -import type { ComparisonWorkloadPreview, ComparisonWorkloadStats } from '../src/renderer/comparison-workload'; +import type { + ComparisonWorkloadPreview, + ComparisonWorkloadStats, +} from '../src/benchmark/targets/measurement/comparison-preview'; export {}; type SlugStats = Extract; type Variant = 'fixed16' | 'adaptive32'; -const comparisonWorkloadPath = '/src/renderer/comparison-workload.ts'; +const comparisonWorkloadPath = '/src/benchmark/targets/measurement/comparison-preview.ts'; const environmentPath = '/src/benchmark/environment.ts'; const fontFixturesPath = '/src/benchmark/font-fixtures.ts'; const productResultPath = '/src/benchmark/product-result.ts'; diff --git a/apps/benchmarks/vitexec/slug-fixed32-performance.probe.ts b/apps/benchmarks/vitexec/slug-fixed32-performance.probe.ts index 5de7e7cd..4dc9cbab 100644 --- a/apps/benchmarks/vitexec/slug-fixed32-performance.probe.ts +++ b/apps/benchmarks/vitexec/slug-fixed32-performance.probe.ts @@ -1,11 +1,14 @@ -import type { ComparisonWorkloadPreview, ComparisonWorkloadStats } from '../src/renderer/comparison-workload'; +import type { + ComparisonWorkloadPreview, + ComparisonWorkloadStats, +} from '../src/benchmark/targets/measurement/comparison-preview'; export {}; type SlugStats = Extract; type Variant = 'fixed16' | 'fixed32'; -const comparisonWorkloadPath = '/src/renderer/comparison-workload.ts'; +const comparisonWorkloadPath = '/src/benchmark/targets/measurement/comparison-preview.ts'; const environmentPath = '/src/benchmark/environment.ts'; const fontFixturesPath = '/src/benchmark/font-fixtures.ts'; const productResultPath = '/src/benchmark/product-result.ts'; diff --git a/apps/benchmarks/vitexec/slug-performance-matrix.probe.ts b/apps/benchmarks/vitexec/slug-performance-matrix.probe.ts index dfdc22cc..c6856faa 100644 --- a/apps/benchmarks/vitexec/slug-performance-matrix.probe.ts +++ b/apps/benchmarks/vitexec/slug-performance-matrix.probe.ts @@ -1,10 +1,13 @@ -import type { ComparisonWorkloadPreview, ComparisonWorkloadStats } from '../src/renderer/comparison-workload'; +import type { + ComparisonWorkloadPreview, + ComparisonWorkloadStats, +} from '../src/benchmark/targets/measurement/comparison-preview'; export {}; type SlugComparisonWorkloadStats = Extract; -const comparisonWorkloadPath = '/src/renderer/comparison-workload.ts'; +const comparisonWorkloadPath = '/src/benchmark/targets/measurement/comparison-preview.ts'; const environmentPath = '/src/benchmark/environment.ts'; const fontFixturesPath = '/src/benchmark/font-fixtures.ts'; const productResultPath = '/src/benchmark/product-result.ts'; diff --git a/docs/log.md b/docs/log.md index d2c3e555..c4610551 100644 --- a/docs/log.md +++ b/docs/log.md @@ -2,6 +2,7 @@ ## 2026-08-03 +- **Workload-owned retained comparison scene** — Moved the remaining 1,573-line multi-technique workload implementation and its 661-line focused test beside the authored workload definitions under `workloads/comparison`. Removed its standalone renderer, RAF, GPU-timer, and telemetry branch. Three Slug performance probes now use an 80-line measurement-owned adapter that creates one `PersistentRenderHost` and activates the same retained scene, completing all 40 fixed-32 browser runs across WebGPU/WebGL2, Inter/CJK, and both candidates. The complete 317-test gate passed; all 42 Presentation cells stayed visible with one renderer, and both timed demos returned to Off-axis / 3D at 59.90/60.02 Icon Grid FPS. - **Canonical benchmark asset and raster metadata boundaries** — Removed renderer-owned font-loader, preload, baked-artifact, and raster-configuration compatibility facades. Live scenes and finite product/conformance targets now call the discriminated `workloads/font-assets` API directly, while Bitmap atlas, MTSDF extension, and Slug allocation inspection live under `benchmark/low-level/raster`. The complete 316-test gate and production build passed; all 19 isolated conformance/product scenarios remained deterministic, all 42 sequential Presentation cells rendered visible pixels with one renderer, and the retained comparison plus exclusive finite-job recovery probe passed on WebGPU and WebGL2. Live renderer chunks fell again to 9.26/3.38 kB minified/gzip for Bitmap, 7.77/2.85 for MTSDF, and 7.28/2.75 for Slug. - **Persistent renderer API cleanup** — Removed the unused standalone Bitmap, MTSDF, and Slug preview constructors and 676 lines of duplicate renderer, RAF, GPU-timer, telemetry, resize, and disposal lifecycle. The remaining contracts are named for persistent scenes, and a boundary regression rejects reintroducing preview entrypoints. The complete 315-test benchmark gate and production build passed; live renderer chunks fell from 16.82/5.19 to 10.37/3.84 kB minified/gzip for Bitmap, 9.02/3.32 to 8.37/3.12 for MTSDF, and 9.26/3.37 to 8.19/3.09 for Slug. All 42 sequential Presentation cells rendered visible pixels with one renderer, and both timed demos completed their authored sequence, returned to Off-axis / 3D, and measured 59.92/60.02 Icon Grid FPS on WebGPU/WebGL2. - **Slug conformance scene locality** — Moved the Slug release-role scene definitions from the reusable renderer directory beside the conformance capture that owns and executes them. The capture script and URL-loaded browser probe now resolve the target-owned module directly, and the benchmark boundary regression rejects the obsolete renderer path. @@ -41,7 +42,7 @@ ## 2026-07-31 -- **React Doctor closure** — Removed the unused interactive-canvas component and runtime canvas-settings hook. The benchmark-local full-scan configuration excludes only Vitexec entrypoints, two URL-loaded evidence modules, and one dynamically imported digest export that static reachability cannot observe. React Doctor completes with zero errors, warnings, affected files, or diagnostics while retaining all live application rules. +- **React Doctor closure** — Removed the unused interactive-canvas component and runtime canvas-settings hook. The benchmark-local full-scan configuration excludes only Vitexec entrypoints, three URL-loaded evidence modules, and one dynamically imported digest export that static reachability cannot observe. React Doctor completes with zero errors, warnings, affected files, or diagnostics while retaining all live application rules. - **Fresh-checkout HarfBuzz bootstrap** — Added an exact root pipx pin before the existing pipx-backed Meson pin, so `mise install` no longer assumes an ambient pipx executable. The minimal Rust toolchain declares Cargo explicitly instead of relying on an implicit profile component that was absent from a fresh Linux mise cache. The macOS prerequisite now names both Homebrew `glib` and `pkgconf`, making the required `glib-2.0` metadata discoverable when the authenticated HarfBuzz fixture workflow configures its pinned utilities. - **Milestone 8.6 regeneration** — Regenerated the affected ABIs, optimized Wasm modules, baked fixtures, identities, size records, autoresearch provenance, and package digests. The complete Rust, TypeScript, Node/Worker, artifact, renderer, conformance, live-product, and packed-consumer sweep passes locally; the four independently valid stack layers pass the clean Linux CI gate in 19m16s, 18m29s, 18m32s, and 19m13s. - **Host-scoped MTSDF admission evidence** — Labeled the compiled admission-module measurement by platform and architecture. The recorded host retains exact evidence freshness; foreign hosts must rebuild, reproduce the portable contract and synthetic output, retain zero imports, emit a complete SHA-256 identity, and remain under reviewed raw/optimized/gzip/Brotli ceilings. Focused contract tests reject stale portable fields, incomplete hashes, and budget overflow. diff --git a/docs/packages/benchmarks.md b/docs/packages/benchmarks.md index 2725af2c..e9d84523 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:9f283a698266447ee391195fa38e059f0be8135c7798573e14c34c1d57eda243' +source_digest: 'sha256:64ff511cf7545a06d2efc9d942a22b1be8b2aa62b2f75f7aab5da4ad728b3a0f' tags: [package, benchmarks, react, vite, product-e2e] sources: - id: manifest @@ -129,8 +129,11 @@ sources: resource: ../../apps/benchmarks/src/benchmark/targets/conformance/raster/slug-capture.ts title: Target-owned finite Slug capture lifecycle and role proofs - id: comparison-workload - resource: ../../apps/benchmarks/src/renderer/comparison-workload.ts - title: Retained comparison-workload renderer + resource: ../../apps/benchmarks/src/workloads/comparison/scene.ts + title: Retained multi-technique workload scene + - id: comparison-measurement-preview + resource: ../../apps/benchmarks/src/benchmark/targets/measurement/comparison-preview.ts + title: Isolated-canvas adapter for Slug performance measurements - id: conformance-surface resource: ../../apps/benchmarks/src/surfaces/conformance/conformance-surface.tsx title: Host-borrowing conformance surface hierarchy @@ -257,6 +260,8 @@ Bitmap, MTSDF, and Slug renderer modules expose only persistent-scene constructi Canonical font loading is owned exclusively by `workloads/font-assets`, and renderer-neutral Bitmap atlas, MTSDF configuration, and Slug allocation inspection is owned by `benchmark/low-level/raster`. Product targets, conformance targets, retained comparison scenes, and live technique scenes call those owners directly; renderer modules no longer re-export loader, preload, baked-artifact, or raster-configuration compatibility facades. +The multi-technique retained implementation lives beside its authored definitions under `workloads/comparison/scene`. It can use only an allowlisted set of generic host, canvas, telemetry, and activation primitives from `renderer`; it cannot create a renderer, animation loop, or GPU timer. Slug performance observations that require an isolated canvas enter through `benchmark/targets/measurement/comparison-preview`, which owns one `PersistentRenderHost`, activates the same workload scene, and guarantees host disposal after release. The app and workload rail preserve the literal lazy scene chunk boundary. + ### Benchmark ipsum corpus The corpus is an executable fixture, not display copy. Its five lines isolate ordinary Latin rhythm, numerals, kerning pairs, punctuation, standard ligature candidates, and compact mathematical notation. Inter must shape every scalar without glyph 0; the renderer rejects the corpus before upload if coverage regresses. Every selectable family receives the identical diagnostic and paragraph source text. The live surface reports source length and missing glyphs, so fixture coverage differences remain visible and comparable instead of being hidden by font-specific copy.