summaryrefslogtreecommitdiffstats
path: root/testing/talos/talos/tests/webgl/benchmarks/terrain/perftest.html
blob: a4721625526b6b4df3477adee5531a40f5797223 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
<html>
<!--
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.

 Version 1.1
   - Added viewport rotation.
   - Adapted for talos with with 4 runs on combinations of alpha/antialias

 Version 1.0
   - Benoit Jacob's WebGL tutorial demos: http://bjacob.github.io/webgl-tutorial/12-texture.html
-->
<head>
<meta charset="utf-8"/>
<script src="resource://talos-powers/TalosContentProfiler.js"></script>
<script type="x-shader/x-vertex" id="vertexShader">
  attribute vec3 vertexPosition;
  attribute vec3 normalVector;
  attribute vec2 textureCoord;
  uniform mat4 modelview;
  uniform mat4 projection;
  varying vec3 varyingNormalVector;
  varying vec2 varyingTextureCoord;
  void main(void) {
    gl_Position = projection * modelview * vec4(vertexPosition, 1.0);
    varyingNormalVector = normalVector;
    varyingTextureCoord = textureCoord;
  }
</script>
<script type="x-shader/x-fragment" id="fragmentShader">
  precision mediump float;
  varying vec3 varyingNormalVector;
  uniform vec3 lightDirection;
  uniform sampler2D grassTextureSampler;
  varying vec2 varyingTextureCoord;
  void main(void) {
    vec3 grassColor = texture2D(grassTextureSampler, varyingTextureCoord).rgb;
    const float ambientLight = 0.3;
    const float diffuseLight = 0.7;
    float c = clamp(dot(normalize(varyingNormalVector), lightDirection), 0.0, 1.0);
    vec3 resultColor = grassColor * (c * diffuseLight + ambientLight);
    gl_FragColor = vec4(resultColor, 1);
  }
</script>
<script>
  var gl;

  var modelviewUniformLoc;
  var projectionUniformLoc;
  var lightDirectionUniformLoc;
  var grassTextureSamplerUniformLoc;

  var modelviewMatrix = new Float32Array(16);
  var projectionMatrix = new Float32Array(16);

  var terrainSize = 32;
  var aspectRatio;

  function startTest(alpha, antialias, doneCallback) {
    gl = document.getElementById("c").getContext("webgl", {alpha, antialias});

    var grassImage = document.getElementById("grass");
    var grassTexture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, grassTexture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, grassImage);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
    gl.generateMipmap(gl.TEXTURE_2D);

    var vertexShader = gl.createShader(gl.VERTEX_SHADER);
    var vertexShaderString = document.getElementById("vertexShader").text;
    gl.shaderSource(vertexShader, vertexShaderString);
    gl.compileShader(vertexShader);

    var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
    var fragmentShaderString = document.getElementById("fragmentShader").text;
    gl.shaderSource(fragmentShader, fragmentShaderString);
    gl.compileShader(fragmentShader);

    var program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);
    gl.useProgram(program);

    var vertexPositionAttrLoc = gl.getAttribLocation(program, "vertexPosition");
    gl.enableVertexAttribArray(vertexPositionAttrLoc);
    var normalVectorAttrLoc = gl.getAttribLocation(program, "normalVector");
    gl.enableVertexAttribArray(normalVectorAttrLoc);
    var textureCoordAttrLoc = gl.getAttribLocation(program, "textureCoord");
    gl.enableVertexAttribArray(textureCoordAttrLoc);

    modelviewUniformLoc = gl.getUniformLocation(program, "modelview");
    projectionUniformLoc = gl.getUniformLocation(program, "projection");
    lightDirectionUniformLoc = gl.getUniformLocation(program, "lightDirection");
    grassTextureSamplerUniformLoc = gl.getUniformLocation(program, "grassTextureSampler");

    var vertices = new Float32Array(terrainSize * terrainSize * 3);
    var normalVectors = new Float32Array(terrainSize * terrainSize * 3);
    var textureCoords = new Float32Array(terrainSize * terrainSize * 2);

    for (var i = 0; i < terrainSize; i++) {
      for (var j = 0; j < terrainSize; j++) {
        var a = 2 * Math.PI * i / terrainSize;
        var b = 2 * Math.PI * j / terrainSize;
        var height = 4 * Math.cos(a) + 6 * Math.sin(b) + Math.cos(4 * a) + Math.sin(5 * b);
        vertices[3 * (i + terrainSize * j) + 0] = i;
        vertices[3 * (i + terrainSize * j) + 1] = height;
        vertices[3 * (i + terrainSize * j) + 2] = j;

        var d_y_d_x = (2 * Math.PI / terrainSize) * (-4 * Math.sin(a) - 4 * Math.sin(4 * a));
        var d_y_d_z = (2 * Math.PI / terrainSize) * (6 * Math.cos(b) + 5 * Math.cos(5 * b));
        var normal_x = d_y_d_x;
        var normal_y = -1;
        var normal_z = d_y_d_z;
        var normal_length = Math.sqrt(normal_x * normal_x + normal_y * normal_y + normal_z * normal_z);
        normalVectors[3 * (i + terrainSize * j) + 0] = normal_x / normal_length;
        normalVectors[3 * (i + terrainSize * j) + 1] = normal_y / normal_length;
        normalVectors[3 * (i + terrainSize * j) + 2] = normal_z / normal_length;

        var textureRepeatingSpeed = 0.5;
        textureCoords[2 * (i + terrainSize * j) + 0] = i * textureRepeatingSpeed;
        textureCoords[2 * (i + terrainSize * j) + 1] = j * textureRepeatingSpeed;
      }
    }

    var vertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexPositionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
    gl.vertexAttribPointer(vertexPositionAttrLoc, 3, gl.FLOAT, false, 0, 0);

    var normalVectorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, normalVectorBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, normalVectors, gl.STATIC_DRAW);
    gl.vertexAttribPointer(normalVectorAttrLoc, 3, gl.FLOAT, false, 0, 0);

    var textureCoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, textureCoords, gl.STATIC_DRAW);
    gl.vertexAttribPointer(textureCoordAttrLoc, 2, gl.FLOAT, false, 0, 0);

    var indexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    var indices = [];
    for (var k = 0; k < terrainSize - 1; k++) {
      for (var l = 0; l < terrainSize - 1; l++) {
        indices.push(k + terrainSize * l);
        indices.push(k + 1 + terrainSize * l);
        indices.push(k + 1 + terrainSize * (l + 1));

        indices.push(k + terrainSize * l);
        indices.push(k + 1 + terrainSize * (l + 1));
        indices.push(k + terrainSize * (l + 1));
      }
    }
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);

    gl.enable(gl.DEPTH_TEST);

    onresize();
    window.addEventListener("resize", onresize);

    startTestLoop(doneCallback);
  }

  function average(arr) {
    var sum = 0;
    for (var i in arr)
      sum += arr[i];
    return sum / (arr.length || 1);
  }

  // We use sample stddev and not population stddev because
  // well.. it's a sample and we can't collect all/infinite number of frames.
  function sampleStdDev(arr) {
    if (arr.length <= 1) {
      return 0;
    }
    var avg = average(arr);

    var squareDiffArr = arr.map( function(v) { return Math.pow(v - avg, 2); } );
    var sum = squareDiffArr.reduce( function(a, b) { return a + b; } );
    var rv = Math.sqrt(sum / (arr.length - 1));
    return rv;
  }

  const PRETEST_DELAY_MS  = 500;
  const WARMUP_TIMESTAMPS = 30; // Must be at least 2
  const MEASURED_FRAMES   = 100;

  var gDoneCallback = function placeholder(intervals) {};
  var gCurrentTimestamp = 0;
  var gResultTimestamps = [];

  function startTestLoop(doneCallback) {
    gDoneCallback = doneCallback;
    gCurrentTimestamp = 0;
    gResultTimestamps = new Array(WARMUP_TIMESTAMPS + MEASURED_FRAMES);

    TalosContentProfiler.subtestStart("Starting requestAnimationFrame loop", true).then(() => {
      requestAnimationFrame(draw);
    });
  }

  function draw(timestamp) {
    // It's possible that under some implementations (even if not our current one),
    // the rAF callback arg will be in some way "optimized", e.g. always point to the
    // estimated next vsync timestamp, in order to allow the callee to have less
    // jitter in its time-dependent positioning/processing.
    // Such behaviour would harm our measurements, especially with vsync off.
    // performance.now() will not have this potential issue and is high-resolution.
    gResultTimestamps[gCurrentTimestamp++] = performance.now();
    var recordedTimestamps = gCurrentTimestamp;
    if (recordedTimestamps >= WARMUP_TIMESTAMPS + MEASURED_FRAMES) {
      TalosContentProfiler.subtestEnd("requestAnimationFrame loop", true).then(() => {
        var intervals = [];
        var lastWarmupTimestampId = WARMUP_TIMESTAMPS - 1;
        for (var i = lastWarmupTimestampId + 1; i < gResultTimestamps.length; i++) {
          intervals.push(gResultTimestamps[i] - gResultTimestamps[i - 1]);
        }

        gDoneCallback(intervals);
      });

      return;
    }

    // Used for rendering reproducible frames which are independent of actual performance (timestamps).
    var simulatedTimestamp = gCurrentTimestamp * 1000 / 60;

    var speed = 0.001;
    var angle =  simulatedTimestamp * speed;
    var c = Math.cos(angle / 10);
    var s = Math.sin(angle / 10);
    var light_x = Math.cos(angle);
    var light_y = -1;
    var light_z = Math.sin(angle);
    var l = Math.sqrt(light_x * light_x + light_y * light_y + light_z * light_z);
    light_x /= l;
    light_y /= l;
    light_z /= l;
    gl.uniform3f(lightDirectionUniformLoc, light_x, light_y, light_z);

    modelviewMatrix[0]  = c;
    modelviewMatrix[1]  = 0;
    modelviewMatrix[2]  = s;
    modelviewMatrix[3]  = 0;

    modelviewMatrix[4]  = 0;
    modelviewMatrix[5]  = 1;
    modelviewMatrix[6]  = 0;
    modelviewMatrix[7]  = 0;

    modelviewMatrix[8]  = -s;
    modelviewMatrix[9]  = 0;
    modelviewMatrix[10] = c;
    modelviewMatrix[11] = 0;

    modelviewMatrix[12] = -terrainSize / 2;
    modelviewMatrix[13] = 0;
    modelviewMatrix[14] = -terrainSize;
    modelviewMatrix[15] = 1;

    gl.uniformMatrix4fv(modelviewUniformLoc, false, modelviewMatrix);

    var fieldOfViewY = Math.PI / 4;
    aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight;
    var zNear = 1;
    var zFar = terrainSize;

    projectionMatrix[0]  = fieldOfViewY / aspectRatio;
    projectionMatrix[1]  = 0;
    projectionMatrix[2]  = 0;
    projectionMatrix[3]  = 0;

    projectionMatrix[4]  = 0;
    projectionMatrix[5]  = fieldOfViewY;
    projectionMatrix[6]  = 0;
    projectionMatrix[7]  = 0;

    projectionMatrix[8]  = 0;
    projectionMatrix[9]  = 0;
    projectionMatrix[10] = (zNear + zFar) / (zNear - zFar);
    projectionMatrix[11] = -1;

    projectionMatrix[12] = 0;
    projectionMatrix[13] = 0;
    projectionMatrix[14] = 2 * zNear * zFar / (zNear - zFar);
    projectionMatrix[15] = 0;

    gl.uniformMatrix4fv(projectionUniformLoc, false, projectionMatrix);

    gl.uniform1i(grassTextureSamplerUniformLoc, 0);

    gl.clearColor(0.4, 0.6, 1.0, 0.5);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    gl.drawElements(gl.TRIANGLES, (terrainSize - 1) * (terrainSize - 1) * 6, gl.UNSIGNED_SHORT, 0);

    if (gCurrentTimestamp == 1) {
      // First frame only - wait a bit (after rendering the scene once)
      requestAnimationFrame(function() {
        setTimeout(requestAnimationFrame, PRETEST_DELAY_MS, draw);
      });
    } else {
      requestAnimationFrame(draw);
    }
  }

  function onresize() {
    var canvas =  document.getElementById("c");
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    aspectRatio = canvas.width / canvas.height;
    gl.viewport(0, 0, canvas.width, canvas.height);
  }

  var gResults = {values: [], names: [], raw: []};

  function setupAndRun(alpha, antialias, name, doneCallback) {
    // Remove the old canvas if exists, and create a new one for the new run
    var c = document.getElementById("c");
    if (c)
        c.remove();
    c = document.createElement("canvas");
    c.id = "c";
    document.body.insertBefore(c, document.body.firstChild);

    // Trigger the run with specified args, with callback function to process the result
    startTest(alpha, antialias, function(intervals) {
        gResults.names.push(name);
        gResults.raw.push(intervals);
        setTimeout(doneCallback, 0);
    });
  }

  function reportResults(results) {
    // Format a nice human-readable report
    var msg = "";
    for (var i = 0; i < results.names.length; i++) {
      var data = results.raw[i];
      var avg   = average(data);
      gResults.values.push(avg); // This is the only "official" reported value for this set
      var sd = sampleStdDev(data);

      // Compose a nice human readable message. Not used officially anywhere.
      msg += results.names[i] + "= Average: " + avg.toFixed(2)
           + "  stddev: " + sd.toFixed(1) + " (" + (100 * sd / avg).toFixed(1) + "%)"
           + "\nIntervals: " + data.map(function(v) {
                // Display with 1 decimal point digit, and make excessively big values noticeable.
                var value = v.toFixed(1);
                // Not scientific, but intervals over 2 * average are.. undesirable.
                var threshold = avg * 2;
                return v < threshold ? value : " [_" + value + "_] ";
             }).join("  ")
           + "\n\n";
    }
    dump(msg); // Put the readable report at talos run-log

    if (window.tpRecordTime) {
      // within talos - report the results
      return tpRecordTime(results.values.join(","), 0, results.names.join(","));
    }
    // Local run in a plain browser, display the formatted report
    alert(msg);
    return undefined;
  }

  // The full test starts here
  function test() {
    // We initially hide the <body>, to reduce the chance of spinning our wheels
    // with incremental ASAP-paint-mode paints during pageload. Now that onload
    // has fired, we un-hide it:
    document.body.style.display = "";

    gResults = {values: [], names: [], raw: []};
    // This test measures average frame interval during WebGL animation as follows:
    // 1. Creates a new WebGL canvas.
    // 2. setup the scene and render 1 frame.
    // 3. Idle a bit (500ms).
    // 4. Render/Animate several (129) frames using requestAnimationFrame.
    // 5. Average the intervals of the last (100) frames <-- the result.
    //
    // The reason for the initial idle is to allow some internal cleanups (like CC/GC etc).
    // The unmeasured warmup rendering intervals are to allow Firefox to settle at a consistent rate.
    // The idle + warmup are common practices in benchmarking where we're mostly interested
    // in the stable/consistent throughput rather than in the throughput during transition state
    // from idle to iterating.

    // Run the same sequence 4 times for all combination of alpha and antialias
    // (Not using promises chaining for better compatibility, ending up with nesting instead)
    // talos unfortunately trims common prefixes, so we prefix with a running number to keep the full name
    setupAndRun(false, false, "0.WebGL-terrain-alpha-no-AA-no", function() {
      setupAndRun(false, true, "1.WebGL-terrain-alpha-no-AA-yes", function() {
        setupAndRun(true, false, "2.WebGL-terrain-alpha-yes-AA-no", function() {
          setupAndRun(true, true, "3.WebGL-terrain-alpha-yes-AA-yes", function() {
            reportResults(gResults);
          });
        });
      });
    });
  }
</script>
</head>
<body onload="test();" style="overflow:hidden; margin:0; display:none">
<canvas id="c"></canvas>
<img src="grass.jpeg" style="display:none" id="grass"/>
</body>