Skip to content

Commit 7bb6220

Browse files
committed
feat: implement device capability detection for optimal rendering mode; auto-set rendering mode on initial mount in ModelPreview component
1 parent abfdaf9 commit 7bb6220

File tree

1 file changed

+150
-5
lines changed

1 file changed

+150
-5
lines changed

packages/stlmaps-app/src/components/ModelPreview.tsx

Lines changed: 150 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useRef, useState, useCallback } from "react";
1+
import { useEffect, useRef, useState, useCallback, useMemo } from "react";
22
import { CircularProgress } from "@mui/material";
33
import * as THREE from "three";
44
// @ts-expect-error
@@ -36,15 +36,152 @@ interface SceneData {
3636
isFirstRender?: boolean;
3737
}
3838

39+
/**
40+
* Detects device capabilities and recommends the appropriate rendering mode
41+
*/
42+
const detectDeviceCapabilities = (): 'quality' | 'performance' => {
43+
console.log("Detecting device capabilities for optimal rendering mode");
44+
45+
// Check for mobile devices first - they generally need performance mode
46+
const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
47+
48+
// Get device memory (available in Chrome)
49+
// @ts-expect-error - navigator.deviceMemory is not in TypeScript defs
50+
const deviceMemory = navigator.deviceMemory || 4; // Default to 4GB if not available
51+
52+
// Check for hardware concurrency (CPU cores)
53+
const cpuCores = navigator.hardwareConcurrency || 4; // Default to 4 cores
54+
55+
// Check screen resolution
56+
const pixelCount = window.screen.width * window.screen.height;
57+
const isHighResolution = pixelCount > (1920 * 1080);
58+
59+
// Try to detect GPU performance using canvas
60+
let gpuPowerScore = 0;
61+
try {
62+
const canvas = document.createElement('canvas');
63+
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
64+
65+
if (gl) {
66+
// @ts-expect-error - This property exists on WebGL contexts
67+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
68+
if (debugInfo) {
69+
// @ts-expect-error - This constant exists when the extension is available
70+
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
71+
72+
// Check for keywords indicating powerful GPUs
73+
const knownHighPerformanceGPUs = [
74+
'NVIDIA', 'RTX', 'GTX', 'Quadro',
75+
'AMD', 'Radeon', 'FirePro',
76+
'Intel Iris', 'Apple M1', 'Apple M2', 'Apple M3'
77+
];
78+
79+
for (const gpu of knownHighPerformanceGPUs) {
80+
if (renderer && renderer.indexOf(gpu) !== -1) {
81+
gpuPowerScore += 1;
82+
}
83+
}
84+
85+
// Mobile GPUs typically have 'Mali', 'Adreno', 'PowerVR' in their name
86+
const mobileGPUs = ['Mali', 'Adreno', 'PowerVR', 'Apple GPU'];
87+
for (const gpu of mobileGPUs) {
88+
if (renderer && renderer.indexOf(gpu) !== -1) {
89+
gpuPowerScore -= 1; // Reduce score for mobile GPUs
90+
}
91+
}
92+
93+
console.log("Detected GPU:", renderer);
94+
}
95+
96+
// Check supported extensions as another indicator
97+
const extensions = gl.getSupportedExtensions() || [];
98+
const advancedExtensions = [
99+
'EXT_color_buffer_float',
100+
'OES_texture_float',
101+
'WEBGL_color_buffer_float',
102+
'WEBGL_compressed_texture_s3tc',
103+
'WEBGL_depth_texture'
104+
];
105+
106+
for (const ext of advancedExtensions) {
107+
if (extensions.includes(ext)) {
108+
gpuPowerScore += 0.5;
109+
}
110+
}
111+
112+
// Test draw call performance with a basic benchmark
113+
const startTime = performance.now();
114+
let testIterations = 0;
115+
const testDrawCalls = () => {
116+
gl.clear(gl.COLOR_BUFFER_BIT);
117+
gl.flush();
118+
testIterations++;
119+
120+
if (performance.now() - startTime < 50) { // 50ms test
121+
testDrawCalls();
122+
}
123+
};
124+
testDrawCalls();
125+
126+
// Higher iterations = better performance
127+
if (testIterations > 1000) {
128+
gpuPowerScore += 2;
129+
} else if (testIterations > 500) {
130+
gpuPowerScore += 1;
131+
}
132+
}
133+
} catch (e) {
134+
console.warn("Error during GPU capability detection:", e);
135+
}
136+
137+
// Combine factors to make decision
138+
let useQualityMode = true;
139+
140+
// Score-based decision
141+
let totalScore = 0;
142+
143+
// Device factors
144+
totalScore -= isMobileDevice ? 3 : 0;
145+
totalScore += Math.min(deviceMemory / 2, 4); // Up to 4 points for 8GB+ RAM
146+
totalScore += Math.min(cpuCores / 2, 4); // Up to 4 points for 8+ cores
147+
totalScore -= isHighResolution ? 1 : 0; // High resolution is demanding
148+
totalScore += gpuPowerScore; // Add GPU power score
149+
150+
// Check for battery status if available
151+
// @ts-expect-error - navigator.getBattery is not in all TypeScript defs
152+
if (navigator.getBattery) {
153+
// @ts-expect-error - navigator.getBattery is not in all TypeScript defs
154+
navigator.getBattery().then(battery => {
155+
if (battery.charging === false && battery.level < 0.5) {
156+
// If on battery and below 50%, prefer performance mode
157+
totalScore -= 2;
158+
}
159+
}).catch(() => {
160+
// Ignore errors from battery API
161+
});
162+
}
163+
164+
console.log("Device capability score:", totalScore);
165+
166+
// Set threshold for quality mode
167+
useQualityMode = totalScore >= 4;
168+
169+
const recommendedMode = useQualityMode ? 'quality' : 'performance';
170+
console.log("Recommended rendering mode:", recommendedMode);
171+
172+
return recommendedMode;
173+
};
174+
39175
const ModelPreview = ({}: ModelPreviewProps) => {
40176
// Get geometry data and settings from the Zustand store
41-
const { geometryDataSets, terrainSettings, renderingSettings } = useLayerStore();
177+
const { geometryDataSets, terrainSettings, renderingSettings, setRenderingMode } = useLayerStore();
42178
const containerRef = useRef<HTMLDivElement>(null);
43179
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
44180
const sceneDataRef = useRef<SceneData | null>(null);
45181
const resizeObserverRef = useRef<ResizeObserver | null>(null);
46182
const [loading, setLoading] = useState<boolean>(true);
47183
const [error, setError] = useState<string | null>(null);
184+
const [hasSetInitialMode, setHasSetInitialMode] = useState<boolean>(false);
48185

49186
// Access the current rendering mode
50187
const renderingMode = renderingSettings.mode;
@@ -521,11 +658,19 @@ const ModelPreview = ({}: ModelPreviewProps) => {
521658
if (!containerRef.current) return;
522659

523660
console.log("ModelPreview mounted");
661+
662+
// Auto-detect and set optimal rendering mode on first mount
663+
if (!hasSetInitialMode) {
664+
const detectedMode = detectDeviceCapabilities();
665+
console.log(`Setting initial rendering mode to ${detectedMode} based on device capabilities`);
666+
setRenderingMode(detectedMode);
667+
setHasSetInitialMode(true);
668+
}
524669

525670
// Clear any existing canvases in the container first
526-
containerRef.current.querySelectorAll('button').forEach(canvas => {
527-
console.log("Removing existing BUTTONS before initialization");
528-
canvas.remove();
671+
containerRef.current.querySelectorAll('button').forEach(button => {
672+
console.log("Removing existing buttons before initialization");
673+
button.remove();
529674
});
530675
// Clear any existing canvases in the container first
531676
containerRef.current.querySelectorAll('canvas').forEach(canvas => {

0 commit comments

Comments
 (0)