1package vulkan
2
3import (
4 "github.com/bnema/neferwl/internal/ports"
5 "github.com/stretchr/testify/require"
6 "image"
7 "math"
8 "testing"
9)
10
11func TestPositionHintGeometryDamageAllocations(t *testing.T) {
12 for _, scale := range []float64{1, 1.5, 2} {
13 r := &Renderer{width: int(640 * scale), height: int(480 * scale)}
14 s := ports.Scene{OutputWidth: 640, OutputHeight: 480, Scale: scale, Border: ports.Border{Active: "#abcdef"}, PositionHint: ports.PositionHint{Opacity: 1, Columns: 2, Counts: [16]uint8{2, 1}, ActiveRow: 1}}
15 dmg := newDamage(&target{}, s, image.Rect(0, 0, r.width, r.height))
16 draws := r.draws(s, nil, dmg)
17 require.NotEmpty(t, draws)
18 w := sceneWalk{scale: scale}
19 b := s.PositionHint.Bounds(640, 480)
20 want := w.physRect(b.X, b.Y, b.W, b.H)
21 var area image.Rectangle
22 for _, d := range draws {
23 area = area.Union(image.Rect(int(d.pc.rect[0]), int(d.pc.rect[1]), int(d.pc.rect[2]), int(d.pc.rect[3])))
24 }
25 require.Equal(t, want, area)
26 require.Zero(t, testing.AllocsPerRun(100, func() { r.draws(s, nil, dmg) }))
27 old := s
28 old.PositionHint = ports.PositionHint{}
29 phys := func(b ports.Rect) image.Rectangle { return w.physRect(b.X, b.Y, b.W, b.H) }
30 got, ok := sceneDelta(old, s, phys, r.width*r.height)
31 require.True(t, ok)
32 require.Equal(t, want, got)
33 s.PositionHint.Opacity = 0.5
34 got, ok = sceneDelta(old, s, phys, r.width*r.height)
35 require.True(t, ok)
36 require.Equal(t, want, got)
37 }
38}
39
40func TestPositionHintDamageTransitions(t *testing.T) {
41 base := ports.Scene{Scale: 1, OutputWidth: 640, OutputHeight: 480, PositionHint: ports.PositionHint{Opacity: 1, Columns: 2, Counts: [16]uint8{2, 1}}}
42 phys := func(r ports.Rect) image.Rectangle { return image.Rect(r.X, r.Y, r.X+r.W, r.Y+r.H) }
43 bounds := func(s ports.Scene) image.Rectangle { return phys(s.PositionHint.Bounds(s.OutputWidth, s.OutputHeight)) }
44 t.Run("disappear", func(t *testing.T) {
45 cur := base
46 cur.PositionHint = ports.PositionHint{}
47 got, ok := sceneDelta(base, cur, phys, 640*480)
48 require.True(t, ok)
49 require.Equal(t, bounds(base), got)
50 })
51 t.Run("shape", func(t *testing.T) {
52 cur := base
53 cur.PositionHint.Columns = 3
54 cur.PositionHint.Counts[2] = 4
55 got, ok := sceneDelta(base, cur, phys, 640*480)
56 require.True(t, ok)
57 require.Equal(t, bounds(base).Union(bounds(cur)), got)
58 })
59 t.Run("fade", func(t *testing.T) {
60 cur := base
61 cur.PositionHint.Opacity = 0.5
62 got, ok := sceneDelta(base, cur, phys, 640*480)
63 require.True(t, ok)
64 require.Equal(t, bounds(base), got)
65 })
66 t.Run("scale", func(t *testing.T) {
67 cur := base
68 cur.Scale = 2
69 _, ok := sceneDelta(base, cur, phys, 640*480)
70 require.False(t, ok, "scale change requires full target redraw; geometry scaling is checked separately")
71 })
72}
73
74func TestPositionHintRoundedQuads(t *testing.T) {
75 for _, scale := range []float64{1, 2} {
76 r := &Renderer{width: int(640 * scale), height: int(480 * scale)}
77 h := hintGrid(3, 2)
78 s := ports.Scene{OutputWidth: 640, OutputHeight: 480, Scale: scale, PositionHint: h}
79 dmg := newDamage(&target{}, s, image.Rect(0, 0, r.width, r.height))
80 draws := r.draws(s, nil, dmg)
81 require.Len(t, draws, 1+6)
82 w := sceneWalk{scale: scale}
83 b := h.Bounds(640, 480)
84 pill := w.physRect(b.X, b.Y, b.W, b.H)
85 for i, d := range draws {
86 require.NotZero(t, d.pc.misc[1]&flagRounded, "draw %d", i)
87 require.Zero(t, d.pc.misc[1]&flagRing)
88 require.Equal(t, roundRect(image.Rect(int(d.pc.rect[0]), int(d.pc.rect[1]), int(d.pc.rect[2]), int(d.pc.rect[3]))), d.round)
89 }
90 require.Equal(t, pill, image.Rect(int(draws[0].pc.rect[0]), int(draws[0].pc.rect[1]), int(draws[0].pc.rect[2]), int(draws[0].pc.rect[3])))
91 require.Equal(t, float32(w.phys(ports.PositionHintRadius)), math.Float32frombits(draws[0].radii[0]))
92 // Active dot (col 1,row 1) is the larger one: radius = phys(active diameter)/2.
93 active := draws[1+1*2+1]
94 require.Equal(t, float32(w.phys(ports.PositionHintActiveDiameter)/2), math.Float32frombits(active.radii[0]))
95 other := draws[1]
96 require.Equal(t, float32(w.phys(ports.PositionHintDiameter)/2), math.Float32frombits(other.radii[0]))
97 }
98}
99
100func TestPositionHintRoundedOrient(t *testing.T) {
101 r := &Renderer{width: 640, height: 480}
102 s := ports.Scene{OutputWidth: 640, OutputHeight: 480, Scale: 1, PositionHint: hintGrid(2, 2)}
103 dmg := newDamage(&target{}, s, image.Rect(0, 0, r.width, r.height))
104 ds := r.draws(s, nil, dmg)
105 r.orient(ds, ports.BufferTransform(1))
106 for _, d := range ds {
107 require.Equal(t, roundRect(image.Rect(int(d.pc.rect[0]), int(d.pc.rect[1]), int(d.pc.rect[2]), int(d.pc.rect[3]))), d.round)
108 }
109}
110
111func TestHintRoundDraws(t *testing.T) {
112 bounds := image.Rect(0, 0, 100, 100)
113 for _, tc := range []struct {
114 name string
115 rect image.Rectangle
116 radius int
117 wantDraws int
118 wantRadius float32
119 }{
120 {"inside", image.Rect(10, 10, 50, 40), 6, 1, 6},
121 {"clipped fully off output", image.Rect(200, 200, 240, 230), 6, 0, 0},
122 {"clipped by bounds", image.Rect(90, 90, 130, 130), 6, 1, 6},
123 {"tiny pill clamps the radius", image.Rect(10, 10, 16, 14), 100, 1, 2},
124 {"zero radius is a plain fill", image.Rect(10, 10, 50, 40), 0, 1, 0},
125 {"negative radius is a plain fill", image.Rect(10, 10, 50, 40), -3, 1, 0},
126 } {
127 t.Run(tc.name, func(t *testing.T) {
128 w := sceneWalk{r: &Renderer{width: 100, height: 100}, alpha: 1, bounds: bounds}
129 w.hintRound(tc.rect, tc.radius, [3]uint8{1, 2, 3})
130 require.Len(t, w.draws, tc.wantDraws)
131 for _, d := range w.draws {
132 if tc.wantRadius == 0 {
133 require.Zero(t, d.pc.misc[1]&flagRounded)
134 continue
135 }
136 require.NotZero(t, d.pc.misc[1]&flagRounded)
137 require.Equal(t, roundRect(tc.rect), d.round, "the clip is the requested rect")
138 require.Equal(t, tc.wantRadius, math.Float32frombits(d.radii[0]))
139 }
140 })
141 }
142}
143
144func TestPositionHintRoundedRotatedOrient(t *testing.T) {
145 for transform := ports.BufferTransform(1); transform < 8; transform++ {
146 r := &Renderer{width: 640, height: 480}
147 sw, sh := r.sceneSize(transform)
148 s := ports.Scene{OutputWidth: sw, OutputHeight: sh, Scale: 1, Transform: transform, PositionHint: hintGrid(2, 2)}
149 ds := r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, sw, sh)))
150 require.Len(t, ds, 1+4)
151 sceneRects := make([]image.Rectangle, len(ds))
152 for i, d := range ds {
153 sceneRects[i] = image.Rect(int(d.pc.rect[0]), int(d.pc.rect[1]), int(d.pc.rect[2]), int(d.pc.rect[3]))
154 }
155 r.orient(ds, transform)
156 for i, d := range ds {
157 want := transform.RectToBuffer(sceneRects[i], sw, sh)
158 require.Equal(t, roundRect(want), d.round, "transform %d draw %d", transform, i)
159 require.NotZero(t, d.pc.misc[1]&flagRounded)
160 }
161 }
162}