detached fac869b

perf(vulkan): draw position hint dots as single rounded quads

The pill and each dot were 1+2*radius scanline fills computed with
math.Sqrt (143 draws for 5x3, 2071 for 16x16). Each is now one
SDF-rounded quad: 1+N draws.
commit
fac869b569de1ba1c42dc7a92903522e4284aa65
author
brice · · 2026-10-21 08:05 UTC
parent
37f59a2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +80 -13
M internal/adapters/vulkan/position_hint.go +7 -13
@@ -2,7 +2,6 @@ package vulkan
22
33 import (
44 "image"
5- "math"
65
76 "github.com/bnema/neferwl/internal/ports"
87 )
@@ -38,20 +37,15 @@ func (w *sceneWalk) positionHint() {
3837 w.alpha = alpha
3938 }
4039
41-// Rounded fills use the existing solid instances and batching pipeline. Each
42-// physical scanline is bounded by the scaled radius; no textures or per-frame
43-// buffers are allocated. Non-overlapping strips preserve translucent alpha.
40+// hintRound draws one rounded quad (SDF in the shader), so the pill and each
41+// dot cost a single draw whatever the scale. Fills are one translucent draw,
42+// so alpha stays exact.
4443 func (w *sceneWalk) hintRound(r image.Rectangle, radius int, color [3]uint8) {
4544 radius = min(radius, r.Dx()/2, r.Dy()/2)
46- if radius <= 0 {
47- w.fill(r, color)
45+ n := len(w.draws)
46+ w.fill(r, color)
47+ if radius <= 0 || len(w.draws) == n {
4848 return
4949 }
50- w.fill(image.Rect(r.Min.X, r.Min.Y+radius, r.Max.X, r.Max.Y-radius), color)
51- for y := range radius {
52- dy := float64(radius-y) - 0.5
53- inset := radius - int(math.Sqrt(float64(radius*radius)-dy*dy)+0.5)
54- w.fill(image.Rect(r.Min.X+inset, r.Min.Y+y, r.Max.X-inset, r.Min.Y+y+1), color)
55- w.fill(image.Rect(r.Min.X+inset, r.Max.Y-y-1, r.Max.X-inset, r.Max.Y-y), color)
56- }
50+ roundedDraw(&w.draws[n], r, float32(radius))
5751 }
@@ -2,7 +2,6 @@ package vulkan
2 2
3 import (3 import (
4 "image"4 "image"
5- "math"
6 5
7 "github.com/bnema/neferwl/internal/ports"6 "github.com/bnema/neferwl/internal/ports"
8 )7 )
@@ -38,20 +37,15 @@ func (w *sceneWalk) positionHint() {
38 w.alpha = alpha37 w.alpha = alpha
39 }38 }
40 39
41-// Rounded fills use the existing solid instances and batching pipeline. Each40+// hintRound draws one rounded quad (SDF in the shader), so the pill and each
42-// physical scanline is bounded by the scaled radius; no textures or per-frame41+// dot cost a single draw whatever the scale. Fills are one translucent draw,
43-// buffers are allocated. Non-overlapping strips preserve translucent alpha.42+// so alpha stays exact.
44 func (w *sceneWalk) hintRound(r image.Rectangle, radius int, color [3]uint8) {43 func (w *sceneWalk) hintRound(r image.Rectangle, radius int, color [3]uint8) {
45 radius = min(radius, r.Dx()/2, r.Dy()/2)44 radius = min(radius, r.Dx()/2, r.Dy()/2)
46- if radius <= 0 {45+ n := len(w.draws)
47- w.fill(r, color)46+ w.fill(r, color)
47+ if radius <= 0 || len(w.draws) == n {
48 return48 return
49 }49 }
50- w.fill(image.Rect(r.Min.X, r.Min.Y+radius, r.Max.X, r.Max.Y-radius), color)50+ roundedDraw(&w.draws[n], r, float32(radius))
51- for y := range radius {
52- dy := float64(radius-y) - 0.5
53- inset := radius - int(math.Sqrt(float64(radius*radius)-dy*dy)+0.5)
54- w.fill(image.Rect(r.Min.X+inset, r.Min.Y+y, r.Max.X-inset, r.Min.Y+y+1), color)
55- w.fill(image.Rect(r.Min.X+inset, r.Max.Y-y-1, r.Max.X-inset, r.Max.Y-y), color)
56- }
57 }51 }
A internal/adapters/vulkan/position_hint_bench_test.go +35 -0
@@ -0,0 +1,35 @@
1+package vulkan
2+
3+import (
4+ "image"
5+ "testing"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+func hintGrid(cols, rows int) ports.PositionHint {
11+ h := ports.PositionHint{Opacity: 1, Columns: uint8(cols), ActiveCol: 1, ActiveRow: 1}
12+ for c := range cols {
13+ h.Counts[c] = uint8(rows)
14+ }
15+ return h
16+}
17+
18+func BenchmarkPositionHintDraws(b *testing.B) {
19+ for _, g := range []struct {
20+ name string
21+ cols, rows int
22+ }{{"5x3", 5, 3}, {"16x16", 16, 16}} {
23+ b.Run(g.name, func(b *testing.B) {
24+ r := &Renderer{width: 1920, height: 1080}
25+ s := ports.Scene{OutputWidth: 1920, OutputHeight: 1080, Scale: 1, PositionHint: hintGrid(g.cols, g.rows)}
26+ dmg := newDamage(&target{}, s, image.Rect(0, 0, r.width, r.height))
27+ b.ReportAllocs()
28+ var n int
29+ for b.Loop() {
30+ n = len(r.draws(s, nil, dmg))
31+ }
32+ b.ReportMetric(float64(n), "draws")
33+ })
34+ }
35+}
@@ -0,0 +1,35 @@
1+package vulkan
2+
3+import (
4+ "image"
5+ "testing"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+func hintGrid(cols, rows int) ports.PositionHint {
11+ h := ports.PositionHint{Opacity: 1, Columns: uint8(cols), ActiveCol: 1, ActiveRow: 1}
12+ for c := range cols {
13+ h.Counts[c] = uint8(rows)
14+ }
15+ return h
16+}
17+
18+func BenchmarkPositionHintDraws(b *testing.B) {
19+ for _, g := range []struct {
20+ name string
21+ cols, rows int
22+ }{{"5x3", 5, 3}, {"16x16", 16, 16}} {
23+ b.Run(g.name, func(b *testing.B) {
24+ r := &Renderer{width: 1920, height: 1080}
25+ s := ports.Scene{OutputWidth: 1920, OutputHeight: 1080, Scale: 1, PositionHint: hintGrid(g.cols, g.rows)}
26+ dmg := newDamage(&target{}, s, image.Rect(0, 0, r.width, r.height))
27+ b.ReportAllocs()
28+ var n int
29+ for b.Loop() {
30+ n = len(r.draws(s, nil, dmg))
31+ }
32+ b.ReportMetric(float64(n), "draws")
33+ })
34+ }
35+}
M internal/adapters/vulkan/position_hint_test.go +38 -0
@@ -4,6 +4,7 @@ import (
44 "github.com/bnema/neferwl/internal/ports"
55 "github.com/stretchr/testify/require"
66 "image"
7+ "math"
78 "testing"
89 )
910
@@ -69,3 +70,40 @@ func TestPositionHintDamageTransitions(t *testing.T) {
6970 require.False(t, ok, "scale change requires full target redraw; geometry scaling is checked separately")
7071 })
7172 }
73+
74+func 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+
100+func 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+}
@@ -4,6 +4,7 @@ import (
4 "github.com/bnema/neferwl/internal/ports"4 "github.com/bnema/neferwl/internal/ports"
5 "github.com/stretchr/testify/require"5 "github.com/stretchr/testify/require"
6 "image"6 "image"
7+ "math"
7 "testing"8 "testing"
8 )9 )
9 10
@@ -69,3 +70,40 @@ func TestPositionHintDamageTransitions(t *testing.T) {
69 require.False(t, ok, "scale change requires full target redraw; geometry scaling is checked separately")70 require.False(t, ok, "scale change requires full target redraw; geometry scaling is checked separately")
70 })71 })
71 }72 }
73+
74+func 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+
100+func 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+}

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 24

Tags 10

↑↓ moveenter openesc close