function getTextRenderInfo(args, callback) {
  hasRequested = true
  args = assign({}, args)
  const totalStart = now()

  // Convert relative URL to absolute so it can be resolved in the worker, and add fallbacks.
  // In the future we'll allow args.font to be a list with unicode ranges too.
  const { defaultFontURL } = CONFIG
  const fonts = [];
  if (defaultFontURL) {
    fonts.push({label: 'default', src: toAbsoluteURL(defaultFontURL)})
  }
  if (args.font) {
    fonts.push({label: 'user', src: toAbsoluteURL(args.font)})
  }
  args.font = fonts

  // Normalize text to a string
  args.text = '' + args.text

  args.sdfGlyphSize = args.sdfGlyphSize || CONFIG.sdfGlyphSize
  args.unicodeFontsURL = args.unicodeFontsURL || CONFIG.unicodeFontsURL

  // Normalize colors
  if (args.colorRanges != null) {
    let colors = {}
    for (let key in args.colorRanges) {
      if (args.colorRanges.hasOwnProperty(key)) {
        let val = args.colorRanges[key]
        if (typeof val !== 'number') {
          val = tempColor.set(val).getHex()
        }
        colors[key] = val
      }
    }
    args.colorRanges = colors
  }

  Object.freeze(args)

  // Init the atlas if needed
  const {textureWidth, sdfExponent} = CONFIG
  const {sdfGlyphSize} = args
  const glyphsPerRow = (textureWidth / sdfGlyphSize * 4)
  let atlas = atlases[sdfGlyphSize]
  if (!atlas) {
    const canvas = document.createElement('canvas')
    canvas.width = textureWidth
    canvas.height = sdfGlyphSize * 256 / glyphsPerRow // start tall enough to fit 256 glyphs
    atlas = atlases[sdfGlyphSize] = {
      glyphCount: 0,
      sdfGlyphSize,
      sdfCanvas: canvas,
      sdfTexture: new Texture(
        canvas,
        undefined,
        undefined,
        undefined,
        LinearFilter,
        LinearFilter
      ),
      contextLost: false,
      glyphsByFont: new Map()
    }
    atlas.sdfTexture.generateMipmaps = false
    initContextLossHandling(atlas)
  }

  const {sdfTexture, sdfCanvas} = atlas

  // Issue request to the typesetting engine in the worker
  const typeset = CONFIG.useWorker ? typesetInWorker : typesetOnMainThread
  typeset(args).then(result => {
    const {glyphIds, glyphFontIndices, fontData, glyphPositions, fontSize, timings} = result
    const neededSDFs = []
    const glyphBounds = new Float32Array(glyphIds.length * 4)
    let boundsIdx = 0
    let positionsIdx = 0
    const quadsStart = now()

    const fontGlyphMaps = fontData.map(font => {
      let map = atlas.glyphsByFont.get(font.src)
      if (!map) {
        atlas.glyphsByFont.set(font.src, map = new Map())
      }
      return map
    })

    glyphIds.forEach((glyphId, i) => {
      const fontIndex = glyphFontIndices[i]
      const {src: fontSrc, unitsPerEm} = fontData[fontIndex]
      let glyphInfo = fontGlyphMaps[fontIndex].get(glyphId)

      // If this is a glyphId not seen before, add it to the atlas
      if (!glyphInfo) {
        const {path, pathBounds} = result.glyphData[fontSrc][glyphId]

        // Margin around path edges in SDF, based on a percentage of the glyph's max dimension.
        // Note we add an extra 0.5 px over the configured value because the outer 0.5 doesn't contain
        // useful interpolated values and will be ignored anyway.
        const fontUnitsMargin = Math.max(pathBounds[2] - pathBounds[0], pathBounds[3] - pathBounds[1])
          / sdfGlyphSize * (CONFIG.sdfMargin * sdfGlyphSize + 0.5)

        const atlasIndex = atlas.glyphCount++
        const sdfViewBox = [
          pathBounds[0] - fontUnitsMargin,
          pathBounds[1] - fontUnitsMargin,
          pathBounds[2] + fontUnitsMargin,
          pathBounds[3] + fontUnitsMargin,
        ]
        fontGlyphMaps[fontIndex].set(glyphId, (glyphInfo = { path, atlasIndex, sdfViewBox }))

        // Collect those that need SDF generation
        neededSDFs.push(glyphInfo)
      }

      // Calculate bounds for renderable quads
      // TODO can we get this back off the main thread?
      const {sdfViewBox} = glyphInfo
      const posX = glyphPositions[positionsIdx++]
      const posY = glyphPositions[positionsIdx++]
      const fontSizeMult = fontSize / unitsPerEm
      glyphBounds[boundsIdx++] = posX + sdfViewBox[0] * fontSizeMult
      glyphBounds[boundsIdx++] = posY + sdfViewBox[1] * fontSizeMult
      glyphBounds[boundsIdx++] = posX + sdfViewBox[2] * fontSizeMult
      glyphBounds[boundsIdx++] = posY + sdfViewBox[3] * fontSizeMult

      // Convert glyphId to SDF index for the shader
      glyphIds[i] = glyphInfo.atlasIndex
    })
    timings.quads = (timings.quads || 0) + (now() - quadsStart)

    const sdfStart = now()
    timings.sdf = {}

    // Grow the texture height by power of 2 if needed
    const currentHeight = sdfCanvas.height
    const neededRows = Math.ceil(atlas.glyphCount / glyphsPerRow)
    const neededHeight = Math.pow(2, Math.ceil(Math.log2(neededRows * sdfGlyphSize)))
    if (neededHeight > currentHeight) {
      // Since resizing the canvas clears its render buffer, it needs special handling to copy the old contents over
      console.info(`Increasing SDF texture size ${currentHeight}->${neededHeight}`)
      resizeWebGLCanvasWithoutClearing(sdfCanvas, textureWidth, neededHeight)
      // As of Three r136 textures cannot be resized once they're allocated on the GPU, we must dispose to reallocate it
      sdfTexture.dispose()
    }

    Promise.all(neededSDFs.map(glyphInfo =>
      generateGlyphSDF(glyphInfo, atlas, args.gpuAccelerateSDF).then(({timing}) => {
        timings.sdf[glyphInfo.atlasIndex] = timing
      })
    )).then(() => {
      if (neededSDFs.length && !atlas.contextLost) {
        safariPre15Workaround(atlas)
        sdfTexture.needsUpdate = true
      }
      timings.sdfTotal = now() - sdfStart
      timings.total = now() - totalStart
      // console.log(`SDF - ${timings.sdfTotal}, Total - ${timings.total - timings.fontLoad}`)

      // Invoke callback with the text layout arrays and updated texture
      callback(Object.freeze({
        parameters: args,
        sdfTexture,
        sdfGlyphSize,
        sdfExponent,
        glyphBounds,
        glyphAtlasIndices: glyphIds,
        glyphColors: result.glyphColors,
        caretPositions: result.caretPositions,
        chunkedBounds: result.chunkedBounds,
        ascender: result.ascender,
        descender: result.descender,
        lineHeight: result.lineHeight,
        capHeight: result.capHeight,
        xHeight: result.xHeight,
        topBaseline: result.topBaseline,
        blockBounds: result.blockBounds,
        visibleBounds: result.visibleBounds,
        timings: result.timings,
      }))
    })
  })

  // While the typesetting request is being handled, go ahead and make sure the atlas canvas context is
  // "warmed up"; the first request will be the longest due to shader program compilation so this gets
  // a head start on that process before SDFs actually start getting processed.
  Promise.resolve().then(() => {
    if (!atlas.contextLost) {
      warmUpSDFCanvas(sdfCanvas)
    }
  })
}
Main entry point for requesting the data needed to render a text string with given font parameters.
This is an asynchronous call, performing most of the logic in a web worker thread.
@param {TypesetParams} args
@param {getTextRenderInfo~callback} callback