diff --git a/apps/example/.gitignore b/apps/example/.gitignore index 1c36e10bb..9244177d9 100644 --- a/apps/example/.gitignore +++ b/apps/example/.gitignore @@ -14,3 +14,6 @@ dist/* local.properties msbuild.binlog node_modules/ + +# CTS files (copied from WebGPU CTS repo via scripts/sync-cts.sh) +src/webgpu-cts/src/ diff --git a/apps/example/metro.config.js b/apps/example/metro.config.js index 28b1ad125..cb234836c 100644 --- a/apps/example/metro.config.js +++ b/apps/example/metro.config.js @@ -12,6 +12,7 @@ const customConfig = { watchFolders: [root], resolver: { ...defaultConfig.resolver, + sourceExts: [...defaultConfig.resolver.sourceExts, 'cjs'], extraNodeModules: { 'three': threePackagePath, }, diff --git a/apps/example/scripts/sync-cts.sh b/apps/example/scripts/sync-cts.sh new file mode 100755 index 000000000..00a28b40a --- /dev/null +++ b/apps/example/scripts/sync-cts.sh @@ -0,0 +1,483 @@ +#!/bin/bash + +# Sync WebGPU CTS files to React Native app +# Usage: ./scripts/sync-cts.sh [path-to-cts-repo] + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_DIR="$(dirname "$SCRIPT_DIR")" +CTS_DIR="${1:-$APP_DIR/../../../cts}" + +# Verify CTS directory exists +if [ ! -d "$CTS_DIR/src/webgpu" ]; then + echo "Error: CTS directory not found at $CTS_DIR" + echo "Usage: $0 [path-to-cts-repo]" + exit 1 +fi + +echo "Syncing CTS from: $CTS_DIR" +echo "To: $APP_DIR/src/webgpu-cts" + +# Clean existing CTS files +echo "Cleaning existing CTS files..." +rm -rf "$APP_DIR/src/webgpu-cts" + +# Create directory structure +mkdir -p "$APP_DIR/src/webgpu-cts/src/common/framework" +mkdir -p "$APP_DIR/src/webgpu-cts/src/common/internal" +mkdir -p "$APP_DIR/src/webgpu-cts/src/common/util" +mkdir -p "$APP_DIR/src/webgpu-cts/src/common/runtime/rn/generated" +mkdir -p "$APP_DIR/src/webgpu-cts/src/common/runtime/helper" +mkdir -p "$APP_DIR/src/webgpu-cts/src/webgpu" +mkdir -p "$APP_DIR/src/webgpu-cts/src/external" + +# Copy common framework files +echo "Copying common/framework..." +cp -r "$CTS_DIR/src/common/framework/"* "$APP_DIR/src/webgpu-cts/src/common/framework/" + +# Copy common internal files +echo "Copying common/internal..." +cp -r "$CTS_DIR/src/common/internal/"* "$APP_DIR/src/webgpu-cts/src/common/internal/" + +# Copy common util files +echo "Copying common/util..." +cp -r "$CTS_DIR/src/common/util/"* "$APP_DIR/src/webgpu-cts/src/common/util/" + +# Copy React Native runtime +echo "Copying common/runtime/rn..." +cp "$CTS_DIR/src/common/runtime/rn/loader.ts" "$APP_DIR/src/webgpu-cts/src/common/runtime/rn/" +cp "$CTS_DIR/src/common/runtime/rn/runtime.ts" "$APP_DIR/src/webgpu-cts/src/common/runtime/rn/" +cp "$CTS_DIR/src/common/runtime/rn/index.ts" "$APP_DIR/src/webgpu-cts/src/common/runtime/rn/" + +# Copy external dependencies (Float16, etc.) +echo "Copying external dependencies..." +cp -r "$CTS_DIR/src/external/"* "$APP_DIR/src/webgpu-cts/src/external/" + +# Copy webgpu test specs (excluding web_platform which uses browser-only APIs) +echo "Copying webgpu specs (this may take a moment)..." +rsync -a --exclude='web_platform' "$CTS_DIR/src/webgpu/" "$APP_DIR/src/webgpu-cts/src/webgpu/" + +# Remove spec files that import from web_platform (browser-only APIs) +echo "Removing spec files that depend on web_platform..." +find "$APP_DIR/src/webgpu-cts/src/webgpu" -name "*.spec.ts" -exec grep -l "from.*web_platform" {} \; | xargs rm -f 2>/dev/null || true + +# Strip .js extensions from imports (Metro bundler doesn't need them) +echo "Stripping .js extensions from imports..." +find "$APP_DIR/src/webgpu-cts/src" -name "*.ts" -type f -exec sed -i '' "s/from '\([^']*\)\.js'/from '\1'/g" {} \; +find "$APP_DIR/src/webgpu-cts/src" -name "*.ts" -type f -exec sed -i '' 's/from "\([^"]*\)\.js"/from "\1"/g' {} \; + +# Fix float16 for Metro (rename .js to .impl.js and create .ts wrapper) +echo "Fixing float16 for Metro compatibility..." +if [ -f "$APP_DIR/src/webgpu-cts/src/external/petamoriken/float16/float16.js" ]; then + mv "$APP_DIR/src/webgpu-cts/src/external/petamoriken/float16/float16.js" \ + "$APP_DIR/src/webgpu-cts/src/external/petamoriken/float16/float16.impl.js" + cat > "$APP_DIR/src/webgpu-cts/src/external/petamoriken/float16/float16.ts" << 'FLOAT16' +// TypeScript wrapper for Metro compatibility +// @ts-expect-error - importing from JS implementation +export * from './float16.impl.js'; +FLOAT16 +fi + +# Remove DefaultTestFileLoader (uses dynamic imports not supported by Metro) +echo "Removing DefaultTestFileLoader (dynamic imports not supported)..." +sed -i '' '/^export class DefaultTestFileLoader/,/^}$/c\ +// DefaultTestFileLoader removed - uses dynamic imports not supported by Metro\ +// Use ReactNativeTestFileLoader from ..\/runtime\/rn\/loader instead +' "$APP_DIR/src/webgpu-cts/src/common/internal/file_loader.ts" + +# Fix perf_hooks require (React Native has performance globally) +echo "Fixing perf_hooks require..." +sed -i '' "s/typeof performance !== 'undefined' ? performance : require('perf_hooks').performance/performance/" "$APP_DIR/src/webgpu-cts/src/common/util/util.ts" + +# Create EventTarget polyfill for React Native +echo "Creating EventTarget polyfill..." +cat > "$APP_DIR/src/webgpu-cts/src/common/util/event-target-polyfill.ts" << 'EVENTTARGET_EOF' +/** + * Minimal EventTarget polyfill for React Native. + * Only implements the subset needed by the CTS. + */ + +type Listener = (event: any) => void; + +interface ListenerEntry { + listener: Listener | EventListenerObject; + options?: AddEventListenerOptions; +} + +// Polyfill for Event if not available +if (typeof globalThis.Event === 'undefined') { + (globalThis as any).Event = class Event { + readonly type: string; + readonly bubbles: boolean; + readonly cancelable: boolean; + readonly composed: boolean; + readonly timeStamp: number; + readonly defaultPrevented: boolean = false; + readonly target: EventTarget | null = null; + readonly currentTarget: EventTarget | null = null; + readonly eventPhase: number = 0; + readonly isTrusted: boolean = false; + + constructor(type: string, eventInitDict?: EventInit) { + this.type = type; + this.bubbles = eventInitDict?.bubbles ?? false; + this.cancelable = eventInitDict?.cancelable ?? false; + this.composed = eventInitDict?.composed ?? false; + this.timeStamp = Date.now(); + } + + preventDefault(): void {} + stopPropagation(): void {} + stopImmediatePropagation(): void {} + composedPath(): EventTarget[] { return []; } + + get srcElement(): EventTarget | null { return this.target; } + get returnValue(): boolean { return !this.defaultPrevented; } + set returnValue(_value: boolean) {} + initEvent(_type: string, _bubbles?: boolean, _cancelable?: boolean): void {} + + static readonly NONE = 0; + static readonly CAPTURING_PHASE = 1; + static readonly AT_TARGET = 2; + static readonly BUBBLING_PHASE = 3; + + readonly NONE = 0; + readonly CAPTURING_PHASE = 1; + readonly AT_TARGET = 2; + readonly BUBBLING_PHASE = 3; + }; +} + +// Polyfill for MessageEvent if not available +if (typeof globalThis.MessageEvent === 'undefined') { + (globalThis as any).MessageEvent = class MessageEvent { + readonly type: string; + readonly data: T; + readonly origin: string = ''; + readonly lastEventId: string = ''; + readonly source: any = null; + readonly ports: readonly any[] = []; + readonly bubbles: boolean = false; + readonly cancelable: boolean = false; + readonly defaultPrevented: boolean = false; + readonly timeStamp: number; + + constructor(type: string, eventInitDict?: { data?: T; bubbles?: boolean; cancelable?: boolean }) { + this.type = type; + this.data = eventInitDict?.data as T; + this.bubbles = eventInitDict?.bubbles ?? false; + this.cancelable = eventInitDict?.cancelable ?? false; + this.timeStamp = Date.now(); + } + + preventDefault(): void {} + stopPropagation(): void {} + stopImmediatePropagation(): void {} + }; +} + +// Polyfill for EventTarget if not available +if (typeof globalThis.EventTarget === 'undefined') { + (globalThis as any).EventTarget = class EventTarget { + private listeners: Map = new Map(); + + addEventListener( + type: string, + listener: Listener | EventListenerObject | null, + options?: boolean | AddEventListenerOptions + ): void { + if (!listener) return; + + const opts: AddEventListenerOptions | undefined = + typeof options === 'boolean' ? { capture: options } : options; + + let typeListeners = this.listeners.get(type); + if (!typeListeners) { + typeListeners = []; + this.listeners.set(type, typeListeners); + } + + const exists = typeListeners.some( + entry => + entry.listener === listener && + entry.options?.capture === opts?.capture + ); + if (!exists) { + typeListeners.push({ listener, options: opts }); + } + } + + removeEventListener( + type: string, + listener: Listener | EventListenerObject | null, + options?: boolean | EventListenerOptions + ): void { + if (!listener) return; + + const opts: EventListenerOptions | undefined = + typeof options === 'boolean' ? { capture: options } : options; + + const typeListeners = this.listeners.get(type); + if (!typeListeners) return; + + const index = typeListeners.findIndex( + entry => + entry.listener === listener && + entry.options?.capture === opts?.capture + ); + if (index !== -1) { + typeListeners.splice(index, 1); + } + } + + dispatchEvent(event: any): boolean { + const typeListeners = this.listeners.get(event.type); + if (!typeListeners) return true; + + for (const entry of [...typeListeners]) { + try { + if (typeof entry.listener === 'function') { + entry.listener.call(this, event); + } else { + entry.listener.handleEvent(event); + } + } catch (e) { + console.error('Error in event listener:', e); + } + + if (entry.options?.once) { + this.removeEventListener(event.type, entry.listener, entry.options); + } + } + + return !event.defaultPrevented; + } + }; +} + +export {}; +EVENTTARGET_EOF + +# Add polyfill import to rn/index.ts +echo "Adding polyfill import to rn/index.ts..." +sed -i '' '1i\ +// Polyfill EventTarget and MessageEvent for React Native\ +import '\''../../util/event-target-polyfill'\'';\ +' "$APP_DIR/src/webgpu-cts/src/common/runtime/rn/index.ts" + +# Stub loadMetadataForSuite (uses Node.js fs module not available in RN) +echo "Stubbing loadMetadataForSuite (fs not available)..." +cat > "$APP_DIR/src/webgpu-cts/src/common/framework/metadata.ts" << 'METADATA_EOF' +/** Metadata about tests (that can't be derived at runtime). */ +export type TestMetadata = { + /** + * Estimated average time-per-subcase, in milliseconds. + * This is used to determine chunking granularity when exporting to WPT with + * chunking enabled (like out-wpt/cts-chunked2sec.https.html). + */ + subcaseMS: number; +}; + +export type TestMetadataListing = { + [testQuery: string]: TestMetadata; +}; + +/** + * Load metadata for a test suite. + * Note: In React Native, this always returns null as we don't have filesystem access. + * This is only used for WPT chunking which isn't needed in RN. + */ +export function loadMetadataForSuite(_suiteDir: string): TestMetadataListing | null { + return null; +} +METADATA_EOF + +# Create React Native compatible options.ts (removes browser window.location dependency) +echo "Creating React Native compatible options.ts..." +cat > "$APP_DIR/src/webgpu-cts/src/common/runtime/helper/options.ts" << 'OPTIONS_EOF' +/** + * React Native compatible stub for options.ts + * Browser-specific URL parsing removed. + */ + +import { unreachable } from '../../util/util'; + +/** Runtime modes for running tests in different types of workers. */ +export type WorkerMode = 'dedicated' | 'service' | 'shared'; + +/** Parse a runner option for different worker modes. */ +export function optionWorkerMode( + _opt: string, + searchParams?: URLSearchParams +): WorkerMode | null { + if (!searchParams) return null; + const value = searchParams.get(_opt); + if (value === null || value === '0') return null; + if (value === 'service') return 'service'; + if (value === 'shared') return 'shared'; + if (value === '' || value === '1' || value === 'dedicated') return 'dedicated'; + unreachable('invalid worker= option value'); +} + +export function optionEnabled(opt: string, searchParams?: URLSearchParams): boolean { + if (!searchParams) return false; + const val = searchParams.get(opt); + return val !== null && val !== '0'; +} + +export function optionString(opt: string, searchParams?: URLSearchParams): string | null { + if (!searchParams) return null; + return searchParams.get(opt); +} + +export interface CTSOptions { + worker: WorkerMode | null; + debug: boolean; + compatibility: boolean; + forceFallbackAdapter: boolean; + enforceDefaultLimits: boolean; + blockAllFeatures: boolean; + unrollConstEvalLoops: boolean; + powerPreference: GPUPowerPreference | null; + subcasesBetweenAttemptingGC: string; + casesBetweenReplacingDevice: string; + logToWebSocket: boolean; +} + +export const kDefaultCTSOptions: Readonly = { + worker: null, debug: false, compatibility: false, forceFallbackAdapter: false, + enforceDefaultLimits: false, blockAllFeatures: false, unrollConstEvalLoops: false, + powerPreference: null, subcasesBetweenAttemptingGC: '5000', + casesBetweenReplacingDevice: 'Infinity', logToWebSocket: false, +}; + +export interface OptionInfo { + description: string; + parser?: (key: string, searchParams?: URLSearchParams) => boolean | string | null; + selectValueDescriptions?: { value: string | null; description: string }[]; +} + +export type OptionsInfos = Record; + +export const kCTSOptionsInfo: OptionsInfos = { + worker: { description: 'run in a worker', parser: optionWorkerMode }, + debug: { description: 'show more info' }, + compatibility: { description: 'request adapters with featureLevel: "compatibility"' }, + forceFallbackAdapter: { description: 'pass forceFallbackAdapter: true to requestAdapter' }, + enforceDefaultLimits: { description: 'force the adapter limits to the default limits' }, + blockAllFeatures: { description: 'block all features on adapter' }, + unrollConstEvalLoops: { description: 'unroll const eval loops in WGSL' }, + powerPreference: { description: 'set default powerPreference', parser: optionString }, + subcasesBetweenAttemptingGC: { description: 'GC interval', parser: optionString }, + casesBetweenReplacingDevice: { description: 'Device replace interval', parser: optionString }, + logToWebSocket: { description: 'log to websocket' }, +}; + +export function camelCaseToSnakeCase(id: string) { + return id.replace(/(.)([A-Z][a-z]+)/g, '$1_$2').replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase(); +} + +function getOptionsInfoFromSearchString( + optionsInfos: OptionsInfos, searchString: string +): Type { + const searchParams = new URLSearchParams(searchString); + const optionValues: Record = {}; + for (const [optionName, info] of Object.entries(optionsInfos)) { + const parser = info.parser || optionEnabled; + optionValues[optionName] = parser(camelCaseToSnakeCase(optionName), searchParams); + } + return optionValues as unknown as Type; +} + +export function parseSearchParamLikeWithOptions( + optionsInfos: OptionsInfos, query: string +): { queries: string[]; options: Type } { + const searchString = query.includes('q=') || query.startsWith('?') ? query : \`q=\${query}\`; + const queries = new URLSearchParams(searchString).getAll('q'); + const options = getOptionsInfoFromSearchString(optionsInfos, searchString); + return { queries, options }; +} + +export function parseSearchParamLikeWithCTSOptions(query: string) { + return parseSearchParamLikeWithOptions(kCTSOptionsInfo, query); +} +OPTIONS_EOF + +# Generate all_specs.ts with correct import paths for RN app +echo "Generating all_specs.ts..." + +# Find all spec files and generate imports +SPEC_FILES=$(find "$APP_DIR/src/webgpu-cts/src/webgpu" -name "*.spec.ts" | sort) +SPEC_COUNT=$(echo "$SPEC_FILES" | wc -l | tr -d ' ') + +echo "Found $SPEC_COUNT spec files" + +# Generate the all_specs.ts file +OUTPUT_FILE="$APP_DIR/src/webgpu-cts/src/common/runtime/rn/generated/all_specs.ts" + +cat > "$OUTPUT_FILE" << 'HEADER' +/** + * Auto-generated file - DO NOT EDIT + * Generated by: scripts/sync-cts.sh + * + * This file statically imports all CTS spec files for React Native. + */ + +import { AllSpecs, SpecEntry } from '../loader'; + +HEADER + +# Generate imports +INDEX=0 +while IFS= read -r SPEC_FILE; do + # Get path relative to webgpu dir + REL_PATH="${SPEC_FILE#$APP_DIR/src/webgpu-cts/src/webgpu/}" + # Remove .spec.ts extension + PATH_NO_EXT="${REL_PATH%.spec.ts}" + # Generate import + echo "import * as spec${INDEX} from '../../../../webgpu/${PATH_NO_EXT}.spec';" >> "$OUTPUT_FILE" + INDEX=$((INDEX + 1)) +done <<< "$SPEC_FILES" + +echo "" >> "$OUTPUT_FILE" +echo "const webgpuSpecs: SpecEntry[] = [" >> "$OUTPUT_FILE" + +# Generate entries +INDEX=0 +while IFS= read -r SPEC_FILE; do + # Get path relative to webgpu dir + REL_PATH="${SPEC_FILE#$APP_DIR/src/webgpu-cts/src/webgpu/}" + # Remove .spec.ts extension + PATH_NO_EXT="${REL_PATH%.spec.ts}" + # Split into path parts + IFS='/' read -ra PARTS <<< "$PATH_NO_EXT" + # Format as array + PATH_ARRAY=$(printf "'%s', " "${PARTS[@]}") + PATH_ARRAY="${PATH_ARRAY%, }" + echo " { path: [${PATH_ARRAY}], spec: spec${INDEX} }," >> "$OUTPUT_FILE" + INDEX=$((INDEX + 1)) +done <<< "$SPEC_FILES" + +cat >> "$OUTPUT_FILE" << 'FOOTER' +]; + +export const allSpecs: AllSpecs = new Map([ + ['webgpu', webgpuSpecs], +]); + +export default allSpecs; +FOOTER + +echo "" +echo "Done! CTS synced to $APP_DIR/src/webgpu-cts" +echo "" +echo "Files copied:" +echo " - src/common/framework/ (test fixtures, params)" +echo " - src/common/internal/ (tree, query, logging)" +echo " - src/common/util/ (utilities)" +echo " - src/common/runtime/rn/ (React Native runtime)" +echo " - src/webgpu/ ($SPEC_COUNT spec files)" +echo "" +echo "Generated:" +echo " - src/common/runtime/rn/generated/all_specs.ts" diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx index c1f2037d7..3491a9e60 100644 --- a/apps/example/src/App.tsx +++ b/apps/example/src/App.tsx @@ -35,6 +35,7 @@ import { ComputeToys } from "./ComputeToys"; import { Reanimated } from "./Reanimated"; import { AsyncStarvation } from "./Diagnostics/AsyncStarvation"; import { DeviceLostHang } from "./Diagnostics/DeviceLostHang"; +import { CTS } from "./CTSScreen"; // The two lines below are needed by three.js import "fast-text-encoding"; @@ -55,6 +56,7 @@ function App() { screenOptions={{ cardStyle: { flex: 1 } }} > + { + const [expanded, setExpanded] = useState(false); + + const statusColor = { + pass: "#4CAF50", + fail: "#F44336", + skip: "#9E9E9E", + warn: "#FF9800", + running: "#2196F3", + notrun: "#9E9E9E", + }[result.status]; + + return ( + setExpanded(!expanded)} + > + + + + {result.name} + + {result.timems.toFixed(1)}ms + + {expanded && result.logs && result.logs.length > 0 && ( + + {result.logs.map((log, i) => ( + + {log} + + ))} + + )} + + ); +}; + +// Preset queries for quick testing +const PRESET_QUERIES = [ + { label: "Adapter Info", query: "webgpu:api,operation,adapter,info:*" }, + { label: "Request Adapter", query: "webgpu:api,operation,adapter,requestAdapter:*" }, + { label: "All Adapter", query: "webgpu:api,operation,adapter,*" }, + { label: "Labels", query: "webgpu:api,operation,labels:*" }, +]; + +export const CTS = () => { + // Start with a very simple query - just adapter info tests + const [query, setQuery] = useState("webgpu:api,operation,adapter,info:*"); + const [status, setStatus] = useState("loading"); + const [error, setError] = useState(null); + const [results, setResults] = useState([]); + const [summary, setSummary] = useState(null); + const [progress, setProgress] = useState({ current: 0, total: 0, name: "" }); + const [specCount, setSpecCount] = useState(0); + + const runnerRef = useRef(null); + + useEffect(() => { + // Initialize runner + try { + console.log("[CTS] Initializing runner..."); + console.log("[CTS] allSpecs loaded:", allSpecs ? "yes" : "no"); + if (allSpecs) { + const webgpuSpecs = allSpecs.get("webgpu"); + console.log("[CTS] webgpu specs count:", webgpuSpecs?.length ?? 0); + setSpecCount(webgpuSpecs?.length ?? 0); + } + + runnerRef.current = new CTSRunner(allSpecs, { + debug: true, + }); + console.log("[CTS] Runner initialized successfully"); + setStatus("ready"); + } catch (e) { + console.error("[CTS] Failed to initialize runner:", e); + setError(e instanceof Error ? e.message : String(e)); + setStatus("error"); + } + + return () => { + runnerRef.current?.requestStop(); + }; + }, []); + + const runTests = useCallback(async () => { + if (!runnerRef.current || status === "running") return; + + setStatus("running"); + setResults([]); + setSummary(null); + setError(null); + setProgress({ current: 0, total: 0, name: "" }); + + try { + console.log("[CTS] Starting test run with query:", query); + const { summary: runSummary, results: runResults } = + await runnerRef.current.runTests(query, { + onTestStart: (name, index, total) => { + console.log(`[CTS] Running ${index + 1}/${total}: ${name}`); + setProgress({ current: index + 1, total, name }); + }, + onTestComplete: (result) => { + console.log(`[CTS] ${result.status}: ${result.name}`); + setResults((prev) => [...prev, result]); + }, + shouldStop: () => runnerRef.current?.isStopRequested() ?? false, + }); + + console.log("[CTS] Test run complete:", runSummary); + setSummary(runSummary); + setStatus("complete"); + } catch (e) { + console.error("[CTS] Test run failed:", e); + setError(e instanceof Error ? e.message : String(e)); + setStatus("error"); + } + }, [query, status]); + + const stopTests = useCallback(() => { + runnerRef.current?.requestStop(); + }, []); + + // Show loading state + if (status === "loading") { + return ( + + + Loading CTS... + + ); + } + + // Show error state + if (status === "error" && !results.length) { + return ( + + Error + + {error} + + { + setError(null); + setStatus("ready"); + }} + > + Retry + + + ); + } + + return ( + + + WebGPU CTS + {specCount} spec files loaded + + {/* Preset query buttons */} + + {PRESET_QUERIES.map((preset) => ( + setQuery(preset.query)} + disabled={status === "running"} + > + + {preset.label} + + + ))} + + + + + {status === "running" ? ( + + Stop + + ) : ( + + Run Tests + + )} + + + {error && ( + + {error} + + )} + + + {status === "running" && ( + + + + Running {progress.current}/{progress.total} + + + {progress.name} + + + )} + + {summary && ( + + Summary + + + Pass: {summary.passed} + + + Fail: {summary.failed} + + + Skip: {summary.skipped} + + + Warn: {summary.warned} + + + + Total: {summary.total} tests in {(summary.timems / 1000).toFixed(2)}s + + + )} + + item.name} + renderItem={({ item }) => } + style={styles.list} + contentContainerStyle={styles.listContent} + /> + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#1a1a1a", + }, + centered: { + justifyContent: "center", + alignItems: "center", + padding: 20, + }, + loadingText: { + color: "#fff", + fontSize: 16, + marginTop: 16, + }, + errorTitle: { + color: "#F44336", + fontSize: 24, + fontWeight: "bold", + marginBottom: 16, + }, + errorScroll: { + maxHeight: 200, + marginBottom: 16, + }, + errorText: { + color: "#F44336", + fontSize: 14, + fontFamily: "monospace", + }, + retryButton: { + backgroundColor: "#2196F3", + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 8, + }, + errorBanner: { + backgroundColor: "#F44336", + padding: 12, + borderRadius: 8, + marginTop: 12, + }, + errorBannerText: { + color: "#fff", + fontSize: 12, + }, + header: { + padding: 16, + borderBottomWidth: 1, + borderBottomColor: "#333", + }, + title: { + fontSize: 24, + fontWeight: "bold", + color: "#fff", + marginBottom: 4, + }, + subtitle: { + fontSize: 12, + color: "#999", + marginBottom: 12, + }, + presetRow: { + marginBottom: 12, + }, + presetButton: { + backgroundColor: "#2a2a2a", + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 16, + marginRight: 8, + borderWidth: 1, + borderColor: "#444", + }, + presetButtonActive: { + backgroundColor: "#2196F3", + borderColor: "#2196F3", + }, + presetButtonText: { + color: "#999", + fontSize: 12, + }, + presetButtonTextActive: { + color: "#fff", + }, + queryInput: { + backgroundColor: "#2a2a2a", + borderRadius: 8, + padding: 12, + color: "#fff", + fontSize: 14, + fontFamily: "monospace", + marginBottom: 12, + }, + buttonRow: { + flexDirection: "row", + gap: 8, + }, + runButton: { + backgroundColor: "#4CAF50", + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 8, + }, + stopButton: { + backgroundColor: "#F44336", + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 8, + }, + buttonText: { + color: "#fff", + fontWeight: "bold", + fontSize: 16, + }, + progressContainer: { + flexDirection: "row", + alignItems: "center", + padding: 12, + backgroundColor: "#2a2a2a", + gap: 8, + }, + progressText: { + color: "#fff", + fontSize: 14, + }, + progressName: { + color: "#999", + fontSize: 12, + flex: 1, + }, + summaryContainer: { + padding: 16, + backgroundColor: "#2a2a2a", + margin: 8, + borderRadius: 8, + }, + summaryTitle: { + color: "#fff", + fontSize: 18, + fontWeight: "bold", + marginBottom: 8, + }, + summaryRow: { + flexDirection: "row", + justifyContent: "space-between", + marginBottom: 8, + }, + summaryItem: { + fontSize: 14, + fontWeight: "500", + }, + summaryTime: { + color: "#999", + fontSize: 12, + }, + list: { + flex: 1, + }, + listContent: { + padding: 8, + }, + testItem: { + backgroundColor: "#2a2a2a", + borderRadius: 8, + padding: 12, + marginBottom: 4, + }, + testHeader: { + flexDirection: "row", + alignItems: "center", + }, + statusDot: { + width: 8, + height: 8, + borderRadius: 4, + marginRight: 8, + }, + testName: { + flex: 1, + color: "#fff", + fontSize: 12, + fontFamily: "monospace", + }, + testTime: { + color: "#999", + fontSize: 11, + marginLeft: 8, + }, + logsContainer: { + marginTop: 8, + padding: 8, + backgroundColor: "#1a1a1a", + borderRadius: 4, + }, + logText: { + color: "#F44336", + fontSize: 11, + fontFamily: "monospace", + }, +}); diff --git a/apps/example/src/Home.tsx b/apps/example/src/Home.tsx index 711978821..320ffacde 100644 --- a/apps/example/src/Home.tsx +++ b/apps/example/src/Home.tsx @@ -7,6 +7,10 @@ import type { StackNavigationProp } from "@react-navigation/stack"; import type { Routes } from "./Route"; export const examples = [ + { + screen: "CTS", + title: "๐Ÿงช WebGPU CTS", + }, { screen: "Tests", title: "๐Ÿงช E2E Tests", diff --git a/apps/example/src/Route.ts b/apps/example/src/Route.ts index 67d82ad22..7b6aa61dc 100644 --- a/apps/example/src/Route.ts +++ b/apps/example/src/Route.ts @@ -1,5 +1,6 @@ export type Routes = { Home: undefined; + CTS: undefined; HelloTriangle: undefined; HelloTriangleMSAA: undefined; Cube: undefined; diff --git a/apps/example/src/webgpu-cts/README.md b/apps/example/src/webgpu-cts/README.md new file mode 100644 index 000000000..d59081bd2 --- /dev/null +++ b/apps/example/src/webgpu-cts/README.md @@ -0,0 +1,114 @@ +# WebGPU CTS for React Native + +This directory contains the [WebGPU Conformance Test Suite (CTS)](https://github.com/nicebuild-org/nicebuild/webgpu-cts) adapted for React Native. + +## Overview + +The WebGPU CTS is designed for browser and Node.js environments with dynamic imports. React Native (via Metro bundler) doesn't support dynamic imports, so we use a build step to generate static imports for all spec files. + +## Syncing the CTS + +To update the CTS files from the upstream repository: + +```bash +# Clone the CTS repo (if not already) +git clone https://github.com/nicebuild-org/nicebuild/webgpu-cts.git /path/to/cts + +# Run the sync script +./scripts/sync-cts.sh /path/to/cts +``` + +The sync script: +1. Copies the necessary CTS source files +2. Excludes `web_platform` tests (browser-only APIs) +3. Strips `.js` extensions from imports (Metro compatibility) +4. Creates polyfills for missing Web APIs (Event, EventTarget, MessageEvent) +5. Stubs Node.js-specific code (fs, perf_hooks) +6. Generates `all_specs.ts` with static imports for all spec files + +## Running Tests + +```tsx +import { CTSRunner } from './webgpu-cts/src/common/runtime/rn'; +import { allSpecs } from './webgpu-cts/src/common/runtime/rn/generated/all_specs'; + +// Create a runner +const runner = new CTSRunner(allSpecs, { + debug: false, + compatibility: false, +}); + +// Run tests matching a query +const { summary, results } = await runner.runTests('webgpu:api,operation,adapter,*', { + onTestStart: (name, index, total) => { + console.log(`Running ${index + 1}/${total}: ${name}`); + }, + onTestComplete: (result) => { + console.log(`${result.status}: ${result.name}`); + }, +}); + +console.log(`Passed: ${summary.passed}, Failed: ${summary.failed}`); +``` + +## Query Syntax + +The CTS uses a hierarchical query syntax: + +- `webgpu:*` - All WebGPU tests +- `webgpu:api,*` - All API tests +- `webgpu:api,operation,*` - All operation tests +- `webgpu:api,operation,adapter,*` - All adapter tests +- `webgpu:api,operation,adapter,info:*` - Specific test file + +## Polyfills + +The following Web APIs are polyfilled for React Native: + +- `Event` - Base event class +- `EventTarget` - Event dispatching +- `MessageEvent` - Used for progress events + +These are automatically loaded via `event-target-polyfill.ts`. + +## Modifications from Upstream + +The sync script makes these modifications: + +| File | Change | +|------|--------| +| `common/framework/metadata.ts` | Stub `loadMetadataForSuite` (no fs access) | +| `common/util/util.ts` | Remove `perf_hooks` require | +| `common/internal/file_loader.ts` | Remove `DefaultTestFileLoader` | +| `common/runtime/helper/options.ts` | Remove `window.location` dependency | +| `external/petamoriken/float16/` | Wrap JS file for Metro | + +## Architecture + +``` +src/webgpu-cts/ +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ common/ +โ”‚ โ”‚ โ”œโ”€โ”€ framework/ # Test fixtures, params +โ”‚ โ”‚ โ”œโ”€โ”€ internal/ # Query parsing, tree building +โ”‚ โ”‚ โ”œโ”€โ”€ runtime/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ rn/ # React Native runtime +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ loader.ts # ReactNativeTestFileLoader +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ runtime.ts # CTSRunner +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ index.ts # Public exports +โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ generated/ +โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ all_specs.ts # Static imports (generated) +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ helper/ +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ options.ts # RN-compatible options +โ”‚ โ”‚ โ””โ”€โ”€ util/ +โ”‚ โ”‚ โ””โ”€โ”€ event-target-polyfill.ts # Web API polyfills +โ”‚ โ”œโ”€โ”€ external/ # Third-party (float16) +โ”‚ โ””โ”€โ”€ webgpu/ # Test specs +``` + +## Contributing + +When contributing CTS changes upstream, note that the React Native runtime files are in: +- `src/common/runtime/rn/loader.ts` +- `src/common/runtime/rn/runtime.ts` +- `src/common/runtime/rn/index.ts`