tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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>