Skip to content

Commit 2f07a62

Browse files
committed
feat: add hardware-aware WebGL shader component with performance monitoring and inject build-time git information into environment variables.
1 parent 7a99b3f commit 2f07a62

2 files changed

Lines changed: 214 additions & 14 deletions

File tree

components/ui/web-gl-shader.tsx

Lines changed: 188 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ export function WebGLShader() {
99
const lastFrameTimeRef = useRef<number>(0);
1010
const framesRenderedRef = useRef<number>(0);
1111
const lastFpsCalculateTimeRef = useRef<number>(0);
12+
const totalFramesRef = useRef<number>(0);
13+
const degradationEventsRef = useRef<number>(0);
14+
const shaderStartTimeRef = useRef<number>(0);
15+
const peakFpsRef = useRef<number>(0);
16+
const minFpsRef = useRef<number>(Infinity);
1217

1318
const [isVisible, setIsVisible] = useState(true);
1419
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
@@ -17,20 +22,42 @@ export function WebGLShader() {
1722

1823
// Stats state
1924
const [stats, setStats] = useState({
25+
// -- Performance --
2026
fps: 0,
27+
peakFps: 0,
28+
minFps: 0,
2129
targetFps: "Uncapped (Native)",
30+
frameTimeMs: 0,
31+
totalFrames: 0,
32+
uptime: "0s",
33+
// -- Rendering --
2234
dprMultiplier: 1.5,
2335
actualDpr: 1,
36+
nativeDpr: 1,
2437
logicalWidth: 0,
2538
logicalHeight: 0,
2639
canvasWidth: 0,
2740
canvasHeight: 0,
28-
frameTimeMs: 0,
41+
// -- Hardware --
2942
concurrency: 4,
3043
memory: 4,
3144
renderer: "Unknown",
3245
vendor: "Unknown",
46+
platform: "Unknown",
47+
touchPoints: 0,
48+
colorDepth: 24,
49+
// -- WebGL --
50+
webglVersion: "WebGL 1.0",
51+
maxTextureSize: 0,
52+
maxViewportDims: "0x0",
53+
shaderPrecision: "Unknown",
54+
extensionsCount: 0,
55+
// -- Network --
56+
connectionType: "Unknown",
57+
// -- State --
3358
degraded: false,
59+
degradationEvents: 0,
60+
reducedMotion: false,
3461
});
3562

3663
const fpsRef = useRef<number>(0); // 0 means uncapped
@@ -105,9 +132,43 @@ export function WebGLShader() {
105132
return;
106133
}
107134

135+
// --- Collect GPU & WebGL stats ---
108136
const rendererDebugInfo = gl.getExtension("WEBGL_debug_renderer_info");
109137
const renderer = rendererDebugInfo ? gl.getParameter(rendererDebugInfo.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER);
110138
const vendor = rendererDebugInfo ? gl.getParameter(rendererDebugInfo.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR);
139+
const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
140+
const maxViewportDims = gl.getParameter(gl.MAX_VIEWPORT_DIMS);
141+
const extensionsCount = (gl.getSupportedExtensions() || []).length;
142+
143+
// Shader precision
144+
let shaderPrecision = "Unknown";
145+
try {
146+
const hp = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT);
147+
const mp = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT);
148+
if (hp && hp.precision > 0) shaderPrecision = `highp (${hp.precision}-bit)`;
149+
else if (mp && mp.precision > 0) shaderPrecision = `mediump (${mp.precision}-bit)`;
150+
else shaderPrecision = "lowp";
151+
} catch { shaderPrecision = "N/A"; }
152+
153+
// Platform info
154+
const platform = typeof navigator !== "undefined" ? (navigator.platform || "Unknown") : "Unknown";
155+
const touchPoints = typeof navigator !== "undefined" ? (navigator.maxTouchPoints || 0) : 0;
156+
const colorDepth = typeof screen !== "undefined" ? screen.colorDepth : 24;
157+
const nativeDpr = typeof window !== "undefined" ? window.devicePixelRatio : 1;
158+
159+
// Connection info
160+
let connectionType = "Unknown";
161+
if (typeof navigator !== "undefined") {
162+
// @ts-expect-error connection is non-standard
163+
const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
164+
if (conn) {
165+
connectionType = conn.effectiveType || conn.type || "Unknown";
166+
}
167+
}
168+
169+
shaderStartTimeRef.current = performance.now();
170+
peakFpsRef.current = 0;
171+
minFpsRef.current = Infinity;
111172

112173
let currentPixelRatio = Math.min(window.devicePixelRatio, dprMultiplier);
113174

@@ -203,6 +264,14 @@ export function WebGLShader() {
203264

204265
let time = 0;
205266

267+
// Format uptime
268+
const formatUptime = () => {
269+
const elapsed = Math.floor((performance.now() - shaderStartTimeRef.current) / 1000);
270+
const m = Math.floor(elapsed / 60);
271+
const s = elapsed % 60;
272+
return m > 0 ? `${m}m ${s}s` : `${s}s`;
273+
};
274+
206275
// Stats update helper
207276
const updateStats = (w: number, h: number, lw: number, lh: number, isDegraded = false) => {
208277
setStats(s => ({
@@ -215,12 +284,26 @@ export function WebGLShader() {
215284
memory,
216285
renderer,
217286
vendor,
287+
platform,
288+
touchPoints,
289+
colorDepth,
290+
nativeDpr,
218291
dprMultiplier,
219292
actualDpr: currentPixelRatio,
293+
webglVersion: "WebGL 1.0",
294+
maxTextureSize,
295+
maxViewportDims: `${maxViewportDims[0]}x${maxViewportDims[1]}`,
296+
shaderPrecision,
297+
extensionsCount,
298+
connectionType,
220299
degraded: s.degraded || isDegraded,
300+
degradationEvents: degradationEventsRef.current,
301+
totalFrames: totalFramesRef.current,
302+
uptime: formatUptime(),
303+
reducedMotion: prefersReducedMotion,
221304
targetFps: fpsRef.current === 0 ? "Uncapped (Native)" : fpsRef.current.toString()
222305
}));
223-
}
306+
};
224307

225308
const resize = () => {
226309
const width = window.innerWidth;
@@ -265,18 +348,30 @@ export function WebGLShader() {
265348

266349
// Track actual FPS
267350
framesRenderedRef.current++;
351+
totalFramesRef.current++;
268352
if (currentTime - lastFpsCalculateTimeRef.current >= 1000) {
269353
const actualFps = framesRenderedRef.current;
270-
setStats(s => ({ ...s, fps: actualFps, frameTimeMs: Number((1000 / Math.max(actualFps, 1)).toFixed(2)) }));
354+
if (actualFps > peakFpsRef.current) peakFpsRef.current = actualFps;
355+
if (actualFps < minFpsRef.current && actualFps > 0) minFpsRef.current = actualFps;
356+
setStats(s => ({
357+
...s,
358+
fps: actualFps,
359+
peakFps: peakFpsRef.current,
360+
minFps: minFpsRef.current === Infinity ? 0 : minFpsRef.current,
361+
frameTimeMs: Number((1000 / Math.max(actualFps, 1)).toFixed(2)),
362+
totalFrames: totalFramesRef.current,
363+
uptime: formatUptime(),
364+
degradationEvents: degradationEventsRef.current,
365+
}));
271366
framesRenderedRef.current = 0;
272367
lastFpsCalculateTimeRef.current = currentTime;
273368

274369
// Frame degradation logic
275-
// If native framerate drops terribly, seamlessly reduce resolution scaling
276370
if (fpsRef.current === 0 && actualFps < 45 && actualFps > 0) {
277371
frameDropsRef.current++;
278372
if (frameDropsRef.current > 3 && currentPixelRatio > 0.5) {
279373
currentPixelRatio = Math.max(0.5, currentPixelRatio - 0.5);
374+
degradationEventsRef.current++;
280375
resize();
281376
updateStats(canvas.width, canvas.height, window.innerWidth, window.innerHeight, true);
282377
frameDropsRef.current = 0;
@@ -340,32 +435,111 @@ export function WebGLShader() {
340435
{/* Stats for nerds toggle & panel */}
341436
<div className="fixed bottom-4 left-4 z-50 flex flex-col items-start gap-2 max-w-[calc(100vw-2rem)] pointer-events-none">
342437
{showStats && (
343-
<div className="bg-black/80 backdrop-blur-md rounded-xl p-3 sm:p-4 text-[10px] sm:text-xs font-mono text-green-400 border border-green-500/30 w-[85vw] sm:w-80 shadow-2xl animate-fade-in shadow-black/50 overflow-hidden pointer-events-auto">
344-
<div className="flex justify-between items-center mb-2 pb-2 border-b border-green-500/20">
438+
<div className="bg-black/80 backdrop-blur-md rounded-xl p-3 sm:p-4 text-[10px] sm:text-xs font-mono text-green-400 border border-green-500/30 w-[85vw] sm:w-80 shadow-2xl animate-fade-in shadow-black/50 pointer-events-auto max-h-[75vh] overflow-y-auto">
439+
<div className="flex justify-between items-center mb-2 pb-2 border-b border-green-500/20 sticky top-0 bg-black/80 backdrop-blur-md -mx-3 sm:-mx-4 px-3 sm:px-4 -mt-3 sm:-mt-4 pt-3 sm:pt-4 z-10">
345440
<span className="font-bold text-green-300 tracking-wider">STATS FOR NERDS</span>
346441
<button onClick={() => setShowStats(false)} className="text-green-500 hover:text-green-300 transition-colors p-1 -mr-1">
347442
<X className="w-4 h-4" />
348443
</button>
349444
</div>
350-
<div className="space-y-1">
445+
446+
{/* Performance Section */}
447+
<div className="space-y-1 mb-2">
448+
<div className="text-green-300/60 uppercase tracking-widest text-[8px] sm:text-[9px] mb-1">Performance</div>
351449
<div className="flex justify-between"><span>Current FPS:</span><span className="text-white">{stats.fps} <span className="text-green-500/50">({stats.frameTimeMs}ms)</span></span></div>
450+
<div className="flex justify-between"><span>Peak FPS:</span><span className="text-white">{stats.peakFps}</span></div>
451+
<div className="flex justify-between"><span>Min FPS:</span><span className="text-white">{stats.minFps}</span></div>
352452
<div className="flex justify-between"><span>Target FPS:</span><span className="text-white">{stats.targetFps}</span></div>
453+
<div className="flex justify-between"><span>Total Frames:</span><span className="text-white">{stats.totalFrames.toLocaleString()}</span></div>
454+
<div className="flex justify-between"><span>Uptime:</span><span className="text-white">{stats.uptime}</span></div>
455+
</div>
456+
457+
{/* Degradation Section */}
458+
<div className="space-y-1 mb-2 pt-2 border-t border-green-500/20">
459+
<div className="text-green-300/60 uppercase tracking-widest text-[8px] sm:text-[9px] mb-1">Health</div>
353460
<div className="flex justify-between"><span>Degraded Mode:</span><span className={stats.degraded ? "text-red-400 font-bold" : "text-white"}>{stats.degraded ? "CRITICAL" : "OK"}</span></div>
461+
<div className="flex justify-between"><span>Degradation Events:</span><span className={stats.degradationEvents > 0 ? "text-yellow-400" : "text-white"}>{stats.degradationEvents}</span></div>
462+
<div className="flex justify-between"><span>Reduced Motion:</span><span className="text-white">{stats.reducedMotion ? "ON" : "OFF"}</span></div>
463+
</div>
354464

355-
<div className="flex justify-between mt-2 pt-2 border-t border-green-500/20"><span>DPR Multiplier:</span><span className="text-white">{stats.dprMultiplier}x</span></div>
465+
{/* Rendering Section */}
466+
<div className="space-y-1 mb-2 pt-2 border-t border-green-500/20">
467+
<div className="text-green-300/60 uppercase tracking-widest text-[8px] sm:text-[9px] mb-1">Rendering</div>
468+
<div className="flex justify-between"><span>Native DPR:</span><span className="text-white">{stats.nativeDpr.toFixed(2)}x</span></div>
469+
<div className="flex justify-between"><span>DPR Multiplier:</span><span className="text-white">{stats.dprMultiplier}x</span></div>
356470
<div className="flex justify-between"><span>Actual DPR:</span><span className="text-white">{stats.actualDpr.toFixed(2)}x</span></div>
357-
358-
<div className="flex justify-between mt-2 pt-2 border-t border-green-500/20"><span>Logical Res:</span><span className="text-white">{stats.logicalWidth}x{stats.logicalHeight}</span></div>
471+
<div className="flex justify-between"><span>Logical Res:</span><span className="text-white">{stats.logicalWidth}x{stats.logicalHeight}</span></div>
359472
<div className="flex justify-between"><span>Render Res:</span><span className="text-white">{Math.round(stats.canvasWidth)}x{Math.round(stats.canvasHeight)}</span></div>
360-
<div className="flex justify-between text-[9px] text-green-500/60 "><span className="mr-4 sm:mr-8 whitespace-nowrap">Pixels Processed:</span><span className="text-right">~{(stats.canvasWidth * stats.canvasHeight).toLocaleString()} px</span></div>
473+
<div className="flex justify-between text-[9px] text-green-500/60"><span className="mr-4 whitespace-nowrap">Pixels/Frame:</span><span className="text-right">~{(stats.canvasWidth * stats.canvasHeight).toLocaleString()} px</span></div>
474+
<div className="flex justify-between"><span>Color Depth:</span><span className="text-white">{stats.colorDepth}-bit</span></div>
475+
</div>
361476

362-
<div className="flex justify-between mt-2 pt-2 border-t border-green-500/20"><span>CPU Threads:</span><span className="text-white">{stats.concurrency}</span></div>
477+
{/* Hardware Section */}
478+
<div className="space-y-1 mb-2 pt-2 border-t border-green-500/20">
479+
<div className="text-green-300/60 uppercase tracking-widest text-[8px] sm:text-[9px] mb-1">Hardware</div>
480+
<div className="flex justify-between"><span>CPU Threads:</span><span className="text-white">{stats.concurrency}</span></div>
363481
<div className="flex justify-between"><span>Device RAM:</span><span className="text-white">~{stats.memory} GB</span></div>
364-
<div className="flex flex-col mt-2 pt-2 border-t border-green-500/20">
365-
<span className="mb-0.5">GPU Subsystem:</span>
482+
<div className="flex justify-between"><span>Platform:</span><span className="text-white">{stats.platform}</span></div>
483+
<div className="flex justify-between"><span>Touch Points:</span><span className="text-white">{stats.touchPoints}</span></div>
484+
<div className="flex flex-col mt-1">
485+
<span className="mb-0.5">GPU Vendor:</span>
486+
<span className="text-white opacity-80 leading-tight break-words text-[9px] sm:text-[10px]">{stats.vendor}</span>
487+
</div>
488+
<div className="flex flex-col mt-1">
489+
<span className="mb-0.5">GPU Renderer:</span>
366490
<span className="text-white opacity-80 leading-tight break-words text-[9px] sm:text-[10px]" title={stats.renderer}>{stats.renderer}</span>
367491
</div>
368492
</div>
493+
494+
{/* WebGL Section */}
495+
<div className="space-y-1 mb-2 pt-2 border-t border-green-500/20">
496+
<div className="text-green-300/60 uppercase tracking-widest text-[8px] sm:text-[9px] mb-1">WebGL Context</div>
497+
<div className="flex justify-between"><span>Version:</span><span className="text-white">{stats.webglVersion}</span></div>
498+
<div className="flex justify-between"><span>Max Tex Size:</span><span className="text-white">{stats.maxTextureSize.toLocaleString()}px</span></div>
499+
<div className="flex justify-between"><span>Max Viewport:</span><span className="text-white">{stats.maxViewportDims}</span></div>
500+
<div className="flex justify-between"><span>Shader Precision:</span><span className="text-white">{stats.shaderPrecision}</span></div>
501+
<div className="flex justify-between"><span>Extensions:</span><span className="text-white">{stats.extensionsCount}</span></div>
502+
</div>
503+
504+
{/* Network Section */}
505+
<div className="space-y-1 mb-2 pt-2 border-t border-green-500/20">
506+
<div className="text-green-300/60 uppercase tracking-widest text-[8px] sm:text-[9px] mb-1">Network</div>
507+
<div className="flex justify-between"><span>Connection:</span><span className="text-white">{stats.connectionType}</span></div>
508+
</div>
509+
510+
{/* Build & Source Section */}
511+
<div className="space-y-1 pt-2 border-t border-green-500/20">
512+
<div className="text-green-300/60 uppercase tracking-widest text-[8px] sm:text-[9px] mb-1">Build & Source</div>
513+
<div className="flex justify-between"><span>Branch:</span><span className="text-white">{process.env.NEXT_PUBLIC_GIT_BRANCH || "unknown"}</span></div>
514+
<div className="flex justify-between items-center">
515+
<span>Commit:</span>
516+
<a
517+
href={`${process.env.NEXT_PUBLIC_REPO_URL || "#"}/commit/${process.env.NEXT_PUBLIC_GIT_COMMIT_HASH || ""}`}
518+
target="_blank"
519+
rel="noopener noreferrer"
520+
className="text-cyan-400 hover:text-cyan-300 underline underline-offset-2 transition-colors"
521+
>
522+
{process.env.NEXT_PUBLIC_GIT_COMMIT_SHORT || "unknown"}
523+
</a>
524+
</div>
525+
<div className="flex flex-col mt-1">
526+
<span className="mb-0.5">Message:</span>
527+
<span className="text-white opacity-80 leading-tight break-words text-[9px] sm:text-[10px]">{process.env.NEXT_PUBLIC_GIT_COMMIT_MESSAGE || "N/A"}</span>
528+
</div>
529+
<div className="flex justify-between"><span>Commit Date:</span><span className="text-white text-[9px]">{process.env.NEXT_PUBLIC_GIT_COMMIT_DATE || "N/A"}</span></div>
530+
<div className="flex justify-between"><span>Build Time:</span><span className="text-white text-[9px]">{process.env.NEXT_PUBLIC_BUILD_TIME || "N/A"}</span></div>
531+
<div className="flex justify-between items-center mt-1">
532+
<span>Source Code:</span>
533+
<a
534+
href={process.env.NEXT_PUBLIC_REPO_URL || "#"}
535+
target="_blank"
536+
rel="noopener noreferrer"
537+
className="text-cyan-400 hover:text-cyan-300 underline underline-offset-2 transition-colors"
538+
>
539+
GitHub ↗
540+
</a>
541+
</div>
542+
</div>
369543
</div>
370544
)}
371545

next.config.mjs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,33 @@
11
import path from "path"
2+
import { execSync } from "child_process"
3+
4+
// Inject git build info at build time
5+
const getGitInfo = () => {
6+
try {
7+
const commitHash = execSync("git rev-parse HEAD").toString().trim();
8+
const commitShort = execSync("git rev-parse --short HEAD").toString().trim();
9+
const branch = execSync("git rev-parse --abbrev-ref HEAD").toString().trim();
10+
const commitMessage = execSync("git log -1 --pretty=%s").toString().trim();
11+
const commitDate = execSync("git log -1 --pretty=%ci").toString().trim();
12+
return { commitHash, commitShort, branch, commitMessage, commitDate };
13+
} catch {
14+
return { commitHash: "unknown", commitShort: "unknown", branch: "unknown", commitMessage: "", commitDate: "" };
15+
}
16+
};
17+
18+
const gitInfo = getGitInfo();
219

320
/** @type {import('next').NextConfig} */
421
const nextConfig = {
22+
env: {
23+
NEXT_PUBLIC_GIT_COMMIT_HASH: gitInfo.commitHash,
24+
NEXT_PUBLIC_GIT_COMMIT_SHORT: gitInfo.commitShort,
25+
NEXT_PUBLIC_GIT_BRANCH: gitInfo.branch,
26+
NEXT_PUBLIC_GIT_COMMIT_MESSAGE: gitInfo.commitMessage,
27+
NEXT_PUBLIC_GIT_COMMIT_DATE: gitInfo.commitDate,
28+
NEXT_PUBLIC_BUILD_TIME: new Date().toISOString(),
29+
NEXT_PUBLIC_REPO_URL: "https://github.com/gobitsnbytes/bitsnbytes",
30+
},
531
typescript: {
632
ignoreBuildErrors: true,
733
},

0 commit comments

Comments
 (0)