gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/gpu/headless/headless.go (about) 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 "errors" 9 "image" 10 "image/color" 11 12 "gioui.org/gpu" 13 "gioui.org/gpu/internal/driver" 14 "gioui.org/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 } 25 26 type context interface { 27 API() gpu.API 28 MakeCurrent() error 29 ReleaseCurrent() 30 Release() 31 } 32 33 var ( 34 newContextPrimary func() (context, error) 35 newContextFallback func() (context, error) 36 ) 37 38 func newContext() (context, error) { 39 funcs := []func() (context, error){newContextPrimary, newContextFallback} 40 var firstErr error 41 for _, f := range funcs { 42 if f == nil { 43 continue 44 } 45 c, err := f() 46 if err != nil { 47 if firstErr == nil { 48 firstErr = err 49 } 50 continue 51 } 52 return c, nil 53 } 54 if firstErr != nil { 55 return nil, firstErr 56 } 57 return nil, errors.New("headless: no available GPU backends") 58 } 59 60 // NewWindow creates a new headless window. 61 func NewWindow(width, height int) (*Window, error) { 62 ctx, err := newContext() 63 if err != nil { 64 return nil, err 65 } 66 w := &Window{ 67 size: image.Point{X: width, Y: height}, 68 ctx: ctx, 69 } 70 err = contextDo(ctx, func() error { 71 dev, err := driver.NewDevice(ctx.API()) 72 if err != nil { 73 return err 74 } 75 fboTex, err := dev.NewTexture( 76 driver.TextureFormatSRGBA, 77 width, height, 78 driver.FilterNearest, driver.FilterNearest, 79 driver.BufferBindingFramebuffer, 80 ) 81 if err != nil { 82 dev.Release() 83 return err 84 } 85 // Note that the gpu takes ownership of dev. 86 gp, err := gpu.NewWithDevice(dev) 87 if err != nil { 88 fboTex.Release() 89 return err 90 } 91 w.fboTex = fboTex 92 w.gpu = gp 93 w.dev = dev 94 return err 95 }) 96 if err != nil { 97 ctx.Release() 98 return nil, err 99 } 100 return w, nil 101 } 102 103 // Release resources associated with the window. 104 func (w *Window) Release() { 105 contextDo(w.ctx, func() error { 106 if w.fboTex != nil { 107 w.fboTex.Release() 108 w.fboTex = nil 109 } 110 if w.gpu != nil { 111 w.gpu.Release() 112 w.gpu = nil 113 } 114 // w.dev is owned and freed by w.gpu. 115 w.dev = nil 116 return nil 117 }) 118 if w.ctx != nil { 119 w.ctx.Release() 120 w.ctx = nil 121 } 122 } 123 124 // Size returns the window size. 125 func (w *Window) Size() image.Point { 126 return w.size 127 } 128 129 // Frame replaces the window content and state with the 130 // operation list. 131 func (w *Window) Frame(frame *op.Ops) error { 132 return contextDo(w.ctx, func() error { 133 w.gpu.Clear(color.NRGBA{}) 134 return w.gpu.Frame(frame, w.fboTex, w.size) 135 }) 136 } 137 138 // Screenshot transfers the Window content at origin img.Rect.Min to img. 139 func (w *Window) Screenshot(img *image.RGBA) error { 140 return contextDo(w.ctx, func() error { 141 return driver.DownloadImage(w.dev, w.fboTex, img) 142 }) 143 } 144 145 func contextDo(ctx context, f func() error) error { 146 errCh := make(chan error) 147 go func() { 148 if err := ctx.MakeCurrent(); err != nil { 149 errCh <- err 150 return 151 } 152 err := f() 153 ctx.ReleaseCurrent() 154 errCh <- err 155 }() 156 return <-errCh 157 }