detached 8d6e44d

fix(wayland): send preferred_buffer_transform only when it differs and never to cursors

commit
8d6e44d514bf71d57968743016a3147f8bb13b47
author
brice · · 2026-10-03 19:15 UTC
parent
c1aa599
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +60 -37
M internal/adapters/wayland/outputmanagement_test.go +1 -1
@@ -285,7 +285,6 @@ func TestOutputManagementUnsupportedSettings(t *testing.T) {
285285 }
286286 }
287287
288-// Each case uses a new connection because wl_display.error terminates that client.
289288 // set_transform reaches the backend, and heads report the placement's
290289 // transform, at bind time and when it changes.
291290 func TestOutputManagementTransform(t *testing.T) {
@@ -365,6 +364,7 @@ func TestOutputManagementTransform(t *testing.T) {
365364 }
366365 }
367366
367+// Each case uses a new connection because wl_display.error terminates that client.
368368 func TestOutputManagementProtocolErrors(t *testing.T) {
369369 const (
370370 enable = wlr.ZwlrOutputConfigurationV1RequestEnableHead
@@ -285,7 +285,6 @@ func TestOutputManagementUnsupportedSettings(t *testing.T) {
285 }285 }
286 }286 }
287 287
288-// Each case uses a new connection because wl_display.error terminates that client.
289 // set_transform reaches the backend, and heads report the placement's288 // set_transform reaches the backend, and heads report the placement's
290 // transform, at bind time and when it changes.289 // transform, at bind time and when it changes.
291 func TestOutputManagementTransform(t *testing.T) {290 func TestOutputManagementTransform(t *testing.T) {
@@ -365,6 +364,7 @@ func TestOutputManagementTransform(t *testing.T) {
365 }364 }
366 }365 }
367 366
367+// Each case uses a new connection because wl_display.error terminates that client.
368 func TestOutputManagementProtocolErrors(t *testing.T) {368 func TestOutputManagementProtocolErrors(t *testing.T) {
369 const (369 const (
370 enable = wlr.ZwlrOutputConfigurationV1RequestEnableHead370 enable = wlr.ZwlrOutputConfigurationV1RequestEnableHead
M internal/adapters/wayland/scale.go +8 -12
@@ -77,14 +77,15 @@ func (surf *surface) sendScale() {
7777 if o != nil {
7878 scale = o.place.Scale
7979 }
80- // The output holds the scene under its transform, so a client that
81- // renders with the same buffer transform avoids a compositing pass.
80+ // The hint lets the client render in the output's orientation (rotated
81+ // outputs are always composed). Normal is the protocol default, so it is
82+ // sent only on change; cursor buffers must stay untransformed.
8283 var t ports.BufferTransform
8384 if o != nil {
8485 t = o.place.Transform
8586 }
86- if surf.wl.Version() >= 6 && (!surf.prefTransformSent || t != surf.prefTransform) {
87- surf.prefTransform, surf.prefTransformSent = t, true
87+ if surf.wl.Version() >= 6 && surf.kind != roleCursor && t != surf.prefTransform {
88+ surf.prefTransform = t
8889 surf.wl.SendPreferredBufferTransform(uint32(t))
8990 }
9091 if scale == surf.scale {
@@ -236,10 +237,8 @@ func (s *surface) source(bw, bh int) ([4]float32, bool) {
236237 v := s.committedViewport.src
237238 scale := float64(max(s.bufferScale, 1))
238239 // Transformed buffer size, in the surface's axes.
239- tw, th := float64(bw), float64(bh)
240- if s.transform.Rotated() {
241- tw, th = th, tw
242- }
240+ iw, ih := s.transformedSize(bw, bh)
241+ tw, th := float64(iw), float64(ih)
243242 x0, y0 := float64(v[0])*scale/256, float64(v[1])*scale/256
244243 x1, y1 := x0+float64(v[2])*scale/256, y0+float64(v[3])*scale/256
245244 // validateViewport tolerates a rounding overshoot: sample inside the buffer.
@@ -252,10 +251,7 @@ func (s *surface) source(bw, bh int) ([4]float32, bool) {
252251 // transformedSize is the buffer size in the surface's axes: width and height
253252 // swap for a 90° or 270° buffer transform.
254253 func (s *surface) transformedSize(bw, bh int) (int, int) {
255- if s.transform.Rotated() {
256- return bh, bw
257- }
258- return bw, bh
254+ return s.transform.Size(bw, bh)
259255 }
260256
261257 func (s *surface) validateViewport(bw, bh int) bool {
@@ -77,14 +77,15 @@ func (surf *surface) sendScale() {
77 if o != nil {77 if o != nil {
78 scale = o.place.Scale78 scale = o.place.Scale
79 }79 }
80- // The output holds the scene under its transform, so a client that80+ // The hint lets the client render in the output's orientation (rotated
81- // renders with the same buffer transform avoids a compositing pass.81+ // outputs are always composed). Normal is the protocol default, so it is
82+ // sent only on change; cursor buffers must stay untransformed.
82 var t ports.BufferTransform83 var t ports.BufferTransform
83 if o != nil {84 if o != nil {
84 t = o.place.Transform85 t = o.place.Transform
85 }86 }
86- if surf.wl.Version() >= 6 && (!surf.prefTransformSent || t != surf.prefTransform) {87+ if surf.wl.Version() >= 6 && surf.kind != roleCursor && t != surf.prefTransform {
87- surf.prefTransform, surf.prefTransformSent = t, true88+ surf.prefTransform = t
88 surf.wl.SendPreferredBufferTransform(uint32(t))89 surf.wl.SendPreferredBufferTransform(uint32(t))
89 }90 }
90 if scale == surf.scale {91 if scale == surf.scale {
@@ -236,10 +237,8 @@ func (s *surface) source(bw, bh int) ([4]float32, bool) {
236 v := s.committedViewport.src237 v := s.committedViewport.src
237 scale := float64(max(s.bufferScale, 1))238 scale := float64(max(s.bufferScale, 1))
238 // Transformed buffer size, in the surface's axes.239 // Transformed buffer size, in the surface's axes.
239- tw, th := float64(bw), float64(bh)240+ iw, ih := s.transformedSize(bw, bh)
240- if s.transform.Rotated() {241+ tw, th := float64(iw), float64(ih)
241- tw, th = th, tw
242- }
243 x0, y0 := float64(v[0])*scale/256, float64(v[1])*scale/256242 x0, y0 := float64(v[0])*scale/256, float64(v[1])*scale/256
244 x1, y1 := x0+float64(v[2])*scale/256, y0+float64(v[3])*scale/256243 x1, y1 := x0+float64(v[2])*scale/256, y0+float64(v[3])*scale/256
245 // validateViewport tolerates a rounding overshoot: sample inside the buffer.244 // validateViewport tolerates a rounding overshoot: sample inside the buffer.
@@ -252,10 +251,7 @@ func (s *surface) source(bw, bh int) ([4]float32, bool) {
252 // transformedSize is the buffer size in the surface's axes: width and height251 // transformedSize is the buffer size in the surface's axes: width and height
253 // swap for a 90° or 270° buffer transform.252 // swap for a 90° or 270° buffer transform.
254 func (s *surface) transformedSize(bw, bh int) (int, int) {253 func (s *surface) transformedSize(bw, bh int) (int, int) {
255- if s.transform.Rotated() {254+ return s.transform.Size(bw, bh)
256- return bh, bw
257- }
258- return bw, bh
259 }255 }
260 256
261 func (s *surface) validateViewport(bw, bh int) bool {257 func (s *surface) validateViewport(bw, bh int) bool {
M internal/adapters/wayland/scale_test.go +49 -21
@@ -68,8 +68,9 @@ func contentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.Cl
6868 return s, events, commands, contents, dir
6969 }
7070
71-// A v6 surface learns the transform of its output once, and again only when
72-// the transform changes.
71+// A v6 surface learns the transform of its output when it differs from
72+// normal (the protocol default), and again only when it changes. A cursor
73+// surface never does: cursor buffers must stay untransformed.
7374 func TestPreferredBufferTransform(t *testing.T) {
7475 s, _, _, _, dir := contentServer(t)
7576 info := testOutputs[0].Info
@@ -80,40 +81,67 @@ func TestPreferredBufferTransform(t *testing.T) {
8081 t.Fatal("display stopped")
8182 }
8283 }
83- setOutputs(1, 1)
84+ newSurface := func(c *wlturbo.Display, comp uint32) (uint32, func() []uint32) {
85+ surf := c.AllocateID()
86+ p := &surfaceTransformProxy{transforms: make(chan uint32, 8)}
87+ p.SetID(surf)
88+ registerWireProxy(c, p)
89+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
90+ return surf, func() []uint32 {
91+ if err := c.Roundtrip(); err != nil {
92+ t.Fatal(err)
93+ }
94+ var got []uint32
95+ for len(p.transforms) > 0 {
96+ got = append(got, <-p.transforms)
97+ }
98+ return got
99+ }
100+ }
101+ setOutputs(0, 1)
84102 c := protocolClient(t, s, dir)
85103 g, _ := c.Registry().FindGlobal("wl_compositor")
86104 comp, err := bindWireID(c, g.Name, g.Interface, 6)
87105 if err != nil {
88106 t.Fatal(err)
89107 }
90- surf := c.AllocateID()
91- p := &surfaceTransformProxy{transforms: make(chan uint32, 8)}
92- p.SetID(surf)
93- registerWireProxy(c, p)
94- requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
95- drain := func() []uint32 {
96- if err := c.Roundtrip(); err != nil {
97- t.Fatal(err)
98- }
99- var got []uint32
100- for len(p.transforms) > 0 {
101- got = append(got, <-p.transforms)
102- }
103- return got
108+ seat := bindProtocol(t, c, "wl_seat")
109+ registerProtocol(t, c, seat)
110+ pointer := c.AllocateID()
111+ registerProtocol(t, c, pointer)
112+ requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
113+
114+ _, normal := newSurface(c, comp)
115+ cursor, cursorSeen := newSurface(c, comp)
116+ if got := normal(); len(got) != 0 {
117+ t.Fatalf("normal output sent transforms %v", got)
104118 }
105- if got := drain(); !slices.Equal(got, []uint32{1}) {
106- t.Fatalf("initial transforms %v, want [1]", got)
119+ cursorSeen()
120+ requestProtocol(t, c, pointer, wayland.PointerRequestSetCursor, uint32(1), cursor, int32(0), int32(0))
121+
122+ setOutputs(1, 1)
123+ if got := normal(); !slices.Equal(got, []uint32{1}) {
124+ t.Fatalf("rotated transforms %v, want [1]", got)
125+ }
126+ if got := cursorSeen(); len(got) != 0 {
127+ t.Fatalf("cursor surface got transforms %v", got)
107128 }
108129 // A scale change alone does not repeat the transform.
109130 setOutputs(1, 2)
110- if got := drain(); len(got) != 0 {
131+ if got := normal(); len(got) != 0 {
111132 t.Fatalf("unchanged transform resent: %v", got)
112133 }
113134 setOutputs(3, 2)
114- if got := drain(); !slices.Equal(got, []uint32{3}) {
135+ if got := normal(); !slices.Equal(got, []uint32{3}) {
115136 t.Fatalf("changed transforms %v, want [3]", got)
116137 }
138+ setOutputs(0, 2)
139+ if got := normal(); !slices.Equal(got, []uint32{0}) {
140+ t.Fatalf("back to normal: %v, want [0]", got)
141+ }
142+ if got := cursorSeen(); len(got) != 0 {
143+ t.Fatalf("cursor surface got transforms %v", got)
144+ }
117145 }
118146
119147 func TestFractionalScaleAndViewport(t *testing.T) {
@@ -68,8 +68,9 @@ func contentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.Cl
68 return s, events, commands, contents, dir68 return s, events, commands, contents, dir
69 }69 }
70 70
71-// A v6 surface learns the transform of its output once, and again only when71+// A v6 surface learns the transform of its output when it differs from
72-// the transform changes.72+// normal (the protocol default), and again only when it changes. A cursor
73+// surface never does: cursor buffers must stay untransformed.
73 func TestPreferredBufferTransform(t *testing.T) {74 func TestPreferredBufferTransform(t *testing.T) {
74 s, _, _, _, dir := contentServer(t)75 s, _, _, _, dir := contentServer(t)
75 info := testOutputs[0].Info76 info := testOutputs[0].Info
@@ -80,40 +81,67 @@ func TestPreferredBufferTransform(t *testing.T) {
80 t.Fatal("display stopped")81 t.Fatal("display stopped")
81 }82 }
82 }83 }
83- setOutputs(1, 1)84+ newSurface := func(c *wlturbo.Display, comp uint32) (uint32, func() []uint32) {
85+ surf := c.AllocateID()
86+ p := &surfaceTransformProxy{transforms: make(chan uint32, 8)}
87+ p.SetID(surf)
88+ registerWireProxy(c, p)
89+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
90+ return surf, func() []uint32 {
91+ if err := c.Roundtrip(); err != nil {
92+ t.Fatal(err)
93+ }
94+ var got []uint32
95+ for len(p.transforms) > 0 {
96+ got = append(got, <-p.transforms)
97+ }
98+ return got
99+ }
100+ }
101+ setOutputs(0, 1)
84 c := protocolClient(t, s, dir)102 c := protocolClient(t, s, dir)
85 g, _ := c.Registry().FindGlobal("wl_compositor")103 g, _ := c.Registry().FindGlobal("wl_compositor")
86 comp, err := bindWireID(c, g.Name, g.Interface, 6)104 comp, err := bindWireID(c, g.Name, g.Interface, 6)
87 if err != nil {105 if err != nil {
88 t.Fatal(err)106 t.Fatal(err)
89 }107 }
90- surf := c.AllocateID()108+ seat := bindProtocol(t, c, "wl_seat")
91- p := &surfaceTransformProxy{transforms: make(chan uint32, 8)}109+ registerProtocol(t, c, seat)
92- p.SetID(surf)110+ pointer := c.AllocateID()
93- registerWireProxy(c, p)111+ registerProtocol(t, c, pointer)
94- requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)112+ requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
95- drain := func() []uint32 {113+
96- if err := c.Roundtrip(); err != nil {114+ _, normal := newSurface(c, comp)
97- t.Fatal(err)115+ cursor, cursorSeen := newSurface(c, comp)
98- }116+ if got := normal(); len(got) != 0 {
99- var got []uint32117+ t.Fatalf("normal output sent transforms %v", got)
100- for len(p.transforms) > 0 {
101- got = append(got, <-p.transforms)
102- }
103- return got
104 }118 }
105- if got := drain(); !slices.Equal(got, []uint32{1}) {119+ cursorSeen()
106- t.Fatalf("initial transforms %v, want [1]", got)120+ requestProtocol(t, c, pointer, wayland.PointerRequestSetCursor, uint32(1), cursor, int32(0), int32(0))
121+
122+ setOutputs(1, 1)
123+ if got := normal(); !slices.Equal(got, []uint32{1}) {
124+ t.Fatalf("rotated transforms %v, want [1]", got)
125+ }
126+ if got := cursorSeen(); len(got) != 0 {
127+ t.Fatalf("cursor surface got transforms %v", got)
107 }128 }
108 // A scale change alone does not repeat the transform.129 // A scale change alone does not repeat the transform.
109 setOutputs(1, 2)130 setOutputs(1, 2)
110- if got := drain(); len(got) != 0 {131+ if got := normal(); len(got) != 0 {
111 t.Fatalf("unchanged transform resent: %v", got)132 t.Fatalf("unchanged transform resent: %v", got)
112 }133 }
113 setOutputs(3, 2)134 setOutputs(3, 2)
114- if got := drain(); !slices.Equal(got, []uint32{3}) {135+ if got := normal(); !slices.Equal(got, []uint32{3}) {
115 t.Fatalf("changed transforms %v, want [3]", got)136 t.Fatalf("changed transforms %v, want [3]", got)
116 }137 }
138+ setOutputs(0, 2)
139+ if got := normal(); !slices.Equal(got, []uint32{0}) {
140+ t.Fatalf("back to normal: %v, want [0]", got)
141+ }
142+ if got := cursorSeen(); len(got) != 0 {
143+ t.Fatalf("cursor surface got transforms %v", got)
144+ }
117 }145 }
118 146
119 func TestFractionalScaleAndViewport(t *testing.T) {147 func TestFractionalScaleAndViewport(t *testing.T) {
M internal/adapters/wayland/surface.go +2 -3
@@ -56,9 +56,8 @@ type surface struct {
5656 on *output
5757 scale float64
5858 // prefTransform is the last preferred buffer transform sent (wl_surface
59- // v6+); prefTransformSent is false until the first one.
60- prefTransform ports.BufferTransform
61- prefTransformSent bool
59+ // v6+); zero (normal) is the protocol default.
60+ prefTransform ports.BufferTransform
6261 // content is the last content built from this surface's own buffer;
6362 // has is false while no buffer is attached.
6463 inputAll bool
@@ -56,9 +56,8 @@ type surface struct {
56 on *output56 on *output
57 scale float6457 scale float64
58 // prefTransform is the last preferred buffer transform sent (wl_surface58 // prefTransform is the last preferred buffer transform sent (wl_surface
59- // v6+); prefTransformSent is false until the first one.59+ // v6+); zero (normal) is the protocol default.
60- prefTransform ports.BufferTransform60+ prefTransform ports.BufferTransform
61- prefTransformSent bool
62 // content is the last content built from this surface's own buffer;61 // content is the last content built from this surface's own buffer;
63 // has is false while no buffer is attached.62 // has is false while no buffer is attached.
64 inputAll bool63 inputAll bool

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close