summaryrefslogtreecommitdiffstats
path: root/dom/canvas/test/webgl-conf/checkout/extra/workload-simulator.html
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/canvas/test/webgl-conf/checkout/extra/workload-simulator.html851
1 files changed, 851 insertions, 0 deletions
diff --git a/dom/canvas/test/webgl-conf/checkout/extra/workload-simulator.html b/dom/canvas/test/webgl-conf/checkout/extra/workload-simulator.html
new file mode 100644
index 0000000000..fa01977321
--- /dev/null
+++ b/dom/canvas/test/webgl-conf/checkout/extra/workload-simulator.html
@@ -0,0 +1,851 @@
+<!DOCTYPE html><meta charset="utf-8">
+<meta name=viewport content="width=900">
+<title>WebGL workload simulator</title>
+<!--
+Copyright (c) 2019 The Khronos Group Inc.
+Use of this source code is governed by an MIT-style license that can be
+found in the LICENSE.txt file.
+-->
+<style>
+body {
+ margin: 0;
+ font-family: monospace;
+ user-select: none;
+ -moz-user-select: -moz-none;
+ -webkit-user-select: none;
+ font-size: 22px;
+ text-size-adjust: none;
+}
+pre { margin: 0; }
+.square img { position: relative; }
+.square {
+ overflow: hidden;
+ border-bottom: 20px solid black;
+ border-right: 20px solid black;
+ display: block;
+}
+
+#fpsSpan { font-size: 30px; }
+#fpsPanel {
+ position: fixed;
+ text-align: right;
+ left: 550px;
+ top: 0px;
+ width: 300px;
+ font-size: 12px;
+ margin: 10px;
+ pointer-events: none;
+}
+
+input, label { white-space: pre; pointer-events: auto; }
+input { margin: 15px 15px; transform: scale(2); }
+input[type="number"] { width: 50px; }
+input[type="range"] {
+ width: 100px;
+ margin: 30px 200px;
+ padding: 0px;
+ transform: scale(4);
+}
+
+.rotate { animation: rotating 2s linear infinite; }
+@keyframes rotating {
+ from { transform: scale(2) rotate(0deg); }
+ to { transform: scale(2) rotate(360deg); }
+}
+
+.bad { color: red; }
+.light { color: #CCCCCC; }
+#warning {
+ color: #FF0000;
+}
+</style>
+
+<canvas id=webglCanvas class=square></canvas>
+<canvas id=canvas class=square style=display:none;></canvas>
+<div id=dom class=square style=display:none;background:white>
+ <img id=img style=width:256px;height:256px;background:black></img>
+</div>
+<div style=margin:2em;margin-top:0>
+<div id=warning></div>
+Drag the WebGL logo.<br>
+<label for=useGl>Renderer: <input type=radio name=renderer id=useGl checked>WebGL</label>
+<label for=use2D><input type=radio name=renderer id=use2D>Canvas 2D</label>
+<label for=useDom><input type=radio name=renderer id=useDom>DOM</label>
+<br>
+<label for=animate><input type=checkbox name=animate id=animate>Animate</label>
+<span id=continuousOptions>
+ <label for=useRaf><input type=radio name=loop id=useRaf checked>requestAnimationFrame</label>
+ <label for=usePostMessage><input type=radio name=loop id=usePostMessage>postMessage</label>
+ <label for=useSetTimeout><input type=radio name=loop id=useSetTimeout>setTimeout</label>
+ <label for=useSetInterval><input type=radio name=loop id=useSetInterval>setInterval</label>
+ <br>
+ <div id=fpsOptions>
+ <input id=fpsSlider type=range min=10 max=240 step=10 value=60>
+ Target FPS: <span id=fpsLabel>60</span>
+ </div>
+</span>
+<details id=canvasOptions>
+ <summary>Canvas options</summary>
+ <div id=canvas2DOptions>
+ Canvas size <input type=number id=canvasSize value=512 min=1> pixels<sup>2</sup>
+ <label for=onscreen><input type=radio name=offscreen id=onscreen checked>Regular canvas</label>
+ <label for=offscreen><input type=radio name=offscreen id=offscreen>OffscreenCanvas (on main thread)</label>
+ <!-- <label for=offscreenWorker><input type=radio name=offscreen id=offscreenWorker>OffscreenCanvas on worker</label> -->
+ <br>
+ <label for=transferControlToOffscreen><input type=checkbox id=transferControlToOffscreen checked>Use transferControlToOffscreen</label>
+ </div>
+</details>
+<div id=glOptions>
+ <details id=glWork>
+ <summary>WebGL rendering work</summary>
+ <div id=pixelsWrapper>
+ <input type=range id=pixels min=65536 max=65536000 value=65536 step=65536>
+ draw <span id=pixelsLabel>64K</span> pixels per view
+ </div>
+ <input type=range id=drawCalls min=0 max=10000 value=0 step=10>
+ using <span id=drawCallsLabel>1</span> draw call(s) per view
+ <br>
+ Multiply the above slider values by:
+ <label for=x1><input type=radio name=multiplier id=x1 checked>1 </label>
+ <label for=x10><input type=radio name=multiplier id=x10>10 </label>
+ <label for=x100><input type=radio name=multiplier id=x100>100 </label>
+ <br>
+ <input type=range id=uploads min=0 max=256 value=0 step=0.25>
+ <span id=uploadLabel>0.00</span> MB glBufferData
+ <br>
+ <label for=finish><input type=checkbox id=finish>glFinish</label>
+ <br>
+ <label for=readPixels><input type=checkbox id=readPixels>glReadPixels</label>
+ </details>
+ <details id=contextCreation>
+ <summary>WebGL context creation options</summary>
+ <label for=useWebGL2><input type=checkbox id=useWebGL2 checked>Use WebGL 2 if available</label>
+ <label for=antialias><input type=checkbox id=antialias checked>Antialias</label>
+ <label for=alpha><input type=checkbox id=alpha checked>Alpha</label>
+ <label for=depth><input type=checkbox id=depth checked>Depth</label>
+ <label for=stencil><input type=checkbox id=stencil>Stencil</label>
+ <label for=premultipliedAlpha><input type=checkbox id=premultipliedAlpha checked>Premultiplied Alpha</label>
+ <label for=preserveDrawingBuffer><input type=checkbox id=preserveDrawingBuffer>Preserve Drawing Buffer</label>
+ <label for=desynchronized><input type=checkbox id=desynchronized>Desynchronized</label>
+ <br>
+ Power preference
+ <label for=ppDefault><input type=radio name=pp id=ppDefault checked>default</label>
+ <label for=lowPower><input type=radio name=pp id=lowPower>low-power</label>
+ <label for=highPerformance><input type=radio name=pp id=highPerformance>high-performance</label>
+ <br>
+ <label for=separateHighPowerContext><input type=checkbox id=separateHighPowerContext>Activate high power GPU (by creating a separate high power context)</label>
+ </details>
+ <div id=multiviewOptions>
+ <input type=range id=views min=1 max=4 value=0 step=1>
+ Draw <span id=viewsLabel>1</span> views(s)
+ <br>
+ <label for=multiview><input type=checkbox id=multiview>Use OVR_multiview2</label>
+ <label for=multiviewCopy><input type=checkbox id=multiviewCopy checked>Copy multiview draw results to main canvas</label>
+ </div>
+ <pre id=contextVersion>
+ </pre>
+ <details id=contextAttributes>
+ <summary>Context attributes</summary>
+ <pre></pre>
+ </details>
+ <details id=supportedExtensions>
+ <summary>Supported Extensions</summary>
+ </details>
+</div>
+<br>
+<input type=range id=jsWork min=0 max=100 value=0>
+<span id=jsWorkLabel>0</span> ms extra Javascript work per frame
+<br>
+<label for=animation><input type=checkbox id=animation>CSS animation</label>
+
+<div id=fpsPanel>
+ <label for=showFps><input type=checkbox id=showFps checked>Show FPS</label>
+ <div id=fps>
+ <span id=fpsSpan></span>
+ <p>
+ <label for=showStats><input type=checkbox id=showStats>More info</label>
+ <div id=stats></div>
+ </div>
+</div>
+
+<iframe id=highPowerFrame style=display:none></iframe>
+
+<h2><center>WebGL workload simulator</center></h2>
+</div>
+
+<script>
+'use strict';
+// Add all elements with ids as global readonly variables.
+for (let element of document.documentElement.querySelectorAll('[id]'))
+ Object.defineProperty(this, element.id, {value: element, writeable: false});
+
+
+/************\
+* Options UI *
+\************/
+
+
+// Set all input elements with values from the query string.
+const controls = document.querySelectorAll('input, details');
+const defaultChecked = {};
+const defaultValues = {};
+const defaultMaxes = {};
+for (const control of controls) {
+ if (!control.id) continue;
+ defaultChecked[control.id] = control.checked;
+ defaultValues[control.id] = control.value;
+ defaultMaxes[control.id] = control.max;
+ const param = window.location.search.match(control.id + '(?:=([^&]*))?');
+ if (param) {
+ if (control.type == 'radio')
+ control.checked = true;
+ else if (control.type == 'checkbox')
+ control.checked = param[1] != 'false';
+ else if (control instanceof HTMLDetailsElement)
+ control.open = true;
+ else
+ control.value = param[1];
+ }
+ control.oninput = updateControls;
+ if (control instanceof HTMLDetailsElement)
+ control.onclick = ()=>setTimeout(updateControls, 0);
+}
+// Some controls require a page reload when changed.
+const reloadControls = ['useWebGL2', 'antialias', 'alpha', 'depth', 'stencil', 'premultipliedAlpha', 'preserveDrawingBuffer', 'desynchronized', 'ppDefault', 'lowPower', 'highPerformance', 'canvasSize', 'onscreen', 'offscreen', 'transferControlToOffscreen'].map(x=>window[x]);
+for (let control of reloadControls) {
+ control.onchange = ()=>{
+ updateControls();
+ callReplaceStateThrottled();
+ location.reload();
+ };
+}
+
+separateHighPowerContext.onchange = ()=>{
+ if (!separateHighPowerContext.checked)
+ highPowerFrame.contentDocument.location.reload();
+ else {
+ const doc = highPowerFrame.contentDocument;
+ const canvas = doc.createElement('canvas');
+ doc.body.appendChild(canvas);
+ canvas.getContext('webgl', {powerPreference: 'high-performance'});
+ }
+}
+separateHighPowerContext.onchange();
+
+let queryString = window.location.search;
+let previousQueryString = queryString;
+let replaceStateScheduled = false;
+function callReplaceStateThrottled() {
+ replaceStateScheduled = false;
+ if (queryString == previousQueryString)
+ return;
+ previousQueryString = queryString;
+ let path = window.location.pathname;
+ history.replaceState(null, null, queryString == '?' ? path : path + queryString);
+}
+const suffixes = ['', 'K', 'M', 'G', 'E']
+const divisors = [];
+for (let i = 0; i < suffixes.length; i++)
+ divisors[i] = Math.pow(10, i * 3);
+const formatSI = (x) => {
+ const order = Math.min(Math.log10(Math.abs(x)) / 3 | 0, suffixes.length);
+ return (x / divisors[order]).toFixed(1) + suffixes[order];
+}
+var multiplier;
+function updateControls() {
+ multiplier = x1.checked ? 1 : x10.checked ? 10 : 100;
+ webglCanvas.style.display = useGl.checked ? 'block' : 'none';
+ canvas.style.display = use2D.checked ? 'block' : 'none';
+ dom.style.display = useDom.checked ? 'block' : 'none';
+ animation.className = animation.checked ? 'rotate' : null;
+ canvasOptions.style.display = useDom.checked ? 'none' : '';
+ transferControlToOffscreen.parentElement.style.display = onscreen.checked ? 'none' : '';
+ continuousOptions.style.display = animate.checked ? '' : 'none';
+ glOptions.style.display = useGl.checked ? '' : 'none';
+ multiviewOptions.style.display = (useGl.checked && multiviewAvailable) ? '' : 'none';
+ fpsOptions.style.display =
+ animate.checked && (useSetTimeout.checked || useSetInterval.checked) ?
+ '' : 'none';
+ fps.style.visibility = showFps.checked ? 'visible' : 'hidden';
+ stats.style.visibility = showStats.checked ? 'visible' : 'hidden';
+ drawCallsLabel.textContent = Math.max(1, drawCalls.value * multiplier);
+ pixelsLabel.textContent = formatSI(pixels.value * multiplier);
+ jsWorkLabel.textContent = jsWork.value;
+ fpsLabel.textContent = fpsSlider.value;
+ uploadLabel.textContent =
+ parseFloat(uploads.value).toFixed(2);
+ viewsLabel.textContent = views.value;
+ // Multiview is currently incompatible with multisampling.
+ if ((views.value > 1 || multiview.checked) && antialias.checked)
+ antialias.click();
+
+ const queryParams = [];
+ for (const control of controls) {
+ if (control.type == 'radio') {
+ if (!defaultChecked[control.id] && control.checked)
+ queryParams.push(control.id);
+ } else if (control.type == 'checkbox') {
+ if (control.checked != defaultChecked[control.id])
+ queryParams.push(defaultChecked[control.id] ? control.id + '=' + control.checked : control.id);
+ } else if (control instanceof HTMLDetailsElement) {
+ if (control.open)
+ queryParams.push(control.id);
+ } else if (control.value != defaultValues[control.id]) {
+ queryParams.push(control.id + '=' + control.value);
+ }
+ }
+ queryString = '?' + queryParams.join('&');
+ if (!replaceStateScheduled) {
+ replaceStateScheduled = true;
+ setTimeout(callReplaceStateThrottled, 200);
+ }
+ render();
+};
+
+
+/**********************\
+* Input event handling *
+\**********************/
+
+
+const imgSize = 256;
+const size = parseInt(canvasSize.value);
+let multiviewAvailable = false;
+let webglVersion;
+
+let mouseDown = false;
+const lastPos = [0, 0];
+document.onmouseup = (e) => { mouseDown = false; }
+document.onmousedown = (e) => {
+ mouseDown = true;
+ lastPos[0] = e.pageX;
+ lastPos[1] = e.pageY;
+};
+document.ontouchstart = (e) => {
+ lastPos[0] = e.touches[0].pageX;
+ lastPos[1] = e.touches[0].pageY;
+}
+const position = [(size - imgSize) / 2, (size - imgSize) / 2];
+let continuousRunning = false;
+let mouseUpdatesThisFrame = 0;
+function mouseMove(e) {
+ mouseUpdatesThisFrame++;
+ countFps("mouse/touchmove event");
+ const xy = [0, 0];
+ if (e.touches) {
+ xy[0] = e.touches[0].pageX;
+ xy[1] = e.touches[0].pageY;
+ } else {
+ xy[0] = e.pageX;
+ xy[1] = e.pageY;
+ }
+ if (e.touches || mouseDown) {
+ for (let i = 0; i < 2; ++i) {
+ position[i] += xy[i] - lastPos[i];
+ position[i] = Math.max(0, Math.min(size - imgSize, position[i]));
+ lastPos[i] = xy[i];
+ }
+ if (!continuousRunning) {
+ render();
+ }
+ }
+}
+document.addEventListener("mousemove", mouseMove, true);
+document.body.addEventListener("touchmove", mouseMove, true);
+for (const element of [dom, canvas, webglCanvas, img])
+ element.onmousedown = element.ontouchstart = (e)=>e.preventDefault();
+
+
+
+/***********\
+* Rendering *
+\***********/
+
+
+webglCanvas.width = webglCanvas.height = canvas.width = canvas.height = size;
+dom.style.width = dom.style.height = size + 'px';
+
+window.onmessage = () => render(false, true);
+
+let ctx;
+let bitmapRenderer;
+let offscreenCanvas;
+let gl;
+let glBitmapRenderer;
+let glOffscreenCanvas;
+let program;
+let buffer;
+const borderSize = 10;
+const vertices = [0, 0, 1, 0, 0, 1, 1, 1];
+const readPixelsArray = new Uint8Array(4);
+let writeBufferArray = new Float32Array(0);
+let interval;
+let intervalFps;
+let timeout = null;
+let rafPending = 0;
+let postMessagePending = 0;
+
+let maxViewsMultiview = 2;
+let multiviewProgram = [];
+let multiviewExt = null;
+let multiviewTexture = null;
+let multiviewFramebuffer = null;
+let viewFramebuffer = [];
+let lastNumViews = 0;
+
+let displayedWarningText = '';
+let warningText = '';
+
+const animationDirection = [1, -1];
+function render(fromRaf, fromPostMessage) {
+ if (fromRaf) rafPending--;
+ if (fromPostMessage) postMessagePending--;
+ // Set up the appropriate render loop callback as specified by the UI, if
+ // continuous rendering is enabled.
+ continuousRunning = animate.checked;
+ if (continuousRunning) {
+ for (let i = 0; i < 2; ++i) {
+ position[i] += animationDirection[i] * 2;
+ if (position[i] > size - imgSize) {
+ position[i] = size - imgSize;
+ animationDirection[i] = -1;
+ }
+ if (position[i] < 0) {
+ position[i] = 0;
+ animationDirection[i] = 1;
+ }
+ }
+ if (useRaf.checked && rafPending == 0) {
+ (window.requestAnimationFrame || window.mozRequestAnimationFrame ||
+ window.webkitRequestAnimationFrame)(function() { render(true); });
+ rafPending++;
+ }
+ if (useSetTimeout.checked) {
+ clearTimeout(timeout);
+ timeout = setTimeout(render, 1 / fpsSlider.value * 1000);
+ }
+ if (useSetInterval.checked) {
+ if (!interval || intervalFps != fpsSlider.value) {
+ clearInterval(interval);
+ intervalFps = fpsSlider.value;
+ interval = setInterval(render, 1 / fpsSlider.value * 1000);
+ }
+ } else {
+ clearInterval(interval);
+ interval = null;
+ }
+ if (usePostMessage.checked) {
+ if (postMessagePending == 0) {
+ ++postMessagePending;
+ window.postMessage('', '*');
+ }
+ }
+ } else {
+ clearInterval(interval);
+ interval = null;
+ }
+
+ countFps("render", mouseUpdatesThisFrame);
+ mouseUpdatesThisFrame = 0;
+
+ // Busy wait for a configurable amount of time.
+ const startMs = Date.now();
+ while (Date.now() - startMs < jsWork.value);
+
+ warningText = '';
+
+ // Initialize GL.
+ if (!gl) {
+ const options = {};
+ for (let option of ['antialias', 'alpha', 'depth', 'stencil', 'premultipliedAlpha', 'preserveDrawingBuffer', 'desynchronized'])
+ options[option] = window[option].checked;
+ options.powerPreference = ppDefault.checked ? 'default' : lowPower.checked ? 'low-power' : 'high-performance';
+ let renderCanvas = webglCanvas;
+ if (offscreen.checked) {
+ if (transferControlToOffscreen.checked) {
+ renderCanvas = webglCanvas.transferControlToOffscreen();
+ } else {
+ glBitmapRenderer = webglCanvas.getContext('bitmaprenderer')
+ renderCanvas = glOffscreenCanvas = new OffscreenCanvas(size, size);
+ }
+ }
+ renderCanvas.width = renderCanvas.height = size;
+ if (useWebGL2.checked)
+ gl = renderCanvas.getContext('webgl2', options);
+ if (gl) {
+ webglVersion = 2;
+ } else {
+ webglVersion = 1;
+ gl = renderCanvas.getContext('webgl', options);
+ const aia = gl.getExtension('ANGLE_instanced_arrays');
+ if (aia)
+ gl.drawArraysInstanced = (a, b, c, d)=>aia.drawArraysInstancedANGLE(a, b, c, d);
+ else {
+ pixels.value = pixels.min;
+ pixelsWrapper.style.display = 'none';
+ gl.drawArraysInstanced = (a, b, c, d)=>gl.drawArrays(a, b, c);
+ }
+ }
+ // Read context info like renderer string and extensions.
+ let renderer = gl.getParameter(gl.RENDERER);
+ let debugRendererInfo = gl.getExtension('WEBGL_debug_renderer_info');
+ if (debugRendererInfo)
+ renderer = gl.getParameter(debugRendererInfo.UNMASKED_RENDERER_WEBGL);
+ contextVersion.textContent = `WebGL Version: ${gl.getParameter(gl.VERSION)}\nRenderer: `;
+ const a = document.createElement('a');
+ a.textContent = renderer;
+ a.href = `https://www.google.com/search?q=${encodeURIComponent(renderer)}`
+ contextVersion.appendChild(a);
+ contextAttributes.getElementsByTagName('pre')[0].textContent = JSON.stringify(gl.getContextAttributes(), 0, 2);
+ for (const e of gl.getSupportedExtensions()) {
+ const a = document.createElement('a');
+ a.textContent = e;
+ a.href = `https://www.khronos.org/registry/webgl/extensions/${e}/`;
+ supportedExtensions.appendChild(a);
+ supportedExtensions.appendChild(document.createElement('br'));
+ }
+
+ // Setup texture
+ const tex = gl.createTexture();
+ gl.activeTexture(gl.TEXTURE0);
+ gl.bindTexture(gl.TEXTURE_2D, tex);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 255]));
+ img.onload = ()=>{
+ gl.activeTexture(gl.TEXTURE0);
+ gl.bindTexture(gl.TEXTURE_2D, tex);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
+ gl.generateMipmap(gl.TEXTURE_2D);
+ render();
+ };
+
+ function setupProgram(vsSource, fsSource, attribs, uniforms) {
+ let prog = gl.createProgram();
+ function compileShader(source, type) {
+ const shader = gl.createShader(type);
+ gl.shaderSource(shader, source);
+ gl.compileShader(shader);
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
+ console.log(gl.getShaderInfoLog(shader));
+ gl.attachShader(prog, shader);
+ }
+ compileShader(vsSource, gl.VERTEX_SHADER);
+ compileShader(fsSource, gl.FRAGMENT_SHADER);
+ for (let i = 0; i < attribs.length; ++i)
+ gl.bindAttribLocation(prog, i, attribs[i]);
+ gl.linkProgram(prog);
+ if (!gl.getProgramParameter(prog, gl.LINK_STATUS))
+ console.log(gl.getProgramInfoLog(prog));
+ for (const attrib of attribs)
+ prog[attrib] = gl.getAttribLocation(prog, attrib);
+ for (const uniform of uniforms)
+ prog[uniform] = gl.getUniformLocation(prog, uniform);
+ return prog;
+ }
+
+ program = setupProgram(`
+ attribute vec2 position;
+ varying vec2 texCoord;
+ uniform vec2 offset;
+ uniform float size;
+ void main() {
+ gl_Position = vec4(position * size + offset + vec2(size - 1., 1. - size), 0, 1);
+ texCoord = vec2(position.x, 1. - position.y);
+ }`,`
+ precision mediump float;
+ varying vec2 texCoord;
+ uniform vec4 colorAddition;
+ uniform sampler2D tex;
+ void main() {
+ gl_FragColor = texture2D(tex, texCoord) + colorAddition * 0.5;
+ }`,
+ ['position', 'texCoordIn'], ['offset', 'tex', 'size', 'colorAddition']);
+ gl.useProgram(program);
+ gl.uniform1i(program.tex, 0);
+
+ if (webglVersion >= 2 && gl.getExtension('OVR_multiview2')) {
+ multiviewAvailable = true;
+ let ext = gl.getExtension('OVR_multiview2')
+ maxViewsMultiview = Math.min(gl.getParameter(ext.MAX_VIEWS_OVR), 16);
+ views.max = maxViewsMultiview;
+ for (let i = 0; i < maxViewsMultiview; ++i) {
+ multiviewProgram[i] = setupProgram(`#version 300 es
+ #extension GL_OVR_multiview2 : enable
+ layout(num_views=${i + 1}) in;
+ in vec2 position;
+ out vec2 texCoord;
+ uniform vec2 offset;
+ uniform float size;
+ void main() {
+ gl_Position = vec4(position * size + offset + vec2(size - 1., 1. - size), 0, 1);
+ texCoord = vec2(position.x, 1. - position.y);
+ }`, `#version 300 es
+ #extension GL_OVR_multiview2 : enable
+ precision mediump float;
+ in vec2 texCoord;
+ out vec4 my_FragColor;
+ uniform sampler2D tex;
+ void main() {
+ vec4 colorAddition = vec4(((gl_ViewID_OVR & 0x4u) != 0u) ? 1.0 : 0.0,
+ ((gl_ViewID_OVR & 0x2u) != 0u) ? 1.0 : 0.0,
+ ((gl_ViewID_OVR & 0x1u) != 0u) ? 1.0 : 0.0,
+ 1.0);
+ my_FragColor = texture(tex, texCoord) + colorAddition * 0.5;
+ }`,
+ ['position', 'texCoordIn'], ['offset', 'tex', 'size']);
+ gl.useProgram(multiviewProgram[i]);
+ gl.uniform1i(multiviewProgram[i].tex, 0);
+ }
+ }
+
+ // Setup vertex buffer
+ buffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STREAM_DRAW);
+ gl.enableVertexAttribArray(program.position);
+ gl.vertexAttribPointer(program.position, 2, gl.FLOAT, false, 0, 0);
+
+ // Setup drawing state
+ gl.viewport(0, 0, size, size);
+ gl.clearColor(1, 1, 1, 1);
+ gl.disable(gl.SCISSOR_TEST);
+ gl.disable(gl.DEPTH_TEST);
+ gl.disable(gl.STENCIL_TEST);
+ gl.disable(gl.BLEND);
+ gl.disable(gl.CULL_FACE);
+
+ updateControls();
+ } // End GL init
+
+ if (useDom.checked) {
+ img.style.left = position[0] + 'px';
+ img.style.top = position[1] + 'px';
+ return;
+ }
+ if (use2D.checked) {
+ if (!ctx) {
+ if (offscreen.checked) {
+ if (transferControlToOffscreen.checked) {
+ offscreenCanvas = canvas.transferControlToOffscreen();
+ } else {
+ bitmapRenderer = canvas.getContext('bitmaprenderer');
+ offscreenCanvas = new OffscreenCanvas(size, size);
+ }
+ ctx = offscreenCanvas.getContext('2d');
+ } else {
+ ctx = canvas.getContext('2d');
+ }
+ }
+ ctx.fillStyle = 'white';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ try {
+ ctx.drawImage(img, position[0], position[1]);
+ } catch (e) {
+ ctx.fillStyle = 'black';
+ ctx.fillRect(position[0], position[1], imgSize, imgSize);
+ }
+ if (offscreen.checked && !transferControlToOffscreen.checked && bitmapRenderer && offscreenCanvas) {
+ bitmapRenderer.transferFromImageBitmap(offscreenCanvas.transferToImageBitmap());
+ }
+ return;
+ }
+
+ // Upload some data to test the PCI bottleneck.
+ if (uploads.value > 0) {
+ if (writeBufferArray.length * 4 != uploads.value * 1024 * 1024) {
+ writeBufferArray = new Float32Array(uploads.value * 1024 * 1024 / 4);
+ // We want to actually use this data in rendering so the graphics driver
+ // can't optimize away the upload. Fill the first few bytes with our real
+ // vertex data.
+ writeBufferArray.set(vertices, 0);
+ }
+ gl.bufferData(gl.ARRAY_BUFFER, writeBufferArray, gl.STREAM_DRAW);
+ }
+
+ // Actually draw the map.
+ const numDrawCalls = Math.max(1, drawCalls.value * multiplier);
+ const numViews = multiviewAvailable ? Math.min(views.value, maxViewsMultiview) : 1;
+ const instances = pixels.value / 64000 * multiplier;
+ const instancesPerCall = Math.max(1, instances / numDrawCalls | 0);
+ const instancesFirstCall = instancesPerCall + numDrawCalls % instancesPerCall;
+ // Set up to scissor out all but one pixel of the map.
+ gl.scissor(position[0], size - position[1] - 1, 1, 1);
+
+ if (multiviewAvailable && (multiview.checked || numViews > 1) && !multiviewFramebuffer) {
+ multiviewTexture = gl.createTexture();
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, multiviewTexture);
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.RGBA8, size, size, maxViewsMultiview);
+ multiviewExt = gl.getExtension('OVR_multiview2');
+ multiviewFramebuffer = gl.createFramebuffer();
+ gl.bindFramebuffer(gl.FRAMEBUFFER, multiviewFramebuffer);
+ multiviewExt.framebufferTextureMultiviewOVR(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, multiviewTexture, 0, 0, numViews);
+ lastNumViews = numViews;
+ for (let i = 0; i < maxViewsMultiview; ++i) {
+ viewFramebuffer[i] = gl.createFramebuffer();
+ gl.bindFramebuffer(gl.FRAMEBUFFER, viewFramebuffer[i]);
+ gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, multiviewTexture, 0, i);
+ }
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
+ }
+
+ let usedProgram = program;
+ if (multiviewAvailable && multiview.checked) {
+ gl.bindFramebuffer(gl.FRAMEBUFFER, multiviewFramebuffer);
+ if (numViews != lastNumViews) {
+ multiviewExt.framebufferTextureMultiviewOVR(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, multiviewTexture, 0, 0, numViews);
+ lastNumViews = numViews;
+ }
+ usedProgram = multiviewProgram[numViews - 1];
+ } else {
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+ }
+
+ gl.useProgram(usedProgram);
+ gl.uniform2f(usedProgram.offset,
+ (position[0] - imgSize) / size * 2,
+ -position[1] / size * 2);
+ gl.uniform1f(usedProgram.size, imgSize * 2 / size);
+
+ if (usedProgram == program) {
+ gl.uniform4f(program.colorAddition, 0.0, 0.0, 0.0, 1.0);
+ }
+
+ for (let viewIndex = 0; viewIndex < numViews; ++viewIndex) {
+ let scissorEnabled = false;
+ if (multiviewAvailable && !multiview.checked && numViews > 1) {
+ gl.bindFramebuffer(gl.FRAMEBUFFER, viewFramebuffer[viewIndex]);
+ gl.uniform4f(program.colorAddition, (viewIndex & 4) ? 1.0 : 0.0, (viewIndex & 2) ? 1.0 : 0.0, (viewIndex & 1) ? 1.0 : 0.0, 1.0);
+ gl.disable(gl.SCISSOR_TEST);
+ }
+ gl.clearColor(1, 1, 1, 1);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+ gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, instancesFirstCall);
+ var instancesDrawn = instancesFirstCall;
+ for (let i = 1; i < numDrawCalls; i++) {
+ if (instancesDrawn > instances && !scissorEnabled) {
+ // If we've drawn all of the requested pixels already, enable the scissor
+ // test so we only draw one pixel per draw call for the rest of the calls.
+ scissorEnabled = true;
+ gl.enable(gl.SCISSOR_TEST);
+ }
+ gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, instancesPerCall);
+ instancesDrawn += instancesPerCall;
+ }
+ if (multiviewAvailable && multiview.checked) {
+ break;
+ }
+ }
+ gl.disable(gl.SCISSOR_TEST);
+
+ if (multiviewAvailable && (multiview.checked || numViews > 1)) {
+ if (multiviewCopy.checked) {
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
+ gl.clearColor(1, 1, 1, 1);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+ let gridCellsPerRow = Math.ceil(Math.sqrt(numViews));
+ for (let i = 0; i < numViews; ++i) {
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, viewFramebuffer[i]);
+ let x = i % gridCellsPerRow;
+ let y = Math.floor(i / gridCellsPerRow);
+ x *= size / gridCellsPerRow;
+ y *= size / gridCellsPerRow;
+ gl.blitFramebuffer(0, 0, size, size, x, y, x + size / gridCellsPerRow, y + size / gridCellsPerRow, gl.COLOR_BUFFER_BIT, gl.NEAREST);
+ }
+ } else {
+ warningText = 'NOTE: Offscreen multiview rendering active - rendering not copied to canvas';
+ }
+ }
+
+ if (finish.checked) {
+ gl.finish();
+ }
+ if (readPixels.checked) {
+ gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, readPixelsArray);
+ }
+ if (offscreen.checked && !transferControlToOffscreen.checked && glOffscreenCanvas && glBitmapRenderer) {
+ glBitmapRenderer.transferFromImageBitmap(glOffscreenCanvas.transferToImageBitmap());
+ }
+}
+
+
+/**************\
+* FPS counters *
+\**************/
+
+
+const counters = {};
+function countFps(name, mouseEventsThisFrame) {
+ let counter = counters[name];
+ if (!counter) {
+ counter = { history: [Date.now() - 16], name: name, count: 0 };
+ counters[name] = counter;
+ }
+ const history = counter.history;
+ history.push(Date.now());
+ while (history.length > 2 &&
+ history[0] + 1000 < history[history.length - 1]) {
+ counter.history.shift();
+ }
+ let averageMs = 0;
+ let maxMs = .1;
+ let minMs = 99999999;
+ for (let i = 1; i < history.length; i++) {
+ let diff = history[i] - history[i - 1];
+ averageMs += diff;
+ maxMs = Math.max(maxMs, diff);
+ minMs = Math.min(minMs, diff);
+ }
+ averageMs /= history.length - 1;
+ counter.fps = 1000 / averageMs;
+ counter.minFps = 1000 / maxMs;
+ counter.maxFps = 1000 / minMs;
+ counter.count++;
+
+ if (mouseEventsThisFrame !== undefined) {
+ counter.mouseEvents = counter.mouseEvents || { multiple: 0, zero: 0 };
+ if (mouseEventsThisFrame > 1) {
+ counter.mouseEvents.multiple++;
+ } else if (mouseEventsThisFrame == 0) {
+ counter.mouseEvents.zero++;
+ }
+ }
+
+ if (showFps.checked) {
+ fpsSpan.innerText = counters['render'].fps.toFixed();
+ if (showStats.checked) {
+ let text = "";
+ for (let key in counters) {
+ counter = counters[key];
+ text += "<b>" + counter.name + "</b><br>";
+ text += counter.fps.toFixed() + " avg FPS<br>";
+ text += "<div class=" +
+ (counter.maxFps - counter.fps > 10 ? "bad" : "") + ">" +
+ counter.maxFps.toFixed() + " max FPS</div>";
+ text += "<div class=" +
+ (counter.fps - counter.minFps > 10 ? "bad" : "") + ">" +
+ counter.minFps.toFixed() + " min FPS</div>";
+ if (counter.mouseEvents) {
+ text += "<div>" + counter.mouseEvents.zero +
+ " frame(s) with no mouse/touch events";
+ text += "<div>" + counter.mouseEvents.multiple +
+ " frame(s) with multiple mouse/touch events";
+ }
+ text += "<div class=light>" + counter.count + " frames</div>";
+ }
+ stats.innerHTML = text;
+ }
+ }
+ if (warningText != displayedWarningText) {
+ warning.textContent = warningText;
+ displayedWarningText = warningText;
+ }
+}
+
+updateControls();
+
+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=';
+</script>