11"use client" ;
22
33import { useEffect , useRef , useState } from "react" ;
4+ import { Terminal , X } from "lucide-react" ;
45
56export function WebGLShader ( ) {
67 const canvasRef = useRef < HTMLCanvasElement > ( null ) ;
78 const animationRef = useRef < number | null > ( null ) ;
89 const lastFrameTimeRef = useRef < number > ( 0 ) ;
10+ const framesRenderedRef = useRef < number > ( 0 ) ;
11+ const lastFpsCalculateTimeRef = useRef < number > ( 0 ) ;
12+
913 const [ isVisible , setIsVisible ] = useState ( true ) ;
1014 const [ prefersReducedMotion , setPrefersReducedMotion ] = useState ( false ) ;
1115 const [ forceStaticFallback , setForceStaticFallback ] = useState ( false ) ;
12- const fpsRef = useRef < number > ( 60 ) ;
16+ const [ showStats , setShowStats ] = useState ( false ) ;
17+
18+ // Stats state
19+ const [ stats , setStats ] = useState ( {
20+ fps : 0 ,
21+ targetFps : "Uncapped (Native)" ,
22+ dprMultiplier : 1.5 ,
23+ actualDpr : 1 ,
24+ canvasWidth : 0 ,
25+ canvasHeight : 0 ,
26+ concurrency : 4 ,
27+ memory : 4 ,
28+ renderer : "Unknown" ,
29+ vendor : "Unknown" ,
30+ degraded : false ,
31+ } ) ;
32+
33+ const fpsRef = useRef < number > ( 0 ) ; // 0 means uncapped
1334 const frameDropsRef = useRef < number > ( 0 ) ;
1435
1536 useEffect ( ( ) => {
@@ -47,18 +68,20 @@ export function WebGLShader() {
4768
4869 // --- Hardware Feature Detection ---
4970 let dprMultiplier = 1.5 ;
71+ let concurrency = 4 ;
72+ let memory = 4 ;
73+
5074 if ( typeof navigator !== "undefined" ) {
51- const concurrency = navigator . hardwareConcurrency || 4 ;
75+ concurrency = navigator . hardwareConcurrency || 4 ;
5276 // @ts -expect-error deviceMemory is non-standard but widely supported in Chromium
53- const memory = navigator . deviceMemory || 4 ;
77+ memory = navigator . deviceMemory || 4 ;
5478
5579 if ( concurrency <= 2 || memory <= 2 ) {
5680 // Extremely low end device, skip WebGL entirely to save battery and avoid panics
5781 setForceStaticFallback ( true ) ;
5882 return ;
5983 } else if ( concurrency <= 4 || memory <= 4 ) {
6084 // Lower end device, start with conservative defaults
61- fpsRef . current = 30 ;
6285 dprMultiplier = 1.0 ;
6386 }
6487 }
@@ -79,7 +102,11 @@ export function WebGLShader() {
79102 return ;
80103 }
81104
82- const pixelRatio = Math . min ( window . devicePixelRatio , dprMultiplier ) ;
105+ const rendererDebugInfo = gl . getExtension ( "WEBGL_debug_renderer_info" ) ;
106+ const renderer = rendererDebugInfo ? gl . getParameter ( rendererDebugInfo . UNMASKED_RENDERER_WEBGL ) : gl . getParameter ( gl . RENDERER ) ;
107+ const vendor = rendererDebugInfo ? gl . getParameter ( rendererDebugInfo . UNMASKED_VENDOR_WEBGL ) : gl . getParameter ( gl . VENDOR ) ;
108+
109+ let currentPixelRatio = Math . min ( window . devicePixelRatio , dprMultiplier ) ;
83110
84111 const vertexShaderSource = `
85112 attribute vec2 position;
@@ -173,18 +200,38 @@ export function WebGLShader() {
173200
174201 let time = 0 ;
175202
203+ // Stats update helper
204+ const updateStats = ( w : number , h : number , isDegraded = false ) => {
205+ setStats ( s => ( {
206+ ...s ,
207+ canvasWidth : w ,
208+ canvasHeight : h ,
209+ concurrency,
210+ memory,
211+ renderer,
212+ vendor,
213+ dprMultiplier,
214+ actualDpr : currentPixelRatio ,
215+ degraded : s . degraded || isDegraded ,
216+ targetFps : fpsRef . current === 0 ? "Uncapped (Native)" : fpsRef . current . toString ( )
217+ } ) ) ;
218+ }
219+
176220 const resize = ( ) => {
177221 const width = window . innerWidth ;
178222 const height = window . innerHeight ;
179- canvas . width = width * pixelRatio ;
180- canvas . height = height * pixelRatio ;
223+ canvas . width = width * currentPixelRatio ;
224+ canvas . height = height * currentPixelRatio ;
181225 canvas . style . width = `${ width } px` ;
182226 canvas . style . height = `${ height } px` ;
183227 gl . viewport ( 0 , 0 , canvas . width , canvas . height ) ;
184228 gl . uniform2f ( resolutionLocation , canvas . width , canvas . height ) ;
229+
230+ updateStats ( canvas . width , canvas . height ) ;
185231 } ;
186232
187233 resize ( ) ;
234+ lastFpsCalculateTimeRef . current = performance . now ( ) ;
188235
189236 // Debounced resize handler
190237 let resizeTimeout : NodeJS . Timeout ;
@@ -198,31 +245,42 @@ export function WebGLShader() {
198245 const animate = ( currentTime : number ) => {
199246 animationRef . current = requestAnimationFrame ( animate ) ;
200247
201- // Skip if not visible or reduced motion is preferred
202248 if ( ! isVisible || prefersReducedMotion ) return ;
203249
204- // Frame rate limiting
205250 const elapsed = currentTime - lastFrameTimeRef . current ;
206- const currentFrameInterval = 1000 / fpsRef . current ;
207-
208- if ( elapsed < currentFrameInterval ) return ;
209-
210- // Dynamic Framerate Scaling (monitor requested framerate vs actual)
211- // Ignore huge jumps (like switching tabs) which might spike the elapsed time
212- if ( elapsed > currentFrameInterval * 1.5 && elapsed < 1000 ) {
213- frameDropsRef . current ++ ;
214- // If we consistently drop frames (e.g. 30 frames dropped), lower the target FPS
215- if ( frameDropsRef . current > 30 && fpsRef . current > 30 ) {
216- fpsRef . current = 30 ;
217- frameDropsRef . current = 0 ;
218- console . warn ( "WebGL Shader: Performance degraded, dynamically scaling target FPS down to 30." ) ;
219- }
251+
252+ // If we have a target FPS (e.g. from downscaling), throttle it
253+ if ( fpsRef . current > 0 ) {
254+ const currentFrameInterval = 1000 / fpsRef . current ;
255+ if ( elapsed < currentFrameInterval ) return ;
256+ lastFrameTimeRef . current = currentTime - ( elapsed % currentFrameInterval ) ;
220257 } else {
221- // Recover frame drop count if it's hitting targets consistently
222- frameDropsRef . current = Math . max ( 0 , frameDropsRef . current - 1 ) ;
258+ lastFrameTimeRef . current = currentTime ;
223259 }
224260
225- lastFrameTimeRef . current = currentTime - ( elapsed % currentFrameInterval ) ;
261+ // Track actual FPS
262+ framesRenderedRef . current ++ ;
263+ if ( currentTime - lastFpsCalculateTimeRef . current >= 1000 ) {
264+ const actualFps = framesRenderedRef . current ;
265+ setStats ( s => ( { ...s , fps : actualFps } ) ) ;
266+ framesRenderedRef . current = 0 ;
267+ lastFpsCalculateTimeRef . current = currentTime ;
268+
269+ // Frame degradation logic
270+ // If native framerate drops terribly, seamlessly reduce resolution scaling
271+ if ( fpsRef . current === 0 && actualFps < 45 && actualFps > 0 ) {
272+ frameDropsRef . current ++ ;
273+ if ( frameDropsRef . current > 3 && currentPixelRatio > 0.5 ) {
274+ currentPixelRatio = Math . max ( 0.5 , currentPixelRatio - 0.5 ) ;
275+ resize ( ) ;
276+ updateStats ( canvas . width , canvas . height , true ) ;
277+ frameDropsRef . current = 0 ;
278+ console . warn ( "WebGL Shader: Performance degraded, dropping pixel multiplier to" , currentPixelRatio ) ;
279+ }
280+ } else if ( actualFps >= 50 ) {
281+ frameDropsRef . current = Math . max ( 0 , frameDropsRef . current - 1 ) ;
282+ }
283+ }
226284
227285 // Slower time progression
228286 time += 0.01 ;
@@ -263,14 +321,52 @@ export function WebGLShader() {
263321 }
264322
265323 return (
266- < canvas
267- ref = { canvasRef }
268- className = "fixed top-0 left-0 z-0 pointer-events-none"
269- style = { {
270- width : "100vw" ,
271- height : "100vh" ,
272- willChange : "auto" ,
273- } }
274- />
324+ < >
325+ < canvas
326+ ref = { canvasRef }
327+ className = "fixed top-0 left-0 z-0 pointer-events-none"
328+ style = { {
329+ width : "100vw" ,
330+ height : "100vh" ,
331+ willChange : "auto" ,
332+ } }
333+ />
334+
335+ { /* Stats for nerds toggle & panel */ }
336+ < div className = "fixed bottom-4 left-4 z-50 flex flex-col items-start gap-2" >
337+ { showStats && (
338+ < div className = "bg-black/80 backdrop-blur-md rounded-xl p-4 text-xs font-mono text-green-400 border border-green-500/30 w-72 shadow-2xl animate-fade-in shadow-black/50" >
339+ < div className = "flex justify-between items-center mb-2 pb-2 border-b border-green-500/20" >
340+ < span className = "font-bold text-green-300 tracking-wider" > STATS FOR NERDS</ span >
341+ < button onClick = { ( ) => setShowStats ( false ) } className = "text-green-500 hover:text-green-300 transition-colors" >
342+ < X className = "w-4 h-4" />
343+ </ button >
344+ </ div >
345+ < div className = "space-y-1" >
346+ < div className = "flex justify-between" > < span > Current FPS:</ span > < span className = "text-white" > { stats . fps } </ span > </ div >
347+ < div className = "flex justify-between" > < span > Target FPS:</ span > < span className = "text-white" > { stats . targetFps } </ span > </ div >
348+ < div className = "flex justify-between" > < span > Degraded Mode:</ span > < span className = { stats . degraded ? "text-red-400 font-bold" : "text-white" } > { stats . degraded ? "YES" : "NO" } </ span > </ div >
349+ < 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 >
350+ < div className = "flex justify-between" > < span > Actual DPR:</ span > < span className = "text-white" > { stats . actualDpr . toFixed ( 2 ) } x</ span > </ div >
351+ < div className = "flex justify-between mt-2 pt-2 border-t border-green-500/20" > < span > Resolution:</ span > < span className = "text-white" > { stats . canvasWidth } x { stats . canvasHeight } </ span > </ div >
352+ < div className = "flex justify-between mt-2 pt-2 border-t border-green-500/20" > < span > Concurrency:</ span > < span className = "text-white" > { stats . concurrency } Cores</ span > </ div >
353+ < div className = "flex justify-between" > < span > Device RAM:</ span > < span className = "text-white" > ~{ stats . memory } GB</ span > </ div >
354+ < div className = "flex flex-col mt-2 pt-2 border-t border-green-500/20" >
355+ < span > GPU Renderer:</ span >
356+ < span className = "text-white truncate opacity-80 mt-0.5" title = { stats . renderer } > { stats . renderer } </ span >
357+ </ div >
358+ </ div >
359+ </ div >
360+ ) }
361+
362+ < button
363+ onClick = { ( ) => setShowStats ( ! showStats ) }
364+ className = "group p-2.5 rounded-full bg-black/40 backdrop-blur-md border border-white/10 hover:bg-black/60 transition-all text-white/50 hover:text-white"
365+ title = "Stats for nerds"
366+ >
367+ < Terminal className = "w-4 h-4" />
368+ </ button >
369+ </ div >
370+ </ >
275371 ) ;
276372}
0 commit comments