diff --git a/src/lab/components/EvidenceGallery.tsx b/src/lab/components/EvidenceGallery.tsx
new file mode 100644
index 0000000..d84fb59
--- /dev/null
+++ b/src/lab/components/EvidenceGallery.tsx
@@ -0,0 +1,30 @@
+import type { EvidenceItem } from '../types';
+
+type EvidenceGalleryProps = {
+ evidence: EvidenceItem[];
+ title?: string;
+};
+
+export default function EvidenceGallery({ evidence, title = 'Evidence' }: EvidenceGalleryProps) {
+ return (
+
+ {title}
+
+ {evidence.map((item) => (
+
+ {item.title}
+ {item.description && {item.description}
}
+
+ {item.caption}
+
+ ))}
+
+
+ );
+}
diff --git a/src/lab/components/ExperimentHeader.tsx b/src/lab/components/ExperimentHeader.tsx
new file mode 100644
index 0000000..7096306
--- /dev/null
+++ b/src/lab/components/ExperimentHeader.tsx
@@ -0,0 +1,14 @@
+import type { LabExperiment } from '../types';
+
+type ExperimentHeaderProps = Pick
;
+
+export default function ExperimentHeader({ title, subtitle, lastUpdated, context }: ExperimentHeaderProps) {
+ return (
+
+ );
+}
diff --git a/src/lab/components/ExperimentNav.tsx b/src/lab/components/ExperimentNav.tsx
new file mode 100644
index 0000000..90ffc5d
--- /dev/null
+++ b/src/lab/components/ExperimentNav.tsx
@@ -0,0 +1,11 @@
+import { Link } from 'react-router-dom';
+
+export default function ExperimentNav() {
+ return (
+
+
+ ← Back to Lab
+
+
+ );
+}
diff --git a/src/lab/components/MetricsSummary.tsx b/src/lab/components/MetricsSummary.tsx
new file mode 100644
index 0000000..b11779c
--- /dev/null
+++ b/src/lab/components/MetricsSummary.tsx
@@ -0,0 +1,31 @@
+import type { LabMetricsSummary } from '../types';
+
+type MetricsSummaryProps = {
+ metrics: LabMetricsSummary;
+};
+
+export default function MetricsSummary({ metrics }: MetricsSummaryProps) {
+ return (
+
+
+
Metrics Summary
+ {metrics.headline && {metrics.headline}}
+
+
+
+ {metrics.rows.map((row) => (
+
+
{row.label}
+
{row.value}
+ {row.hint &&
{row.hint}
}
+
+ ))}
+
+
+
+ Bottleneck: {metrics.bottleneck}
+ {metrics.bottleneckDetail ? ` — ${metrics.bottleneckDetail}` : ''}
+
+
+ );
+}
diff --git a/src/lab/components/TechnicalTakeaways.tsx b/src/lab/components/TechnicalTakeaways.tsx
new file mode 100644
index 0000000..92f01d0
--- /dev/null
+++ b/src/lab/components/TechnicalTakeaways.tsx
@@ -0,0 +1,16 @@
+type TechnicalTakeawaysProps = {
+ takeaways: string[];
+};
+
+export default function TechnicalTakeaways({ takeaways }: TechnicalTakeawaysProps) {
+ return (
+
+ Technical Takeaways
+
+ {takeaways.slice(0, 5).map((takeaway) => (
+ - {takeaway}
+ ))}
+
+
+ );
+}
diff --git a/src/lab/data/experiments.ts b/src/lab/data/experiments.ts
new file mode 100644
index 0000000..e442158
--- /dev/null
+++ b/src/lab/data/experiments.ts
@@ -0,0 +1,50 @@
+import type { LabExperiment } from '../types';
+
+export const labExperiments: LabExperiment[] = [
+ {
+ slug: 'overdraw',
+ title: 'Overdraw Stress Test',
+ subtitle: 'Transparent Stacking',
+ lastUpdated: '2026-02-25',
+ context: 'Controlled transparency stacking to quantify overdraw cost under XR-style fragment pressure.',
+ metrics: {
+ headline: 'RenderLoop (GPU)',
+ rows: [
+ { label: 'Baseline Render', value: '6.37 ms' },
+ { label: 'Overdraw Render', value: '13.64 ms' },
+ { label: 'Delta', value: '+7.27 ms', hint: 'CPU stable ~0.5 ms' }
+ ],
+ bottleneck: 'GPU-bound',
+ bottleneckDetail: 'Fragment / overdraw (alpha blending)'
+ },
+ evidence: [
+ {
+ title: 'Unity Profiler Comparison',
+ description: 'Baseline and stress comparison under identical scene conditions.',
+ imagePath: '/lab/Overdraw_CPU_RenderThread_Comparison.png',
+ caption: 'Baseline render at 6.37 ms versus overdraw render at 13.64 ms with CPU stable near 0.5 ms.'
+ },
+ {
+ title: 'Frame Debugger Transparent Pass',
+ description: 'Transparent pass breakdown during stacked alpha rendering.',
+ imagePath: '/lab/Overdraw_FrameDebugger_TransparentPass.png',
+ caption: 'Transparent pass cost rises sharply as layered alpha surfaces multiply fragment processing.'
+ },
+ {
+ title: 'Experiment Summary Capture',
+ description: 'Consolidated capture of profiler and debugging evidence.',
+ imagePath: '/lab/Overdraw_Experiment_Summary.png',
+ caption: 'Summary view confirms the fragment bottleneck signature under controlled overdraw stress.'
+ }
+ ],
+ takeaways: [
+ 'Transparency reduces early-Z effectiveness; stacked layers multiply fragment work.',
+ 'XR stereo amplifies overdraw cost (per-eye fragment workload).',
+ 'SrcAlpha/OneMinusSrcAlpha blending behaves as a fill-rate multiplier under stacking.',
+ 'Mitigation: reduce transparent layers, convert to opaque/cutout, constrain stacking.'
+ ]
+ }
+];
+
+export const getExperimentBySlug = (slug: string): LabExperiment | undefined =>
+ labExperiments.find((experiment) => experiment.slug === slug);
diff --git a/src/lab/pages/LabExperimentPage.tsx b/src/lab/pages/LabExperimentPage.tsx
new file mode 100644
index 0000000..cef540e
--- /dev/null
+++ b/src/lab/pages/LabExperimentPage.tsx
@@ -0,0 +1,35 @@
+import { Navigate, useParams } from 'react-router-dom';
+import Navbar from '../../components/Navbar';
+import EvidenceGallery from '../components/EvidenceGallery';
+import ExperimentHeader from '../components/ExperimentHeader';
+import ExperimentNav from '../components/ExperimentNav';
+import MetricsSummary from '../components/MetricsSummary';
+import TechnicalTakeaways from '../components/TechnicalTakeaways';
+import { getExperimentBySlug } from '../data/experiments';
+
+export default function LabExperimentPage() {
+ const { slug } = useParams<{ slug: string }>();
+ const experiment = slug ? getExperimentBySlug(slug) : undefined;
+
+ if (!experiment) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/lab/pages/LabIndexPage.tsx b/src/lab/pages/LabIndexPage.tsx
new file mode 100644
index 0000000..b8ab8df
--- /dev/null
+++ b/src/lab/pages/LabIndexPage.tsx
@@ -0,0 +1,51 @@
+import { Link } from 'react-router-dom';
+import Navbar from '../../components/Navbar';
+import ResultsTable from '../../components/ResultsTable';
+import { performanceResults } from '../../data/performanceResults';
+import { labExperiments } from '../data/experiments';
+
+export default function LabIndexPage() {
+ return (
+
+
+
+
+ XR Performance Lab
+
+ Controlled experiment index for profiling bottlenecks in XR rendering pipelines.
+
+
+
+
+ Experiments
+
+ {labExperiments.map((experiment) => (
+
+
+ {experiment.title} ({experiment.subtitle})
+
+
+ Bottleneck: {experiment.metrics.bottleneck}
+ {experiment.metrics.bottleneckDetail ? ` — ${experiment.metrics.bottleneckDetail}` : ''}
+
+ {experiment.lastUpdated &&
Last updated: {experiment.lastUpdated}
}
+
+ ))}
+
+
+
+
+
+
Published Results
+
Aggregated benchmark table for all tracked runs.
+
+
+
+
+
+ );
+}
diff --git a/src/lab/types.ts b/src/lab/types.ts
new file mode 100644
index 0000000..ae52d75
--- /dev/null
+++ b/src/lab/types.ts
@@ -0,0 +1,32 @@
+export type BottleneckType = 'GPU-bound' | 'CPU-bound' | 'Balanced' | 'Unknown';
+
+export type LabMetricRow = {
+ label: string;
+ value: string;
+ hint?: string;
+};
+
+export type LabMetricsSummary = {
+ headline?: string;
+ rows: LabMetricRow[];
+ bottleneck: BottleneckType;
+ bottleneckDetail?: string;
+};
+
+export type EvidenceItem = {
+ title: string;
+ description?: string;
+ imagePath: string;
+ caption: string;
+};
+
+export type LabExperiment = {
+ slug: string;
+ title: string;
+ subtitle: string;
+ lastUpdated?: string;
+ context?: string;
+ metrics: LabMetricsSummary;
+ evidence: EvidenceItem[];
+ takeaways: string[];
+};
diff --git a/src/main.tsx b/src/main.tsx
index ea9e363..6eeea30 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,10 +1,13 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
-
+
+
+
);
diff --git a/src/sections/XRLabSection.tsx b/src/sections/XRLabSection.tsx
index 739c5bb..ccf4b33 100644
--- a/src/sections/XRLabSection.tsx
+++ b/src/sections/XRLabSection.tsx
@@ -1,92 +1,40 @@
-import EvidenceCard from '../components/EvidenceCard';
-import ExperimentAccordion from '../components/ExperimentAccordion';
-import ResultsTable from '../components/ResultsTable';
+import { Link } from 'react-router-dom';
import SectionHeading from '../components/SectionHeading';
-import { performanceResults } from '../data/performanceResults';
-import { xrExperiments, xrLabConfig } from '../data/xrLab';
-
-const evidenceItems = [
- {
- title: 'Unity Profiler (CPU / Main Thread)',
- description: 'Main-thread frame budget and scheduling behavior capture.',
- imagePath: '/lab/Overdraw_CPU_RenderThread_Comparison.png',
- caption:
- 'Baseline vs overdraw comparison. CPU remains ~0.5 ms while render thread increases from 6.37 ms to 13.64 ms under identical scene conditions.'
- },
- {
- title: 'Unity Profiler (GPU / Render Thread)',
- description: 'GPU frame cost and render-thread contribution snapshot.',
- imagePath: '/lab/Overdraw_FrameDebugger_TransparentPass.png',
- caption:
- 'Render-thread dominated under overdraw stress. Transparent stacking doubled fragment workload, increasing GPU cost by ~7 ms.'
- },
- {
- title: 'Frame Debugger / RenderDoc Capture',
- description: 'Pass-level evidence for overdraw and submission behavior.',
- imagePath: '/lab/Overdraw_Experiment_Summary.png',
- caption:
- '201 sequential transparent draw calls (ZWrite Off). Fragment workload scales linearly with stacked layers.'
- }
-];
+import { labExperiments } from '../lab/data/experiments';
export default function XRLabSection() {
- const lastUpdated = new Date().toLocaleDateString();
-
return (