1package core_test
2
3import (
4 "image"
5 "math"
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/require"
10
11 "github.com/bnema/nefercap/internal/core"
12 "github.com/bnema/nefercap/internal/ports"
13)
14
15// gridWant builds the expected lines for a physical size and the pixel
16// coordinates of the three vertical and three horizontal lines.
17func gridWant(w, h int, xs, ys [3]int) core.GridLines {
18 var g core.GridLines
19 for i := range 3 {
20 g[i] = image.Rect(xs[i], 0, xs[i]+1, h)
21 g[3+i] = image.Rect(0, ys[i], w, ys[i]+1)
22 }
23 return g
24}
25
26func TestMonitorGridExactPhysicalLines(t *testing.T) {
27 for name, c := range map[string]struct {
28 w, h int
29 scale float64
30 want core.GridLines
31 }{
32 "1920x1080 at 1": {1920, 1080, 1, gridWant(1920, 1080, [3]int{640, 960, 1280}, [3]int{360, 540, 720})},
33 "1600x900 at 1.2": {1600, 900, 1.2, gridWant(1920, 1080, [3]int{640, 960, 1280}, [3]int{360, 540, 720})},
34 "1536x864 at 1.25": {1536, 864, 1.25, gridWant(1920, 1080, [3]int{640, 960, 1280}, [3]int{360, 540, 720})},
35 "1280x720 at 1.5": {1280, 720, 1.5, gridWant(1920, 1080, [3]int{640, 960, 1280}, [3]int{360, 540, 720})},
36 "960x540 at 2": {960, 540, 2, gridWant(1920, 1080, [3]int{640, 960, 1280}, [3]int{360, 540, 720})},
37 "odd 1366x768 at 1": {1366, 768, 1, gridWant(1366, 768, [3]int{455, 683, 910}, [3]int{256, 384, 512})},
38 // 1367*1.25 = 1708.75 and 769*1.25 = 961.25 round up to whole buffer pixels.
39 "odd fractional 1367x769 at 1.25": {1367, 769, 1.25, gridWant(1709, 962, [3]int{569, 854, 1139}, [3]int{320, 481, 641})},
40 // 1001*1.2 = 1201.2 and 601*1.2 = 721.2.
41 "odd fractional 1001x601 at 1.2": {1001, 601, 1.2, gridWant(1202, 722, [3]int{400, 601, 801}, [3]int{240, 361, 481})},
42 "1x1 at 1": {1, 1, 1, gridWant(1, 1, [3]int{}, [3]int{})},
43 "2x2 at 1": {2, 2, 1, gridWant(2, 2, [3]int{0, 1, 1}, [3]int{0, 1, 1})},
44 "3x3 at 1": {3, 3, 1, gridWant(3, 3, [3]int{1, 1, 2}, [3]int{1, 1, 2})},
45 } {
46 t.Run(name, func(t *testing.T) {
47 got, err := core.MonitorGrid(c.w, c.h, c.scale)
48 require.NoError(t, err)
49 assert.Equal(t, c.want, got)
50 for i, r := range got {
51 if i < 3 {
52 assert.Equal(t, 1, r.Dx(), "vertical %d is one physical pixel wide", i)
53 } else {
54 assert.Equal(t, 1, r.Dy(), "horizontal %d is one physical pixel tall", i)
55 }
56 }
57 })
58 }
59}
60
61func TestMonitorGridLineOrderAndBounds(t *testing.T) {
62 g, err := core.MonitorGrid(1920, 1080, 1)
63 require.NoError(t, err)
64 assert.Equal(t, core.GridLineCount, len(g))
65 assert.Less(t, g[0].Min.X, g[1].Min.X)
66 assert.Less(t, g[1].Min.X, g[2].Min.X)
67 assert.Less(t, g[3].Min.Y, g[4].Min.Y)
68 assert.Less(t, g[4].Min.Y, g[5].Min.Y)
69 whole := image.Rect(0, 0, 1920, 1080)
70 for _, r := range g {
71 assert.True(t, r.In(whole), "%v stays inside the physical monitor", r)
72 }
73}
74
75func TestPhysicalExtentMatchesExactFractionalScales(t *testing.T) {
76 // Compositor fractional scales are k/120. The buffer edge is
77 // ceil(logical*k/120) computed exactly in integers; float noise at exact
78 // boundaries (for example 1600*1.2) must not add a pixel.
79 for k := 120; k <= 480; k++ {
80 scale := float64(k) / 120
81 for n := 1; n <= ports.MaxDimension; n += 1 + n/97 {
82 want := (n*k + 119) / 120
83 got, err := core.PhysicalExtent(n, scale)
84 require.NoError(t, err)
85 if got != want {
86 t.Fatalf("PhysicalExtent(%d, %d/120) = %d, want %d", n, k, got, want)
87 }
88 }
89 for n := 1; n <= 4096; n++ { // every small size
90 want := (n*k + 119) / 120
91 got, err := core.PhysicalExtent(n, scale)
92 require.NoError(t, err)
93 if got != want {
94 t.Fatalf("PhysicalExtent(%d, %d/120) = %d, want %d", n, k, got, want)
95 }
96 }
97 }
98}
99
100func TestMonitorGridSweepInvariants(t *testing.T) {
101 for k := 120; k <= 360; k += 7 {
102 scale := float64(k) / 120
103 for w := 1; w <= 3000; w += 13 {
104 h := w/2 + 1
105 g, err := core.MonitorGrid(w, h, scale)
106 require.NoError(t, err)
107 pw, _ := core.PhysicalExtent(w, scale)
108 ph, _ := core.PhysicalExtent(h, scale)
109 for i, r := range g {
110 if !r.In(image.Rect(0, 0, pw, ph)) || r.Empty() {
111 t.Fatalf("%dx%d@%d/120 line %d = %v outside %dx%d", w, h, k, i, r, pw, ph)
112 }
113 if i < 3 && (r.Dx() != 1 || r.Dy() != ph) || i >= 3 && (r.Dy() != 1 || r.Dx() != pw) {
114 t.Fatalf("%dx%d@%d/120 line %d = %v is not one pixel wide over the full monitor", w, h, k, i, r)
115 }
116 }
117 if g[0].Min.X > g[1].Min.X || g[1].Min.X > g[2].Min.X || g[3].Min.Y > g[4].Min.Y || g[4].Min.Y > g[5].Min.Y {
118 t.Fatalf("%dx%d@%d/120 lines out of order: %v", w, h, k, g)
119 }
120 }
121 }
122}
123
124func TestMonitorGridRejectsInvalidInput(t *testing.T) {
125 nan, inf := math.NaN(), math.Inf(1)
126 for name, c := range map[string]struct {
127 w, h int
128 scale float64
129 }{
130 "zero width": {0, 1080, 1},
131 "zero height": {1920, 0, 1},
132 "negative width": {-1, 1080, 1},
133 "negative height": {1920, -1, 1},
134 "width too large": {ports.MaxDimension + 1, 1080, 1},
135 "height too large": {1920, ports.MaxDimension + 1, 1},
136 "zero scale": {1920, 1080, 0},
137 "negative scale": {1920, 1080, -1},
138 "NaN scale": {1920, 1080, nan},
139 "infinite scale": {1920, 1080, inf},
140 "negative infinity": {1920, 1080, -inf},
141 "scale too large": {1920, 1080, core.MaxGridScale * 2},
142 "vanishing scale": {1, 1, 1e-12},
143 } {
144 t.Run(name, func(t *testing.T) {
145 g, err := core.MonitorGrid(c.w, c.h, c.scale)
146 assert.ErrorIs(t, err, core.ErrInvalidGrid)
147 assert.Equal(t, core.GridLines{}, g, "no partial result")
148 })
149 }
150 _, err := core.PhysicalExtent(0, 1)
151 assert.ErrorIs(t, err, core.ErrInvalidGrid)
152 _, err = core.PhysicalExtent(10, nan)
153 assert.ErrorIs(t, err, core.ErrInvalidGrid)
154}
155
156func TestMonitorGridAcceptsLimits(t *testing.T) {
157 g, err := core.MonitorGrid(ports.MaxDimension, ports.MaxDimension, core.MaxGridScale)
158 require.NoError(t, err)
159 pw := ports.MaxDimension * core.MaxGridScale
160 assert.Equal(t, pw, g[3].Dx())
161}
162
163func TestMonitorGridAllocations(t *testing.T) {
164 var g core.GridLines
165 n := testing.AllocsPerRun(200, func() {
166 g, _ = core.MonitorGrid(1600, 900, 1.2)
167 _, _ = core.MonitorGrid(0, 900, 1.2) // the error path too
168 })
169 assert.Zero(t, n)
170 _ = g
171}
172
173func BenchmarkMonitorGrid(b *testing.B) {
174 b.ReportAllocs()
175 var g core.GridLines
176 for i := 0; b.Loop(); i++ {
177 g, _ = core.MonitorGrid(1600+i%3, 900, 1.2)
178 }
179 _ = g
180}