detached c1aa599

refactor(ports): share the rotated size of a transform

commit
c1aa5992139ded78df42ed8f38b33918abd142cf
author
brice · · 2026-10-03 19:15 UTC
parent
1c259ae
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
10 files · +44 -41
M internal/adapters/vulkan/compose.go +2 -4
@@ -332,10 +332,8 @@ func (r *Renderer) contentDraw(rect, full image.Rectangle, w, h int, source [4]f
332332 source = [4]float32{0, 0, float32(w), float32(h)}
333333 }
334334 // The crop in the surface's axes: its sides swap for a rotation.
335- cw, ch := float64(source[2]), float64(source[3])
336- if t.Rotated() {
337- cw, ch = ch, cw
338- }
335+ iw, ih := t.Size(int(source[2]), int(source[3]))
336+ cw, ch := float64(iw), float64(ih)
339337 // src maps a target point to buffer pixels: affine, so its value at
340338 // the target origin and its steps along x and y define it.
341339 src := func(x, y float64) (float64, float64) {
@@ -332,10 +332,8 @@ func (r *Renderer) contentDraw(rect, full image.Rectangle, w, h int, source [4]f
332 source = [4]float32{0, 0, float32(w), float32(h)}332 source = [4]float32{0, 0, float32(w), float32(h)}
333 }333 }
334 // The crop in the surface's axes: its sides swap for a rotation.334 // The crop in the surface's axes: its sides swap for a rotation.
335- cw, ch := float64(source[2]), float64(source[3])335+ iw, ih := t.Size(int(source[2]), int(source[3]))
336- if t.Rotated() {336+ cw, ch := float64(iw), float64(ih)
337- cw, ch = ch, cw
338- }
339 // src maps a target point to buffer pixels: affine, so its value at337 // src maps a target point to buffer pixels: affine, so its value at
340 // the target origin and its steps along x and y define it.338 // the target origin and its steps along x and y define it.
341 src := func(x, y float64) (float64, float64) {339 src := func(x, y float64) (float64, float64) {
M internal/adapters/vulkan/damage_test.go +1 -4
@@ -40,10 +40,7 @@ func TestRendererDamageMatchesFullRedraw(t *testing.T) {
4040 // out (its scene is 64×48; a rotated output's target is 48×64).
4141 func damageMatchesFullRedraw(t *testing.T, tr, out ports.BufferTransform) {
4242 const w, h = 32, 32
43- tw, th := 64, 48
44- if out.Rotated() {
45- tw, th = th, tw
46- }
43+ tw, th := out.Size(64, 48)
4744 newR := func() *Renderer {
4845 r, err := New(tw, th)
4946 if err != nil {
@@ -40,10 +40,7 @@ func TestRendererDamageMatchesFullRedraw(t *testing.T) {
40 // out (its scene is 64×48; a rotated output's target is 48×64).40 // out (its scene is 64×48; a rotated output's target is 48×64).
41 func damageMatchesFullRedraw(t *testing.T, tr, out ports.BufferTransform) {41 func damageMatchesFullRedraw(t *testing.T, tr, out ports.BufferTransform) {
42 const w, h = 32, 3242 const w, h = 32, 32
43- tw, th := 64, 4843+ tw, th := out.Size(64, 48)
44- if out.Rotated() {
45- tw, th = th, tw
46- }
47 newR := func() *Renderer {44 newR := func() *Renderer {
48 r, err := New(tw, th)45 r, err := New(tw, th)
49 if err != nil {46 if err != nil {
M internal/adapters/vulkan/orient.go +1 -4
@@ -9,10 +9,7 @@ import (
99 // sceneSize is the size of a scene in scene-physical pixels: the target's,
1010 // swapped when the output transform t rotates by 90 or 270 degrees.
1111 func (r *Renderer) sceneSize(t ports.BufferTransform) (int, int) {
12- if t.Rotated() {
13- return r.height, r.width
14- }
15- return r.width, r.height
12+ return t.Size(r.width, r.height)
1613 }
1714
1815 // orient maps draws built in scene space to the target, in place and without
@@ -9,10 +9,7 @@ import (
9 // sceneSize is the size of a scene in scene-physical pixels: the target's,9 // sceneSize is the size of a scene in scene-physical pixels: the target's,
10 // swapped when the output transform t rotates by 90 or 270 degrees.10 // swapped when the output transform t rotates by 90 or 270 degrees.
11 func (r *Renderer) sceneSize(t ports.BufferTransform) (int, int) {11 func (r *Renderer) sceneSize(t ports.BufferTransform) (int, int) {
12- if t.Rotated() {12+ return t.Size(r.width, r.height)
13- return r.height, r.width
14- }
15- return r.width, r.height
16 }13 }
17 14
18 // orient maps draws built in scene space to the target, in place and without15 // orient maps draws built in scene space to the target, in place and without
M internal/adapters/vulkan/scene.go +1 -3
@@ -326,9 +326,7 @@ func (w *sceneWalk) surfaceRects(content *ports.SurfaceContent, x, y float64, cl
326326 if content.Source[2] > 0 {
327327 sourceW, sourceH = int(content.Source[2]), int(content.Source[3])
328328 }
329- if content.Transform.Rotated() {
330- sourceW, sourceH = sourceH, sourceW
331- }
329+ sourceW, sourceH = content.Transform.Size(sourceW, sourceH)
332330 if near(full.Dx(), sourceW) && near(full.Dy(), sourceH) {
333331 full.Max = full.Min.Add(image.Pt(sourceW, sourceH))
334332 }
@@ -326,9 +326,7 @@ func (w *sceneWalk) surfaceRects(content *ports.SurfaceContent, x, y float64, cl
326 if content.Source[2] > 0 {326 if content.Source[2] > 0 {
327 sourceW, sourceH = int(content.Source[2]), int(content.Source[3])327 sourceW, sourceH = int(content.Source[2]), int(content.Source[3])
328 }328 }
329- if content.Transform.Rotated() {329+ sourceW, sourceH = content.Transform.Size(sourceW, sourceH)
330- sourceW, sourceH = sourceH, sourceW
331- }
332 if near(full.Dx(), sourceW) && near(full.Dy(), sourceH) {330 if near(full.Dx(), sourceW) && near(full.Dy(), sourceH) {
333 full.Max = full.Min.Add(image.Pt(sourceW, sourceH))331 full.Max = full.Min.Add(image.Pt(sourceW, sourceH))
334 }332 }
M internal/adapters/vulkan/transform_test.go +1 -4
@@ -37,10 +37,7 @@ func TestRendererBufferTransform(t *testing.T) {
3737 c.Source = [4]float32{1, 0, 2, 2}
3838 bx0, bw = 1, 2
3939 }
40- sw, sh := bw, bh
41- if tr.Rotated() {
42- sw, sh = bh, bw
43- }
40+ sw, sh := tr.Size(bw, bh)
4441 c.Transform, c.LogicalW, c.LogicalH = tr, sw, sh
4542 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: sw, H: sh}}}}
4643 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {
@@ -37,10 +37,7 @@ func TestRendererBufferTransform(t *testing.T) {
37 c.Source = [4]float32{1, 0, 2, 2}37 c.Source = [4]float32{1, 0, 2, 2}
38 bx0, bw = 1, 238 bx0, bw = 1, 2
39 }39 }
40- sw, sh := bw, bh40+ sw, sh := tr.Size(bw, bh)
41- if tr.Rotated() {
42- sw, sh = bh, bw
43- }
44 c.Transform, c.LogicalW, c.LogicalH = tr, sw, sh41 c.Transform, c.LogicalW, c.LogicalH = tr, sw, sh
45 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: sw, H: sh}}}}42 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: sw, H: sh}}}}
46 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {43 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {
M internal/adapters/wayland/screencopy.go +1 -10
@@ -30,20 +30,11 @@ func captureRegion(o *output, logical image.Rectangle) image.Rectangle {
3030 if scale <= 0 {
3131 scale = 1
3232 }
33- sw, sh := sceneSize(o)
33+ sw, sh := o.place.Transform.Size(o.place.Info.Width, o.place.Info.Height)
3434 p := image.Rect(int(math.Floor(float64(logical.Min.X)*scale)), int(math.Floor(float64(logical.Min.Y)*scale)), int(math.Ceil(float64(logical.Max.X)*scale)), int(math.Ceil(float64(logical.Max.Y)*scale)))
3535 return o.place.Transform.RectToBuffer(p, sw, sh).Intersect(image.Rect(0, 0, o.place.Info.Width, o.place.Info.Height))
3636 }
3737
38-// sceneSize is the scene-physical size of the output: the mode size, swapped
39-// for a 90° or 270° transform.
40-func sceneSize(o *output) (int, int) {
41- w, h := o.place.Info.Width, o.place.Info.Height
42- if o.place.Transform.Rotated() {
43- return h, w
44- }
45- return w, h
46-}
4738 func captureTime(t time.Time) (uint32, uint32, uint32) {
4839 sec := uint64(t.Unix())
4940 return uint32(sec >> 32), uint32(sec), uint32(t.Nanosecond())
@@ -30,20 +30,11 @@ func captureRegion(o *output, logical image.Rectangle) image.Rectangle {
30 if scale <= 0 {30 if scale <= 0 {
31 scale = 131 scale = 1
32 }32 }
33- sw, sh := sceneSize(o)33+ sw, sh := o.place.Transform.Size(o.place.Info.Width, o.place.Info.Height)
34 p := image.Rect(int(math.Floor(float64(logical.Min.X)*scale)), int(math.Floor(float64(logical.Min.Y)*scale)), int(math.Ceil(float64(logical.Max.X)*scale)), int(math.Ceil(float64(logical.Max.Y)*scale)))34 p := image.Rect(int(math.Floor(float64(logical.Min.X)*scale)), int(math.Floor(float64(logical.Min.Y)*scale)), int(math.Ceil(float64(logical.Max.X)*scale)), int(math.Ceil(float64(logical.Max.Y)*scale)))
35 return o.place.Transform.RectToBuffer(p, sw, sh).Intersect(image.Rect(0, 0, o.place.Info.Width, o.place.Info.Height))35 return o.place.Transform.RectToBuffer(p, sw, sh).Intersect(image.Rect(0, 0, o.place.Info.Width, o.place.Info.Height))
36 }36 }
37 37
38-// sceneSize is the scene-physical size of the output: the mode size, swapped
39-// for a 90° or 270° transform.
40-func sceneSize(o *output) (int, int) {
41- w, h := o.place.Info.Width, o.place.Info.Height
42- if o.place.Transform.Rotated() {
43- return h, w
44- }
45- return w, h
46-}
47 func captureTime(t time.Time) (uint32, uint32, uint32) {38 func captureTime(t time.Time) (uint32, uint32, uint32) {
48 sec := uint64(t.Unix())39 sec := uint64(t.Unix())
49 return uint32(sec >> 32), uint32(sec), uint32(t.Nanosecond())40 return uint32(sec >> 32), uint32(sec), uint32(t.Nanosecond())
M internal/core/screens.go +1 -4
@@ -35,10 +35,7 @@ func (s *screen) name() string { return s.info.Name }
3535 // modeSize is the physical mode size in the screen's logical orientation:
3636 // width and height swap for a 90/270 transform.
3737 func (s *screen) modeSize() (w, h int) {
38- if s.transform.Rotated() {
39- return s.info.Height, s.info.Width
40- }
41- return s.info.Width, s.info.Height
38+ return s.transform.Size(s.info.Width, s.info.Height)
4239 }
4340
4441 // rect is the screen's place in the global logical layout.
@@ -35,10 +35,7 @@ func (s *screen) name() string { return s.info.Name }
35 // modeSize is the physical mode size in the screen's logical orientation:35 // modeSize is the physical mode size in the screen's logical orientation:
36 // width and height swap for a 90/270 transform.36 // width and height swap for a 90/270 transform.
37 func (s *screen) modeSize() (w, h int) {37 func (s *screen) modeSize() (w, h int) {
38- if s.transform.Rotated() {38+ return s.transform.Size(s.info.Width, s.info.Height)
39- return s.info.Height, s.info.Width
40- }
41- return s.info.Width, s.info.Height
42 }39 }
43 40
44 // rect is the screen's place in the global logical layout.41 // rect is the screen's place in the global logical layout.
M internal/ports/client_commands.go +2 -5
@@ -73,11 +73,8 @@ func (o OutputPlacement) ToTarget(x, y float64) (float64, float64) {
7373 s = 1
7474 }
7575 px, py := (x-float64(o.X))*s, (y-float64(o.Y))*s
76- sw, sh := float64(o.Info.Width), float64(o.Info.Height)
77- if o.Transform.Rotated() {
78- sw, sh = sh, sw
79- }
80- return o.Transform.ToBuffer(px, py, sw, sh)
76+ sw, sh := o.Transform.Size(o.Info.Width, o.Info.Height)
77+ return o.Transform.ToBuffer(px, py, float64(sw), float64(sh))
8178 }
8279
8380 // Layout is the global arrangement of outputs in logical pixels.
@@ -73,11 +73,8 @@ func (o OutputPlacement) ToTarget(x, y float64) (float64, float64) {
73 s = 173 s = 1
74 }74 }
75 px, py := (x-float64(o.X))*s, (y-float64(o.Y))*s75 px, py := (x-float64(o.X))*s, (y-float64(o.Y))*s
76- sw, sh := float64(o.Info.Width), float64(o.Info.Height)76+ sw, sh := o.Transform.Size(o.Info.Width, o.Info.Height)
77- if o.Transform.Rotated() {77+ return o.Transform.ToBuffer(px, py, float64(sw), float64(sh))
78- sw, sh = sh, sw
79- }
80- return o.Transform.ToBuffer(px, py, sw, sh)
81 }78 }
82 79
83 // Layout is the global arrangement of outputs in logical pixels.80 // Layout is the global arrangement of outputs in logical pixels.
M internal/ports/scene.go +9 -0
@@ -210,6 +210,15 @@ type BufferTransform uint8
210210 // Rotated reports whether width and height swap between buffer and surface.
211211 func (t BufferTransform) Rotated() bool { return t&1 != 0 }
212212
213+// Size returns the size of a w×h space seen through t: width and height swap
214+// when t is Rotated.
215+func (t BufferTransform) Size(w, h int) (int, int) {
216+ if t.Rotated() {
217+ return h, w
218+ }
219+ return w, h
220+}
221+
213222 // ToBuffer maps a point of a w×h surface (logical, before scale) to the
214223 // same point of its buffer, in the buffer's untransformed w'×h' axes, where
215224 // w' and h' swap when the transform is Rotated.
@@ -210,6 +210,15 @@ type BufferTransform uint8
210 // Rotated reports whether width and height swap between buffer and surface.210 // Rotated reports whether width and height swap between buffer and surface.
211 func (t BufferTransform) Rotated() bool { return t&1 != 0 }211 func (t BufferTransform) Rotated() bool { return t&1 != 0 }
212 212
213+// Size returns the size of a w×h space seen through t: width and height swap
214+// when t is Rotated.
215+func (t BufferTransform) Size(w, h int) (int, int) {
216+ if t.Rotated() {
217+ return h, w
218+ }
219+ return w, h
220+}
221+
213 // ToBuffer maps a point of a w×h surface (logical, before scale) to the222 // ToBuffer maps a point of a w×h surface (logical, before scale) to the
214 // same point of its buffer, in the buffer's untransformed w'×h' axes, where223 // same point of its buffer, in the buffer's untransformed w'×h' axes, where
215 // w' and h' swap when the transform is Rotated.224 // w' and h' swap when the transform is Rotated.
M internal/ports/transform_test.go +25 -3
@@ -57,10 +57,32 @@ func TestBufferTransformInvert(t *testing.T) {
5757 }
5858 }
5959
60+// RectToBuffer is the bounding box of the rect's corners under ToBuffer.
6061 func TestRectToBuffer(t *testing.T) {
61- r := image.Rect(0, 0, 2, 1)
62- assert.Equal(t, image.Rect(0, 5, 1, 7), BufferTransform(1).RectToBuffer(r, 7, 3))
63- assert.Equal(t, r, BufferTransform(0).RectToBuffer(r, 7, 3))
62+ const w, h = 7, 3
63+ r := image.Rect(1, 0, 3, 2)
64+ assert.Equal(t, image.Rect(0, 5, 1, 7), BufferTransform(1).RectToBuffer(image.Rect(0, 0, 2, 1), w, h))
65+ for tr := BufferTransform(0); tr < 8; tr++ {
66+ got := tr.RectToBuffer(r, w, h)
67+ x0, y0 := tr.ToBuffer(float64(r.Min.X), float64(r.Min.Y), w, h)
68+ x1, y1 := tr.ToBuffer(float64(r.Max.X), float64(r.Max.Y), w, h)
69+ x2, y2 := tr.ToBuffer(float64(r.Min.X), float64(r.Max.Y), w, h)
70+ x3, y3 := tr.ToBuffer(float64(r.Max.X), float64(r.Min.Y), w, h)
71+ want := image.Rect(int(min(x0, x1, x2, x3)), int(min(y0, y1, y2, y3)), int(max(x0, x1, x2, x3)), int(max(y0, y1, y2, y3)))
72+ assert.Equal(t, want, got, "transform %d", tr)
73+ assert.Equal(t, r.Dx()*r.Dy(), got.Dx()*got.Dy(), "transform %d area", tr)
74+ }
75+}
76+
77+func TestBufferTransformSize(t *testing.T) {
78+ for tr := BufferTransform(0); tr < 8; tr++ {
79+ w, h := tr.Size(7, 3)
80+ if tr.Rotated() {
81+ assert.Equal(t, [2]int{3, 7}, [2]int{w, h}, "transform %d", tr)
82+ } else {
83+ assert.Equal(t, [2]int{7, 3}, [2]int{w, h}, "transform %d", tr)
84+ }
85+ }
6486 }
6587
6688 func TestOutputPlacementToTarget(t *testing.T) {
@@ -57,10 +57,32 @@ func TestBufferTransformInvert(t *testing.T) {
57 }57 }
58 }58 }
59 59
60+// RectToBuffer is the bounding box of the rect's corners under ToBuffer.
60 func TestRectToBuffer(t *testing.T) {61 func TestRectToBuffer(t *testing.T) {
61- r := image.Rect(0, 0, 2, 1)62+ const w, h = 7, 3
62- assert.Equal(t, image.Rect(0, 5, 1, 7), BufferTransform(1).RectToBuffer(r, 7, 3))63+ r := image.Rect(1, 0, 3, 2)
63- assert.Equal(t, r, BufferTransform(0).RectToBuffer(r, 7, 3))64+ assert.Equal(t, image.Rect(0, 5, 1, 7), BufferTransform(1).RectToBuffer(image.Rect(0, 0, 2, 1), w, h))
65+ for tr := BufferTransform(0); tr < 8; tr++ {
66+ got := tr.RectToBuffer(r, w, h)
67+ x0, y0 := tr.ToBuffer(float64(r.Min.X), float64(r.Min.Y), w, h)
68+ x1, y1 := tr.ToBuffer(float64(r.Max.X), float64(r.Max.Y), w, h)
69+ x2, y2 := tr.ToBuffer(float64(r.Min.X), float64(r.Max.Y), w, h)
70+ x3, y3 := tr.ToBuffer(float64(r.Max.X), float64(r.Min.Y), w, h)
71+ want := image.Rect(int(min(x0, x1, x2, x3)), int(min(y0, y1, y2, y3)), int(max(x0, x1, x2, x3)), int(max(y0, y1, y2, y3)))
72+ assert.Equal(t, want, got, "transform %d", tr)
73+ assert.Equal(t, r.Dx()*r.Dy(), got.Dx()*got.Dy(), "transform %d area", tr)
74+ }
75+}
76+
77+func TestBufferTransformSize(t *testing.T) {
78+ for tr := BufferTransform(0); tr < 8; tr++ {
79+ w, h := tr.Size(7, 3)
80+ if tr.Rotated() {
81+ assert.Equal(t, [2]int{3, 7}, [2]int{w, h}, "transform %d", tr)
82+ } else {
83+ assert.Equal(t, [2]int{7, 3}, [2]int{w, h}, "transform %d", tr)
84+ }
85+ }
64 }86 }
65 87
66 func TestOutputPlacementToTarget(t *testing.T) {88 func TestOutputPlacementToTarget(t *testing.T) {

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