Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>GPU Benchmark for Mobile</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary: #6200ee; | |
| --primary-dark: #3700b3; | |
| --secondary: #03dac6; | |
| --background: #121212; | |
| --surface: #1e1e1e; | |
| --error: #cf6679; | |
| --text-primary: rgba(255, 255, 255, 0.87); | |
| --text-secondary: rgba(255, 255, 255, 0.6); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| } | |
| body { | |
| background-color: var(--background); | |
| color: var(--text-primary); | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| padding: 20px; | |
| } | |
| header { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| padding: 20px; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| h1 { | |
| font-size: 2rem; | |
| margin-bottom: 10px; | |
| background: linear-gradient(45deg, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| color: transparent; | |
| } | |
| .subtitle { | |
| color: var(--text-secondary); | |
| font-size: 0.9rem; | |
| } | |
| .card { | |
| background-color: var(--surface); | |
| border-radius: 12px; | |
| padding: 20px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); | |
| } | |
| .test-container { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 15px; | |
| margin-top: 20px; | |
| } | |
| .test-item { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 12px 15px; | |
| background-color: rgba(255, 255, 255, 0.05); | |
| border-radius: 8px; | |
| transition: all 0.3s ease; | |
| } | |
| .test-item:hover { | |
| background-color: rgba(255, 255, 255, 0.1); | |
| } | |
| .test-info { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .test-name { | |
| font-weight: 500; | |
| } | |
| .test-description { | |
| font-size: 0.8rem; | |
| color: var(--text-secondary); | |
| } | |
| .test-result { | |
| font-weight: 600; | |
| } | |
| .progress-container { | |
| width: 100%; | |
| height: 6px; | |
| background-color: rgba(255, 255, 255, 0.1); | |
| border-radius: 3px; | |
| margin-top: 5px; | |
| overflow: hidden; | |
| } | |
| .progress-bar { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--primary), var(--secondary)); | |
| border-radius: 3px; | |
| width: 0%; | |
| transition: width 0.5s ease; | |
| } | |
| button { | |
| background-color: var(--primary); | |
| color: white; | |
| border: none; | |
| padding: 12px 24px; | |
| border-radius: 25px; | |
| font-size: 1rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 10px; | |
| width: 100%; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 2px 10px rgba(98, 0, 238, 0.3); | |
| } | |
| button:hover { | |
| background-color: var(--primary-dark); | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 15px rgba(98, 0, 238, 0.4); | |
| } | |
| button:disabled { | |
| background-color: rgba(98, 0, 238, 0.5); | |
| cursor: not-allowed; | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| .result-container { | |
| margin-top: 30px; | |
| opacity: 0; | |
| transition: opacity 0.5s ease; | |
| } | |
| .result-container.show { | |
| opacity: 1; | |
| } | |
| .score { | |
| font-size: 3rem; | |
| font-weight: 700; | |
| text-align: center; | |
| margin: 20px 0; | |
| background: linear-gradient(45deg, var(--secondary), var(--primary)); | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| color: transparent; | |
| } | |
| .device-info { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); | |
| gap: 10px; | |
| margin-top: 20px; | |
| } | |
| .info-item { | |
| background-color: rgba(255, 255, 255, 0.05); | |
| padding: 10px; | |
| border-radius: 8px; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .info-label { | |
| font-size: 0.7rem; | |
| color: var(--text-secondary); | |
| margin-bottom: 5px; | |
| } | |
| .info-value { | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| } | |
| .canvas-container { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: -1; | |
| opacity: 0.1; | |
| } | |
| canvas { | |
| display: block; | |
| } | |
| .spinner { | |
| width: 20px; | |
| height: 20px; | |
| border: 3px solid rgba(255, 255, 255, 0.3); | |
| border-radius: 50%; | |
| border-top-color: white; | |
| animation: spin 1s ease-in-out infinite; | |
| display: none; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| footer { | |
| margin-top: auto; | |
| text-align: center; | |
| padding: 20px; | |
| color: var(--text-secondary); | |
| font-size: 0.8rem; | |
| } | |
| .share-buttons { | |
| display: flex; | |
| gap: 10px; | |
| margin-top: 20px; | |
| } | |
| .share-button { | |
| flex: 1; | |
| background-color: rgba(255, 255, 255, 0.1); | |
| color: white; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| } | |
| .share-button:hover { | |
| background-color: rgba(255, 255, 255, 0.2); | |
| } | |
| @media (max-width: 480px) { | |
| h1 { | |
| font-size: 1.5rem; | |
| } | |
| .device-info { | |
| grid-template-columns: 1fr 1fr; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="canvas-container"> | |
| <canvas id="backgroundCanvas"></canvas> | |
| </div> | |
| <header> | |
| <h1>Mobile GPU Benchmark</h1> | |
| <p class="subtitle">Test your device's WebGL performance in the browser</p> | |
| </header> | |
| <main> | |
| <div class="card"> | |
| <div class="test-container"> | |
| <div class="test-item"> | |
| <div class="test-info"> | |
| <span class="test-name">WebGL Support</span> | |
| <span class="test-description">Checking WebGL availability</span> | |
| <div class="progress-container"> | |
| <div class="progress-bar" id="webgl-progress"></div> | |
| </div> | |
| </div> | |
| <span class="test-result" id="webgl-result">-</span> | |
| </div> | |
| <div class="test-item"> | |
| <div class="test-info"> | |
| <span class="test-name">Shader Complexity</span> | |
| <span class="test-description">Testing fragment shader performance</span> | |
| <div class="progress-container"> | |
| <div class="progress-bar" id="shader-progress"></div> | |
| </div> | |
| </div> | |
| <span class="test-result" id="shader-result">-</span> | |
| </div> | |
| <div class="test-item"> | |
| <div class="test-info"> | |
| <span class="test-name">Texture Handling</span> | |
| <span class="test-description">Testing texture rendering capabilities</span> | |
| <div class="progress-container"> | |
| <div class="progress-bar" id="texture-progress"></div> | |
| </div> | |
| </div> | |
| <span class="test-result" id="texture-result">-</span> | |
| </div> | |
| <div class="test-item"> | |
| <div class="test-info"> | |
| <span class="test-name">Geometry Processing</span> | |
| <span class="test-description">Testing vertex processing speed</span> | |
| <div class="progress-container"> | |
| <div class="progress-bar" id="geometry-progress"></div> | |
| </div> | |
| </div> | |
| <span class="test-result" id="geometry-result">-</span> | |
| </div> | |
| <div class="test-item"> | |
| <div class="test-info"> | |
| <span class="test-name">Compute Performance</span> | |
| <span class="test-description">Testing general computational power</span> | |
| <div class="progress-container"> | |
| <div class="progress-bar" id="compute-progress"></div> | |
| </div> | |
| </div> | |
| <span class="test-result" id="compute-result">-</span> | |
| </div> | |
| </div> | |
| <button id="runBenchmark"> | |
| <span id="buttonText">Run Benchmark</span> | |
| <div class="spinner" id="spinner"></div> | |
| </button> | |
| </div> | |
| <div class="card result-container" id="resultContainer"> | |
| <h2>Benchmark Results</h2> | |
| <div class="score" id="finalScore">0</div> | |
| <div class="device-info"> | |
| <div class="info-item"> | |
| <span class="info-label">Device Performance</span> | |
| <span class="info-value" id="performance-category">-</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="info-label">Renderer</span> | |
| <span class="info-value" id="renderer-info">-</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="info-label">Resolution</span> | |
| <span class="info-value" id="resolution-info">-</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="info-label">FPS</span> | |
| <span class="info-value" id="fps-info">-</span> | |
| </div> | |
| </div> | |
| <div class="share-buttons"> | |
| <button class="share-button" id="shareButton"> | |
| <i class="fas fa-share-alt"></i> | |
| <span>Share</span> | |
| </button> | |
| <button class="share-button" id="saveButton"> | |
| <i class="fas fa-save"></i> | |
| <span>Save</span> | |
| </button> | |
| </div> | |
| </div> | |
| </main> | |
| <footer> | |
| <p>This benchmark tests your device's WebGL 1.0 capabilities. Results may vary between browsers.</p> | |
| </footer> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Elements | |
| const runButton = document.getElementById('runBenchmark'); | |
| const buttonText = document.getElementById('buttonText'); | |
| const spinner = document.getElementById('spinner'); | |
| const resultContainer = document.getElementById('resultContainer'); | |
| const finalScore = document.getElementById('finalScore'); | |
| // Test result elements | |
| const testResults = { | |
| webgl: document.getElementById('webgl-result'), | |
| shader: document.getElementById('shader-result'), | |
| texture: document.getElementById('texture-result'), | |
| geometry: document.getElementById('geometry-result'), | |
| compute: document.getElementById('compute-result') | |
| }; | |
| // Progress bars | |
| const progressBars = { | |
| webgl: document.getElementById('webgl-progress'), | |
| shader: document.getElementById('shader-progress'), | |
| texture: document.getElementById('texture-progress'), | |
| geometry: document.getElementById('geometry-progress'), | |
| compute: document.getElementById('compute-progress') | |
| }; | |
| // Device info | |
| const performanceCategory = document.getElementById('performance-category'); | |
| const rendererInfo = document.getElementById('renderer-info'); | |
| const resolutionInfo = document.getElementById('resolution-info'); | |
| const fpsInfo = document.getElementById('fps-info'); | |
| // Background animation | |
| const backgroundCanvas = document.getElementById('backgroundCanvas'); | |
| let gl; | |
| // Set canvas size | |
| function resizeCanvas() { | |
| backgroundCanvas.width = window.innerWidth; | |
| backgroundCanvas.height = window.innerHeight; | |
| if (gl) { | |
| gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); | |
| } | |
| } | |
| // Initialize WebGL | |
| function initWebGL() { | |
| try { | |
| gl = backgroundCanvas.getContext('webgl') || backgroundCanvas.getContext('experimental-webgl'); | |
| if (!gl) { | |
| return false; | |
| } | |
| // Simple vertex shader | |
| const vsSource = ` | |
| attribute vec2 aPosition; | |
| void main() { | |
| gl_Position = vec4(aPosition, 0.0, 1.0); | |
| } | |
| `; | |
| // Fragment shader for background animation | |
| const fsSource = ` | |
| precision highp float; | |
| uniform float uTime; | |
| uniform vec2 uResolution; | |
| void main() { | |
| vec2 uv = gl_FragCoord.xy / uResolution.xy; | |
| uv.x *= uResolution.x / uResolution.y; | |
| float time = uTime * 0.3; | |
| // Animated gradient with noise | |
| vec3 col = vec3( | |
| sin(uv.x * 2.0 + time) * 0.5 + 0.5, | |
| cos(uv.y * 3.0 - time * 1.5) * 0.5 + 0.5, | |
| sin((uv.x + uv.y) * 5.0 + time * 2.0) * 0.5 + 0.5 | |
| ); | |
| gl_FragColor = vec4(col * 0.3, 1.0); | |
| } | |
| `; | |
| const vertexShader = gl.createShader(gl.VERTEX_SHADER); | |
| gl.shaderSource(vertexShader, vsSource); | |
| gl.compileShader(vertexShader); | |
| const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); | |
| gl.shaderSource(fragmentShader, fsSource); | |
| gl.compileShader(fragmentShader); | |
| const shaderProgram = gl.createProgram(); | |
| gl.attachShader(shaderProgram, vertexShader); | |
| gl.attachShader(shaderProgram, fragmentShader); | |
| gl.linkProgram(shaderProgram); | |
| gl.useProgram(shaderProgram); | |
| // Create position buffer | |
| const positions = [-1, -1, 1, -1, -1, 1, 1, 1]; | |
| const positionBuffer = gl.createBuffer(); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); | |
| gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); | |
| // Bind position attribute | |
| const aPosition = gl.getAttribLocation(shaderProgram, 'aPosition'); | |
| gl.enableVertexAttribArray(aPosition); | |
| gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0); | |
| // Get uniform locations | |
| const uTime = gl.getUniformLocation(shaderProgram, 'uTime'); | |
| const uResolution = gl.getUniformLocation(shaderProgram, 'uResolution'); | |
| // Animation loop | |
| let startTime = Date.now(); | |
| function animate() { | |
| const time = (Date.now() - startTime) / 1000; | |
| gl.uniform1f(uTime, time); | |
| gl.uniform2f(uResolution, gl.drawingBufferWidth, gl.drawingBufferHeight); | |
| gl.clear(gl.COLOR_BUFFER_BIT); | |
| gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | |
| requestAnimationFrame(animate); | |
| } | |
| animate(); | |
| return true; | |
| } catch (e) { | |
| console.error('WebGL initialization failed:', e); | |
| return false; | |
| } | |
| } | |
| // Initialize on load | |
| window.addEventListener('load', function() { | |
| resizeCanvas(); | |
| // Initial WebGL test | |
| const webglSupported = initWebGL(); | |
| testResults.webgl.textContent = webglSupported ? 'Supported' : 'Not Supported'; | |
| progressBars.webgl.style.width = webglSupported ? '100%' : '0%'; | |
| if (!webglSupported) { | |
| runButton.disabled = true; | |
| buttonText.textContent = 'WebGL Not Supported'; | |
| } | |
| // Set device info | |
| if (webglSupported && gl) { | |
| const debugInfo = gl.getExtension('WEBGL_debug_renderer_info'); | |
| if (debugInfo) { | |
| const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL); | |
| rendererInfo.textContent = renderer.split('/')[0]; // Clean up long strings | |
| } | |
| } | |
| resolutionInfo.textContent = `${window.innerWidth} × ${window.innerHeight}`; | |
| }); | |
| window.addEventListener('resize', function() { | |
| resizeCanvas(); | |
| resolutionInfo.textContent = `${window.innerWidth} × ${window.innerHeight}`; | |
| }); | |
| // Benchmark tests | |
| async function runBenchmark() { | |
| runButton.disabled = true; | |
| buttonText.textContent = 'Running...'; | |
| spinner.style.display = 'block'; | |
| // Reset progress bars and results | |
| for (const key in progressBars) { | |
| progressBars[key].style.width = '0%'; | |
| testResults[key].textContent = '-'; | |
| } | |
| // Wait for initial rendering | |
| await new Promise(resolve => setTimeout(resolve, 300)); | |
| try { | |
| let totalScore = 0; | |
| if (!gl) { | |
| throw new Error('WebGL not available'); | |
| } | |
| // 1. WebGL Support (already checked) | |
| animateProgress(progressBars.webgl, 100, 200); | |
| testResults.webgl.textContent = 'Supported'; | |
| const webglScore = 100; | |
| totalScore += webglScore; | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| // 2. Shader Complexity Test | |
| const shaderScore = await runShaderTest(); | |
| animateProgress(progressBars.shader, shaderScore, 500); | |
| testResults.shader.textContent = `${shaderScore} FPS`; | |
| totalScore += shaderScore; | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| // 3. Texture Handling Test | |
| const textureScore = await runTextureTest(); | |
| animateProgress(progressBars.texture, textureScore, 500); | |
| testResults.texture.textContent = `${textureScore} MB/s`; | |
| totalScore += textureScore; | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| // 4. Geometry Processing Test | |
| const geometryScore = await runGeometryTest(); | |
| animateProgress(progressBars.geometry, geometryScore, 500); | |
| testResults.geometry.textContent = `${geometryScore} K Tris`; | |
| totalScore += geometryScore; | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| // 5. Compute Performance Test | |
| const computeScore = await runComputeTest(); | |
| animateProgress(progressBars.compute, computeScore, 500); | |
| testResults.compute.textContent = `${computeScore} Ops/s`; | |
| totalScore += computeScore; | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| // Calculate final score (normalized) | |
| const normalizedScore = Math.round(totalScore / 4); | |
| finalScore.textContent = normalizedScore; | |
| fpsInfo.textContent = `${shaderScore} FPS`; | |
| // Set performance category | |
| if (normalizedScore >= 80) { | |
| performanceCategory.textContent = 'Excellent'; | |
| } else if (normalizedScore >= 60) { | |
| performanceCategory.textContent = 'Good'; | |
| } else if (normalizedScore >= 40) { | |
| performanceCategory.textContent = 'Average'; | |
| } else if (normalizedScore >= 20) { | |
| performanceCategory.textContent = 'Below Average'; | |
| } else { | |
| performanceCategory.textContent = 'Poor'; | |
| } | |
| // Show results | |
| resultContainer.classList.add('show'); | |
| } catch (error) { | |
| console.error('Benchmark failed:', error); | |
| alert('Benchmark failed: ' + error.message); | |
| } finally { | |
| runButton.disabled = false; | |
| buttonText.textContent = 'Run Again'; | |
| spinner.style.display = 'none'; | |
| } | |
| } | |
| function animateProgress(progressBar, targetPercent, duration) { | |
| const startTime = Date.now(); | |
| const interval = setInterval(() => { | |
| const elapsed = Date.now() - startTime; | |
| const progress = Math.min(elapsed / duration, 1); | |
| const currentWidth = progress * targetPercent; | |
| progressBar.style.width = `${currentWidth}%`; | |
| if (progress === 1) { | |
| clearInterval(interval); | |
| } | |
| }, 16); | |
| } | |
| async function runShaderTest() { | |
| return new Promise(resolve => { | |
| // Measure FPS with a complex shader | |
| const testCanvas = document.createElement('canvas'); | |
| testCanvas.width = 512; | |
| testCanvas.height = 512; | |
| const testGl = testCanvas.getContext('webgl') || testCanvas.getContext('experimental-webgl'); | |
| if (!testGl) { | |
| resolve(0); | |
| return; | |
| } | |
| // Complex fragment shader for testing | |
| const fsSource = ` | |
| precision highp float; | |
| uniform float uTime; | |
| uniform vec2 uResolution; | |
| float noise(vec3 p) { | |
| vec3 i = floor(p); | |
| vec4 a = dot(i, vec3(1., 57., 21.)) + vec4(0., 57., 21., 78.); | |
| vec3 f = cos((p-i)*acos(-1.))*(-.5)+.5; | |
| a = mix(sin(cos(a)*a),sin(cos(1.+a)*(1.+a)), f.x); | |
| a.xy = mix(a.xz, a.yw, f.y); | |
| return mix(a.x, a.y, f.z); | |
| } | |
| float sphere(vec3 p, vec4 spr) { | |
| return length(spr.xyz-p) - spr.w; | |
| } | |
| float scene(vec3 p) { | |
| float d = sphere(p, vec4(0.0, 0.0, 0.0, 1.0)); | |
| for (int i = 0; i < 5; i++) { | |
| float fi = float(i); | |
| d = min(d, sphere(p, vec4( | |
| sin(uTime+fi*2.0)*2.0, | |
| cos(uTime+fi*1.5)*2.0, | |
| sin(uTime+fi*0.9)*2.0, | |
| 0.7 | |
| ))); | |
| } | |
| return d; | |
| } | |
| vec3 getNormal(vec3 p) { | |
| vec3 eps = vec3(0.001, 0.0, 0.0); | |
| return normalize(vec3( | |
| scene(p+eps.xyy) - scene(p-eps.xyy), | |
| scene(p+eps.yxy) - scene(p-eps.yxy), | |
| scene(p+eps.yyx) - scene(p-eps.yyx) | |
| )); | |
| } | |
| void main() { | |
| vec2 uv = gl_FragCoord.xy / uResolution.xy; | |
| uv = uv * 2.0 - 1.0; | |
| uv.x *= uResolution.x / uResolution.y; | |
| vec3 rd = normalize(vec3(uv, 1.0)); | |
| vec3 ro = vec3(0.0, 0.0, -3.0); | |
| vec3 cp = vec3(0.0, 0.0, -2.0); | |
| float t = 0.0; | |
| for (int i = 0; i < 32; i++) { | |
| vec3 p = ro + rd * t; | |
| float d = scene(p); | |
| t += d; | |
| if (d < 0.001 || t > 20.0) break; | |
| } | |
| vec3 p = ro + rd * t; | |
| vec3 n = getNormal(p); | |
| vec3 l = normalize(vec3(1.0, 1.0, 1.0)); | |
| float diff = max(0.0, dot(n, l)); | |
| vec3 col = vec3(diff) + vec3(0.1); | |
| gl_FragColor = vec4(col, 1.0); | |
| } | |
| `; | |
| const vsSource = ` | |
| attribute vec2 aPosition; | |
| void main() { | |
| gl_Position = vec4(aPosition, 0.0, 1.0); | |
| } | |
| `; | |
| const vertexShader = testGl.createShader(testGl.VERTEX_SHADER); | |
| testGl.shaderSource(vertexShader, vsSource); | |
| testGl.compileShader(vertexShader); | |
| const fragmentShader = testGl.createShader(testGl.FRAGMENT_SHADER); | |
| testGl.shaderSource(fragmentShader, fsSource); | |
| testGl.compileShader(fragmentShader); | |
| const shaderProgram = testGl.createProgram(); | |
| testGl.attachShader(shaderProgram, vertexShader); | |
| testGl.attachShader(shaderProgram, fragmentShader); | |
| testGl.linkProgram(shaderProgram); | |
| testGl.useProgram(shaderProgram); | |
| const positions = [-1, -1, 1, -1, -1, 1, 1, 1]; | |
| const positionBuffer = testGl.createBuffer(); | |
| testGl.bindBuffer(testGl.ARRAY_BUFFER, positionBuffer); | |
| testGl.bufferData(testGl.ARRAY_BUFFER, new Float32Array(positions), testGl.STATIC_DRAW); | |
| const aPosition = testGl.getAttribLocation(shaderProgram, 'aPosition'); | |
| testGl.enableVertexAttribArray(aPosition); | |
| testGl.vertexAttribPointer(aPosition, 2, testGl.FLOAT, false, 0, 0); | |
| const uTime = testGl.getUniformLocation(shaderProgram, 'uTime'); | |
| const uResolution = testGl.getUniformLocation(shaderProgram, 'uResolution'); | |
| // Measure FPS for 2 seconds | |
| let frameCount = 0; | |
| let fps = 0; | |
| let startTime = performance.now(); | |
| let lastTime = startTime; | |
| function render() { | |
| const now = performance.now(); | |
| frameCount++; | |
| if (now - startTime >= 2000) { | |
| fps = Math.round(frameCount / ((now - startTime) / 1000)); | |
| resolve(fps); | |
| return; | |
| } | |
| testGl.uniform1f(uTime, (now - startTime) / 1000); | |
| testGl.uniform2f(uResolution, testCanvas.width, testCanvas.height); | |
| testGl.clear(testGl.COLOR_BUFFER_BIT); | |
| testGl.drawArrays(testGl.TRIANGLE_STRIP, 0, 4); | |
| requestAnimationFrame(render); | |
| } | |
| render(); | |
| }); | |
| } | |
| async function runTextureTest() { | |
| return new Promise(resolve => { | |
| const testCanvas = document.createElement('canvas'); | |
| testCanvas.width = 512; | |
| testCanvas.height = 512; | |
| const testGl = testCanvas.getContext('webgl') || testCanvas.getContext('experimental-webgl'); | |
| if (!testGl) { | |
| resolve(0); | |
| return; | |
| } | |
| // Create texture (512x512 RGBA) | |
| const texture = testGl.createTexture(); | |
| testGl.bindTexture(testGl.TEXTURE_2D, texture); | |
| const texSize = 512; | |
| const texData = new Uint8Array(texSize * texSize * 4); | |
| for (let i = 0; i < texSize * texSize * 4; i += 4) { | |
| texData[i] = Math.random() * 255; // R | |
| texData[i+1] = Math.random() * 255; // G | |
| texData[i+2] = Math.random() * 255; // B | |
| texData[i+3] = 255; // A | |
| } | |
| // Upload texture | |
| testGl.texImage2D(testGl.TEXTURE_2D, 0, testGl.RGBA, texSize, texSize, 0, testGl.RGBA, testGl.UNSIGNED_BYTE, texData); | |
| testGl.texParameteri(testGl.TEXTURE_2D, testGl.TEXTURE_MIN_FILTER, testGl.NEAREST); | |
| testGl.texParameteri(testGl.TEXTURE_2D, testGl.TEXTURE_MAG_FILTER, testGl.NEAREST); | |
| // Create simple shader | |
| const vsSource = ` | |
| attribute vec2 aPosition; | |
| attribute vec2 aTexCoord; | |
| varying vec2 vTexCoord; | |
| void main() { | |
| gl_Position = vec4(aPosition, 0.0, 1.0); | |
| vTexCoord = aTexCoord; | |
| } | |
| `; | |
| const fsSource = ` | |
| precision highp float; | |
| varying vec2 vTexCoord; | |
| uniform sampler2D uTexture; | |
| void main() { | |
| gl_FragColor = texture2D(uTexture, vTexCoord); | |
| } | |
| `; | |
| const vertexShader = testGl.createShader(testGl.VERTEX_SHADER); | |
| testGl.shaderSource(vertexShader, vsSource); | |
| testGl.compileShader(vertexShader); | |
| const fragmentShader = testGl.createShader(testGl.FRAGMENT_SHADER); | |
| testGl.shaderSource(fragmentShader, fsSource); | |
| testGl.compileShader(fragmentShader); | |
| const shaderProgram = testGl.createProgram(); | |
| testGl.attachShader(shaderProgram, vertexShader); | |
| testGl.attachShader(shaderProgram, fragmentShader); | |
| testGl.linkProgram(shaderProgram); | |
| testGl.useProgram(shaderProgram); | |
| // Fullscreen quad with tex coords | |
| const positions = [-1, -1, 1, -1, -1, 1, 1, 1]; | |
| const texCoords = [0, 0, 1, 0, 0, 1, 1, 1]; | |
| const positionBuffer = testGl.createBuffer(); | |
| testGl.bindBuffer(testGl.ARRAY_BUFFER, positionBuffer); | |
| testGl.bufferData(testGl.ARRAY_BUFFER, new Float32Array(positions), testGl.STATIC_DRAW); | |
| const texCoordBuffer = testGl.createBuffer(); | |
| testGl.bindBuffer(testGl.ARRAY_BUFFER, texCoordBuffer); | |
| testGl.bufferData(testGl.ARRAY_BUFFER, new Float32Array(texCoords), testGl.STATIC_DRAW); | |
| const aPosition = testGl.getAttribLocation(shaderProgram, 'aPosition'); | |
| testGl.enableVertexAttribArray(aPosition); | |
| testGl.bindBuffer(testGl.ARRAY_BUFFER, positionBuffer); | |
| testGl.vertexAttribPointer(aPosition, 2, testGl.FLOAT, false, 0, 0); | |
| const aTexCoord = testGl.getAttribLocation(shaderProgram, 'aTexCoord'); | |
| testGl.enableVertexAttribArray(aTexCoord); | |
| testGl.bindBuffer(testGl.ARRAY_BUFFER, texCoordBuffer); | |
| testGl.vertexAttribPointer(aTexCoord, 2, testGl.FLOAT, false, 0, 0); | |
| const uTexture = testGl.getUniformLocation(shaderProgram, 'uTexture'); | |
| testGl.uniform1i(uTexture, 0); | |
| // Measure texture rendering speed | |
| let frameCount = 0; | |
| let startTime = performance.now(); | |
| let mbPerSecond = 0; | |
| function render() { | |
| frameCount++; | |
| if (frameCount === 100) { | |
| const duration = (performance.now() - startTime) / 1000; // in seconds | |
| const totalData = 512 * 512 * 4 * frameCount; // bytes | |
| mbPerSecond = Math.round((totalData / (1024 * 1024)) / duration); | |
| resolve(mbPerSecond); | |
| return; | |
| } | |
| // Modify texture to force upload | |
| if (frameCount % 10 === 0) { | |
| for (let i = 0; i < texSize * texSize * 4; i += 4) { | |
| texData[i] = Math.random() * 255; | |
| texData[i+1] = Math.random() * 255; | |
| texData[i+2] = Math.random() * 255; | |
| } | |
| testGl.texSubImage2D(testGl.TEXTURE_2D, 0, 0, 0, texSize, texSize, testGl.RGBA, testGl.UNSIGNED_BYTE, texData); | |
| } | |
| testGl.clear(testGl.COLOR_BUFFER_BIT); | |
| testGl.drawArrays(testGl.TRIANGLE_STRIP, 0, 4); | |
| requestAnimationFrame(render); | |
| } | |
| render(); | |
| }); | |
| } | |
| async function runGeometryTest() { | |
| return new Promise(resolve => { | |
| const testCanvas = document.createElement('canvas'); | |
| testCanvas.width = 512; | |
| testCanvas.height = 512; | |
| const testGl = testCanvas.getContext('webgl') || testCanvas.getContext('experimental-webgl'); | |
| if (!testGl) { | |
| resolve(0); | |
| return; | |
| } | |
| // Create simple shader | |
| const vsSource = ` | |
| attribute vec2 aPosition; | |
| void main() { | |
| gl_Position = vec4(aPosition, 0.0, 1.0); | |
| gl_PointSize = 2.0; | |
| } | |
| `; | |
| const fsSource = ` | |
| precision highp float; | |
| void main() { | |
| gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); | |
| } | |
| `; | |
| const vertexShader = testGl.createShader(testGl.VERTEX_SHADER); | |
| testGl.shaderSource(vertexShader, vsSource); | |
| testGl.compileShader(vertexShader); | |
| const fragmentShader = testGl.createShader(testGl.FRAGMENT_SHADER); | |
| testGl.shaderSource(fragmentShader, fsSource); | |
| testGl.compileShader(fragmentShader); | |
| const shaderProgram = testGl.createProgram(); | |
| testGl.attachShader(shaderProgram, vertexShader); | |
| testGl.attachShader(shaderProgram, fragmentShader); | |
| testGl.linkProgram(shaderProgram); | |
| testGl.useProgram(shaderProgram); | |
| // Create position buffer (will be updated each frame) | |
| const positionBuffer = testGl.createBuffer(); | |
| testGl.bindBuffer(testGl.ARRAY_BUFFER, positionBuffer); | |
| const aPosition = testGl.getAttribLocation(shaderProgram, 'aPosition'); | |
| testGl.enableVertexAttribArray(aPosition); | |
| testGl.vertexAttribPointer(aPosition, 2, testGl.FLOAT, false, 0, 0); | |
| // Measure geometry processing speed | |
| let maxTriangles = 0; | |
| let currentCount = 1000; // start with 1k triangles | |
| let startTime = performance.now(); | |
| let lastStableCount = 0; | |
| let stableFrames = 0; | |
| function render() { | |
| // Generate triangles (each triangle has 3 points) | |
| const positions = new Float32Array(currentCount * 3 * 2); | |
| for (let i = 0; i < currentCount * 3 * 2; i += 2) { | |
| positions[i] = Math.random() * 2 - 1; | |
| positions[i+1] = Math.random() * 2 - 1; | |
| } | |
| testGl.bufferData(testGl.ARRAY_BUFFER, positions, testGl.DYNAMIC_DRAW); | |
| testGl.clear(testGl.COLOR_BUFFER_BIT); | |
| testGl.drawArrays(testGl.TRIANGLES, 0, currentCount * 3); | |
| // Check if we can increase the count | |
| const frameTime = performance.now() - startTime; | |
| if (frameTime < 16) { // ~60fps | |
| currentCount *= 1.2; | |
| stableFrames = 0; | |
| } else if (frameTime < 33) { // ~30fps | |
| stableFrames++; | |
| if (stableFrames > 5) { | |
| lastStableCount = Math.round(currentCount); | |
| resolve(Math.round(lastStableCount / 1000)); // return in thousands | |
| return; | |
| } | |
| currentCount *= 1.05; | |
| } else { // below 30fps | |
| lastStableCount = Math.round(currentCount * 0.8); | |
| resolve(Math.round(lastStableCount / 1000)); // return in thousands | |
| return; | |
| } | |
| startTime = performance.now(); | |
| requestAnimationFrame(render); | |
| } | |
| render(); | |
| }); | |
| } | |
| async function runComputeTest() { | |
| return new Promise(resolve => { | |
| // This test uses WebGL to simulate general computation (like GPGPU) | |
| const testCanvas = document.createElement('canvas'); | |
| testCanvas.width = 256; | |
| testCanvas.height = 256; | |
| const testGl = testCanvas.getContext('webgl') || testCanvas.getContext('experimental-webgl'); | |
| if (!testGl) { | |
| resolve(0); | |
| return; | |
| } | |
| // Create texture for computation | |
| const texture = testGl.createTexture(); | |
| testGl.bindTexture(testGl.TEXTURE_2D, texture); | |
| testGl.texParameteri(testGl.TEXTURE_2D, testGl.TEXTURE_MIN_FILTER, testGl.NEAREST); | |
| testGl.texParameteri(testGl.TEXTURE_2D, testGl.TEXTURE_MAG_FILTER, testGl.NEAREST); | |
| testGl.texParameteri(testGl.TEXTURE_2D, testGl.TEXTURE_WRAP_S, testGl.CLAMP_TO_EDGE); | |
| testGl.texParameteri(testGl.TEXTURE_2D, testGl.TEXTURE_WRAP_T, testGl.CLAMP_TO_EDGE); | |
| const texSize = 256; | |
| testGl.texImage2D(testGl.TEXTURE_2D, 0, testGl.RGBA, texSize, texSize, 0, testGl.RGBA, testGl.UNSIGNED_BYTE, null); | |
| // Create framebuffer | |
| const framebuffer = testGl.createFramebuffer(); | |
| testGl.bindFramebuffer(testGl.FRAMEBUFFER, framebuffer); | |
| testGl.framebufferTexture2D(testGl.FRAMEBUFFER, testGl.COLOR_ATTACHMENT0, testGl.TEXTURE_2D, texture, 0); | |
| // Compute shader (simulates many operations) | |
| const fsSource = ` | |
| precision highp float; | |
| uniform vec2 uResolution; | |
| uniform float uTime; | |
| uniform int uIterations; | |
| void main() { | |
| vec2 uv = gl_FragCoord.xy / uResolution.xy; | |
| // Complex computation simulation | |
| vec2 p = uv * 2.0 - 1.0; | |
| float v = 0.0; | |
| for (int i = 0; i < 1024; i++) { | |
| if (i >= uIterations) break; | |
| float fi = float(i); | |
| v += 0.01 * sin( | |
| p.x * sin(uTime * 0.2 + fi * 0.1) * 50.0 + | |
| p.y * sin(uTime * 0.3 + fi * 0.2) * 50.0 + | |
| uTime * 2.0 | |
| ); | |
| p = vec2( | |
| sin(v) + p.x + cos(uTime + fi * 0.3), | |
| cos(v) + p.y + sin(uTime + fi * 0.4) | |
| ); | |
| } | |
| gl_FragColor = vec4(vec3(v), 1.0); | |
| } | |
| `; | |
| const vsSource = ` | |
| attribute vec2 aPosition; | |
| void main() { | |
| gl_Position = vec4(aPosition, 0.0, 1.0); | |
| } | |
| `; | |
| const vertexShader = testGl.createShader(testGl.VERTEX_SHADER); | |
| testGl.shaderSource(vertexShader, vsSource); | |
| testGl.compileShader(vertexShader); | |
| const fragmentShader = testGl.createShader(testGl.FRAGMENT_SHADER); | |
| testGl.shaderSource(fragmentShader, fsSource); | |
| testGl.compileShader(fragmentShader); | |
| const shaderProgram = testGl.createProgram(); | |
| testGl.attachShader(shaderProgram, vertexShader); | |
| testGl.attachShader(shaderProgram, fragmentShader); | |
| testGl.linkProgram(shaderProgram); | |
| testGl.useProgram(shaderProgram); | |
| const positions = [-1, -1, 1, -1, -1, 1, 1, 1]; | |
| const positionBuffer = testGl.createBuffer(); | |
| testGl.bindBuffer(testGl.ARRAY_BUFFER, positionBuffer); | |
| testGl.bufferData(testGl.ARRAY_BUFFER, new Float32Array(positions), testGl.STATIC_DRAW); | |
| const aPosition = testGl.getAttribLocation(shaderProgram, 'aPosition'); | |
| testGl.enableVertexAttribArray(aPosition); | |
| testGl.vertexAttribPointer(aPosition, 2, testGl.FLOAT, false, 0, 0); | |
| const uResolution = testGl.getUniformLocation(shaderProgram, 'uResolution'); | |
| const uTime = testGl.getUniformLocation(shaderProgram, 'uTime'); | |
| const uIterations = testGl.getUniformLocation(shaderProgram, 'uIterations'); | |
| // Find maximum iterations that can run at 60fps | |
| let iterations = 10; | |
| let lastStable = 0; | |
| let startTime = performance.now(); | |
| let opsPerSecond = 0; | |
| function render() { | |
| testGl.uniform2f(uResolution, texSize, texSize); | |
| testGl.uniform1f(uTime, (performance.now() - startTime) / 1000); | |
| testGl.uniform1i(uIterations, iterations); | |
| testGl.viewport(0, 0, texSize, texSize); | |
| testGl.clear(testGl.COLOR_BUFFER_BIT); | |
| testGl.drawArrays(testGl.TRIANGLE_STRIP, 0, 4); | |
| // Read back result to ensure computation happens | |
| const pixels = new Uint8Array(4); | |
| testGl.readPixels(0, 0, 1, 1, testGl.RGBA, testGl.UNSIGNED_BYTE, pixels); | |
| // Measure frame time | |
| const frameTime = performance.now() - startTime; | |
| if (frameTime < 16) { // ~60fps | |
| iterations = Math.floor(iterations * 1.3); | |
| } else { | |
| // Estimate ops per second | |
| opsPerSecond = Math.round(iterations * texSize * texSize * 60); | |
| resolve(opsPerSecond / 1000000); // return in millions | |
| return; | |
| } | |
| startTime = performance.now(); | |
| requestAnimationFrame(render); | |
| } | |
| render(); | |
| }); | |
| } | |
| // Event listeners | |
| runButton.addEventListener('click', runBenchmark); | |
| // Share button | |
| document.getElementById('shareButton').addEventListener('click', function() { | |
| if (navigator.share) { | |
| navigator.share({ | |
| title: 'My GPU Benchmark Results', | |
| text: `I scored ${finalScore.textContent} on the Mobile GPU Benchmark!`, | |
| url: window.location.href | |
| }).catch(err => { | |
| console.log('Error sharing:', err); | |
| fallbackShare(); | |
| }); | |
| } else { | |
| fallbackShare(); | |
| } | |
| }); | |
| function fallbackShare() { | |
| const text = `My GPU Benchmark Score: ${finalScore.textContent}\n\nTest your device at: ${window.location.href}`; | |
| if (navigator.clipboard) { | |
| navigator.clipboard.writeText(text).then(() => { | |
| alert('Results copied to clipboard!'); | |
| }).catch(err => { | |
| prompt('Copy to clipboard:', text); | |
| }); | |
| } else { | |
| prompt('Copy to clipboard:', text); | |
| } | |
| } | |
| // Save button | |
| document.getElementById('saveButton').addEventListener('click', function() { | |
| alert('In a real app, this would save your results to local storage or export them.'); | |
| }); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: absolute; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">This website has been generated by <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body> | |
| </html> |