fix(drm): keep connector array GC-visible and harden VT resume, flips and shutdown
- commit
- 25625c3dec38ae11dc1c0cea7b192cb0e1e9ba43
- author
- brice · · 2026-09-24 08:25 UTC
- parent
- 0679ea5
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +57 -21
M cmd/nefertty/main.go +2 -0
| @@ -184,6 +184,8 @@ func run() error { | |||
| 184 | 184 | reason = "error" | |
| 185 | 185 | case ctx.Err() != nil: | |
| 186 | 186 | reason = "signal" | |
| 187 | + | case *timeout > 0 && time.Since(start) >= *timeout: | |
| 188 | + | reason = "timeout" | |
| 187 | 189 | } | |
| 188 | 190 | log.Info().Str("reason", reason).AnErr("error", err).Dur("uptime", time.Since(start)).Msg("exit") | |
| 189 | 191 | return err | |
| @@ -184,6 +184,8 @@ func run() error { | |||||
| 184 | reason = "error" | 184 | reason = "error" | ||
| 185 | case ctx.Err() != nil: | 185 | case ctx.Err() != nil: | ||
| 186 | reason = "signal" | 186 | reason = "signal" | ||
| 187 | + | case *timeout > 0 && time.Since(start) >= *timeout: | |||
| 188 | + | reason = "timeout" | |||
| 187 | } | 189 | } | ||
| 188 | log.Info().Str("reason", reason).AnErr("error", err).Dur("uptime", time.Since(start)).Msg("exit") | 190 | log.Info().Str("reason", reason).AnErr("error", err).Dur("uptime", time.Since(start)).Msg("exit") | ||
| 189 | return err | 191 | return err | ||
M internal/adapters/drm/kms.go +5 -4
| @@ -62,7 +62,8 @@ type getConnector struct { | |||
| 62 | 62 | type getEncoder struct{ encoderID, encoderType, crtcID, possibleCrtcs, possibleClones uint32 } | |
| 63 | 63 | ||
| 64 | 64 | type modeCrtc struct { | |
| 65 | - | setConnectors uint64 | |
| 65 | + | // unsafe.Pointer (not uint64) so the GC keeps it valid if the stack moves. | |
| 66 | + | setConnectors unsafe.Pointer | |
| 66 | 67 | countConnectors uint32 | |
| 67 | 68 | crtcID, fbID uint32 | |
| 68 | 69 | x, y uint32 | |
| @@ -111,7 +112,7 @@ type connector struct { | |||
| 111 | 112 | encoders []uint32 | |
| 112 | 113 | } | |
| 113 | 114 | ||
| 114 | - | var connectorTypes = map[uint32]string{1: "VGA", 2: "DVI-I", 3: "DVI-D", 10: "DP", 11: "HDMI-A", 12: "HDMI-B", 14: "eDP", 15: "Virtual", 16: "DSI"} | |
| 115 | + | var 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"} | |
| 115 | 116 | ||
| 116 | 117 | // pickConnector returns the first connected connector and its preferred (else first) mode. | |
| 117 | 118 | func pickConnector(conns []connector) (connector, modeInfo, error) { | |
| @@ -146,7 +147,7 @@ func resources(fd int) (crtcs, conns []uint32, err error) { | |||
| 146 | 147 | if err := ioctl(fd, ioctlGetResources, unsafe.Pointer(&r)); err != nil { | |
| 147 | 148 | return nil, nil, fmt.Errorf("get resources: %w", err) | |
| 148 | 149 | } | |
| 149 | - | return crtcs[:r.countCrtcs], conns[:r.countConns], nil | |
| 150 | + | return crtcs[:min(int(r.countCrtcs), len(crtcs))], conns[:min(int(r.countConns), len(conns))], nil | |
| 150 | 151 | } | |
| 151 | 152 | ||
| 152 | 153 | func readConnector(fd int, id uint32) (connector, error) { | |
| @@ -260,7 +261,7 @@ func copyXRGB(dst []byte, pitch int, img *image.RGBA) { | |||
| 260 | 261 | ||
| 261 | 262 | func setCrtc(fd int, crtc, conn, fb uint32, mode *modeInfo) error { | |
| 262 | 263 | ids := []uint32{conn} | |
| 263 | - | c := modeCrtc{crtcID: crtc, fbID: fb, countConnectors: 1, setConnectors: uint64(uintptr(unsafe.Pointer(&ids[0])))} | |
| 264 | + | c := modeCrtc{crtcID: crtc, fbID: fb, countConnectors: 1, setConnectors: unsafe.Pointer(&ids[0])} | |
| 264 | 265 | if mode != nil { | |
| 265 | 266 | c.mode, c.modeValid = *mode, 1 | |
| 266 | 267 | } | |
| @@ -62,7 +62,8 @@ type getConnector struct { | |||||
| 62 | type getEncoder struct{ encoderID, encoderType, crtcID, possibleCrtcs, possibleClones uint32 } | 62 | type getEncoder struct{ encoderID, encoderType, crtcID, possibleCrtcs, possibleClones uint32 } | ||
| 63 | 63 | ||||
| 64 | type modeCrtc struct { | 64 | type modeCrtc struct { | ||
| 65 | - | setConnectors uint64 | 65 | + | // unsafe.Pointer (not uint64) so the GC keeps it valid if the stack moves. |
| 66 | + | setConnectors unsafe.Pointer | |||
| 66 | countConnectors uint32 | 67 | countConnectors uint32 | ||
| 67 | crtcID, fbID uint32 | 68 | crtcID, fbID uint32 | ||
| 68 | x, y uint32 | 69 | x, y uint32 | ||
| @@ -111,7 +112,7 @@ type connector struct { | |||||
| 111 | encoders []uint32 | 112 | encoders []uint32 | ||
| 112 | } | 113 | } | ||
| 113 | 114 | ||||
| 114 | - | var connectorTypes = map[uint32]string{1: "VGA", 2: "DVI-I", 3: "DVI-D", 10: "DP", 11: "HDMI-A", 12: "HDMI-B", 14: "eDP", 15: "Virtual", 16: "DSI"} | 115 | + | var 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"} |
| 115 | 116 | ||||
| 116 | // pickConnector returns the first connected connector and its preferred (else first) mode. | 117 | // pickConnector returns the first connected connector and its preferred (else first) mode. | ||
| 117 | func pickConnector(conns []connector) (connector, modeInfo, error) { | 118 | func pickConnector(conns []connector) (connector, modeInfo, error) { | ||
| @@ -146,7 +147,7 @@ func resources(fd int) (crtcs, conns []uint32, err error) { | |||||
| 146 | if err := ioctl(fd, ioctlGetResources, unsafe.Pointer(&r)); err != nil { | 147 | if err := ioctl(fd, ioctlGetResources, unsafe.Pointer(&r)); err != nil { | ||
| 147 | return nil, nil, fmt.Errorf("get resources: %w", err) | 148 | return nil, nil, fmt.Errorf("get resources: %w", err) | ||
| 148 | } | 149 | } | ||
| 149 | - | return crtcs[:r.countCrtcs], conns[:r.countConns], nil | 150 | + | return crtcs[:min(int(r.countCrtcs), len(crtcs))], conns[:min(int(r.countConns), len(conns))], nil |
| 150 | } | 151 | } | ||
| 151 | 152 | ||||
| 152 | func readConnector(fd int, id uint32) (connector, error) { | 153 | func readConnector(fd int, id uint32) (connector, error) { | ||
| @@ -260,7 +261,7 @@ func copyXRGB(dst []byte, pitch int, img *image.RGBA) { | |||||
| 260 | 261 | ||||
| 261 | func setCrtc(fd int, crtc, conn, fb uint32, mode *modeInfo) error { | 262 | func setCrtc(fd int, crtc, conn, fb uint32, mode *modeInfo) error { | ||
| 262 | ids := []uint32{conn} | 263 | ids := []uint32{conn} | ||
| 263 | - | c := modeCrtc{crtcID: crtc, fbID: fb, countConnectors: 1, setConnectors: uint64(uintptr(unsafe.Pointer(&ids[0])))} | 264 | + | c := modeCrtc{crtcID: crtc, fbID: fb, countConnectors: 1, setConnectors: unsafe.Pointer(&ids[0])} |
| 264 | if mode != nil { | 265 | if mode != nil { | ||
| 265 | c.mode, c.modeValid = *mode, 1 | 266 | c.mode, c.modeValid = *mode, 1 | ||
| 266 | } | 267 | } | ||
M internal/adapters/drm/output.go +19 -3
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "errors" | |
| 6 | 6 | "fmt" | |
| 7 | 7 | "image" | |
| 8 | + | "sync" | |
| 8 | 9 | "time" | |
| 9 | 10 | ||
| 10 | 11 | "github.com/bnema/nefertty/internal/ports" | |
| @@ -59,7 +60,9 @@ func Open(fd int, card string, log zerowrap.Logger) (*Output, error) { | |||
| 59 | 60 | return nil, err | |
| 60 | 61 | } | |
| 61 | 62 | o := &Output{fd: fd, crtc: crtc, conn: c, mode: mode, log: log} | |
| 62 | - | o.saved, _ = getCrtc(fd, crtc) | |
| 63 | + | if o.saved, err = getCrtc(fd, crtc); err != nil { | |
| 64 | + | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | |
| 65 | + | } | |
| 63 | 66 | for i := range o.bufs { | |
| 64 | 67 | if o.bufs[i], err = newDumb(fd, o.Width(), o.Height()); err != nil { | |
| 65 | 68 | o.Close() | |
| @@ -124,7 +127,11 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (Renderer, | |||
| 124 | 127 | } | |
| 125 | 128 | events := make(chan int, 4) | |
| 126 | 129 | evErr := make(chan error, 1) | |
| 127 | - | go o.readEvents(ctx, events, evErr) | |
| 130 | + | evCtx, stop := context.WithCancel(ctx) | |
| 131 | + | var reader sync.WaitGroup | |
| 132 | + | reader.Go(func() { o.readEvents(evCtx, events, evErr) }) | |
| 133 | + | // Join the reader before the card fd can be closed by the caller. | |
| 134 | + | defer func() { stop(); reader.Wait() }() | |
| 128 | 135 | surfaces := make(map[ports.WindowID]ports.SurfaceContent) | |
| 129 | 136 | var scene ports.Scene | |
| 130 | 137 | haveScene, dirty, enabled := false, false, true | |
| @@ -138,7 +145,8 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (Renderer, | |||
| 138 | 145 | case err := <-evErr: | |
| 139 | 146 | return err | |
| 140 | 147 | case on := <-active: | |
| 141 | - | if on && !enabled { | |
| 148 | + | // Modeset on every enable: a fast disable+enable can coalesce to one true. | |
| 149 | + | if on { | |
| 142 | 150 | if err := o.modeset(); err != nil { | |
| 143 | 151 | o.log.Error().Err(err).Msg("resume") | |
| 144 | 152 | return err | |
| @@ -148,6 +156,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (Renderer, | |||
| 148 | 156 | enabled = on | |
| 149 | 157 | o.log.Info().Bool("enabled", on).Msg("output") | |
| 150 | 158 | case n := <-events: | |
| 159 | + | if !o.pending { | |
| 160 | + | continue // stale event from before a modeset | |
| 161 | + | } | |
| 151 | 162 | o.pending = false | |
| 152 | 163 | o.flips += n | |
| 153 | 164 | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | |
| @@ -173,6 +184,11 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (Renderer, | |||
| 173 | 184 | return fmt.Errorf("render frame: %w", err) | |
| 174 | 185 | } | |
| 175 | 186 | if err := o.present(r.Pixels()); err != nil { | |
| 187 | + | if errors.Is(err, unix.EBUSY) { | |
| 188 | + | // A flip is still in flight; retry after its event. | |
| 189 | + | o.pending, o.flipStart = true, time.Now() | |
| 190 | + | continue // dirty stays true | |
| 191 | + | } | |
| 176 | 192 | if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) { | |
| 177 | 193 | // Lost DRM master mid-switch; the seat disable follows. | |
| 178 | 194 | o.log.Warn().Err(err).Msg("flip refused") | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "errors" | 5 | "errors" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "image" | 7 | "image" | ||
| 8 | + | "sync" | |||
| 8 | "time" | 9 | "time" | ||
| 9 | 10 | ||||
| 10 | "github.com/bnema/nefertty/internal/ports" | 11 | "github.com/bnema/nefertty/internal/ports" | ||
| @@ -59,7 +60,9 @@ func Open(fd int, card string, log zerowrap.Logger) (*Output, error) { | |||||
| 59 | return nil, err | 60 | return nil, err | ||
| 60 | } | 61 | } | ||
| 61 | o := &Output{fd: fd, crtc: crtc, conn: c, mode: mode, log: log} | 62 | o := &Output{fd: fd, crtc: crtc, conn: c, mode: mode, log: log} | ||
| 62 | - | o.saved, _ = getCrtc(fd, crtc) | 63 | + | if o.saved, err = getCrtc(fd, crtc); err != nil { |
| 64 | + | log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit") | |||
| 65 | + | } | |||
| 63 | for i := range o.bufs { | 66 | for i := range o.bufs { | ||
| 64 | if o.bufs[i], err = newDumb(fd, o.Width(), o.Height()); err != nil { | 67 | if o.bufs[i], err = newDumb(fd, o.Width(), o.Height()); err != nil { | ||
| 65 | o.Close() | 68 | o.Close() | ||
| @@ -124,7 +127,11 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (Renderer, | |||||
| 124 | } | 127 | } | ||
| 125 | events := make(chan int, 4) | 128 | events := make(chan int, 4) | ||
| 126 | evErr := make(chan error, 1) | 129 | evErr := make(chan error, 1) | ||
| 127 | - | go o.readEvents(ctx, events, evErr) | 130 | + | evCtx, stop := context.WithCancel(ctx) |
| 131 | + | var reader sync.WaitGroup | |||
| 132 | + | reader.Go(func() { o.readEvents(evCtx, events, evErr) }) | |||
| 133 | + | // Join the reader before the card fd can be closed by the caller. | |||
| 134 | + | defer func() { stop(); reader.Wait() }() | |||
| 128 | surfaces := make(map[ports.WindowID]ports.SurfaceContent) | 135 | surfaces := make(map[ports.WindowID]ports.SurfaceContent) | ||
| 129 | var scene ports.Scene | 136 | var scene ports.Scene | ||
| 130 | haveScene, dirty, enabled := false, false, true | 137 | haveScene, dirty, enabled := false, false, true | ||
| @@ -138,7 +145,8 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (Renderer, | |||||
| 138 | case err := <-evErr: | 145 | case err := <-evErr: | ||
| 139 | return err | 146 | return err | ||
| 140 | case on := <-active: | 147 | case on := <-active: | ||
| 141 | - | if on && !enabled { | 148 | + | // Modeset on every enable: a fast disable+enable can coalesce to one true. |
| 149 | + | if on { | |||
| 142 | if err := o.modeset(); err != nil { | 150 | if err := o.modeset(); err != nil { | ||
| 143 | o.log.Error().Err(err).Msg("resume") | 151 | o.log.Error().Err(err).Msg("resume") | ||
| 144 | return err | 152 | return err | ||
| @@ -148,6 +156,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (Renderer, | |||||
| 148 | enabled = on | 156 | enabled = on | ||
| 149 | o.log.Info().Bool("enabled", on).Msg("output") | 157 | o.log.Info().Bool("enabled", on).Msg("output") | ||
| 150 | case n := <-events: | 158 | case n := <-events: | ||
| 159 | + | if !o.pending { | |||
| 160 | + | continue // stale event from before a modeset | |||
| 161 | + | } | |||
| 151 | o.pending = false | 162 | o.pending = false | ||
| 152 | o.flips += n | 163 | o.flips += n | ||
| 153 | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | 164 | if d := time.Since(o.flipStart); d > 20*time.Millisecond { | ||
| @@ -173,6 +184,11 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (Renderer, | |||||
| 173 | return fmt.Errorf("render frame: %w", err) | 184 | return fmt.Errorf("render frame: %w", err) | ||
| 174 | } | 185 | } | ||
| 175 | if err := o.present(r.Pixels()); err != nil { | 186 | if err := o.present(r.Pixels()); err != nil { | ||
| 187 | + | if errors.Is(err, unix.EBUSY) { | |||
| 188 | + | // A flip is still in flight; retry after its event. | |||
| 189 | + | o.pending, o.flipStart = true, time.Now() | |||
| 190 | + | continue // dirty stays true | |||
| 191 | + | } | |||
| 176 | if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) { | 192 | if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) { | ||
| 177 | // Lost DRM master mid-switch; the seat disable follows. | 193 | // Lost DRM master mid-switch; the seat disable follows. | ||
| 178 | o.log.Warn().Err(err).Msg("flip refused") | 194 | o.log.Warn().Err(err).Msg("flip refused") | ||
M internal/adapters/seat/seat.go +4 -5
| @@ -128,9 +128,11 @@ func (s *Seat) Name() string { | |||
| 128 | 128 | } | |
| 129 | 129 | ||
| 130 | 130 | // Subscribe returns a channel that receives the seat state after every enable/disable. | |
| 131 | + | // The current state is sent first. | |
| 131 | 132 | func (s *Seat) Subscribe() <-chan bool { | |
| 132 | 133 | c := make(chan bool, 1) | |
| 133 | 134 | s.mu.Lock() | |
| 135 | + | c <- s.active | |
| 134 | 136 | s.subs = append(s.subs, c) | |
| 135 | 137 | s.mu.Unlock() | |
| 136 | 138 | return c | |
| @@ -155,13 +157,10 @@ func (s *Seat) dispatchLocked(ctx context.Context, timeoutMs int) error { | |||
| 155 | 157 | func (s *Seat) Run(ctx context.Context) error { | |
| 156 | 158 | for ctx.Err() == nil { | |
| 157 | 159 | fds := []unix.PollFd{{Fd: getFD(s.handle), Events: unix.POLLIN}} | |
| 158 | - | n, err := unix.Poll(fds, 100) | |
| 159 | - | if err != nil && !errors.Is(err, unix.EINTR) { | |
| 160 | + | if _, err := unix.Poll(fds, 100); err != nil && !errors.Is(err, unix.EINTR) { | |
| 160 | 161 | return fmt.Errorf("seat poll: %w", err) | |
| 161 | 162 | } | |
| 162 | - | if n <= 0 { | |
| 163 | - | continue | |
| 164 | - | } | |
| 163 | + | // Dispatch every tick: libseat may queue events without the fd becoming readable. | |
| 165 | 164 | s.mu.Lock() | |
| 166 | 165 | r := dispatch(s.handle, 0) | |
| 167 | 166 | s.mu.Unlock() | |
| @@ -128,9 +128,11 @@ func (s *Seat) Name() string { | |||||
| 128 | } | 128 | } | ||
| 129 | 129 | ||||
| 130 | // Subscribe returns a channel that receives the seat state after every enable/disable. | 130 | // Subscribe returns a channel that receives the seat state after every enable/disable. | ||
| 131 | + | // The current state is sent first. | |||
| 131 | func (s *Seat) Subscribe() <-chan bool { | 132 | func (s *Seat) Subscribe() <-chan bool { | ||
| 132 | c := make(chan bool, 1) | 133 | c := make(chan bool, 1) | ||
| 133 | s.mu.Lock() | 134 | s.mu.Lock() | ||
| 135 | + | c <- s.active | |||
| 134 | s.subs = append(s.subs, c) | 136 | s.subs = append(s.subs, c) | ||
| 135 | s.mu.Unlock() | 137 | s.mu.Unlock() | ||
| 136 | return c | 138 | return c | ||
| @@ -155,13 +157,10 @@ func (s *Seat) dispatchLocked(ctx context.Context, timeoutMs int) error { | |||||
| 155 | func (s *Seat) Run(ctx context.Context) error { | 157 | func (s *Seat) Run(ctx context.Context) error { | ||
| 156 | for ctx.Err() == nil { | 158 | for ctx.Err() == nil { | ||
| 157 | fds := []unix.PollFd{{Fd: getFD(s.handle), Events: unix.POLLIN}} | 159 | fds := []unix.PollFd{{Fd: getFD(s.handle), Events: unix.POLLIN}} | ||
| 158 | - | n, err := unix.Poll(fds, 100) | 160 | + | if _, err := unix.Poll(fds, 100); err != nil && !errors.Is(err, unix.EINTR) { |
| 159 | - | if err != nil && !errors.Is(err, unix.EINTR) { | |||
| 160 | return fmt.Errorf("seat poll: %w", err) | 161 | return fmt.Errorf("seat poll: %w", err) | ||
| 161 | } | 162 | } | ||
| 162 | - | if n <= 0 { | 163 | + | // Dispatch every tick: libseat may queue events without the fd becoming readable. |
| 163 | - | continue | |||
| 164 | - | } | |||
| 165 | s.mu.Lock() | 164 | s.mu.Lock() | ||
| 166 | r := dispatch(s.handle, 0) | 165 | r := dispatch(s.handle, 0) | ||
| 167 | s.mu.Unlock() | 166 | s.mu.Unlock() | ||
M internal/app/app.go +14 -9
| @@ -57,6 +57,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 57 | 57 | } | |
| 58 | 58 | defer hw.close() | |
| 59 | 59 | width, height = hw.out.Width(), hw.out.Height() | |
| 60 | + | hw.inputActive, hw.outputActive = hw.seat.Subscribe(), hw.seat.Subscribe() | |
| 60 | 61 | } | |
| 61 | 62 | client := make(chan ports.ClientEvent, 32) | |
| 62 | 63 | input := make(chan ports.InputEvent, 32) | |
| @@ -108,7 +109,9 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 108 | 109 | go func() { | |
| 109 | 110 | defer workers.Done() | |
| 110 | 111 | if hw != nil { | |
| 111 | - | done <- libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Width: width, Height: height, Active: hw.seat.Subscribe(), Log: logging.For(ctx, "input")}, input) | |
| 112 | + | done <- safe("input", func() error { | |
| 113 | + | return libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Width: width, Height: height, Active: hw.inputActive, Log: logging.For(ctx, "input")}, input) | |
| 114 | + | }) | |
| 112 | 115 | return | |
| 113 | 116 | } | |
| 114 | 117 | _ = headlessinput.Run(ctx, km, script, input, logging.For(ctx, "input")) | |
| @@ -140,13 +143,15 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 140 | 143 | go func() { | |
| 141 | 144 | defer workers.Done() | |
| 142 | 145 | if hw != nil { | |
| 143 | - | done <- hw.out.Run(ctx, func(w, h int) (drm.Renderer, error) { | |
| 144 | - | r, err := vulkan.New(w, h) | |
| 145 | - | if err != nil { | |
| 146 | - | return nil, err | |
| 147 | - | } | |
| 148 | - | return r, nil | |
| 149 | - | }, hw.seat.Subscribe(), renderScenes, contents) | |
| 146 | + | done <- safe("output", func() error { | |
| 147 | + | return hw.out.Run(ctx, func(w, h int) (drm.Renderer, error) { | |
| 148 | + | r, err := vulkan.New(w, h) | |
| 149 | + | if err != nil { | |
| 150 | + | return nil, err | |
| 151 | + | } | |
| 152 | + | return r, nil | |
| 153 | + | }, hw.outputActive, renderScenes, contents) | |
| 154 | + | }) | |
| 150 | 155 | return | |
| 151 | 156 | } | |
| 152 | 157 | done <- headless.Run(ctx, headless.Options{Width: 1920, Height: 1080, ScreenshotDir: opts.ScreenshotDir, Log: logging.For(ctx, "render"), NewRenderer: func(w, h int) (headless.Renderer, error) { | |
| @@ -160,7 +165,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 160 | 165 | ||
| 161 | 166 | if hw != nil { | |
| 162 | 167 | workers.Add(1) | |
| 163 | - | go func() { defer workers.Done(); done <- hw.seat.Run(ctx) }() | |
| 168 | + | go func() { defer workers.Done(); done <- safe("seat", func() error { return hw.seat.Run(ctx) }) }() | |
| 164 | 169 | } | |
| 165 | 170 | var result error | |
| 166 | 171 | select { | |
| @@ -57,6 +57,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 57 | } | 57 | } | ||
| 58 | defer hw.close() | 58 | defer hw.close() | ||
| 59 | width, height = hw.out.Width(), hw.out.Height() | 59 | width, height = hw.out.Width(), hw.out.Height() | ||
| 60 | + | hw.inputActive, hw.outputActive = hw.seat.Subscribe(), hw.seat.Subscribe() | |||
| 60 | } | 61 | } | ||
| 61 | client := make(chan ports.ClientEvent, 32) | 62 | client := make(chan ports.ClientEvent, 32) | ||
| 62 | input := make(chan ports.InputEvent, 32) | 63 | input := make(chan ports.InputEvent, 32) | ||
| @@ -108,7 +109,9 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 108 | go func() { | 109 | go func() { | ||
| 109 | defer workers.Done() | 110 | defer workers.Done() | ||
| 110 | if hw != nil { | 111 | if hw != nil { | ||
| 111 | - | done <- libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Width: width, Height: height, Active: hw.seat.Subscribe(), Log: logging.For(ctx, "input")}, input) | 112 | + | done <- safe("input", func() error { |
| 113 | + | return libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Width: width, Height: height, Active: hw.inputActive, Log: logging.For(ctx, "input")}, input) | |||
| 114 | + | }) | |||
| 112 | return | 115 | return | ||
| 113 | } | 116 | } | ||
| 114 | _ = headlessinput.Run(ctx, km, script, input, logging.For(ctx, "input")) | 117 | _ = headlessinput.Run(ctx, km, script, input, logging.For(ctx, "input")) | ||
| @@ -140,13 +143,15 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 140 | go func() { | 143 | go func() { | ||
| 141 | defer workers.Done() | 144 | defer workers.Done() | ||
| 142 | if hw != nil { | 145 | if hw != nil { | ||
| 143 | - | done <- hw.out.Run(ctx, func(w, h int) (drm.Renderer, error) { | 146 | + | done <- safe("output", func() error { |
| 144 | - | r, err := vulkan.New(w, h) | 147 | + | return hw.out.Run(ctx, func(w, h int) (drm.Renderer, error) { |
| 145 | - | if err != nil { | 148 | + | r, err := vulkan.New(w, h) |
| 146 | - | return nil, err | 149 | + | if err != nil { |
| 147 | - | } | 150 | + | return nil, err |
| 148 | - | return r, nil | 151 | + | } |
| 149 | - | }, hw.seat.Subscribe(), renderScenes, contents) | 152 | + | return r, nil |
| 153 | + | }, hw.outputActive, renderScenes, contents) | |||
| 154 | + | }) | |||
| 150 | return | 155 | return | ||
| 151 | } | 156 | } | ||
| 152 | done <- headless.Run(ctx, headless.Options{Width: 1920, Height: 1080, ScreenshotDir: opts.ScreenshotDir, Log: logging.For(ctx, "render"), NewRenderer: func(w, h int) (headless.Renderer, error) { | 157 | done <- headless.Run(ctx, headless.Options{Width: 1920, Height: 1080, ScreenshotDir: opts.ScreenshotDir, Log: logging.For(ctx, "render"), NewRenderer: func(w, h int) (headless.Renderer, error) { | ||
| @@ -160,7 +165,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 160 | 165 | ||||
| 161 | if hw != nil { | 166 | if hw != nil { | ||
| 162 | workers.Add(1) | 167 | workers.Add(1) | ||
| 163 | - | go func() { defer workers.Done(); done <- hw.seat.Run(ctx) }() | 168 | + | go func() { defer workers.Done(); done <- safe("seat", func() error { return hw.seat.Run(ctx) }) }() |
| 164 | } | 169 | } | ||
| 165 | var result error | 170 | var result error | ||
| 166 | select { | 171 | select { | ||
M internal/app/drm.go +13 -0
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "errors" | |
| 6 | 6 | "fmt" | |
| 7 | 7 | "path/filepath" | |
| 8 | + | "runtime/debug" | |
| 8 | 9 | "sort" | |
| 9 | 10 | ||
| 10 | 11 | "github.com/bnema/nefertty/internal/adapters/drm" | |
| @@ -18,6 +19,18 @@ type drmBackend struct { | |||
| 18 | 19 | seat *seat.Seat | |
| 19 | 20 | out *drm.Output | |
| 20 | 21 | fd int | |
| 22 | + | ||
| 23 | + | inputActive, outputActive <-chan bool | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | // safe turns a panic in a hardware goroutine into an error, so the TTY is still restored. | |
| 27 | + | func safe(name string, fn func() error) (err error) { | |
| 28 | + | defer func() { | |
| 29 | + | if p := recover(); p != nil { | |
| 30 | + | err = fmt.Errorf("%s panic: %v\n%s", name, p, debug.Stack()) | |
| 31 | + | } | |
| 32 | + | }() | |
| 33 | + | return fn() | |
| 21 | 34 | } | |
| 22 | 35 | ||
| 23 | 36 | // openDRM opens the seat and the first card with a connected display. | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "errors" | 5 | "errors" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "path/filepath" | 7 | "path/filepath" | ||
| 8 | + | "runtime/debug" | |||
| 8 | "sort" | 9 | "sort" | ||
| 9 | 10 | ||||
| 10 | "github.com/bnema/nefertty/internal/adapters/drm" | 11 | "github.com/bnema/nefertty/internal/adapters/drm" | ||
| @@ -18,6 +19,18 @@ type drmBackend struct { | |||||
| 18 | seat *seat.Seat | 19 | seat *seat.Seat | ||
| 19 | out *drm.Output | 20 | out *drm.Output | ||
| 20 | fd int | 21 | fd int | ||
| 22 | + | ||||
| 23 | + | inputActive, outputActive <-chan bool | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | // safe turns a panic in a hardware goroutine into an error, so the TTY is still restored. | |||
| 27 | + | func safe(name string, fn func() error) (err error) { | |||
| 28 | + | defer func() { | |||
| 29 | + | if p := recover(); p != nil { | |||
| 30 | + | err = fmt.Errorf("%s panic: %v\n%s", name, p, debug.Stack()) | |||
| 31 | + | } | |||
| 32 | + | }() | |||
| 33 | + | return fn() | |||
| 21 | } | 34 | } | ||
| 22 | 35 | ||||
| 23 | // openDRM opens the seat and the first card with a connected display. | 36 | // openDRM opens the seat and the first card with a connected display. | ||