v0.2.0 13c6a90

kms.go RawHistoryFolder

1// Package drm drives KMS outputs with raw ioctls and atomic commits.
2package drm
3
4import (
5	"fmt"
6	"math"
7	"time"
8	"unsafe"
9
10	"github.com/bnema/neferwl/internal/ports"
11	"golang.org/x/sys/unix"
12)
13
14// ioctl numbers for amd64/arm64 (DRM_IOWR('d', nr, size)).
15const (
16	ioctlGetResources = 0xC04064A0
17	ioctlGetCrtc      = 0xC06864A1
18	ioctlGetEncoder   = 0xC01464A6
19	ioctlGetConnector = 0xC05064A7
20	ioctlRmFB         = 0xC00464AF
21
22	eventFlipDone    = 2
23	modeTypePrefered = 1 << 3
24	connected        = 1
25)
26
27type cardRes struct {
28	fbs, crtcs, connectors, encoders                uint64
29	countFbs, countCrtcs, countConns, countEncoders uint32
30	minW, maxW, minH, maxH                          uint32
31}
32
33type modeInfo struct {
34	Clock                                  uint32
35	HDisplay, HSyncStart, HSyncEnd, HTotal uint16
36	HSkew                                  uint16
37	VDisplay, VSyncStart, VSyncEnd, VTotal uint16
38	VScan                                  uint16
39	VRefresh, Flags, Type                  uint32
40	Name                                   [32]byte
41}
42
43func (m modeInfo) String() string {
44	return fmt.Sprintf("%dx%d@%.3f", m.HDisplay, m.VDisplay, float64(m.refreshMilli())/1000)
45}
46
47type getConnector struct {
48	encoders, modes, props, propValues       uint64
49	countModes, countProps, countEncoders    uint32
50	encoderID, connectorID, connType, typeID uint32
51	connection, mmW, mmH, subpixel           uint32
52	_                                        uint32
53}
54
55type getEncoder struct{ encoderID, encoderType, crtcID, possibleCrtcs, possibleClones uint32 }
56
57type modeCrtc struct {
58	// unsafe.Pointer (not uint64) so the GC keeps it valid if the stack moves.
59	setConnectors   unsafe.Pointer
60	countConnectors uint32
61	crtcID, fbID    uint32
62	x, y            uint32
63	gammaSize       uint32
64	modeValid       uint32
65	mode            modeInfo
66}
67
68func ioctl(fd int, req uintptr, arg unsafe.Pointer) error {
69	for {
70		_, _, e := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), req, uintptr(arg))
71		if e == unix.EINTR || e == unix.EAGAIN {
72			continue
73		}
74		if e != 0 {
75			return e
76		}
77		return nil
78	}
79}
80
81// connector is the subset of KMS connector state used to pick an output.
82type connector struct {
83	id, encoderID uint32
84	name          string
85	connected     bool
86	nonDesktop    bool
87	mmW, mmH      int
88	modes         []modeInfo
89	encoders      []uint32
90}
91
92var connectorTypes = map[uint32]string{1: "VGA", 2: "DVI-I", 3: "DVI-D", 10: "DP", 11: "HDMI-A", 12: "HDMI-B", 7: "LVDS", 14: "eDP", 15: "Virtual", 16: "DSI", 17: "DPI", 20: "USB"}
93
94// Want configures connectors; see ports.OutputConfig.
95type Want struct {
96	Disabled map[string]bool
97	// Modes maps a connector to W, H and Hz (0: highest refresh at W x H).
98	Modes map[string][3]float64
99	// HDR settings are retained per connector; phase 1 never applies them to KMS.
100	HDR map[string]HDRSettings
101	// NoScanout disables direct scanout (render.direct-scanout = off).
102	NoScanout bool
103	// NoTearing ignores tearing requests (render.tearing = off).
104	NoTearing bool
105	// NoVRR keeps variable refresh off (render.vrr = off).
106	NoVRR bool
107	// Sampled are the dmabuf formats the renderer composes: direct
108	// scanout is offered only for those, so a refused buffer still draws.
109	Sampled []ports.DMABufFormat
110	// Device is the KMS device (dev_t) offered in scanout tranches.
111	Device uint64
112	// TraceFlips logs each frame and commit completion with its timing
113	// (--debug=drm-flip).
114	TraceFlips bool
115	// VRRFlipGap delays a game frame after the previous flip under VRR
116	// (render.vrr-flip-gap, 0: off; see vrr_flip_gap.go).
117	VRRFlipGap time.Duration
118}
119
120// HDRSettings is the requested HDR state and SDR reference white (nits).
121type HDRSettings struct {
122	Enabled       bool
123	SDRBrightness int
124}
125
126// usable reports whether a connector should be driven.
127func (w Want) usable(c connector) bool {
128	return c.connected && !c.nonDesktop && len(c.modes) > 0 && !w.Disabled[c.name]
129}
130
131// refreshMilli is the exact refresh in mHz, computed from the timings like wlroots.
132func (m modeInfo) refreshMilli() int {
133	if m.HTotal == 0 || m.VTotal == 0 {
134		return int(m.VRefresh) * 1000
135	}
136	r := (int64(m.Clock)*1_000_000/int64(m.HTotal) + int64(m.VTotal)/2) / int64(m.VTotal)
137	if m.Flags&(1<<4) != 0 { // interlace
138		r *= 2
139	}
140	if m.Flags&(1<<5) != 0 { // doublescan
141		r /= 2
142	}
143	if m.VScan > 1 {
144		r /= int64(m.VScan)
145	}
146	return int(r)
147}
148
149// pickMode returns the configured mode of the connector, else its preferred
150// one, else the first.
151func (w Want) pickMode(c connector) modeInfo {
152	if m, ok := w.Modes[c.name]; ok {
153		if mode, ok := pickMode(c.modes, int(m[0]), int(m[1]), m[2]); ok {
154			return mode
155		}
156	}
157	for _, m := range c.modes {
158		if m.Type&modeTypePrefered != 0 {
159			return m
160		}
161	}
162	return c.modes[0]
163}
164
165// pickMode finds W x H with the refresh closest to hz, or the highest when hz is 0.
166func pickMode(modes []modeInfo, w, h int, hz float64) (modeInfo, bool) {
167	best, found := modeInfo{}, false
168	score := func(m modeInfo) float64 {
169		r := float64(m.refreshMilli()) / 1000
170		if hz == 0 {
171			return -r
172		}
173		return math.Abs(r - hz)
174	}
175	for _, m := range modes {
176		if int(m.HDisplay) != w || int(m.VDisplay) != h || m.Flags&(1<<4) != 0 {
177			continue
178		}
179		if !found || score(m) < score(best) {
180			best, found = m, true
181		}
182	}
183	return best, found
184}
185
186func resources(fd int) (crtcs, conns []uint32, err error) {
187	var r cardRes
188	if err := ioctl(fd, ioctlGetResources, unsafe.Pointer(&r)); err != nil {
189		return nil, nil, fmt.Errorf("get resources: %w", err)
190	}
191	crtcs = make([]uint32, r.countCrtcs)
192	conns = make([]uint32, r.countConns)
193	r = cardRes{countCrtcs: r.countCrtcs, countConns: r.countConns}
194	if len(crtcs) > 0 {
195		r.crtcs = uint64(uintptr(unsafe.Pointer(&crtcs[0])))
196	}
197	if len(conns) > 0 {
198		r.connectors = uint64(uintptr(unsafe.Pointer(&conns[0])))
199	}
200	if err := ioctl(fd, ioctlGetResources, unsafe.Pointer(&r)); err != nil {
201		return nil, nil, fmt.Errorf("get resources: %w", err)
202	}
203	return crtcs[:min(int(r.countCrtcs), len(crtcs))], conns[:min(int(r.countConns), len(conns))], nil
204}
205
206func readConnector(fd int, id uint32) (connector, error) {
207	g := getConnector{connectorID: id}
208	if err := ioctl(fd, ioctlGetConnector, unsafe.Pointer(&g)); err != nil {
209		return connector{}, err
210	}
211	modes := make([]modeInfo, g.countModes)
212	encs := make([]uint32, g.countEncoders)
213	g2 := getConnector{connectorID: id, countModes: g.countModes, countEncoders: g.countEncoders}
214	if len(modes) > 0 {
215		g2.modes = uint64(uintptr(unsafe.Pointer(&modes[0])))
216	}
217	if len(encs) > 0 {
218		g2.encoders = uint64(uintptr(unsafe.Pointer(&encs[0])))
219	}
220	if err := ioctl(fd, ioctlGetConnector, unsafe.Pointer(&g2)); err != nil {
221		return connector{}, err
222	}
223	name := connectorTypes[g2.connType]
224	if name == "" {
225		name = "Unknown"
226	}
227	props, err := objProps(fd, id, objConnector)
228	if err != nil {
229		return connector{}, fmt.Errorf("connector %d properties: %w", id, err)
230	}
231	return connector{
232		id: id, encoderID: g2.encoderID, name: fmt.Sprintf("%s-%d", name, g2.typeID),
233		connected: g2.connection == connected, nonDesktop: props["non-desktop"][1] == 1,
234		mmW: int(g2.mmW), mmH: int(g2.mmH),
235		modes:    modes[:min(int(g2.countModes), len(modes))],
236		encoders: encs[:min(int(g2.countEncoders), len(encs))],
237	}, nil
238}
239
240// pickCrtc prefers the connector's current encoder CRTC, else any free CRTC
241// an encoder allows. used holds CRTCs already driving other outputs.
242func pickCrtc(fd int, c connector, crtcs []uint32, used map[uint32]bool) (uint32, error) {
243	encs := c.encoders
244	if c.encoderID != 0 {
245		encs = append([]uint32{c.encoderID}, encs...)
246	}
247	for _, id := range encs {
248		e := getEncoder{encoderID: id}
249		if ioctl(fd, ioctlGetEncoder, unsafe.Pointer(&e)) != nil {
250			continue
251		}
252		if id == c.encoderID && e.crtcID != 0 && !used[e.crtcID] {
253			return e.crtcID, nil
254		}
255		for i, crtc := range crtcs {
256			if e.possibleCrtcs&(1<<i) != 0 && !used[crtc] {
257				return crtc, nil
258			}
259		}
260	}
261	return 0, fmt.Errorf("no free CRTC for %s", c.name)
262}
263
264func getCrtc(fd int, crtc uint32) (modeCrtc, error) {
265	c := modeCrtc{crtcID: crtc}
266	err := ioctl(fd, ioctlGetCrtc, unsafe.Pointer(&c))
267	return c, err
268}

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