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