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