1package wayland
2
3import (
4 "context"
5 "runtime"
6 "testing"
7 "unsafe"
8
9 "github.com/stretchr/testify/require"
10
11 "github.com/bnema/nefercap/internal/adapters/wayland/testserver"
12 "github.com/bnema/nefercap/internal/ports"
13)
14
15func bigOutput() []testserver.OutputSpec {
16 return []testserver.OutputSpec{{Name: "BIG", Width: 64, Height: 48, Scale: 1}}
17}
18
19// Without the extension a region is cropped from the output frame, borrowing
20// the shared storage: no pixel is copied and the full stride is kept.
21func TestCaptureRegionCropsOutputFrame(t *testing.T) {
22 s, srv := newSource(t, testserver.Config{Outputs: bigOutput()})
23 out := firstOutput(t, s)
24 f, err := s.Capture(context.Background(), ports.Target{OutputID: out.ID, Region: ports.Region{X: 10, Y: 5, Width: 20, Height: 8}})
25 require.NoError(t, err)
26 require.NoError(t, f.Validate())
27 require.Equal(t, [3]int{20, 8, 64 * 4}, [3]int{f.Width, f.Height, f.Stride})
28 want := testserver.Pixel(10, 5, 1)
29 require.Equal(t, want[:], f.Row(0)[:4])
30 want = testserver.Pixel(29, 12, 1)
31 require.Equal(t, want[:], f.Row(7)[19*4:])
32 require.Equal(t, "output", srv.Stats().LastSource)
33 require.Equal(t, 1, srv.Stats().Buffers, "a single screenshot allocates a single buffer")
34}
35
36func TestCropIsAWindowOfTheFrame(t *testing.T) {
37 out := &output{id: 1, width: 64, height: 48, scale: 1}
38 f := ports.Frame{Pixels: make([]byte, 64*48*4), Width: 64, Height: 48, Stride: 64 * 4, Format: ports.XRGB8888}
39 c, err := cropFrame(f, ports.Region{X: 10, Y: 5, Width: 20, Height: 8}, out)
40 require.NoError(t, err)
41 require.Equal(t, unsafe.Add(unsafe.Pointer(&f.Pixels[0]), 5*64*4+10*4), unsafe.Pointer(&c.Pixels[0]))
42 require.Equal(t, 7*64*4+20*4, len(c.Pixels), "the window ends with the last row's pixels")
43}
44
45func TestCropRoundsToBufferPixels(t *testing.T) {
46 s, _ := newSource(t, testserver.Config{Outputs: []testserver.OutputSpec{{Name: "HI", Width: 64, Height: 48, Scale: 2}}})
47 out := firstOutput(t, s)
48 // logical size is 32x24 (mode over scale): one logical pixel is two buffer pixels
49 f, err := s.Capture(context.Background(), ports.Target{OutputID: out.ID, Region: ports.Region{X: 3, Y: 2, Width: 5, Height: 4}})
50 require.NoError(t, err)
51 require.Equal(t, [2]int{10, 8}, [2]int{f.Width, f.Height})
52 want := testserver.Pixel(6, 4, 1)
53 require.Equal(t, want[:], f.Row(0)[:4])
54}
55
56func TestCropClipsToOutputAndRejectsOutside(t *testing.T) {
57 s, _ := newSource(t, testserver.Config{Outputs: bigOutput()})
58 out := firstOutput(t, s)
59 f, err := s.Capture(context.Background(), ports.Target{OutputID: out.ID, Region: ports.Region{X: 60, Y: 40, Width: 100, Height: 100}})
60 require.NoError(t, err)
61 require.Equal(t, [2]int{4, 8}, [2]int{f.Width, f.Height})
62 _, err = s.Capture(context.Background(), ports.Target{OutputID: out.ID, Region: ports.Region{X: 64, Y: 0, Width: 4, Height: 4}})
63 require.ErrorIs(t, err, ports.ErrInvalidRegion)
64}
65
66func TestCropUsesXdgOutputLogicalSize(t *testing.T) {
67 s, _ := newSource(t, testserver.Config{Outputs: []testserver.OutputSpec{{Name: "FR", Width: 64, Height: 48, Scale: 1, LogicalWidth: 32, LogicalHeight: 24}}})
68 f, err := s.Capture(context.Background(), ports.Target{OutputID: firstOutput(t, s).ID, Region: ports.Region{Width: 16, Height: 12}})
69 require.NoError(t, err)
70 require.Equal(t, [2]int{32, 24}, [2]int{f.Width, f.Height}, "fractional scale: logical 16x12 is 32x24 buffer pixels")
71}
72
73// The crop must not allocate: it is a window, not a copy.
74func TestCropAllocations(t *testing.T) {
75 if raceEnabled {
76 t.Skip("race instrumentation changes allocation counts")
77 }
78 out := &output{id: 1, width: 1920, height: 1080, scale: 1}
79 f := ports.Frame{Pixels: make([]byte, 1920*1080*4), Width: 1920, Height: 1080, Stride: 1920 * 4, Format: ports.XRGB8888}
80 r := ports.Region{X: 100, Y: 100, Width: 800, Height: 600}
81 runtime.GC()
82 avg := testing.AllocsPerRun(1000, func() {
83 if _, err := cropFrame(f, r, out); err != nil {
84 panic(err)
85 }
86 })
87 require.Zero(t, avg)
88}