function updateTiles(tileset, frameState, passOptions) {
  tileset._styleEngine.applyStyle(tileset);
  tileset._styleApplied = true;

  const { commandList, context } = frameState;
  const numberOfInitialCommands = commandList.length;
  const selectedTiles = tileset._selectedTiles;

  const bivariateVisibilityTest =
    tileset.isSkippingLevelOfDetail &&
    tileset._hasMixedContent &&
    context.stencilBuffer &&
    selectedTiles.length > 0;

  tileset._backfaceCommands.length = 0;

  if (bivariateVisibilityTest) {
    if (!defined(tileset._stencilClearCommand)) {
      tileset._stencilClearCommand = new ClearCommand({
        stencil: 0,
        pass: Pass.CESIUM_3D_TILE,
        renderState: RenderState.fromCache({
          stencilMask: StencilConstants.SKIP_LOD_MASK,
        }),
      });
    }
    commandList.push(tileset._stencilClearCommand);
  }

  const { statistics, tileVisible } = tileset;
  const isRender = passOptions.isRender;
  const lengthBeforeUpdate = commandList.length;

  for (let i = 0; i < selectedTiles.length; ++i) {
    const tile = selectedTiles[i];
    // Raise the tileVisible event before update in case the tileVisible event
    // handler makes changes that update needs to apply to WebGL resources
    if (isRender) {
      tileVisible.raiseEvent(tile);
    }
    processUpdateHeight(tileset, tile, frameState);
    tile.update(tileset, frameState, passOptions);
    statistics.incrementSelectionCounts(tile.content);
    ++statistics.selected;
  }
  const emptyTiles = tileset._emptyTiles;
  for (let i = 0; i < emptyTiles.length; ++i) {
    const tile = emptyTiles[i];
    tile.update(tileset, frameState, passOptions);
  }

  let addedCommandsLength = commandList.length - lengthBeforeUpdate;

  tileset._backfaceCommands.trim();

  if (bivariateVisibilityTest) {
    /*
     * Consider 'effective leaf' tiles as selected tiles that have no selected descendants. They may have children,
     * but they are currently our effective leaves because they do not have selected descendants. These tiles
     * are those where with tile._finalResolution === true.
     * Let 'unresolved' tiles be those with tile._finalResolution === false.
     *
     * 1. Render just the backfaces of unresolved tiles in order to lay down z
     * 2. Render all frontfaces wherever tile._selectionDepth > stencilBuffer.
     *    Replace stencilBuffer with tile._selectionDepth, when passing the z test.
     *    Because children are always drawn before ancestors {@link Cesium3DTilesetTraversal#traverseAndSelect},
     *    this effectively draws children first and does not draw ancestors if a descendant has already
     *    been drawn at that pixel.
     *    Step 1 prevents child tiles from appearing on top when they are truly behind ancestor content.
     *    If they are behind the backfaces of the ancestor, then they will not be drawn.
     *
     * NOTE: Step 2 sometimes causes visual artifacts when backfacing child content has some faces that
     * partially face the camera and are inside of the ancestor content. Because they are inside, they will
     * not be culled by the depth writes in Step 1, and because they partially face the camera, the stencil tests
     * will draw them on top of the ancestor content.
     *
     * NOTE: Because we always render backfaces of unresolved tiles, if the camera is looking at the backfaces
     * of an object, they will always be drawn while loading, even if backface culling is enabled.
     */

    const backfaceCommands = tileset._backfaceCommands.values;
    const backfaceCommandsLength = backfaceCommands.length;

    commandList.length += backfaceCommandsLength;

    // copy commands to the back of the commandList
    for (let i = addedCommandsLength - 1; i >= 0; --i) {
      commandList[lengthBeforeUpdate + backfaceCommandsLength + i] =
        commandList[lengthBeforeUpdate + i];
    }

    // move backface commands to the front of the commandList
    for (let i = 0; i < backfaceCommandsLength; ++i) {
      commandList[lengthBeforeUpdate + i] = backfaceCommands[i];
    }
  }

  // Number of commands added by each update above
  addedCommandsLength = commandList.length - numberOfInitialCommands;
  statistics.numberOfCommands = addedCommandsLength;

  if (!isRender) {
    return;
  }

  // Only run EDL if simple attenuation is on
  if (
    tileset.pointCloudShading.attenuation &&
    tileset.pointCloudShading.eyeDomeLighting &&
    addedCommandsLength > 0
  ) {
    tileset._pointCloudEyeDomeLighting.update(
      frameState,
      numberOfInitialCommands,
      tileset.pointCloudShading,
      tileset.boundingSphere,
    );
  }

  if (
    tileset.debugShowGeometricError ||
    tileset.debugShowRenderingStatistics ||
    tileset.debugShowMemoryUsage ||
    tileset.debugShowUrl
  ) {
    if (!defined(tileset._tileDebugLabels)) {
      tileset._tileDebugLabels = new LabelCollection();
    }
    updateTileDebugLabels(tileset, frameState);
  } else {
    tileset._tileDebugLabels =
      tileset._tileDebugLabels && tileset._tileDebugLabels.destroy();
  }
}
@private
@param {Cesium3DTileset} tileset
@param {FrameState} frameState
@param {object} passOptions