chromium/chrome/test/data/xr/e2e_test_files/html/webxr_test_basic_viewport_scale.html

<!--
Tests for WebXR dynamic viewport scaling.
-->
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="../resources/webxr_e2e.css">
  </head>
  <body>
    <canvas id="webgl-canvas"></canvas>
    <script src="../../../../../../third_party/blink/web_tests/resources/testharness.js"></script>
    <script src="../resources/webxr_e2e.js"></script>
    <script>var shouldAutoCreateNonImmersiveSession = false;</script>
    <script src="../resources/webxr_boilerplate.js"></script>
    <script>
      // Tests that requesting a viewport scale takes effect immediately if done
      // before calling getViewport.
      function stepRequestViewportScaleSameFrame() {
        let sessionInfo = sessionInfos[sessionTypes.AR];
        const referenceSpace = sessionInfo.currentRefSpace;
        const session = sessionInfo.currentSession;

        let frameCount = 0;

        onARFrameCallback = (session, frame) => {
          const pose = frame.getViewerPose(referenceSpace);
          const baseLayer = session.renderState.baseLayer;
          const framebufferPixels = baseLayer.framebufferWidth * baseLayer.framebufferHeight;
          let viewportPixels = 0;
          for (const view of pose.views) {
            view.requestViewportScale(frameCount > 0 ? 0.5 : 1.0);
            let viewport = baseLayer.getViewport(view);
            viewportPixels += viewport.width * viewport.height;

            // Draw something to the framebuffer to ensure that the driver side applies the buffer size.
            gl.bindFramebuffer(gl.FRAMEBUFFER, baseLayer.framebuffer);
            gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
            gl.clearColor(0.0, 0.0, 1.0, 0.5);
            gl.clear(gl.COLOR_BUFFER_BIT);
          }
          updateSingleTestProgressMessage(
            'state:' +
              ' frameCount=' + frameCount +
              ' viewportPixels=' + viewportPixels +
              ' framebufferPixels=' + framebufferPixels);
          assert_greater_than(viewportPixels, 0);
          assert_less_than_equal(viewportPixels, framebufferPixels);
          if (frameCount == 0) {
            // For the ARCore device, assume that the default viewport entirely fills
            // the framebuffer. This isn't required by the spec, so this check may
            // need to be changed for other devices that don't do this.
            assert_equals(viewportPixels, framebufferPixels);
          } else {
            assert_less_than(viewportPixels, framebufferPixels);
            done();
          }
          ++frameCount;
        };
      }

      // Tests that requesting a viewport scale after a preceding getViewport call takes
      // effect on the next animation frame, keeping the viewport consistent within a frame.
      function stepRequestViewportScaleNextFrame() {
        let sessionInfo = sessionInfos[sessionTypes.AR];
        const referenceSpace = sessionInfo.currentRefSpace;
        const session = sessionInfo.currentSession;

        let appliedViewportScaleSameFrame = false;
        let frameCount = 0;

        onARFrameCallback = (session, frame) => {
          const pose = frame.getViewerPose(referenceSpace);
          const baseLayer = session.renderState.baseLayer;
          const framebufferPixels = baseLayer.framebufferWidth * baseLayer.framebufferHeight;
          let viewportPixels = 0;
          for (const view of pose.views) {
            // Call getViewport first before requesting a viewport scale. This locks in
            // the size for the current animation frame.
            const viewportA = baseLayer.getViewport(view);
            viewportPixels += viewportA.width * viewportA.height;

            // Request a size to apply to the next frame.
            view.requestViewportScale(frameCount > 0 ? 0.5 : 1.0);

            // Calling getViewport again must return the same size as the earlier call.
            const viewportB = baseLayer.getViewport(view);
            assert_equals(viewportA.width, viewportB.width);
            assert_equals(viewportA.height, viewportB.height);

            // Draw something to the framebuffer to ensure that the driver side applies the buffer size.
            gl.bindFramebuffer(gl.FRAMEBUFFER, baseLayer.framebuffer);
            gl.viewport(viewportA.x, viewportA.y, viewportA.width, viewportA.height);
            gl.clearColor(0.0, 0.0, 1.0, 0.5);
            gl.clear(gl.COLOR_BUFFER_BIT);
          }
          updateSingleTestProgressMessage(
            'state:' +
              ' frameCount=' + frameCount +
              ' viewportPixels=' + viewportPixels +
              ' framebufferPixels=' + framebufferPixels);
          assert_greater_than(viewportPixels, 0);
          assert_less_than_equal(viewportPixels, framebufferPixels);
          if (frameCount == 1) {
            assert_equals(viewportPixels, framebufferPixels);
          } else if (frameCount > 1) {
            assert_less_than(viewportPixels, framebufferPixels);
            done();
          }
          ++frameCount;
        };
      }

      // Tests that the recommendedViewportScale exists, is reduced when GPU load increases, then
      // increases again when load is low.
      function stepRequestRecommendedViewportScale() {
        let sessionInfo = sessionInfos[sessionTypes.AR];
        const referenceSpace = sessionInfo.currentRefSpace;
        const session = sessionInfo.currentSession;

        let gotAnyRecommendedViewportScale = false;
        let gotReducedRecommendedViewportScale = false;
        let gotIncreasedRecommendedViewportScale = false;
        let clearRectangleCount = 32;
        let minViewportScale = 99999;
        let maxViewportScale = 0;
        let maxRectangles = 0;
        let frameCount = 0;

        onARFrameCallback = (session, frame) => {
          const pose = frame.getViewerPose(referenceSpace);
          gl.enable(gl.SCISSOR_TEST);
          for (const view of pose.views) {
            const recScale = view.recommendedViewportScale;
            if (recScale) {
              if (recScale > 0) {
                gotAnyRecommendedViewportScale = true;
                maxViewportScale = Math.max(maxViewportScale, recScale);
              }
              if (gotReducedRecommendedViewportScale) {
                if (recScale > minViewportScale) {
                  if (!gotIncreasedRecommendedViewportScale) {
                    // Only log the increase once - the session may not terminate immediately after calling done().
                    console.log("got recommendedViewportScale increase from " + minViewportScale + " to " + recScale +
                                ", frameCount=" + frameCount);
                  }
                  gotIncreasedRecommendedViewportScale = true;
                }
              } else {
                if (recScale < maxViewportScale) {
                  gotReducedRecommendedViewportScale = true;
                  minViewportScale=Math.min(minViewportScale, recScale);
                  console.log("got recommendedViewportScale=" + recScale + " at clearRectangleCount=" + clearRectangleCount +
                              ", frameCount=" + frameCount);
                }
              }
            }

            view.requestViewportScale(view.recommendedViewportScale);

            const baseLayer = session.renderState.baseLayer;
            let viewport = baseLayer.getViewport(view);

            // Draw an exponentially increasing number of translucent rectangles to stress the GPU
            // and trigger a decrease of the recommended viewport size. Once the viewport size
            // decreased, revert back to a single rectangle and wait for the size to increase
            // again.
            gl.bindFramebuffer(gl.FRAMEBUFFER, baseLayer.framebuffer);
            gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
            maxRectangles = Math.max(maxRectangles, clearRectangleCount);
            for (let i = 0; i < clearRectangleCount; ++i) {
              const xOffset = viewport.width * Math.random() / 2;
              const yOffset = viewport.height * Math.random() / 2;
              gl.scissor(viewport.x + xOffset, viewport.y + yOffset, viewport.width / 2, viewport.height / 2);
              gl.clearColor(Math.random(), Math.random(), Math.random(), Math.random() / 2);
              gl.clear(gl.COLOR_BUFFER_BIT);
            }
            if (gotReducedRecommendedViewportScale) {
              // Done with the load test, wait for size to increase again.
              clearRectangleCount = 1;
            } else {
              // Add more load every 4th frame. The GPU load feedback is delayed, and we don't want the
              // exponential growth to overload the system excessively since that could cause test timeouts.
              if (frameCount % 4 == 0) {
                clearRectangleCount *= 2;
              }
            }
          }
          updateSingleTestProgressMessage(
            'state:' +
              ' frameCount=' + frameCount +
              ' clearRectangleCount=' + clearRectangleCount +
              ' (max=' + maxRectangles + ')' +
              ' gotAnyRecommendedViewportScale=' + gotAnyRecommendedViewportScale +
              ' gotReducedRecommendedViewportScale=' + gotReducedRecommendedViewportScale +
              ' gotIncreasedRecommendedViewportScale=' + gotIncreasedRecommendedViewportScale);
          gl.disable(gl.SCISSOR_TEST);

          if (gotAnyRecommendedViewportScale &&
              gotReducedRecommendedViewportScale &&
              gotIncreasedRecommendedViewportScale) {
            done();
          } else {
            // Check that the test finishes within a reasonable number of frames.
            // This helps ensure that malfunctioning viewport scaling shows up
            // as a test failure and not just a generic timeout. It's in the "else"
            // branch here to avoid emitting errors for animation frames that are
            // still arriving after the test ended.
            //
            // Since the rectangle count is doubled each frame in the load-increasing
            // test phase, this potentially allows drawing up to 2^30 (~1 billion)
            // rectangles followed by 20 frames of recovery which should be plenty.
            assert_less_than(frameCount, 50, "Viewport scale didn't update within the expected number of frames.")
          }
          ++frameCount;
        };
      }
    </script>
  </body>
</html>