headless.go raw

   1  // SPDX-License-Identifier: Unlicense OR MIT
   2  
   3  // Package headless implements headless windows for rendering
   4  // an operation list to an image.
   5  package headless
   6  
   7  import (
   8  	"image"
   9  	"image/color"
  10  	"runtime"
  11  
  12  	"github.com/p9c/p9/pkg/gel/gio/gpu"
  13  	"github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver"
  14  	"github.com/p9c/p9/pkg/gel/gio/op"
  15  )
  16  
  17  // Window is a headless window.
  18  type Window struct {
  19  	size   image.Point
  20  	ctx    context
  21  	dev    driver.Device
  22  	gpu    gpu.GPU
  23  	fboTex driver.Texture
  24  	fbo    driver.Framebuffer
  25  }
  26  
  27  type context interface {
  28  	API() gpu.API
  29  	MakeCurrent() error
  30  	ReleaseCurrent()
  31  	Release()
  32  }
  33  
  34  // NewWindow creates a new headless window.
  35  func NewWindow(width, height int) (*Window, error) {
  36  	ctx, err := newContext()
  37  	if err != nil {
  38  		return nil, err
  39  	}
  40  	w := &Window{
  41  		size: image.Point{X: width, Y: height},
  42  		ctx:  ctx,
  43  	}
  44  	err = contextDo(ctx, func() error {
  45  		api := ctx.API()
  46  		dev, err := driver.NewDevice(api)
  47  		if err != nil {
  48  			return err
  49  		}
  50  		dev.Viewport(0, 0, width, height)
  51  		fboTex, err := dev.NewTexture(
  52  			driver.TextureFormatSRGB,
  53  			width, height,
  54  			driver.FilterNearest, driver.FilterNearest,
  55  			driver.BufferBindingFramebuffer,
  56  		)
  57  		if err != nil {
  58  			return nil
  59  		}
  60  		const depthBits = 16
  61  		fbo, err := dev.NewFramebuffer(fboTex, depthBits)
  62  		if err != nil {
  63  			fboTex.Release()
  64  			return err
  65  		}
  66  		gp, err := gpu.New(api)
  67  		if err != nil {
  68  			fbo.Release()
  69  			fboTex.Release()
  70  			return err
  71  		}
  72  		w.fboTex = fboTex
  73  		w.fbo = fbo
  74  		w.gpu = gp
  75  		w.dev = dev
  76  		return err
  77  	})
  78  	if err != nil {
  79  		ctx.Release()
  80  		return nil, err
  81  	}
  82  	return w, nil
  83  }
  84  
  85  // Release resources associated with the window.
  86  func (w *Window) Release() {
  87  	contextDo(w.ctx, func() error {
  88  		if w.fbo != nil {
  89  			w.fbo.Release()
  90  			w.fbo = nil
  91  		}
  92  		if w.fboTex != nil {
  93  			w.fboTex.Release()
  94  			w.fboTex = nil
  95  		}
  96  		if w.gpu != nil {
  97  			w.gpu.Release()
  98  			w.gpu = nil
  99  		}
 100  		return nil
 101  	})
 102  	if w.ctx != nil {
 103  		w.ctx.Release()
 104  		w.ctx = nil
 105  	}
 106  }
 107  
 108  // Frame replace the window content and state with the
 109  // operation list.
 110  func (w *Window) Frame(frame *op.Ops) error {
 111  	return contextDo(w.ctx, func() error {
 112  		w.dev.BindFramebuffer(w.fbo)
 113  		w.gpu.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff})
 114  		w.gpu.Collect(w.size, frame)
 115  		return w.gpu.Frame()
 116  	})
 117  }
 118  
 119  // Screenshot returns an image with the content of the window.
 120  func (w *Window) Screenshot() (*image.RGBA, error) {
 121  	var img *image.RGBA
 122  	err := contextDo(w.ctx, func() error {
 123  		var err error
 124  		img, err = driver.DownloadImage(w.dev, w.fbo, image.Rectangle{Max: w.size})
 125  		return err
 126  	})
 127  	if err != nil {
 128  		return nil, err
 129  	}
 130  	return img, nil
 131  }
 132  
 133  func contextDo(ctx context, f func() error) error {
 134  	errCh := make(chan error)
 135  	go func() {
 136  		runtime.LockOSThread()
 137  		defer runtime.UnlockOSThread()
 138  		if err := ctx.MakeCurrent(); err != nil {
 139  			errCh <- err
 140  			return
 141  		}
 142  		err := f()
 143  		ctx.ReleaseCurrent()
 144  		errCh <- err
 145  	}()
 146  	return <-errCh
 147  }
 148