1package vulkan
2
3import (
4 "bytes"
5 "fmt"
6 "image/color"
7 "os"
8 "testing"
9
10 "github.com/bnema/neferwl/internal/ports"
11)
12
13// damagedContent writes px into the content's pool (the client drawing
14// into the same buffer) and records the damage of the new Seq.
15func damagedContent(t *testing.T, f *os.File, base ports.SurfaceContent, seq uint64, px []byte, hist []ports.SeqDamage) ports.SurfaceContent {
16 t.Helper()
17 if _, err := f.WriteAt(px, 0); err != nil {
18 t.Fatal(err)
19 }
20 c := base
21 c.Seq, c.DamageHistory = seq, hist
22 return c
23}
24
25// Partial redraws are pixel-identical to full ones: with both targets
26// ages (A then B, then one render) and with shm double buffers
27// (render, skip, render).
28func TestRendererDamageMatchesFullRedraw(t *testing.T) {
29 for _, tr := range []ports.BufferTransform{0, 1, 6} {
30 t.Run(fmt.Sprintf("transform %d", tr), func(t *testing.T) { damageMatchesFullRedraw(t, tr, 0) })
31 }
32 // A rotated or flipped output composes the scene into its target.
33 for _, out := range []ports.BufferTransform{1, 6} {
34 t.Run(fmt.Sprintf("output transform %d", out), func(t *testing.T) { damageMatchesFullRedraw(t, 0, out) })
35 }
36}
37
38// damageMatchesFullRedraw checks partial frames of a window whose buffer
39// carries transform tr against full redraws, on an output with transform
40// out (its scene is 64×48; a rotated output's target is 48×64).
41func damageMatchesFullRedraw(t *testing.T, tr, out ports.BufferTransform) {
42 const w, h = 32, 32
43 tw, th := out.Size(64, 48)
44 newR := func() *Renderer {
45 r, err := New(tw, th)
46 if err != nil {
47 t.Skipf("Vulkan unavailable: %v", err)
48 }
49 t.Cleanup(r.Close)
50 return r
51 }
52 damaged, full := newR(), newR()
53 bufsD, err := damaged.ExportTargets(2, nil, false)
54 if err != nil {
55 t.Skipf("no exportable targets: %v", err)
56 }
57 for _, b := range bufsD {
58 b.Planes[0].File.Close()
59 }
60 px := func(c color.RGBA, x0, y0, x1, y1 int, into []byte) []byte {
61 for y := y0; y < y1; y++ {
62 for x := x0; x < x1; x++ {
63 copy(into[(y*w+x)*4:], []byte{c.B, c.G, c.R, 255})
64 }
65 }
66 return into
67 }
68 pixels := px(color.RGBA{10, 20, 30, 255}, 0, 0, w, h, make([]byte, w*h*4))
69 base := shmContent(t, w, h, w*4, pixels)
70 base.ID, base.Transform = 1, tr
71 f := base.SHM.File
72 scene := ports.Scene{Seq: 5, Transform: out, OutputWidth: 64, OutputHeight: 48, Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: w, H: h}}}}
73 hist := []ports.SeqDamage{{Seq: 1, Full: true}}
74 c := damagedContent(t, f, *base, 1, pixels, hist)
75 step := func(i int, c ports.SurfaceContent) {
76 t.Helper()
77 damaged.UseTarget(i % 2)
78 contents := map[ports.WindowID]ports.SurfaceContent{1: c}
79 if err := render(damaged, scene, contents); err != nil {
80 t.Fatal(err)
81 }
82 if err := render(full, ports.Scene{Transform: out, OutputWidth: 64, OutputHeight: 48, Scale: 1, Background: scene.Background, Windows: scene.Windows}, contents); err != nil {
83 t.Fatal(err)
84 }
85 if a, b := readPixels(t, damaged), readPixels(t, full); !bytes.Equal(a.Pix, b.Pix) {
86 t.Fatalf("frame %d differs from a full redraw", i)
87 }
88 }
89 step(0, c)
90 step(1, c) // target 1 drawn whole once
91 before := damaged.redrawn
92 // A: a red square; B: a green one elsewhere. Target 0 holds Seq 1,
93 // so its next frame redraws the union of both.
94 for i, d := range []struct {
95 col color.RGBA
96 rect ports.Rect
97 }{{color.RGBA{255, 0, 0, 255}, ports.Rect{X: 2, Y: 2, W: 4, H: 4}}, {color.RGBA{0, 255, 0, 255}, ports.Rect{X: 20, Y: 10, W: 3, H: 5}}} {
98 seq := uint64(2 + i)
99 px(d.col, d.rect.X, d.rect.Y, d.rect.X+d.rect.W, d.rect.Y+d.rect.H, pixels)
100 hist = append(hist, ports.SeqDamage{Seq: seq, Rects: []ports.Rect{d.rect}})
101 c = damagedContent(t, f, *base, seq, pixels, hist)
102 }
103 step(2, c)
104 if got := damaged.redrawn - before; tr == 0 && got >= 64*48 {
105 t.Fatalf("partial frame redrew %d pixels", got)
106 }
107 // Render, skip (no frame), render: target 1 is two contents behind.
108 px(color.RGBA{0, 0, 255, 255}, 0, 30, 2, 32, pixels)
109 hist = append(hist, ports.SeqDamage{Seq: 4, Rects: []ports.Rect{{Y: 30, W: 2, H: 2}}})
110 c = damagedContent(t, f, *base, 4, pixels, hist)
111 step(3, c)
112 step(4, c)
113}
114
115// A window left out of a frame (on an overlay plane) and drawn again in
116// the same scene is redrawn whole; one held but no longer drawn is
117// cleared.
118func TestRendererDamageWindowLeftOutAndBack(t *testing.T) {
119 newR := func() *Renderer {
120 r, err := New(64, 48)
121 if err != nil {
122 t.Skipf("Vulkan unavailable: %v", err)
123 }
124 t.Cleanup(r.Close)
125 return r
126 }
127 damaged, full := newR(), newR()
128 c := solidContent(t, 16, 16, color.RGBA{200, 10, 10, 255})
129 c.ID, c.Seq = 1, 1
130 contents := map[ports.WindowID]ports.SurfaceContent{1: c}
131 with := ports.Scene{Seq: 9, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 16, H: 16}}}}
132 without := with
133 without.Windows = nil
134 check := func(s ports.Scene, what string) {
135 t.Helper()
136 if err := render(damaged, s, contents); err != nil {
137 t.Fatal(err)
138 }
139 plain := s
140 plain.Seq = 0
141 if err := render(full, plain, contents); err != nil {
142 t.Fatal(err)
143 }
144 if !bytes.Equal(readPixels(t, damaged).Pix, readPixels(t, full).Pix) {
145 t.Fatalf("%s differs from a full redraw", what)
146 }
147 }
148 check(without, "overlay frame")
149 check(with, "overlay refused, window composed")
150 check(without, "window on the overlay again")
151}