workload-simulator.html (33018B)
1 <!DOCTYPE html><meta charset="utf-8"> 2 <meta name=viewport content="width=900"> 3 <title>WebGL workload simulator</title> 4 <!-- 5 Copyright (c) 2019 The Khronos Group Inc. 6 Use of this source code is governed by an MIT-style license that can be 7 found in the LICENSE.txt file. 8 --> 9 <style> 10 body { 11 margin: 0; 12 font-family: monospace; 13 user-select: none; 14 -moz-user-select: -moz-none; 15 -webkit-user-select: none; 16 font-size: 22px; 17 text-size-adjust: none; 18 } 19 pre { margin: 0; } 20 .square img { position: relative; } 21 .square { 22 overflow: hidden; 23 border-bottom: 20px solid black; 24 border-right: 20px solid black; 25 display: block; 26 } 27 28 #fpsSpan { font-size: 30px; } 29 #fpsPanel { 30 position: fixed; 31 text-align: right; 32 left: 550px; 33 top: 0px; 34 width: 300px; 35 font-size: 12px; 36 margin: 10px; 37 pointer-events: none; 38 } 39 40 input, label { white-space: pre; pointer-events: auto; } 41 input { margin: 15px 15px; transform: scale(2); } 42 input[type="number"] { width: 50px; } 43 input[type="range"] { 44 width: 100px; 45 margin: 30px 200px; 46 padding: 0px; 47 transform: scale(4); 48 } 49 50 .rotate { animation: rotating 2s linear infinite; } 51 @keyframes rotating { 52 from { transform: scale(2) rotate(0deg); } 53 to { transform: scale(2) rotate(360deg); } 54 } 55 56 .bad { color: red; } 57 .light { color: #CCCCCC; } 58 #warning { 59 color: #FF0000; 60 } 61 </style> 62 63 <canvas id=webglCanvas class=square></canvas> 64 <canvas id=canvas class=square style=display:none;></canvas> 65 <div id=dom class=square style=display:none;background:white> 66 <img id=img style=width:256px;height:256px;background:black></img> 67 </div> 68 <div style=margin:2em;margin-top:0> 69 <div id=warning></div> 70 Drag the WebGL logo.<br> 71 <label for=useGl>Renderer: <input type=radio name=renderer id=useGl checked>WebGL</label> 72 <label for=use2D><input type=radio name=renderer id=use2D>Canvas 2D</label> 73 <label for=useDom><input type=radio name=renderer id=useDom>DOM</label> 74 <br> 75 <label for=animate><input type=checkbox name=animate id=animate>Animate</label> 76 <span id=continuousOptions> 77 <label for=useRaf><input type=radio name=loop id=useRaf checked>requestAnimationFrame</label> 78 <label for=usePostMessage><input type=radio name=loop id=usePostMessage>postMessage</label> 79 <label for=useSetTimeout><input type=radio name=loop id=useSetTimeout>setTimeout</label> 80 <label for=useSetInterval><input type=radio name=loop id=useSetInterval>setInterval</label> 81 <br> 82 <div id=fpsOptions> 83 <input id=fpsSlider type=range min=10 max=240 step=10 value=60> 84 Target FPS: <span id=fpsLabel>60</span> 85 </div> 86 </span> 87 <details id=canvasOptions> 88 <summary>Canvas options</summary> 89 <div id=canvas2DOptions> 90 Canvas size <input type=number id=canvasSize value=512 min=1> pixels<sup>2</sup> 91 <label for=onscreen><input type=radio name=offscreen id=onscreen checked>Regular canvas</label> 92 <label for=offscreen><input type=radio name=offscreen id=offscreen>OffscreenCanvas (on main thread)</label> 93 <!-- <label for=offscreenWorker><input type=radio name=offscreen id=offscreenWorker>OffscreenCanvas on worker</label> --> 94 <br> 95 <label for=transferControlToOffscreen><input type=checkbox id=transferControlToOffscreen checked>Use transferControlToOffscreen</label> 96 </div> 97 </details> 98 <div id=glOptions> 99 <details id=glWork> 100 <summary>WebGL rendering work</summary> 101 <div id=pixelsWrapper> 102 <input type=range id=pixels min=65536 max=65536000 value=65536 step=65536> 103 draw <span id=pixelsLabel>64K</span> pixels per view 104 </div> 105 <input type=range id=drawCalls min=0 max=10000 value=0 step=10> 106 using <span id=drawCallsLabel>1</span> draw call(s) per view 107 <br> 108 Multiply the above slider values by: 109 <label for=x1><input type=radio name=multiplier id=x1 checked>1 </label> 110 <label for=x10><input type=radio name=multiplier id=x10>10 </label> 111 <label for=x100><input type=radio name=multiplier id=x100>100 </label> 112 <br> 113 <input type=range id=uploads min=0 max=256 value=0 step=0.25> 114 <span id=uploadLabel>0.00</span> MB glBufferData 115 <br> 116 <label for=finish><input type=checkbox id=finish>glFinish</label> 117 <br> 118 <label for=readPixels><input type=checkbox id=readPixels>glReadPixels</label> 119 </details> 120 <details id=contextCreation> 121 <summary>WebGL context creation options</summary> 122 <label for=useWebGL2><input type=checkbox id=useWebGL2 checked>Use WebGL 2 if available</label> 123 <label for=antialias><input type=checkbox id=antialias checked>Antialias</label> 124 <label for=alpha><input type=checkbox id=alpha checked>Alpha</label> 125 <label for=depth><input type=checkbox id=depth checked>Depth</label> 126 <label for=stencil><input type=checkbox id=stencil>Stencil</label> 127 <label for=premultipliedAlpha><input type=checkbox id=premultipliedAlpha checked>Premultiplied Alpha</label> 128 <label for=preserveDrawingBuffer><input type=checkbox id=preserveDrawingBuffer>Preserve Drawing Buffer</label> 129 <label for=desynchronized><input type=checkbox id=desynchronized>Desynchronized</label> 130 <br> 131 Power preference 132 <label for=ppDefault><input type=radio name=pp id=ppDefault checked>default</label> 133 <label for=lowPower><input type=radio name=pp id=lowPower>low-power</label> 134 <label for=highPerformance><input type=radio name=pp id=highPerformance>high-performance</label> 135 <br> 136 <label for=separateHighPowerContext><input type=checkbox id=separateHighPowerContext>Activate high power GPU (by creating a separate high power context)</label> 137 </details> 138 <div id=multiviewOptions> 139 <input type=range id=views min=1 max=4 value=0 step=1> 140 Draw <span id=viewsLabel>1</span> views(s) 141 <br> 142 <label for=multiview><input type=checkbox id=multiview>Use OVR_multiview2</label> 143 <label for=multiviewCopy><input type=checkbox id=multiviewCopy checked>Copy multiview draw results to main canvas</label> 144 </div> 145 <pre id=contextVersion> 146 </pre> 147 <details id=contextAttributes> 148 <summary>Context attributes</summary> 149 <pre></pre> 150 </details> 151 <details id=supportedExtensions> 152 <summary>Supported Extensions</summary> 153 </details> 154 </div> 155 <br> 156 <input type=range id=jsWork min=0 max=100 value=0> 157 <span id=jsWorkLabel>0</span> ms extra Javascript work per frame 158 <br> 159 <label for=animation><input type=checkbox id=animation>CSS animation</label> 160 161 <div id=fpsPanel> 162 <label for=showFps><input type=checkbox id=showFps checked>Show FPS</label> 163 <div id=fps> 164 <span id=fpsSpan></span> 165 <p> 166 <label for=showStats><input type=checkbox id=showStats>More info</label> 167 <div id=stats></div> 168 </div> 169 </div> 170 171 <iframe id=highPowerFrame style=display:none></iframe> 172 173 <h2><center>WebGL workload simulator</center></h2> 174 </div> 175 176 <script> 177 'use strict'; 178 // Add all elements with ids as global readonly variables. 179 for (let element of document.documentElement.querySelectorAll('[id]')) 180 Object.defineProperty(this, element.id, {value: element, writeable: false}); 181 182 183 /************\ 184 * Options UI * 185 \************/ 186 187 188 // Set all input elements with values from the query string. 189 const controls = document.querySelectorAll('input, details'); 190 const defaultChecked = {}; 191 const defaultValues = {}; 192 const defaultMaxes = {}; 193 for (const control of controls) { 194 if (!control.id) continue; 195 defaultChecked[control.id] = control.checked; 196 defaultValues[control.id] = control.value; 197 defaultMaxes[control.id] = control.max; 198 const param = window.location.search.match(control.id + '(?:=([^&]*))?'); 199 if (param) { 200 if (control.type == 'radio') 201 control.checked = true; 202 else if (control.type == 'checkbox') 203 control.checked = param[1] != 'false'; 204 else if (control instanceof HTMLDetailsElement) 205 control.open = true; 206 else 207 control.value = param[1]; 208 } 209 control.oninput = updateControls; 210 if (control instanceof HTMLDetailsElement) 211 control.onclick = ()=>setTimeout(updateControls, 0); 212 } 213 // Some controls require a page reload when changed. 214 const reloadControls = ['useWebGL2', 'antialias', 'alpha', 'depth', 'stencil', 'premultipliedAlpha', 'preserveDrawingBuffer', 'desynchronized', 'ppDefault', 'lowPower', 'highPerformance', 'canvasSize', 'onscreen', 'offscreen', 'transferControlToOffscreen'].map(x=>window[x]); 215 for (let control of reloadControls) { 216 control.onchange = ()=>{ 217 updateControls(); 218 callReplaceStateThrottled(); 219 location.reload(); 220 }; 221 } 222 223 separateHighPowerContext.onchange = ()=>{ 224 if (!separateHighPowerContext.checked) 225 highPowerFrame.contentDocument.location.reload(); 226 else { 227 const doc = highPowerFrame.contentDocument; 228 const canvas = doc.createElement('canvas'); 229 doc.body.appendChild(canvas); 230 canvas.getContext('webgl', {powerPreference: 'high-performance'}); 231 } 232 } 233 separateHighPowerContext.onchange(); 234 235 let queryString = window.location.search; 236 let previousQueryString = queryString; 237 let replaceStateScheduled = false; 238 function callReplaceStateThrottled() { 239 replaceStateScheduled = false; 240 if (queryString == previousQueryString) 241 return; 242 previousQueryString = queryString; 243 let path = window.location.pathname; 244 history.replaceState(null, null, queryString == '?' ? path : path + queryString); 245 } 246 const suffixes = ['', 'K', 'M', 'G', 'E'] 247 const divisors = []; 248 for (let i = 0; i < suffixes.length; i++) 249 divisors[i] = Math.pow(10, i * 3); 250 const formatSI = (x) => { 251 const order = Math.min(Math.log10(Math.abs(x)) / 3 | 0, suffixes.length); 252 return (x / divisors[order]).toFixed(1) + suffixes[order]; 253 } 254 var multiplier; 255 function updateControls() { 256 multiplier = x1.checked ? 1 : x10.checked ? 10 : 100; 257 webglCanvas.style.display = useGl.checked ? 'block' : 'none'; 258 canvas.style.display = use2D.checked ? 'block' : 'none'; 259 dom.style.display = useDom.checked ? 'block' : 'none'; 260 animation.className = animation.checked ? 'rotate' : null; 261 canvasOptions.style.display = useDom.checked ? 'none' : ''; 262 transferControlToOffscreen.parentElement.style.display = onscreen.checked ? 'none' : ''; 263 continuousOptions.style.display = animate.checked ? '' : 'none'; 264 glOptions.style.display = useGl.checked ? '' : 'none'; 265 multiviewOptions.style.display = (useGl.checked && multiviewAvailable) ? '' : 'none'; 266 fpsOptions.style.display = 267 animate.checked && (useSetTimeout.checked || useSetInterval.checked) ? 268 '' : 'none'; 269 fps.style.visibility = showFps.checked ? 'visible' : 'hidden'; 270 stats.style.visibility = showStats.checked ? 'visible' : 'hidden'; 271 drawCallsLabel.textContent = Math.max(1, drawCalls.value * multiplier); 272 pixelsLabel.textContent = formatSI(pixels.value * multiplier); 273 jsWorkLabel.textContent = jsWork.value; 274 fpsLabel.textContent = fpsSlider.value; 275 uploadLabel.textContent = 276 parseFloat(uploads.value).toFixed(2); 277 viewsLabel.textContent = views.value; 278 // Multiview is currently incompatible with multisampling. 279 if ((views.value > 1 || multiview.checked) && antialias.checked) 280 antialias.click(); 281 282 const queryParams = []; 283 for (const control of controls) { 284 if (control.type == 'radio') { 285 if (!defaultChecked[control.id] && control.checked) 286 queryParams.push(control.id); 287 } else if (control.type == 'checkbox') { 288 if (control.checked != defaultChecked[control.id]) 289 queryParams.push(defaultChecked[control.id] ? control.id + '=' + control.checked : control.id); 290 } else if (control instanceof HTMLDetailsElement) { 291 if (control.open) 292 queryParams.push(control.id); 293 } else if (control.value != defaultValues[control.id]) { 294 queryParams.push(control.id + '=' + control.value); 295 } 296 } 297 queryString = '?' + queryParams.join('&'); 298 if (!replaceStateScheduled) { 299 replaceStateScheduled = true; 300 setTimeout(callReplaceStateThrottled, 200); 301 } 302 render(); 303 }; 304 305 306 /**********************\ 307 * Input event handling * 308 \**********************/ 309 310 311 const imgSize = 256; 312 const size = parseInt(canvasSize.value); 313 let multiviewAvailable = false; 314 let webglVersion; 315 316 let mouseDown = false; 317 const lastPos = [0, 0]; 318 document.onmouseup = (e) => { mouseDown = false; } 319 document.onmousedown = (e) => { 320 mouseDown = true; 321 lastPos[0] = e.pageX; 322 lastPos[1] = e.pageY; 323 }; 324 document.ontouchstart = (e) => { 325 lastPos[0] = e.touches[0].pageX; 326 lastPos[1] = e.touches[0].pageY; 327 } 328 const position = [(size - imgSize) / 2, (size - imgSize) / 2]; 329 let continuousRunning = false; 330 let mouseUpdatesThisFrame = 0; 331 function mouseMove(e) { 332 mouseUpdatesThisFrame++; 333 countFps("mouse/touchmove event"); 334 const xy = [0, 0]; 335 if (e.touches) { 336 xy[0] = e.touches[0].pageX; 337 xy[1] = e.touches[0].pageY; 338 } else { 339 xy[0] = e.pageX; 340 xy[1] = e.pageY; 341 } 342 if (e.touches || mouseDown) { 343 for (let i = 0; i < 2; ++i) { 344 position[i] += xy[i] - lastPos[i]; 345 position[i] = Math.max(0, Math.min(size - imgSize, position[i])); 346 lastPos[i] = xy[i]; 347 } 348 if (!continuousRunning) { 349 render(); 350 } 351 } 352 } 353 document.addEventListener("mousemove", mouseMove, true); 354 document.body.addEventListener("touchmove", mouseMove, true); 355 for (const element of [dom, canvas, webglCanvas, img]) 356 element.onmousedown = element.ontouchstart = (e)=>e.preventDefault(); 357 358 359 360 /***********\ 361 * Rendering * 362 \***********/ 363 364 365 webglCanvas.width = webglCanvas.height = canvas.width = canvas.height = size; 366 dom.style.width = dom.style.height = size + 'px'; 367 368 window.onmessage = () => render(false, true); 369 370 let ctx; 371 let bitmapRenderer; 372 let offscreenCanvas; 373 let gl; 374 let glBitmapRenderer; 375 let glOffscreenCanvas; 376 let program; 377 let buffer; 378 const borderSize = 10; 379 const vertices = [0, 0, 1, 0, 0, 1, 1, 1]; 380 const readPixelsArray = new Uint8Array(4); 381 let writeBufferArray = new Float32Array(0); 382 let interval; 383 let intervalFps; 384 let timeout = null; 385 let rafPending = 0; 386 let postMessagePending = 0; 387 388 let maxViewsMultiview = 2; 389 let multiviewProgram = []; 390 let multiviewExt = null; 391 let multiviewTexture = null; 392 let multiviewFramebuffer = null; 393 let viewFramebuffer = []; 394 let lastNumViews = 0; 395 396 let displayedWarningText = ''; 397 let warningText = ''; 398 399 const animationDirection = [1, -1]; 400 function render(fromRaf, fromPostMessage) { 401 if (fromRaf) rafPending--; 402 if (fromPostMessage) postMessagePending--; 403 // Set up the appropriate render loop callback as specified by the UI, if 404 // continuous rendering is enabled. 405 continuousRunning = animate.checked; 406 if (continuousRunning) { 407 for (let i = 0; i < 2; ++i) { 408 position[i] += animationDirection[i] * 2; 409 if (position[i] > size - imgSize) { 410 position[i] = size - imgSize; 411 animationDirection[i] = -1; 412 } 413 if (position[i] < 0) { 414 position[i] = 0; 415 animationDirection[i] = 1; 416 } 417 } 418 if (useRaf.checked && rafPending == 0) { 419 (window.requestAnimationFrame || window.mozRequestAnimationFrame || 420 window.webkitRequestAnimationFrame)(function() { render(true); }); 421 rafPending++; 422 } 423 if (useSetTimeout.checked) { 424 clearTimeout(timeout); 425 timeout = setTimeout(render, 1 / fpsSlider.value * 1000); 426 } 427 if (useSetInterval.checked) { 428 if (!interval || intervalFps != fpsSlider.value) { 429 clearInterval(interval); 430 intervalFps = fpsSlider.value; 431 interval = setInterval(render, 1 / fpsSlider.value * 1000); 432 } 433 } else { 434 clearInterval(interval); 435 interval = null; 436 } 437 if (usePostMessage.checked) { 438 if (postMessagePending == 0) { 439 ++postMessagePending; 440 window.postMessage('', '*'); 441 } 442 } 443 } else { 444 clearInterval(interval); 445 interval = null; 446 } 447 448 countFps("render", mouseUpdatesThisFrame); 449 mouseUpdatesThisFrame = 0; 450 451 // Busy wait for a configurable amount of time. 452 const startMs = Date.now(); 453 while (Date.now() - startMs < jsWork.value); 454 455 warningText = ''; 456 457 // Initialize GL. 458 if (!gl) { 459 const options = {}; 460 for (let option of ['antialias', 'alpha', 'depth', 'stencil', 'premultipliedAlpha', 'preserveDrawingBuffer', 'desynchronized']) 461 options[option] = window[option].checked; 462 options.powerPreference = ppDefault.checked ? 'default' : lowPower.checked ? 'low-power' : 'high-performance'; 463 let renderCanvas = webglCanvas; 464 if (offscreen.checked) { 465 if (transferControlToOffscreen.checked) { 466 renderCanvas = webglCanvas.transferControlToOffscreen(); 467 } else { 468 glBitmapRenderer = webglCanvas.getContext('bitmaprenderer') 469 renderCanvas = glOffscreenCanvas = new OffscreenCanvas(size, size); 470 } 471 } 472 renderCanvas.width = renderCanvas.height = size; 473 if (useWebGL2.checked) 474 gl = renderCanvas.getContext('webgl2', options); 475 if (gl) { 476 webglVersion = 2; 477 } else { 478 webglVersion = 1; 479 gl = renderCanvas.getContext('webgl', options); 480 const aia = gl.getExtension('ANGLE_instanced_arrays'); 481 if (aia) 482 gl.drawArraysInstanced = (a, b, c, d)=>aia.drawArraysInstancedANGLE(a, b, c, d); 483 else { 484 pixels.value = pixels.min; 485 pixelsWrapper.style.display = 'none'; 486 gl.drawArraysInstanced = (a, b, c, d)=>gl.drawArrays(a, b, c); 487 } 488 } 489 // Read context info like renderer string and extensions. 490 let renderer = gl.getParameter(gl.RENDERER); 491 let debugRendererInfo = gl.getExtension('WEBGL_debug_renderer_info'); 492 if (debugRendererInfo) 493 renderer = gl.getParameter(debugRendererInfo.UNMASKED_RENDERER_WEBGL); 494 contextVersion.textContent = `WebGL Version: ${gl.getParameter(gl.VERSION)}\nRenderer: `; 495 const a = document.createElement('a'); 496 a.textContent = renderer; 497 a.href = `https://www.google.com/search?q=${encodeURIComponent(renderer)}` 498 contextVersion.appendChild(a); 499 contextAttributes.getElementsByTagName('pre')[0].textContent = JSON.stringify(gl.getContextAttributes(), 0, 2); 500 for (const e of gl.getSupportedExtensions()) { 501 const a = document.createElement('a'); 502 a.textContent = e; 503 a.href = `https://www.khronos.org/registry/webgl/extensions/${e}/`; 504 supportedExtensions.appendChild(a); 505 supportedExtensions.appendChild(document.createElement('br')); 506 } 507 508 // Setup texture 509 const tex = gl.createTexture(); 510 gl.activeTexture(gl.TEXTURE0); 511 gl.bindTexture(gl.TEXTURE_2D, tex); 512 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 255])); 513 img.onload = ()=>{ 514 gl.activeTexture(gl.TEXTURE0); 515 gl.bindTexture(gl.TEXTURE_2D, tex); 516 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); 517 gl.generateMipmap(gl.TEXTURE_2D); 518 render(); 519 }; 520 521 function setupProgram(vsSource, fsSource, attribs, uniforms) { 522 let prog = gl.createProgram(); 523 function compileShader(source, type) { 524 const shader = gl.createShader(type); 525 gl.shaderSource(shader, source); 526 gl.compileShader(shader); 527 if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) 528 console.log(gl.getShaderInfoLog(shader)); 529 gl.attachShader(prog, shader); 530 } 531 compileShader(vsSource, gl.VERTEX_SHADER); 532 compileShader(fsSource, gl.FRAGMENT_SHADER); 533 for (let i = 0; i < attribs.length; ++i) 534 gl.bindAttribLocation(prog, i, attribs[i]); 535 gl.linkProgram(prog); 536 if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) 537 console.log(gl.getProgramInfoLog(prog)); 538 for (const attrib of attribs) 539 prog[attrib] = gl.getAttribLocation(prog, attrib); 540 for (const uniform of uniforms) 541 prog[uniform] = gl.getUniformLocation(prog, uniform); 542 return prog; 543 } 544 545 program = setupProgram(` 546 attribute vec2 position; 547 varying vec2 texCoord; 548 uniform vec2 offset; 549 uniform float size; 550 void main() { 551 gl_Position = vec4(position * size + offset + vec2(size - 1., 1. - size), 0, 1); 552 texCoord = vec2(position.x, 1. - position.y); 553 }`,` 554 precision mediump float; 555 varying vec2 texCoord; 556 uniform vec4 colorAddition; 557 uniform sampler2D tex; 558 void main() { 559 gl_FragColor = texture2D(tex, texCoord) + colorAddition * 0.5; 560 }`, 561 ['position', 'texCoordIn'], ['offset', 'tex', 'size', 'colorAddition']); 562 gl.useProgram(program); 563 gl.uniform1i(program.tex, 0); 564 565 if (webglVersion >= 2 && gl.getExtension('OVR_multiview2')) { 566 multiviewAvailable = true; 567 let ext = gl.getExtension('OVR_multiview2') 568 maxViewsMultiview = Math.min(gl.getParameter(ext.MAX_VIEWS_OVR), 16); 569 views.max = maxViewsMultiview; 570 for (let i = 0; i < maxViewsMultiview; ++i) { 571 multiviewProgram[i] = setupProgram(`#version 300 es 572 #extension GL_OVR_multiview2 : enable 573 layout(num_views=${i + 1}) in; 574 in vec2 position; 575 out vec2 texCoord; 576 uniform vec2 offset; 577 uniform float size; 578 void main() { 579 gl_Position = vec4(position * size + offset + vec2(size - 1., 1. - size), 0, 1); 580 texCoord = vec2(position.x, 1. - position.y); 581 }`, `#version 300 es 582 #extension GL_OVR_multiview2 : enable 583 precision mediump float; 584 in vec2 texCoord; 585 out vec4 my_FragColor; 586 uniform sampler2D tex; 587 void main() { 588 vec4 colorAddition = vec4(((gl_ViewID_OVR & 0x4u) != 0u) ? 1.0 : 0.0, 589 ((gl_ViewID_OVR & 0x2u) != 0u) ? 1.0 : 0.0, 590 ((gl_ViewID_OVR & 0x1u) != 0u) ? 1.0 : 0.0, 591 1.0); 592 my_FragColor = texture(tex, texCoord) + colorAddition * 0.5; 593 }`, 594 ['position', 'texCoordIn'], ['offset', 'tex', 'size']); 595 gl.useProgram(multiviewProgram[i]); 596 gl.uniform1i(multiviewProgram[i].tex, 0); 597 } 598 } 599 600 // Setup vertex buffer 601 buffer = gl.createBuffer(); 602 gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 603 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STREAM_DRAW); 604 gl.enableVertexAttribArray(program.position); 605 gl.vertexAttribPointer(program.position, 2, gl.FLOAT, false, 0, 0); 606 607 // Setup drawing state 608 gl.viewport(0, 0, size, size); 609 gl.clearColor(1, 1, 1, 1); 610 gl.disable(gl.SCISSOR_TEST); 611 gl.disable(gl.DEPTH_TEST); 612 gl.disable(gl.STENCIL_TEST); 613 gl.disable(gl.BLEND); 614 gl.disable(gl.CULL_FACE); 615 616 updateControls(); 617 } // End GL init 618 619 if (useDom.checked) { 620 img.style.left = position[0] + 'px'; 621 img.style.top = position[1] + 'px'; 622 return; 623 } 624 if (use2D.checked) { 625 if (!ctx) { 626 if (offscreen.checked) { 627 if (transferControlToOffscreen.checked) { 628 offscreenCanvas = canvas.transferControlToOffscreen(); 629 } else { 630 bitmapRenderer = canvas.getContext('bitmaprenderer'); 631 offscreenCanvas = new OffscreenCanvas(size, size); 632 } 633 ctx = offscreenCanvas.getContext('2d'); 634 } else { 635 ctx = canvas.getContext('2d'); 636 } 637 } 638 ctx.fillStyle = 'white'; 639 ctx.fillRect(0, 0, canvas.width, canvas.height); 640 try { 641 ctx.drawImage(img, position[0], position[1]); 642 } catch (e) { 643 ctx.fillStyle = 'black'; 644 ctx.fillRect(position[0], position[1], imgSize, imgSize); 645 } 646 if (offscreen.checked && !transferControlToOffscreen.checked && bitmapRenderer && offscreenCanvas) { 647 bitmapRenderer.transferFromImageBitmap(offscreenCanvas.transferToImageBitmap()); 648 } 649 return; 650 } 651 652 // Upload some data to test the PCI bottleneck. 653 if (uploads.value > 0) { 654 if (writeBufferArray.length * 4 != uploads.value * 1024 * 1024) { 655 writeBufferArray = new Float32Array(uploads.value * 1024 * 1024 / 4); 656 // We want to actually use this data in rendering so the graphics driver 657 // can't optimize away the upload. Fill the first few bytes with our real 658 // vertex data. 659 writeBufferArray.set(vertices, 0); 660 } 661 gl.bufferData(gl.ARRAY_BUFFER, writeBufferArray, gl.STREAM_DRAW); 662 } 663 664 // Actually draw the map. 665 const numDrawCalls = Math.max(1, drawCalls.value * multiplier); 666 const numViews = multiviewAvailable ? Math.min(views.value, maxViewsMultiview) : 1; 667 const instances = pixels.value / 64000 * multiplier; 668 const instancesPerCall = Math.max(1, instances / numDrawCalls | 0); 669 const instancesFirstCall = instancesPerCall + numDrawCalls % instancesPerCall; 670 // Set up to scissor out all but one pixel of the map. 671 gl.scissor(position[0], size - position[1] - 1, 1, 1); 672 673 if (multiviewAvailable && (multiview.checked || numViews > 1) && !multiviewFramebuffer) { 674 multiviewTexture = gl.createTexture(); 675 gl.bindTexture(gl.TEXTURE_2D_ARRAY, multiviewTexture); 676 gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 677 gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 678 gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 679 gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 680 gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.RGBA8, size, size, maxViewsMultiview); 681 multiviewExt = gl.getExtension('OVR_multiview2'); 682 multiviewFramebuffer = gl.createFramebuffer(); 683 gl.bindFramebuffer(gl.FRAMEBUFFER, multiviewFramebuffer); 684 multiviewExt.framebufferTextureMultiviewOVR(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, multiviewTexture, 0, 0, numViews); 685 lastNumViews = numViews; 686 for (let i = 0; i < maxViewsMultiview; ++i) { 687 viewFramebuffer[i] = gl.createFramebuffer(); 688 gl.bindFramebuffer(gl.FRAMEBUFFER, viewFramebuffer[i]); 689 gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, multiviewTexture, 0, i); 690 } 691 gl.bindFramebuffer(gl.FRAMEBUFFER, null); 692 gl.bindTexture(gl.TEXTURE_2D_ARRAY, null); 693 } 694 695 let usedProgram = program; 696 if (multiviewAvailable && multiview.checked) { 697 gl.bindFramebuffer(gl.FRAMEBUFFER, multiviewFramebuffer); 698 if (numViews != lastNumViews) { 699 multiviewExt.framebufferTextureMultiviewOVR(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, multiviewTexture, 0, 0, numViews); 700 lastNumViews = numViews; 701 } 702 usedProgram = multiviewProgram[numViews - 1]; 703 } else { 704 gl.bindFramebuffer(gl.FRAMEBUFFER, null); 705 } 706 707 gl.useProgram(usedProgram); 708 gl.uniform2f(usedProgram.offset, 709 (position[0] - imgSize) / size * 2, 710 -position[1] / size * 2); 711 gl.uniform1f(usedProgram.size, imgSize * 2 / size); 712 713 if (usedProgram == program) { 714 gl.uniform4f(program.colorAddition, 0.0, 0.0, 0.0, 1.0); 715 } 716 717 for (let viewIndex = 0; viewIndex < numViews; ++viewIndex) { 718 let scissorEnabled = false; 719 if (multiviewAvailable && !multiview.checked && numViews > 1) { 720 gl.bindFramebuffer(gl.FRAMEBUFFER, viewFramebuffer[viewIndex]); 721 gl.uniform4f(program.colorAddition, (viewIndex & 4) ? 1.0 : 0.0, (viewIndex & 2) ? 1.0 : 0.0, (viewIndex & 1) ? 1.0 : 0.0, 1.0); 722 gl.disable(gl.SCISSOR_TEST); 723 } 724 gl.clearColor(1, 1, 1, 1); 725 gl.clear(gl.COLOR_BUFFER_BIT); 726 gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, instancesFirstCall); 727 var instancesDrawn = instancesFirstCall; 728 for (let i = 1; i < numDrawCalls; i++) { 729 if (instancesDrawn > instances && !scissorEnabled) { 730 // If we've drawn all of the requested pixels already, enable the scissor 731 // test so we only draw one pixel per draw call for the rest of the calls. 732 scissorEnabled = true; 733 gl.enable(gl.SCISSOR_TEST); 734 } 735 gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, instancesPerCall); 736 instancesDrawn += instancesPerCall; 737 } 738 if (multiviewAvailable && multiview.checked) { 739 break; 740 } 741 } 742 gl.disable(gl.SCISSOR_TEST); 743 744 if (multiviewAvailable && (multiview.checked || numViews > 1)) { 745 if (multiviewCopy.checked) { 746 gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null); 747 gl.clearColor(1, 1, 1, 1); 748 gl.clear(gl.COLOR_BUFFER_BIT); 749 let gridCellsPerRow = Math.ceil(Math.sqrt(numViews)); 750 for (let i = 0; i < numViews; ++i) { 751 gl.bindFramebuffer(gl.READ_FRAMEBUFFER, viewFramebuffer[i]); 752 let x = i % gridCellsPerRow; 753 let y = Math.floor(i / gridCellsPerRow); 754 x *= size / gridCellsPerRow; 755 y *= size / gridCellsPerRow; 756 gl.blitFramebuffer(0, 0, size, size, x, y, x + size / gridCellsPerRow, y + size / gridCellsPerRow, gl.COLOR_BUFFER_BIT, gl.NEAREST); 757 } 758 } else { 759 warningText = 'NOTE: Offscreen multiview rendering active - rendering not copied to canvas'; 760 } 761 } 762 763 if (finish.checked) { 764 gl.finish(); 765 } 766 if (readPixels.checked) { 767 gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, readPixelsArray); 768 } 769 if (offscreen.checked && !transferControlToOffscreen.checked && glOffscreenCanvas && glBitmapRenderer) { 770 glBitmapRenderer.transferFromImageBitmap(glOffscreenCanvas.transferToImageBitmap()); 771 } 772 } 773 774 775 /**************\ 776 * FPS counters * 777 \**************/ 778 779 780 const counters = {}; 781 function countFps(name, mouseEventsThisFrame) { 782 let counter = counters[name]; 783 if (!counter) { 784 counter = { history: [Date.now() - 16], name: name, count: 0 }; 785 counters[name] = counter; 786 } 787 const history = counter.history; 788 history.push(Date.now()); 789 while (history.length > 2 && 790 history[0] + 1000 < history[history.length - 1]) { 791 counter.history.shift(); 792 } 793 let averageMs = 0; 794 let maxMs = .1; 795 let minMs = 99999999; 796 for (let i = 1; i < history.length; i++) { 797 let diff = history[i] - history[i - 1]; 798 averageMs += diff; 799 maxMs = Math.max(maxMs, diff); 800 minMs = Math.min(minMs, diff); 801 } 802 averageMs /= history.length - 1; 803 counter.fps = 1000 / averageMs; 804 counter.minFps = 1000 / maxMs; 805 counter.maxFps = 1000 / minMs; 806 counter.count++; 807 808 if (mouseEventsThisFrame !== undefined) { 809 counter.mouseEvents = counter.mouseEvents || { multiple: 0, zero: 0 }; 810 if (mouseEventsThisFrame > 1) { 811 counter.mouseEvents.multiple++; 812 } else if (mouseEventsThisFrame == 0) { 813 counter.mouseEvents.zero++; 814 } 815 } 816 817 if (showFps.checked) { 818 fpsSpan.innerText = counters['render'].fps.toFixed(); 819 if (showStats.checked) { 820 let text = ""; 821 for (let key in counters) { 822 counter = counters[key]; 823 text += "<b>" + counter.name + "</b><br>"; 824 text += counter.fps.toFixed() + " avg FPS<br>"; 825 text += "<div class=" + 826 (counter.maxFps - counter.fps > 10 ? "bad" : "") + ">" + 827 counter.maxFps.toFixed() + " max FPS</div>"; 828 text += "<div class=" + 829 (counter.fps - counter.minFps > 10 ? "bad" : "") + ">" + 830 counter.minFps.toFixed() + " min FPS</div>"; 831 if (counter.mouseEvents) { 832 text += "<div>" + counter.mouseEvents.zero + 833 " frame(s) with no mouse/touch events"; 834 text += "<div>" + counter.mouseEvents.multiple + 835 " frame(s) with multiple mouse/touch events"; 836 } 837 text += "<div class=light>" + counter.count + " frames</div>"; 838 } 839 stats.innerHTML = text; 840 } 841 } 842 if (warningText != displayedWarningText) { 843 warning.textContent = warningText; 844 displayedWarningText = warningText; 845 } 846 } 847 848 updateControls(); 849 850 img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAgMAAAAhHED1AAAADFBMVEUAAAA5AAByAACZAAADU26qAAAFIUlEQVR42u2ZPW7jRhTHOVQkFiwEFU4RFTwCj0Am8AG8wHIDrC8QZBH4BAGJNGkdIL7BLpCo8Q1CpkqrDRCkVWEvkKSIChVUQHLy3pshRZlDeUhVCeZf6GOt+XHmvf+8+VjLMjIyMjIyMjIyMjIyMjIyMjIyMjL6j2txEUWX45u/uueo6l04qvlUNCfE9yPaf8zbendme84fB7af8ad6O6g9SzsAfjsEcNNtz6sBuVhylR7OGsCgQXhHzf5crVaCuNftQHvgd2LkF28GdKHVgcdD4C7udbvQ6sCRg9mNZheW6vZEyHUAaW/agKDhBadurxjwNN3oh1BpvGWpb6K12uOJ7gh6Mjbb6I6gL2Ffaeag1zKzZ/Jgn4wA6uo0wJUpgI8fvaDCPrCq+wcPBegaxsthgFgANvSRAEXnNxcaE2lL4UwwJh37v+F7DOTl5SkX7AiQWdaEPgI5CltRwhFy5bxwDwBGqXCoM/gtaw0Sba4G1DbKCbBBwOYYMKuzrAaIGOYIsAngSkc0AK/uoRogfLhJCxw9/mz+FCAeUfYAZBLWcUnx3OEDs2OAHGMPQBo5FIA0R0ByBIBf7EMojoka4EgfBxWOPi7QmeERwMEA2viiBIgsFtAMR++XXYBL0ySF+CoBcxliD/7oVV6JEwJm1UsCsCtKQkEzZqsGeNLHCPBLDx4WV9YsheoCgBTt45MzPXhVAnzpkjkEKSjn8HQIp4++YvwXykpAxpr3AQICZDDSzIpzF36SFpTakPG/qUrE5Itp9FINEC5JINZrADiYrJwykzH5l7SZEycAIQHS3QTb7ea8gqAD4J8U/5UnpwCpBEwwTVubrwHg89sZ3wEgdGjkGgD025bhU9fwAV0Z51TawJqHZicBTD5+A11JIfY+hBLewJqif2i2kwCL72x06xaCgXmblwwLi18yXUAKwc8gjg5/j3lzK4ZvXjUIkJAVfsa8OZwA80p7CHGBLgoA8A3/LYquOU0msOYzgNoH0sc+vH4mSsSnCHA5pVEHIGYSvPKJADDMPwDQiSz6/DQgseRchtdqUvdAAMRcsPsAQV2zPaomUFOqJz2IaTZO+gB+vTDOqZ5BVSsn/P0CRADIQkALjdMHqAsKNMWKCnW1sMX0E1mo4BE5FbbidEmTNR1ec5sGHREA4jKnmuj3Adx61XCoH7i2kINcYSSIi0sxTvsAdVnHphtaBGACb7HvDL8HBf7hAddHBNxFEVQm5cIiKj8uVFucwzSL4A0mJlYmPD0UVrMKq5Y2ubZb2A+oywBh0C8bOyN3cQ1gGr0IO5Nhi4CMvq4hYHfXVJHgbd1sohrA7MPvWcdJtPYm5MxMZGZTF1URpipuAOzXv7LuBgOfHRIgES0SVm/+6EMetAA/Zd0tDhR1LjqU0O67hHb3MmL4jLXfAnzdBkzqLRKuz3JpXWLmGP9W7p8BWFmf/GCtSG+t76Kkm4bqKDVfiqO7fINl6er5jeatNVbeYZ84Ts7wg7Z6r7sZu92vg1COPXA0e9W+E8cXz42hPrJUYw9dzcH1QX0oDHUTybkqWkuNw7PdHH1Dxd3OWv/Uo7g6mqaVjhXcvssndq/n0NYVzOPx8znXM2jrEmh/iOSrbgnVugX6kU5bC3EzmGhOCPU91JA5qr6IGjBFZ2dehVnWtaL9flBZuDnrMk8m/YzrRAVh+LVs61J35LUue920/+NqXIVdvKZxrKJz7sYXC/MfBEZGRkZGRkZGRkZGRkZGRkZGRkb/N/0Lo92VZbHxh60AAAAASUVORK5CYII='; 851 </script>