hoverinc/ray-tracing-renderer

View on GitHub
src/renderer/TileRender.js

Summary

Maintainability
B
4 hrs
Test Coverage
F
0%
import { clamp } from './util';

// TileRender is based on the concept of a compute shader's work group.

// Sampling the scene with the RayTracingRenderer can be very slow (<1 fps).
// This overworks the GPU and tends to lock up the OS, making it unresponsive.

// To fix this, we can split the screen into smaller tiles, and sample the scene one tile at a time
// The tile size is set such that each tile takes approximatly a constant amount of time to render.

// Since the render time of a tile is dependent on the device, we find the desired tile dimensions by measuring
// the time it takes to render an arbitrarily-set tile size and adjusting the size according to the benchmark.

export function makeTileRender(gl) {
  const desiredMsPerTile = 21;

  let currentTile = -1;
  let numTiles = 1;

  let tileWidth;
  let tileHeight;

  let columns;
  let rows;

  let width = 0;
  let height = 0;

  let totalElapsedMs;

  // initial number of pixels per rendered tile
  // based on correlation between system performance and max supported render buffer size
  // adjusted dynamically according to system performance
  let pixelsPerTile = pixelsPerTileEstimate(gl);

  function reset() {
    currentTile = -1;
    totalElapsedMs = NaN;
  }

  function setSize(w, h) {
    width = w;
    height = h;
    reset();
    calcTileDimensions();
  }

  function calcTileDimensions() {
    const aspectRatio = width / height;

    // quantize the width of the tile so that it evenly divides the entire window
    tileWidth = Math.ceil(width / Math.round(width / Math.sqrt(pixelsPerTile * aspectRatio)));
    tileHeight = Math.ceil(tileWidth / aspectRatio);

    columns = Math.ceil(width / tileWidth);
    rows = Math.ceil(height / tileHeight);
    numTiles = columns * rows;
  }

  function updatePixelsPerTile() {
    const msPerTile = totalElapsedMs / numTiles;

    const error = desiredMsPerTile - msPerTile;

     // tweak to find balance. higher = faster convergence, lower = less fluctuations to microstutters
    const strength = 5000;

    // sqrt prevents massive fluctuations in pixelsPerTile for the occasional stutter
    pixelsPerTile += strength * Math.sign(error) * Math.sqrt(Math.abs(error));
    pixelsPerTile = clamp(pixelsPerTile, 8192, width * height);
  }

  function nextTile(elapsedFrameMs) {
    currentTile++;
    totalElapsedMs += elapsedFrameMs;

    if (currentTile % numTiles === 0) {
      if (totalElapsedMs) {
        updatePixelsPerTile();
        calcTileDimensions();
      }

      totalElapsedMs = 0;
      currentTile = 0;
    }

    const isLastTile = currentTile === numTiles - 1;

    const x = currentTile % columns;
    const y = Math.floor(currentTile / columns) % rows;

    return {
      x: x * tileWidth,
      y: y * tileHeight,
      tileWidth,
      tileHeight,
      isFirstTile: currentTile === 0,
      isLastTile,
    };
  }

  return {
    nextTile,
    reset,
    setSize,
  };
}

function pixelsPerTileEstimate(gl) {
  const maxRenderbufferSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE);

  if (maxRenderbufferSize <= 8192) {
    return 200000;
  } else if (maxRenderbufferSize === 16384) {
    return 400000;
  } else if (maxRenderbufferSize >= 32768) {
    return 600000;
  }
}