Package limn.graphics

Interface ReadableSurface

All Superinterfaces:
GpuSurface
All Known Subinterfaces:
RenderTarget

public interface ReadableSurface extends GpuSurface
A GpuSurface whose pixels can be brought back to the CPU. Rendering into a surface and compositing it into the scene needs none of this; reading one is what an export, a thumbnail or a reference image for a rendering test needs, and it is a capability rather than a promise of GpuSurface; a surface that wraps memory it does not own (a decoder's picture, say) can composite without being able to hand its pixels over.

Which of the two reads you want is a colour-space decision, and it is the one thing here that is easy to get silently wrong. An offscreen target holds scene-referred linear light; the display transform (exposure, tonemap, sRGB encode) runs later, when the surface is composited. So the numbers in the surface and the picture a person saw are two different images, and the methods are named apart rather than separated by a flag so that neither can be reached without choosing it:

  • readDisplayReferred(): what was on screen. Exposure, tonemap and sRGB encode applied, alpha un-premultiplied, 8 bits per channel. For anything a person will look at.
  • readSceneReferred(): what the renderer wrote. Linear, premultiplied, float, no transform. For further processing, or for a test that asserts on the pass's own output.

Both are synchronous GPU reads: they stall the pipeline until everything queued has finished, which on a frame path shows up as a frame-time cliff that profiles as "the renderer got slow" rather than as this call. Read outside the frames that matter, or accept the cost knowingly; the encode that usually follows can move off the UI thread (Images.saveAsync(limn.graphics.Image, limn.graphics.ImageEncodeOptions, java.nio.file.Path)), but the read itself cannot.

Both must be called on the UI thread with the owning window's GL context current, i.e. from inside a frame, like every other GpuSurface operation.

Coordinates are device pixels with the origin at the top-left, matching Image and the rest of this package, not the bottom-up convention of the underlying graphics API. The returned rows are top-down for the same reason. Whatever flip that costs belongs here: an encoder takes Image as it finds it, and a second flip at encode time would cancel this one for one path and not another.

  • Method Summary

    Modifier and Type
    Method
    Description
    default Image
    Reads the whole surface as the display saw it.
    readDisplayReferred(int x, int y, int widthPx, int heightPx)
    Reads a rectangle of this surface as the display saw it: the surface's display transform (exposure, RenderTarget.exposure() where the surface has one, then tonemap, then sRGB encode) applied once, alpha divided back out to straight, quantized to 8 bits per channel.
    default ScenePixels
    Reads the whole surface as the renderer wrote it.
    readSceneReferred(int x, int y, int widthPx, int heightPx)
    Reads a rectangle of this surface exactly as the renderer wrote it: linear light, premultiplied, float, no exposure and no tonemap.

    Methods inherited from interface limn.graphics.GpuSurface

    dispose, heightPx, resize, widthPx
  • Method Details

    • readDisplayReferred

      Image readDisplayReferred(int x, int y, int widthPx, int heightPx)
      Reads a rectangle of this surface as the display saw it: the surface's display transform (exposure, RenderTarget.exposure() where the surface has one, then tonemap, then sRGB encode) applied once, alpha divided back out to straight, quantized to 8 bits per channel.

      Applied once is the contract, and the mirror-image mistake is worth naming: a surface whose contents are already display-referred must return them unchanged rather than transforming them again. Reading a window's framebuffer, which has already been through the composite, is not this method; it is GpuRenderer.captureFramebuffer.

      Where alpha is 0 the result is a fully transparent pixel with all colour channels 0. Un-premultiplying is a division by alpha, so it loses precision as alpha approaches 0 and has no answer at all when alpha is exactly 0; this picks the one answer that composites back to the same picture.

      Parameters:
      x - left edge in device pixels, 0 at the left of the surface
      y - top edge in device pixels, 0 at the top of the surface
      widthPx - rectangle width, at least 1
      heightPx - rectangle height, at least 1
      Returns:
      a new image of exactly widthPx * heightPx, straight alpha, top-down
      Throws:
      IllegalArgumentException - if the rectangle is empty or reaches outside the surface
      IllegalStateException - if the surface has been disposed
    • readDisplayReferred

      default Image readDisplayReferred()
      Reads the whole surface as the display saw it. See readDisplayReferred(int, int, int, int).
    • readSceneReferred

      ScenePixels readSceneReferred(int x, int y, int widthPx, int heightPx)
      Reads a rectangle of this surface exactly as the renderer wrote it: linear light, premultiplied, float, no exposure and no tonemap. Values above 1 survive, which is why this does not return an Image.

      A multisampled surface is resolved first; one sample per pixel is not what the surface shows, and a read that quietly returned sample 0 would differ from the composite by exactly the antialiasing.

      Parameters:
      x - left edge in device pixels, 0 at the left of the surface
      y - top edge in device pixels, 0 at the top of the surface
      widthPx - rectangle width, at least 1
      heightPx - rectangle height, at least 1
      Returns:
      new pixels of exactly widthPx * heightPx, premultiplied, top-down
      Throws:
      IllegalArgumentException - if the rectangle is empty or reaches outside the surface
      IllegalStateException - if the surface has been disposed
    • readSceneReferred

      default ScenePixels readSceneReferred()
      Reads the whole surface as the renderer wrote it. See readSceneReferred(int, int, int, int).