function updateSpecularMaps(manager, frameState) {
  const radianceCubeMap = manager._radianceCubeMap;
  radianceCubeMap.generateMipmap();

  const mipmapLevels = manager._mipmapLevels;
  const textureDimensions = manager._textureDimensions;
  let width = textureDimensions.x / 2;
  let height = textureDimensions.y / 2;
  const context = frameState.context;

  let facesCopied = 0;
  const getPostExecute = (command, index, texture, face, level) => () => {
    if (manager.isDestroyed() || command.canceled) {
      DynamicEnvironmentMapManager._activeComputeCommandCount--;
      return;
    }

    // Copy output texture to corresponding face and mipmap level
    const commands = manager._convolutionComputeCommands;
    commands[index] = undefined;

    radianceCubeMap.copyFace(frameState, texture, face, level);
    facesCopied++;
    DynamicEnvironmentMapManager._activeComputeCommandCount--;

    texture.destroy();
    manager._specularMapTextures[index] = undefined;

    // All faces for each mipmap level have been copied
    const length = manager._specularMapTextures.length;
    if (facesCopied >= length) {
      manager._irradianceCommandDirty = true;
      radianceCubeMap.sampler = new Sampler({
        minificationFilter: TextureMinificationFilter.LINEAR_MIPMAP_LINEAR,
      });

      manager._shouldRegenerateShaders = true;

      // Cleanup shared resources
      manager._va.destroy();
      manager._va = undefined;
      manager._convolveSP.destroy();
      manager._convolveSP = undefined;
    }
  };

  let index = 0;
  for (let level = 1; level < mipmapLevels; ++level) {
    for (const face of CubeMap.faceNames()) {
      if (defined(manager._specularMapTextures[index])) {
        manager._specularMapTextures[index].destroy();
      }

      const texture = (manager._specularMapTextures[index] = new Texture({
        context: context,
        width: width,
        height: height,
        pixelDatatype: PixelDatatype.UNSIGNED_BYTE,
        pixelFormat: PixelFormat.RGBA,
      }));

      let vertexArray = manager._va;
      if (!defined(vertexArray)) {
        vertexArray = CubeMap.createVertexArray(context, face);
        manager._va = vertexArray;
      }

      let shaderProgram = manager._convolveSP;
      if (!defined(shaderProgram)) {
        shaderProgram = ShaderProgram.fromCache({
          context: context,
          vertexShaderSource: ConvolveSpecularMapVS,
          fragmentShaderSource: ConvolveSpecularMapFS,
          attributeLocations: {
            positions: 0,
          },
        });
        manager._convolveSP = shaderProgram;
      }

      const command = new ComputeCommand({
        shaderProgram: shaderProgram,
        vertexArray: vertexArray,
        outputTexture: texture,
        // Persist so we can use a shared shader progam and vertex array across all commands
        // Shared resources are instead destroyed in postExecute
        persists: true,
        owner: manager,
        uniformMap: {
          u_roughness: () => level / (mipmapLevels - 1),
          u_radianceTexture: () => radianceCubeMap ?? context.defaultTexture,
          u_faceDirection: () => {
            return CubeMap.getDirection(face, scratchCartesian);
          },
        },
      });
      command.postExecute = getPostExecute(
        command,
        index,
        texture,
        face,
        level,
      );
      manager._convolutionComputeCommands[index] = command;
      DynamicEnvironmentMapManager._queueCommand(command, frameState);
      ++index;
    }

    width /= 2;
    height /= 2;
  }
}
Creates a mipmap chain for the cubemap by convolving the environment map for each roughness level
@param {DynamicEnvironmentMapManager} manager this manager
@param {FrameState} frameState the current frameState
@private