1package app
2
3import (
4 "image/color"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "github.com/bnema/neferwl/internal/adapters/drm/gbm"
10 "github.com/bnema/neferwl/internal/adapters/vulkan"
11 "github.com/bnema/neferwl/internal/ports"
12 "golang.org/x/sys/unix"
13)
14
15// fourccXRGB8888 is DRM_FORMAT_XRGB8888.
16const fourccXRGB8888 = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
17
18// renderNodeFor opens the render node of the GPU the renderer runs on.
19func renderNodeFor(t *testing.T, dev uint64) *os.File {
20 t.Helper()
21 nodes, _ := filepath.Glob("/dev/dri/renderD*")
22 for _, n := range nodes {
23 var st unix.Stat_t
24 if unix.Stat(n, &st) != nil || st.Rdev != dev {
25 continue
26 }
27 f, err := os.OpenFile(n, os.O_RDWR|unix.O_CLOEXEC, 0)
28 if err != nil {
29 t.Skipf("cannot open %s: %v", n, err)
30 }
31 return f
32 }
33 t.Skipf("no render node for device %#x", dev)
34 return nil
35}
36
37// Images allocated by GBM are imported as the renderer's targets (the path
38// the DRM output takes) and frames drawn into them read back. Skips without
39// a GPU, libgbm or an openable render node.
40func TestGBMImagesAreRenderTargets(t *testing.T) {
41 const w, h = 128, 64
42 r, err := vulkan.New(w, h)
43 if err != nil {
44 t.Skipf("Vulkan unavailable: %v", err)
45 }
46 defer r.Close()
47 sup := r.DMABuf()
48 if sup.Device == 0 || len(sup.Formats) == 0 {
49 t.Skip("device does not import dmabufs")
50 }
51 mods := r.TargetModifiers(nil, false)
52 if len(mods) == 0 {
53 t.Skip("no importable target modifier")
54 }
55 t.Logf("renderer target modifiers: %#x", mods)
56 node := renderNodeFor(t, sup.Device)
57 defer node.Close()
58 dev, err := gbm.Open(int(node.Fd()))
59 if err != nil {
60 t.Skipf("GBM unavailable: %v", err)
61 }
62 defer dev.Close()
63
64 // A render node cannot always allocate scanout buffers.
65 var bufs [2]*gbm.Buffer
66 for _, usage := range []gbm.Usage{gbm.UseScanout | gbm.UseRendering, gbm.UseRendering} {
67 var cerr error
68 for i := range bufs {
69 if bufs[i], cerr = dev.Create(w, h, fourccXRGB8888, mods, usage); cerr != nil {
70 break
71 }
72 }
73 if cerr == nil {
74 t.Logf("GBM usage %#x", usage)
75 break
76 }
77 t.Logf("GBM usage %#x: %v", usage, cerr)
78 for i := range bufs {
79 bufs[i].Close()
80 bufs[i] = nil
81 }
82 }
83 if bufs[0] == nil || bufs[1] == nil {
84 t.Skip("GBM allocated no image with the renderer's modifiers")
85 }
86 defer bufs[0].Close()
87 defer bufs[1].Close()
88
89 var dmas []ports.DMABuf
90 for _, b := range bufs {
91 d, err := b.DMABuf()
92 if err != nil {
93 t.Fatal(err)
94 }
95 dmas = append(dmas, d)
96 }
97 defer func() {
98 for _, d := range dmas {
99 for _, p := range d.Planes {
100 p.File.Close()
101 }
102 }
103 }()
104 t.Logf("GBM picked modifier %#x, %d plane(s), stride %d", dmas[0].Modifier, len(dmas[0].Planes), dmas[0].Planes[0].Stride)
105 if err := r.ImportTargets(dmas); err != nil {
106 t.Fatalf("ImportTargets: %v", err)
107 }
108 t.Log("Vulkan import of the GBM images succeeded")
109
110 // Target 0: red background with a blue window; target 1: green.
111 scenes := []ports.Scene{
112 {Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 32, Y: 16, W: 64, H: 32}}}},
113 {Background: "#00ff00"},
114 }
115 contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 1, Height: 1, LogicalW: 64, LogicalH: 32, Opaque: true, Solid: &ports.SolidColor{B: 1, A: 1}}}
116 type px struct {
117 x, y int
118 want color.RGBA
119 }
120 checks := [][]px{
121 {{4, 4, color.RGBA{255, 0, 0, 255}}, {w - 4, h - 4, color.RGBA{255, 0, 0, 255}}, {64, 32, color.RGBA{0, 0, 255, 255}}, {36, 20, color.RGBA{0, 0, 255, 255}}},
122 {{4, 4, color.RGBA{0, 255, 0, 255}}, {64, 32, color.RGBA{0, 255, 0, 255}}},
123 }
124 // Twice around, so each image is drawn into again after the other one.
125 for round := range 2 {
126 for i := range scenes {
127 r.UseTarget(i)
128 s := scenes[i]
129 s.Seq = uint64(round*2 + i + 1)
130 done, err := r.Render(s, contents)
131 if err != nil {
132 t.Fatalf("render %d: %v", i, err)
133 }
134 if done != nil {
135 done.Close()
136 }
137 img := r.Pixels()
138 if img == nil {
139 t.Fatalf("target %d: no pixels", i)
140 }
141 for _, c := range checks[i] {
142 if got := img.At(c.x, c.y); got != c.want {
143 t.Errorf("round %d target %d pixel (%d,%d) = %v, want %v", round, i, c.x, c.y, got, c.want)
144 }
145 }
146 }
147 }
148 if err := r.ImportTargets(nil); err != nil {
149 t.Fatal(err)
150 }
151}